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(_) => { serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string()) } serde_json::Value::Object(obj) => { if obj.is_empty() { "{}".to_string() } else { serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()) } } } }