diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-23 15:11:36 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-23 15:11:36 +0900 |
| commit | d6e0c6d34449224ac3687daf551a0acfd15cee32 (patch) | |
| tree | d6767718ad566542d4770d4688d9961e0f74ea3d /crates/mozart/src | |
| parent | 7e45efd8a1f488b1a684f9efe31ff39009fc9e54 (diff) | |
| download | php-mozart-d6e0c6d34449224ac3687daf551a0acfd15cee32.tar.gz php-mozart-d6e0c6d34449224ac3687daf551a0acfd15cee32.tar.zst php-mozart-d6e0c6d34449224ac3687daf551a0acfd15cee32.zip | |
refactor(cli): route command output through Console abstraction
Replace direct println\!/eprintln\! calls with console.write(),
console.info(), and console.write_stdout() across all command
handlers to respect verbosity settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src')
28 files changed, 1361 insertions, 893 deletions
diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs index 8675135..335343d 100644 --- a/crates/mozart/src/commands/archive.rs +++ b/crates/mozart/src/commands/archive.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use std::path::PathBuf; #[derive(Args)] @@ -233,8 +234,7 @@ pub async fn execute( } else { target_path.display().to_string() }; - eprint!("Created: "); - println!("{}", display_path); + console.write_stdout(&format!("Created: {}", display_path), Verbosity::Normal); Ok(()) } diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs index 5dc602a..013e0c7 100644 --- a/crates/mozart/src/commands/audit.rs +++ b/crates/mozart/src/commands/audit.rs @@ -2,6 +2,7 @@ use clap::Args; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use mozart_core::console::Verbosity; use mozart_registry::packagist::SecurityAdvisory; #[derive(Args)] @@ -71,7 +72,7 @@ struct AuditResult { pub async fn execute( args: &AuditArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Validate format let format = args.format.as_str(); @@ -103,7 +104,7 @@ pub async fn execute( let packages = load_packages(&working_dir, args.locked, args.no_dev)?; if packages.is_empty() { - eprintln!("No packages - skipping audit."); + console.info("No packages - skipping audit."); return Ok(()); } @@ -121,7 +122,7 @@ pub async fn execute( }; // Filter advisories by installed versions and severity - let matched = filter_advisories(&all_advisories, &packages, &args.ignore_severity); + let matched = filter_advisories(&all_advisories, &packages, &args.ignore_severity, console); // Detect abandoned packages let abandoned = if abandoned_mode == "ignore" { @@ -143,10 +144,10 @@ pub async fn execute( // Render output match format { - "table" => render_table(&result), - "plain" => render_plain(&result), - "json" => render_json(&result)?, - "summary" => render_summary(&result), + "table" => render_table(&result, console), + "plain" => render_plain(&result, console), + "json" => render_json(&result, console)?, + "summary" => render_summary(&result, console), _ => unreachable!(), } @@ -254,6 +255,7 @@ fn filter_advisories( all_advisories: &BTreeMap<String, Vec<SecurityAdvisory>>, packages: &[PackageEntry], ignore_severity: &[String], + console: &mozart_core::console::Console, ) -> BTreeMap<String, Vec<MatchedAdvisory>> { let ignore_set: std::collections::HashSet<String> = ignore_severity.iter().map(|s| s.to_lowercase()).collect(); @@ -274,9 +276,12 @@ fn filter_advisories( let installed_ver = match mozart_semver::Version::parse(version_str) { Ok(v) => v, Err(_) => { - eprintln!( - "Warning: could not parse version \"{}\" for package \"{}\", skipping advisory matching", - version_str, pkg.name + console.write( + &format!( + "Warning: could not parse version \"{}\" for package \"{}\", skipping advisory matching", + version_str, pkg.name + ), + Verbosity::Normal, ); continue; } @@ -299,9 +304,12 @@ fn filter_advisories( let constraint = match mozart_semver::VersionConstraint::parse(&normalized_constraint) { Ok(c) => c, Err(_) => { - eprintln!( - "Warning: could not parse affected versions \"{}\" for advisory \"{}\", skipping", - advisory.affected_versions, advisory.advisory_id + console.write( + &format!( + "Warning: could not parse affected versions \"{}\" for advisory \"{}\", skipping", + advisory.affected_versions, advisory.advisory_id + ), + Verbosity::Normal, ); continue; } @@ -384,12 +392,12 @@ fn detect_abandoned(packages: &[PackageEntry]) -> Vec<AbandonedPackage> { // ─── Output rendering ───────────────────────────────────────────────────────── -fn render_table(result: &AuditResult) { +fn render_table(result: &AuditResult, console: &mozart_core::console::Console) { if result.total_advisory_count == 0 && result.abandoned.is_empty() { - eprintln!( + console.info(&format!( "{}", mozart_core::console::info("No security vulnerability advisories found.") - ); + )); return; } @@ -403,8 +411,11 @@ fn render_table(result: &AuditResult) { "Found {} security vulnerability {} affecting {} package(s):", result.total_advisory_count, advisory_word, result.affected_package_count ); - eprintln!("{}", mozart_core::console::highlight(&header)); - eprintln!(); + console.write( + &format!("{}", mozart_core::console::highlight(&header)), + Verbosity::Normal, + ); + console.write("", Verbosity::Normal); for advisories in result.advisories.values() { for matched in advisories { @@ -436,26 +447,32 @@ fn render_table(result: &AuditResult) { vw = value_width ); - eprintln!("{}", separator); + console.write(&separator, Verbosity::Normal); for (label, value) in &rows { - eprintln!( - "| {:<lw$} | {:<vw$} |", - label, - value, - lw = label_width, - vw = value_width + console.write( + &format!( + "| {:<lw$} | {:<vw$} |", + label, + value, + lw = label_width, + vw = value_width + ), + Verbosity::Normal, ); } - eprintln!("{}", separator); - eprintln!(); + console.write(&separator, Verbosity::Normal); + console.write("", Verbosity::Normal); } } } if !result.abandoned.is_empty() { let header = format!("Found {} abandoned package(s):", result.abandoned.len()); - eprintln!("{}", mozart_core::console::highlight(&header)); - eprintln!(); + console.write( + &format!("{}", mozart_core::console::highlight(&header)), + Verbosity::Normal, + ); + console.write("", Verbosity::Normal); let name_width = 20usize; let ver_width = result @@ -478,46 +495,55 @@ fn render_table(result: &AuditResult) { .unwrap_or(0) .max("Suggested Replacement".len()); - eprintln!( - "| {:<nw$} | {:<vw$} | {:<rw$} |", - "Abandoned Package", - "Version", - "Suggested Replacement", - nw = name_width, - vw = ver_width, - rw = repl_width + console.write( + &format!( + "| {:<nw$} | {:<vw$} | {:<rw$} |", + "Abandoned Package", + "Version", + "Suggested Replacement", + nw = name_width, + vw = ver_width, + rw = repl_width + ), + Verbosity::Normal, ); - eprintln!( - "+-{:-<nw$}-+-{:-<vw$}-+-{:-<rw$}-+", - "", - "", - "", - nw = name_width, - vw = ver_width, - rw = repl_width + console.write( + &format!( + "+-{:-<nw$}-+-{:-<vw$}-+-{:-<rw$}-+", + "", + "", + "", + nw = name_width, + vw = ver_width, + rw = repl_width + ), + Verbosity::Normal, ); for pkg in &result.abandoned { let replacement = pkg .replacement .as_deref() .unwrap_or("No replacement suggested"); - eprintln!( - "| {:<nw$} | {:<vw$} | {:<rw$} |", - pkg.name, - pkg.version, - replacement, - nw = name_width, - vw = ver_width, - rw = repl_width + console.write( + &format!( + "| {:<nw$} | {:<vw$} | {:<rw$} |", + pkg.name, + pkg.version, + replacement, + nw = name_width, + vw = ver_width, + rw = repl_width + ), + Verbosity::Normal, ); } - eprintln!(); + console.write("", Verbosity::Normal); } } -fn render_plain(result: &AuditResult) { +fn render_plain(result: &AuditResult, console: &mozart_core::console::Console) { if result.total_advisory_count == 0 && result.abandoned.is_empty() { - eprintln!("No security vulnerability advisories found."); + console.info("No security vulnerability advisories found."); return; } @@ -527,44 +553,77 @@ fn render_plain(result: &AuditResult) { } else { "advisories" }; - eprintln!( - "Found {} security vulnerability {} affecting {} package(s):", - result.total_advisory_count, advisory_word, result.affected_package_count + console.write( + &format!( + "Found {} security vulnerability {} affecting {} package(s):", + result.total_advisory_count, advisory_word, result.affected_package_count + ), + Verbosity::Normal, ); - eprintln!(); + console.write("", Verbosity::Normal); for advisories in result.advisories.values() { for matched in advisories { let adv = &matched.advisory; - eprintln!("Package: {}", adv.package_name); - eprintln!("Version: {}", matched.installed_version); - eprintln!("Severity: {}", adv.severity.as_deref().unwrap_or("")); - eprintln!("Advisory ID: {}", adv.advisory_id); - eprintln!("CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE")); - eprintln!("Title: {}", adv.title); - eprintln!("URL: {}", adv.link.as_deref().unwrap_or("")); - eprintln!("Affected versions: {}", adv.affected_versions); - eprintln!("Reported at: {}", adv.reported_at); - eprintln!("--------"); + console.write(&format!("Package: {}", adv.package_name), Verbosity::Normal); + console.write( + &format!("Version: {}", matched.installed_version), + Verbosity::Normal, + ); + console.write( + &format!("Severity: {}", adv.severity.as_deref().unwrap_or("")), + Verbosity::Normal, + ); + console.write( + &format!("Advisory ID: {}", adv.advisory_id), + Verbosity::Normal, + ); + console.write( + &format!("CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE")), + Verbosity::Normal, + ); + console.write(&format!("Title: {}", adv.title), Verbosity::Normal); + console.write( + &format!("URL: {}", adv.link.as_deref().unwrap_or("")), + Verbosity::Normal, + ); + console.write( + &format!("Affected versions: {}", adv.affected_versions), + Verbosity::Normal, + ); + console.write( + &format!("Reported at: {}", adv.reported_at), + Verbosity::Normal, + ); + console.write("--------", Verbosity::Normal); } } } for pkg in &result.abandoned { match &pkg.replacement { - Some(repl) => eprintln!( - "{} ({}) is abandoned. Use {} instead.", - pkg.name, pkg.version, repl + Some(repl) => console.write( + &format!( + "{} ({}) is abandoned. Use {} instead.", + pkg.name, pkg.version, repl + ), + Verbosity::Normal, ), - None => eprintln!( - "{} ({}) is abandoned. No replacement was suggested.", - pkg.name, pkg.version + None => console.write( + &format!( + "{} ({}) is abandoned. No replacement was suggested.", + pkg.name, pkg.version + ), + Verbosity::Normal, ), } } } -fn render_json(result: &AuditResult) -> anyhow::Result<()> { +fn render_json( + result: &AuditResult, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { // Build advisories map: package_name -> [advisory objects] let mut advisories_map: serde_json::Map<String, serde_json::Value> = serde_json::Map::new(); for (pkg_name, advisories) in &result.advisories { @@ -594,35 +653,44 @@ fn render_json(result: &AuditResult) -> anyhow::Result<()> { "abandoned": abandoned_map, }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); Ok(()) } -fn render_summary(result: &AuditResult) { +fn render_summary(result: &AuditResult, console: &mozart_core::console::Console) { if result.total_advisory_count == 0 { - eprintln!("No security vulnerability advisories found."); + console.info("No security vulnerability advisories found."); } else { let advisory_word = if result.total_advisory_count == 1 { "advisory" } else { "advisories" }; - eprintln!( - "Found {} security vulnerability {} affecting {} package(s).", - result.total_advisory_count, advisory_word, result.affected_package_count + console.write( + &format!( + "Found {} security vulnerability {} affecting {} package(s).", + result.total_advisory_count, advisory_word, result.affected_package_count + ), + Verbosity::Normal, ); - eprintln!("Run \"mozart audit\" for a full list of advisories."); + console.info("Run \"mozart audit\" for a full list of advisories."); } for pkg in &result.abandoned { match &pkg.replacement { - Some(repl) => eprintln!( - "{} ({}) is abandoned. Use {} instead.", - pkg.name, pkg.version, repl + Some(repl) => console.write( + &format!( + "{} ({}) is abandoned. Use {} instead.", + pkg.name, pkg.version, repl + ), + Verbosity::Normal, ), - None => eprintln!( - "{} ({}) is abandoned. No replacement was suggested.", - pkg.name, pkg.version + None => console.write( + &format!( + "{} ({}) is abandoned. No replacement was suggested.", + pkg.name, pkg.version + ), + Verbosity::Normal, ), } } @@ -682,14 +750,19 @@ mod tests { // ── filter_advisories ──────────────────────────────────────────────────── + fn make_console() -> mozart_core::console::Console { + mozart_core::console::Console::new(0, false, false, false, false) + } + #[test] fn test_filter_advisories_matching() { + let console = make_console(); let advisory = make_advisory("PKSA-0001", "vendor/pkg", ">=1.0,<2.0", None); let mut all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); all.insert("vendor/pkg".to_string(), vec![advisory]); let packages = vec![make_pkg("vendor/pkg", "1.5.0", Some("1.5.0.0"))]; - let result = filter_advisories(&all, &packages, &[]); + let result = filter_advisories(&all, &packages, &[], &console); assert_eq!(result.len(), 1); assert_eq!(result["vendor/pkg"].len(), 1); @@ -697,30 +770,33 @@ mod tests { #[test] fn test_filter_advisories_not_matching() { + let console = make_console(); let advisory = make_advisory("PKSA-0002", "vendor/pkg", ">=1.0,<2.0", None); let mut all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); all.insert("vendor/pkg".to_string(), vec![advisory]); let packages = vec![make_pkg("vendor/pkg", "2.0.0", Some("2.0.0.0"))]; - let result = filter_advisories(&all, &packages, &[]); + let result = filter_advisories(&all, &packages, &[], &console); assert!(result.is_empty()); } #[test] fn test_filter_advisories_ignore_severity() { + let console = make_console(); let advisory = make_advisory("PKSA-0003", "vendor/pkg", ">=1.0,<2.0", Some("low")); let mut all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); all.insert("vendor/pkg".to_string(), vec![advisory]); let packages = vec![make_pkg("vendor/pkg", "1.5.0", Some("1.5.0.0"))]; - let result = filter_advisories(&all, &packages, &["low".to_string()]); + let result = filter_advisories(&all, &packages, &["low".to_string()], &console); assert!(result.is_empty()); } #[test] fn test_filter_advisories_multiple_packages() { + let console = make_console(); let adv1 = make_advisory("PKSA-0004", "vendor/pkg1", ">=1.0,<2.0", None); let adv2 = make_advisory("PKSA-0005", "vendor/pkg2", ">=3.0,<4.0", None); let mut all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); @@ -731,7 +807,7 @@ mod tests { make_pkg("vendor/pkg1", "1.5.0", Some("1.5.0.0")), make_pkg("vendor/pkg2", "3.5.0", Some("3.5.0.0")), ]; - let result = filter_advisories(&all, &packages, &[]); + let result = filter_advisories(&all, &packages, &[], &console); assert_eq!(result.len(), 2); assert_eq!(result["vendor/pkg1"].len(), 1); @@ -740,6 +816,7 @@ mod tests { #[test] fn test_filter_advisories_complex_constraint() { + let console = make_console(); // OR constraint: >=1.0,<1.5|>=2.0,<2.3 let advisory = make_advisory("PKSA-0006", "vendor/pkg", ">=1.0,<1.5|>=2.0,<2.3", None); let mut all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); @@ -747,16 +824,17 @@ mod tests { // 2.1.0 is in [2.0, 2.3) so should match let packages = vec![make_pkg("vendor/pkg", "2.1.0", Some("2.1.0.0"))]; - let result = filter_advisories(&all, &packages, &[]); + let result = filter_advisories(&all, &packages, &[], &console); assert_eq!(result.len(), 1); } #[test] fn test_filter_advisories_no_advisories() { + let console = make_console(); let all: BTreeMap<String, Vec<SecurityAdvisory>> = BTreeMap::new(); let packages = vec![make_pkg("vendor/pkg", "1.5.0", Some("1.5.0.0"))]; - let result = filter_advisories(&all, &packages, &[]); + let result = filter_advisories(&all, &packages, &[], &console); assert!(result.is_empty()); } @@ -1047,18 +1125,20 @@ mod tests { }; // Should not panic - render_json(&result).unwrap(); + let console = make_console(); + render_json(&result, &console).unwrap(); } #[test] fn test_render_json_empty() { + let console = make_console(); let result = AuditResult { advisories: BTreeMap::new(), abandoned: vec![], affected_package_count: 0, total_advisory_count: 0, }; - render_json(&result).unwrap(); + render_json(&result, &console).unwrap(); } // ── argument validation ─────────────────────────────────────────────────── diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs index 3946acd..905c3f8 100644 --- a/crates/mozart/src/commands/browse.rs +++ b/crates/mozart/src/commands/browse.rs @@ -38,7 +38,7 @@ pub async fn execute( "No composer.json found in the current directory and no package specified." ); } - eprintln!("No package specified, opening homepage for the root package"); + console.info("No package specified, opening homepage for the root package"); let root = mozart_core::package::read_from_file(&composer_json)?; vec![root.name.clone()] } else { @@ -56,14 +56,14 @@ pub async fn execute( mozart_core::console::Verbosity::Normal, ); } else { - open_browser(&url)?; + open_browser(&url, console)?; } } ResolveResult::NotFound => { - eprintln!( - "{}", - console_format!("<warning>Package {} not found</warning>", package_name) - ); + console.info(&console_format!( + "<warning>Package {} not found</warning>", + package_name + )); exit_code = 1; } ResolveResult::NoUrl => { @@ -72,7 +72,7 @@ pub async fn execute( } else { format!("Invalid or missing repository URL for {}", package_name) }; - eprintln!("{}", console_format!("<warning>{}</warning>", msg)); + console.info(&console_format!("<warning>{}</warning>", msg)); exit_code = 1; } } @@ -255,7 +255,7 @@ fn is_valid_url(url: &str) -> bool { } } -fn open_browser(url: &str) -> anyhow::Result<()> { +fn open_browser(url: &str, console: &mozart_core::console::Console) -> anyhow::Result<()> { #[cfg(target_os = "macos")] { Command::new("open").arg(url).status()?; @@ -290,10 +290,10 @@ fn open_browser(url: &str) -> anyhow::Result<()> { Command::new("open").arg(url).status()?; return Ok(()); } - eprintln!( + console.info(&format!( "No suitable browser opener found. Please open manually: {}", url - ); + )); Ok(()) } } diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs index 7e74f80..957120d 100644 --- a/crates/mozart/src/commands/bump.rs +++ b/crates/mozart/src/commands/bump.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use std::collections::HashMap; use std::path::PathBuf; @@ -154,34 +155,34 @@ pub async fn execute( let total_changes = require_changes.len() + require_dev_changes.len(); if total_changes == 0 { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>No requirements to update in {}.</info>", composer_json_path.display() - ) + ), + Verbosity::Normal, ); return Ok(()); } if args.dry_run { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>{} would be updated with:</info>", composer_json_path.display() - ) + ), + Verbosity::Normal, ); for (name, _old, new) in &require_changes { - println!( - "{}", - console_format!("<info> - require.{name}: {new}</info>") + console.write_stdout( + &console_format!("<info> - require.{name}: {new}</info>"), + Verbosity::Normal, ); } for (name, _old, new) in &require_dev_changes { - println!( - "{}", - console_format!("<info> - require-dev.{name}: {new}</info>") + console.write_stdout( + &console_format!("<info> - require-dev.{name}: {new}</info>"), + Verbosity::Normal, ); } // Return exit code 1 when dry-run detects changes (useful for CI to detect un-bumped constraints) @@ -209,12 +210,12 @@ pub async fn execute( updated_lock.content_hash = new_hash; updated_lock.write_to_file(&lock_path)?; - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>{} has been updated ({total_changes} changes).</info>", composer_json_path.display() - ) + ), + Verbosity::Normal, ); Ok(()) diff --git a/crates/mozart/src/commands/check_platform_reqs.rs b/crates/mozart/src/commands/check_platform_reqs.rs index b3197c1..d3ecacd 100644 --- a/crates/mozart/src/commands/check_platform_reqs.rs +++ b/crates/mozart/src/commands/check_platform_reqs.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::{Console, Verbosity}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -83,7 +84,10 @@ pub async fn execute( if requirements.is_empty() { // No platform requirements to check if format == "json" { - println!("{}", serde_json::to_string_pretty(&serde_json::json!([]))?); + console.write_stdout( + &serde_json::to_string_pretty(&serde_json::json!([]))?, + Verbosity::Normal, + ); } return Ok(()); } @@ -99,8 +103,8 @@ pub async fn execute( // Render output match format { - "json" => render_json(&results)?, - _ => render_text(&results), + "json" => render_json(&results, console)?, + _ => render_text(&results, console), } if exit_code != 0 { @@ -353,7 +357,7 @@ fn determine_exit_code(results: &[CheckResult]) -> i32 { // ─── Rendering ─────────────────────────────────────────────────────────────── -fn render_text(results: &[CheckResult]) { +fn render_text(results: &[CheckResult], console: &Console) { if results.is_empty() { return; } @@ -370,11 +374,14 @@ fn render_text(results: &[CheckResult]) { match result.status { CheckStatus::Success => { - println!( - "{} {} {}", - mozart_core::console::info(&padded_name), - mozart_core::console::comment(&padded_version), - mozart_core::console::info("success"), + console.write_stdout( + &format!( + "{} {} {}", + mozart_core::console::info(&padded_name), + mozart_core::console::comment(&padded_version), + mozart_core::console::info("success"), + ), + Verbosity::Normal, ); } CheckStatus::Failed => { @@ -383,13 +390,16 @@ fn render_text(results: &[CheckResult]) { .as_ref() .map(|(c, p)| (c.as_str(), p.as_str())) .unwrap_or(("", "")); - println!( - "{} {} {} requires {} ({})", - mozart_core::console::comment(&padded_name), - mozart_core::console::comment(&padded_version), - mozart_core::console::error("failed"), - provider, - constraint, + console.write_stdout( + &format!( + "{} {} {} requires {} ({})", + mozart_core::console::comment(&padded_name), + mozart_core::console::comment(&padded_version), + mozart_core::console::error("failed"), + provider, + constraint, + ), + Verbosity::Normal, ); } CheckStatus::Missing => { @@ -398,20 +408,23 @@ fn render_text(results: &[CheckResult]) { .as_ref() .map(|(c, p)| (c.as_str(), p.as_str())) .unwrap_or(("*", "")); - println!( - "{} {} {} requires {} ({})", - mozart_core::console::comment(&padded_name), - mozart_core::console::comment(&padded_version), - mozart_core::console::error("missing"), - provider, - constraint, + console.write_stdout( + &format!( + "{} {} {} requires {} ({})", + mozart_core::console::comment(&padded_name), + mozart_core::console::comment(&padded_version), + mozart_core::console::error("missing"), + provider, + constraint, + ), + Verbosity::Normal, ); } } } } -fn render_json(results: &[CheckResult]) -> anyhow::Result<()> { +fn render_json(results: &[CheckResult], console: &Console) -> anyhow::Result<()> { let json_results: Vec<serde_json::Value> = results .iter() .map(|r| { @@ -437,7 +450,10 @@ fn render_json(results: &[CheckResult]) -> anyhow::Result<()> { }) .collect(); - println!("{}", serde_json::to_string_pretty(&json_results)?); + console.write_stdout( + &serde_json::to_string_pretty(&json_results)?, + Verbosity::Normal, + ); Ok(()) } diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index df31f8d..3947afd 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -504,7 +504,7 @@ fn load_config_section( pub async fn execute( args: &ConfigArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // 1. Handle --editor mode if args.editor { @@ -526,7 +526,7 @@ pub async fn execute( } // 4b. Read mode - execute_read(args, cli, &config_file_path) + execute_read(args, cli, &config_file_path, console) } // ─── execute_editor() ──────────────────────────────────────────────────────── @@ -895,6 +895,7 @@ fn execute_read( args: &ConfigArgs, cli: &super::Cli, config_file_path: &Path, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Build the effective config for config-section keys. let mut config = ComposerConfig::defaults(); @@ -934,20 +935,23 @@ fn execute_read( if args.list { for (key, value) in &config.values { - println!("[{}] {}", key, render_value(value)); + console.write_stdout( + &format!("[{}] {}", key, render_value(value)), + mozart_core::console::Verbosity::Quiet, + ); } return Ok(()); } match &args.setting_key { None => { - eprintln!( + console.error(&format!( "{}", mozart_core::console::error( "No command specified. Use --list to show all config values, \ - or provide a setting key." + or provide a setting key." ) - ); + )); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); @@ -959,7 +963,10 @@ fn execute_read( if let Some(repos) = raw["repositories"].as_array() { for entry in repos { if entry.get("name").and_then(|n| n.as_str()) == Some(repo_name) { - println!("{}", render_value(entry)); + console.write_stdout( + &render_value(entry), + mozart_core::console::Verbosity::Quiet, + ); return Ok(()); } } @@ -971,7 +978,7 @@ fn execute_read( if key.starts_with("extra.") || key.starts_with("suggest.") { let raw = read_json_file(config_file_path, args.global)?; if let Some(v) = get_nested(&raw, key) { - println!("{}", render_value(v)); + console.write_stdout(&render_value(v), mozart_core::console::Verbosity::Quiet); return Ok(()); } return Err(anyhow!("Setting \"{}\" does not exist.", key)); @@ -981,7 +988,7 @@ fn execute_read( if CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str()) { let raw = read_json_file(config_file_path, args.global)?; if let Some(v) = raw.get(key.as_str()) { - println!("{}", render_value(v)); + console.write_stdout(&render_value(v), mozart_core::console::Verbosity::Quiet); return Ok(()); } // Fall through to config section lookup @@ -990,7 +997,8 @@ fn execute_read( // 4. Standard config key lookup match config.get(key) { Some(value) => { - println!("{}", render_value(value)); + console + .write_stdout(&render_value(value), mozart_core::console::Verbosity::Quiet); } None => { return Err(anyhow!("Setting \"{}\" does not exist.", key)); diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index da8108c..83ad21d 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -135,15 +135,17 @@ fn dir_from_package_name(package_name: &str) -> &str { } /// Remove VCS metadata directories from the target directory. -fn remove_vcs_metadata(target_dir: &Path) -> anyhow::Result<()> { +fn remove_vcs_metadata( + target_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { for vcs_dir in VCS_DIRS { let path = target_dir.join(vcs_dir); if path.exists() { std::fs::remove_dir_all(&path)?; - eprintln!( - "{}", - console_format!("<comment>Removed VCS metadata directory: {vcs_dir}</comment>") - ); + console.info(&console_format!( + "<comment>Removed VCS metadata directory: {vcs_dir}</comment>" + )); } } Ok(()) @@ -344,7 +346,7 @@ pub async fn execute( // Default (neither flag): remove. let vcs_removed = args.remove_vcs || !args.keep_vcs; if vcs_removed { - remove_vcs_metadata(&target_dir)?; + remove_vcs_metadata(&target_dir, console)?; } // --- Step 6: Read composer.json and optionally install dependencies --- diff --git a/crates/mozart/src/commands/dependency.rs b/crates/mozart/src/commands/dependency.rs index 0184f53..6dcaec8 100644 --- a/crates/mozart/src/commands/dependency.rs +++ b/crates/mozart/src/commands/dependency.rs @@ -519,9 +519,12 @@ fn sample_versions_from_constraint( /// Print results as a flat table. /// /// Columns: package name | version | link description | link constraint -pub fn print_table(results: &[DependencyResult]) { +pub fn print_table(results: &[DependencyResult], console: &mozart_core::console::Console) { if results.is_empty() { - println!("{}", mozart_core::console::info("No relationships found.")); + console.write_stdout( + &format!("{}", mozart_core::console::info("No relationships found.")), + mozart_core::console::Verbosity::Normal, + ); return; } @@ -551,15 +554,18 @@ pub fn print_table(results: &[DependencyResult]) { if !seen.insert(key) { continue; } - println!( - "{:<name_w$} {:<ver_w$} {:<desc_w$} {}", - mozart_core::console::info(&r.package_name), - mozart_core::console::comment(&r.package_version), - r.link_description, - mozart_core::console::comment(&r.link_constraint), - name_w = name_w, - ver_w = ver_w, - desc_w = desc_w, + console.write_stdout( + &format!( + "{:<name_w$} {:<ver_w$} {:<desc_w$} {}", + mozart_core::console::info(&r.package_name), + mozart_core::console::comment(&r.package_version), + r.link_description, + mozart_core::console::comment(&r.link_constraint), + name_w = name_w, + ver_w = ver_w, + desc_w = desc_w, + ), + mozart_core::console::Verbosity::Normal, ); } } @@ -573,9 +579,16 @@ pub fn print_table(results: &[DependencyResult]) { /// └─ vendor/b 2.0.0 requires ^2.0 /// └─ root/project ROOT requires ^2.0 /// ``` -pub fn print_tree(results: &[DependencyResult], depth: usize) { +pub fn print_tree( + results: &[DependencyResult], + depth: usize, + console: &mozart_core::console::Console, +) { if results.is_empty() && depth == 0 { - println!("{}", mozart_core::console::info("No relationships found.")); + console.write_stdout( + &format!("{}", mozart_core::console::info("No relationships found.")), + mozart_core::console::Verbosity::Normal, + ); return; } @@ -584,17 +597,20 @@ pub fn print_tree(results: &[DependencyResult], depth: usize) { let is_last = i + 1 == count; let prefix = tree_prefix(depth, is_last); - println!( - "{}{:<} {} {} {}", - prefix, - mozart_core::console::info(&r.package_name), - mozart_core::console::comment(&r.package_version), - r.link_description, - mozart_core::console::comment(&r.link_constraint), + console.write_stdout( + &format!( + "{}{:<} {} {} {}", + prefix, + mozart_core::console::info(&r.package_name), + mozart_core::console::comment(&r.package_version), + r.link_description, + mozart_core::console::comment(&r.link_constraint), + ), + mozart_core::console::Verbosity::Normal, ); if !r.children.is_empty() { - print_tree(&r.children, depth + 1); + print_tree(&r.children, depth + 1, console); } } } @@ -777,11 +793,13 @@ mod tests { #[test] fn test_print_table_empty() { - print_table(&[]); + let console = mozart_core::console::Console::new(0, false, false, false, false); + print_table(&[], &console); } #[test] fn test_print_table_single() { + let console = mozart_core::console::Console::new(0, false, false, false, false); let results = vec![DependencyResult { package_name: "vendor/a".to_string(), package_version: "1.0.0".to_string(), @@ -790,16 +808,18 @@ mod tests { link_constraint: "^2.0".to_string(), children: vec![], }]; - print_table(&results); + print_table(&results, &console); } #[test] fn test_print_tree_empty() { - print_tree(&[], 0); + let console = mozart_core::console::Console::new(0, false, false, false, false); + print_tree(&[], 0, &console); } #[test] fn test_print_tree_nested() { + let console = mozart_core::console::Console::new(0, false, false, false, false); let results = vec![DependencyResult { package_name: "vendor/a".to_string(), package_version: "1.0.0".to_string(), @@ -815,6 +835,6 @@ mod tests { children: vec![], }], }]; - print_tree(&results, 0); + print_tree(&results, 0, &console); } } diff --git a/crates/mozart/src/commands/depends.rs b/crates/mozart/src/commands/depends.rs index c65f775..514ce11 100644 --- a/crates/mozart/src/commands/depends.rs +++ b/crates/mozart/src/commands/depends.rs @@ -62,19 +62,19 @@ pub async fn execute( let results = super::dependency::get_dependents(&packages, &needles, None, false, recursive)?; if results.is_empty() { - eprintln!( + console.info(&format!( "There is no installed package depending on \"{}\"", args.package - ); + )); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); } if args.tree { - super::dependency::print_tree(&results, 0); + super::dependency::print_tree(&results, 0, console); } else { - super::dependency::print_table(&results); + super::dependency::print_table(&results, console); } Ok(()) diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs index 0c027d7..bb6e886 100644 --- a/crates/mozart/src/commands/diagnose.rs +++ b/crates/mozart/src/commands/diagnose.rs @@ -1,5 +1,6 @@ use clap::Args; use colored::Colorize; +use mozart_core::console::{Console, Verbosity}; use std::path::{Path, PathBuf}; #[derive(Args)] @@ -25,32 +26,42 @@ enum CheckResult { /// Print "Checking {label}: OK/WARNING/FAIL/SKIP" and ratchet exit_code. /// /// Exit code ratchet: Warning → 1 (if currently 0), Fail → 2 (always overrides 1). -fn print_check(label: &str, result: &CheckResult, exit_code: &mut i32) { +fn print_check(label: &str, result: &CheckResult, exit_code: &mut i32, console: &Console) { match result { CheckResult::Ok(detail) => { let ok_str = "OK".green().bold(); match detail { - Some(d) => println!("Checking {label}: {ok_str} ({d})"), - None => println!("Checking {label}: {ok_str}"), + Some(d) => { + console.write_stdout( + &format!("Checking {label}: {ok_str} ({d})"), + Verbosity::Normal, + ); + } + None => { + console.write_stdout(&format!("Checking {label}: {ok_str}"), Verbosity::Normal); + } } } CheckResult::Warning(msg) => { let warn_str = "WARNING".yellow().bold(); - println!("Checking {label}: {warn_str}"); - println!(" {}", msg.yellow()); + console.write_stdout(&format!("Checking {label}: {warn_str}"), Verbosity::Normal); + console.write_stdout(&format!(" {}", msg.yellow()), Verbosity::Normal); if *exit_code < 1 { *exit_code = 1; } } CheckResult::Fail(msg) => { let fail_str = "FAIL".red().bold(); - println!("Checking {label}: {fail_str}"); - println!(" {}", msg.red()); + console.write_stdout(&format!("Checking {label}: {fail_str}"), Verbosity::Normal); + console.write_stdout(&format!(" {}", msg.red()), Verbosity::Normal); *exit_code = 2; } CheckResult::Skip(reason) => { let skip_str = "SKIP".cyan().bold(); - println!("Checking {label}: {skip_str} ({reason})"); + console.write_stdout( + &format!("Checking {label}: {skip_str} ({reason})"), + Verbosity::Normal, + ); } CheckResult::Info(_) => { // Info results are not "checked" — use print_info_line instead. @@ -59,9 +70,9 @@ fn print_check(label: &str, result: &CheckResult, exit_code: &mut i32) { } /// Print an informational line (not a check result). -fn print_info_line(result: &CheckResult) { +fn print_info_line(result: &CheckResult, console: &Console) { if let CheckResult::Info(msg) = result { - println!("{msg}"); + console.write_stdout(msg, Verbosity::Normal); } } @@ -389,7 +400,7 @@ fn check_cache_dir(cache_dir: &Path) -> CheckResult { pub async fn execute( _args: &DiagnoseArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &Console, ) -> anyhow::Result<()> { let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), @@ -413,8 +424,8 @@ pub async fn execute( }; // 1. Mozart version info - print_info_line(&check_version()); - println!(); + print_info_line(&check_version(), console); + console.write_stdout("", Verbosity::Normal); // 2. HTTPS connectivity to Packagist let https_result = check_http_connectivity("https://repo.packagist.org/packages.json").await; @@ -422,6 +433,7 @@ pub async fn execute( "https connectivity to packagist", &https_result, &mut exit_code, + console, ); // 3. HTTP connectivity to Packagist @@ -430,27 +442,38 @@ pub async fn execute( "http connectivity to packagist", &http_result, &mut exit_code, + console, ); // 4. GitHub API connectivity let github_result = check_github_api().await; - print_check("github.com connectivity", &github_result, &mut exit_code); + print_check( + "github.com connectivity", + &github_result, + &mut exit_code, + console, + ); // 5. HTTP proxy config let proxy_result = check_http_proxy(); - print_check("http proxy", &proxy_result, &mut exit_code); + print_check("http proxy", &proxy_result, &mut exit_code, console); // 6. composer.json validation let composer_json_result = check_composer_json(&working_dir); - print_check("composer.json", &composer_json_result, &mut exit_code); + print_check( + "composer.json", + &composer_json_result, + &mut exit_code, + console, + ); // 7. composer.lock freshness let lock_result = check_composer_lock(&working_dir); - print_check("composer.lock", &lock_result, &mut exit_code); + print_check("composer.lock", &lock_result, &mut exit_code, console); // 8. Git availability let git_result = check_git(); - print_check("git", &git_result, &mut exit_code); + print_check("git", &git_result, &mut exit_code, console); // 9. Disk space — working directory let disk_wd_result = check_disk_space(&working_dir, "working directory"); @@ -458,6 +481,7 @@ pub async fn execute( "disk free space (working directory)", &disk_wd_result, &mut exit_code, + console, ); // 9b. Disk space — cache directory @@ -466,22 +490,32 @@ pub async fn execute( "disk free space (cache directory)", &disk_cache_result, &mut exit_code, + console, ); // 10. Cache directory status let cache_result = check_cache_dir(&cache_dir); - print_check("cache directory", &cache_result, &mut exit_code); + print_check("cache directory", &cache_result, &mut exit_code, console); - println!(); + console.write_stdout("", Verbosity::Normal); if exit_code == 0 { - println!("{}", "No issues found.".green()); + console.write_stdout( + &format!("{}", "No issues found.".green()), + Verbosity::Normal, + ); } else if exit_code == 1 { - println!( - "{}", - "Some warnings were found. See above for details.".yellow() + console.write_stdout( + &format!( + "{}", + "Some warnings were found. See above for details.".yellow() + ), + Verbosity::Normal, ); } else { - println!("{}", "Some errors were found. See above for details.".red()); + console.write_stdout( + &format!("{}", "Some errors were found. See above for details.".red()), + Verbosity::Normal, + ); } if exit_code != 0 { @@ -666,10 +700,11 @@ mod tests { #[test] fn test_check_result_exit_code_ratcheting() { + let console = Console::new(0, false, false, false, false); let mut exit_code = 0i32; // Ok does not change exit code - print_check("label", &CheckResult::Ok(None), &mut exit_code); + print_check("label", &CheckResult::Ok(None), &mut exit_code, &console); assert_eq!(exit_code, 0); // Warning raises to 1 @@ -677,11 +712,12 @@ mod tests { "label", &CheckResult::Warning("warn".to_string()), &mut exit_code, + &console, ); assert_eq!(exit_code, 1); // Another Ok does not lower from 1 - print_check("label", &CheckResult::Ok(None), &mut exit_code); + print_check("label", &CheckResult::Ok(None), &mut exit_code, &console); assert_eq!(exit_code, 1); // Fail raises to 2 @@ -689,6 +725,7 @@ mod tests { "label", &CheckResult::Fail("fail".to_string()), &mut exit_code, + &console, ); assert_eq!(exit_code, 2); @@ -697,6 +734,7 @@ mod tests { "label", &CheckResult::Warning("another warn".to_string()), &mut exit_code, + &console, ); assert_eq!(exit_code, 2); } diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index ef96939..350ff5c 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -21,8 +21,9 @@ pub struct ExecArgs { pub async fn execute( args: &ExecArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { + use mozart_core::console::Verbosity; let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), None => std::env::current_dir()?, @@ -38,15 +39,21 @@ pub async fn execute( bin_dir.display() ); } - println!( - "{}", - console_format!("<comment>Available binaries:</comment>") + console.write_stdout( + &console_format!("<comment>Available binaries:</comment>"), + Verbosity::Normal, ); for (name, is_local) in &binaries { if *is_local { - println!("{}", console_format!("<info>- {} (local)</info>", name)); + console.write_stdout( + &console_format!("<info>- {} (local)</info>", name), + Verbosity::Normal, + ); } else { - println!("{}", console_format!("<info>- {}</info>", name)); + console.write_stdout( + &console_format!("<info>- {}</info>", name), + Verbosity::Normal, + ); } } return Ok(()); diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs index 15ec531..0b5ab9e 100644 --- a/crates/mozart/src/commands/init.rs +++ b/crates/mozart/src/commands/init.rs @@ -348,7 +348,7 @@ async fn build_interactive( let mut require = parse_requirements(&args.require)?; let interactive_require = - interactive_search_packages("require", &require, preferred_stability).await?; + interactive_search_packages("require", &require, preferred_stability, console).await?; for (name, constraint) in interactive_require { require.insert(name, constraint); } @@ -367,7 +367,8 @@ async fn build_interactive( .map(|(k, v)| (k.clone(), v.clone())) .collect(); let interactive_dev = - interactive_search_packages("require-dev", &all_required, preferred_stability).await?; + interactive_search_packages("require-dev", &all_required, preferred_stability, console) + .await?; for (name, constraint) in interactive_dev { require_dev.insert(name, constraint); } @@ -417,6 +418,7 @@ async fn interactive_search_packages( label: &str, already_required: &BTreeMap<String, String>, preferred_stability: Stability, + console: &console::Console, ) -> anyhow::Result<BTreeMap<String, String>> { let stdin = std::io::stdin(); let mut selected: BTreeMap<String, String> = BTreeMap::new(); @@ -442,10 +444,9 @@ async fn interactive_search_packages( let (results, total) = match packagist::search_packages(&query, None).await { Ok(r) => r, Err(e) => { - eprintln!( - "{}", - console_format!("<warning>Search failed: {e}. Try again.</warning>") - ); + console.info(&console_format!( + "<warning>Search failed: {e}. Try again.</warning>" + )); continue; } }; @@ -461,21 +462,18 @@ async fn interactive_search_packages( .collect(); if filtered.is_empty() { - eprintln!( - "{}", - console_format!( - "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" - ) - ); + console.info(&console_format!( + "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" + )); continue; } - eprintln!( + console.info(&format!( "\nFound {} package{} for \"{}\":", filtered.len(), if filtered.len() == 1 { "" } else { "s" }, query, - ); + )); let name_width = filtered.iter().map(|r| r.name.len()).max().unwrap_or(0); for (idx, result) in filtered.iter().enumerate() { @@ -484,15 +482,15 @@ async fn interactive_search_packages( } else { format!(" — {}", result.description) }; - eprintln!( + console.info(&format!( " [{idx}] {:<width$}{desc}", result.name, idx = idx + 1, width = name_width, - ); + )); } - eprintln!(" [0] Search again / enter full package name"); - eprintln!(); + console.info(" [0] Search again / enter full package name"); + console.info(""); // Ask user to pick eprint!("Enter package # or name (leave empty to finish): "); @@ -518,10 +516,9 @@ async fn interactive_search_packages( } else if num <= filtered.len() { filtered[num - 1].name.to_lowercase() } else { - eprintln!( - "{}", - console_format!("<warning>Invalid selection: {num}</warning>") - ); + console.info(&console_format!( + "<warning>Invalid selection: {num}</warning>" + )); continue; } } else { @@ -533,25 +530,21 @@ async fn interactive_search_packages( match validation::parse_require_string(&package_name) { Ok((n, v)) => (n.to_lowercase(), v), Err(e) => { - eprintln!("{}", console_format!("<warning>Invalid: {e}</warning>")); + console.info(&console_format!("<warning>Invalid: {e}</warning>")); continue; } } } else { if !validation::validate_package_name(&package_name) { - eprintln!( - "{}", - console_format!("<warning>Invalid package name: \"{package_name}\"</warning>") - ); + console.info(&console_format!( + "<warning>Invalid package name: \"{package_name}\"</warning>" + )); continue; } - eprintln!( - "{}", - console_format!( - "<info>Using version constraint for {package_name} from Packagist...</info>" - ) - ); + console.info(&console_format!( + "<info>Using version constraint for {package_name} from Packagist...</info>" + )); match packagist::fetch_package_versions(&package_name, None).await { Ok(versions) => { @@ -563,33 +556,24 @@ async fn interactive_search_packages( &best.version_normalized, stability, ); - eprintln!( - "{}", - console_format!( - "<info>Using version {c} for {package_name}</info>" - ) - ); + console.info(&console_format!( + "<info>Using version {c} for {package_name}</info>" + )); (package_name, c) } None => { - eprintln!( - "{}", - console_format!( - "<warning>Could not find a version of \"{package_name}\" matching \ - your minimum-stability. Try specifying it explicitly.</warning>" - ) - ); + console.info(&console_format!( + "<warning>Could not find a version of \"{package_name}\" matching \ + your minimum-stability. Try specifying it explicitly.</warning>" + )); continue; } } } Err(e) => { - eprintln!( - "{}", - console_format!( - "<warning>Could not fetch versions for \"{package_name}\": {e}</warning>" - ) - ); + console.info(&console_format!( + "<warning>Could not fetch versions for \"{package_name}\": {e}</warning>" + )); continue; } } diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index 97ecd80..6eedd05 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -269,6 +269,7 @@ fn warn_platform_requirements( packages: &[&lockfile::LockedPackage], ignore_platform_reqs: bool, ignore_platform_req: &[String], + console: &console::Console, ) { if ignore_platform_reqs { return; @@ -284,14 +285,14 @@ fn warn_platform_requirements( if is_platform_package(req_name) { let lower = req_name.to_lowercase(); if !ignored_set.contains(&lower) { - eprintln!( + console.info(&format!( "{}", console::warning(&format!( "Platform requirement {req_name} {req_constraint} (required by {}) \ has not been verified. Platform detection is not yet fully implemented.", pkg.name )) - ); + )); } } } @@ -377,6 +378,7 @@ pub async fn install_from_lock( &packages_to_install, config.ignore_platform_reqs, &config.ignore_platform_req, + console, ); // Step 3: Read currently installed packages diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs index d07305e..9a46c94 100644 --- a/crates/mozart/src/commands/licenses.rs +++ b/crates/mozart/src/commands/licenses.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::{Console, Verbosity}; use serde::Serialize; use std::collections::HashSet; use std::path::{Path, PathBuf}; @@ -31,7 +32,7 @@ struct LicenseEntry { pub async fn execute( args: &LicensesArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), @@ -87,9 +88,9 @@ pub async fn execute( // Render output match format { - "json" => render_json(&root_name, &root_version, &root_licenses, &entries)?, - "summary" => render_summary(&entries), - _ => render_text(&root_name, &root_version, &root_licenses, &entries), + "json" => render_json(&root_name, &root_version, &root_licenses, &entries, console)?, + "summary" => render_summary(&entries, console), + _ => render_text(&root_name, &root_version, &root_licenses, &entries, console), } Ok(()) @@ -202,27 +203,35 @@ fn render_text( root_version: &str, root_licenses: &[String], entries: &[LicenseEntry], + console: &Console, ) { let license_display = if root_licenses.is_empty() { "none".to_string() } else { root_licenses.join(", ") }; - // Print root package header - println!("Name: {}", mozart_core::console::comment(root_name)); - println!("Version: {}", mozart_core::console::comment(root_version)); - println!( - "Licenses: {}", - mozart_core::console::comment(&license_display) + console.write_stdout( + &format!("Name: {}", mozart_core::console::comment(root_name)), + Verbosity::Normal, ); - println!("Dependencies:"); - println!(); + console.write_stdout( + &format!("Version: {}", mozart_core::console::comment(root_version)), + Verbosity::Normal, + ); + console.write_stdout( + &format!( + "Licenses: {}", + mozart_core::console::comment(&license_display) + ), + Verbosity::Normal, + ); + console.write_stdout("Dependencies:", Verbosity::Normal); + console.write_stdout("", Verbosity::Normal); if entries.is_empty() { return; } - // Compute column widths (factor in header strings for minimum width) let name_width = entries .iter() .map(|e| e.name.len()) @@ -236,13 +245,15 @@ fn render_text( .unwrap_or(0) .max("Version".len()); - // Print header row - println!( - "{:<nw$} {:<vw$} Licenses", - "Name", - "Version", - nw = name_width, - vw = version_width + console.write_stdout( + &format!( + "{:<nw$} {:<vw$} Licenses", + "Name", + "Version", + nw = name_width, + vw = version_width + ), + Verbosity::Normal, ); for entry in entries { @@ -251,13 +262,16 @@ fn render_text( } else { entry.licenses.join(", ") }; - println!( - "{:<nw$} {:<vw$} {}", - entry.name, - entry.version, - license_str, - nw = name_width, - vw = version_width + console.write_stdout( + &format!( + "{:<nw$} {:<vw$} {}", + entry.name, + entry.version, + license_str, + nw = name_width, + vw = version_width + ), + Verbosity::Normal, ); } } @@ -267,6 +281,7 @@ fn render_json( root_version: &str, root_licenses: &[String], entries: &[LicenseEntry], + console: &Console, ) -> anyhow::Result<()> { let root_license_arr: Vec<serde_json::Value> = root_licenses .iter() @@ -300,21 +315,20 @@ fn render_json( let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut ser = serde_json::Serializer::with_formatter(buf, formatter); output.serialize(&mut ser)?; - println!("{}", String::from_utf8(ser.into_inner())?); + console.write_stdout(&String::from_utf8(ser.into_inner())?, Verbosity::Normal); Ok(()) } -fn render_summary(entries: &[LicenseEntry]) { +fn render_summary(entries: &[LicenseEntry], console: &Console) { let counts = count_licenses(entries); if counts.is_empty() { - println!("No dependencies found."); + console.write_stdout("No dependencies found.", Verbosity::Normal); return; } const COL2_HEADER: &str = "Number of dependencies"; - // Compute column widths (at least as wide as the header strings) let license_width = counts .iter() .map(|(l, _)| l.len()) @@ -328,34 +342,43 @@ fn render_summary(entries: &[LicenseEntry]) { .unwrap_or(0) .max(COL2_HEADER.len()); - // Each column is padded with 1 space on each side, so the border dash count = width + 2 let border_col1 = "-".repeat(license_width + 2); let border_col2 = "-".repeat(count_width + 2); - // Top border - println!(" {} {}", border_col1, border_col2); - // Header row (two leading spaces = one space indent + one space left-padding) - println!( - " {:<lw$} {:<cw$}", - "License", - COL2_HEADER, - lw = license_width, - cw = count_width + console.write_stdout( + &format!(" {} {}", border_col1, border_col2), + Verbosity::Normal, ); - // Mid border - println!(" {} {}", border_col1, border_col2); - // Data rows - for (license, count) in &counts { - println!( + console.write_stdout( + &format!( " {:<lw$} {:<cw$}", - license, - count, + "License", + COL2_HEADER, lw = license_width, cw = count_width + ), + Verbosity::Normal, + ); + console.write_stdout( + &format!(" {} {}", border_col1, border_col2), + Verbosity::Normal, + ); + for (license, count) in &counts { + console.write_stdout( + &format!( + " {:<lw$} {:<cw$}", + license, + count, + lw = license_width, + cw = count_width + ), + Verbosity::Normal, ); } - // Bottom border - println!(" {} {}", border_col1, border_col2); + console.write_stdout( + &format!(" {} {}", border_col1, border_col2), + Verbosity::Normal, + ); } // ─── Tests ─────────────────────────────────────────────────────────────────── diff --git a/crates/mozart/src/commands/outdated.rs b/crates/mozart/src/commands/outdated.rs index 9da8e0e..09e36a9 100644 --- a/crates/mozart/src/commands/outdated.rs +++ b/crates/mozart/src/commands/outdated.rs @@ -100,7 +100,7 @@ struct OutdatedEntry { pub async fn execute( args: &OutdatedArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Validate mutually exclusive level filters let level_count = args.major_only as u8 + args.minor_only as u8 + args.patch_only as u8; @@ -214,8 +214,8 @@ pub async fn execute( // Render output let format = args.format.as_deref().unwrap_or("text"); match format { - "json" => render_json(&entries)?, - _ => render_text(&entries), + "json" => render_json(&entries, console)?, + _ => render_text(&entries, console), } // --strict: exit with code 1 if any outdated packages exist @@ -448,11 +448,13 @@ fn passes_level_filter(args: &OutdatedArgs, current: &str, latest: &str) -> bool // ─── Rendering ─────────────────────────────────────────────────────────────── -fn render_text(entries: &[OutdatedEntry]) { +fn render_text(entries: &[OutdatedEntry], console: &mozart_core::console::Console) { + use mozart_core::console::Verbosity; + if entries.is_empty() { - println!( - "{}", - mozart_core::console::info("All packages are up to date.") + console.write_stdout( + &mozart_core::console::info("All packages are up to date.").to_string(), + Verbosity::Normal, ); return; } @@ -490,17 +492,24 @@ fn render_text(entries: &[OutdatedEntry]) { ), }; - println!( - "{} {} {} {}", - name_str, - mozart_core::console::comment(&cur_col), - lat_str, - entry.description + console.write_stdout( + &format!( + "{} {} {} {}", + name_str, + mozart_core::console::comment(&cur_col), + lat_str, + entry.description + ), + Verbosity::Normal, ); } } -fn render_json(entries: &[OutdatedEntry]) -> anyhow::Result<()> { +fn render_json( + entries: &[OutdatedEntry], + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { + use mozart_core::console::Verbosity; let json_entries: Vec<serde_json::Value> = entries .iter() .map(|entry| { @@ -521,7 +530,7 @@ fn render_json(entries: &[OutdatedEntry]) -> anyhow::Result<()> { .collect(); let output = serde_json::json!({ "installed": json_entries }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); Ok(()) } @@ -745,10 +754,17 @@ mod tests { // ── render_json (smoke test with no network) ────────────────────────────── + fn test_console() -> mozart_core::console::Console { + mozart_core::console::Console { + interactive: false, + verbosity: mozart_core::console::Verbosity::Normal, + decorated: false, + } + } + #[test] fn test_render_json_empty() { - // Should succeed without error on empty input - render_json(&[]).unwrap(); + render_json(&[], &test_console()).unwrap(); } #[test] @@ -771,6 +787,6 @@ mod tests { is_direct: false, }, ]; - render_json(&entries).unwrap(); + render_json(&entries, &test_console()).unwrap(); } } diff --git a/crates/mozart/src/commands/prohibits.rs b/crates/mozart/src/commands/prohibits.rs index f3b7916..8eb4166 100644 --- a/crates/mozart/src/commands/prohibits.rs +++ b/crates/mozart/src/commands/prohibits.rs @@ -71,21 +71,21 @@ pub async fn execute( )?; if results.is_empty() { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>{} {} can be installed.</info>", args.package, args.version - ) + ), + mozart_core::console::Verbosity::Normal, ); return Ok(()); } if args.tree { - super::dependency::print_tree(&results, 0); + super::dependency::print_tree(&results, 0, console); } else { - super::dependency::print_table(&results); + super::dependency::print_table(&results, console); } // Fix #5: Print resolution hint message @@ -114,10 +114,10 @@ pub async fn execute( }) .unwrap_or("update"); - eprintln!( + console.info(&format!( "Not finding what you were looking for? Try calling `composer {} \"{}:{}\" --dry-run` to get another view on the problem.", composer_command, args.package, args.version - ); + )); // Fix #3: Return exit code 1 when prohibitors are found Err(mozart_core::exit_code::bail_silent( diff --git a/crates/mozart/src/commands/reinstall.rs b/crates/mozart/src/commands/reinstall.rs index b38d9f7..abc207f 100644 --- a/crates/mozart/src/commands/reinstall.rs +++ b/crates/mozart/src/commands/reinstall.rs @@ -152,7 +152,7 @@ pub async fn execute( } if selected.is_empty() { - eprintln!("Found no packages to reinstall, aborting."); + console.info("Found no packages to reinstall, aborting."); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); @@ -171,9 +171,15 @@ pub async fn execute( for pkg in &selected { let locked = find_locked_package(&all_locked, &pkg.name); if let Some(lp) = locked { - println!(" - Would reinstall {} ({})", lp.name, lp.version); + console.write_stdout( + &format!(" - Would reinstall {} ({})", lp.name, lp.version), + mozart_core::console::Verbosity::Normal, + ); } else { - println!(" - Would reinstall {} (not found in lock file)", pkg.name); + console.write_stdout( + &format!(" - Would reinstall {} (not found in lock file)", pkg.name), + mozart_core::console::Verbosity::Normal, + ); } } return Ok(()); @@ -242,7 +248,10 @@ pub async fn execute( } if reinstalled_count == 0 { - println!("Nothing was reinstalled."); + console.write_stdout( + "Nothing was reinstalled.", + mozart_core::console::Verbosity::Normal, + ); return Ok(()); } diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index 4808dd0..2529391 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use mozart_core::package; use mozart_core::validation; @@ -148,9 +149,9 @@ pub async fn execute( if args.dev { // Only look in require-dev if raw.require_dev.contains_key(&name) { - println!( - "{}", - console_format!("<info>Removing {name} from require-dev</info>") + console.write_stdout( + &console_format!("<info>Removing {name} from require-dev</info>"), + Verbosity::Normal, ); raw.require_dev.remove(&name); any_removed = true; @@ -160,16 +161,16 @@ pub async fn execute( } else { // Auto-detect: look in require first, then require-dev if raw.require.contains_key(&name) { - println!( - "{}", - console_format!("<info>Removing {name} from require</info>") + console.write_stdout( + &console_format!("<info>Removing {name} from require</info>"), + Verbosity::Normal, ); raw.require.remove(&name); any_removed = true; } else if raw.require_dev.contains_key(&name) { - println!( - "{}", - console_format!("<info>Removing {name} from require-dev</info>") + console.write_stdout( + &console_format!("<info>Removing {name} from require-dev</info>"), + Verbosity::Normal, ); raw.require_dev.remove(&name); any_removed = true; @@ -181,22 +182,22 @@ pub async fn execute( // Step 6: Write updated composer.json (unless --dry-run) if args.dry_run { - println!( - "{}", - console_format!("<comment>Dry run: composer.json not modified.</comment>") + console.write_stdout( + &console_format!("<comment>Dry run: composer.json not modified.</comment>"), + Verbosity::Normal, ); } else if any_removed { package::write_to_file(&raw, &composer_path)?; } - eprintln!("./composer.json has been updated"); + console.info("./composer.json has been updated"); // Step 7: Handle --no-update early return if args.no_update { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<comment>Not updating dependencies, only modifying composer.json.</comment>" - ) + ), + Verbosity::Normal, ); return Ok(()); } diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index 76647b4..0931b66 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -59,16 +59,16 @@ fn resolve_file_path(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result< pub async fn execute( args: &RepositoryArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let action = args.action.as_deref().unwrap_or("list"); match action { - "list" | "ls" | "show" => execute_list(args, cli), + "list" | "ls" | "show" => execute_list(args, cli, console), "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), + "get-url" | "geturl" => execute_get_url(args, cli, console), "disable" => execute_disable(args, cli), "enable" => execute_enable(args, cli), _ => Err(anyhow!( @@ -79,7 +79,11 @@ pub async fn execute( // ─── list ───────────────────────────────────────────────────────────────────── -fn execute_list(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { +fn execute_list( + args: &RepositoryArgs, + cli: &super::Cli, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let file_path = resolve_file_path(args, cli)?; let json = read_json_file(&file_path, args.global)?; @@ -90,7 +94,10 @@ fn execute_list(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { 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"); + console.write_stdout( + &format!("[{key}] disabled"), + mozart_core::console::Verbosity::Normal, + ); if key == "packagist.org" { has_packagist_disable = true; } @@ -108,11 +115,17 @@ fn execute_list(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { .unwrap_or("unknown"); let url = entry.get("url").and_then(|u| u.as_str()).unwrap_or(""); - println!("[{name}] {repo_type} {url}"); + console.write_stdout( + &format!("[{name}] {repo_type} {url}"), + mozart_core::console::Verbosity::Normal, + ); } if !has_packagist_disable { - println!("[packagist.org] composer https://repo.packagist.org"); + console.write_stdout( + "[packagist.org] composer https://repo.packagist.org", + mozart_core::console::Verbosity::Normal, + ); } Ok(()) @@ -251,7 +264,11 @@ fn execute_set_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<() // ─── get-url ────────────────────────────────────────────────────────────────── -fn execute_get_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<()> { +fn execute_get_url( + args: &RepositoryArgs, + cli: &super::Cli, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let name = args .name .as_deref() @@ -267,7 +284,10 @@ fn execute_get_url(args: &RepositoryArgs, cli: &super::Cli) -> anyhow::Result<() let entry = &repos[idx]; match entry.get("url") { Some(url_val) => { - println!("{}", render_value(url_val)); + console.write_stdout( + &render_value(url_val), + mozart_core::console::Verbosity::Normal, + ); Ok(()) } None => Err(anyhow!("The \"{name}\" repository does not have a URL")), diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index 46914cf..2dda5fb 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use mozart_core::package::{self, Stability}; use mozart_core::validation; @@ -135,6 +136,7 @@ async fn interactive_search_packages( already_required: &std::collections::HashSet<String>, preferred_stability: Stability, fixed: bool, + console: &mozart_core::console::Console, ) -> anyhow::Result<Vec<String>> { let stdin = std::io::stdin(); if !stdin.is_terminal() { @@ -168,10 +170,9 @@ async fn interactive_search_packages( let (results, total) = match packagist::search_packages(&query, None).await { Ok(r) => r, Err(e) => { - eprintln!( - "{}", - console_format!("<warning>Search failed: {e}. Try again.</warning>") - ); + console.info(&console_format!( + "<warning>Search failed: {e}. Try again.</warning>" + )); continue; } }; @@ -184,21 +185,18 @@ async fn interactive_search_packages( .collect(); if filtered.is_empty() { - eprintln!( - "{}", - console_format!( - "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" - ) - ); + console.info(&console_format!( + "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" + )); continue; } - eprintln!( + console.info(&format!( "\nFound {} package{} for \"{}\":", filtered.len(), if filtered.len() == 1 { "" } else { "s" }, query - ); + )); let name_width = filtered.iter().map(|r| r.name.len()).max().unwrap_or(0); for (idx, result) in filtered.iter().enumerate() { @@ -207,15 +205,15 @@ async fn interactive_search_packages( } else { format!(" — {}", result.description) }; - eprintln!( + console.info(&format!( " [{idx}] {:<width$}{desc}", result.name, idx = idx + 1, width = name_width, - ); + )); } - eprintln!(" [0] Search again / enter full package name"); - eprintln!(); + console.info(" [0] Search again / enter full package name"); + console.info(""); // Ask user to pick eprint!("Enter package # or name (leave empty to finish): "); @@ -243,10 +241,9 @@ async fn interactive_search_packages( } else if num <= filtered.len() { filtered[num - 1].name.to_lowercase() } else { - eprintln!( - "{}", - console_format!("<warning>Invalid selection: {num}</warning>") - ); + console.info(&console_format!( + "<warning>Invalid selection: {num}</warning>" + )); continue; } } else { @@ -259,25 +256,21 @@ async fn interactive_search_packages( match validation::parse_require_string(&package_name) { Ok((n, v)) => (n.to_lowercase(), v), Err(e) => { - eprintln!("{}", console_format!("<warning>Invalid: {e}</warning>")); + console.info(&console_format!("<warning>Invalid: {e}</warning>")); continue; } } } else { if !validation::validate_package_name(&package_name) { - eprintln!( - "{}", - console_format!("<warning>Invalid package name: \"{package_name}\"</warning>") - ); + console.info(&console_format!( + "<warning>Invalid package name: \"{package_name}\"</warning>" + )); continue; } - eprintln!( - "{}", - console_format!( - "<info>Using version constraint for {package_name} from Packagist...</info>" - ) - ); + console.info(&console_format!( + "<info>Using version constraint for {package_name} from Packagist...</info>" + )); match packagist::fetch_package_versions(&package_name, None).await { Ok(versions) => { @@ -293,32 +286,23 @@ async fn interactive_search_packages( stability, ) }; - eprintln!( - "{}", - console_format!( - "<info>Using version {c} for {package_name}</info>" - ) - ); + console.info(&console_format!( + "<info>Using version {c} for {package_name}</info>" + )); (package_name, c) } None => { - eprintln!( - "{}", - console_format!( - "<warning>Could not find a version of \"{package_name}\" matching your minimum-stability. Try specifying it explicitly.</warning>" - ) - ); + console.info(&console_format!( + "<warning>Could not find a version of \"{package_name}\" matching your minimum-stability. Try specifying it explicitly.</warning>" + )); continue; } } } Err(e) => { - eprintln!( - "{}", - console_format!( - "<warning>Could not fetch versions for \"{package_name}\": {e}</warning>" - ) - ); + console.info(&console_format!( + "<warning>Could not fetch versions for \"{package_name}\": {e}</warning>" + )); continue; } } @@ -392,8 +376,13 @@ pub async fn execute( }) .unwrap_or(Stability::Stable); - let found = - interactive_search_packages(&already_required, preferred_stability, args.fixed).await?; + let found = interactive_search_packages( + &already_required, + preferred_stability, + args.fixed, + console, + ) + .await?; if found.is_empty() { // Nothing selected — exit cleanly @@ -470,11 +459,11 @@ pub async fn execute( anyhow::bail!("Invalid package name: \"{name}\""); } - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>Using version constraint for {name} from Packagist...</info>" - ) + ), + Verbosity::Normal, ); let versions = packagist::fetch_package_versions(&name, None).await?; @@ -497,9 +486,9 @@ pub async fn execute( ) }; - println!( - "{}", - console_format!("<info>Using version {constraint} for {name}</info>") + console.write_stdout( + &console_format!("<info>Using version {constraint} for {name}</info>"), + Verbosity::Normal, ); (name, constraint) @@ -525,23 +514,17 @@ pub async fn execute( if *is_dev { // Adding to require-dev: check require (prod) if raw.require.contains_key(name.as_str()) { - eprintln!( - "{}", - console_format!( - "<warning>{name} is currently present in the require key and will be moved to the require-dev key.</warning>" - ) - ); + console.info(&console_format!( + "<warning>{name} is currently present in the require key and will be moved to the require-dev key.</warning>" + )); raw.require.remove(name.as_str()); } } else { // Adding to require (prod): check require-dev if raw.require_dev.contains_key(name.as_str()) { - eprintln!( - "{}", - console_format!( - "<warning>{name} is currently present in the require-dev key and will be moved to the require key.</warning>" - ) - ); + console.info(&console_format!( + "<warning>{name} is currently present in the require-dev key and will be moved to the require key.</warning>" + )); raw.require_dev.remove(name.as_str()); } } @@ -557,16 +540,16 @@ pub async fn execute( }; if let Some(existing) = target.get(name) { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<comment>Updating {name} from {existing} to {constraint} in {section_name}</comment>" - ) + ), + Verbosity::Normal, ); } else { - println!( - "{}", - console_format!("<info>Adding {name} ({constraint}) to {section_name}</info>") + console.write_stdout( + &console_format!("<info>Adding {name} ({constraint}) to {section_name}</info>"), + Verbosity::Normal, ); } @@ -592,9 +575,9 @@ pub async fn execute( // Write back composer.json (unless --dry-run) if args.dry_run { - println!( - "{}", - console_format!("<comment>Dry run: composer.json not modified.</comment>") + console.write_stdout( + &console_format!("<comment>Dry run: composer.json not modified.</comment>"), + Verbosity::Normal, ); } else { package::write_to_file(&raw, &composer_path)?; @@ -602,11 +585,11 @@ pub async fn execute( // Handle --no-update: skip resolution entirely if args.no_update { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<comment>Not updating dependencies, only modifying composer.json.</comment>" - ) + ), + Verbosity::Normal, ); return Ok(()); } @@ -674,16 +657,20 @@ pub async fn execute( Err(e) => { // Fix 1: Revert composer.json (and composer.lock) on failure if !args.dry_run { - eprintln!( - "Installation failed, reverting ./composer.json to its original content." + console.write_error( + "Installation failed, reverting ./composer.json to its original content.", ); if let Err(revert_err) = std::fs::write(&composer_path, &original_composer_json) { - eprintln!("Warning: Failed to revert composer.json: {revert_err}"); + console.write_error(&format!( + "Warning: Failed to revert composer.json: {revert_err}" + )); } if let Some(ref lock_content) = original_composer_lock && let Err(revert_err) = std::fs::write(&lock_path_for_backup, lock_content) { - eprintln!("Warning: Failed to revert composer.lock: {revert_err}"); + console.write_error(&format!( + "Warning: Failed to revert composer.lock: {revert_err}" + )); } } return Err(mozart_core::exit_code::bail( diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs index dc98b91..2d88a81 100644 --- a/crates/mozart/src/commands/run_script.rs +++ b/crates/mozart/src/commands/run_script.rs @@ -77,7 +77,7 @@ const ALL_SCRIPT_EVENTS: &[&str] = &[ pub async fn execute( args: &RunScriptArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), @@ -87,7 +87,7 @@ pub async fn execute( let (scripts, descriptions) = load_scripts(&working_dir)?; if args.list { - return list_scripts(&scripts, &descriptions); + return list_scripts(&scripts, &descriptions, console); } if cli.no_scripts { @@ -151,6 +151,7 @@ pub async fn execute( dev_mode, &mut event_stack, cli.verbose, + console, )?; if exit_code != 0 { @@ -210,11 +211,14 @@ fn load_scripts( fn list_scripts( scripts: &BTreeMap<String, Vec<String>>, descriptions: &BTreeMap<String, String>, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { - println!("scripts:"); + use mozart_core::console::Verbosity; + + console.write_stdout("scripts:", Verbosity::Normal); for name in scripts.keys() { let desc = descriptions.get(name).map(|s| s.as_str()).unwrap_or(""); - println!(" {} {}", name, desc); + console.write_stdout(&format!(" {} {}", name, desc), Verbosity::Normal); } Ok(()) } @@ -232,6 +236,7 @@ fn run_script( dev_mode: bool, event_stack: &mut Vec<String>, verbose: u8, + console: &mozart_core::console::Console, ) -> anyhow::Result<i32> { if event_stack.contains(&script_name.to_string()) { anyhow::bail!( @@ -258,6 +263,7 @@ fn run_script( dev_mode, event_stack, verbose, + console, )?; if code > max_exit_code { max_exit_code = code; @@ -287,6 +293,7 @@ fn run_script_entry( dev_mode: bool, event_stack: &mut Vec<String>, verbose: u8, + console: &mozart_core::console::Console, ) -> anyhow::Result<i32> { let suppress_additional_args = entry.contains("@no_additional_args"); let effective_args: &[String] = if suppress_additional_args { @@ -312,10 +319,10 @@ fn run_script_entry( }; if is_php_callback(&entry) { - eprintln!( + console.info(&format!( "Skipping PHP callback '{}' -- Mozart cannot execute PHP class methods.", entry - ); + )); return Ok(0); } @@ -344,6 +351,7 @@ fn run_script_entry( dev_mode, event_stack, verbose, + console, ); } @@ -514,6 +522,14 @@ mod tests { use super::*; use std::fs; + fn test_console() -> mozart_core::console::Console { + mozart_core::console::Console { + interactive: false, + verbosity: mozart_core::console::Verbosity::Normal, + decorated: false, + } + } + // ── Classifier tests ────────────────────────────────────────────────────── #[test] @@ -652,7 +668,7 @@ mod tests { descriptions.insert("test".to_string(), "Run tests".to_string()); // Just verify the function doesn't error - let result = list_scripts(&scripts, &descriptions); + let result = list_scripts(&scripts, &descriptions, &test_console()); assert!(result.is_ok()); } @@ -693,6 +709,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -720,6 +737,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); @@ -751,6 +769,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); @@ -777,6 +796,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -802,6 +822,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); @@ -830,6 +851,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -858,6 +880,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -885,6 +908,7 @@ mod tests { true, &mut stack, 0, + &test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -976,7 +1000,7 @@ mod tests { assert!(scripts.contains_key("test")); assert!(scripts.contains_key("lint")); - let result = list_scripts(&scripts, &descriptions); + let result = list_scripts(&scripts, &descriptions, &test_console()); assert!(result.is_ok()); } diff --git a/crates/mozart/src/commands/search.rs b/crates/mozart/src/commands/search.rs index d7be821..b747b0e 100644 --- a/crates/mozart/src/commands/search.rs +++ b/crates/mozart/src/commands/search.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use mozart_registry::packagist::SearchResult; use serde::Serialize; @@ -104,7 +105,7 @@ fn passes_only_vendor(result: &SearchResult, query: &str) -> bool { pub async fn execute( args: &SearchArgs, _cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { if args.only_name && args.only_vendor { anyhow::bail!("--only-name and --only-vendor cannot be used together"); @@ -115,12 +116,9 @@ pub async fn execute( let format = args.format.as_deref().unwrap_or("text"); if !matches!(format, "text" | "json") { - eprintln!( - "{}", - console_format!( - "<error>Unsupported format \"{format}\". See help for supported formats.</error>" - ) - ); + console.error(&console_format!( + "<error>Unsupported format \"{format}\". See help for supported formats.</error>" + )); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); @@ -153,17 +151,19 @@ pub async fn execute( match format { "json" => { let json = serde_json::to_string_pretty(&vendor_names)?; - println!("{json}"); + console.write_stdout(&json, Verbosity::Normal); } _ => { if vendor_names.is_empty() { - eprintln!( - "{}", - console_format!("<warning>No packages found for \"{query}\"</warning>") - ); + console.info(&console_format!( + "<warning>No packages found for \"{query}\"</warning>" + )); } else { for vendor in &vendor_names { - println!("{}", console_format!("<info>{vendor}</info>")); + console.write_stdout( + &console_format!("<info>{vendor}</info>"), + Verbosity::Normal, + ); } } } @@ -179,14 +179,13 @@ pub async fn execute( .map(|r| SearchResultOutput::from(*r)) .collect(); let json = serde_json::to_string_pretty(&output)?; - println!("{json}"); + console.write_stdout(&json, Verbosity::Normal); } _ => { if results.is_empty() { - eprintln!( - "{}", - console_format!("<warning>No packages found for \"{query}\"</warning>") - ); + console.info(&console_format!( + "<warning>No packages found for \"{query}\"</warning>" + )); return Ok(()); } @@ -211,7 +210,10 @@ pub async fn execute( }; let padding = " ".repeat(name_width.saturating_sub(result.name.len())); - println!("{}{}{}{}", result.name, padding, warning, desc_display); + console.write_stdout( + &format!("{}{}{}{}", result.name, padding, warning, desc_display), + Verbosity::Normal, + ); } } } diff --git a/crates/mozart/src/commands/self_update.rs b/crates/mozart/src/commands/self_update.rs index a0cd59d..afc77f3 100644 --- a/crates/mozart/src/commands/self_update.rs +++ b/crates/mozart/src/commands/self_update.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use std::io::Write; use std::path::{Path, PathBuf}; @@ -54,7 +55,7 @@ const BACKUP_EXTENSION: &str = ".old"; pub async fn execute( args: &SelfUpdateArgs, _cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let current_exe = std::env::current_exe() .map_err(|e| anyhow::anyhow!("Could not determine current executable path: {e}"))?; @@ -68,9 +69,9 @@ pub async fn execute( })?; if args.rollback { - rollback(¤t_exe, &data_dir) + rollback(¤t_exe, &data_dir, console) } else { - update(args, ¤t_exe, &data_dir).await + update(args, ¤t_exe, &data_dir, console).await } } @@ -227,6 +228,7 @@ async fn download_asset( asset: &GitHubAsset, dest: &Path, show_progress: bool, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(300)) @@ -272,7 +274,7 @@ async fn download_asset( } if show_progress && total_bytes > 0 { - eprintln!(); // newline after progress + console.info(""); // newline after progress } Ok(()) @@ -280,7 +282,12 @@ async fn download_asset( // ─── Core update flow ───────────────────────────────────────────────────────── -async fn update(args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path) -> anyhow::Result<()> { +async fn update( + args: &SelfUpdateArgs, + current_exe: &Path, + data_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let current_version = get_current_version(); let channel = effective_channel(args.preview); @@ -298,33 +305,38 @@ async fn update(args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path) -> a // If no explicit version was requested and we're already up-to-date, bail early if args.version.is_none() && target_version == current_version { - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>You are already using the latest available Mozart version {current_version} ({channel} channel).</info>" - ) + ), + Verbosity::Normal, ); if args.clean_backups { // Preserve the most recent backup let latest = find_latest_backup(data_dir).ok(); clean_backups(data_dir, latest.as_deref())?; - println!( - "{}", - console_format!("<comment>Old backups removed.</comment>") + console.write_stdout( + &console_format!("<comment>Old backups removed.</comment>"), + Verbosity::Normal, ); } return Ok(()); } - println!("Upgrading to version {target_version} ({channel} channel)."); + console.info(&format!( + "Upgrading to version {target_version} ({channel} channel)." + )); // Find the platform asset let asset_name = platform_asset_name()?; let asset = find_asset(target_release, &asset_name)?; - println!("Downloading {} ({} bytes)...", asset.name, asset.size); + console.info(&format!( + "Downloading {} ({} bytes)...", + asset.name, asset.size + )); // Download to a tempfile let tmp = tempfile::Builder::new() @@ -333,7 +345,7 @@ async fn update(args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path) -> a .map_err(|e| anyhow::anyhow!("Could not create temporary file: {e}"))?; let tmp_path = tmp.path().to_path_buf(); - download_asset(asset, &tmp_path, !args.no_progress).await?; + download_asset(asset, &tmp_path, !args.no_progress, console).await?; // Set executable permission on Unix #[cfg(unix)] @@ -362,19 +374,21 @@ async fn update(args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path) -> a // tmp is still in scope and will be cleaned up; the replace succeeded drop(tmp); - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>Mozart updated successfully from {current_version} to {target_version}</info>" - ) + ), + Verbosity::Normal, ); - println!("Use `mozart self-update --rollback` to return to version {current_version}"); + console.info(&format!( + "Use `mozart self-update --rollback` to return to version {current_version}" + )); if args.clean_backups { clean_backups(data_dir, Some(&backup_path))?; - println!( - "{}", - console_format!("<comment>Old backups removed.</comment>") + console.write_stdout( + &console_format!("<comment>Old backups removed.</comment>"), + Verbosity::Normal, ); } @@ -383,11 +397,15 @@ async fn update(args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path) -> a // ─── Rollback ───────────────────────────────────────────────────────────────── -fn rollback(current_exe: &Path, data_dir: &Path) -> anyhow::Result<()> { +fn rollback( + current_exe: &Path, + data_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let backup = find_latest_backup(data_dir)?; let backup_version = version_from_backup(&backup); - println!("Rolling back to version {backup_version}..."); + console.info(&format!("Rolling back to version {backup_version}...")); // Set executable permission on Unix before replacing #[cfg(unix)] @@ -402,9 +420,9 @@ fn rollback(current_exe: &Path, data_dir: &Path) -> anyhow::Result<()> { self_replace::self_replace(&backup) .map_err(|e| anyhow::anyhow!("Could not restore backup: {e}"))?; - println!( - "{}", - console_format!("<info>Rollback successful. Restored version {backup_version}</info>") + console.write_stdout( + &console_format!("<info>Rollback successful. Restored version {backup_version}</info>"), + Verbosity::Normal, ); let _ = current_exe; // suppress unused warning diff --git a/crates/mozart/src/commands/show.rs b/crates/mozart/src/commands/show.rs index f194bce..9bcc4ce 100644 --- a/crates/mozart/src/commands/show.rs +++ b/crates/mozart/src/commands/show.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use mozart_core::matches_wildcard; use std::collections::{HashMap, HashSet}; @@ -104,7 +105,7 @@ pub struct ShowArgs { pub async fn execute( args: &ShowArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Validate mutually exclusive level filters let level_count = args.major_only as u8 + args.minor_only as u8 + args.patch_only as u8; @@ -154,11 +155,11 @@ pub async fn execute( // Fix 8: --ignore without --outdated warning if !args.ignore.is_empty() && !args.outdated { - eprintln!( - "{}", - console_format!( + console.write( + &console_format!( "<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>" - ) + ), + Verbosity::Normal, ); } @@ -169,36 +170,40 @@ pub async fn execute( // --platform: show detected platform packages if args.platform { - return show_platform(args, &working_dir); + return show_platform(args, &working_dir, console); } // --self: show root package info (unless --installed or --locked override) if args.self_info && !args.installed && !args.locked { - return show_self(args, &working_dir); + return show_self(args, &working_dir, console); } // --tree: show dependency tree (uses lock file) if args.tree { - return show_tree(args, &working_dir); + return show_tree(args, &working_dir, console); } // --available: show available versions for installed packages if args.available { - return show_available(args, &working_dir).await; + return show_available(args, &working_dir, console).await; } // --locked: show from lock file if args.locked { - return execute_locked(args, &working_dir).await; + return execute_locked(args, &working_dir, console).await; } // Default: installed mode - execute_installed(args, &working_dir).await + execute_installed(args, &working_dir, console).await } // ─── Installed mode ──────────────────────────────────────────────────────── -async fn execute_installed(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +async fn execute_installed( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let vendor_dir = working_dir.join("vendor"); let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?; @@ -208,11 +213,11 @@ async fn execute_installed(args: &ShowArgs, working_dir: &Path) -> anyhow::Resul if composer_json_path.exists() { let root = mozart_core::package::read_from_file(&composer_json_path)?; if !root.require.is_empty() || !root.require_dev.is_empty() { - eprintln!( - "{}", - console_format!( + console.write( + &console_format!( "<warning>No dependencies installed. Try running mozart install or update.</warning>" - ) + ), + Verbosity::Normal, ); } } @@ -232,7 +237,7 @@ async fn execute_installed(args: &ShowArgs, working_dir: &Path) -> anyhow::Resul Some(p) => { let install_path = vendor_dir.join(&p.name); let path_str = resolve_path(&install_path); - println!("{} {}", p.name, path_str); + console.write_stdout(&format!("{} {}", p.name, path_str), Verbosity::Normal); } None => { anyhow::bail!( @@ -251,11 +256,11 @@ async fn execute_installed(args: &ShowArgs, working_dir: &Path) -> anyhow::Resul if let Some(ref package_filter) = args.package { if package_filter.contains('*') { packages.retain(|p| matches_wildcard(&p.name, package_filter)); - show_installed_package_list(&packages, args, &vendor_dir).await?; + show_installed_package_list(&packages, args, &vendor_dir, console).await?; return Ok(()); } else { // Single package detail view - return show_installed_package_detail(&installed, package_filter, working_dir); + return show_installed_package_detail(&installed, package_filter, working_dir, console); } } @@ -264,13 +269,13 @@ async fn execute_installed(args: &ShowArgs, working_dir: &Path) -> anyhow::Resul for pkg in &packages { let install_path = vendor_dir.join(&pkg.name); let path_str = resolve_path(&install_path); - println!("{} {}", pkg.name, path_str); + console.write_stdout(&format!("{} {}", pkg.name, path_str), Verbosity::Normal); } return Ok(()); } // List view - show_installed_package_list(&packages, args, &vendor_dir).await + show_installed_package_list(&packages, args, &vendor_dir, console).await } fn filter_installed_packages<'a>( @@ -315,13 +320,14 @@ async fn show_installed_package_list( packages: &[&mozart_registry::installed::InstalledPackageEntry], args: &ShowArgs, _vendor_dir: &Path, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // --latest / --outdated: fetch latest versions from Packagist let show_latest = args.latest || args.outdated; if args.name_only { for pkg in packages { - println!("{}", pkg.name); + console.write_stdout(&pkg.name, Verbosity::Normal); } return Ok(()); } @@ -384,7 +390,7 @@ async fn show_installed_package_list( // JSON output let format = args.format.as_deref().unwrap_or("text"); if format == "json" { - render_installed_json(&entries)?; + render_installed_json(&entries, console)?; if args.strict && has_outdated { return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, @@ -474,12 +480,18 @@ async fn show_installed_package_list( } None => format!("{:<width$}", "", width = latest_width), }; - println!( - "{} {} {} {}", - name_str, version_str, latest_str, entry.description + console.write_stdout( + &format!( + "{} {} {} {}", + name_str, version_str, latest_str, entry.description + ), + Verbosity::Normal, ); } else { - println!("{} {} {}", name_str, version_str, entry.description); + console.write_stdout( + &format!("{} {} {}", name_str, version_str, entry.description), + Verbosity::Normal, + ); } } @@ -557,7 +569,10 @@ async fn fetch_latest_for_package(name: &str) -> anyhow::Result<LatestInfo> { }) } -fn render_installed_json(entries: &[InstalledListEntry]) -> anyhow::Result<()> { +fn render_installed_json( + entries: &[InstalledListEntry], + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let json_entries: Vec<serde_json::Value> = entries .iter() .map(|entry| { @@ -583,7 +598,7 @@ fn render_installed_json(entries: &[InstalledListEntry]) -> anyhow::Result<()> { .collect(); let output = serde_json::json!({ "installed": json_entries }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); Ok(()) } @@ -591,6 +606,7 @@ fn show_installed_package_detail( installed: &mozart_registry::installed::InstalledPackages, package_name: &str, working_dir: &Path, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Find the package (case-insensitive) let pkg = installed @@ -610,39 +626,60 @@ fn show_installed_package_detail( let vendor_dir = working_dir.join("vendor"); - println!("{} : {}", console_format!("<info>name</info>"), pkg.name); - println!( - "{} : {}", - console_format!("<info>descrip.</info>"), - get_installed_description(pkg) + console.write_stdout( + &format!("{} : {}", console_format!("<info>name</info>"), pkg.name), + Verbosity::Normal, ); - println!( - "{} : {}", - console_format!("<info>keywords</info>"), - get_installed_keywords(pkg) + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>descrip.</info>"), + get_installed_description(pkg) + ), + Verbosity::Normal, ); - println!( - "{} : {}", - console_format!("<info>versions</info>"), - format_version_highlight(&pkg.version) + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>keywords</info>"), + get_installed_keywords(pkg) + ), + Verbosity::Normal, + ); + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>versions</info>"), + format_version_highlight(&pkg.version) + ), + Verbosity::Normal, ); - println!( - "{} : {}", - console_format!("<info>type</info>"), - pkg.package_type.as_deref().unwrap_or("library") + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>type</info>"), + pkg.package_type.as_deref().unwrap_or("library") + ), + Verbosity::Normal, ); // License if let Some(licenses) = get_installed_license(pkg) { - println!("{} : {}", console_format!("<info>license</info>"), licenses); + console.write_stdout( + &format!("{} : {}", console_format!("<info>license</info>"), licenses), + Verbosity::Normal, + ); } // Homepage if let Some(homepage) = get_installed_homepage(pkg) { - println!( - "{} : {}", - console_format!("<info>homepage</info>"), - homepage + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>homepage</info>"), + homepage + ), + Verbosity::Normal, ); } @@ -654,12 +691,15 @@ fn show_installed_package_detail( .get("reference") .and_then(|v| v.as_str()) .unwrap_or(""); - println!( - "{} : [{}] {} {}", - console_format!("<info>source</info>"), - source_type, - console_format!("<comment>{}</comment>", source_url), - source_ref + console.write_stdout( + &format!( + "{} : [{}] {} {}", + console_format!("<info>source</info>"), + source_type, + console_format!("<comment>{}</comment>", source_url), + source_ref + ), + Verbosity::Normal, ); } @@ -668,22 +708,28 @@ fn show_installed_package_detail( let dist_type = dist.get("type").and_then(|v| v.as_str()).unwrap_or(""); let dist_url = dist.get("url").and_then(|v| v.as_str()).unwrap_or(""); let dist_ref = dist.get("reference").and_then(|v| v.as_str()).unwrap_or(""); - println!( - "{} : [{}] {} {}", - console_format!("<info>dist</info>"), - dist_type, - console_format!("<comment>{}</comment>", dist_url), - dist_ref + console.write_stdout( + &format!( + "{} : [{}] {} {}", + console_format!("<info>dist</info>"), + dist_type, + console_format!("<comment>{}</comment>", dist_url), + dist_ref + ), + Verbosity::Normal, ); } // Path let install_path = vendor_dir.join(&pkg.name); if install_path.exists() { - println!( - "{} : {}", - console_format!("<info>path</info>"), - install_path.display() + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>path</info>"), + install_path.display() + ), + Verbosity::Normal, ); } @@ -691,11 +737,14 @@ fn show_installed_package_detail( if let Some(requires) = pkg.extra_fields.get("require").and_then(|v| v.as_object()) && !requires.is_empty() { - println!(); - println!("{}", console_format!("<info>requires</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout(&console_format!("<info>requires</info>"), Verbosity::Normal); for (name, constraint) in requires { let c = constraint.as_str().unwrap_or(""); - println!("{} {}", name, console_format!("<comment>{}</comment>", c)); + console.write_stdout( + &format!("{} {}", name, console_format!("<comment>{}</comment>", c)), + Verbosity::Normal, + ); } } @@ -706,11 +755,17 @@ fn show_installed_package_detail( .and_then(|v| v.as_object()) && !requires_dev.is_empty() { - println!(); - println!("{}", console_format!("<info>requires (dev)</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout( + &console_format!("<info>requires (dev)</info>"), + Verbosity::Normal, + ); for (name, constraint) in requires_dev { let c = constraint.as_str().unwrap_or(""); - println!("{} {}", name, console_format!("<comment>{}</comment>", c)); + console.write_stdout( + &format!("{} {}", name, console_format!("<comment>{}</comment>", c)), + Verbosity::Normal, + ); } } @@ -719,7 +774,11 @@ fn show_installed_package_detail( // ─── Locked mode ─────────────────────────────────────────────────────────── -async fn execute_locked(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +async fn execute_locked( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let lock_path = working_dir.join("composer.lock"); if !lock_path.exists() { anyhow::bail!( @@ -759,12 +818,12 @@ async fn execute_locked(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( if let Some(ref package_filter) = args.package { if package_filter.contains('*') { packages.retain(|p| matches_wildcard(&p.name, package_filter)); - show_locked_package_list(&packages, args).await?; + show_locked_package_list(&packages, args, console).await?; } else { - show_locked_package_detail(&lock, package_filter)?; + show_locked_package_detail(&lock, package_filter, console)?; } } else { - show_locked_package_list(&packages, args).await?; + show_locked_package_list(&packages, args, console).await?; } Ok(()) @@ -773,12 +832,13 @@ async fn execute_locked(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( async fn show_locked_package_list( packages: &[&mozart_registry::lockfile::LockedPackage], args: &ShowArgs, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let show_latest = args.latest || args.outdated; if args.name_only { for pkg in packages { - println!("{}", pkg.name); + console.write_stdout(&pkg.name, Verbosity::Normal); } return Ok(()); } @@ -839,7 +899,7 @@ async fn show_locked_package_list( // JSON format let format = args.format.as_deref().unwrap_or("text"); if format == "json" { - render_locked_json(&entries)?; + render_locked_json(&entries, console)?; if args.strict && has_outdated { return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, @@ -929,12 +989,18 @@ async fn show_locked_package_list( } None => format!("{:<width$}", "", width = latest_width), }; - println!( - "{} {} {} {}", - name_str, version_str, latest_str, entry.description + console.write_stdout( + &format!( + "{} {} {} {}", + name_str, version_str, latest_str, entry.description + ), + Verbosity::Normal, ); } else { - println!("{} {} {}", name_str, version_str, entry.description); + console.write_stdout( + &format!("{} {} {}", name_str, version_str, entry.description), + Verbosity::Normal, + ); } } @@ -955,7 +1021,10 @@ struct LockedListEntry { latest_info: Option<LatestInfo>, } -fn render_locked_json(entries: &[LockedListEntry]) -> anyhow::Result<()> { +fn render_locked_json( + entries: &[LockedListEntry], + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let json_entries: Vec<serde_json::Value> = entries .iter() .map(|entry| { @@ -981,13 +1050,14 @@ fn render_locked_json(entries: &[LockedListEntry]) -> anyhow::Result<()> { .collect(); let output = serde_json::json!({ "installed": json_entries }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); Ok(()) } fn show_locked_package_detail( lock: &mozart_registry::lockfile::LockFile, package_name: &str, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { // Search in both packages and packages-dev let pkg = lock @@ -1003,11 +1073,17 @@ fn show_locked_package_detail( } }; - println!("{} : {}", console_format!("<info>name</info>"), pkg.name); - println!( - "{} : {}", - console_format!("<info>descrip.</info>"), - pkg.description.as_deref().unwrap_or("") + console.write_stdout( + &format!("{} : {}", console_format!("<info>name</info>"), pkg.name), + Verbosity::Normal, + ); + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>descrip.</info>"), + pkg.description.as_deref().unwrap_or("") + ), + Verbosity::Normal, ); // Keywords @@ -1016,85 +1092,115 @@ fn show_locked_package_detail( .as_ref() .map(|kw| kw.join(", ")) .unwrap_or_default(); - println!( - "{} : {}", - console_format!("<info>keywords</info>"), - keywords + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>keywords</info>"), + keywords + ), + Verbosity::Normal, ); - println!( - "{} : * {}", - console_format!("<info>versions</info>"), - format_version(&pkg.version) + console.write_stdout( + &format!( + "{} : * {}", + console_format!("<info>versions</info>"), + format_version(&pkg.version) + ), + Verbosity::Normal, ); - println!( - "{} : {}", - console_format!("<info>type</info>"), - pkg.package_type.as_deref().unwrap_or("library") + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>type</info>"), + pkg.package_type.as_deref().unwrap_or("library") + ), + Verbosity::Normal, ); // License if let Some(ref licenses) = pkg.license { - println!( - "{} : {}", - console_format!("<info>license</info>"), - licenses.join(", ") + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>license</info>"), + licenses.join(", ") + ), + Verbosity::Normal, ); } // Homepage if let Some(ref homepage) = pkg.homepage { - println!( - "{} : {}", - console_format!("<info>homepage</info>"), - homepage + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>homepage</info>"), + homepage + ), + Verbosity::Normal, ); } // Source if let Some(ref source) = pkg.source { - println!( - "{} : [{}] {} {}", - console_format!("<info>source</info>"), - source.source_type, - console_format!("<comment>{}</comment>", &source.url), - source.reference.as_deref().unwrap_or("") + console.write_stdout( + &format!( + "{} : [{}] {} {}", + console_format!("<info>source</info>"), + source.source_type, + console_format!("<comment>{}</comment>", &source.url), + source.reference.as_deref().unwrap_or("") + ), + Verbosity::Normal, ); } // Dist if let Some(ref dist) = pkg.dist { - println!( - "{} : [{}] {} {}", - console_format!("<info>dist</info>"), - dist.dist_type, - console_format!("<comment>{}</comment>", &dist.url), - dist.reference.as_deref().unwrap_or("") + console.write_stdout( + &format!( + "{} : [{}] {} {}", + console_format!("<info>dist</info>"), + dist.dist_type, + console_format!("<comment>{}</comment>", &dist.url), + dist.reference.as_deref().unwrap_or("") + ), + Verbosity::Normal, ); } // Requires if !pkg.require.is_empty() { - println!(); - println!("{}", console_format!("<info>requires</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout(&console_format!("<info>requires</info>"), Verbosity::Normal); for (name, constraint) in &pkg.require { - println!( - "{} {}", - name, - console_format!("<comment>{}</comment>", constraint) + console.write_stdout( + &format!( + "{} {}", + name, + console_format!("<comment>{}</comment>", constraint) + ), + Verbosity::Normal, ); } } // Requires (dev) if !pkg.require_dev.is_empty() { - println!(); - println!("{}", console_format!("<info>requires (dev)</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout( + &console_format!("<info>requires (dev)</info>"), + Verbosity::Normal, + ); for (name, constraint) in &pkg.require_dev { - println!( - "{} {}", - name, - console_format!("<comment>{}</comment>", constraint) + console.write_stdout( + &format!( + "{} {}", + name, + console_format!("<comment>{}</comment>", constraint) + ), + Verbosity::Normal, ); } } @@ -1103,13 +1209,16 @@ fn show_locked_package_detail( if let Some(ref suggests) = pkg.suggest && !suggests.is_empty() { - println!(); - println!("{}", console_format!("<info>suggests</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout(&console_format!("<info>suggests</info>"), Verbosity::Normal); for (name, reason) in suggests { - println!( - "{} {}", - name, - console_format!("<comment>{}</comment>", reason) + console.write_stdout( + &format!( + "{} {}", + name, + console_format!("<comment>{}</comment>", reason) + ), + Verbosity::Normal, ); } } @@ -1119,7 +1228,11 @@ fn show_locked_package_detail( // ─── Self mode ───────────────────────────────────────────────────────────── -fn show_self(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +fn show_self( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let composer_json_path = working_dir.join("composer.json"); if !composer_json_path.exists() { anyhow::bail!("No composer.json found in {}", working_dir.display()); @@ -1127,54 +1240,78 @@ fn show_self(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { let root = mozart_core::package::read_from_file(&composer_json_path)?; if args.name_only { - println!("{}", root.name); + console.write_stdout(&root.name, Verbosity::Normal); return Ok(()); } - println!("{} : {}", console_format!("<info>name</info>"), root.name); - println!( - "{} : {}", - console_format!("<info>descrip.</info>"), - root.description.as_deref().unwrap_or("") + console.write_stdout( + &format!("{} : {}", console_format!("<info>name</info>"), root.name), + Verbosity::Normal, + ); + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>descrip.</info>"), + root.description.as_deref().unwrap_or("") + ), + Verbosity::Normal, ); - println!( - "{} : {}", - console_format!("<info>type</info>"), - root.package_type.as_deref().unwrap_or("project") + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>type</info>"), + root.package_type.as_deref().unwrap_or("project") + ), + Verbosity::Normal, ); if let Some(ref license) = root.license { - println!("{} : {}", console_format!("<info>license</info>"), license); + console.write_stdout( + &format!("{} : {}", console_format!("<info>license</info>"), license), + Verbosity::Normal, + ); } if let Some(ref homepage) = root.homepage { - println!( - "{} : {}", - console_format!("<info>homepage</info>"), - homepage + console.write_stdout( + &format!( + "{} : {}", + console_format!("<info>homepage</info>"), + homepage + ), + Verbosity::Normal, ); } // Requires if !root.require.is_empty() { - println!(); - println!("{}", console_format!("<info>requires</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout(&console_format!("<info>requires</info>"), Verbosity::Normal); for (name, constraint) in &root.require { - println!( - "{} {}", - name, - console_format!("<comment>{}</comment>", constraint) + console.write_stdout( + &format!( + "{} {}", + name, + console_format!("<comment>{}</comment>", constraint) + ), + Verbosity::Normal, ); } } // Requires (dev) if !root.require_dev.is_empty() { - println!(); - println!("{}", console_format!("<info>requires (dev)</info>")); + console.write_stdout("", Verbosity::Normal); + console.write_stdout( + &console_format!("<info>requires (dev)</info>"), + Verbosity::Normal, + ); for (name, constraint) in &root.require_dev { - println!( - "{} {}", - name, - console_format!("<comment>{}</comment>", constraint) + console.write_stdout( + &format!( + "{} {}", + name, + console_format!("<comment>{}</comment>", constraint) + ), + Verbosity::Normal, ); } } @@ -1184,7 +1321,11 @@ fn show_self(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { // ─── Tree mode ───────────────────────────────────────────────────────────── -fn show_tree(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +fn show_tree( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let lock_path = working_dir.join("composer.lock"); let composer_json_path = working_dir.join("composer.json"); @@ -1228,13 +1369,13 @@ fn show_tree(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { }; // Print root - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>{}</info> <comment>{}</comment>", &root.name, root.description.as_deref().unwrap_or("") - ) + ), + Verbosity::Normal, ); // Render each root dependency as a tree @@ -1253,12 +1394,14 @@ fn show_tree(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { child_prefix, &mut visited_global, 0, + console, ); } Ok(()) } +#[allow(clippy::too_many_arguments)] fn print_tree_node( pkg_name: &str, constraint: &str, @@ -1267,6 +1410,7 @@ fn print_tree_node( child_prefix: &str, visited: &mut HashSet<String>, depth: usize, + console: &mozart_core::console::Console, ) { const MAX_DEPTH: usize = 10; @@ -1277,17 +1421,23 @@ fn print_tree_node( let description = pkg.description.as_deref().unwrap_or(""); let version = format_version(&pkg.version); - println!( - "{} {} {}", - prefix, - console_format!("<info>{}</info> <comment>{}</comment>", pkg_name, &version), - description + console.write_stdout( + &format!( + "{} {} {}", + prefix, + console_format!("<info>{}</info> <comment>{}</comment>", pkg_name, &version), + description + ), + Verbosity::Normal, ); // Detect circular dependency or depth limit if visited.contains(&key) || depth >= MAX_DEPTH { if visited.contains(&key) { - println!("{} {} (circular dependency)", child_prefix, pkg_name); + console.write_stdout( + &format!("{} {} (circular dependency)", child_prefix, pkg_name), + Verbosity::Normal, + ); } return; } @@ -1327,6 +1477,7 @@ fn print_tree_node( &grandchild_prefix, visited, depth + 1, + console, ); } @@ -1334,11 +1485,14 @@ fn print_tree_node( } else { // Package not found in lock file (platform package or not installed) if !is_platform_package(&key) { - println!( - "{} {} {} (not installed)", - prefix, - console_format!("<comment>{}</comment>", pkg_name), - constraint + console.write_stdout( + &format!( + "{} {} {} (not installed)", + prefix, + console_format!("<comment>{}</comment>", pkg_name), + constraint + ), + Verbosity::Normal, ); } } @@ -1359,7 +1513,11 @@ fn is_platform_package(name: &str) -> bool { // ─── Platform mode ───────────────────────────────────────────────────────── -fn show_platform(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +fn show_platform( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { // Collect platform info from lock file and system detection let mut platform_packages: Vec<(String, String, String)> = Vec::new(); // (name, version, source) @@ -1423,23 +1581,23 @@ fn show_platform(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { }) }) .collect(); - println!( - "{}", - serde_json::to_string_pretty(&serde_json::json!({ "platform": json_entries }))? + console.write_stdout( + &serde_json::to_string_pretty(&serde_json::json!({ "platform": json_entries }))?, + Verbosity::Normal, ); return Ok(()); } if platform_packages.is_empty() { - eprintln!( - "No platform packages detected. Install PHP or add platform requirements to composer.json." + console.info( + "No platform packages detected. Install PHP or add platform requirements to composer.json.", ); return Ok(()); } if args.name_only { for (name, _, _) in &platform_packages { - println!("{name}"); + console.write_stdout(name, Verbosity::Normal); } return Ok(()); } @@ -1456,14 +1614,17 @@ fn show_platform(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { .unwrap_or(0); for (name, version, _source) in &platform_packages { - println!( - "{} {}", - console_format!("<info>{:<width$}</info>", name, width = name_width), - console_format!( - "<comment>{:<width$}</comment>", - version, - width = version_width + console.write_stdout( + &format!( + "{} {}", + console_format!("<info>{:<width$}</info>", name, width = name_width), + console_format!( + "<comment>{:<width$}</comment>", + version, + width = version_width + ), ), + Verbosity::Normal, ); } @@ -1472,10 +1633,14 @@ fn show_platform(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { // ─── Available mode ───────────────────────────────────────────────────────── -async fn show_available(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<()> { +async fn show_available( + args: &ShowArgs, + working_dir: &Path, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { // If a specific package name is given, show available versions for it if let Some(ref pkg_name) = args.package { - return show_available_versions(pkg_name, args).await; + return show_available_versions(pkg_name, args, console).await; } // Otherwise, show all installed packages with their available (latest) versions @@ -1490,13 +1655,13 @@ async fn show_available(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( let lock_path = working_dir.join("composer.lock"); if lock_path.exists() { let lock = mozart_registry::lockfile::LockFile::read_from_file(&lock_path)?; - println!( - "{}", - console_format!( + console.write_stdout( + &console_format!( "<info>Available versions for locked packages (from Packagist):</info>" - ) + ), + Verbosity::Normal, ); - println!(); + console.write_stdout("", Verbosity::Normal); let mut all_packages: Vec<&mozart_registry::lockfile::LockedPackage> = lock.packages.iter().collect(); @@ -1510,26 +1675,28 @@ async fn show_available(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( if is_platform_package(&pkg.name) { continue; } - show_available_versions_inline(&pkg.name).await; + show_available_versions_inline(&pkg.name, console).await; } return Ok(()); } - eprintln!( - "{}", - console_format!( + console.write( + &console_format!( "<warning>No dependencies installed. Try running mozart install or update.</warning>" - ) + ), + Verbosity::Normal, ); return Ok(()); } }; - println!( - "{}", - console_format!("<info>Available versions for installed packages (from Packagist):</info>") + console.write_stdout( + &console_format!( + "<info>Available versions for installed packages (from Packagist):</info>" + ), + Verbosity::Normal, ); - println!(); + console.write_stdout("", Verbosity::Normal); let format = args.format.as_deref().unwrap_or("text"); @@ -1559,7 +1726,7 @@ async fn show_available(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( } } let output = serde_json::json!({ "packages": json_entries }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); return Ok(()); } @@ -1567,16 +1734,23 @@ async fn show_available(args: &ShowArgs, working_dir: &Path) -> anyhow::Result<( if is_platform_package(&pkg.name) { continue; } - show_available_versions_inline(&pkg.name).await; + show_available_versions_inline(&pkg.name, console).await; } Ok(()) } -async fn show_available_versions(pkg_name: &str, args: &ShowArgs) -> anyhow::Result<()> { +async fn show_available_versions( + pkg_name: &str, + args: &ShowArgs, + console: &mozart_core::console::Console, +) -> anyhow::Result<()> { let versions = mozart_registry::packagist::fetch_package_versions(pkg_name, None).await?; if versions.is_empty() { - println!("No versions found for {pkg_name}"); + console.write_stdout( + &format!("No versions found for {pkg_name}"), + Verbosity::Normal, + ); return Ok(()); } @@ -1587,27 +1761,33 @@ async fn show_available_versions(pkg_name: &str, args: &ShowArgs) -> anyhow::Res "name": pkg_name, "versions": version_strings, }); - println!("{}", serde_json::to_string_pretty(&output)?); + console.write_stdout(&serde_json::to_string_pretty(&output)?, Verbosity::Normal); return Ok(()); } - println!( - "{}", - console_format!("<info>Available versions for {pkg_name}:</info>") + console.write_stdout( + &console_format!("<info>Available versions for {pkg_name}:</info>"), + Verbosity::Normal, ); for v in &versions { - println!(" {}", console_format!("<comment>{}</comment>", &v.version)); + console.write_stdout( + &format!(" {}", console_format!("<comment>{}</comment>", &v.version)), + Verbosity::Normal, + ); } Ok(()) } -async fn show_available_versions_inline(pkg_name: &str) { +async fn show_available_versions_inline(pkg_name: &str, console: &mozart_core::console::Console) { match mozart_registry::packagist::fetch_package_versions(pkg_name, None).await { Ok(versions) => { if versions.is_empty() { - println!( - "{}: no versions found", - console_format!("<info>{}</info>", pkg_name) + console.write_stdout( + &format!( + "{}: no versions found", + console_format!("<info>{}</info>", pkg_name) + ), + Verbosity::Normal, ); return; } @@ -1622,17 +1802,23 @@ async fn show_available_versions_inline(pkg_name: &str) { } else { String::new() }; - println!( - "{}: {}{}", - console_format!("<info>{}</info>", pkg_name), - console_format!("<comment>{}</comment>", &shown.join(", ")), - rest + console.write_stdout( + &format!( + "{}: {}{}", + console_format!("<info>{}</info>", pkg_name), + console_format!("<comment>{}</comment>", &shown.join(", ")), + rest + ), + Verbosity::Normal, ); } Err(_) => { - println!( - "{}: (could not fetch from Packagist)", - console_format!("<comment>{}</comment>", pkg_name) + console.write_stdout( + &format!( + "{}: (could not fetch from Packagist)", + console_format!("<comment>{}</comment>", pkg_name) + ), + Verbosity::Normal, ); } } diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs index acf6ee6..7cefe96 100644 --- a/crates/mozart/src/commands/status.rs +++ b/crates/mozart/src/commands/status.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use sha1::{Digest, Sha1}; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -48,7 +49,7 @@ struct PackageStatus { pub async fn execute( args: &StatusArgs, cli: &super::Cli, - _console: &mozart_core::console::Console, + console: &mozart_core::console::Console, ) -> anyhow::Result<()> { let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), @@ -59,7 +60,7 @@ pub async fn execute( let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?; if installed.packages.is_empty() { - eprintln!("No packages installed."); + console.info("No packages installed."); return Ok(()); } @@ -75,7 +76,7 @@ pub async fn execute( Some(d) => d, None => { if cli.verbose > 1 { - eprintln!(" Skipping {} — no dist info available", pkg.name); + console.verbose(&format!(" Skipping {} — no dist info available", pkg.name)); } continue; } @@ -85,11 +86,11 @@ pub async fn execute( let install_path = resolve_install_path(pkg, &vendor_dir); if !install_path.exists() { if cli.verbose > 0 { - eprintln!( + console.verbose(&format!( " Skipping {} — install path does not exist: {}", pkg.name, install_path.display() - ); + )); } continue; } @@ -106,7 +107,7 @@ pub async fn execute( } if cli.verbose > 0 { - eprintln!(" Checking {} ...", pkg.name); + console.verbose(&format!(" Checking {} ...", pkg.name)); } // Download original archive to a temp dir @@ -122,7 +123,10 @@ pub async fn execute( let bytes = match downloaded { Ok(b) => b, Err(e) => { - eprintln!(" Warning: could not download dist for {}: {}", pkg.name, e); + console.info(&format!( + " Warning: could not download dist for {}: {}", + pkg.name, e + )); let _ = std::fs::remove_dir_all(&tmp_dir); continue; } @@ -135,17 +139,20 @@ pub async fn execute( mozart_registry::downloader::extract_tar_gz(&bytes, &tmp_dir) } other => { - eprintln!( + console.info(&format!( " Warning: unsupported dist type '{}' for {}", other, pkg.name - ); + )); let _ = std::fs::remove_dir_all(&tmp_dir); continue; } }; if let Err(e) = extract_result { - eprintln!(" Warning: could not extract dist for {}: {}", pkg.name, e); + console.info(&format!( + " Warning: could not extract dist for {}: {}", + pkg.name, e + )); let _ = std::fs::remove_dir_all(&tmp_dir); continue; } @@ -168,18 +175,17 @@ pub async fn execute( } if modified_packages.is_empty() { - eprintln!("No local changes"); + console.info("No local changes"); return Ok(()); } - eprintln!("You have changes in the following dependencies:\n"); + console.info("You have changes in the following dependencies:\n"); for pkg_status in &modified_packages { if let Some(ref note) = pkg_status.note { - println!("{}", note); + console.write_stdout(note, Verbosity::Normal); } else { - // Show full install path, matching Composer's format - println!("{}", pkg_status.install_path); + console.write_stdout(&pkg_status.install_path, Verbosity::Normal); if show_files { let mut sorted_changes: Vec<&FileChange> = pkg_status.changes.iter().collect(); @@ -191,7 +197,10 @@ pub async fn execute( ChangeKind::Added => '+', ChangeKind::Removed => '-', }; - println!(" {} {}", prefix, change.path); + console.write_stdout( + &format!(" {} {}", prefix, change.path), + Verbosity::Normal, + ); } } } @@ -199,7 +208,7 @@ pub async fn execute( // Hint about --verbose if not already showing files and there are modified packages if !show_files { - eprintln!("Use --verbose (-v) to see a list of files"); + console.info("Use --verbose (-v) to see a list of files"); } // Exit with code 1 if modifications found diff --git a/crates/mozart/src/commands/suggests.rs b/crates/mozart/src/commands/suggests.rs index 3fb2f00..1dd898f 100644 --- a/crates/mozart/src/commands/suggests.rs +++ b/crates/mozart/src/commands/suggests.rs @@ -1,5 +1,6 @@ use clap::Args; use mozart_core::console; +use mozart_core::console::Verbosity; use mozart_core::console_format; use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Path, PathBuf}; @@ -43,7 +44,7 @@ struct Suggestion { pub async fn execute( args: &SuggestsArgs, cli: &super::Cli, - _console: &console::Console, + console: &console::Console, ) -> anyhow::Result<()> { let working_dir = match &cli.working_dir { Some(dir) => PathBuf::from(dir), @@ -134,26 +135,29 @@ pub async fn execute( let shown = filtered.len(); let diff = total_before_direct_filter.saturating_sub(shown); if diff > 0 { - println!( - "{} by transitive dependencies can be shown with {}", - console_format!("<info>{diff} additional suggestions</info>"), - console_format!("<info>--all</info>"), + console.write_stdout( + &format!( + "{} by transitive dependencies can be shown with {}", + console_format!("<info>{diff} additional suggestions</info>"), + console_format!("<info>--all</info>"), + ), + Verbosity::Normal, ); } } // 6. Render output if args.list { - render_list(&filtered); + render_list(&filtered, console); } else if args.by_suggestion && !args.by_package { - render_by_suggestion(&filtered); + render_by_suggestion(&filtered, console); } else if args.by_package && args.by_suggestion { - render_by_package(&filtered); - println!("{}", "-".repeat(78)); - render_by_suggestion(&filtered); + render_by_package(&filtered, console); + console.write_stdout(&"-".repeat(78), Verbosity::Normal); + render_by_suggestion(&filtered, console); } else { // Default: by-package - render_by_package(&filtered); + render_by_package(&filtered, console); } Ok(()) @@ -431,64 +435,70 @@ fn deduplicate_suggestions(suggestions: Vec<Suggestion>) -> Vec<Suggestion> { // ─── Rendering ─────────────────────────────────────────────────────────────── -fn render_list(suggestions: &[&Suggestion]) { +fn render_list(suggestions: &[&Suggestion], console: &console::Console) { let mut targets: Vec<&str> = suggestions.iter().map(|s| s.target.as_str()).collect(); targets.sort_unstable(); targets.dedup(); for t in targets { - println!("{}", console_format!("<info>{}</info>", t)); + console.write_stdout(&console_format!("<info>{}</info>", t), Verbosity::Normal); } } -fn render_by_package(suggestions: &[&Suggestion]) { +fn render_by_package(suggestions: &[&Suggestion], console: &console::Console) { // Group by source, preserving insertion order via BTreeMap (sorted) let mut grouped: BTreeMap<&str, Vec<&Suggestion>> = BTreeMap::new(); for s in suggestions { grouped.entry(s.source.as_str()).or_default().push(s); } for (source, items) in &grouped { - println!( - "{}", - console_format!("<comment>{}</comment> suggests:", source) + console.write_stdout( + &console_format!("<comment>{}</comment> suggests:", source), + Verbosity::Normal, ); for s in items { let reason = sanitize_reason(&s.reason); if reason.is_empty() { - println!("{}", console_format!(" - <info>{}</info>", &s.target)); + console.write_stdout( + &console_format!(" - <info>{}</info>", &s.target), + Verbosity::Normal, + ); } else { - println!( - "{}", - console_format!(" - <info>{}</info>: {}", &s.target, reason) + console.write_stdout( + &console_format!(" - <info>{}</info>: {}", &s.target, reason), + Verbosity::Normal, ); } } - println!(); + console.write_stdout("", Verbosity::Normal); } } -fn render_by_suggestion(suggestions: &[&Suggestion]) { +fn render_by_suggestion(suggestions: &[&Suggestion], console: &console::Console) { // Group by target let mut grouped: BTreeMap<&str, Vec<&Suggestion>> = BTreeMap::new(); for s in suggestions { grouped.entry(s.target.as_str()).or_default().push(s); } for (target, items) in &grouped { - println!( - "{}", - console_format!("<info>{}</info> is suggested by:", target) + console.write_stdout( + &console_format!("<info>{}</info> is suggested by:", target), + Verbosity::Normal, ); for s in items { let reason = sanitize_reason(&s.reason); if reason.is_empty() { - println!("{}", console_format!(" - <comment>{}</comment>", &s.source)); + console.write_stdout( + &console_format!(" - <comment>{}</comment>", &s.source), + Verbosity::Normal, + ); } else { - println!( - "{}", - console_format!(" - <comment>{}</comment>: {}", &s.source, reason) + console.write_stdout( + &console_format!(" - <comment>{}</comment>: {}", &s.source, reason), + Verbosity::Normal, ); } } - println!(); + console.write_stdout("", Verbosity::Normal); } } diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 6155306..b937d5c 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -379,7 +379,11 @@ fn glob_segment_matches_inner(pattern: &[u8], text: &[u8]) -> bool { /// /// Non-wildcard specifiers are passed through unchanged (even if not in the lock, /// so the resolver can report the error naturally). -pub fn expand_wildcards(specifiers: &[String], lock: &lockfile::LockFile) -> Vec<String> { +pub fn expand_wildcards( + specifiers: &[String], + lock: &lockfile::LockFile, + console: &mozart_core::console::Console, +) -> Vec<String> { // Collect all locked package names (prod + dev) let all_names: Vec<String> = lock .packages @@ -407,13 +411,10 @@ pub fn expand_wildcards(specifiers: &[String], lock: &lockfile::LockFile) -> Vec } } if !matched { - eprintln!( - "{}", - console::warning(&format!( - "No locked packages matched the pattern '{}'. Pattern will be ignored.", - spec - )) - ); + console.info(&console::warning(&format!( + "No locked packages matched the pattern '{}'. Pattern will be ignored.", + spec + ))); } } else { let lower = spec.to_lowercase(); @@ -524,10 +525,10 @@ pub fn expand_packages( lock: Option<&lockfile::LockFile>, with_dependencies: bool, with_all_dependencies: bool, + console: &mozart_core::console::Console, ) -> Vec<String> { - // First expand wildcards (requires a lock file) let mut packages: Vec<String> = if let Some(lock) = lock { - expand_wildcards(specifiers, lock) + expand_wildcards(specifiers, lock, console) } else { // No lock file: pass through as-is (no wildcards can be resolved) specifiers.iter().map(|s| s.to_lowercase()).collect() @@ -556,21 +557,21 @@ pub fn expand_packages( /// /// When stdin is not a TTY (e.g. in CI or piped input), emits a warning and /// returns the full package list unchanged. -pub fn interactive_select_packages(packages: Vec<String>) -> Vec<String> { +pub fn interactive_select_packages( + packages: Vec<String>, + console: &mozart_core::console::Console, +) -> Vec<String> { use std::io::{self, BufRead, IsTerminal, Write}; let stdin = io::stdin(); if !stdin.is_terminal() { - eprintln!( - "{}", - console::warning( - "Interactive mode requires a TTY. Running non-interactively with all packages." - ) - ); + console.info(&console::warning( + "Interactive mode requires a TTY. Running non-interactively with all packages.", + )); return packages; } - eprintln!("Select packages to update (y/n for each):"); + console.info("Select packages to update (y/n for each):"); let mut selected = Vec::new(); let stdin_locked = stdin.lock(); @@ -593,7 +594,7 @@ pub fn interactive_select_packages(packages: Vec<String>) -> Vec<String> { break; } _ => { - eprintln!(" Please answer y or n."); + console.info(" Please answer y or n."); } } } @@ -925,11 +926,12 @@ pub async fn execute( Some(lock), args.with_dependencies, args.with_all_dependencies, + console, ); // 2. Interactive selection (filter the expanded list) if args.interactive { - expanded = interactive_select_packages(expanded); + expanded = interactive_select_packages(expanded, console); } expanded @@ -958,7 +960,7 @@ pub async fn execute( .map(|p| p.name.to_lowercase()), ) .collect(); - interactive_select_packages(all_names) + interactive_select_packages(all_names, console) } } } else { @@ -1330,6 +1332,14 @@ mod tests { } } + fn test_console() -> mozart_core::console::Console { + mozart_core::console::Console { + interactive: false, + verbosity: mozart_core::console::Verbosity::Normal, + decorated: false, + } + } + // ──────────── parse_minimum_stability ──────────── #[test] @@ -1723,7 +1733,7 @@ mod tests { fn test_expand_wildcards_no_wildcard_passthrough() { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); let specs = vec!["psr/log".to_string(), "nonexistent/pkg".to_string()]; - let result = expand_wildcards(&specs, &lock); + let result = expand_wildcards(&specs, &lock, &test_console()); assert_eq!(result, vec!["psr/log", "nonexistent/pkg"]); } @@ -1735,7 +1745,7 @@ mod tests { make_locked_package("monolog/monolog", "3.8.0"), ]); let specs = vec!["symfony/*".to_string()]; - let mut result = expand_wildcards(&specs, &lock); + let mut result = expand_wildcards(&specs, &lock, &test_console()); result.sort(); assert_eq!(result, vec!["symfony/console", "symfony/http-kernel"]); } @@ -1745,7 +1755,7 @@ mod tests { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); let specs = vec!["unknown/*".to_string()]; // Should return empty (no match), no panic - let result = expand_wildcards(&specs, &lock); + let result = expand_wildcards(&specs, &lock, &test_console()); assert!(result.is_empty()); } @@ -1753,7 +1763,7 @@ mod tests { fn test_expand_wildcards_deduplication() { let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); let specs = vec!["psr/log".to_string(), "psr/log".to_string()]; - let result = expand_wildcards(&specs, &lock); + let result = expand_wildcards(&specs, &lock, &test_console()); assert_eq!(result.len(), 1); assert_eq!(result[0], "psr/log"); } @@ -1763,7 +1773,7 @@ mod tests { let mut lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); let specs = vec!["phpunit/*".to_string()]; - let result = expand_wildcards(&specs, &lock); + let result = expand_wildcards(&specs, &lock, &test_console()); assert_eq!(result, vec!["phpunit/phpunit"]); } @@ -1883,6 +1893,7 @@ mod tests { Some(&lock), true, // with_dependencies false, // with_all_dependencies + &test_console(), ); assert!(result.contains(&"symfony/console".to_string())); diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs index 8c6b6c3..cec36b5 100644 --- a/crates/mozart/src/commands/validate.rs +++ b/crates/mozart/src/commands/validate.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::Verbosity; use mozart_core::console_format; use std::path::{Path, PathBuf}; @@ -144,7 +145,14 @@ pub async fn execute( // Output results let check_publish = !args.no_check_publish; - output_result(&file, &result, check_publish, check_lock, &lock_errors); + output_result( + console, + &file, + &result, + check_publish, + check_lock, + &lock_errors, + ); // Validate dependencies' composer.json files let (dep_errors, dep_warnings) = if args.with_dependencies { @@ -489,12 +497,9 @@ fn validate_dependencies( dep_errors += 1; let pkg_name = format!("{}/{}", vendor_str, pkg_entry.file_name().to_string_lossy()); - eprintln!( - "{}", - console_format!( - "<warning>{pkg_name}: composer.json contains invalid JSON</warning>" - ) - ); + console.info(&console_format!( + "<warning>{pkg_name}: composer.json contains invalid JSON</warning>" + )); continue; }; @@ -508,11 +513,11 @@ fn validate_dependencies( format!("{}/{}", vendor_str, pkg_entry.file_name().to_string_lossy()); for e in &result.errors { - eprintln!("{}", console_format!("<error>{pkg_name}: {e}</error>")); + console.error(&console_format!("<error>{pkg_name}: {e}</error>")); dep_errors += 1; } for w in &result.warnings { - eprintln!("{}", console_format!("<warning>{pkg_name}: {w}</warning>")); + console.info(&console_format!("<warning>{pkg_name}: {w}</warning>")); dep_warnings += 1; } } @@ -568,6 +573,7 @@ fn check_lock_freshness( // ─── Output ────────────────────────────────────────────────────────────────── fn output_result( + console: &mozart_core::console::Console, file: &Path, result: &ValidationResult, check_publish: bool, @@ -578,48 +584,37 @@ fn output_result( // Print header message if result.has_errors() { - eprintln!( - "{}", - console_format!( - "<error>{name} is invalid, the following errors/warnings were found:</error>" - ) - ); + console.error(&console_format!( + "<error>{name} is invalid, the following errors/warnings were found:</error>" + )); } else if result.has_publish_errors() && check_publish { - eprintln!( - "{}", - console_format!("<info>{name} is valid for simple usage with Composer but has</info>") - ); - eprintln!( - "{}", - mozart_core::console::info( - "strict errors that make it unable to be published as a package" - ) - ); - eprintln!( - "{}", - mozart_core::console::warning( - "See https://getcomposer.org/doc/04-schema.md for details on the schema" - ) - ); + console.info(&console_format!( + "<info>{name} is valid for simple usage with Composer but has</info>" + )); + console.info(&mozart_core::console::info( + "strict errors that make it unable to be published as a package", + )); + console.info(&mozart_core::console::warning( + "See https://getcomposer.org/doc/04-schema.md for details on the schema", + )); } else if result.has_warnings() { - eprintln!( - "{}", - console_format!("<info>{name} is valid, but with a few warnings</info>") - ); - eprintln!( - "{}", - mozart_core::console::warning( - "See https://getcomposer.org/doc/04-schema.md for details on the schema" - ) - ); + console.info(&console_format!( + "<info>{name} is valid, but with a few warnings</info>" + )); + console.info(&mozart_core::console::warning( + "See https://getcomposer.org/doc/04-schema.md for details on the schema", + )); } else if !lock_errors.is_empty() { let kind = if check_lock { "errors" } else { "warnings" }; - println!( - "{}", - console_format!("<info>{name} is valid but your composer.lock has some {kind}</info>") + console.write_stdout( + &console_format!("<info>{name} is valid but your composer.lock has some {kind}</info>"), + Verbosity::Normal, ); } else { - println!("{}", console_format!("<info>{name} is valid</info>")); + console.write_stdout( + &console_format!("<info>{name} is valid</info>"), + Verbosity::Normal, + ); } // Collect error and warning message lines @@ -662,18 +657,17 @@ fn output_result( // Print errors for msg in &all_errors { if msg.starts_with('#') { - eprintln!("{}", mozart_core::console::error(msg)); + console.error(&mozart_core::console::error(msg)); } else { - eprintln!("{msg}"); + console.error(msg); } } - // Print warnings for msg in &all_warnings { if msg.starts_with('#') { - eprintln!("{}", mozart_core::console::warning(msg)); + console.info(&mozart_core::console::warning(msg)); } else { - eprintln!("{msg}"); + console.info(msg); } } } |
