aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands/config_helpers.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 12:53:07 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 12:53:07 +0900
commitf8358b7c94e52da868a223832f1ccc417b4beedf (patch)
tree80f59a0fe7c8ded1b043dddcf85b4c60458d7c4e /crates/mozart/src/commands/config_helpers.rs
parentf2386320d1934f7e52b4cda36d19c86c239423b0 (diff)
downloadphp-mozart-f8358b7c94e52da868a223832f1ccc417b4beedf.tar.gz
php-mozart-f8358b7c94e52da868a223832f1ccc417b4beedf.tar.zst
php-mozart-f8358b7c94e52da868a223832f1ccc417b4beedf.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands/config_helpers.rs')
-rw-r--r--crates/mozart/src/commands/config_helpers.rs158
1 files changed, 158 insertions, 0 deletions
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<PathBuf> {
+ 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<serde_json::Value> {
+ 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::<Vec<_>>().join(", ")
+ }
+ serde_json::Value::Object(obj) => {
+ if obj.is_empty() {
+ "{}".to_string()
+ } else {
+ serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string())
+ }
+ }
+ }
+}