aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart/src/commands.rs1
-rw-r--r--crates/mozart/src/commands/config.rs130
-rw-r--r--crates/mozart/src/commands/config_helpers.rs158
-rw-r--r--crates/mozart/src/commands/repository.rs891
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());
+ }
}