diff options
Diffstat (limited to 'crates/mozart/src')
| -rw-r--r-- | crates/mozart/src/commands.rs | 1 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config.rs | 130 | ||||
| -rw-r--r-- | crates/mozart/src/commands/config_helpers.rs | 158 | ||||
| -rw-r--r-- | crates/mozart/src/commands/repository.rs | 891 |
4 files changed, 1052 insertions, 128 deletions
diff --git a/crates/mozart/src/commands.rs b/crates/mozart/src/commands.rs index cf3300f..968763f 100644 --- a/crates/mozart/src/commands.rs +++ b/crates/mozart/src/commands.rs @@ -7,6 +7,7 @@ pub mod check_platform_reqs; pub mod clear_cache; pub mod completion; pub mod config; +pub mod config_helpers; pub mod create_project; pub mod dependency; pub mod depends; diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 2b7d7ba..d6a5278 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -3,6 +3,11 @@ use clap::Args; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use super::config_helpers::{ + add_repository, composer_home, read_json_file, remove_repository, render_value, working_dir, + write_json_file, +}; + #[derive(Args)] pub struct ConfigArgs { /// Setting key @@ -381,49 +386,6 @@ fn match_repository_key(key: &str) -> Option<&str> { None } -/// 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. -fn add_repository( - json: &mut serde_json::Value, - name: &str, - config: serde_json::Value, - append: bool, -) { - // Ensure repositories is an array - if !json["repositories"].is_array() { - json["repositories"] = serde_json::json!([]); - } - - // Remove any existing entry with the same name - 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. -fn remove_repository(json: &mut serde_json::Value, name: &str) { - if let Some(repos) = json["repositories"].as_array_mut() { - repos.retain(|entry| { - // Match by name field or {name: false} disabled entry - if let Some(entry_name) = entry.get("name").and_then(|n| n.as_str()) { - entry_name != name - } else { - // Check for disabled repo: {"packagist.org": false} - let disabled_key_matches = entry - .as_object() - .map(|obj| obj.contains_key(name)) - .unwrap_or(false); - !disabled_key_matches - } - }); - } -} // ─── JSON path helpers ──────────────────────────────────────────────────────── @@ -490,64 +452,9 @@ fn resolve_config_file_path(args: &ConfigArgs, cli: &super::Cli) -> anyhow::Resu Ok(working_dir(cli)?.join("composer.json")) } -/// Read a JSON file as `serde_json::Value`. -/// If the file does not exist, return a default skeleton: -/// `{"config": {}}` for global files, `{}` for local. -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. -fn write_json_file(path: &Path, value: &serde_json::Value) -> anyhow::Result<()> { - // Create parent directories if needed - 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(()) -} // ─── Helpers ────────────────────────────────────────────────────────────────── -/// Return the Composer home directory, respecting `COMPOSER_HOME` and -/// falling back to the platform default (`~/.config/composer` on Unix, -/// `%APPDATA%/Composer` on Windows). -fn composer_home() -> String { - if let Ok(home) = std::env::var("COMPOSER_HOME") { - return home; - } - - // Platform-specific defaults - #[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"))] - { - // Prefer XDG_CONFIG_HOME if set, otherwise fall back to ~/.config/composer - 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()) - } - } -} /// Load the `config` section from a JSON file (global `config.json` or local /// `composer.json`). Returns an empty map when the file is absent or has no @@ -570,36 +477,9 @@ fn load_config_section( } } -/// Build the working directory path, preferring `--working-dir` over `cwd`. -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()?), - } -} // ─── Value rendering ───────────────────────────────────────────────────────── -/// Render a `serde_json::Value` as a human-readable string suitable for -/// single-line display (matching Composer's behaviour). -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()) - } - } - } -} // ─── execute() ─────────────────────────────────────────────────────────────── 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()) + } + } + } +} diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index e5d43ec..ca2eaf8 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -1,4 +1,11 @@ +use anyhow::anyhow; use clap::Args; +use std::path::PathBuf; + +use super::config_helpers::{ + add_repository, composer_home, insert_repository, read_json_file, remove_repository, + render_value, working_dir, write_json_file, +}; #[derive(Args)] pub struct RepositoryArgs { @@ -35,10 +42,888 @@ pub struct RepositoryArgs { pub after: Option<String>, } +fn resolve_file_path(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<PathBuf> { + if args.global && args.file.is_some() { + anyhow::bail!("Cannot combine --global and --file"); + } + if args.global { + return Ok(PathBuf::from(composer_home()).join("config.json")); + } + if let Some(ref file) = args.file { + return Ok(PathBuf::from(file)); + } + Ok(working_dir(cli)?.join("composer.json")) +} + pub async fn execute( - _args: &RepositoryArgs, - _cli: &super::Cli, + args: &RepositoryArgs, + cli: &super::Cli, _console: &mozart_core::console::Console, ) -> anyhow::Result<()> { - todo!() + let action = args + .action + .as_deref() + .unwrap_or("list"); + + match action { + "list" | "ls" | "show" => execute_list(args, cli), + "add" => execute_add(args, cli), + "remove" | "rm" | "delete" => execute_remove(args, cli), + "set-url" | "seturl" => execute_set_url(args, cli), + "get-url" | "geturl" => execute_get_url(args, cli), + "disable" => execute_disable(args, cli), + "enable" => execute_enable(args, cli), + _ => Err(anyhow!( + "Unknown action \"{action}\". Expected one of: list, add, remove, set-url, get-url, enable, disable" + )), + } +} + +// ─── list ───────────────────────────────────────────────────────────────────── + +fn execute_list(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let file_path = resolve_file_path(args, cli)?; + let json = read_json_file(&file_path, args.global)?; + + let mut has_packagist_disable = false; + + if let Some(repos) = json["repositories"].as_array() { + for entry in repos { + if let Some(obj) = entry.as_object() { + // Check for disabled repo entry like {"packagist.org": false} + if let Some((key, _)) = obj.iter().find(|(_, v)| v == &&serde_json::json!(false)) { + println!("[{key}] disabled"); + if key == "packagist.org" { + has_packagist_disable = true; + } + continue; + } + } + + let name = entry + .get("name") + .and_then(|n| n.as_str()) + .unwrap_or("unnamed"); + let repo_type = entry + .get("type") + .and_then(|t| t.as_str()) + .unwrap_or("unknown"); + let url = entry + .get("url") + .and_then(|u| u.as_str()) + .unwrap_or(""); + + println!("[{name}] {repo_type} {url}"); + } + } + + if !has_packagist_disable { + println!("[packagist.org] composer https://repo.packagist.org"); + } + + Ok(()) +} + +// ─── add ────────────────────────────────────────────────────────────────────── + +fn execute_add(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Repository name is required for \"add\""))?; + + if args.before.is_some() && args.after.is_some() { + anyhow::bail!("Cannot combine --before and --after"); + } + + let entry = match (&args.arg1, &args.arg2) { + (Some(type_or_json), Some(url)) => { + // type + url + serde_json::json!({ + "name": name, + "type": type_or_json, + "url": url, + }) + } + (Some(json_str), None) => { + // Try to parse as JSON + match serde_json::from_str::<serde_json::Value>(json_str) { + Ok(mut parsed) => { + // Inject the name if not already present + if let Some(obj) = parsed.as_object_mut() + && !obj.contains_key("name") + { + obj.insert("name".to_string(), serde_json::json!(name)); + } + parsed + } + Err(_) => { + anyhow::bail!( + "Invalid JSON for repository config. Expected: <type> <url> or a JSON string" + ); + } + } + } + _ => { + anyhow::bail!( + "Missing arguments for \"add\". Expected: <name> <type> <url> or <name> <json>" + ); + } + }; + + let file_path = resolve_file_path(args, cli)?; + let mut json = read_json_file(&file_path, args.global)?; + + if let Some(ref target) = args.before { + insert_repository(&mut json, name, entry, target, true)?; + } else if let Some(ref target) = args.after { + insert_repository(&mut json, name, entry, target, false)?; + } else { + add_repository(&mut json, name, entry, args.append); + } + + write_json_file(&file_path, &json)?; + Ok(()) +} + +// ─── remove ─────────────────────────────────────────────────────────────────── + +fn execute_remove(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Repository name is required for \"remove\""))?; + + let file_path = resolve_file_path(args, cli)?; + let mut json = read_json_file(&file_path, args.global)?; + + if name == "packagist.org" || name == "packagist" { + // Removing packagist.org means disabling it + remove_repository(&mut json, "packagist.org"); + let disable_entry = serde_json::json!({"packagist.org": false}); + add_repository(&mut json, "packagist.org", disable_entry, args.append); + } else { + remove_repository(&mut json, name); + } + + // Clean up empty repositories array + if json["repositories"] + .as_array() + .map(|a| a.is_empty()) + .unwrap_or(false) + && let Some(obj) = json.as_object_mut() + { + obj.remove("repositories"); + } + + write_json_file(&file_path, &json)?; + Ok(()) +} + +// ─── set-url ────────────────────────────────────────────────────────────────── + +fn execute_set_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Repository name is required for \"set-url\""))?; + let new_url = args + .arg1 + .as_deref() + .ok_or_else(|| anyhow!("New URL is required for \"set-url\""))?; + + let file_path = resolve_file_path(args, cli)?; + let mut json = read_json_file(&file_path, args.global)?; + + let found = json["repositories"] + .as_array_mut() + .and_then(|repos| { + repos.iter_mut().find(|entry| { + entry.get("name").and_then(|n| n.as_str()) == Some(name) + }) + }); + + match found { + Some(entry) => { + entry["url"] = serde_json::json!(new_url); + write_json_file(&file_path, &json)?; + Ok(()) + } + None => Err(anyhow!("Repository \"{name}\" not found")), + } +} + +// ─── get-url ────────────────────────────────────────────────────────────────── + +fn execute_get_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .ok_or_else(|| anyhow!("Repository name is required for \"get-url\""))?; + + let file_path = resolve_file_path(args, cli)?; + let json = read_json_file(&file_path, args.global)?; + + let found = json["repositories"] + .as_array() + .and_then(|repos| { + repos.iter().find(|entry| { + entry.get("name").and_then(|n| n.as_str()) == Some(name) + }) + }); + + match found { + Some(entry) => { + let url = entry + .get("url") + .map(render_value) + .unwrap_or_default(); + println!("{url}"); + Ok(()) + } + None => Err(anyhow!("Repository \"{name}\" not found")), + } +} + +// ─── disable ────────────────────────────────────────────────────────────────── + +fn execute_disable(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .unwrap_or("packagist.org"); + + if name != "packagist.org" && name != "packagist" { + anyhow::bail!("Only \"packagist.org\" can be disabled with this action"); + } + + let file_path = resolve_file_path(args, cli)?; + let mut json = read_json_file(&file_path, args.global)?; + + // Remove any existing packagist.org disable entry first + remove_repository(&mut json, "packagist.org"); + + let disable_entry = serde_json::json!({"packagist.org": false}); + add_repository(&mut json, "packagist.org", disable_entry, args.append); + + write_json_file(&file_path, &json)?; + Ok(()) +} + +// ─── enable ─────────────────────────────────────────────────────────────────── + +fn execute_enable(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { + let name = args + .name + .as_deref() + .unwrap_or("packagist.org"); + + if name != "packagist.org" && name != "packagist" { + anyhow::bail!("Only \"packagist.org\" can be enabled with this action"); + } + + let file_path = resolve_file_path(args, cli)?; + let mut json = read_json_file(&file_path, args.global)?; + + remove_repository(&mut json, "packagist.org"); + + // Clean up empty repositories array + if json["repositories"] + .as_array() + .map(|a| a.is_empty()) + .unwrap_or(false) + && let Some(obj) = json.as_object_mut() + { + obj.remove("repositories"); + } + + write_json_file(&file_path, &json)?; + Ok(()) +} + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn make_args(action: Option<&str>, name: Option<&str>, arg1: Option<&str>, arg2: Option<&str>) -> RepositoryArgs { + RepositoryArgs { + action: action.map(|s| s.to_string()), + name: name.map(|s| s.to_string()), + arg1: arg1.map(|s| s.to_string()), + arg2: arg2.map(|s| s.to_string()), + global: false, + file: None, + append: false, + before: None, + after: None, + } + } + + fn make_cli() -> super::super::Cli { + use clap::Parser; + super::super::Cli::parse_from(["mozart", "repository", "list"]) + } + + // ── list ──────────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_list_empty() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("list"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + // Should succeed and print packagist.org + let result = execute(&args, &cli, &console).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_list_with_repos() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("list"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_list_with_disabled_packagist() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"packagist.org": false}]}"#, + ).unwrap(); + + let mut args = make_args(Some("list"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_ok()); + } + + // ── add ───────────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_add_type_url() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("add"), Some("my-repo"), Some("vcs"), Some("https://example.com")); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos.len(), 1); + assert_eq!(repos[0]["name"], "my-repo"); + assert_eq!(repos[0]["type"], "vcs"); + assert_eq!(repos[0]["url"], "https://example.com"); + } + + #[tokio::test] + async fn test_add_json() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args( + Some("add"), + Some("my-repo"), + Some(r#"{"type":"path","url":"../local-pkg"}"#), + None, + ); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["type"], "path"); + assert_eq!(repos[0]["name"], "my-repo"); + } + + #[tokio::test] + async fn test_add_prepend_default() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("add"), Some("new-repo"), Some("vcs"), Some("https://new.com")); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "new-repo"); + assert_eq!(repos[1]["name"], "existing"); + } + + #[tokio::test] + async fn test_add_append() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("add"), Some("new-repo"), Some("vcs"), Some("https://new.com")); + args.file = Some(file.to_str().unwrap().to_string()); + args.append = true; + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "existing"); + assert_eq!(repos[1]["name"], "new-repo"); + } + + #[tokio::test] + async fn test_add_before() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "a", "type": "vcs", "url": "https://a.com"}, {"name": "b", "type": "vcs", "url": "https://b.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("add"), Some("new"), Some("vcs"), Some("https://new.com")); + args.file = Some(file.to_str().unwrap().to_string()); + args.before = Some("b".to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "a"); + assert_eq!(repos[1]["name"], "new"); + assert_eq!(repos[2]["name"], "b"); + } + + #[tokio::test] + async fn test_add_after() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "a", "type": "vcs", "url": "https://a.com"}, {"name": "b", "type": "vcs", "url": "https://b.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("add"), Some("new"), Some("vcs"), Some("https://new.com")); + args.file = Some(file.to_str().unwrap().to_string()); + args.after = Some("a".to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "a"); + assert_eq!(repos[1]["name"], "new"); + assert_eq!(repos[2]["name"], "b"); + } + + #[tokio::test] + async fn test_add_before_and_after_error() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("add"), Some("new"), Some("vcs"), Some("https://new.com")); + args.file = Some(file.to_str().unwrap().to_string()); + args.before = Some("a".to_string()); + args.after = Some("b".to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_add_missing_args() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("add"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_add_missing_name() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("add"), None, Some("vcs"), Some("https://url.com")); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── remove ────────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_remove() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("remove"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert!(json.get("repositories").is_none()); + } + + #[tokio::test] + async fn test_remove_packagist_disables() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("remove"), Some("packagist.org"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["packagist.org"], false); + } + + #[tokio::test] + async fn test_remove_alias_rm() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("rm"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert!(json.get("repositories").is_none()); + } + + #[tokio::test] + async fn test_remove_missing_name() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("remove"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── set-url ───────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_set_url() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://old.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("set-url"), Some("my-repo"), Some("https://new.com"), None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert_eq!(json["repositories"][0]["url"], "https://new.com"); + } + + #[tokio::test] + async fn test_set_url_not_found() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); + + let mut args = make_args(Some("set-url"), Some("missing"), Some("https://new.com"), None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_set_url_alias() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://old.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("seturl"), Some("my-repo"), Some("https://new.com"), None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert_eq!(json["repositories"][0]["url"], "https://new.com"); + } + + // ── get-url ───────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_get_url() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, + ).unwrap(); + + let mut args = make_args(Some("get-url"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_get_url_not_found() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); + + let mut args = make_args(Some("get-url"), Some("missing"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── disable ───────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_disable_packagist() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("disable"), Some("packagist.org"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["packagist.org"], false); + } + + #[tokio::test] + async fn test_disable_without_name_defaults_to_packagist() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("disable"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["packagist.org"], false); + } + + #[tokio::test] + async fn test_disable_non_packagist_error() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("disable"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── enable ────────────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_enable_packagist() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write( + &file, + r#"{"repositories": [{"packagist.org": false}]}"#, + ).unwrap(); + + let mut args = make_args(Some("enable"), Some("packagist.org"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + execute(&args, &cli, &console).await.unwrap(); + + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); + assert!(json.get("repositories").is_none()); + } + + #[tokio::test] + async fn test_enable_non_packagist_error() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("enable"), Some("my-repo"), None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── unknown action ────────────────────────────────────────────────────── + + #[tokio::test] + async fn test_unknown_action() { + let dir = tempfile::TempDir::new().unwrap(); + let file = dir.path().join("composer.json"); + std::fs::write(&file, "{}").unwrap(); + + let mut args = make_args(Some("invalid"), None, None, None); + args.file = Some(file.to_str().unwrap().to_string()); + + let cli = make_cli(); + let console = mozart_core::console::Console::new(0, false, false, false, false); + let result = execute(&args, &cli, &console).await; + assert!(result.is_err()); + } + + // ── insert_repository helper ──────────────────────────────────────────── + + #[test] + fn test_insert_before() { + let mut json = serde_json::json!({ + "repositories": [ + {"name": "a", "type": "vcs", "url": "https://a.com"}, + {"name": "b", "type": "vcs", "url": "https://b.com"}, + ] + }); + + let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); + insert_repository(&mut json, "new", entry, "b", true).unwrap(); + + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "a"); + assert_eq!(repos[1]["name"], "new"); + assert_eq!(repos[2]["name"], "b"); + } + + #[test] + fn test_insert_after() { + let mut json = serde_json::json!({ + "repositories": [ + {"name": "a", "type": "vcs", "url": "https://a.com"}, + {"name": "b", "type": "vcs", "url": "https://b.com"}, + ] + }); + + let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); + insert_repository(&mut json, "new", entry, "a", false).unwrap(); + + let repos = json["repositories"].as_array().unwrap(); + assert_eq!(repos[0]["name"], "a"); + assert_eq!(repos[1]["name"], "new"); + assert_eq!(repos[2]["name"], "b"); + } + + #[test] + fn test_insert_target_not_found() { + let mut json = serde_json::json!({"repositories": []}); + let entry = serde_json::json!({"name": "new", "type": "vcs", "url": "https://new.com"}); + let result = insert_repository(&mut json, "new", entry, "nonexistent", true); + assert!(result.is_err()); + } } |
