From f8358b7c94e52da868a223832f1ccc417b4beedf Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 22 Feb 2026 12:53:07 +0900 Subject: feat(repository): implement repository command with all actions Extract shared helpers (composer_home, read/write_json_file, add/remove_repository, render_value) from config.rs into config_helpers.rs module. Implement all 7 repository actions: list, add, remove, set-url, get-url, disable, enable with --append/--before/--after positioning support. Co-Authored-By: Claude Opus 4.6 --- crates/mozart/src/commands/config_helpers.rs | 158 +++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 crates/mozart/src/commands/config_helpers.rs (limited to 'crates/mozart/src/commands/config_helpers.rs') diff --git a/crates/mozart/src/commands/config_helpers.rs b/crates/mozart/src/commands/config_helpers.rs new file mode 100644 index 0000000..953f18b --- /dev/null +++ b/crates/mozart/src/commands/config_helpers.rs @@ -0,0 +1,158 @@ +use anyhow::anyhow; +use std::path::{Path, PathBuf}; + +/// Return the Composer home directory, respecting `COMPOSER_HOME` and +/// falling back to the platform default (`~/.config/composer` on Unix, +/// `%APPDATA%/Composer` on Windows). +pub(crate) fn composer_home() -> String { + if let Ok(home) = std::env::var("COMPOSER_HOME") { + return home; + } + + #[cfg(target_os = "windows")] + { + std::env::var("APPDATA") + .map(|p| format!("{p}/Composer")) + .unwrap_or_else(|_| "C:/ProgramData/ComposerSetup/bin".to_string()) + } + + #[cfg(not(target_os = "windows"))] + { + if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + format!("{xdg}/composer") + } else { + std::env::var("HOME") + .map(|h| format!("{h}/.config/composer")) + .unwrap_or_else(|_| "/tmp/composer".to_string()) + } + } +} + +/// Build the working directory path, preferring `--working-dir` over `cwd`. +pub(crate) fn working_dir(cli: &super::Cli) -> anyhow::Result { + match &cli.working_dir { + Some(d) => Ok(PathBuf::from(d)), + None => Ok(std::env::current_dir()?), + } +} + +/// Read a JSON file as `serde_json::Value`. +/// If the file does not exist, return a default skeleton: +/// `{"config": {}}` for global files, `{}` for local. +pub(crate) fn read_json_file(path: &Path, is_global: bool) -> anyhow::Result { + if !path.exists() { + if is_global { + return Ok(serde_json::json!({"config": {}})); + } + return Ok(serde_json::json!({})); + } + let content = std::fs::read_to_string(path)?; + let value: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| anyhow!("Failed to parse JSON from {}: {}", path.display(), e))?; + Ok(value) +} + +/// Write a `serde_json::Value` back to a file with 4-space indentation + trailing newline. +pub(crate) fn write_json_file(path: &Path, value: &serde_json::Value) -> anyhow::Result<()> { + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + std::fs::create_dir_all(parent)?; + } + mozart_core::package::write_to_file(value, path)?; + Ok(()) +} + +/// Add a repository entry to the `repositories` array in json. +/// If `append` is true, push to end; otherwise insert at beginning. +/// Removes any existing entry with the same name first. +pub(crate) fn add_repository( + json: &mut serde_json::Value, + name: &str, + config: serde_json::Value, + append: bool, +) { + if !json["repositories"].is_array() { + json["repositories"] = serde_json::json!([]); + } + + remove_repository(json, name); + + let repos = json["repositories"].as_array_mut().unwrap(); + if append { + repos.push(config); + } else { + repos.insert(0, config); + } +} + +/// Remove a repository entry by name from the `repositories` array. +pub(crate) fn remove_repository(json: &mut serde_json::Value, name: &str) { + if let Some(repos) = json["repositories"].as_array_mut() { + repos.retain(|entry| { + if let Some(entry_name) = entry.get("name").and_then(|n| n.as_str()) { + entry_name != name + } else { + let disabled_key_matches = entry + .as_object() + .map(|obj| obj.contains_key(name)) + .unwrap_or(false); + !disabled_key_matches + } + }); + } +} + +/// Insert a repository entry before or after a named repository. +/// Returns an error if the target repository is not found. +pub(crate) fn insert_repository( + json: &mut serde_json::Value, + name: &str, + config: serde_json::Value, + target: &str, + before: bool, +) -> anyhow::Result<()> { + if !json["repositories"].is_array() { + json["repositories"] = serde_json::json!([]); + } + + remove_repository(json, name); + + let repos = json["repositories"].as_array_mut().unwrap(); + + let pos = repos + .iter() + .position(|entry| { + entry.get("name").and_then(|n| n.as_str()) == Some(target) + || entry + .as_object() + .map(|obj| obj.contains_key(target)) + .unwrap_or(false) + }) + .ok_or_else(|| anyhow!("Repository \"{target}\" not found"))?; + + let insert_pos = if before { pos } else { pos + 1 }; + repos.insert(insert_pos, config); + Ok(()) +} + +/// Render a `serde_json::Value` as a human-readable string suitable for +/// single-line display (matching Composer's behaviour). +pub(crate) fn render_value(v: &serde_json::Value) -> String { + match v { + serde_json::Value::Null => "NULL".to_string(), + serde_json::Value::Bool(b) => if *b { "true" } else { "false" }.to_string(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Array(arr) => { + arr.iter().map(render_value).collect::>().join(", ") + } + serde_json::Value::Object(obj) => { + if obj.is_empty() { + "{}".to_string() + } else { + serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()) + } + } + } +} -- cgit v1.3.1