aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/commands
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 23:38:32 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 23:38:32 +0900
commit52310761f67220c9c075cd847205825a720035ee (patch)
tree0528fc94aea7853e41313e19964d74a958dae9c9 /crates/mozart/src/commands
parent92da9e37c68beb180e45e550fba5acd7d28dca27 (diff)
downloadphp-mozart-52310761f67220c9c075cd847205825a720035ee.tar.gz
php-mozart-52310761f67220c9c075cd847205825a720035ee.tar.zst
php-mozart-52310761f67220c9c075cd847205825a720035ee.zip
feat(console): add structured error handling, verbosity, and suggestions
Implement Phase 7.2 error handling & UX infrastructure: - Add exit_code module with MozartError, bail()/bail_silent() helpers, and Composer-compatible exit code constants (0-5, 100) - Redesign Console struct with Verbosity enum (Quiet/Normal/Verbose/ VeryVerbose/Debug), ANSI auto-detection via IsTerminal, and verbosity-gated output methods (info/verbose/debug/error) - Thread Console through all 33 command execute() signatures - Replace all std::process::exit() calls with structured MozartError returns handled in main() - Migrate eprintln\! status messages to console.info() for quiet-mode suppression - Add suggest module with Levenshtein distance and "Did you mean?" formatting for future package name suggestions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/commands')
-rw-r--r--crates/mozart/src/commands/about.rs6
-rw-r--r--crates/mozart/src/commands/archive.rs11
-rw-r--r--crates/mozart/src/commands/audit.rs6
-rw-r--r--crates/mozart/src/commands/browse.rs15
-rw-r--r--crates/mozart/src/commands/bump.rs76
-rw-r--r--crates/mozart/src/commands/check_platform_reqs.rs6
-rw-r--r--crates/mozart/src/commands/clear_cache.rs14
-rw-r--r--crates/mozart/src/commands/config.rs6
-rw-r--r--crates/mozart/src/commands/create_project.rs71
-rw-r--r--crates/mozart/src/commands/depends.rs6
-rw-r--r--crates/mozart/src/commands/diagnose.rs6
-rw-r--r--crates/mozart/src/commands/dump_autoload.rs10
-rw-r--r--crates/mozart/src/commands/exec.rs6
-rw-r--r--crates/mozart/src/commands/fund.rs6
-rw-r--r--crates/mozart/src/commands/global.rs10
-rw-r--r--crates/mozart/src/commands/init.rs10
-rw-r--r--crates/mozart/src/commands/install.rs73
-rw-r--r--crates/mozart/src/commands/licenses.rs6
-rw-r--r--crates/mozart/src/commands/outdated.rs6
-rw-r--r--crates/mozart/src/commands/prohibits.rs6
-rw-r--r--crates/mozart/src/commands/reinstall.rs24
-rw-r--r--crates/mozart/src/commands/remove.rs83
-rw-r--r--crates/mozart/src/commands/repository.rs6
-rw-r--r--crates/mozart/src/commands/require.rs110
-rw-r--r--crates/mozart/src/commands/run_script.rs6
-rw-r--r--crates/mozart/src/commands/search.rs6
-rw-r--r--crates/mozart/src/commands/self_update.rs6
-rw-r--r--crates/mozart/src/commands/show.rs6
-rw-r--r--crates/mozart/src/commands/status.rs6
-rw-r--r--crates/mozart/src/commands/suggests.rs6
-rw-r--r--crates/mozart/src/commands/update.rs213
-rw-r--r--crates/mozart/src/commands/validate.rs44
32 files changed, 508 insertions, 364 deletions
diff --git a/crates/mozart/src/commands/about.rs b/crates/mozart/src/commands/about.rs
index 4b12c08..d60aecf 100644
--- a/crates/mozart/src/commands/about.rs
+++ b/crates/mozart/src/commands/about.rs
@@ -4,7 +4,11 @@ use clap::Args;
#[derive(Args)]
pub struct AboutArgs {}
-pub fn execute(_args: &AboutArgs, _cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ _args: &AboutArgs,
+ _cli: &super::Cli,
+ _console: &console::Console,
+) -> anyhow::Result<()> {
let version = env!("CARGO_PKG_VERSION");
println!(
"{}",
diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs
index 93a7558..9be45e9 100644
--- a/crates/mozart/src/commands/archive.rs
+++ b/crates/mozart/src/commands/archive.rs
@@ -82,7 +82,11 @@ impl Drop for PackageMeta {
// ─── Main entry point ─────────────────────────────────────────────────────────
-pub fn execute(args: &ArchiveArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ArchiveArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
use crate::archiver::{
ArchiveFormat, collect_archivable_files, create_archive, generate_archive_filename,
parse_composer_excludes, parse_gitattributes, parse_gitignore_pattern,
@@ -216,7 +220,10 @@ pub fn execute(args: &ArchiveArgs, cli: &super::Cli) -> anyhow::Result<()> {
// 9. Create archive
let target_path = output_dir.join(format!("{}.{}", filename_base, format.extension()));
- eprintln!("Creating the archive into \"{}\".", output_dir.display());
+ console.info(&format!(
+ "Creating the archive into \"{}\".",
+ output_dir.display()
+ ));
create_archive(&meta.source_dir, &files, &target_path, &format)?;
// Print relative path if possible
diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs
index 3791157..3e69bb3 100644
--- a/crates/mozart/src/commands/audit.rs
+++ b/crates/mozart/src/commands/audit.rs
@@ -70,7 +70,11 @@ struct AuditResult {
// ─── Main entry point ─────────────────────────────────────────────────────────
-pub fn execute(args: &AuditArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &AuditArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Validate format
let format = args.format.as_str();
if format != "table" && format != "plain" && format != "json" && format != "summary" {
diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs
index d17c6d0..0a89ae7 100644
--- a/crates/mozart/src/commands/browse.rs
+++ b/crates/mozart/src/commands/browse.rs
@@ -18,7 +18,11 @@ pub struct BrowseArgs {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &BrowseArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &BrowseArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
@@ -46,21 +50,18 @@ pub fn execute(args: &BrowseArgs, cli: &super::Cli) -> anyhow::Result<()> {
if args.show {
println!("{}", url);
} else {
- println!(
- "{}",
- crate::console::info(&format!("Opening {} in browser.", url))
- );
+ console.info(&format!("Opening {} in browser.", url));
open_browser(&url)?;
}
}
None => {
- eprintln!(
+ console.info(&format!(
"{}",
crate::console::warning(&format!(
"No URL found for package \"{}\".",
package_name
))
- );
+ ));
exit_code = 1;
}
}
diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs
index b27eec6..4c37dd6 100644
--- a/crates/mozart/src/commands/bump.rs
+++ b/crates/mozart/src/commands/bump.rs
@@ -22,7 +22,11 @@ pub struct BumpArgs {
// ─── Main entry point ─────────────────────────────────────────────────────────
-pub fn execute(args: &BumpArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &BumpArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
@@ -46,13 +50,13 @@ pub fn execute(args: &BumpArgs, cli: &super::Cli) -> anyhow::Result<()> {
if let Some(ref pkg_type) = root.package_type
&& pkg_type != "project"
{
- eprintln!(
+ console.info(&format!(
"{}",
crate::console::warning(&format!(
"Warning: Bumping constraints for a non-project package (type=\"{pkg_type}\"). \
Libraries should not pin their dependencies."
))
- );
+ ));
}
// Check lock file existence
@@ -65,14 +69,11 @@ pub fn execute(args: &BumpArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Check lock file freshness
if !lock.is_fresh(&composer_json_content) {
- eprintln!(
- "{}",
- crate::console::error(
- "composer.lock is not up to date with composer.json. \
- Run `mozart install` or `mozart update` to refresh it."
- )
- );
- std::process::exit(2);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::LOCK_FILE_INVALID,
+ "composer.lock is not up to date with composer.json. \
+ Run `mozart install` or `mozart update` to refresh it.",
+ ));
}
// Build map: package name (lowercase) → (pretty_version, version_normalized)
@@ -343,7 +344,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
let updated = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&updated).unwrap();
@@ -374,7 +380,12 @@ mod tests {
dry_run: true,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
// composer.json should be unchanged
let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
@@ -406,7 +417,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
// No changes should be made
let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
@@ -444,7 +460,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
@@ -484,7 +505,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
@@ -521,8 +547,8 @@ mod tests {
};
let _cli = make_cli(dir.path());
- // The execute function calls std::process::exit(2) for stale lock.
- // We can't test that directly, but we can verify the lock IS stale
+ // The execute function returns Err(MozartError) with LOCK_FILE_INVALID for stale lock.
+ // We verify the lock IS stale here as a prerequisite check.
let lock_loaded = LockFile::read_from_file(&dir.path().join("composer.lock")).unwrap();
assert!(!lock_loaded.is_fresh(composer_json));
}
@@ -563,7 +589,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
// The lock file content-hash should now match the updated composer.json
let updated_composer = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
@@ -605,7 +636,12 @@ mod tests {
dry_run: false,
};
let cli = make_cli(dir.path());
- execute(&args, &cli).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ execute(&args, &cli, &console).unwrap();
let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
diff --git a/crates/mozart/src/commands/check_platform_reqs.rs b/crates/mozart/src/commands/check_platform_reqs.rs
index 358df4a..ad7b860 100644
--- a/crates/mozart/src/commands/check_platform_reqs.rs
+++ b/crates/mozart/src/commands/check_platform_reqs.rs
@@ -52,7 +52,11 @@ struct CheckResult {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &CheckPlatformReqsArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &CheckPlatformReqsArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/clear_cache.rs b/crates/mozart/src/commands/clear_cache.rs
index 819ca9f..59baff3 100644
--- a/crates/mozart/src/commands/clear_cache.rs
+++ b/crates/mozart/src/commands/clear_cache.rs
@@ -8,7 +8,11 @@ pub struct ClearCacheArgs {
pub gc: bool,
}
-pub fn execute(args: &ClearCacheArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ClearCacheArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
let config = build_cache_config(cli);
if args.gc {
@@ -19,8 +23,8 @@ pub fn execute(args: &ClearCacheArgs, cli: &super::Cli) -> anyhow::Result<()> {
repo_cache.gc(config.cache_ttl, u64::MAX)?;
files_cache.gc(config.cache_files_ttl, config.cache_files_maxsize)?;
- eprintln!("Cache garbage collection complete.");
- eprintln!("Cache directory: {}", config.cache_dir.display());
+ console.info("Cache garbage collection complete.");
+ console.info(&format!("Cache directory: {}", config.cache_dir.display()));
} else {
// Full clear of all cache directories
let repo_cache = Cache::repo(&config);
@@ -44,8 +48,8 @@ pub fn execute(args: &ClearCacheArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
}
- eprintln!("Cache cleared.");
- eprintln!("Cache directory: {}", config.cache_dir.display());
+ console.info("Cache cleared.");
+ console.info(&format!("Cache directory: {}", config.cache_dir.display()));
}
Ok(())
diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs
index 552dd2f..e875a92 100644
--- a/crates/mozart/src/commands/config.rs
+++ b/crates/mozart/src/commands/config.rs
@@ -603,7 +603,11 @@ fn render_value(v: &serde_json::Value) -> String {
// ─── execute() ───────────────────────────────────────────────────────────────
-pub fn execute(args: &ConfigArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ConfigArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
// 1. Handle --editor mode
if args.editor {
return execute_editor(args, cli);
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index fabf7b4..654eb0e 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -170,41 +170,45 @@ fn is_dir_non_empty(path: &Path) -> bool {
.unwrap_or(false)
}
-pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &CreateProjectArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// --- Handle deprecated / no-op flags ---
if args.prefer_source {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning("Source installs not yet supported, falling back to dist.")
- );
+ ));
}
if args.dev {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"The --dev flag is deprecated. Dev packages are installed by default."
)
- );
+ ));
}
if args.no_custom_installers {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"The --no-custom-installers flag is deprecated. Use --no-plugins instead."
)
- );
+ ));
}
if !args.repository.is_empty() || args.repository_url.is_some() || args.add_repository {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"Custom repository options (--repository, --repository-url, --add-repository) \
are not yet supported and will be ignored."
)
- );
+ ));
}
// --- Step 1: Parse package argument ---
@@ -268,11 +272,11 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
};
// --- Step 4: Fetch package versions and find best match ---
- eprintln!(
+ console.info(&format!(
"{}",
console::info(&format!("Creating project from package {package_name}"))
- );
- eprintln!("Loading composer repositories with package information");
+ ));
+ console.info("Loading composer repositories with package information");
let versions = packagist::fetch_package_versions(&package_name, None)?;
@@ -306,10 +310,10 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
let concrete_version = best.version.clone();
- eprintln!(
+ console.info(&format!(
"{}",
console::info(&format!("Installing {package_name} ({concrete_version})"))
- );
+ ));
// --- Step 5: Create target directory and download+extract ---
std::fs::create_dir_all(&target_dir)?;
@@ -337,10 +341,10 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
other => anyhow::bail!("Unsupported dist type: {other}"),
}
- eprintln!(
+ console.info(&format!(
"{}",
console::info(&format!("Created project in {}", target_dir.display()))
- );
+ ));
// --- Step 7: VCS removal ---
// Remove VCS metadata unless --keep-vcs is set.
@@ -354,13 +358,13 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
let composer_path = target_dir.join("composer.json");
if !composer_path.exists() {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(&format!(
"No composer.json found in {}. Skipping dependency installation.",
target_dir.display()
))
- );
+ ));
return Ok(());
}
@@ -372,10 +376,10 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
// --- Step 6 continued: dependency resolution and install ---
if args.no_install {
- eprintln!(
+ console.info(&format!(
"{}",
console::comment("Skipping dependency installation (--no-install).")
- );
+ ));
return Ok(());
}
@@ -416,15 +420,14 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
repo_cache: None,
};
- eprintln!("Resolving dependencies...");
+ console.info("Resolving dependencies...");
- let resolved = match resolver::resolve(&request) {
- Ok(packages) => packages,
- Err(e) => {
- eprintln!("{}", console::error(&e.to_string()));
- std::process::exit(1);
- }
- };
+ let resolved = resolver::resolve(&request).map_err(|e| {
+ crate::exit_code::bail(
+ crate::exit_code::DEPENDENCY_RESOLUTION_FAILED,
+ e.to_string(),
+ )
+ })?;
let composer_json_content = std::fs::read_to_string(&composer_path)?;
@@ -444,22 +447,22 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
.filter(|c| matches!(c.kind, super::update::ChangeKind::Install { .. }))
.collect();
- eprintln!(
+ console.info(&format!(
"{}",
console::info(&format!(
"Package operations: {} install{}, 0 updates, 0 removals",
installs.len(),
if installs.len() == 1 { "" } else { "s" },
))
- );
+ ));
for change in &changes {
if let super::update::ChangeKind::Install { new_version } = &change.kind {
- eprintln!(" - Installing {} ({})", change.name, new_version);
+ console.info(&format!(" - Installing {} ({})", change.name, new_version));
}
}
- eprintln!("Writing lock file");
+ console.info("Writing lock file");
let lock_path = target_dir.join("composer.lock");
new_lock.write_to_file(&lock_path)?;
@@ -473,10 +476,10 @@ pub fn execute(args: &CreateProjectArgs, cli: &super::Cli) -> anyhow::Result<()>
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
if prefer_source {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning("Source installs are not yet supported. Falling back to dist.")
- );
+ ));
}
super::install::install_from_lock(
diff --git a/crates/mozart/src/commands/depends.rs b/crates/mozart/src/commands/depends.rs
index fa84f7d..80e70f1 100644
--- a/crates/mozart/src/commands/depends.rs
+++ b/crates/mozart/src/commands/depends.rs
@@ -19,7 +19,11 @@ pub struct DependsArgs {
pub locked: bool,
}
-pub fn execute(args: &DependsArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &DependsArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs
index 51f2127..199ed60 100644
--- a/crates/mozart/src/commands/diagnose.rs
+++ b/crates/mozart/src/commands/diagnose.rs
@@ -371,7 +371,11 @@ fn check_cache_dir(cache_dir: &Path) -> CheckResult {
// ─── Main execute function ─────────────────────────────────────────────────────
-pub fn execute(_args: &DiagnoseArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ _args: &DiagnoseArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs
index 7d5b748..6f38ab9 100644
--- a/crates/mozart/src/commands/dump_autoload.rs
+++ b/crates/mozart/src/commands/dump_autoload.rs
@@ -48,7 +48,11 @@ pub struct DumpAutoloadArgs {
pub strict_ambiguous: bool,
}
-pub fn execute(args: &DumpAutoloadArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &DumpAutoloadArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
@@ -61,7 +65,7 @@ pub fn execute(args: &DumpAutoloadArgs, cli: &super::Cli) -> anyhow::Result<()>
let suffix = crate::autoload::determine_suffix(&working_dir, &vendor_dir)?;
if args.dry_run {
- eprintln!("Dry run: would generate autoload files");
+ console.info("Dry run: would generate autoload files");
return Ok(());
}
@@ -79,7 +83,7 @@ pub fn execute(args: &DumpAutoloadArgs, cli: &super::Cli) -> anyhow::Result<()>
ignore_platform_reqs: args.ignore_platform_reqs,
})?;
- eprintln!("Generated autoload files");
+ console.info("Generated autoload files");
Ok(())
}
diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs
index 9ef1624..32781a5 100644
--- a/crates/mozart/src/commands/exec.rs
+++ b/crates/mozart/src/commands/exec.rs
@@ -17,7 +17,11 @@ pub struct ExecArgs {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &ExecArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ExecArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/fund.rs b/crates/mozart/src/commands/fund.rs
index 19e4825..e8a42f5 100644
--- a/crates/mozart/src/commands/fund.rs
+++ b/crates/mozart/src/commands/fund.rs
@@ -23,7 +23,11 @@ struct FundingEntry {
// ─── Main entry point ───────────────────────────────────────────────────────
-pub fn execute(args: &FundArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &FundArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/global.rs b/crates/mozart/src/commands/global.rs
index 42f3703..e6e7bc8 100644
--- a/crates/mozart/src/commands/global.rs
+++ b/crates/mozart/src/commands/global.rs
@@ -13,7 +13,11 @@ pub struct GlobalArgs {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &GlobalArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &GlobalArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
use clap::Parser as _;
use std::fs;
@@ -21,9 +25,7 @@ pub fn execute(args: &GlobalArgs, cli: &super::Cli) -> anyhow::Result<()> {
fs::create_dir_all(&home)?;
- if !cli.quiet {
- eprintln!("Changed current directory to {}", home.display());
- }
+ console.info(&format!("Changed current directory to {}", home.display()));
// SAFETY: single-threaded at this point; no concurrent env access
unsafe {
diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs
index fb7520f..be104c6 100644
--- a/crates/mozart/src/commands/init.rs
+++ b/crates/mozart/src/commands/init.rs
@@ -55,9 +55,11 @@ pub struct InitArgs {
pub autoload: Option<String>,
}
-pub fn execute(args: &InitArgs, cli: &super::Cli) -> anyhow::Result<()> {
- let console = console::Console::new(cli.no_interaction, cli.quiet);
-
+pub fn execute(
+ args: &InitArgs,
+ cli: &super::Cli,
+ console: &console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir().context("Failed to get current directory")?,
@@ -78,7 +80,7 @@ pub fn execute(args: &InitArgs, cli: &super::Cli) -> anyhow::Result<()> {
}
let composer = if console.interactive {
- build_interactive(args, &console, &working_dir)?
+ build_interactive(args, console, &working_dir)?
} else {
build_non_interactive(args, &working_dir)?
};
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index b5f9142..fb7335b 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -496,58 +496,51 @@ pub fn install_from_lock(
Ok(())
}
-pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &InstallArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Step 1: Resolve the working directory
let working_dir = resolve_working_dir(cli);
// Step 2: Validate arguments
if !args.packages.is_empty() {
let pkgs = args.packages.join(" ");
- eprintln!(
- "{}",
- console::error(&format!(
+ return Err(crate::exit_code::bail(
+ crate::exit_code::GENERAL_ERROR,
+ format!(
"Invalid argument {pkgs}. Use \"mozart require {pkgs}\" instead to add packages to your composer.json."
- ))
- );
- std::process::exit(1);
+ ),
+ ));
}
if args.no_install {
- eprintln!(
- "{}",
- console::error(
- "Invalid option \"--no-install\". Use \"mozart update --no-install\" instead if you are trying to update the composer.lock file."
- )
- );
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::GENERAL_ERROR,
+ "Invalid option \"--no-install\". Use \"mozart update --no-install\" instead if you are trying to update the composer.lock file.",
+ ));
}
if args.dev {
- eprintln!(
- "{}",
- console::warning(
- "The --dev option is deprecated. Dev packages are installed by default."
- )
- );
+ console.info(&console::warning(
+ "The --dev option is deprecated. Dev packages are installed by default.",
+ ));
}
if args.no_suggest {
- eprintln!(
- "{}",
- console::warning("The --no-suggest option is deprecated and has no effect.")
- );
+ console.info(&console::warning(
+ "The --no-suggest option is deprecated and has no effect.",
+ ));
}
// Step 3: Read composer.lock
let lock_path = working_dir.join("composer.lock");
if !lock_path.exists() {
- eprintln!(
- "{}",
- console::warning(
- "No composer.lock file present. Run \"mozart update\" to generate one."
- )
- );
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::LOCK_FILE_INVALID,
+ "No composer.lock file present. Run \"mozart update\" to generate one.",
+ ));
}
let lock = lockfile::LockFile::read_from_file(&lock_path)?;
@@ -556,12 +549,9 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
if composer_json_path.exists() {
let content = std::fs::read_to_string(&composer_json_path)?;
if !lock.is_fresh(&content) {
- eprintln!(
- "{}",
- console::warning(
- "Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `mozart update`."
- )
- );
+ console.info(&console::warning(
+ "Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `mozart update`."
+ ));
}
}
@@ -573,12 +563,9 @@ pub fn execute(args: &InstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
if prefer_source {
- eprintln!(
- "{}",
- console::warning(
- "Warning: Source installs are not yet supported. Falling back to dist."
- )
- );
+ console.info(&console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist.",
+ ));
}
// Step 6: Determine dev mode and vendor directory
diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs
index 6a5c295..0703976 100644
--- a/crates/mozart/src/commands/licenses.rs
+++ b/crates/mozart/src/commands/licenses.rs
@@ -27,7 +27,11 @@ struct LicenseEntry {
// ─── Main entry point ───────────────────────────────────────────────────────
-pub fn execute(args: &LicensesArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &LicensesArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/outdated.rs b/crates/mozart/src/commands/outdated.rs
index f355e09..b6672c4 100644
--- a/crates/mozart/src/commands/outdated.rs
+++ b/crates/mozart/src/commands/outdated.rs
@@ -96,7 +96,11 @@ struct OutdatedEntry {
// ─── Main entry point ───────────────────────────────────────────────────────
-pub fn execute(args: &OutdatedArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &OutdatedArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/prohibits.rs b/crates/mozart/src/commands/prohibits.rs
index d30e57f..f545bb2 100644
--- a/crates/mozart/src/commands/prohibits.rs
+++ b/crates/mozart/src/commands/prohibits.rs
@@ -22,7 +22,11 @@ pub struct ProhibitsArgs {
pub locked: bool,
}
-pub fn execute(args: &ProhibitsArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ProhibitsArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/reinstall.rs b/crates/mozart/src/commands/reinstall.rs
index 2015d62..78611b4 100644
--- a/crates/mozart/src/commands/reinstall.rs
+++ b/crates/mozart/src/commands/reinstall.rs
@@ -65,7 +65,11 @@ pub struct ReinstallArgs {
// ─── Main entry point ─────────────────────────────────────────────────────────
-pub fn execute(args: &ReinstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ReinstallArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Step 1: Resolve working directory
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
@@ -167,7 +171,10 @@ pub fn execute(args: &ReinstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
let locked = match locked {
Some(lp) => lp,
None => {
- eprintln!(" Warning: {} is not in the lock file; skipping.", pkg.name);
+ console.info(&format!(
+ " Warning: {} is not in the lock file; skipping.",
+ pkg.name
+ ));
continue;
}
};
@@ -175,15 +182,18 @@ pub fn execute(args: &ReinstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
let dist = match &locked.dist {
Some(d) => d,
None => {
- eprintln!(
+ console.info(&format!(
" Warning: {} has no dist information; skipping.",
locked.name
- );
+ ));
continue;
}
};
- eprintln!(" - Reinstalling {} ({})", locked.name, locked.version);
+ console.info(&format!(
+ " - Reinstalling {} ({})",
+ locked.name, locked.version
+ ));
// Remove vendor directory for this package
let pkg_dir = vendor_dir.join(&locked.name);
@@ -218,7 +228,7 @@ pub fn execute(args: &ReinstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 9: Regenerate autoloader unless --no-autoloader.
if !args.no_autoloader {
- eprintln!("Generating autoload files");
+ console.info("Generating autoload files");
let dev_mode = !args.no_dev && installed.dev;
let suffix = lock.content_hash.clone();
@@ -237,7 +247,7 @@ pub fn execute(args: &ReinstallArgs, cli: &super::Cli) -> anyhow::Result<()> {
ignore_platform_reqs: args.ignore_platform_reqs,
})?;
- eprintln!("Generated autoload files");
+ console.info("Generated autoload files");
}
Ok(())
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index ecfbba8..6969745 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -96,7 +96,11 @@ pub struct RemoveArgs {
pub apcu_autoloader_prefix: Option<String>,
}
-pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &RemoveArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Step 1: Validate inputs
if args.packages.is_empty() && !args.unused {
anyhow::bail!("Not enough arguments (missing: \"packages\").");
@@ -104,20 +108,20 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 2: Handle deprecated flags
if args.update_with_dependencies {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"The -w / --update-with-dependencies flag is deprecated. Use --with-all-dependencies instead."
)
- );
+ ));
}
if args.update_with_all_dependencies {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"The -W / --update-with-all-dependencies flag is deprecated. Use --with-all-dependencies instead."
)
- );
+ ));
}
// Step 3: Resolve working directory and read composer.json
@@ -135,12 +139,12 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 4: Handle --unused flag (deferred implementation)
if args.unused {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(
"--unused is not yet fully implemented. The resolver will naturally prune unreachable packages."
)
- );
+ ));
// Fall through: if no explicit packages were named, nothing to remove.
if args.packages.is_empty() {
return Ok(());
@@ -168,12 +172,12 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
raw.require_dev.remove(&name);
any_removed = true;
} else {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(&format!(
"{name} is not required in require-dev and has not been removed."
))
- );
+ ));
}
} else {
// Auto-detect: look in require first, then require-dev
@@ -192,12 +196,12 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
raw.require_dev.remove(&name);
any_removed = true;
} else {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(&format!(
"{name} is not required in your composer.json and has not been removed."
))
- );
+ ));
}
}
}
@@ -272,35 +276,34 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
};
// Print header messages
- eprintln!("Loading composer repositories with package information");
+ console.info("Loading composer repositories with package information");
if dev_mode {
- eprintln!("Updating dependencies (including require-dev)");
+ console.info("Updating dependencies (including require-dev)");
} else {
- eprintln!("Updating dependencies");
+ console.info("Updating dependencies");
}
- eprintln!("Resolving dependencies...");
+ console.info("Resolving dependencies...");
// Run resolver
- let mut resolved = match resolver::resolve(&request) {
- Ok(packages) => packages,
- Err(e) => {
- eprintln!("{}", console::error(&e.to_string()));
- std::process::exit(1);
- }
- };
+ let mut resolved = resolver::resolve(&request).map_err(|e| {
+ crate::exit_code::bail(
+ crate::exit_code::DEPENDENCY_RESOLUTION_FAILED,
+ e.to_string(),
+ )
+ })?;
// Read old lock file (if any) for change reporting and partial update
let old_lock = if lock_path.exists() {
match lockfile::LockFile::read_from_file(&lock_path) {
Ok(l) => Some(l),
Err(e) => {
- eprintln!(
+ console.info(&format!(
"{}",
console::warning(&format!(
"Could not read existing composer.lock: {}. Treating as a fresh install.",
e
))
- );
+ ));
None
}
}
@@ -341,12 +344,12 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
// For --minimal-changes, additionally pin packages beyond the allow list
if args.minimal_changes {
- eprintln!(
+ console.info(&format!(
"{}",
console::info(
"Minimal changes mode: preserving locked versions for non-removed packages."
)
- );
+ ));
}
resolved = super::update::apply_partial_update(resolved, lock, &allow_list);
@@ -385,7 +388,7 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
.filter(|c| matches!(c.kind, super::update::ChangeKind::Remove { .. }))
.collect();
- eprintln!(
+ console.info(&format!(
"{}",
console::info(&format!(
"Package operations: {} install{}, {} update{}, {} removal{}",
@@ -396,23 +399,29 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
removals.len(),
if removals.len() == 1 { "" } else { "s" },
))
- );
+ ));
// Print individual change lines
for change in &changes {
match &change.kind {
super::update::ChangeKind::Remove { old_version } => {
if args.dry_run {
- eprintln!(" - Would remove {} ({})", change.name, old_version);
+ console.info(&format!(
+ " - Would remove {} ({})",
+ change.name, old_version
+ ));
} else {
- eprintln!(" - Removing {} ({})", change.name, old_version);
+ console.info(&format!(" - Removing {} ({})", change.name, old_version));
}
}
super::update::ChangeKind::Install { new_version } => {
if args.dry_run {
- eprintln!(" - Would install {} ({})", change.name, new_version);
+ console.info(&format!(
+ " - Would install {} ({})",
+ change.name, new_version
+ ));
} else {
- eprintln!(" - Installing {} ({})", change.name, new_version);
+ console.info(&format!(" - Installing {} ({})", change.name, new_version));
}
}
super::update::ChangeKind::Update {
@@ -420,15 +429,15 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
new_version,
} => {
if args.dry_run {
- eprintln!(
+ console.info(&format!(
" - Would update {} ({} => {})",
change.name, old_version, new_version
- );
+ ));
} else {
- eprintln!(
+ console.info(&format!(
" - Updating {} ({} => {})",
change.name, old_version, new_version
- );
+ ));
}
}
super::update::ChangeKind::Unchanged => {}
@@ -437,7 +446,7 @@ pub fn execute(args: &RemoveArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Write lock file (unless --dry-run)
if !args.dry_run {
- eprintln!("Writing lock file");
+ console.info("Writing lock file");
new_lock.write_to_file(&lock_path)?;
}
diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs
index 3a094cc..c54601b 100644
--- a/crates/mozart/src/commands/repository.rs
+++ b/crates/mozart/src/commands/repository.rs
@@ -35,6 +35,10 @@ pub struct RepositoryArgs {
pub after: Option<String>,
}
-pub fn execute(_args: &RepositoryArgs, _cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ _args: &RepositoryArgs,
+ _cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
todo!()
}
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 7709f86..6e76b6e 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -343,7 +343,11 @@ fn interactive_search_packages(
Ok(selected)
}
-pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &RequireArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Collect the effective list of packages to add.
// If none were provided on the CLI, try interactive search (unless --no-interaction).
let cli_packages: Vec<String> = if args.packages.is_empty() {
@@ -399,26 +403,19 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Handle deprecated flags
if args.no_suggest {
- eprintln!(
- "{}",
- console::warning("The --no-suggest option is deprecated and has no effect.")
- );
+ console.info(&console::warning(
+ "The --no-suggest option is deprecated and has no effect.",
+ ));
}
if args.update_with_dependencies {
- eprintln!(
- "{}",
- console::warning(
- "The -w / --update-with-dependencies flag is deprecated. Use --with-dependencies instead."
- )
- );
+ console.info(&console::warning(
+ "The -w / --update-with-dependencies flag is deprecated. Use --with-dependencies instead."
+ ));
}
if args.update_with_all_dependencies {
- eprintln!(
- "{}",
- console::warning(
- "The -W / --update-with-all-dependencies flag is deprecated. Use --with-all-dependencies instead."
- )
- );
+ console.info(&console::warning(
+ "The -W / --update-with-all-dependencies flag is deprecated. Use --with-all-dependencies instead."
+ ));
}
// Resolve working directory
@@ -600,20 +597,22 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
};
// Print header messages
- eprintln!("Loading composer repositories with package information");
+ console.info("Loading composer repositories with package information");
if dev_mode {
- eprintln!("Updating dependencies (including require-dev)");
+ console.info("Updating dependencies (including require-dev)");
} else {
- eprintln!("Updating dependencies");
+ console.info("Updating dependencies");
}
- eprintln!("Resolving dependencies...");
+ console.info("Resolving dependencies...");
// Run resolver
let mut resolved = match resolver::resolve(&request) {
Ok(packages) => packages,
Err(e) => {
- eprintln!("{}", console::error(&e.to_string()));
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::DEPENDENCY_RESOLUTION_FAILED,
+ e.to_string(),
+ ));
}
};
@@ -622,13 +621,10 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
match lockfile::LockFile::read_from_file(&lock_path) {
Ok(l) => Some(l),
Err(e) => {
- eprintln!(
- "{}",
- console::warning(&format!(
- "Could not read existing composer.lock: {}. Treating as a fresh install.",
- e
- ))
- );
+ console.info(&console::warning(&format!(
+ "Could not read existing composer.lock: {}. Treating as a fresh install.",
+ e
+ )));
None
}
}
@@ -693,34 +689,37 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
.filter(|c| matches!(c.kind, super::update::ChangeKind::Remove { .. }))
.collect();
- eprintln!(
- "{}",
- console::info(&format!(
- "Package operations: {} install{}, {} update{}, {} removal{}",
- installs.len(),
- if installs.len() == 1 { "" } else { "s" },
- updates.len(),
- if updates.len() == 1 { "" } else { "s" },
- removals.len(),
- if removals.len() == 1 { "" } else { "s" },
- ))
- );
+ console.info(&format!(
+ "Package operations: {} install{}, {} update{}, {} removal{}",
+ installs.len(),
+ if installs.len() == 1 { "" } else { "s" },
+ updates.len(),
+ if updates.len() == 1 { "" } else { "s" },
+ removals.len(),
+ if removals.len() == 1 { "" } else { "s" },
+ ));
// Print individual change lines
for change in &changes {
match &change.kind {
super::update::ChangeKind::Remove { old_version } => {
if args.dry_run {
- eprintln!(" - Would remove {} ({})", change.name, old_version);
+ console.info(&format!(
+ " - Would remove {} ({})",
+ change.name, old_version
+ ));
} else {
- eprintln!(" - Removing {} ({})", change.name, old_version);
+ console.info(&format!(" - Removing {} ({})", change.name, old_version));
}
}
super::update::ChangeKind::Install { new_version } => {
if args.dry_run {
- eprintln!(" - Would install {} ({})", change.name, new_version);
+ console.info(&format!(
+ " - Would install {} ({})",
+ change.name, new_version
+ ));
} else {
- eprintln!(" - Installing {} ({})", change.name, new_version);
+ console.info(&format!(" - Installing {} ({})", change.name, new_version));
}
}
super::update::ChangeKind::Update {
@@ -728,15 +727,15 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
new_version,
} => {
if args.dry_run {
- eprintln!(
+ console.info(&format!(
" - Would update {} ({} => {})",
change.name, old_version, new_version
- );
+ ));
} else {
- eprintln!(
+ console.info(&format!(
" - Updating {} ({} => {})",
change.name, old_version, new_version
- );
+ ));
}
}
super::update::ChangeKind::Unchanged => {}
@@ -745,7 +744,7 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Write lock file (unless --dry-run)
if !args.dry_run {
- eprintln!("Writing lock file");
+ console.info("Writing lock file");
new_lock.write_to_file(&lock_path)?;
}
@@ -759,12 +758,9 @@ pub fn execute(args: &RequireArgs, cli: &super::Cli) -> anyhow::Result<()> {
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
if prefer_source {
- eprintln!(
- "{}",
- crate::console::warning(
- "Warning: Source installs are not yet supported. Falling back to dist."
- )
- );
+ console.info(&crate::console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist.",
+ ));
}
super::install::install_from_lock(
diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs
index 845d4bd..8f2b5cd 100644
--- a/crates/mozart/src/commands/run_script.rs
+++ b/crates/mozart/src/commands/run_script.rs
@@ -46,7 +46,11 @@ const INTERNAL_ONLY_EVENTS: &[&str] = &[
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &RunScriptArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &RunScriptArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/search.rs b/crates/mozart/src/commands/search.rs
index e14c4ff..d172976 100644
--- a/crates/mozart/src/commands/search.rs
+++ b/crates/mozart/src/commands/search.rs
@@ -59,7 +59,11 @@ fn passes_only_vendor(result: &SearchResult, query: &str) -> bool {
vendor.eq_ignore_ascii_case(query)
}
-pub fn execute(args: &SearchArgs, _cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &SearchArgs,
+ _cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let query = args.tokens.join(" ");
let (all_results, total) = crate::packagist::search_packages(&query, args.r#type.as_deref())?;
diff --git a/crates/mozart/src/commands/self_update.rs b/crates/mozart/src/commands/self_update.rs
index 2420a7d..29c67ef 100644
--- a/crates/mozart/src/commands/self_update.rs
+++ b/crates/mozart/src/commands/self_update.rs
@@ -50,7 +50,11 @@ const BACKUP_EXTENSION: &str = ".old";
// ─── Public entry point ───────────────────────────────────────────────────────
-pub fn execute(args: &SelfUpdateArgs, _cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &SelfUpdateArgs,
+ _cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let current_exe = std::env::current_exe()
.map_err(|e| anyhow::anyhow!("Could not determine current executable path: {e}"))?;
diff --git a/crates/mozart/src/commands/show.rs b/crates/mozart/src/commands/show.rs
index 498d170..a8ae995 100644
--- a/crates/mozart/src/commands/show.rs
+++ b/crates/mozart/src/commands/show.rs
@@ -99,7 +99,11 @@ pub struct ShowArgs {
pub ignore_platform_reqs: bool,
}
-pub fn execute(args: &ShowArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ShowArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs
index ae38621..dc26a5f 100644
--- a/crates/mozart/src/commands/status.rs
+++ b/crates/mozart/src/commands/status.rs
@@ -44,7 +44,11 @@ struct PackageStatus {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &StatusArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &StatusArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/suggests.rs b/crates/mozart/src/commands/suggests.rs
index 8d0898a..df58e47 100644
--- a/crates/mozart/src/commands/suggests.rs
+++ b/crates/mozart/src/commands/suggests.rs
@@ -38,7 +38,11 @@ struct Suggestion {
// ─── Main entry point ────────────────────────────────────────────────────────
-pub fn execute(args: &SuggestsArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &SuggestsArgs,
+ cli: &super::Cli,
+ _console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 930c458..d4056e4 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -630,57 +630,53 @@ pub fn apply_minimal_changes(
// Main execute function
// ─────────────────────────────────────────────────────────────────────────────
-pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &UpdateArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
// Step 1: Resolve the working directory
let working_dir = super::install::resolve_working_dir(cli);
// Step 2: Handle deprecated flags
if args.dev {
- eprintln!(
- "{}",
- console::warning(
- "The --dev option is deprecated. Dev packages are updated by default."
- )
- );
+ console.info(&console::warning(
+ "The --dev option is deprecated. Dev packages are updated by default.",
+ ));
}
if args.no_suggest {
- eprintln!(
- "{}",
- console::warning("The --no-suggest option is deprecated and has no effect.")
- );
+ console.info(&console::warning(
+ "The --no-suggest option is deprecated and has no effect.",
+ ));
}
// Warn about still-deferred flags
if args.patch_only {
- eprintln!(
- "{}",
- console::warning("--patch-only is not yet implemented and will be ignored.")
- );
+ console.info(&console::warning(
+ "--patch-only is not yet implemented and will be ignored.",
+ ));
}
if args.root_reqs {
- eprintln!(
- "{}",
- console::warning("--root-reqs is not yet implemented and will be ignored.")
- );
+ console.info(&console::warning(
+ "--root-reqs is not yet implemented and will be ignored.",
+ ));
}
if args.bump_after_update.is_some() {
- eprintln!(
- "{}",
- console::warning("--bump-after-update is not yet implemented and will be ignored.")
- );
+ console.info(&console::warning(
+ "--bump-after-update is not yet implemented and will be ignored.",
+ ));
}
// Step 3: Read composer.json
let composer_json_path = working_dir.join("composer.json");
if !composer_json_path.exists() {
- eprintln!(
- "{}",
- console::error(&format!(
+ return Err(crate::exit_code::bail(
+ crate::exit_code::GENERAL_ERROR,
+ format!(
"Composer could not find a composer.json file in {}",
working_dir.display()
- ))
- );
- std::process::exit(1);
+ ),
+ ));
}
let composer_json = package::read_from_file(&composer_json_path)?;
let composer_json_content = std::fs::read_to_string(&composer_json_path)?;
@@ -690,7 +686,7 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 4: Handle --lock mode (early return)
if args.lock {
- return handle_lock_mode(&lock_path, &composer_json_content, args.dry_run);
+ return handle_lock_mode(&lock_path, &composer_json_content, args.dry_run, console);
}
let dev_mode = !args.no_dev;
@@ -739,19 +735,21 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
};
// Step 6: Print header and run resolver
- eprintln!("Loading composer repositories with package information");
+ console.info("Loading composer repositories with package information");
if dev_mode {
- eprintln!("Updating dependencies (including require-dev)");
+ console.info("Updating dependencies (including require-dev)");
} else {
- eprintln!("Updating dependencies");
+ console.info("Updating dependencies");
}
- eprintln!("Resolving dependencies...");
+ console.info("Resolving dependencies...");
let mut resolved = match resolver::resolve(&request) {
Ok(packages) => packages,
Err(e) => {
- eprintln!("{}", console::error(&e.to_string()));
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::DEPENDENCY_RESOLUTION_FAILED,
+ e.to_string(),
+ ));
}
};
@@ -760,13 +758,10 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
match lockfile::LockFile::read_from_file(&lock_path) {
Ok(l) => Some(l),
Err(e) => {
- eprintln!(
- "{}",
- console::warning(&format!(
- "Could not read existing composer.lock: {}. Treating as a fresh install.",
- e
- ))
- );
+ console.info(&console::warning(&format!(
+ "Could not read existing composer.lock: {}. Treating as a fresh install.",
+ e
+ )));
None
}
}
@@ -782,13 +777,10 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
let update_packages: Vec<String> = if !args.packages.is_empty() {
match &old_lock {
None => {
- eprintln!(
- "{}",
- console::error(
- "No lock file found. Cannot perform partial update. Run `mozart update` first."
- )
- );
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::NO_LOCK_FILE_FOR_PARTIAL_UPDATE,
+ "No lock file found. Cannot perform partial update. Run `mozart update` first.",
+ ));
}
Some(lock) => {
// 1. Expand wildcards
@@ -813,10 +805,9 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
if args.interactive {
match &old_lock {
None => {
- eprintln!(
- "{}",
- console::warning("No lock file found. --interactive mode skipped.")
- );
+ console.info(&console::warning(
+ "No lock file found. --interactive mode skipped.",
+ ));
vec![]
}
Some(lock) => {
@@ -843,13 +834,10 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
if !update_packages.is_empty() {
match &old_lock {
None => {
- eprintln!(
- "{}",
- console::error(
- "No lock file found. Cannot perform partial update. Run `mozart update` first."
- )
- );
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::NO_LOCK_FILE_FOR_PARTIAL_UPDATE,
+ "No lock file found. Cannot perform partial update. Run `mozart update` first.",
+ ));
}
Some(lock) => {
resolved = apply_partial_update(resolved, lock, &update_packages);
@@ -859,10 +847,7 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Full update with --minimal-changes: pin everything to locked versions
// (only updates packages whose constraints have changed in composer.json)
if let Some(ref lock) = old_lock {
- eprintln!(
- "{}",
- console::info("Minimal changes mode: preserving locked versions where possible.")
- );
+ console.info("Minimal changes mode: preserving locked versions where possible.");
resolved = apply_minimal_changes(resolved, lock);
}
}
@@ -892,18 +877,15 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
.filter(|c| matches!(c.kind, ChangeKind::Remove { .. }))
.collect();
- eprintln!(
- "{}",
- console::info(&format!(
- "Package operations: {} install{}, {} update{}, {} removal{}",
- installs.len(),
- if installs.len() == 1 { "" } else { "s" },
- updates.len(),
- if updates.len() == 1 { "" } else { "s" },
- removals.len(),
- if removals.len() == 1 { "" } else { "s" },
- ))
- );
+ console.info(&format!(
+ "Package operations: {} install{}, {} update{}, {} removal{}",
+ installs.len(),
+ if installs.len() == 1 { "" } else { "s" },
+ updates.len(),
+ if updates.len() == 1 { "" } else { "s" },
+ removals.len(),
+ if removals.len() == 1 { "" } else { "s" },
+ ));
// Print individual change lines
let prefix = if args.dry_run { "Would" } else { "" };
@@ -911,16 +893,22 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
match &change.kind {
ChangeKind::Remove { old_version } => {
if args.dry_run {
- eprintln!(" - {} remove {} ({})", prefix, change.name, old_version);
+ console.info(&format!(
+ " - {} remove {} ({})",
+ prefix, change.name, old_version
+ ));
} else {
- eprintln!(" - Removing {} ({})", change.name, old_version);
+ console.info(&format!(" - Removing {} ({})", change.name, old_version));
}
}
ChangeKind::Install { new_version } => {
if args.dry_run {
- eprintln!(" - {} install {} ({})", prefix, change.name, new_version);
+ console.info(&format!(
+ " - {} install {} ({})",
+ prefix, change.name, new_version
+ ));
} else {
- eprintln!(" - Installing {} ({})", change.name, new_version);
+ console.info(&format!(" - Installing {} ({})", change.name, new_version));
}
}
ChangeKind::Update {
@@ -928,15 +916,15 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
new_version,
} => {
if args.dry_run {
- eprintln!(
+ console.info(&format!(
" - {} update {} ({} => {})",
prefix, change.name, old_version, new_version
- );
+ ));
} else {
- eprintln!(
+ console.info(&format!(
" - Updating {} ({} => {})",
change.name, old_version, new_version
- );
+ ));
}
}
ChangeKind::Unchanged => {}
@@ -945,7 +933,7 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Step 11: Write lock file (unless --dry-run)
if !args.dry_run {
- eprintln!("Writing lock file");
+ console.info("Writing lock file");
new_lock.write_to_file(&lock_path)?;
}
@@ -959,12 +947,9 @@ pub fn execute(args: &UpdateArgs, cli: &super::Cli) -> anyhow::Result<()> {
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
if prefer_source {
- eprintln!(
- "{}",
- crate::console::warning(
- "Warning: Source installs are not yet supported. Falling back to dist."
- )
- );
+ console.info(&crate::console::warning(
+ "Warning: Source installs are not yet supported. Falling back to dist.",
+ ));
}
super::install::install_from_lock(
@@ -1001,13 +986,13 @@ fn handle_lock_mode(
lock_path: &std::path::Path,
composer_json_content: &str,
dry_run: bool,
+ console: &crate::console::Console,
) -> anyhow::Result<()> {
if !lock_path.exists() {
- eprintln!(
- "{}",
- console::error("No lock file found. Run `mozart update` to generate one.")
- );
- std::process::exit(1);
+ return Err(crate::exit_code::bail(
+ crate::exit_code::LOCK_FILE_INVALID,
+ "No lock file found. Run `mozart update` to generate one.",
+ ));
}
let mut lock = lockfile::LockFile::read_from_file(lock_path)?;
@@ -1015,7 +1000,7 @@ fn handle_lock_mode(
let new_hash = lockfile::LockFile::compute_content_hash(composer_json_content)?;
if new_hash == lock.content_hash {
- eprintln!("Lock file is already up to date");
+ console.info("Lock file is already up to date");
return Ok(());
}
@@ -1023,9 +1008,9 @@ fn handle_lock_mode(
if !dry_run {
lock.write_to_file(lock_path)?;
- eprintln!("Lock file hash updated successfully.");
+ console.info("Lock file hash updated successfully.");
} else {
- eprintln!("Would update lock file hash.");
+ console.info("Would update lock file hash.");
}
Ok(())
@@ -1375,7 +1360,12 @@ mod tests {
// Composer.json content that will produce a different hash
let composer_json_content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#;
- let result = handle_lock_mode(&lock_path, composer_json_content, false);
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ let result = handle_lock_mode(&lock_path, composer_json_content, false, &console);
assert!(result.is_ok());
// Read back and verify hash changed
@@ -1398,7 +1388,12 @@ mod tests {
lock.content_hash = correct_hash.clone();
lock.write_to_file(&lock_path).unwrap();
- let result = handle_lock_mode(&lock_path, composer_json_content, false);
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ let result = handle_lock_mode(&lock_path, composer_json_content, false, &console);
assert!(result.is_ok());
// Hash should not have changed
@@ -1417,7 +1412,12 @@ mod tests {
let composer_json_content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#;
- let result = handle_lock_mode(&lock_path, composer_json_content, true);
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ let result = handle_lock_mode(&lock_path, composer_json_content, true, &console);
assert!(result.is_ok());
// Hash should NOT have changed (dry_run=true)
@@ -1717,7 +1717,12 @@ mod tests {
let expected_hash =
lockfile::LockFile::compute_content_hash(composer_json_content).unwrap();
- handle_lock_mode(&lock_path, composer_json_content, false).unwrap();
+ let console = crate::console::Console {
+ interactive: false,
+ verbosity: crate::console::Verbosity::Normal,
+ decorated: false,
+ };
+ handle_lock_mode(&lock_path, composer_json_content, false, &console).unwrap();
let updated = lockfile::LockFile::read_from_file(&lock_path).unwrap();
assert_eq!(updated.content_hash, expected_hash);
diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs
index 7a01761..1dec3fe 100644
--- a/crates/mozart/src/commands/validate.rs
+++ b/crates/mozart/src/commands/validate.rs
@@ -67,7 +67,11 @@ impl ValidationResult {
// ─── Entry point ─────────────────────────────────────────────────────────────
-pub fn execute(args: &ValidateArgs, cli: &super::Cli) -> anyhow::Result<()> {
+pub fn execute(
+ args: &ValidateArgs,
+ cli: &super::Cli,
+ console: &crate::console::Console,
+) -> anyhow::Result<()> {
let working_dir = match &cli.working_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
@@ -79,24 +83,28 @@ pub fn execute(args: &ValidateArgs, cli: &super::Cli) -> anyhow::Result<()> {
None => working_dir.join("composer.json"),
};
+ // Validate-specific exit codes (matching Composer's behavior):
+ // 3 = file not found or not readable
+ // 2 = JSON parse error
+ const VALIDATE_FILE_ERROR: i32 = 3;
+ const VALIDATE_JSON_ERROR: i32 = 2;
+
// Check file exists
if !file.exists() {
- eprintln!(
- "{}",
- crate::console::error(&format!("{} not found.", file.display()))
- );
- std::process::exit(3);
+ return Err(crate::exit_code::bail(
+ VALIDATE_FILE_ERROR,
+ format!("{} not found.", file.display()),
+ ));
}
// Read file content
let content = match std::fs::read_to_string(&file) {
Ok(c) => c,
Err(_) => {
- eprintln!(
- "{}",
- crate::console::error(&format!("{} is not readable.", file.display()))
- );
- std::process::exit(3);
+ return Err(crate::exit_code::bail(
+ VALIDATE_FILE_ERROR,
+ format!("{} is not readable.", file.display()),
+ ));
}
};
@@ -104,12 +112,10 @@ pub fn execute(args: &ValidateArgs, cli: &super::Cli) -> anyhow::Result<()> {
let json_value: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => {
- eprintln!(
- "{}",
- crate::console::error(&format!("{} does not contain valid JSON", file.display()))
- );
- eprintln!("{e}");
- std::process::exit(2);
+ return Err(crate::exit_code::bail(
+ VALIDATE_JSON_ERROR,
+ format!("{} does not contain valid JSON: {e}", file.display()),
+ ));
}
};
@@ -130,7 +136,7 @@ pub fn execute(args: &ValidateArgs, cli: &super::Cli) -> anyhow::Result<()> {
// Stub for --with-dependencies
if args.with_dependencies {
- eprintln!("The --with-dependencies option is not yet implemented");
+ console.info("The --with-dependencies option is not yet implemented");
}
let exit_code = compute_exit_code(
@@ -141,7 +147,7 @@ pub fn execute(args: &ValidateArgs, cli: &super::Cli) -> anyhow::Result<()> {
args.strict,
);
if exit_code != 0 {
- std::process::exit(exit_code);
+ return Err(crate::exit_code::bail_silent(exit_code));
}
Ok(())