diff options
Diffstat (limited to 'crates')
39 files changed, 1244 insertions, 953 deletions
diff --git a/crates/mozart-core/src/console.rs b/crates/mozart-core/src/console.rs index e036b11..3379307 100644 --- a/crates/mozart-core/src/console.rs +++ b/crates/mozart-core/src/console.rs @@ -67,6 +67,46 @@ pub fn hyperlink(url: &str, text: &str, decorated: bool) -> String { } // --------------------------------------------------------------------------- +// IoInterface +// --------------------------------------------------------------------------- + +/// The central IO abstraction, mirroring `\Composer\IO\IOInterface`. +/// +/// All Mozart commands and library functions that need to produce output +/// or interact with the user accept `&dyn IoInterface` (or an +/// `Arc<Mutex<Box<dyn IoInterface>>>` at the top-level command boundary). +pub trait IoInterface: Send + Sync { + fn write(&self, msg: &str, required: Verbosity); + fn write_stdout(&self, msg: &str, required: Verbosity); + fn write_error(&self, msg: &str); + + fn info(&self, msg: &str); + fn verbose(&self, msg: &str); + fn very_verbose(&self, msg: &str); + fn debug(&self, msg: &str); + fn error(&self, msg: &str); + + fn is_interactive(&self) -> bool; + fn is_decorated(&self) -> bool; + fn verbosity(&self) -> Verbosity; + + fn is_verbose(&self) -> bool; + fn is_very_verbose(&self) -> bool; + fn is_debug(&self) -> bool; + fn is_quiet(&self) -> bool; + + fn ask(&self, prompt: &str, default: &str) -> String; + #[allow(clippy::type_complexity)] + fn ask_validated( + &self, + prompt: &str, + default: &str, + validator: Box<dyn Fn(&str) -> Result<(), String>>, + ) -> Result<String, String>; + fn confirm(&self, prompt: &str) -> bool; +} + +// --------------------------------------------------------------------------- // Verbosity // --------------------------------------------------------------------------- @@ -212,6 +252,18 @@ impl Console { // Query methods // ----------------------------------------------------------------------- + pub fn verbosity(&self) -> Verbosity { + self.verbosity + } + + pub fn is_interactive(&self) -> bool { + self.interactive + } + + pub fn is_decorated(&self) -> bool { + self.decorated + } + pub fn is_verbose(&self) -> bool { self.verbosity >= Verbosity::Verbose } @@ -245,15 +297,13 @@ impl Console { .unwrap_or_else(|_| default.to_string()) } - pub fn ask_validated<F>( + #[allow(clippy::type_complexity)] + pub fn ask_validated( &self, prompt: &str, default: &str, - validator: F, - ) -> Result<String, String> - where - F: Fn(&str) -> Result<(), String>, - { + validator: Box<dyn Fn(&str) -> Result<(), String>>, + ) -> Result<String, String> { if !self.interactive { validator(default)?; return Ok(default.to_string()); @@ -289,6 +339,130 @@ impl Console { } } +impl<T: IoInterface + ?Sized> IoInterface for Box<T> { + fn write(&self, msg: &str, required: Verbosity) { + (**self).write(msg, required) + } + fn write_stdout(&self, msg: &str, required: Verbosity) { + (**self).write_stdout(msg, required) + } + fn write_error(&self, msg: &str) { + (**self).write_error(msg) + } + fn info(&self, msg: &str) { + (**self).info(msg) + } + fn verbose(&self, msg: &str) { + (**self).verbose(msg) + } + fn very_verbose(&self, msg: &str) { + (**self).very_verbose(msg) + } + fn debug(&self, msg: &str) { + (**self).debug(msg) + } + fn error(&self, msg: &str) { + (**self).error(msg) + } + fn is_interactive(&self) -> bool { + (**self).is_interactive() + } + fn is_decorated(&self) -> bool { + (**self).is_decorated() + } + fn verbosity(&self) -> Verbosity { + (**self).verbosity() + } + fn is_verbose(&self) -> bool { + (**self).is_verbose() + } + fn is_very_verbose(&self) -> bool { + (**self).is_very_verbose() + } + fn is_debug(&self) -> bool { + (**self).is_debug() + } + fn is_quiet(&self) -> bool { + (**self).is_quiet() + } + fn ask(&self, prompt: &str, default: &str) -> String { + (**self).ask(prompt, default) + } + fn ask_validated( + &self, + prompt: &str, + default: &str, + validator: Box<dyn Fn(&str) -> Result<(), String>>, + ) -> Result<String, String> { + (**self).ask_validated(prompt, default, validator) + } + fn confirm(&self, prompt: &str) -> bool { + (**self).confirm(prompt) + } +} + +impl IoInterface for Console { + fn write(&self, msg: &str, required: Verbosity) { + self.write(msg, required) + } + fn write_stdout(&self, msg: &str, required: Verbosity) { + self.write_stdout(msg, required) + } + fn write_error(&self, msg: &str) { + self.write_error(msg) + } + fn info(&self, msg: &str) { + self.info(msg) + } + fn verbose(&self, msg: &str) { + self.verbose(msg) + } + fn very_verbose(&self, msg: &str) { + self.very_verbose(msg) + } + fn debug(&self, msg: &str) { + self.debug(msg) + } + fn error(&self, msg: &str) { + self.error(msg) + } + fn is_interactive(&self) -> bool { + self.is_interactive() + } + fn is_decorated(&self) -> bool { + self.is_decorated() + } + fn verbosity(&self) -> Verbosity { + self.verbosity() + } + fn is_verbose(&self) -> bool { + self.is_verbose() + } + fn is_very_verbose(&self) -> bool { + self.is_very_verbose() + } + fn is_debug(&self) -> bool { + self.is_debug() + } + fn is_quiet(&self) -> bool { + self.is_quiet() + } + fn ask(&self, prompt: &str, default: &str) -> String { + self.ask(prompt, default) + } + fn ask_validated( + &self, + prompt: &str, + default: &str, + validator: Box<dyn Fn(&str) -> Result<(), String>>, + ) -> Result<String, String> { + self.ask_validated(prompt, default, validator) + } + fn confirm(&self, prompt: &str) -> bool { + self.confirm(prompt) + } +} + /// Writes a message to the output. /// /// ref: \Composer\IO\IOInterface::write() @@ -304,7 +478,7 @@ macro_rules! console_writeln { $crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,) }; ($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => { - if ($console).verbosity >= $verbosity { + if $console.lock().unwrap().verbosity() >= $verbosity { ::std::println!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*)); } }; @@ -325,7 +499,7 @@ macro_rules! console_write { $crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,) }; ($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => { - if ($console).verbosity >= $verbosity { + if $console.lock().unwrap().verbosity() >= $verbosity { ::std::print!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*)); } }; @@ -346,7 +520,7 @@ macro_rules! console_writeln_error { $crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,) }; ($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => { - if ($console).verbosity >= $verbosity { + if $console.lock().unwrap().verbosity() >= $verbosity { ::std::eprintln!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*)); } }; @@ -367,7 +541,7 @@ macro_rules! console_write_error { $crate::console_writeln!($console, $verbosity, $fmt, $($arg)*,) }; ($console:expr, $verbosity:expr, $fmt:literal, $($arg:tt)*) => { - if ($console).verbosity >= $verbosity { + if $console.lock().unwrap().verbosity() >= $verbosity { ::std::eprint!("{}", &::mozart_console_macros::console_format!($fmt, $($arg)*)); } }; diff --git a/crates/mozart-core/src/installer/suggested_packages_reporter.rs b/crates/mozart-core/src/installer/suggested_packages_reporter.rs index 2a356fc..43ef8c4 100644 --- a/crates/mozart-core/src/installer/suggested_packages_reporter.rs +++ b/crates/mozart-core/src/installer/suggested_packages_reporter.rs @@ -5,7 +5,7 @@ //! Composer's reporter exposes so other entry points (install/update) can //! emit a minimalistic post-install hint with the same code path. -use crate::console::{Console, Verbosity}; +use crate::console::{IoInterface, Verbosity}; use crate::console_format; use crate::installer::installed_repo::InstalledRepoLite; use indexmap::IndexSet; @@ -82,14 +82,14 @@ impl RootInfo { /// install/update one-liner). pub struct SuggestedPackagesReporter<'a> { suggested_packages: Vec<Suggestion>, - console: &'a Console, + io: &'a dyn IoInterface, } impl<'a> SuggestedPackagesReporter<'a> { - pub fn new(console: &'a Console) -> Self { + pub fn new(io: &'a dyn IoInterface) -> Self { Self { suggested_packages: Vec::new(), - console, + io, } } @@ -204,7 +204,7 @@ impl<'a> SuggestedPackagesReporter<'a> { ) { let suggestions = self.get_filtered_suggestions(installed_repo, only_dependents_of); if !suggestions.is_empty() { - self.console.write( + self.io.write( &console_format!( "<info>{} package suggestions were added by new dependencies, use `composer suggest` to see details.</info>", suggestions.len() @@ -215,7 +215,7 @@ impl<'a> SuggestedPackagesReporter<'a> { } fn write_line(&self, msg: &str) { - if self.console.verbosity >= Verbosity::Normal { + if self.io.verbosity() >= Verbosity::Normal { println!("{msg}"); } } diff --git a/crates/mozart-core/src/repository/advisory.rs b/crates/mozart-core/src/repository/advisory.rs index 02a6e1a..08f59a1 100644 --- a/crates/mozart-core/src/repository/advisory.rs +++ b/crates/mozart-core/src/repository/advisory.rs @@ -1,7 +1,7 @@ use super::packagist::SecurityAdvisory; use super::repository::RepositorySet; use crate::advisory::{AbandonedHandling, AuditFormat}; -use crate::console::Console; +use crate::console::IoInterface; use crate::{console_writeln, console_writeln_error}; use indexmap::IndexMap; use std::collections::BTreeMap; @@ -88,7 +88,7 @@ impl Auditor { /// Returns a bitmask: 0=ok, 1=vulnerable, 2=abandoned, 3=both. pub async fn audit( &self, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, repo_set: &RepositorySet, packages: &[PackageInfo], options: &AuditOptions<'_>, @@ -132,7 +132,7 @@ impl Auditor { &ignored_advisories, &unreachable_repos, &abandoned_packages, - console, + &io, ); return Ok(bitmask); } @@ -152,8 +152,8 @@ impl Auditor { let msg = format!( "Found {ignored_total} ignored security vulnerability advisor{plurality} affecting {ignored_pkg_count} package{pkg_plurality}{punctuation}" ); - console_writeln_error!(console, "<info>{msg}</info>"); - self.output_advisories_ignored(console, &ignored_advisories, format); + console_writeln_error!(io, "<info>{msg}</info>"); + self.output_advisories_ignored(&io, &ignored_advisories, format); } if active_pkg_count > 0 { @@ -168,38 +168,35 @@ impl Auditor { "Found {active_total} security vulnerability advisor{plurality} affecting {active_pkg_count} package{pkg_plurality}{punctuation}" ); if options.warning_only { - console_writeln_error!(console, "<warning>{msg}</warning>"); + console_writeln_error!(io, "<warning>{msg}</warning>"); } else { - console_writeln_error!(console, "<error>{msg}</error>"); + console_writeln_error!(io, "<error>{msg}</error>"); } - self.output_advisories(console, &advisories, format); + self.output_advisories(&io, &advisories, format); } if format == AuditFormat::Summary { - console_writeln_error!( - console, - "Run \"mozart audit\" for a full list of advisories." - ); + console_writeln_error!(io, "Run \"mozart audit\" for a full list of advisories."); } } else { console_writeln_error!( - console, + io, "<info>No security vulnerability advisories found.</info>", ); } if !unreachable_repos.is_empty() { console_writeln_error!( - console, + io, "<warning>The following repositories were unreachable:</warning>", ); for repo in &unreachable_repos { - console_writeln_error!(console, " - {repo}"); + console_writeln_error!(io, " - {repo}"); } } if !abandoned_packages.is_empty() && format != AuditFormat::Summary { - self.output_abandoned_packages(console, &abandoned_packages, format); + self.output_abandoned_packages(&io, &abandoned_packages, format); } Ok(bitmask) @@ -364,13 +361,13 @@ impl Auditor { fn output_advisories( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<MatchedAdvisory>>, format: AuditFormat, ) { match format { - AuditFormat::Table => self.output_advisories_table(console, advisories), - AuditFormat::Plain => self.output_advisories_plain(console, advisories), + AuditFormat::Table => self.output_advisories_table(io, advisories), + AuditFormat::Plain => self.output_advisories_plain(io, advisories), AuditFormat::Summary => {} AuditFormat::Json => unreachable!(), } @@ -378,13 +375,13 @@ impl Auditor { fn output_advisories_ignored( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>, format: AuditFormat, ) { match format { - AuditFormat::Table => self.output_ignored_advisories_table(console, advisories), - AuditFormat::Plain => self.output_ignored_advisories_plain(console, advisories), + AuditFormat::Table => self.output_ignored_advisories_table(io, advisories), + AuditFormat::Plain => self.output_ignored_advisories_plain(io, advisories), AuditFormat::Summary => {} AuditFormat::Json => unreachable!(), } @@ -392,30 +389,25 @@ impl Auditor { fn output_advisories_table( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<MatchedAdvisory>>, ) { for pkg_advisories in advisories.values() { for matched in pkg_advisories { - self.render_advisory_table( - console, - &matched.advisory, - &matched.installed_version, - None, - ); + self.render_advisory_table(io, &matched.advisory, &matched.installed_version, None); } } } fn output_ignored_advisories_table( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>, ) { for pkg_advisories in advisories.values() { for ignored in pkg_advisories { self.render_advisory_table( - console, + io, &ignored.advisory, &ignored.installed_version, ignored.ignore_reason.as_deref(), @@ -426,7 +418,7 @@ impl Auditor { fn render_advisory_table( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, adv: &SecurityAdvisory, installed_version: &str, ignore_reason: Option<&str>, @@ -459,10 +451,10 @@ impl Auditor { vw = value_width ); - console_writeln_error!(console, "{}", separator); + console_writeln_error!(io, "{}", separator); for (label, value) in &rows { console_writeln_error!( - console, + io, "| {:<lw$} | {:<vw$} |", label, value, @@ -470,27 +462,22 @@ impl Auditor { vw = value_width, ); } - console_writeln_error!(console, "{}", &separator); - console_writeln_error!(console, ""); + console_writeln_error!(io, "{}", &separator); + console_writeln_error!(io, ""); } fn output_advisories_plain( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<MatchedAdvisory>>, ) { let mut first = true; for pkg_advisories in advisories.values() { for matched in pkg_advisories { if !first { - console_writeln_error!(console, "--------"); + console_writeln_error!(io, "--------"); } - self.render_advisory_plain( - console, - &matched.advisory, - &matched.installed_version, - None, - ); + self.render_advisory_plain(io, &matched.advisory, &matched.installed_version, None); first = false; } } @@ -498,17 +485,17 @@ impl Auditor { fn output_ignored_advisories_plain( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>, ) { let mut first = true; for pkg_advisories in advisories.values() { for ignored in pkg_advisories { if !first { - console_writeln_error!(console, "--------"); + console_writeln_error!(io, "--------"); } self.render_advisory_plain( - console, + io, &ignored.advisory, &ignored.installed_version, ignored.ignore_reason.as_deref(), @@ -520,39 +507,35 @@ impl Auditor { fn render_advisory_plain( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, adv: &SecurityAdvisory, installed_version: &str, ignore_reason: Option<&str>, ) { - console_writeln_error!(console, "Package: {}", adv.package_name); - console_writeln_error!(console, "Version: {installed_version}"); - console_writeln_error!( - console, - "Severity: {}", - adv.severity.as_deref().unwrap_or(""), - ); - console_writeln_error!(console, "Advisory ID: {}", adv.advisory_id); - console_writeln_error!(console, "CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE")); - console_writeln_error!(console, "Title: {}", adv.title); - console_writeln_error!(console, "URL: {}", adv.link.as_deref().unwrap_or("")); - console_writeln_error!(console, "Affected versions: {}", adv.affected_versions); - console_writeln_error!(console, "Reported at: {}", adv.reported_at); + console_writeln_error!(io, "Package: {}", adv.package_name); + console_writeln_error!(io, "Version: {installed_version}"); + console_writeln_error!(io, "Severity: {}", adv.severity.as_deref().unwrap_or(""),); + console_writeln_error!(io, "Advisory ID: {}", adv.advisory_id); + console_writeln_error!(io, "CVE: {}", adv.cve.as_deref().unwrap_or("NO CVE")); + console_writeln_error!(io, "Title: {}", adv.title); + console_writeln_error!(io, "URL: {}", adv.link.as_deref().unwrap_or("")); + console_writeln_error!(io, "Affected versions: {}", adv.affected_versions); + console_writeln_error!(io, "Reported at: {}", adv.reported_at); if let Some(reason) = ignore_reason { - console_writeln_error!(console, "Ignore reason: {reason}"); + console_writeln_error!(io, "Ignore reason: {reason}"); } } fn output_abandoned_packages( &self, - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, packages: &[AbandonedPackage], format: AuditFormat, ) { let count = packages.len(); let plurality = if count == 1 { "" } else { "s" }; console_writeln_error!( - console, + io, "<error>Found {count} abandoned package{plurality}:</error>", ); @@ -560,14 +543,14 @@ impl Auditor { for pkg in packages { match &pkg.replacement { Some(repl) => console_writeln_error!( - console, + io, "{} ({}) is abandoned. Use {} instead.", pkg.name, pkg.version, repl, ), None => console_writeln_error!( - console, + io, "{} ({}) is abandoned. No replacement was suggested.", pkg.name, pkg.version, @@ -598,7 +581,7 @@ impl Auditor { .max("Suggested Replacement".len()); console_writeln_error!( - console, + io, "| {:<nw$} | {:<vw$} | {:<rw$} |", "Abandoned Package", "Version", @@ -608,7 +591,7 @@ impl Auditor { rw = repl_width, ); console_writeln_error!( - console, + io, "+-{:-<nw$}-+-{:-<vw$}-+-{:-<rw$}-+", "", "", @@ -623,7 +606,7 @@ impl Auditor { .as_deref() .unwrap_or("No replacement suggested"); console_writeln_error!( - console, + io, "| {:<nw$} | {:<vw$} | {:<rw$} |", pkg.name, pkg.version, @@ -633,7 +616,7 @@ impl Auditor { rw = repl_width, ); } - console_writeln_error!(console, ""); + console_writeln_error!(io, ""); } fn render_json( @@ -642,7 +625,7 @@ impl Auditor { ignored_advisories: &BTreeMap<String, Vec<IgnoredAdvisory>>, unreachable_repos: &[String], abandoned_packages: &[AbandonedPackage], - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { let mut advisories_map: serde_json::Map<String, serde_json::Value> = serde_json::Map::new(); for (pkg_name, matched_list) in advisories { @@ -720,7 +703,7 @@ impl Auditor { } let json_str = serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string()); - console_writeln!(console, "{}", &json_str); + console_writeln!(io, "{}", &json_str); } } diff --git a/crates/mozart/src/commands.rs b/crates/mozart/src/commands.rs index d0139d5..b062fdf 100644 --- a/crates/mozart/src/commands.rs +++ b/crates/mozart/src/commands.rs @@ -1,3 +1,5 @@ +use mozart_core::console::IoInterface; + pub mod about; pub mod archive; pub mod audit; @@ -271,6 +273,9 @@ pub async fn execute(cli: &Cli) -> anyhow::Result<()> { cli.no_ansi, cli.no_interaction, ); + let io = std::sync::Arc::new(std::sync::Mutex::new( + Box::new(console) as Box<dyn IoInterface> + )); // Initialize HTTPS root certificates from `config.cafile` / `config.capath` // before any command makes a network request. @@ -279,41 +284,39 @@ pub async fn execute(cli: &Cli) -> anyhow::Result<()> { let command = cli.command.as_ref().expect("command must be set"); match command { - Commands::About(args) => about::execute(args, cli, &console).await, - Commands::Archive(args) => archive::execute(args, cli, &console).await, - Commands::Audit(args) => audit::execute(args, cli, &console).await, - Commands::Browse(args) => browse::execute(args, cli, &console).await, - Commands::Bump(args) => bump::execute(args, cli, &console).await, - Commands::CheckPlatformReqs(args) => { - check_platform_reqs::execute(args, cli, &console).await - } - Commands::ClearCache(args) => clear_cache::execute(args, cli, &console).await, - Commands::Completion(args) => completion::execute(args, cli, &console).await, - Commands::Config(args) => config::execute(args, cli, &console).await, - Commands::CreateProject(args) => create_project::execute(args, cli, &console).await, - Commands::Depends(args) => depends::execute(args, cli, &console).await, - Commands::Diagnose(args) => diagnose::execute(args, cli, &console).await, - Commands::DumpAutoload(args) => dump_autoload::execute(args, cli, &console).await, - Commands::Exec(args) => exec::execute(args, cli, &console).await, - Commands::Fund(args) => fund::execute(args, cli, &console).await, - Commands::Global(args) => global::execute(args, cli, &console).await, - Commands::Init(args) => init::execute(args, cli, &console).await, - Commands::Install(args) => install::execute(args, cli, &console).await, - Commands::Licenses(args) => licenses::execute(args, cli, &console).await, - Commands::Outdated(args) => outdated::execute(args, cli, &console).await, - Commands::Prohibits(args) => prohibits::execute(args, cli, &console).await, - Commands::Reinstall(args) => reinstall::execute(args, cli, &console).await, - Commands::Remove(args) => remove::execute(args, cli, &console).await, - Commands::Repository(args) => repository::execute(args, cli, &console).await, - Commands::Require(args) => require::execute(args, cli, &console).await, - Commands::RunScript(args) => run_script::execute(args, cli, &console).await, - Commands::Search(args) => search::execute(args, cli, &console).await, - Commands::SelfUpdate(args) => self_update::execute(args, cli, &console).await, - Commands::Show(args) => show::execute(args, cli, &console).await, - Commands::Status(args) => status::execute(args, cli, &console).await, - Commands::Suggests(args) => suggests::execute(args, cli, &console).await, - Commands::Update(args) => update::execute(args, cli, &console).await, - Commands::Validate(args) => validate::execute(args, cli, &console).await, + Commands::About(args) => about::execute(args, cli, io).await, + Commands::Archive(args) => archive::execute(args, cli, io).await, + Commands::Audit(args) => audit::execute(args, cli, io).await, + Commands::Browse(args) => browse::execute(args, cli, io).await, + Commands::Bump(args) => bump::execute(args, cli, io).await, + Commands::CheckPlatformReqs(args) => check_platform_reqs::execute(args, cli, io).await, + Commands::ClearCache(args) => clear_cache::execute(args, cli, io).await, + Commands::Completion(args) => completion::execute(args, cli, io).await, + Commands::Config(args) => config::execute(args, cli, io).await, + Commands::CreateProject(args) => create_project::execute(args, cli, io).await, + Commands::Depends(args) => depends::execute(args, cli, io).await, + Commands::Diagnose(args) => diagnose::execute(args, cli, io).await, + Commands::DumpAutoload(args) => dump_autoload::execute(args, cli, io).await, + Commands::Exec(args) => exec::execute(args, cli, io).await, + Commands::Fund(args) => fund::execute(args, cli, io).await, + Commands::Global(args) => global::execute(args, cli, io).await, + Commands::Init(args) => init::execute(args, cli, io).await, + Commands::Install(args) => install::execute(args, cli, io).await, + Commands::Licenses(args) => licenses::execute(args, cli, io).await, + Commands::Outdated(args) => outdated::execute(args, cli, io).await, + Commands::Prohibits(args) => prohibits::execute(args, cli, io).await, + Commands::Reinstall(args) => reinstall::execute(args, cli, io).await, + Commands::Remove(args) => remove::execute(args, cli, io).await, + Commands::Repository(args) => repository::execute(args, cli, io).await, + Commands::Require(args) => require::execute(args, cli, io).await, + Commands::RunScript(args) => run_script::execute(args, cli, io).await, + Commands::Search(args) => search::execute(args, cli, io).await, + Commands::SelfUpdate(args) => self_update::execute(args, cli, io).await, + Commands::Show(args) => show::execute(args, cli, io).await, + Commands::Status(args) => status::execute(args, cli, io).await, + Commands::Suggests(args) => suggests::execute(args, cli, io).await, + Commands::Update(args) => update::execute(args, cli, io).await, + Commands::Validate(args) => validate::execute(args, cli, io).await, } } diff --git a/crates/mozart/src/commands/about.rs b/crates/mozart/src/commands/about.rs index 04c3aa1..6b97ec4 100644 --- a/crates/mozart/src/commands/about.rs +++ b/crates/mozart/src/commands/about.rs @@ -1,6 +1,6 @@ use clap::Args; use mozart_core::MOZART_VERSION; -use mozart_core::console; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; #[derive(Args)] @@ -9,10 +9,10 @@ pub struct AboutArgs {} pub async fn execute( _args: &AboutArgs, _cli: &super::Cli, - console: &console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { console_writeln!( - console, + io, r#"<info>Mozart - Dependency Manager for PHP - version {MOZART_VERSION}</info> <comment>Mozart is a dependency manager tracking local dependencies of your projects and libraries. See https://getcomposer.org/ for more information.</comment>"#, diff --git a/crates/mozart/src/commands/archive.rs b/crates/mozart/src/commands/archive.rs index d83bdb5..e1c0fa3 100644 --- a/crates/mozart/src/commands/archive.rs +++ b/crates/mozart/src/commands/archive.rs @@ -1,5 +1,6 @@ use crate::composer::Composer; use clap::Args; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::factory::create_config; use mozart_core::package::archiver::{ArchiveManager, ArchivePackage}; @@ -34,7 +35,7 @@ pub struct ArchiveArgs { pub async fn execute( args: &ArchiveArgs, cli: &super::Cli, - io: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; @@ -49,7 +50,7 @@ pub async fn execute( let dir = args.dir.as_deref().unwrap_or(&config.archive_dir); archive( - io, + &io, args.package.as_deref(), args.version.as_deref(), format, @@ -64,7 +65,7 @@ pub async fn execute( #[allow(clippy::too_many_arguments)] async fn archive( - io: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, package_name: Option<&str>, version: Option<&str>, format: &str, @@ -92,7 +93,9 @@ async fn archive( working_dir.join(dest) }; - io.info(&format!("Creating the archive into \"{}\".", dest)); + io.lock() + .unwrap() + .info(&format!("Creating the archive into \"{}\".", dest)); let package_path = archive_manager .archive( &package, @@ -135,7 +138,7 @@ fn load_root_package(working_dir: &Path) -> anyhow::Result<ArchivePackage> { } async fn select_package( - io: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, package_name: &str, version: Option<&str>, repo_cache: &mozart_core::repository::cache::Cache, @@ -143,7 +146,9 @@ async fn select_package( use mozart_core::package::Stability; use mozart_core::repository::version::find_best_candidate; - io.info("Searching for the specified package."); + io.lock() + .unwrap() + .info("Searching for the specified package."); // Strip @stability suffix from the version constraint (e.g. "^1.0@beta" → "^1.0", Stability::Beta) let (version, min_stability) = if let Some(raw) = version { @@ -180,12 +185,12 @@ async fn select_package( } let package = matches[0]; if matches.len() > 1 { - io.info(&format!( + io.lock().unwrap().info(&format!( "Found multiple matches, selected {} {}.", package_name, package.version )); } else { - io.info(&format!( + io.lock().unwrap().info(&format!( "Found an exact match {} {}.", package_name, package.version )); @@ -197,7 +202,7 @@ async fn select_package( .ok_or_else(|| { anyhow::anyhow!("No suitable version found for package \"{}\"", package_name) })?; - io.info(&format!( + io.lock().unwrap().info(&format!( "Found an exact match {} {}.", package_name, package.version )); diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs index eafcba4..9672d57 100644 --- a/crates/mozart/src/commands/audit.rs +++ b/crates/mozart/src/commands/audit.rs @@ -4,6 +4,7 @@ use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; use mozart_core::advisory::{AbandonedHandling, AuditConfig, AuditFormat}; +use mozart_core::console::IoInterface; use mozart_core::repository::advisory::{AuditOptions, Auditor, PackageInfo}; use mozart_core::repository::cache::{Cache, build_cache_config}; use mozart_core::repository::repository::RepositorySet; @@ -38,7 +39,7 @@ pub struct AuditArgs { pub async fn execute( args: &AuditArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; @@ -84,7 +85,7 @@ pub async fn execute( let packages = get_packages(&composer, args)?; if packages.is_empty() { - console.info("No packages - skipping audit."); + io.lock().unwrap().info("No packages - skipping audit."); return Ok(()); } @@ -95,7 +96,7 @@ pub async fn execute( // Run audit let exit_code = Auditor::new() .audit( - console, + io.clone(), &repo_set, &packages, &AuditOptions { diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs index d7c9ce2..aabbdf8 100644 --- a/crates/mozart/src/commands/browse.rs +++ b/crates/mozart/src/commands/browse.rs @@ -1,6 +1,6 @@ use crate::composer::Composer; use clap::Args; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; @@ -24,7 +24,11 @@ pub struct BrowseArgs { pub show: bool, } -pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> anyhow::Result<()> { +pub async fn execute( + args: &BrowseArgs, + cli: &super::Cli, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let cache = Cache::repo(&build_cache_config(cli.no_cache)); @@ -33,7 +37,7 @@ pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> let packages: Vec<String> = if args.packages.is_empty() { console_writeln_error!( - console, + io, "No package specified, opening homepage for the root package" ); // Mirrors HomeCommand's `$this->requireComposer()->getPackage()->getName()`. @@ -55,7 +59,7 @@ pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> 'outer: for repo in repos.iter() { for view in repo.find_packages(package_name).await? { package_exists = true; - if handle_package(&view, args.homepage, args.show, console)? { + if handle_package(&view, args.homepage, args.show, io.clone())? { handled = true; break 'outer; } @@ -64,11 +68,7 @@ pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> if !package_exists { return_code = 1; - console_writeln_error!( - console, - "<warning>Package {} not found</warning>", - package_name, - ); + console_writeln_error!(io, "<warning>Package {} not found</warning>", package_name,); } if !handled { @@ -78,7 +78,7 @@ pub async fn execute(args: &BrowseArgs, cli: &super::Cli, console: &Console) -> } else { "Invalid or missing repository URL" }; - console_writeln_error!(console, "<warning>{} for {}</warning>", kind, package_name); + console_writeln_error!(io, "<warning>{} for {}</warning>", kind, package_name); } } @@ -108,7 +108,7 @@ fn handle_package( view: &CompletePackageView, show_homepage: bool, show_only: bool, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<bool> { let mut url = view .support_source @@ -123,10 +123,11 @@ fn handle_package( }; if show_only { - console_writeln!(console, "<info>{}</info>", url); + console_writeln!(io, "<info>{}</info>", url); } else { - open_browser(&url, console)?; + open_browser(&url, io)?; } + Ok(true) } @@ -134,7 +135,10 @@ fn is_valid_url(url: &str) -> bool { url::Url::parse(url).is_ok() } -fn open_browser(url: &str, console: &Console) -> anyhow::Result<()> { +fn open_browser( + url: &str, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { #[cfg(target_os = "windows")] { Command::new("cmd") @@ -153,7 +157,7 @@ fn open_browser(url: &str, console: &Console) -> anyhow::Result<()> { Command::new("open").arg(url).status()?; } else { console_writeln_error!( - console, + io, "No suitable browser opening command found, open yourself: {}", url, ); @@ -175,8 +179,12 @@ fn which(cmd: &str) -> bool { mod tests { use super::*; - fn console() -> Console { - Console::new(0, false, false, false, true) + fn console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + std::sync::Arc::new(std::sync::Mutex::new( + Box::new(mozart_core::console::Console::new( + 0, false, false, false, true, + )) as Box<dyn IoInterface>, + )) } fn view( @@ -212,7 +220,7 @@ mod tests { Some("https://github.com/vendor/pkg.git"), Some("https://vendor.example.com"), ); - assert!(handle_package(&v, false, true, &console()).unwrap()); + assert!(handle_package(&v, false, true, console()).unwrap()); } #[test] @@ -222,13 +230,13 @@ mod tests { Some("https://github.com/vendor/pkg.git"), Some("https://vendor.example.com"), ); - assert!(handle_package(&v, false, true, &console()).unwrap()); + assert!(handle_package(&v, false, true, console()).unwrap()); } #[test] fn handle_package_falls_back_to_homepage_when_no_source() { let v = view(None, None, Some("https://vendor.example.com")); - assert!(handle_package(&v, false, true, &console()).unwrap()); + assert!(handle_package(&v, false, true, console()).unwrap()); } #[test] @@ -238,23 +246,23 @@ mod tests { Some("https://github.com/vendor/pkg.git"), Some("https://vendor.example.com"), ); - assert!(handle_package(&v, true, true, &console()).unwrap()); + assert!(handle_package(&v, true, true, console()).unwrap()); } #[test] fn handle_package_returns_false_when_no_valid_url() { let v = view(None, None, None); - assert!(!handle_package(&v, false, true, &console()).unwrap()); + assert!(!handle_package(&v, false, true, console()).unwrap()); // Invalid URL strings still cause `handlePackage` to bail. let bad = view(Some("not-a-url"), None, None); - assert!(!handle_package(&bad, false, true, &console()).unwrap()); + assert!(!handle_package(&bad, false, true, console()).unwrap()); } #[test] fn handle_package_show_homepage_with_missing_homepage_returns_false() { let v = view(Some("https://github.com/vendor/pkg"), None, None); // -H and homepage absent → falls through and bails. - assert!(!handle_package(&v, true, true, &console()).unwrap()); + assert!(!handle_package(&v, true, true, console()).unwrap()); } } diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs index f7c8142..5e8634d 100644 --- a/crates/mozart/src/commands/bump.rs +++ b/crates/mozart/src/commands/bump.rs @@ -2,7 +2,7 @@ use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; use mozart_core::composer::LocalRepository; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::package::{Link, Package}; use mozart_core::{console_writeln, console_writeln_error}; use std::collections::BTreeMap; @@ -29,12 +29,16 @@ pub struct BumpArgs { pub dry_run: bool, } -pub async fn execute(args: &BumpArgs, cli: &super::Cli, console: &Console) -> anyhow::Result<()> { +pub async fn execute( + args: &BumpArgs, + cli: &super::Cli, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let composer = Composer::require(&working_dir)?; let exit = do_bump( - console, + io, &composer, args.dev_only, args.no_dev_only, @@ -57,7 +61,7 @@ pub async fn execute(args: &BumpArgs, cli: &super::Cli, console: &Console) -> an /// warning when the package has no `type` set. `bump` itself passes `--dev-only`; /// `update --bump` will pass its own combined option name once that command is ported. pub async fn do_bump( - io: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, composer: &Composer, dev_only: bool, no_dev_only: bool, @@ -435,12 +439,12 @@ mod tests { } } - fn quiet_console() -> Console { - Console { - interactive: false, - verbosity: mozart_core::console::Verbosity::Normal, - decorated: false, - } + fn quiet_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + std::sync::Arc::new(std::sync::Mutex::new( + Box::new(mozart_core::console::Console::new( + 0, false, false, false, false, + )) as Box<dyn IoInterface>, + )) } #[tokio::test] @@ -465,8 +469,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let updated = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&updated).unwrap(); @@ -495,8 +498,7 @@ mod tests { dry_run: true, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - let result = execute(&args, &cli, &console).await; + let result = execute(&args, &cli, quiet_io()).await; // dry-run with changes returns exit code 1 (for CI usage) let err = result.unwrap_err(); @@ -533,8 +535,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); // No changes should be made let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); @@ -570,8 +571,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); @@ -609,8 +609,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); @@ -644,8 +643,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - let result = execute(&args, &cli, &console).await; + let result = execute(&args, &cli, quiet_io()).await; // stale lock file should return exit code 2 (ERROR_LOCK_OUTDATED) let err = result.unwrap_err(); @@ -677,8 +675,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); // The lock file content-hash should now match the updated composer.json let updated_composer = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); @@ -727,8 +724,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); @@ -785,8 +781,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); @@ -827,8 +822,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); @@ -869,8 +863,7 @@ mod tests { dry_run: false, }; let cli = make_cli(dir.path()); - let console = quiet_console(); - execute(&args, &cli, &console).await.unwrap(); + execute(&args, &cli, quiet_io()).await.unwrap(); let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); diff --git a/crates/mozart/src/commands/check_platform_reqs.rs b/crates/mozart/src/commands/check_platform_reqs.rs index 31cdb35..2dbcd3b 100644 --- a/crates/mozart/src/commands/check_platform_reqs.rs +++ b/crates/mozart/src/commands/check_platform_reqs.rs @@ -1,5 +1,5 @@ use clap::Args; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::installer::{InstalledCandidate, InstalledRepoLite}; @@ -59,7 +59,7 @@ struct CheckRow { pub async fn execute( args: &CheckPlatformReqsArgs, cli: &super::Cli, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let composer_json_path = working_dir.join("composer.json"); @@ -82,7 +82,7 @@ pub async fn execute( anyhow::bail!("No composer.lock found. Run `mozart install` or `mozart update` first."); } console_writeln_error!( - console, + io, "<info>Checking {}platform requirements using the lock file</info>", dev_text, ); @@ -97,14 +97,14 @@ pub async fn execute( let installed = mozart_core::repository::installed::InstalledPackages::read(&vendor_dir)?; console_writeln_error!( - console, + io, "<info>Checking {}platform requirements for packages in the vendor dir</info>", dev_text, ); load_installed(&installed, args.no_dev, &mut installed_repo, &mut requires); } else { console_writeln_error!( - console, + io, "<warning>No vendor dir present, checking {}platform requirements from the lock file</warning>", dev_text, ); @@ -238,7 +238,7 @@ pub async fn execute( exit_code = exit_code.max(1); } - print_table(&results, format, console)?; + print_table(&results, format, io.clone())?; if exit_code != 0 { return Err(mozart_core::exit_code::bail_silent(exit_code)); @@ -369,7 +369,11 @@ fn push_platform_link( }); } -fn print_table(results: &[CheckRow], format: &str, console: &Console) -> anyhow::Result<()> { +fn print_table( + results: &[CheckRow], + format: &str, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { if format == "json" { let rows: Vec<serde_json::Value> = results .iter() @@ -401,7 +405,7 @@ fn print_table(results: &[CheckRow], format: &str, console: &Console) -> anyhow: }) }) .collect(); - console_writeln!(console, "{}", &serde_json::to_string_pretty(&rows)?); + console_writeln!(io, "{}", &serde_json::to_string_pretty(&rows)?); return Ok(()); } @@ -440,19 +444,19 @@ fn print_table(results: &[CheckRow], format: &str, console: &Console) -> anyhow: match r.status { Status::Success => { console_writeln!( - console, + io, "<info>{padded_name}</info> <comment>{padded_version}</comment> {link_text} <info>success</info>{provider_suffix}", ); } Status::Failed => { console_writeln!( - console, + io, "<comment>{padded_name}</comment> <comment>{padded_version}</comment> {link_text} <error>failed</error>{provider_suffix}", ); } Status::Missing => { console_writeln!( - console, + io, "<comment>{padded_name}</comment> <comment>{padded_version}</comment> {link_text} <error>missing</error>{provider_suffix}", ); } @@ -465,11 +469,14 @@ fn print_table(results: &[CheckRow], format: &str, console: &Console) -> anyhow: #[cfg(test)] mod tests { use super::*; + use mozart_core::console::Console; use std::collections::BTreeMap; use tempfile::tempdir; - fn test_console() -> Console { - Console::new(0, true, false, true, true) + fn test_console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + std::sync::Arc::new(std::sync::Mutex::new( + Box::new(Console::new(0, true, false, true, true)) as Box<dyn IoInterface>, + )) } fn write_lock( @@ -686,7 +693,7 @@ mod tests { let console = test_console(); // Capture by rendering through serde directly (the print_table writer // goes to stdout via a macro — keep the assertion on the JSON shape). - print_table(&[row.clone()], "json", &console).unwrap(); + print_table(&[row.clone()], "json", console).unwrap(); // Reproduce the same shape and assert key invariants. let value = serde_json::json!({ diff --git a/crates/mozart/src/commands/clear_cache.rs b/crates/mozart/src/commands/clear_cache.rs index 6a601da..dbca5c8 100644 --- a/crates/mozart/src/commands/clear_cache.rs +++ b/crates/mozart/src/commands/clear_cache.rs @@ -1,10 +1,10 @@ -use std::{borrow::Cow, path::Path}; - use crate::composer::Composer; use clap::Args; +use mozart_core::console::IoInterface; use mozart_core::console_writeln_error; use mozart_core::factory::create_config; use mozart_core::repository::cache::Cache; +use std::{borrow::Cow, path::Path}; #[derive(Args)] pub struct ClearCacheArgs { @@ -16,7 +16,7 @@ pub struct ClearCacheArgs { pub async fn execute( args: &ClearCacheArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let composer = Composer::try_load(cli.working_dir()?)?; let config = if let Some(composer) = &composer { @@ -42,7 +42,7 @@ pub async fn execute( if !path.exists() { console_writeln_error!( - console, + io, "<info>Cache directory does not exist ({key}): {}</info>", path.display(), ); @@ -52,7 +52,7 @@ pub async fn execute( let cache = Cache::new(path.to_owned(), config.cache_read_only); if !cache.is_enabled() { console_writeln_error!( - console, + io, "<info>Cache is not enabled ({key}): {}</info>", path.display(), ); @@ -61,7 +61,7 @@ pub async fn execute( if args.gc { console_writeln_error!( - console, + io, "<info>Garbage-collecting cache ({key}): {}</info>", path.display(), ); @@ -73,7 +73,7 @@ pub async fn execute( }; } else { console_writeln_error!( - console, + io, "<info>Clearing cache ({key}): {}</info>", path.display(), ); @@ -82,9 +82,9 @@ pub async fn execute( } if args.gc { - console_writeln_error!(console, "<info>All caches garbage-collected.</info>"); + console_writeln_error!(io, "<info>All caches garbage-collected.</info>"); } else { - console_writeln_error!(console, "<info>All caches cleared.</info>"); + console_writeln_error!(io, "<info>All caches cleared.</info>"); } Ok(()) diff --git a/crates/mozart/src/commands/completion.rs b/crates/mozart/src/commands/completion.rs index 7cae278..3622f07 100644 --- a/crates/mozart/src/commands/completion.rs +++ b/crates/mozart/src/commands/completion.rs @@ -1,6 +1,7 @@ use clap::Args; use clap::CommandFactory; use clap_complete::aot::Shell; +use mozart_core::console::IoInterface; #[derive(Args)] pub struct CompletionArgs { @@ -12,7 +13,7 @@ pub struct CompletionArgs { pub async fn execute( args: &CompletionArgs, _cli: &super::Cli, - _console: &mozart_core::console::Console, + _io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let shell = match args.shell { Some(s) => s, diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 5012163..7f4fd06 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -5,6 +5,7 @@ use anyhow::anyhow; use clap::Args; use mozart_core::composer::composer_home; use mozart_core::config::resolve_references; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::factory::create_config; use std::collections::BTreeMap; @@ -407,7 +408,7 @@ fn load_config_section( pub async fn execute( args: &ConfigArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { // 1. Handle --editor mode if args.editor { @@ -429,7 +430,7 @@ pub async fn execute( } // 4b. Read mode - execute_read(args, cli, &config_file_path, console) + execute_read(args, cli, &config_file_path, io.clone()) } fn execute_editor(args: &ConfigArgs, cli: &super::Cli) -> anyhow::Result<()> { @@ -972,7 +973,7 @@ fn execute_read( args: &ConfigArgs, cli: &super::Cli, config_file_path: &Path, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { // Build the effective config for config-section keys. // Global baseline (defaults + platform dirs + $COMPOSER_HOME/config.json), @@ -997,7 +998,7 @@ fn execute_read( if args.list { for (key, value) in config.entries() { console_writeln!( - console, + io, mozart_core::console::Verbosity::Quiet, "[{}] {}", key, @@ -1020,7 +1021,7 @@ fn execute_read( for entry in repos { if entry.get("name").and_then(|n| n.as_str()) == Some(repo_name) { console_writeln!( - console, + io, mozart_core::console::Verbosity::Quiet, "{}", &render_value(entry), @@ -1037,7 +1038,7 @@ fn execute_read( let raw = read_json_file(config_file_path, args.global)?; if let Some(v) = get_nested(&raw, key) { console_writeln!( - console, + io, mozart_core::console::Verbosity::Quiet, "{}", &render_value(v), @@ -1052,7 +1053,7 @@ fn execute_read( let raw = read_json_file(config_file_path, args.global)?; if let Some(v) = raw.get(key.as_str()) { console_writeln!( - console, + io, mozart_core::console::Verbosity::Quiet, "{}", &render_value(v), @@ -1066,7 +1067,7 @@ fn execute_read( match config.get(key) { Some(value) => { console_writeln!( - console, + io, mozart_core::console::Verbosity::Quiet, "{}", &render_value(&value), diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 2b2fbe1..276bd3a 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -1,6 +1,6 @@ use clap::Args; use indexmap::IndexMap; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::package::{self, Stability}; use mozart_core::repository::downloader; @@ -146,12 +146,15 @@ fn dir_from_package_name(package_name: &str) -> &str { } /// Remove VCS metadata directories from the target directory. -fn remove_vcs_metadata(target_dir: &Path, console: &Console) -> anyhow::Result<()> { +fn remove_vcs_metadata( + target_dir: &Path, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { for vcs_dir in VCS_DIRS { let path = target_dir.join(vcs_dir); if path.exists() { std::fs::remove_dir_all(&path)?; - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<comment>Removed VCS metadata directory: {vcs_dir}</comment>" )); } @@ -280,29 +283,29 @@ fn version_satisfies_constraint(packagist_version: &str, constraint: &str) -> bo pub async fn execute( args: &CreateProjectArgs, cli: &super::Cli, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { // --- Deprecated / aliased flags --- if args.dev { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>You are using the deprecated option \"dev\". Dev packages are installed by default now.</warning>" )); } if args.no_custom_installers { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>You are using the deprecated option \"no-custom-installers\". Use \"no-plugins\" instead.</warning>" )); } // --- --ask interactive prompt for the project directory --- - let directory_arg: Option<String> = if console.interactive && args.ask { + let directory_arg: Option<String> = if io.lock().unwrap().is_interactive() && args.ask { let package = args .package .as_deref() .ok_or_else(|| anyhow::anyhow!("Not enough arguments (missing: \"package\")."))?; let lower = package.to_lowercase(); let basename = dir_from_package_name(&lower).to_string(); - let answer = console.ask( + let answer = io.lock().unwrap().ask( &console_format!("New project directory [<comment>{basename}</comment>]: "), &basename, ); @@ -334,7 +337,7 @@ pub async fn execute( let secure_http = !args.no_secure_http; install_project( - console, + &io, cli, args, args.package.as_deref(), @@ -359,7 +362,7 @@ pub async fn execute( #[allow(clippy::too_many_arguments)] async fn install_project( - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, cli: &super::Cli, args: &CreateProjectArgs, package_name: Option<&str>, @@ -382,7 +385,7 @@ async fn install_project( // Mozart does not yet support custom repositories on the create-project // command — warn and ignore (deferred; tracked under priority 2). if repositories.is_some() || add_repository { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>Custom repository options (--repository, --repository-url, --add-repository) \ are not yet supported and will be ignored.</warning>" )); @@ -392,7 +395,7 @@ async fn install_project( let root_result = if let Some(name) = package_name { Some( install_root_package( - console, + io, cli, args, name, @@ -433,18 +436,17 @@ async fn install_project( let mut vcs_removed = false; if !args.keep_vcs { let should_remove = if installed_from_vcs { - args.remove_vcs - || !console.interactive - || console.confirm(&console_format!( - "<info>Do you want to remove the existing VCS (.git, .svn..) history?</info> [<comment>y,n</comment>]? " - )) + let remove_vcs_confirmed = io.lock().unwrap().confirm(&console_format!( + "<info>Do you want to remove the existing VCS (.git, .svn..) history?</info> [<comment>y,n</comment>]? " + )); + args.remove_vcs || !io.lock().unwrap().is_interactive() || remove_vcs_confirmed } else { // Default for dist installs: scrub VCS metadata that may have been // shipped inside the archive (matches Mozart's pre-split behaviour). true }; if should_remove { - remove_vcs_metadata(&target_dir, console)?; + remove_vcs_metadata(&target_dir, io.clone())?; vcs_removed = true; } } @@ -452,7 +454,7 @@ async fn install_project( // --- Read composer.json from the new project --- let composer_path = target_dir.join("composer.json"); if !composer_path.exists() { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>No composer.json found in {}. Skipping dependency installation.</warning>", target_dir.display() )); @@ -468,7 +470,7 @@ async fn install_project( } if no_install { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<comment>Skipping dependency installation (--no-install).</comment>" )); return Ok(()); @@ -542,7 +544,7 @@ async fn install_project( block_insecure: false, }; - console.info("Resolving dependencies..."); + io.lock().unwrap().info("Resolving dependencies..."); let resolved = resolver::resolve(&request).await.map_err(|e| { mozart_core::exit_code::bail( @@ -573,7 +575,7 @@ async fn install_project( .filter(|c| matches!(c.kind, super::update::ChangeKind::Install { .. })) .collect(); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Package operations: {} install{}, 0 updates, 0 removals</info>", installs.len(), if installs.len() == 1 { "" } else { "s" } @@ -581,18 +583,20 @@ async fn install_project( for change in &changes { if let super::update::ChangeKind::Install { new_version } = &change.kind { - console.info(&format!(" - Installing {} ({})", change.name, new_version)); + io.lock() + .unwrap() + .info(&format!(" - Installing {} ({})", change.name, new_version)); } } - console.info("Writing lock file"); + io.lock().unwrap().info("Writing lock file"); let lock_path = target_dir.join("composer.lock"); new_lock.write_to_file(&lock_path)?; let vendor_dir = target_dir.join("vendor"); if prefer_source { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>Source installs are not yet supported. Falling back to dist.</warning>" )); } @@ -633,7 +637,7 @@ async fn install_project( download_only: false, prefer_source: args.prefer_source, }, - console, + io.clone(), &mut executor, ) .await?; @@ -643,7 +647,7 @@ async fn install_project( #[allow(clippy::too_many_arguments)] async fn install_root_package( - console: &Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, cli: &super::Cli, _args: &CreateProjectArgs, package_name: &str, @@ -704,7 +708,7 @@ async fn install_root_package( } let short = shortest_path(&working_dir, &target_dir); - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<info>Creating a \"{package_name}\" project at \"{short}\"</info>" )); @@ -760,11 +764,13 @@ async fn install_root_package( let concrete_version = best.version.clone(); // --- Print "Installing" line + plugin notice --- - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<info>Installing {name} ({concrete_version})</info>" )); if disable_plugins { - console.write_error(&console_format!("<info>Plugins have been disabled.</info>")); + io.lock() + .unwrap() + .write_error(&console_format!("<info>Plugins have been disabled.</info>")); } // --- Create the target directory and download + extract the dist archive --- @@ -800,7 +806,7 @@ async fn install_root_package( // Mozart only supports dist downloads today, so this is always false. let installed_from_vcs = false; - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<info>Created project in {}</info>", target_dir.display() )); diff --git a/crates/mozart/src/commands/dependency.rs b/crates/mozart/src/commands/dependency.rs index 0bdd3da..70d1644 100644 --- a/crates/mozart/src/commands/dependency.rs +++ b/crates/mozart/src/commands/dependency.rs @@ -4,13 +4,13 @@ //! `prohibits` (aka `why-not`) answers: "Which packages prevent version X of package Y from being //! installed?" -use indexmap::IndexSet; -use std::collections::BTreeMap; -use std::path::Path; - use anyhow::Result; +use indexmap::IndexSet; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; +use std::collections::BTreeMap; +use std::path::Path; /// Inputs for [`do_execute`], collected from the `depends` / `prohibits` CLI args. pub struct DoExecuteArgs<'a> { @@ -31,7 +31,7 @@ pub struct DoExecuteArgs<'a> { /// "who prevents X version V from being installed?". pub fn do_execute( cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, args: DoExecuteArgs<'_>, ) -> Result<()> { let DoExecuteArgs { @@ -48,7 +48,7 @@ pub fn do_execute( let packages = load_packages(&working_dir, locked)?; if packages.is_empty() { - console.write_error( + io.lock().unwrap().write_error( "No dependencies installed. Try running mozart install or update, or use --locked.", ); return Err(mozart_core::exit_code::bail_silent( @@ -91,14 +91,14 @@ pub fn do_execute( if results.is_empty() { if inverted { console_writeln!( - console, + io, "<info>{} {} can be installed.</info>", package, version.unwrap_or(""), ); return Ok(()); } - console.info(&format!( + io.lock().unwrap().info(&format!( "There is no installed package depending on \"{}\"", package )); @@ -108,9 +108,9 @@ pub fn do_execute( } if tree { - print_tree(&results, 0, console); + print_tree(&results, 0, io.clone()); } else { - print_table(&results, console); + print_table(&results, io.clone()); } if !inverted { @@ -142,7 +142,7 @@ pub fn do_execute( }) .unwrap_or("update"); - console.info(&format!( + io.lock().unwrap().info(&format!( "Not finding what you were looking for? Try calling `composer {} \"{}:{}\" --dry-run` to get another view on the problem.", composer_command, package, @@ -644,9 +644,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], console: &mozart_core::console::Console) { +pub fn print_table( + results: &[DependencyResult], + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { if results.is_empty() { - console_writeln!(console, "<info>No relationships found.</info>"); + console_writeln!(io, "<info>No relationships found.</info>"); return; } @@ -677,7 +680,7 @@ pub fn print_table(results: &[DependencyResult], console: &mozart_core::console: continue; } console_writeln!( - console, + io, "{:<name_w$} {:<ver_w$} {:<desc_w$} {}", console_format!("<info>{}</info>", r.package_name), console_format!("<comment>{}</comment>", r.package_version), @@ -702,10 +705,10 @@ pub fn print_table(results: &[DependencyResult], console: &mozart_core::console: pub fn print_tree( results: &[DependencyResult], depth: usize, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { if results.is_empty() && depth == 0 { - console_writeln!(console, "<info>No relationships found.</info>"); + console_writeln!(io, "<info>No relationships found.</info>"); return; } @@ -715,7 +718,7 @@ pub fn print_tree( let prefix = tree_prefix(depth, is_last); console_writeln!( - console, + io, "{}{:<} {} {} {}", prefix, console_format!("<info>{}</info>", r.package_name), @@ -725,7 +728,7 @@ pub fn print_tree( ); if !r.children.is_empty() { - print_tree(&r.children, depth + 1, console); + print_tree(&r.children, depth + 1, io.clone()); } } } @@ -896,15 +899,21 @@ mod tests { ); } + fn test_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + std::sync::Arc::new(std::sync::Mutex::new( + Box::new(mozart_core::console::Console::new( + 0, false, false, false, false, + )) as Box<dyn IoInterface>, + )) + } + #[test] fn test_print_table_empty() { - let console = mozart_core::console::Console::new(0, false, false, false, false); - print_table(&[], &console); + print_table(&[], test_io()); } #[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(), @@ -913,18 +922,16 @@ mod tests { link_constraint: "^2.0".to_string(), children: vec![], }]; - print_table(&results, &console); + print_table(&results, test_io()); } #[test] fn test_print_tree_empty() { - let console = mozart_core::console::Console::new(0, false, false, false, false); - print_tree(&[], 0, &console); + print_tree(&[], 0, test_io()); } #[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(), @@ -940,6 +947,6 @@ mod tests { children: vec![], }], }]; - print_tree(&results, 0, &console); + print_tree(&results, 0, test_io()); } } diff --git a/crates/mozart/src/commands/depends.rs b/crates/mozart/src/commands/depends.rs index 9324b82..4a11176 100644 --- a/crates/mozart/src/commands/depends.rs +++ b/crates/mozart/src/commands/depends.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::IoInterface; #[derive(Args)] pub struct DependsArgs { @@ -21,11 +22,11 @@ pub struct DependsArgs { pub async fn execute( args: &DependsArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { super::dependency::do_execute( cli, - console, + io, super::dependency::DoExecuteArgs { package: &args.package, version: None, diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs index 2e171e5..73047c0 100644 --- a/crates/mozart/src/commands/diagnose.rs +++ b/crates/mozart/src/commands/diagnose.rs @@ -4,7 +4,7 @@ use colored::Colorize; use mozart_core::MOZART_VERSION; use mozart_core::config::Config; use mozart_core::config_validator::{ValidatorOptions, validate_manifest}; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::factory::create_config; use mozart_core::http::HttpDownloader; @@ -53,37 +53,42 @@ impl CheckResult { /// messages, `<error>FAIL</>` + messages, or `<info>SKIP</>` + reason. /// /// Ratchets `exit_code`: `Warning` → 1 (if currently 0), `Fail` → 2 (always). -fn output_result(label: &str, result: &CheckResult, exit_code: &mut i32, console: &Console) { +fn output_result( + label: &str, + result: &CheckResult, + exit_code: &mut i32, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { let prefix = format!("Checking {label}: "); match result { CheckResult::Ok(detail) => { let ok = "OK".green().bold(); match detail { Some(d) => { - console_writeln!(console, "{prefix}{ok} {}", format!("({d})").bright_black()) + console_writeln!(io, "{prefix}{ok} {}", format!("({d})").bright_black()) } - None => console_writeln!(console, "{prefix}{ok}"), + None => console_writeln!(io, "{prefix}{ok}"), } } CheckResult::Warning(msgs) => { - console_writeln!(console, "{prefix}{}", "WARNING".yellow().bold()); + console_writeln!(io, "{prefix}{}", "WARNING".yellow().bold()); for msg in msgs { - console_writeln!(console, "{}", msg.yellow()); + console_writeln!(io, "{}", msg.yellow()); } if *exit_code < 1 { *exit_code = 1; } } CheckResult::Fail(msgs) => { - console_writeln!(console, "{prefix}{}", "FAIL".red().bold()); + console_writeln!(io, "{prefix}{}", "FAIL".red().bold()); for msg in msgs { - console_writeln!(console, "{}", msg.red()); + console_writeln!(io, "{}", msg.red()); } *exit_code = 2; } CheckResult::Skip(reason) => { console_writeln!( - console, + io, "{prefix}{} {}", "SKIP".cyan().bold(), format!("({reason})").bright_black(), @@ -351,7 +356,7 @@ fn parse_df_available_kib(df_output: &str) -> Option<u64> { pub async fn execute( _args: &DiagnoseArgs, cli: &super::Cli, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; @@ -370,7 +375,7 @@ pub async fn execute( // Step 4b (`checkVersion`) is deferred until self-update lands. // Step 5: Mozart version line. - console_writeln!(console, "Mozart version {MOZART_VERSION}"); + console_writeln!(io, "Mozart version {MOZART_VERSION}"); // Step 6: Mozart and its dependencies for vulnerabilities. Deferred — needs // a Mozart Auditor port. @@ -378,7 +383,7 @@ pub async fn execute( "Mozart and its dependencies for vulnerabilities", &CheckResult::Skip("audit is not yet implemented in Mozart".to_string()), &mut exit_code, - console, + io.clone(), ); // Steps 7-8 (PHP/OpenSSL/curl/zip detection) are PHP-runtime concerns @@ -390,7 +395,7 @@ pub async fn execute( "composer.json", &check_composer_schema(&working_dir), &mut exit_code, - console, + io.clone(), ); let lock_path = working_dir.join("composer.lock"); @@ -399,7 +404,7 @@ pub async fn execute( "composer.lock", &check_composer_lock_schema(&lock_path), &mut exit_code, - console, + io.clone(), ); } } @@ -409,24 +414,24 @@ pub async fn execute( "platform settings", &CheckResult::Skip("platform settings checks are not applicable to Mozart".to_string()), &mut exit_code, - console, + io.clone(), ); // Step 11: git settings. - output_result("git settings", &check_git(), &mut exit_code, console); + output_result("git settings", &check_git(), &mut exit_code, io.clone()); // Step 12: HTTP / HTTPS connectivity to packagist. output_result( "http connectivity to packagist", &check_http("http", &http_downloader, &config).await, &mut exit_code, - console, + io.clone(), ); output_result( "https connectivity to packagist", &check_http("https", &http_downloader, &config).await, &mut exit_code, - console, + io.clone(), ); // Step 13: every additional `composer`-type repo. @@ -448,7 +453,7 @@ pub async fn execute( &format!("connectivity to {url}"), &check_composer_repo(url, &http_downloader, &config).await, &mut exit_code, - console, + io.clone(), ); } } @@ -463,7 +468,7 @@ pub async fn execute( "disk free space", &check_disk_space(&config), &mut exit_code, - console, + io.clone(), ); // Mirrors the `COMPOSER_IPRESOLVE` warning emitted by `checkPlatform`. @@ -471,7 +476,7 @@ pub async fn execute( && (val == "4" || val == "6") { console_writeln!( - console, + io, "{}", format!("The COMPOSER_IPRESOLVE env var is set to {val} which may result in network failures below.").yellow(), ); @@ -536,28 +541,32 @@ mod tests { #[test] fn test_output_result_exit_code_ratcheting() { - let console = Console::new(0, false, false, false, false); + let console: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> = std::sync::Arc::new( + std::sync::Mutex::new(Box::new(mozart_core::console::Console::new( + 0, false, false, false, false, + )) as Box<dyn IoInterface>), + ); let mut exit_code = 0i32; - output_result("label", &CheckResult::ok(), &mut exit_code, &console); + output_result("label", &CheckResult::ok(), &mut exit_code, console.clone()); assert_eq!(exit_code, 0); output_result( "label", &CheckResult::warn("warn"), &mut exit_code, - &console, + console.clone(), ); assert_eq!(exit_code, 1); - output_result("label", &CheckResult::ok(), &mut exit_code, &console); + output_result("label", &CheckResult::ok(), &mut exit_code, console.clone()); assert_eq!(exit_code, 1); output_result( "label", &CheckResult::fail("fail"), &mut exit_code, - &console, + console.clone(), ); assert_eq!(exit_code, 2); @@ -565,7 +574,7 @@ mod tests { "label", &CheckResult::warn("another warn"), &mut exit_code, - &console, + console, ); assert_eq!(exit_code, 2); } diff --git a/crates/mozart/src/commands/dump_autoload.rs b/crates/mozart/src/commands/dump_autoload.rs index f8222bb..fa6c112 100644 --- a/crates/mozart/src/commands/dump_autoload.rs +++ b/crates/mozart/src/commands/dump_autoload.rs @@ -2,6 +2,7 @@ use crate::composer::Composer; use clap::Args; use mozart_core::autoload::AutoloadGeneratorExt; use mozart_core::composer::AutoloadDumpOptions; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; #[derive(Args, Default)] @@ -54,7 +55,7 @@ pub struct DumpAutoloadArgs { pub async fn execute( args: &DumpAutoloadArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let composer = Composer::require(cli.working_dir()?)?; @@ -71,7 +72,7 @@ pub async fn execute( { missing = true; console_writeln!( - console, + io, r#"<warning>Not all dependencies are installed. Make sure to run a "composer install" to install missing dependencies</warning>"#, ); break; @@ -99,13 +100,13 @@ pub async fn execute( if authoritative { console_writeln!( - console, + io, "<info>Generating optimized autoload files (authoritative)</info>", ); } else if optimize { - console_writeln!(console, "<info>Generating optimized autoload files</info>"); + console_writeln!(io, "<info>Generating optimized autoload files</info>"); } else { - console_writeln!(console, "<info>Generating autoload files</info>"); + console_writeln!(io, "<info>Generating autoload files</info>"); } let dev_mode = if args.dev { @@ -147,16 +148,16 @@ pub async fn execute( if authoritative { console_writeln!( - console, + io, "<info>Generated optimized autoload files (authoritative) containing {number_of_classes} classes</info>", ); } else if optimize { console_writeln!( - console, + io, "<info>Generated optimized autoload files containing {number_of_classes} classes</info>", ); } else { - console_writeln!(console, "<info>Generated autoload files</info>"); + console_writeln!(io, "<info>Generated autoload files</info>"); } if missing_dependencies || args.strict_psr && class_map.has_psr_violations() { diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index 63c29c9..9cbb478 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -1,7 +1,7 @@ use crate::composer::Composer; use clap::Args; -use mozart_core::console_writeln; use mozart_core::package::Package; +use mozart_core::{console::IoInterface, console_writeln}; use std::path::{Path, PathBuf}; #[derive(Args)] @@ -21,7 +21,7 @@ pub struct ExecArgs { pub async fn execute( args: &ExecArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; @@ -36,12 +36,12 @@ pub async fn execute( bin_dir.display(), ); } - console_writeln!(console, "<comment>Available binaries:</comment>"); + console_writeln!(io, "<comment>Available binaries:</comment>"); for (bin, is_local) in &bins { if *is_local { - console_writeln!(console, "<info>- {bin} (local)</info>"); + console_writeln!(io, "<info>- {bin} (local)</info>"); } else { - console_writeln!(console, "<info>- {bin}</info>"); + console_writeln!(io, "<info>- {bin}</info>"); } } return Ok(()); diff --git a/crates/mozart/src/commands/fund.rs b/crates/mozart/src/commands/fund.rs index 792edd6..85cd8c3 100644 --- a/crates/mozart/src/commands/fund.rs +++ b/crates/mozart/src/commands/fund.rs @@ -1,6 +1,6 @@ use crate::composer::Composer; use clap::Args; -use mozart_core::console::{Console, hyperlink}; +use mozart_core::console::{IoInterface, hyperlink}; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::exit_code; @@ -17,10 +17,14 @@ pub struct FundArgs { pub format: Option<String>, } -pub async fn execute(args: &FundArgs, cli: &super::Cli, console: &Console) -> anyhow::Result<()> { +pub async fn execute( + args: &FundArgs, + cli: &super::Cli, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { let format = args.format.as_deref().unwrap_or("text"); if !matches!(format, "text" | "json") { - console.error(&console_format!( + io.lock().unwrap().error(&console_format!( "<error>Unsupported format \"{format}\". See help for supported formats.</error>" )); return Err(exit_code::bail_silent(exit_code::GENERAL_ERROR)); @@ -92,8 +96,8 @@ pub async fn execute(args: &FundArgs, cli: &super::Cli, console: &Console) -> an // BTreeMap iteration is alphabetical — covers `ksort($fundings)`. match format { - "json" => render_json(&fundings, console)?, - _ => render_text(&fundings, console), + "json" => render_json(&fundings, io.clone())?, + _ => render_text(&fundings, io.clone()), } Ok(()) @@ -139,10 +143,13 @@ fn rewrite_github_url(url: &str, funding_type: Option<&str>) -> String { url.to_string() } -fn render_text(fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>, console: &Console) { +fn render_text( + fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { if fundings.is_empty() { console_writeln!( - console, + io, "No funding links were found in your package dependencies. \ This doesn't mean they don't need your support!", ); @@ -150,36 +157,36 @@ fn render_text(fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>, conso } console_writeln!( - console, + io, "The following packages were found in your dependencies which publish funding information:", ); let mut prev: Option<String> = None; for (vendor, url_map) in fundings { - console_writeln!(console, ""); - console_writeln!(console, "<comment>{vendor}</comment>"); + console_writeln!(io, ""); + console_writeln!(io, "<comment>{vendor}</comment>"); for (url, packages) in url_map { let line = format!(" <info>{}</info>", packages.join(", ")); if prev.as_deref() != Some(line.as_str()) { - console_writeln!(console, "{line}"); + console_writeln!(io, "{line}"); prev = Some(line); } - let link = hyperlink(url, url, console.decorated); - console_writeln!(console, " {link}"); + let link = hyperlink(url, url, io.lock().unwrap().is_decorated()); + console_writeln!(io, " {link}"); } } - console_writeln!(console, ""); + console_writeln!(io, ""); console_writeln!( - console, + io, "Please consider following these links and sponsoring the work of package authors!", ); - console_writeln!(console, "Thank you!"); + console_writeln!(io, "Thank you!"); } fn render_json( fundings: &BTreeMap<String, BTreeMap<String, Vec<String>>>, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let buf = Vec::new(); let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); @@ -192,13 +199,14 @@ fn render_json( } else { fundings.serialize(&mut ser)?; } - console_writeln!(console, "{}", &String::from_utf8(ser.into_inner())?); + console_writeln!(io, "{}", &String::from_utf8(ser.into_inner())?); Ok(()) } #[cfg(test)] mod tests { use super::*; + use mozart_core::console::Console; fn make_funding_json(entries: &[(&str, &str)]) -> Vec<serde_json::Value> { entries diff --git a/crates/mozart/src/commands/global.rs b/crates/mozart/src/commands/global.rs index 05e8aae..5d5cb81 100644 --- a/crates/mozart/src/commands/global.rs +++ b/crates/mozart/src/commands/global.rs @@ -1,5 +1,5 @@ use clap::Args; -use mozart_core::composer::composer_home; +use mozart_core::{composer::composer_home, console::IoInterface}; #[derive(Args)] pub struct GlobalArgs { @@ -14,7 +14,7 @@ pub struct GlobalArgs { pub async fn execute( args: &GlobalArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { use clap::Parser as _; use std::fs; @@ -32,7 +32,9 @@ pub async fn execute( fs::create_dir_all(&home)?; - console.info(&format!("Changed current directory to {}", home.display())); + io.lock() + .unwrap() + .info(&format!("Changed current directory to {}", home.display())); // SAFETY: single-threaded at this point; no concurrent env access unsafe { diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs index 90a5806..2008e8e 100644 --- a/crates/mozart/src/commands/init.rs +++ b/crates/mozart/src/commands/init.rs @@ -1,7 +1,7 @@ use anyhow::{Context, bail}; use clap::Args; use colored::Colorize; -use mozart_core::console; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::package::{ self, RawAuthor, RawAutoload, RawPackageData, RawRepository, Stability, @@ -64,7 +64,7 @@ pub struct InitArgs { pub async fn execute( args: &InitArgs, cli: &super::Cli, - console: &console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); @@ -85,29 +85,31 @@ pub async fn execute( ); } - let composer = if console.interactive { - build_interactive(args, console, &working_dir, &repo_cache).await? + let composer = if io.lock().unwrap().is_interactive() { + build_interactive(args, &io, &working_dir, &repo_cache).await? } else { build_non_interactive(args, &working_dir)? }; let json = package::to_json_pretty(&composer)?; - if console.interactive { - console.info(""); - console.info(&json); - console.info(""); + if io.lock().unwrap().is_interactive() { + io.lock().unwrap().info(""); + io.lock().unwrap().info(&json); + io.lock().unwrap().info(""); - if !console.confirm(&console_format!( + if !io.lock().unwrap().confirm(&console_format!( "Do you confirm generation [<comment>yes</comment>]?" )) { - console.error("Command aborted"); + io.lock().unwrap().error("Command aborted"); return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, )); } } else { - console.info(&format!("Writing {}", composer_file.display())); + io.lock() + .unwrap() + .info(&format!("Writing {}", composer_file.display())); } package::write_to_file(&composer, &composer_file).context("Failed to write composer.json")?; @@ -129,17 +131,19 @@ pub async fn execute( if !has_dependencies { let dump_args = super::dump_autoload::DumpAutoloadArgs::default(); - if let Err(e) = super::dump_autoload::execute(&dump_args, cli, console).await { - console.error(&format!("Could not run dump-autoload. ({e})")); + if let Err(e) = super::dump_autoload::execute(&dump_args, cli, io.clone()).await { + io.lock() + .unwrap() + .error(&format!("Could not run dump-autoload. ({e})")); } } } // Offer to add /vendor/ to .gitignore - if console.interactive && working_dir.join(".git").is_dir() { + if io.lock().unwrap().is_interactive() && working_dir.join(".git").is_dir() { let gitignore_path = working_dir.join(".gitignore"); if !has_vendor_ignore(&gitignore_path) - && console.confirm(&console_format!( + && io.lock().unwrap().confirm(&console_format!( "Would you like the <info>vendor</info> directory added to your <info>.gitignore</info> [<comment>yes</comment>]?" )) { @@ -149,15 +153,15 @@ pub async fn execute( // Run `composer update` after init when the new project has dependencies // and the user confirms — Composer's L190-193. - if console.interactive + if io.lock().unwrap().is_interactive() && has_dependencies - && console.confirm(&console_format!( + && io.lock().unwrap().confirm(&console_format!( "Would you like to install dependencies now [<comment>yes</comment>]?" )) { let update_args = super::update::UpdateArgs::default(); - if let Err(e) = super::update::execute(&update_args, cli, console).await { - console.error(&format!( + if let Err(e) = super::update::execute(&update_args, cli, io.clone()).await { + io.lock().unwrap().error(&format!( "Could not update dependencies. Run `composer update` to see more information. ({e})" )); } @@ -167,10 +171,10 @@ pub async fn execute( if let Some(ref autoload) = composer.autoload && let Some((ns, path)) = autoload.psr4.iter().next() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "PSR-4 autoloading configured. Use \"<comment>namespace {ns};</comment>\" in {path}" )); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "Include the Composer autoloader with: <comment>require 'vendor/autoload.php';</comment>" )); } @@ -233,31 +237,33 @@ fn build_non_interactive(args: &InitArgs, working_dir: &Path) -> anyhow::Result< async fn build_interactive( args: &InitArgs, - console: &console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, working_dir: &Path, repo_cache: &mozart_core::repository::cache::Cache, ) -> anyhow::Result<RawPackageData> { - console.info(""); - console.info(&format!( + io.lock().unwrap().info(""); + io.lock().unwrap().info(&format!( " {} ", "Welcome to the Mozart config generator".white().on_blue() )); - console.info(""); - console.info("This command will guide you through creating your composer.json config."); - console.info(""); + io.lock().unwrap().info(""); + io.lock() + .unwrap() + .info("This command will guide you through creating your composer.json config."); + io.lock().unwrap().info(""); // Package name let default_name = args .name .clone() .unwrap_or_else(|| get_default_package_name(working_dir)); - let name = console.ask_validated( + let name = io.lock().unwrap().ask_validated( &console_format!( "Package name (<vendor>/<name>) [<comment>{}</comment>]", &default_name, ), &default_name, - |val| { + Box::new(|val| { if validation::validate_package_name(val) { Ok(()) } else { @@ -265,13 +271,13 @@ async fn build_interactive( "The package name {val} is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name" )) } - }, + }), ) .map_err(|e| anyhow::anyhow!(e))?; // Description let default_desc = args.description.clone().unwrap_or_default(); - let description = console.ask( + let description = io.lock().unwrap().ask( &console_format!("Description [<comment>{}</comment>]", &default_desc), &default_desc, ); @@ -287,7 +293,7 @@ async fn build_interactive( .clone() .or_else(get_default_author) .unwrap_or_default(); - let author_input = console.ask( + let author_input = io.lock().unwrap().ask( &if !default_author.is_empty() { console_format!("Author [<comment>{}</comment>, n to skip]", &default_author) } else { @@ -311,14 +317,14 @@ async fn build_interactive( // validator throws InvalidArgumentException, Symfony's QuestionHelper // catches it and re-prompts when maxAttempts is null). let default_stability = args.stability.clone().unwrap_or_default(); - let stability_input = console + let stability_input = io.lock().unwrap() .ask_validated( &console_format!( "Minimum Stability [<comment>{}</comment>]", &default_stability ), &default_stability, - |val| { + Box::new(|val| { if val.is_empty() || validation::validate_stability(val) { Ok(()) } else { @@ -326,7 +332,7 @@ async fn build_interactive( "Invalid minimum stability \"{val}\". Must be empty or one of: dev, alpha, beta, rc, stable" )) } - }, + }), ) .map_err(|e| anyhow::anyhow!(e))?; let minimum_stability = if stability_input.is_empty() { @@ -337,7 +343,7 @@ async fn build_interactive( // Package Type let default_type = args.r#type.clone().unwrap_or_default(); - let type_input = console.ask( + let type_input = io.lock().unwrap().ask( &console_format!( "Package Type (e.g. library, project, metapackage, composer-plugin) [<comment>{}</comment>]", &default_type, @@ -357,7 +363,7 @@ async fn build_interactive( .clone() .or_else(|| std::env::var("COMPOSER_DEFAULT_LICENSE").ok()) .unwrap_or_default(); - let license_input = console.ask( + let license_input = io.lock().unwrap().ask( &console_format!("License [<comment>{}</comment>]", &default_license), &default_license, ); @@ -379,15 +385,17 @@ async fn build_interactive( .map(Stability::parse) .unwrap_or(Stability::Stable); - console.info(""); - console.info(&console_format!("<info>Define your dependencies.</info>")); - console.info(""); + io.lock().unwrap().info(""); + io.lock() + .unwrap() + .info(&console_format!("<info>Define your dependencies.</info>")); + io.lock().unwrap().info(""); // Composer (InitCommand::interact L389-403): if --require was passed, // skip the confirmation; otherwise ask before entering the discovery loop. let mut require = parse_requirements(&args.require)?; if !require.is_empty() - || console.confirm(&console_format!( + || io.lock().unwrap().confirm(&console_format!( "Would you like to define your dependencies (require) interactively [<comment>yes</comment>]?" )) { @@ -396,7 +404,7 @@ async fn build_interactive( &require, preferred_stability, repo_cache, - console, + io, ) .await?; for (name, constraint) in interactive_require { @@ -405,15 +413,15 @@ async fn build_interactive( } // Dev Dependencies - console.info(""); - console.info(&console_format!( + io.lock().unwrap().info(""); + io.lock().unwrap().info(&console_format!( "<info>Define your dev dependencies.</info>" )); - console.info(""); + io.lock().unwrap().info(""); let mut require_dev = parse_requirements(&args.require_dev)?; if !require_dev.is_empty() - || console.confirm(&console_format!( + || io.lock().unwrap().confirm(&console_format!( "Would you like to define your dev dependencies (require-dev) interactively [<comment>yes</comment>]?" )) { @@ -427,7 +435,7 @@ async fn build_interactive( &all_required, preferred_stability, repo_cache, - console, + io, ) .await?; for (name, constraint) in interactive_dev { @@ -439,14 +447,14 @@ async fn build_interactive( // via askAndValidate (loops until valid). `n`/`no` skips. let default_autoload = args.autoload.clone().unwrap_or_else(|| "src/".to_string()); let namespace = validation::namespace_from_package_name(&name).unwrap_or_default(); - let autoload_input = console + let autoload_input = io.lock().unwrap() .ask_validated( &console_format!( "Add PSR-4 autoload mapping? Maps namespace \"{namespace}\" to the entered relative path. [<comment>{}</comment>, n to skip]", &default_autoload, ), &default_autoload, - |val| { + Box::new(|val| { if val == "n" || val == "no" || validation::validate_autoload_path(val) { Ok(()) } else { @@ -454,7 +462,7 @@ async fn build_interactive( "The src folder name \"{val}\" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/" )) } - }, + }), ) .map_err(|e| anyhow::anyhow!(e))?; let autoload = if autoload_input == "n" || autoload_input == "no" { @@ -488,7 +496,7 @@ async fn interactive_search_packages( already_required: &BTreeMap<String, String>, preferred_stability: Stability, repo_cache: &mozart_core::repository::cache::Cache, - console: &console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<BTreeMap<String, String>> { let stdin = std::io::stdin(); let mut selected: BTreeMap<String, String> = BTreeMap::new(); @@ -514,7 +522,7 @@ async fn interactive_search_packages( let (results, total) = match packagist::search_packages(&query, None).await { Ok(r) => r, Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Search failed: {e}. Try again.</warning>" )); continue; @@ -532,13 +540,13 @@ async fn interactive_search_packages( .collect(); if filtered.is_empty() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" )); continue; } - console.info(&format!( + io.lock().unwrap().info(&format!( "\nFound {} package{} for \"{}\":", filtered.len(), if filtered.len() == 1 { "" } else { "s" }, @@ -552,15 +560,17 @@ async fn interactive_search_packages( } else { format!(" — {}", result.description) }; - console.info(&format!( + io.lock().unwrap().info(&format!( " [{idx}] {:<width$}{desc}", result.name, idx = idx + 1, width = name_width, )); } - console.info(" [0] Search again / enter full package name"); - console.info(""); + io.lock() + .unwrap() + .info(" [0] Search again / enter full package name"); + io.lock().unwrap().info(""); // Ask user to pick eprint!("Enter package # or name (leave empty to finish): "); @@ -586,7 +596,7 @@ async fn interactive_search_packages( } else if num <= filtered.len() { filtered[num - 1].name.to_lowercase() } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Invalid selection: {num}</warning>" )); continue; @@ -600,19 +610,21 @@ async fn interactive_search_packages( match validation::parse_require_string(&package_name) { Ok((n, v)) => (n.to_lowercase(), v), Err(e) => { - console.info(&console_format!("<warning>Invalid: {e}</warning>")); + io.lock() + .unwrap() + .info(&console_format!("<warning>Invalid: {e}</warning>")); continue; } } } else { if !validation::validate_package_name(&package_name) { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Invalid package name: \"{package_name}\"</warning>" )); continue; } - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Using version constraint for {package_name} from Packagist...</info>" )); @@ -626,13 +638,13 @@ async fn interactive_search_packages( &best.version_normalized, stability, ); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Using version {c} for {package_name}</info>" )); (package_name, c) } None => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not find a version of \"{package_name}\" matching \ your minimum-stability. Try specifying it explicitly.</warning>" )); @@ -641,7 +653,7 @@ async fn interactive_search_packages( } } Err(e) => { - console.info(&console_format!( + io.lock().unwrap().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 4a997dd..7d4aac3 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -1,6 +1,6 @@ use clap::Args; use indexmap::IndexSet; -use mozart_core::console; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::package::{Package, RootPackage, RootPackageData}; use mozart_core::repository::installed; @@ -496,7 +496,7 @@ fn warn_platform_requirements( packages: &[&lockfile::LockedPackage], ignore_platform_reqs: bool, ignore_platform_req: &[String], - console: &console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { if ignore_platform_reqs { return; @@ -512,7 +512,7 @@ fn warn_platform_requirements( if mozart_core::platform::is_platform_package(req_name) { let lower = req_name.to_lowercase(); if !ignored_set.contains(&lower) { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Platform requirement {req_name} {req_constraint} (required by {}) \ has not been verified. Platform detection is not yet fully implemented.</warning>", pkg.name @@ -528,7 +528,7 @@ pub async fn install_from_lock( working_dir: &Path, vendor_dir: &Path, config: &InstallConfig, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, executor: &mut dyn InstallerExecutor, ) -> anyhow::Result<()> { let dev_mode = config.dev_mode; @@ -542,18 +542,24 @@ pub async fn install_from_lock( // Print install mode header if dev_mode { - console.info("Installing dependencies from lock file (including require-dev)"); + io.lock() + .unwrap() + .info("Installing dependencies from lock file (including require-dev)"); } else { - console.info("Installing dependencies from lock file"); + io.lock() + .unwrap() + .info("Installing dependencies from lock file"); } - console.info("Verifying lock file contents can be installed on current platform."); + io.lock() + .unwrap() + .info("Verifying lock file contents can be installed on current platform."); // Step 2: Warn about platform requirements warn_platform_requirements( &packages_to_install, config.ignore_platform_reqs, &config.ignore_platform_req, - console, + io.clone(), ); // Step 3: Read currently installed packages @@ -573,9 +579,11 @@ pub async fn install_from_lock( .collect(); if installs.is_empty() && updates.is_empty() && removals.is_empty() { - console.info("Nothing to install, update or remove"); + io.lock() + .unwrap() + .info("Nothing to install, update or remove"); } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Package operations: {} install{}, {} update{}, {} removal{}</info>", installs.len(), if installs.len() == 1 { "" } else { "s" }, @@ -590,20 +598,22 @@ pub async fn install_from_lock( // mirror Composer's `Transaction::moveUninstallsToFront`. if config.dry_run { for name in &removals { - console.info(&console_format!(" - Would remove <info>{}</info>", name)); + io.lock() + .unwrap() + .info(&console_format!(" - Would remove <info>{}</info>", name)); } for (pkg, action) in &ops { match action { Action::Skip => {} Action::Install => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Would install <info>{}</info> (<comment>{}</comment>)", pkg.name, pkg.version )); } Action::Update => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Would upgrade <info>{}</info> (<comment>{}</comment>)", pkg.name, pkg.version @@ -619,7 +629,9 @@ pub async fn install_from_lock( }; for name in &removals { - console.info(&console_format!(" - Removing <info>{}</info>", name)); + io.lock() + .unwrap() + .info(&console_format!(" - Removing <info>{}</info>", name)); // Mirrors Composer's `UninstallOperation::show`, which renders // the package's `getFullPrettyVersion()` — for dev packages // backed by git/hg that includes the (truncated) source ref. @@ -674,7 +686,7 @@ pub async fn install_from_lock( // new root alias on a previously-installed package. Action::Skip => None, Action::Install => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Installing <info>{}</info> (<comment>{}</comment>)", pkg.name, pkg.version @@ -682,7 +694,7 @@ pub async fn install_from_lock( Some(PackageOperation::Install { package: pkg }) } Action::Update => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Upgrading <info>{}</info> (<comment>{}</comment>)", pkg.name, pkg.version @@ -799,14 +811,14 @@ pub async fn install_from_lock( // Step 9: Generate autoloader (unless no_autoloader or download_only) if !config.no_autoloader && !config.download_only { - console.info("Generating autoload files"); + io.lock().unwrap().info("Generating autoload files"); if config.classmap_authoritative { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Classmap-authoritative mode: autoloader will only look up classes in the classmap.</info>" )); } else if config.optimize_autoloader { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Optimize autoloader: classmap scanning is not yet fully supported. PSR-4/PSR-0 autoloading will still be used.</info>" )); } @@ -839,7 +851,7 @@ pub async fn install_from_lock( pub async fn execute( args: &InstallArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repositories = std::sync::Arc::new( @@ -850,15 +862,7 @@ pub async fn execute( let mut executor = FilesystemExecutor::new(mozart_core::repository::cache::Cache::files(&cache_config)); let working_dir = cli.working_dir()?; - run( - &working_dir, - None, - args, - console, - repositories, - &mut executor, - ) - .await + run(&working_dir, None, args, io, repositories, &mut executor).await } /// Library entry point — pure logic, no `Cli` access. @@ -875,7 +879,7 @@ pub async fn run( working_dir: &Path, path_repo_base_override: Option<&Path>, args: &InstallArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, repositories: std::sync::Arc<mozart_core::repository::repository::RepositorySet>, executor: &mut dyn InstallerExecutor, ) -> anyhow::Result<()> { @@ -883,13 +887,13 @@ pub async fn run( // 1. deprecation warnings, 2. reject packages, 3. reject --no-install, // 4. Mozart-only prefer-install mutual-exclusion. if args.dev { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>You are using the deprecated option \"--dev\". It has no effect and will break in Composer 3.</warning>" )); } if args.no_suggest { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>" )); } @@ -922,7 +926,7 @@ pub async fn run( // If no lock file present, fall back to update (matching Composer behavior). let lock_path = working_dir.join("composer.lock"); if !lock_path.exists() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.</warning>" )); let update_args = super::update::UpdateArgs { @@ -964,7 +968,7 @@ pub async fn run( working_dir, path_repo_base_override, &update_args, - console, + io, repositories, executor, ) @@ -985,7 +989,7 @@ pub async fn run( if composer_json_path.exists() { let content = std::fs::read_to_string(&composer_json_path)?; if !lock.is_fresh(&content) { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `mozart update`.</warning>" )); } @@ -996,7 +1000,7 @@ pub async fn run( let missing = lock.get_missing_requirement_info(&root_pkg, dev_mode); if !missing.is_empty() { for line in &missing { - console.info(line); + io.lock().unwrap().info(line); } // Mirrors `Composer\Installer::doInstall()` lines 749-756: when // `config.allow-missing-requirements` is true, print the warnings @@ -1022,13 +1026,13 @@ pub async fn run( &args.ignore_platform_req, ); if !lock_problems.is_empty() { - console.info( + io.lock().unwrap().info( "Your lock file does not contain a compatible set of packages. Please run composer update.", ); - console.info(""); + io.lock().unwrap().info(""); for (i, msg) in lock_problems.iter().enumerate() { - console.info(&format!(" Problem {}", i + 1)); - console.info(&format!(" {msg}")); + io.lock().unwrap().info(&format!(" Problem {}", i + 1)); + io.lock().unwrap().info(&format!(" {msg}")); } return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED, @@ -1068,7 +1072,7 @@ pub async fn run( download_only: args.download_only, prefer_source, }, - console, + io, executor, ) .await diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs index 7a3847f..ece276d 100644 --- a/crates/mozart/src/commands/licenses.rs +++ b/crates/mozart/src/commands/licenses.rs @@ -1,7 +1,7 @@ use crate::composer::Composer; use clap::Args; use indexmap::IndexMap; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console::hyperlink; use mozart_core::console_writeln; use mozart_core::package::Package; @@ -68,7 +68,7 @@ impl PackageUrls for LicenseEntry { pub async fn execute( args: &LicensesArgs, cli: &super::Cli, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let format = args.format.as_deref().unwrap_or("text"); @@ -113,15 +113,15 @@ pub async fn execute( &root_version, &root_licenses, &entries, - console, + io, )?, - "summary" => render_summary(&entries, console), + "summary" => render_summary(&entries, io.clone()), _ => render_text( &root_pretty_name, &root_version, &root_licenses, &entries, - console, + io, ), } @@ -271,18 +271,18 @@ fn render_text( root_version: &str, root_licenses: &[String], entries: &[LicenseEntry], - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { let license_display = if root_licenses.is_empty() { "none".to_string() } else { root_licenses.join(", ") }; - console_writeln!(console, "Name: <comment>{root_pretty_name}</comment>"); - console_writeln!(console, "Version: <comment>{root_version}</comment>"); - console_writeln!(console, "Licenses: <comment>{license_display}</comment>"); - console_writeln!(console, "Dependencies:"); - console_writeln!(console, ""); + console_writeln!(io, "Name: <comment>{root_pretty_name}</comment>"); + console_writeln!(io, "Version: <comment>{root_version}</comment>"); + console_writeln!(io, "Licenses: <comment>{license_display}</comment>"); + console_writeln!(io, "Dependencies:"); + console_writeln!(io, ""); if entries.is_empty() { return; @@ -302,7 +302,7 @@ fn render_text( .max("Version".len()); console_writeln!( - console, + io, "{:<nw$} {:<vw$} Licenses", "Name", "Version", @@ -318,11 +318,11 @@ fn render_text( }; let padded_name = format!("{:<nw$}", entry.pretty_name, nw = name_width); let name_cell = match package_info::view_source_or_homepage_url(entry) { - Some(url) => hyperlink(&url, &padded_name, console.decorated), + Some(url) => hyperlink(&url, &padded_name, io.lock().unwrap().is_decorated()), None => padded_name, }; console_writeln!( - console, + io, "{} {:<vw$} {}", name_cell, entry.version, @@ -337,7 +337,7 @@ fn render_json( root_version: &str, root_licenses: &[String], entries: &[LicenseEntry], - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let root_license_arr: Vec<serde_json::Value> = root_licenses .iter() @@ -371,15 +371,18 @@ 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)?; - console_writeln!(console, "{}", &String::from_utf8(ser.into_inner())?); + console_writeln!(io, "{}", &String::from_utf8(ser.into_inner())?); Ok(()) } -fn render_summary(entries: &[LicenseEntry], console: &Console) { +fn render_summary( + entries: &[LicenseEntry], + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { let counts = tally_licenses(entries); if counts.is_empty() { - console_writeln!(console, "No dependencies found."); + console_writeln!(io, "No dependencies found."); return; } @@ -401,19 +404,19 @@ fn render_summary(entries: &[LicenseEntry], console: &Console) { let border_col1 = "-".repeat(license_width + 2); let border_col2 = "-".repeat(count_width + 2); - console_writeln!(console, " {} {}", border_col1, border_col2); + console_writeln!(io, " {} {}", border_col1, border_col2); console_writeln!( - console, + io, " {:<lw$} {:<cw$}", "License", COL2_HEADER, lw = license_width, cw = count_width, ); - console_writeln!(console, " {} {}", border_col1, border_col2); + console_writeln!(io, " {} {}", border_col1, border_col2); for (license, count) in &counts { console_writeln!( - console, + io, " {:<lw$} {:<cw$}", license, count, @@ -421,7 +424,7 @@ fn render_summary(entries: &[LicenseEntry], console: &Console) { cw = count_width, ); } - console_writeln!(console, " {} {}", border_col1, border_col2); + console_writeln!(io, " {} {}", border_col1, border_col2); } /// Mirror of `LicensesCommand::execute`'s `summary` accumulator. diff --git a/crates/mozart/src/commands/outdated.rs b/crates/mozart/src/commands/outdated.rs index 12646f4..8ed9cd5 100644 --- a/crates/mozart/src/commands/outdated.rs +++ b/crates/mozart/src/commands/outdated.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::IoInterface; #[derive(Args)] pub struct OutdatedArgs { @@ -62,7 +63,7 @@ pub struct OutdatedArgs { pub async fn execute( args: &OutdatedArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let show_args = super::show::ShowArgs { package: args.package.clone(), @@ -83,5 +84,5 @@ pub async fn execute( ..Default::default() }; - super::show::execute(&show_args, cli, console).await + super::show::execute(&show_args, cli, io).await } diff --git a/crates/mozart/src/commands/prohibits.rs b/crates/mozart/src/commands/prohibits.rs index 4bb00e4..0a77e8b 100644 --- a/crates/mozart/src/commands/prohibits.rs +++ b/crates/mozart/src/commands/prohibits.rs @@ -1,4 +1,5 @@ use clap::Args; +use mozart_core::console::IoInterface; #[derive(Args)] pub struct ProhibitsArgs { @@ -24,11 +25,11 @@ pub struct ProhibitsArgs { pub async fn execute( args: &ProhibitsArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { super::dependency::do_execute( cli, - console, + io, super::dependency::DoExecuteArgs { package: &args.package, version: Some(&args.version), diff --git a/crates/mozart/src/commands/reinstall.rs b/crates/mozart/src/commands/reinstall.rs index 52dfd52..fdad41e 100644 --- a/crates/mozart/src/commands/reinstall.rs +++ b/crates/mozart/src/commands/reinstall.rs @@ -2,6 +2,7 @@ use crate::composer::Composer; use clap::Args; use mozart_core::autoload::AutoloadGeneratorExt; use mozart_core::composer::{AutoloadDumpOptions, LocalPackage}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::validation::package_name_to_regexp; @@ -62,7 +63,7 @@ pub struct ReinstallArgs { pub async fn execute( args: &ReinstallArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let composer = Composer::require(&working_dir)?; @@ -101,7 +102,7 @@ pub async fn execute( } } if !matched { - console.error(&console_format!( + io.lock().unwrap().error(&console_format!( "<warning>Pattern \"{}\" does not match any currently installed packages.</warning>", pattern )); @@ -110,7 +111,7 @@ pub async fn execute( } if packages_to_reinstall.is_empty() { - console.error(&console_format!( + io.lock().unwrap().error(&console_format!( "<warning>Found no packages to reinstall, aborting.</warning>" )); return Err(mozart_core::exit_code::bail_silent( @@ -146,7 +147,7 @@ pub async fn execute( let dist = match package.dist() { Some(d) => d, None => { - console.info(&format!( + io.lock().unwrap().info(&format!( " Warning: {} has no dist information; skipping.", package.pretty_name() )); @@ -154,7 +155,7 @@ pub async fn execute( } }; - console.info(&format!( + io.lock().unwrap().info(&format!( " - Reinstalling {} ({})", package.pretty_name(), package.pretty_version() diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index c2d4d47..5bb3be3 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -1,5 +1,6 @@ use clap::Args; use indexmap::{IndexMap, IndexSet}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::package; @@ -100,7 +101,7 @@ pub struct RemoveArgs { pub async fn execute( args: &RemoveArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); @@ -111,7 +112,7 @@ pub async fn execute( // Only -w/--update-with-dependencies is deprecated in Composer; -W is an alias, not deprecated if args.update_with_dependencies { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>You are using the deprecated option \"update-with-dependencies\". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies.</warning>" )); } @@ -137,7 +138,7 @@ pub async fn execute( args, &repo_cache, cli.no_cache, - console, + &io, ) .await; } @@ -152,24 +153,24 @@ pub async fn execute( if args.dev { if composer.require_dev.contains_key(&name) { - console_writeln!(console, "<info>Removing {name} from require-dev</info>"); + console_writeln!(io, "<info>Removing {name} from require-dev</info>"); composer.require_dev.remove(&name); packages_removed.push(name); } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>{name} is not required in your composer.json and has not been removed</warning>" )); } } else if composer.require.contains_key(&name) { - console_writeln!(console, "<info>Removing {name} from require</info>"); + console_writeln!(io, "<info>Removing {name} from require</info>"); composer.require.remove(&name); packages_removed.push(name); } else if composer.require_dev.contains_key(&name) { - console_writeln!(console, "<info>Removing {name} from require-dev</info>"); + console_writeln!(io, "<info>Removing {name} from require-dev</info>"); composer.require_dev.remove(&name); packages_removed.push(name); } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>{name} is not required in your composer.json and has not been removed</warning>" )); } @@ -178,11 +179,11 @@ pub async fn execute( if !args.dry_run && !packages_removed.is_empty() { package::write_to_file(&composer, &composer_path)?; } - console.info("./composer.json has been updated"); + io.lock().unwrap().info("./composer.json has been updated"); if args.no_update { console_writeln!( - console, + io, "<comment>Not updating dependencies, only modifying composer.json.</comment>" ); return Ok(()); @@ -269,16 +270,16 @@ pub async fn execute( block_insecure: false, }; - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Running composer update {pkg_names}{flags}</info>" )); - console.info("Loading composer repositories with package information"); + io.lock().unwrap().info("Loading composer repositories with package information"); if dev_mode { - console.info("Updating dependencies (including require-dev)"); + io.lock().unwrap().info("Updating dependencies (including require-dev)"); } else { - console.info("Updating dependencies"); + io.lock().unwrap().info("Updating dependencies"); } - console.info("Resolving dependencies..."); + io.lock().unwrap().info("Resolving dependencies..."); let mut resolved = resolver::resolve(&request).await.map_err(|e| { mozart_core::exit_code::bail( @@ -291,7 +292,7 @@ pub async fn execute( match lockfile::LockFile::read_from_file(&lock_path) { Ok(l) => Some(l), Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not read existing composer.lock: {}. Treating as a fresh install.</warning>", e )); @@ -324,7 +325,7 @@ pub async fn execute( }; if args.minimal_changes { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Minimal changes mode: preserving locked versions for non-removed packages.</info>" )); } @@ -367,7 +368,7 @@ pub async fn execute( .filter(|c| matches!(c.kind, super::update::ChangeKind::Uninstall { .. })) .collect(); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Package operations: {} install{}, {} update{}, {} removal{}</info>", installs.len(), if installs.len() == 1 { "" } else { "s" }, @@ -381,12 +382,12 @@ pub async fn execute( match &change.kind { super::update::ChangeKind::Uninstall { old_version } => { if args.dry_run { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Would remove {} ({})", change.name, old_version )); } else { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Removing {} ({})", change.name, old_version )); @@ -394,12 +395,12 @@ pub async fn execute( } super::update::ChangeKind::Install { new_version } => { if args.dry_run { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Would install {} ({})", change.name, new_version )); } else { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Installing {} ({})", change.name, new_version )); @@ -410,12 +411,12 @@ pub async fn execute( new_version, } => { if args.dry_run { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Would update {} ({} => {})", change.name, old_version, new_version )); } else { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Updating {} ({} => {})", change.name, old_version, new_version )); @@ -425,7 +426,7 @@ pub async fn execute( } if !args.dry_run { - console.info("Writing lock file"); + io.lock().unwrap().info("Writing lock file"); new_lock.write_to_file(&lock_path)?; } @@ -452,7 +453,7 @@ pub async fn execute( download_only: false, prefer_source: false, }, - console, + io.clone(), &mut executor, ) .await?; @@ -465,7 +466,9 @@ pub async fn execute( if let Err(e) = pipeline_result { if !args.dry_run && !packages_removed.is_empty() { let _ = std::fs::write(&composer_path, &composer_backup); - console.error("\nRemoval failed, reverting ./composer.json to its original content."); + io.lock() + .unwrap() + .error("\nRemoval failed, reverting ./composer.json to its original content."); } return Err(e); } @@ -480,7 +483,7 @@ pub async fn execute( .iter() .any(|p| p.name.eq_ignore_ascii_case(name)) { - console.error(&format!( + io.lock().unwrap().error(&format!( "Removal failed, {name} is still present, it may be required by another package. See `mozart why {name}`." )); still_present = true; @@ -501,7 +504,7 @@ async fn remove_unused( args: &RemoveArgs, repo_cache: &mozart_core::repository::cache::Cache, no_cache: bool, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let lock_path = working_dir.join("composer.lock"); @@ -573,7 +576,9 @@ async fn remove_unused( block_insecure: false, }; - console.info("Resolving dependencies to detect unused packages..."); + io.lock() + .unwrap() + .info("Resolving dependencies to detect unused packages..."); let resolved = resolver::resolve(&request).await.map_err(|e| { mozart_core::exit_code::bail( @@ -600,19 +605,23 @@ async fn remove_unused( } if unused.is_empty() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>No unused packages to remove</info>" )); return Ok(()); } for name in &unused { - console.info(&format!(" - Removing unused package: {name}")); + io.lock() + .unwrap() + .info(&format!(" - Removing unused package: {name}")); } - console.info(&format!("Found {} unused package(s).", unused.len())); + io.lock() + .unwrap() + .info(&format!("Found {} unused package(s).", unused.len())); if args.dry_run { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<comment>Dry run: lock file not modified.</comment>" )); return Ok(()); @@ -632,7 +641,7 @@ async fn remove_unused( }) .await?; - console.info("Writing lock file"); + io.lock().unwrap().info("Writing lock file"); new_lock.write_to_file(&lock_path)?; if !args.no_install { @@ -659,7 +668,7 @@ async fn remove_unused( download_only: false, prefer_source: false, }, - console, + io.clone(), &mut executor, ) .await?; diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index 3905c77..adf34b5 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use clap::Args; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use super::base_config::BaseConfigContext; @@ -43,17 +44,17 @@ pub struct RepositoryArgs { pub async fn execute( args: &RepositoryArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let action = args.action.as_deref().unwrap_or("list"); let ctx = BaseConfigContext::initialize(args.global, args.file.as_deref(), cli)?; match action { - "list" | "ls" | "show" => list_repositories(&ctx, console), + "list" | "ls" | "show" => list_repositories(&ctx, io.clone()), "add" => execute_add(&ctx, args), "remove" | "rm" | "delete" => execute_remove(&ctx, args), "set-url" | "seturl" => execute_set_url(&ctx, args), - "get-url" | "geturl" => execute_get_url(&ctx, args, console), + "get-url" | "geturl" => execute_get_url(&ctx, args, io.clone()), "disable" => execute_disable(&ctx, args), "enable" => execute_enable(&ctx, args), _ => Err(anyhow!( @@ -68,7 +69,7 @@ pub async fn execute( /// repository with a host ending in `packagist.org` is already in the list. fn list_repositories( ctx: &BaseConfigContext, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let json = ctx.config_source.read()?; let repos_raw = &json["repositories"]; @@ -94,7 +95,7 @@ fn list_repositories( } if display_repos.is_empty() { - console_writeln!(console, "No repositories configured"); + console_writeln!(io, "No repositories configured"); return Ok(()); } @@ -104,7 +105,7 @@ fn list_repositories( && let Some((key, val)) = obj.iter().next() && val == &serde_json::Value::Bool(false) { - console_writeln!(console, "[{key}] disabled"); + console_writeln!(io, "[{key}] disabled"); continue; } @@ -118,7 +119,7 @@ fn list_repositories( .unwrap_or("unknown"); let url = entry.get("url").map(render_value).unwrap_or_default(); - console_writeln!(console, "[{name}] {repo_type} {url}"); + console_writeln!(io, "[{name}] {repo_type} {url}"); } Ok(()) @@ -210,7 +211,7 @@ fn execute_set_url(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Re fn execute_get_url( ctx: &BaseConfigContext, args: &RepositoryArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let name = args .name @@ -223,7 +224,7 @@ fn execute_get_url( // Assoc-keyed fast path (mirrors Composer's `isset($repos[$name])` check). if let Some(repo) = repos_raw.as_object().and_then(|obj| obj.get(name)) { if let Some(url) = repo.get("url").and_then(|u| u.as_str()) { - console_writeln!(console, "{}", url); + console_writeln!(io, "{}", url); return Ok(()); } anyhow::bail!("The {} repository does not have a URL", name); @@ -234,7 +235,7 @@ fn execute_get_url( for repo in &repos { if repo.get("name").and_then(|n| n.as_str()) == Some(name) { if let Some(url) = repo.get("url").and_then(|u| u.as_str()) { - console_writeln!(console, "{}", url); + console_writeln!(io, "{}", url); return Ok(()); } anyhow::bail!("The {} repository does not have a URL", name); @@ -283,6 +284,11 @@ fn execute_enable(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Res mod tests { use super::*; + fn make_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + let console = mozart_core::console::Console::new(0, false, false, false, false); + std::sync::Arc::new(std::sync::Mutex::new(Box::new(console))) + } + fn make_args( action: Option<&str>, name: Option<&str>, @@ -317,9 +323,9 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); + let io = make_io(); // Empty repos → synthesises [packagist.org] disabled - let result = execute(&args, &cli, &console).await; + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -336,8 +342,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -351,8 +357,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -372,8 +378,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -392,8 +398,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -419,8 +425,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -447,8 +453,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -476,8 +482,8 @@ mod tests { args.append = true; let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -505,8 +511,8 @@ mod tests { args.before = Some("b".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -535,8 +541,8 @@ mod tests { args.after = Some("a".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -563,8 +569,8 @@ mod tests { args.after = Some("b".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -578,8 +584,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -593,8 +599,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -611,8 +617,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -629,8 +635,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -651,8 +657,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -669,8 +675,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -693,8 +699,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -716,8 +722,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -740,8 +746,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -761,8 +767,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -776,8 +782,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -791,8 +797,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -811,8 +817,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -831,8 +837,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -847,8 +853,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -862,8 +868,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -880,8 +886,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -899,8 +905,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -918,8 +924,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_ok()); } @@ -937,8 +943,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!( @@ -957,8 +963,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!(msg.contains("There is no"), "unexpected message: {msg}"); @@ -984,8 +990,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -1008,8 +1014,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -1071,8 +1077,8 @@ mod tests { args.file = Some(file.to_str().unwrap().to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } @@ -1096,8 +1102,8 @@ mod tests { args.before = Some("b".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -1127,8 +1133,8 @@ mod tests { args.after = Some("a".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - execute(&args, &cli, &console).await.unwrap(); + let io = make_io(); + execute(&args, &cli, io).await.unwrap(); let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); @@ -1154,8 +1160,8 @@ mod tests { args.before = Some("nonexistent".to_string()); let cli = make_cli(); - let console = mozart_core::console::Console::new(0, false, false, false, false); - let result = execute(&args, &cli, &console).await; + let io = make_io(); + let result = execute(&args, &cli, io).await; assert!(result.is_err()); } } diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index 9ec4195..4db6e09 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -1,5 +1,6 @@ use clap::Args; use indexmap::{IndexMap, IndexSet}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::package::{self, RawPackageData, Stability}; @@ -144,14 +145,17 @@ struct CommandState { /// Reverts composer.json (and composer.lock) to their pre-command state. /// Mirrors Composer\Command\RequireCommand::revertComposerFile(). -fn revert_composer_file(state: &CommandState, console: &mozart_core::console::Console) { +fn revert_composer_file( + state: &CommandState, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { if state.newly_created { - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "\nInstallation failed, deleting {}.", state.json_path.display() )); if let Err(e) = std::fs::remove_file(&state.json_path) { - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "Warning: Failed to delete {}: {e}", state.json_path.display() )); @@ -160,7 +164,7 @@ fn revert_composer_file(state: &CommandState, console: &mozart_core::console::Co if state.lock_path.exists() && let Err(e) = std::fs::remove_file(&state.lock_path) { - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "Warning: Failed to delete {}: {e}", state.lock_path.display() )); @@ -171,12 +175,12 @@ fn revert_composer_file(state: &CommandState, console: &mozart_core::console::Co } else { " to its".to_string() }; - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "\nInstallation failed, reverting {}{msg} original content.", state.json_path.display() )); if let Err(e) = std::fs::write(&state.json_path, &state.composer_backup) { - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "Warning: Failed to revert {}: {e}", state.json_path.display() )); @@ -184,7 +188,7 @@ fn revert_composer_file(state: &CommandState, console: &mozart_core::console::Co if let Some(ref lock_content) = state.lock_backup && let Err(e) = std::fs::write(&state.lock_path, lock_content) { - console.write_error(&format!( + io.lock().unwrap().write_error(&format!( "Warning: Failed to revert {}: {e}", state.lock_path.display() )); @@ -253,7 +257,7 @@ async fn update_requirements_after_resolution( _sort_packages: bool, _dry_run: bool, _fixed: bool, - _console: &mozart_core::console::Console, + _io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { Ok(()) } @@ -266,7 +270,7 @@ async fn do_update( cli: &super::Cli, raw: &RawPackageData, additions: &[(String, String, bool)], - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let vendor_dir = working_dir.join("vendor"); @@ -349,19 +353,23 @@ async fn do_update( block_insecure, }; - console.info("Loading composer repositories with package information"); + io.lock() + .unwrap() + .info("Loading composer repositories with package information"); if dev_mode { - console.info("Updating dependencies (including require-dev)"); + io.lock() + .unwrap() + .info("Updating dependencies (including require-dev)"); } else { - console.info("Updating dependencies"); + io.lock().unwrap().info("Updating dependencies"); } - console.info("Resolving dependencies..."); + io.lock().unwrap().info("Resolving dependencies..."); let mut resolved = match resolver::resolve(&request).await { Ok(packages) => packages, Err(e) => { if !args.dry_run { - revert_composer_file(state, console); + revert_composer_file(state, io.clone()); } // Suggest explicit version constraint retry for the first package without one. // Mirrors Composer\Command\RequireCommand::doUpdate() L496-502. @@ -394,7 +402,7 @@ async fn do_update( match lockfile::LockFile::read_from_file(&state.lock_path) { Ok(l) => Some(l), Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not read existing composer.lock: {e}. \ Treating as a fresh install.</warning>" )); @@ -467,7 +475,7 @@ async fn do_update( .filter(|c| matches!(c.kind, super::update::ChangeKind::Uninstall { .. })) .collect(); - console.info(&format!( + io.lock().unwrap().info(&format!( "Package operations: {} install{}, {} update{}, {} removal{}", installs.len(), if installs.len() == 1 { "" } else { "s" }, @@ -481,19 +489,25 @@ async fn do_update( match &change.kind { super::update::ChangeKind::Uninstall { old_version } => { if args.dry_run { - console.info(&format!(" - Would remove {} ({old_version})", change.name)); + io.lock() + .unwrap() + .info(&format!(" - Would remove {} ({old_version})", change.name)); } else { - console.info(&format!(" - Removing {} ({old_version})", change.name)); + io.lock() + .unwrap() + .info(&format!(" - Removing {} ({old_version})", change.name)); } } super::update::ChangeKind::Install { new_version } => { if args.dry_run { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Would install {} ({new_version})", change.name )); } else { - console.info(&format!(" - Installing {} ({new_version})", change.name)); + io.lock() + .unwrap() + .info(&format!(" - Installing {} ({new_version})", change.name)); } } super::update::ChangeKind::Update { @@ -501,12 +515,12 @@ async fn do_update( new_version, } => { if args.dry_run { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Would update {} ({old_version} => {new_version})", change.name )); } else { - console.info(&format!( + io.lock().unwrap().info(&format!( " - Updating {} ({old_version} => {new_version})", change.name )); @@ -516,7 +530,7 @@ async fn do_update( } if !args.dry_run { - console.info("Writing lock file"); + io.lock().unwrap().info("Writing lock file"); new_lock.write_to_file(&state.lock_path)?; } @@ -528,7 +542,7 @@ async fn do_update( .map(|s| s.eq_ignore_ascii_case("source")) .unwrap_or(false); if prefer_source { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Warning: Source installs are not yet supported. \ Falling back to dist.</warning>" )); @@ -573,7 +587,7 @@ async fn do_update( download_only: false, prefer_source: args.prefer_source, }, - console, + io.clone(), &mut executor, ) .await?; @@ -591,7 +605,7 @@ async fn interactive_search_packages( preferred_stability: Stability, fixed: bool, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<Vec<String>> { let stdin = std::io::stdin(); if !stdin.is_terminal() { @@ -623,7 +637,7 @@ async fn interactive_search_packages( let (results, total) = match packagist::search_packages(&query, None).await { Ok(r) => r, Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Search failed: {e}. Try again.</warning>" )); continue; @@ -637,13 +651,13 @@ async fn interactive_search_packages( .collect(); if filtered.is_empty() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>No new packages found for \"{query}\" (total: {total}).</warning>" )); continue; } - console.info(&format!( + io.lock().unwrap().info(&format!( "\nFound {} package{} for \"{}\":", filtered.len(), if filtered.len() == 1 { "" } else { "s" }, @@ -657,15 +671,17 @@ async fn interactive_search_packages( } else { format!(" — {}", result.description) }; - console.info(&format!( + io.lock().unwrap().info(&format!( " [{idx}] {:<width$}{desc}", result.name, idx = idx + 1, width = name_width, )); } - console.info(" [0] Search again / enter full package name"); - console.info(""); + io.lock() + .unwrap() + .info(" [0] Search again / enter full package name"); + io.lock().unwrap().info(""); eprint!("Enter package # or name (leave empty to finish): "); let _ = std::io::stderr().flush(); @@ -689,7 +705,7 @@ async fn interactive_search_packages( } else if num <= filtered.len() { filtered[num - 1].name.to_lowercase() } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Invalid selection: {num}</warning>" )); continue; @@ -702,19 +718,21 @@ async fn interactive_search_packages( match validation::parse_require_string(&package_name) { Ok((n, v)) => (n.to_lowercase(), v), Err(e) => { - console.info(&console_format!("<warning>Invalid: {e}</warning>")); + io.lock() + .unwrap() + .info(&console_format!("<warning>Invalid: {e}</warning>")); continue; } } } else { if !validation::validate_package_name(&package_name) { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Invalid package name: \"{package_name}\"</warning>" )); continue; } - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Using version constraint for {package_name} from Packagist...</info>" )); @@ -732,13 +750,13 @@ async fn interactive_search_packages( stability, ) }; - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Using version {c} for {package_name}</info>" )); (package_name, c) } None => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not find a version of \"{package_name}\" \ matching your minimum-stability. Try specifying it \ explicitly.</warning>" @@ -748,7 +766,7 @@ async fn interactive_search_packages( } } Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not fetch versions for \"{package_name}\": \ {e}</warning>" )); @@ -782,7 +800,7 @@ async fn interactive_search_packages( pub async fn execute( args: &RequireArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); @@ -790,19 +808,19 @@ pub async fn execute( // --- Deprecated flag warnings --- // Mirrors Composer\Command\RequireCommand::execute() L134-136. if args.no_suggest { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>You are using the deprecated option \"--no-suggest\". \ It has no effect and will break in Composer 3.</warning>" )); } if args.update_with_dependencies { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>The -w / --update-with-dependencies flag is deprecated. \ Use --with-dependencies instead.</warning>" )); } if args.update_with_all_dependencies { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>The -W / --update-with-all-dependencies flag is deprecated. \ Use --with-all-dependencies instead.</warning>" )); @@ -841,7 +859,7 @@ pub async fn execute( preferred_stability, args.fixed, &repo_cache, - console, + &io, ) .await?; @@ -905,13 +923,13 @@ pub async fn execute( .filter(|t| !t.is_empty()) .unwrap_or("library"); if package_type != "project" && !args.dev { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<error>The \"--fixed\" option is only allowed for packages with a \ \"project\" type or for dev dependencies to prevent possible \ misuses.</error>" )); if raw.package_type.is_none() { - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<error>If your package is not a library, you can explicitly specify \ the \"type\" by using \"mozart config type project\".</error>" )); @@ -948,7 +966,7 @@ pub async fn execute( } console_writeln!( - console, + io, "<info>Using version constraint for {name} from Packagist...</info>" ); @@ -966,10 +984,7 @@ pub async fn execute( let constraint = version_selector.find_recommended_require_version_string(&best, args.fixed); - console_writeln!( - console, - "<info>Using version {constraint} for {name}</info>", - ); + console_writeln!(io, "<info>Using version {constraint} for {name}</info>",); (name, constraint) } @@ -1002,7 +1017,7 @@ pub async fn execute( } else { ("without", require_key) }; - console.write_error(&console_format!( + io.lock().unwrap().write_error(&console_format!( "<warning>{pkg} is currently present in the {remove_key} key and you ran the \ command {with_without} the --dev flag, which will move it to the \ {target_key} key.</warning>" @@ -1028,12 +1043,12 @@ pub async fn execute( if let Some(existing) = target.get(name) { console_writeln!( - console, + io, "<comment>Updating {name} from {existing} to {constraint} in {section_name}</comment>", ); } else { console_writeln!( - console, + io, "<info>Adding {name} ({constraint}) to {section_name}</info>", ); } @@ -1061,7 +1076,7 @@ pub async fn execute( // Mirrors Composer\Command\RequireCommand::execute() L323-325. if args.dry_run { console_writeln!( - console, + io, "<comment>Dry run: composer.json not modified.</comment>", ); } else { @@ -1070,7 +1085,7 @@ pub async fn execute( // Print "has been created|updated". // Mirrors Composer\Command\RequireCommand::execute() L327. - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>{} has been {}</info>", composer_path.display(), if newly_created { "created" } else { "updated" } @@ -1079,14 +1094,14 @@ pub async fn execute( // --- --no-update: skip resolution --- if args.no_update { console_writeln!( - console, + io, "<comment>Not updating dependencies, only modifying composer.json.</comment>" ); return Ok(()); } // --- Resolution + lock + install --- - let update_result = do_update(&mut state, args, cli, &raw, &additions, console).await; + let update_result = do_update(&mut state, args, cli, &raw, &additions, &io).await; // Mirrors Composer's `finally` block: cleanup newly-created file on dry-run. if args.dry_run && state.newly_created { @@ -1104,7 +1119,7 @@ pub async fn execute( sort_packages, args.dry_run, args.fixed, - console, + io, ) .await?; diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs index e4b701a..c2e52a6 100644 --- a/crates/mozart/src/commands/run_script.rs +++ b/crates/mozart/src/commands/run_script.rs @@ -1,9 +1,11 @@ use crate::composer::Composer; use clap::Args; +use mozart_core::console::IoInterface; use mozart_core::script_events; use mozart_core::{console_writeln, console_writeln_error}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; use std::time::Duration; #[derive(Args)] @@ -35,14 +37,14 @@ pub struct RunScriptArgs { pub async fn execute( args: &RunScriptArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; if args.list { Composer::require(&working_dir)?; let (scripts, descriptions) = load_scripts(&working_dir)?; - return list_scripts(&scripts, &descriptions, console); + return list_scripts(&scripts, &descriptions, io.clone()); } let script = match &args.script { @@ -106,7 +108,7 @@ pub async fn execute( dev_mode, &mut event_stack, cli.verbose, - console, + io, )?; if exit_code != 0 { @@ -162,19 +164,19 @@ fn load_scripts( fn list_scripts( scripts: &BTreeMap<String, Vec<String>>, descriptions: &BTreeMap<String, String>, - console: &mozart_core::console::Console, + io: Arc<Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { if scripts.is_empty() { return Ok(()); } - console_writeln_error!(console, "<info>scripts:</info>"); + console_writeln_error!(io, "<info>scripts:</info>"); let name_width = scripts.keys().map(|n| n.len() + 2).max().unwrap_or(0); for name in scripts.keys() { let desc = descriptions.get(name).map(|s| s.as_str()).unwrap_or(""); let padded = format!(" {:<w$}", name, w = name_width - 2); - console_writeln!(console, "{} {}", padded, desc); + console_writeln!(io, "{} {}", padded, desc); } Ok(()) } @@ -190,7 +192,7 @@ fn run_script( dev_mode: bool, event_stack: &mut Vec<String>, verbose: u8, - console: &mozart_core::console::Console, + io: Arc<Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<i32> { if event_stack.contains(&script.to_string()) { anyhow::bail!( @@ -217,7 +219,7 @@ fn run_script( dev_mode, event_stack, verbose, - console, + io.clone(), )?; if code > max_exit_code { max_exit_code = code; @@ -247,7 +249,7 @@ fn run_script_entry( dev_mode: bool, event_stack: &mut Vec<String>, verbose: u8, - console: &mozart_core::console::Console, + io: Arc<Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<i32> { let suppress_additional_args = entry.contains("@no_additional_args"); let effective_args: &[String] = if suppress_additional_args { &[] } else { args }; @@ -269,7 +271,7 @@ fn run_script_entry( }; if is_php_callback(&entry) { - console.info(&format!( + io.lock().unwrap().info(&format!( "Skipping PHP callback '{}' -- Mozart cannot execute PHP class methods.", entry )); @@ -301,7 +303,7 @@ fn run_script_entry( dev_mode, event_stack, verbose, - console, + io, ); } @@ -455,12 +457,10 @@ 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, - } + fn test_console() -> Arc<Mutex<Box<dyn IoInterface>>> { + Arc::new(Mutex::new(Box::new(mozart_core::console::Console::new( + 0, false, false, false, false, + )) as Box<dyn IoInterface>)) } #[test] @@ -594,7 +594,7 @@ mod tests { let mut descriptions = BTreeMap::new(); descriptions.insert("test".to_string(), "Run tests".to_string()); - let result = list_scripts(&scripts, &descriptions, &test_console()); + let result = list_scripts(&scripts, &descriptions, test_console()); assert!(result.is_ok()); } @@ -602,7 +602,7 @@ mod tests { fn test_list_scripts_empty_silent() { let scripts: BTreeMap<String, Vec<String>> = BTreeMap::new(); let descriptions: BTreeMap<String, String> = BTreeMap::new(); - let result = list_scripts(&scripts, &descriptions, &test_console()); + let result = list_scripts(&scripts, &descriptions, test_console()); assert!(result.is_ok()); } @@ -641,7 +641,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -669,7 +669,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); @@ -701,7 +701,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); @@ -728,7 +728,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -754,7 +754,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); @@ -783,7 +783,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -811,7 +811,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -839,7 +839,7 @@ mod tests { true, &mut stack, 0, - &test_console(), + test_console(), ) .unwrap(); assert_eq!(code, 0); @@ -929,7 +929,7 @@ mod tests { assert!(scripts.contains_key("test")); assert!(scripts.contains_key("lint")); - let result = list_scripts(&scripts, &descriptions, &test_console()); + 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 a5ab04a..7259e6c 100644 --- a/crates/mozart/src/commands/search.rs +++ b/crates/mozart/src/commands/search.rs @@ -1,5 +1,5 @@ use clap::Args; -use mozart_core::console::{Console, hyperlink}; +use mozart_core::console::{IoInterface, hyperlink}; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::repository::packagist::SearchResult; @@ -68,12 +68,16 @@ fn is_abandoned(result: &SearchResult) -> bool { } } -pub async fn execute(args: &SearchArgs, cli: &super::Cli, console: &Console) -> anyhow::Result<()> { +pub async fn execute( + args: &SearchArgs, + cli: &super::Cli, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { // 1. Format check first — matches Composer's `SearchCommand::execute` // L61-66 ordering. let format = args.format.as_deref().unwrap_or("text"); if !matches!(format, "text" | "json") { - console.error(&console_format!( + io.lock().unwrap().error(&console_format!( "<error>Unsupported format \"{format}\". See help for supported formats.</error>" )); return Err(mozart_core::exit_code::bail_silent( @@ -121,8 +125,8 @@ pub async fn execute(args: &SearchArgs, cli: &super::Cli, console: &Console) -> // 7. Render. Empty results emit nothing in text mode (matches Composer) // and `[]` in JSON mode. match format { - "json" => render_json(&results, console)?, - _ => render_text(&results, console), + "json" => render_json(&results, io.clone())?, + _ => render_text(&results, io.clone()), } Ok(()) @@ -133,13 +137,16 @@ pub async fn execute(args: &SearchArgs, cli: &super::Cli, console: &Console) -> /// JSON_UNESCAPED_UNICODE`). `serde_json` does not escape forward slashes /// or non-ASCII Unicode by default, so the encoder configuration alone /// covers the latter two flags. -fn render_json(results: &[SearchResult], console: &Console) -> anyhow::Result<()> { +fn render_json( + results: &[SearchResult], + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) -> anyhow::Result<()> { let output: Vec<SearchResultOutput> = results.iter().map(SearchResultOutput::from).collect(); let buf = Vec::new(); let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); let mut ser = serde_json::Serializer::with_formatter(buf, formatter); output.serialize(&mut ser)?; - console_writeln!(console, "{}", &String::from_utf8(ser.into_inner())?); + console_writeln!(io, "{}", &String::from_utf8(ser.into_inner())?); Ok(()) } @@ -148,7 +155,10 @@ fn render_json(results: &[SearchResult], console: &Console) -> anyhow::Result<() /// else plain `name`, padded to the longest-name column. /// - `<warning>! Abandoned !</warning> ` prefix when abandoned. /// - Description, truncated with `...` to fit the terminal width. -fn render_text(results: &[SearchResult], console: &Console) { +fn render_text( + results: &[SearchResult], + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, +) { if results.is_empty() { return; } @@ -182,14 +192,14 @@ fn render_text(results: &[SearchResult], console: &Console) { let padded_name = if !result.url.is_empty() { format!( "{}{}", - hyperlink(&result.url, &result.name, console.decorated), + hyperlink(&result.url, &result.name, io.lock().unwrap().is_decorated()), " ".repeat(padding_width) ) } else { format!("{}{}", result.name, " ".repeat(padding_width)) }; - console_writeln!(console, "{padded_name}{warning}{desc_display}"); + console_writeln!(io, "{padded_name}{warning}{desc_display}"); } } diff --git a/crates/mozart/src/commands/self_update.rs b/crates/mozart/src/commands/self_update.rs index a326914..4b6c27a 100644 --- a/crates/mozart/src/commands/self_update.rs +++ b/crates/mozart/src/commands/self_update.rs @@ -1,5 +1,6 @@ use clap::Args; use mozart_core::MOZART_VERSION; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use std::io::Write; use std::path::{Path, PathBuf}; @@ -47,7 +48,7 @@ const BACKUP_EXTENSION: &str = ".old"; pub async fn execute( args: &SelfUpdateArgs, _cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let current_exe = std::env::current_exe() .map_err(|e| anyhow::anyhow!("Could not determine current executable path: {e}"))?; @@ -61,9 +62,9 @@ pub async fn execute( })?; if args.rollback { - rollback(¤t_exe, &data_dir, console) + rollback(¤t_exe, &data_dir, io.clone()) } else { - update(args, ¤t_exe, &data_dir, console).await + update(args, ¤t_exe, &data_dir, &io).await } } @@ -203,7 +204,7 @@ async fn download_asset( asset: &GitHubAsset, dest: &Path, show_progress: bool, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let client = mozart_core::http::client_builder() .timeout(std::time::Duration::from_secs(300)) @@ -248,7 +249,7 @@ async fn download_asset( } if show_progress && total_bytes > 0 { - console.info(""); // newline after progress + io.lock().unwrap().info(""); // newline after progress } Ok(()) @@ -258,7 +259,7 @@ async fn update( args: &SelfUpdateArgs, current_exe: &Path, data_dir: &Path, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let current_version = MOZART_VERSION; let channel = effective_channel(args.preview); @@ -278,7 +279,7 @@ async fn update( // If no explicit version was requested and we're already up-to-date, bail early if args.version.is_none() && target_version == current_version { console_writeln!( - console, + io, "<info>You are already using the latest available Mozart version {current_version} ({channel} channel).</info>" ); @@ -286,13 +287,13 @@ async fn update( // Preserve the most recent backup let latest = find_latest_backup(data_dir).ok(); clean_backups(data_dir, latest.as_deref())?; - console_writeln!(console, "<comment>Old backups removed.</comment>"); + console_writeln!(io, "<comment>Old backups removed.</comment>"); } return Ok(()); } - console.info(&format!( + io.lock().unwrap().info(&format!( "Upgrading to version {target_version} ({channel} channel)." )); @@ -300,7 +301,7 @@ async fn update( let asset_name = platform_asset_name()?; let asset = find_asset(target_release, &asset_name)?; - console.info(&format!( + io.lock().unwrap().info(&format!( "Downloading {} ({} bytes)...", asset.name, asset.size )); @@ -312,7 +313,7 @@ async fn update( .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, console).await?; + download_asset(asset, &tmp_path, !args.no_progress, io.clone()).await?; // Set executable permission on Unix #[cfg(unix)] @@ -342,16 +343,16 @@ async fn update( drop(tmp); console_writeln!( - console, + io, "<info>Mozart updated successfully from {current_version} to {target_version}</info>" ); - console.info(&format!( + io.lock().unwrap().info(&format!( "Use `mozart self-update --rollback` to return to version {current_version}" )); if args.clean_backups { clean_backups(data_dir, Some(&backup_path))?; - console_writeln!(console, "<comment>Old backups removed.</comment>"); + console_writeln!(io, "<comment>Old backups removed.</comment>"); } Ok(()) @@ -360,12 +361,14 @@ async fn update( fn rollback( current_exe: &Path, data_dir: &Path, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let backup = find_latest_backup(data_dir)?; let backup_version = version_from_backup(&backup); - console.info(&format!("Rolling back to version {backup_version}...")); + io.lock() + .unwrap() + .info(&format!("Rolling back to version {backup_version}...")); // Set executable permission on Unix before replacing #[cfg(unix)] @@ -381,7 +384,7 @@ fn rollback( .map_err(|e| anyhow::anyhow!("Could not restore backup: {e}"))?; console_writeln!( - console, + io, "<info>Rollback successful. Restored version {backup_version}</info>", ); diff --git a/crates/mozart/src/commands/show.rs b/crates/mozart/src/commands/show.rs index 8876694..d0a2218 100644 --- a/crates/mozart/src/commands/show.rs +++ b/crates/mozart/src/commands/show.rs @@ -1,5 +1,6 @@ use clap::Args; use indexmap::{IndexMap, IndexSet}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::console_writeln_error; @@ -108,7 +109,7 @@ pub struct ShowArgs { pub async fn execute( args: &ShowArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); @@ -116,7 +117,7 @@ pub async fn execute( // A9: --installed deprecation warning (mirrors Composer 143-145) if args.installed && !args.self_info { console_writeln_error!( - console, + io, "<warning>You are using the deprecated option \"installed\". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>", ); } @@ -167,7 +168,7 @@ pub async fn execute( // --ignore without --outdated warning if !args.ignore.is_empty() && !args.outdated { console_writeln_error!( - console, + io, "<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>", ); } @@ -176,31 +177,31 @@ pub async fn execute( // --platform: show detected platform packages if args.platform { - return show_platform(args, &working_dir, console); + return show_platform(args, &working_dir, io.clone()); } // --self: show root package info if args.self_info && !args.installed && !args.locked { - return show_self(args, &working_dir, console); + return show_self(args, &working_dir, io.clone()); } // --tree: show dependency tree if args.tree { - return show_tree(args, &working_dir, console); + return show_tree(args, &working_dir, io.clone()); } // --available: show available versions if args.available { - return show_available(args, &working_dir, &repo_cache, console).await; + return show_available(args, &working_dir, &repo_cache, &io).await; } // --locked: show from lock file if args.locked { - return execute_locked(args, &working_dir, &repo_cache, console).await; + return execute_locked(args, &working_dir, &repo_cache, &io).await; } // Default: installed mode - execute_installed(args, &working_dir, &repo_cache, console).await + execute_installed(args, &working_dir, &repo_cache, &io).await } // ============================================================================ @@ -534,7 +535,7 @@ fn render_package_list( entries: &mut [PackageEntry], args: &ShowArgs, section_key: &str, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<bool> { let show_latest = args.latest || args.outdated; @@ -546,13 +547,13 @@ fn render_package_list( let has_outdated = entries.iter().any(|e| e.latest_info.is_some()); if args.format == "json" { - render_list_json(entries, section_key, console)?; + render_list_json(entries, section_key, io)?; return Ok(has_outdated); } // A6: Color legend (mirrors Composer 626-642) if show_latest && !entries.is_empty() { - print_color_legend(console); + print_color_legend(io.clone()); } // A7: Direct/Transitive split (mirrors Composer 671-695) @@ -563,28 +564,28 @@ fn render_package_list( entries.iter().filter(|e| !e.is_direct).collect(); console_writeln!( - console, + io, "<info>Direct dependencies required in composer.json:</info>", ); if direct_entries.is_empty() { - console_writeln!(console, "Everything up to date"); + console_writeln!(io, "Everything up to date"); } else { - print_package_rows(&direct_entries, args, console); + print_package_rows(&direct_entries, args, io.clone()); } - console_writeln!(console, ""); + console_writeln!(io, ""); console_writeln!( - console, + io, "<info>Transitive dependencies not required in composer.json:</info>", ); if transitive_entries.is_empty() { - console_writeln!(console, "Everything up to date"); + console_writeln!(io, "Everything up to date"); } else { - print_package_rows(&transitive_entries, args, console); + print_package_rows(&transitive_entries, args, io.clone()); } } else { let all_refs: Vec<&PackageEntry> = entries.iter().collect(); - print_package_rows(&all_refs, args, console); + print_package_rows(&all_refs, args, io); } Ok(has_outdated) @@ -595,7 +596,7 @@ fn render_package_list( fn print_package_rows( entries: &[&PackageEntry], args: &ShowArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { let show_latest = args.latest || args.outdated; @@ -654,7 +655,7 @@ fn print_package_rows( ); // A6: ASCII prefix markers for non-decorated terminals (Composer 736/1438) - let ascii_prefix = if !console.decorated && show_latest { + let ascii_prefix = if !io.lock().unwrap().is_decorated() && show_latest { match category { Some(ListUpdateKind::Compatible) => "! ", Some(ListUpdateKind::Incompatible) => "~ ", @@ -692,7 +693,7 @@ fn print_package_rows( None => format!("{:<width$}", "", width = latest_width), }; console_writeln!( - console, + io, "{}{} {} {} {}", ascii_prefix, name_str, @@ -702,7 +703,7 @@ fn print_package_rows( ); } else { console_writeln!( - console, + io, "{}{} {} {}", ascii_prefix, name_str, @@ -726,40 +727,41 @@ fn print_package_rows( entry.name, replacement ) }; - console_writeln_error!(console, "<warning>{}</warning>", msg); + console_writeln_error!(io, "<warning>{}</warning>", msg); } } } /// Print the color legend before the list (A6, mirrors Composer 626-642). -fn print_color_legend(console: &mozart_core::console::Console) { - if console.decorated { - console_writeln!(console, "<info>Color legend:</info>"); +fn print_color_legend(io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>) { + let is_decorated = io.lock().unwrap().is_decorated(); + if is_decorated { + console_writeln!(io, "<info>Color legend:</info>"); console_writeln!( - console, + io, "- {} release available - update recommended", console_format!("<highlight>patch or minor</highlight>"), ); console_writeln!( - console, + io, "- {} release available - update possible", console_format!("<comment>major</comment>"), ); console_writeln!( - console, + io, "- {} version", console_format!("<info>up to date</info>"), ); } else { - console_writeln!(console, "Legend:"); + console_writeln!(io, "Legend:"); console_writeln!( - console, + io, "! patch or minor release available - update recommended", ); - console_writeln!(console, "~ major release available - update possible"); - console_writeln!(console, "= up to date version"); + console_writeln!(io, "~ major release available - update possible"); + console_writeln!(io, "= up to date version"); } - console_writeln!(console, ""); + console_writeln!(io, ""); } /// Emit the JSON list output. Uses `section_key` as the top-level key @@ -767,7 +769,7 @@ fn print_color_legend(console: &mozart_core::console::Console) { fn render_list_json( entries: &[PackageEntry], section_key: &str, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let json_entries: Vec<serde_json::Value> = entries .iter() @@ -794,7 +796,7 @@ fn render_list_json( .collect(); let output = serde_json::json!({ section_key: json_entries }); - console_writeln!(console, "{}", &serde_json::to_string_pretty(&output)?); + console_writeln!(io, "{}", &serde_json::to_string_pretty(&output)?); Ok(()) } @@ -929,32 +931,32 @@ async fn print_package_detail( detail: &PackageDetail, args: &ShowArgs, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { if args.format == "json" { - return print_package_detail_json(detail, args, repo_cache, console).await; + return print_package_detail_json(detail, args, repo_cache, io).await; } console_writeln!( - console, + io, "{} : {}", console_format!("<info>name</info>"), detail.name, ); console_writeln!( - console, + io, "{} : {}", console_format!("<info>descrip.</info>"), detail.description, ); console_writeln!( - console, + io, "{} : {}", console_format!("<info>keywords</info>"), detail.keywords.join(", "), ); console_writeln!( - console, + io, "{} : {}", console_format!("<info>versions</info>"), format_version_highlight(&detail.version), @@ -963,7 +965,7 @@ async fn print_package_detail( // A13: released if let Some(ref date) = detail.release_date { console_writeln!( - console, + io, "{} : {}", console_format!("<info>released</info>"), date, @@ -989,7 +991,7 @@ async fn print_package_detail( } }; console_writeln!( - console, + io, "{} : {}", console_format!("<info>latest</info>"), latest_str, @@ -998,7 +1000,7 @@ async fn print_package_detail( } console_writeln!( - console, + io, "{} : {}", console_format!("<info>type</info>"), detail.package_type.as_deref().unwrap_or("library"), @@ -1006,7 +1008,7 @@ async fn print_package_detail( for license_id in &detail.licenses { console_writeln!( - console, + io, "{} : {}", console_format!("<info>license</info>"), format_license_for_show(license_id), @@ -1015,7 +1017,7 @@ async fn print_package_detail( if let Some(ref homepage) = detail.homepage { console_writeln!( - console, + io, "{} : {}", console_format!("<info>homepage</info>"), homepage, @@ -1026,7 +1028,7 @@ async fn print_package_detail( let src_type = detail.source_type.as_deref().unwrap_or(""); let src_ref = detail.source_ref.as_deref().unwrap_or(""); console_writeln!( - console, + io, "{} : [{}] {} {}", console_format!("<info>source</info>"), src_type, @@ -1039,7 +1041,7 @@ async fn print_package_detail( let dist_type = detail.dist_type.as_deref().unwrap_or(""); let dist_ref = detail.dist_ref.as_deref().unwrap_or(""); console_writeln!( - console, + io, "{} : [{}] {} {}", console_format!("<info>dist</info>"), dist_type, @@ -1049,18 +1051,13 @@ async fn print_package_detail( } if let Some(ref path) = detail.install_path { - console_writeln!( - console, - "{} : {}", - console_format!("<info>path</info>"), - path, - ); + console_writeln!(io, "{} : {}", console_format!("<info>path</info>"), path,); } // A13: names (when multiple) if detail.names.len() > 1 { console_writeln!( - console, + io, "{} : {}", console_format!("<info>names</info>"), detail.names.join(", "), @@ -1072,12 +1069,12 @@ async fn print_package_detail( && let Some(obj) = support.as_object() && !obj.is_empty() { - console_writeln!(console, ""); - console_writeln!(console, "<info>support</info>"); + console_writeln!(io, ""); + console_writeln!(io, "<info>support</info>"); for (key, val) in obj { let v = val.as_str().unwrap_or(""); console_writeln!( - console, + io, "{} {}", key, console_format!("<comment>{}</comment>", v), @@ -1087,8 +1084,8 @@ async fn print_package_detail( // A13: autoload if let Some(ref autoload) = detail.autoload { - console_writeln!(console, ""); - console_writeln!(console, "<info>autoload</info>"); + console_writeln!(io, ""); + console_writeln!(io, "<info>autoload</info>"); if let Some(obj) = autoload.as_object() { for (loader_type, config) in obj { match config { @@ -1096,7 +1093,7 @@ async fn print_package_detail( for (k, v) in map { let v_str = v.as_str().unwrap_or(""); console_writeln!( - console, + io, "{}: {} => {}", loader_type, k, @@ -1108,7 +1105,7 @@ async fn print_package_detail( for item in arr { let v_str = item.as_str().unwrap_or(""); console_writeln!( - console, + io, "{}: {}", loader_type, console_format!("<comment>{}</comment>", v_str), @@ -1122,12 +1119,12 @@ async fn print_package_detail( } // Links: requires, requires-dev, conflict, provide, replace, suggests (A12) - print_links_section("requires", &detail.require, console); - print_links_section("requires (dev)", &detail.require_dev, console); - print_links_section("conflict", &detail.conflict, console); - print_links_section("provide", &detail.provide, console); - print_links_section("replace", &detail.replace, console); - print_links_section("suggests", &detail.suggest, console); + print_links_section("requires", &detail.require, io.clone()); + print_links_section("requires (dev)", &detail.require_dev, io.clone()); + print_links_section("conflict", &detail.conflict, io.clone()); + print_links_section("provide", &detail.provide, io.clone()); + print_links_section("replace", &detail.replace, io.clone()); + print_links_section("suggests", &detail.suggest, io.clone()); Ok(()) } @@ -1136,16 +1133,16 @@ async fn print_package_detail( fn print_links_section( label: &str, links: &BTreeMap<String, String>, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { if links.is_empty() { return; } - console_writeln!(console, ""); - console_writeln!(console, "<info>{}</info>", label); + console_writeln!(io, ""); + console_writeln!(io, "<info>{}</info>", label); for (name, constraint) in links { console_writeln!( - console, + io, "{} {}", name, console_format!("<comment>{}</comment>", constraint), @@ -1159,7 +1156,7 @@ async fn print_package_detail_json( detail: &PackageDetail, args: &ShowArgs, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let mut obj = serde_json::json!({ "name": detail.name, @@ -1215,7 +1212,7 @@ async fn print_package_detail_json( } } - console_writeln!(console, "{}", &serde_json::to_string_pretty(&obj)?); + console_writeln!(io, "{}", &serde_json::to_string_pretty(&obj)?); Ok(()) } @@ -1227,7 +1224,7 @@ async fn execute_installed( args: &ShowArgs, working_dir: &Path, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let vendor_dir = working_dir.join("vendor"); let installed = mozart_core::repository::installed::InstalledPackages::read(&vendor_dir)?; @@ -1238,7 +1235,7 @@ async fn execute_installed( let root = mozart_core::package::read_from_file(&composer_json_path)?; if !root.require.is_empty() || !root.require_dev.is_empty() { console_writeln_error!( - console, + io, "<warning>No dependencies installed. Try running mozart install or update.</warning>", ); } @@ -1259,7 +1256,7 @@ async fn execute_installed( Some(p) => { let install_path = vendor_dir.join(&p.name); let path_str = resolve_path(&install_path); - console_writeln!(console, "{} {}", p.name, path_str); + console_writeln!(io, "{} {}", p.name, path_str); } None => { anyhow::bail!( @@ -1296,7 +1293,7 @@ async fn execute_installed( } }; let detail = installed_to_detail(pkg, &vendor_dir); - return print_package_detail(&detail, args, repo_cache, console).await; + return print_package_detail(&detail, args, repo_cache, io).await; } } @@ -1305,7 +1302,7 @@ async fn execute_installed( for pkg in &packages { let install_path = vendor_dir.join(&pkg.name); let path_str = resolve_path(&install_path); - console_writeln!(console, "{} {}", pkg.name, path_str); + console_writeln!(io, "{} {}", pkg.name, path_str); } return Ok(()); } @@ -1314,7 +1311,7 @@ async fn execute_installed( let show_latest = args.latest || args.outdated; if args.name_only && !show_latest { for pkg in &packages { - console_writeln!(console, "{}", &pkg.name); + console_writeln!(io, "{}", &pkg.name); } return Ok(()); } @@ -1327,13 +1324,13 @@ async fn execute_installed( if args.name_only { for e in &entries { - console_writeln!(console, "{}", &e.name); + console_writeln!(io, "{}", &e.name); } return Ok(()); } // A10: --strict exit code - let has_outdated = render_package_list(&mut entries, args, "installed", console)?; + let has_outdated = render_package_list(&mut entries, args, "installed", io.clone())?; if args.strict && has_outdated { return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, @@ -1378,7 +1375,7 @@ async fn execute_locked( args: &ShowArgs, working_dir: &Path, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let lock_path = working_dir.join("composer.lock"); if !lock_path.exists() { @@ -1424,14 +1421,14 @@ async fn execute_locked( } }; let detail = locked_to_detail(pkg); - return print_package_detail(&detail, args, repo_cache, console).await; + return print_package_detail(&detail, args, repo_cache, io).await; } } // --path list mode if args.path { console_writeln_error!( - console, + io, "<warning>--path is not supported with --locked</warning>", ); return Ok(()); @@ -1441,7 +1438,7 @@ async fn execute_locked( let show_latest = args.latest || args.outdated; if args.name_only && !show_latest { for pkg in &packages { - console_writeln!(console, "{}", &pkg.name); + console_writeln!(io, "{}", &pkg.name); } return Ok(()); } @@ -1454,13 +1451,13 @@ async fn execute_locked( if args.name_only { for e in &entries { - console_writeln!(console, "{}", &e.name); + console_writeln!(io, "{}", &e.name); } return Ok(()); } // A10: --strict exit code; A14: use "locked" as the JSON key - let has_outdated = render_package_list(&mut entries, args, "locked", console)?; + let has_outdated = render_package_list(&mut entries, args, "locked", io.clone())?; if args.strict && has_outdated { return Err(mozart_core::exit_code::bail_silent( mozart_core::exit_code::GENERAL_ERROR, @@ -1477,7 +1474,7 @@ async fn execute_locked( fn show_self( args: &ShowArgs, working_dir: &Path, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let composer_json_path = working_dir.join("composer.json"); if !composer_json_path.exists() { @@ -1486,31 +1483,31 @@ fn show_self( let root = mozart_core::package::read_from_file(&composer_json_path)?; if args.name_only { - console_writeln!(console, "{}", &root.name); + console_writeln!(io, "{}", &root.name); return Ok(()); } console_writeln!( - console, + io, "{} : {}", console_format!("<info>name</info>"), root.name, ); console_writeln!( - console, + io, "{} : {}", console_format!("<info>descrip.</info>"), root.description.as_deref().unwrap_or(""), ); console_writeln!( - console, + io, "{} : {}", console_format!("<info>type</info>"), root.package_type.as_deref().unwrap_or("project"), ); if let Some(ref license) = root.license { console_writeln!( - console, + io, "{} : {}", console_format!("<info>license</info>"), format_license_for_show(license), @@ -1518,7 +1515,7 @@ fn show_self( } if let Some(ref homepage) = root.homepage { console_writeln!( - console, + io, "{} : {}", console_format!("<info>homepage</info>"), homepage, @@ -1527,11 +1524,11 @@ fn show_self( // Requires if !root.require.is_empty() { - console_writeln!(console, ""); - console_writeln!(console, "<info>requires</info>"); + console_writeln!(io, ""); + console_writeln!(io, "<info>requires</info>"); for (name, constraint) in &root.require { console_writeln!( - console, + io, "{} {}", name, console_format!("<comment>{}</comment>", constraint), @@ -1541,11 +1538,11 @@ fn show_self( // Requires (dev) if !root.require_dev.is_empty() { - console_writeln!(console, ""); - console_writeln!(console, "<info>requires (dev)</info>"); + console_writeln!(io, ""); + console_writeln!(io, "<info>requires (dev)</info>"); for (name, constraint) in &root.require_dev { console_writeln!( - console, + io, "{} {}", name, console_format!("<comment>{}</comment>", constraint), @@ -1563,7 +1560,7 @@ fn show_self( fn show_tree( args: &ShowArgs, working_dir: &Path, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let lock_path = working_dir.join("composer.lock"); let composer_json_path = working_dir.join("composer.json"); @@ -1604,7 +1601,7 @@ fn show_tree( }; console_writeln!( - console, + io, "<info>{}</info> <comment>{}</comment>", &root.name, root.description.as_deref().unwrap_or(""), @@ -1625,7 +1622,7 @@ fn show_tree( child_prefix, &mut visited_global, 0, - console, + io.clone(), ); } @@ -1641,7 +1638,7 @@ fn print_tree_node( child_prefix: &str, visited: &mut IndexSet<String>, depth: usize, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { const MAX_DEPTH: usize = 10; @@ -1652,7 +1649,7 @@ fn print_tree_node( let version = format_version(&pkg.version); console_writeln!( - console, + io, "{} {} {}", prefix, console_format!("<info>{}</info> <comment>{}</comment>", pkg_name, &version), @@ -1661,12 +1658,7 @@ fn print_tree_node( if visited.contains(&key) || depth >= MAX_DEPTH { if visited.contains(&key) { - console_writeln!( - console, - "{} {} (circular dependency)", - child_prefix, - pkg_name, - ); + console_writeln!(io, "{} {} (circular dependency)", child_prefix, pkg_name,); } return; } @@ -1704,7 +1696,7 @@ fn print_tree_node( &grandchild_prefix, visited, depth + 1, - console, + io.clone(), ); } @@ -1712,7 +1704,7 @@ fn print_tree_node( } else { if !is_platform_package(&key) { console_writeln!( - console, + io, "{} {} {} (not installed)", prefix, console_format!("<comment>{}</comment>", pkg_name), @@ -1729,7 +1721,7 @@ fn print_tree_node( fn show_platform( args: &ShowArgs, working_dir: &Path, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let mut platform_packages: Vec<(String, String, String)> = Vec::new(); @@ -1785,7 +1777,7 @@ fn show_platform( }) .collect(); console_writeln!( - console, + io, "{}", &serde_json::to_string_pretty(&serde_json::json!({ "platform": json_entries }))?, ); @@ -1793,7 +1785,7 @@ fn show_platform( } if platform_packages.is_empty() { - console.info( + io.lock().unwrap().info( "No platform packages detected. Install PHP or add platform requirements to composer.json.", ); return Ok(()); @@ -1801,7 +1793,7 @@ fn show_platform( if args.name_only { for (name, _, _) in &platform_packages { - console_writeln!(console, "{}", name); + console_writeln!(io, "{}", name); } return Ok(()); } @@ -1819,7 +1811,7 @@ fn show_platform( for (name, version, _source) in &platform_packages { console_writeln!( - console, + io, "{} {}", console_format!("<info>{:<width$}</info>", name, width = name_width), console_format!( @@ -1841,10 +1833,10 @@ async fn show_available( args: &ShowArgs, working_dir: &Path, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { if let Some(ref pkg_name) = args.package { - return show_available_versions(pkg_name, repo_cache, args, console).await; + return show_available_versions(pkg_name, repo_cache, args, io).await; } let vendor_dir = working_dir.join("vendor"); @@ -1857,10 +1849,10 @@ async fn show_available( if lock_path.exists() { let lock = mozart_core::repository::lockfile::LockFile::read_from_file(&lock_path)?; console_writeln!( - console, + io, "<info>Available versions for locked packages (from Packagist):</info>", ); - console_writeln!(console, ""); + console_writeln!(io, ""); let mut all_packages: Vec<&mozart_core::repository::lockfile::LockedPackage> = lock.packages.iter().collect(); @@ -1874,13 +1866,13 @@ async fn show_available( if is_platform_package(&pkg.name) { continue; } - show_available_versions_inline(&pkg.name, repo_cache, console).await; + show_available_versions_inline(&pkg.name, repo_cache, io).await; } return Ok(()); } console_writeln_error!( - console, + io, "<warning>No dependencies installed. Try running mozart install or update.</warning>", ); return Ok(()); @@ -1888,10 +1880,10 @@ async fn show_available( }; console_writeln!( - console, + io, "<info>Available versions for installed packages (from Packagist):</info>", ); - console_writeln!(console, ""); + console_writeln!(io, ""); if args.format == "json" { let mut json_entries: Vec<serde_json::Value> = Vec::new(); @@ -1921,7 +1913,7 @@ async fn show_available( } } let output = serde_json::json!({ "packages": json_entries }); - console_writeln!(console, "{}", &serde_json::to_string_pretty(&output)?); + console_writeln!(io, "{}", &serde_json::to_string_pretty(&output)?); return Ok(()); } @@ -1929,7 +1921,7 @@ async fn show_available( if is_platform_package(&pkg.name) { continue; } - show_available_versions_inline(&pkg.name, repo_cache, console).await; + show_available_versions_inline(&pkg.name, repo_cache, io).await; } Ok(()) @@ -1939,12 +1931,12 @@ async fn show_available_versions( pkg_name: &str, repo_cache: &mozart_core::repository::cache::Cache, args: &ShowArgs, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let versions = mozart_core::repository::packagist::fetch_package_versions(pkg_name, repo_cache).await?; if versions.is_empty() { - console_writeln!(console, "No versions found for {pkg_name}"); + console_writeln!(io, "No versions found for {pkg_name}"); return Ok(()); } @@ -1954,14 +1946,14 @@ async fn show_available_versions( "name": pkg_name, "versions": version_strings, }); - console_writeln!(console, "{}", &serde_json::to_string_pretty(&output)?); + console_writeln!(io, "{}", &serde_json::to_string_pretty(&output)?); return Ok(()); } - console_writeln!(console, "<info>Available versions for {pkg_name}:</info>"); + console_writeln!(io, "<info>Available versions for {pkg_name}:</info>"); for v in &versions { console_writeln!( - console, + io, " {}", console_format!("<comment>{}</comment>", &v.version), ); @@ -1972,13 +1964,13 @@ async fn show_available_versions( async fn show_available_versions_inline( pkg_name: &str, repo_cache: &mozart_core::repository::cache::Cache, - console: &mozart_core::console::Console, + io: &std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) { match mozart_core::repository::packagist::fetch_package_versions(pkg_name, repo_cache).await { Ok(versions) => { if versions.is_empty() { console_writeln!( - console, + io, "{}: no versions found", console_format!("<info>{}</info>", pkg_name), ); @@ -1995,7 +1987,7 @@ async fn show_available_versions_inline( String::new() }; console_writeln!( - console, + io, "{}: {}{}", console_format!("<info>{}</info>", pkg_name), console_format!("<comment>{}</comment>", &shown.join(", ")), @@ -2004,7 +1996,7 @@ async fn show_available_versions_inline( } Err(_) => { console_writeln!( - console, + io, "{}: (could not fetch from Packagist)", console_format!("<comment>{}</comment>", pkg_name), ); diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs index f0445bf..7cd7546 100644 --- a/crates/mozart/src/commands/status.rs +++ b/crates/mozart/src/commands/status.rs @@ -1,7 +1,7 @@ use crate::composer::Composer; use clap::Args; use mozart_core::composer::{InstallationSource, LocalPackage}; -use mozart_core::console::Console; +use mozart_core::console::IoInterface; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; @@ -23,7 +23,7 @@ struct VerRef { pub async fn execute( _args: &StatusArgs, cli: &super::Cli, - console: &Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let composer = Composer::require(cli.working_dir()?)?; @@ -105,45 +105,45 @@ pub async fn execute( } if errors.is_empty() && unpushed_changes.is_empty() && vcs_version_changes.is_empty() { - console_writeln_error!(console, "<info>No local changes</info>"); + console_writeln_error!(io, "<info>No local changes</info>"); return Ok(()); } if !errors.is_empty() { console_writeln_error!( - console, + io, "<error>You have changes in the following dependencies:</error>" ); for (path, changes) in &errors { if cli.is_verbose() { - console_writeln!(console, "<info>{path}</info>:"); - console_writeln!(console, "{}", &indent_block(changes)); + console_writeln!(io, "<info>{path}</info>:"); + console_writeln!(io, "{}", &indent_block(changes)); } else { - console_writeln!(console, "{}", path); + console_writeln!(io, "{}", path); } } } if !unpushed_changes.is_empty() { console_writeln_error!( - console, + io, "<warning>You have unpushed changes on the current branch in the following dependencies:</warning>" ); for (path, changes) in &unpushed_changes { if cli.is_verbose() { - console_writeln!(console, "<info>{path}</info>:"); - console_writeln!(console, "{}", &indent_block(changes)); + console_writeln!(io, "<info>{path}</info>:"); + console_writeln!(io, "{}", &indent_block(changes)); } else { - console_writeln!(console, "{}", path); + console_writeln!(io, "{}", path); } } } if !vcs_version_changes.is_empty() { console_writeln_error!( - console, + io, "<warning>You have version variations in the following dependencies:</warning>" ); @@ -159,23 +159,23 @@ pub async fn execute( } else { change.current.version.clone() }; - if console.is_very_verbose() { + if io.lock().unwrap().is_very_verbose() { prev.push_str(&format!(" ({})", change.previous.reference)); curr.push_str(&format!(" ({})", change.current.reference)); } - console_writeln!(console, "<info>{path}</info>:"); + console_writeln!(io, "<info>{path}</info>:"); console_writeln!( - console, + io, " From <comment>{prev}</comment> to <comment>{curr}</comment>" ); } else { - console_writeln!(console, "{}", path); + console_writeln!(io, "{}", path); } } } if !cli.is_verbose() { - console_writeln_error!(console, "Use --verbose (-v) to see a list of files"); + console_writeln_error!(io, "Use --verbose (-v) to see a list of files"); } let code = (if !errors.is_empty() { 1 } else { 0 }) diff --git a/crates/mozart/src/commands/suggests.rs b/crates/mozart/src/commands/suggests.rs index 1643e94..b882f5c 100644 --- a/crates/mozart/src/commands/suggests.rs +++ b/crates/mozart/src/commands/suggests.rs @@ -1,6 +1,6 @@ use clap::Args; use indexmap::IndexSet; -use mozart_core::console; +use mozart_core::console::IoInterface; use mozart_core::installer::{ InstalledRepoLite, MODE_BY_PACKAGE, MODE_BY_SUGGESTION, MODE_LIST, RootInfo, SuggestedPackagesReporter, @@ -37,8 +37,10 @@ pub struct SuggestsArgs { pub async fn execute( args: &SuggestsArgs, cli: &super::Cli, - console: &console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { + let io_guard = io.lock().unwrap(); + let console = &**io_guard; let working_dir = cli.working_dir()?; let lock_path = working_dir.join("composer.lock"); @@ -314,8 +316,8 @@ mod tests { } } - fn console() -> console::Console { - console::Console::new(0, false, false, true, true) + fn console() -> mozart_core::console::Console { + mozart_core::console::Console::new(0, false, false, true, true) } #[test] diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 5498983..f4bcf64 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -1,6 +1,7 @@ use crate::composer::Composer; use clap::Args; use indexmap::{IndexMap, IndexSet}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::package; use mozart_core::platform::is_platform_package; @@ -468,7 +469,7 @@ pub fn expand_wildcards( specifiers: &[String], lock: &lockfile::LockFile, root_requires: &IndexSet<String>, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> Vec<String> { // Collect all locked package names (prod + dev) plus the current root // require names. Mirrors Composer's @@ -519,7 +520,7 @@ pub fn expand_wildcards( } } if !matched { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Package '{}' listed for update is not in the lock file. Specifier will be ignored.</warning>", spec )); @@ -748,10 +749,10 @@ pub fn expand_packages( with_all_dependencies: bool, root_requires: &IndexSet<String>, repo_requires: &IndexMap<String, IndexSet<String>>, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> Vec<String> { let mut packages: Vec<String> = if let Some(lock) = lock { - expand_wildcards(specifiers, lock, root_requires, console) + expand_wildcards(specifiers, lock, root_requires, io) } else { // No lock file: pass through as-is (no wildcards can be resolved) specifiers.iter().map(|s| s.to_lowercase()).collect() @@ -779,19 +780,21 @@ pub fn expand_packages( /// returns the full package list unchanged. pub fn interactive_select_packages( packages: Vec<String>, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> Vec<String> { use std::io::{self, BufRead, IsTerminal, Write}; let stdin = io::stdin(); if !stdin.is_terminal() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Interactive mode requires a TTY. Running non-interactively with all packages.</warning>" )); return packages; } - console.info("Select packages to update (y/n for each):"); + io.lock() + .unwrap() + .info("Select packages to update (y/n for each):"); let mut selected = Vec::new(); let stdin_locked = stdin.lock(); @@ -814,7 +817,7 @@ pub fn interactive_select_packages( break; } _ => { - console.info(" Please answer y or n."); + io.lock().unwrap().info(" Please answer y or n."); } } } @@ -921,7 +924,7 @@ fn major_minor(version: &str) -> (u64, u64) { pub async fn execute( args: &UpdateArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repositories = std::sync::Arc::new( @@ -937,7 +940,7 @@ pub async fn execute( &working_dir, None, args, - console, + io.clone(), repositories, &mut executor, ) @@ -962,25 +965,27 @@ pub async fn run( working_dir: &std::path::Path, path_repo_base_override: Option<&std::path::Path>, args: &UpdateArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, repositories: std::sync::Arc<mozart_core::repository::repository::RepositorySet>, executor: &mut dyn mozart_core::repository::installer_executor::InstallerExecutor, ) -> anyhow::Result<()> { // Step 2: Handle deprecated flags if args.dev { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>The --dev option is deprecated. Dev packages are updated by default.</warning>" )); } if args.no_suggest { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>" )); } // --root-reqs: if no packages specified, auto-populate with root requirements if args.root_reqs && args.packages.is_empty() { - console.info("Using root requirements as the update list (--root-reqs)."); + io.lock() + .unwrap() + .info("Using root requirements as the update list (--root-reqs)."); } // Step 3: Read composer.json @@ -1183,7 +1188,7 @@ pub async fn run( args.with_all_dependencies, &root_requires, &repo_requires, - console, + io.clone(), ) .into_iter() .collect(); @@ -1439,11 +1444,15 @@ pub async fn run( }; // Step 6: Print header and run resolver - console.info("Loading composer repositories with package information"); + io.lock() + .unwrap() + .info("Loading composer repositories with package information"); if dev_mode { - console.info("Updating dependencies (including require-dev)"); + io.lock() + .unwrap() + .info("Updating dependencies (including require-dev)"); } else { - console.info("Updating dependencies"); + io.lock().unwrap().info("Updating dependencies"); } let mut resolved = match resolver::resolve(&request).await { Ok(packages) => packages, @@ -1460,7 +1469,7 @@ pub async fn run( match lockfile::LockFile::read_from_file(&lock_path) { Ok(l) => Some(l), Err(e) => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>Could not read existing composer.lock: {}. Treating as a fresh install.</warning>", e )); @@ -1518,12 +1527,12 @@ pub async fn run( args.with_all_dependencies, &root_requires, &repo_requires, - console, + io.clone(), ); // 2. Interactive selection (filter the expanded list) if args.interactive { - expanded = interactive_select_packages(expanded, console); + expanded = interactive_select_packages(expanded, io.clone()); } expanded @@ -1535,7 +1544,7 @@ pub async fn run( if args.interactive { match &old_lock { None => { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>No lock file found. --interactive mode skipped.</warning>" )); vec![] @@ -1552,7 +1561,7 @@ pub async fn run( .map(|p| p.name.to_lowercase()), ) .collect(); - interactive_select_packages(all_names, console) + interactive_select_packages(all_names, io.clone()) } } } else { @@ -1574,14 +1583,18 @@ pub async fn run( } } } else if args.minimal_changes && update_packages.is_empty() && old_lock.is_some() { - console.info("Minimal changes mode: preserving locked versions where possible."); + io.lock() + .unwrap() + .info("Minimal changes mode: preserving locked versions where possible."); } // Apply --patch-only filter: restrict updates to patch-level changes only if args.patch_only && let Some(ref lock) = old_lock { - console.info("Patch-only mode: restricting updates to patch-level changes."); + io.lock() + .unwrap() + .info("Patch-only mode: restricting updates to patch-level changes."); resolved = apply_patch_only(resolved, lock); } @@ -1647,7 +1660,7 @@ pub async fn run( .filter(|c| matches!(c.kind, ChangeKind::Uninstall { .. })) .collect(); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>Lock file operations: {} install{}, {} update{}, {} removal{}</info>", installs.len(), if installs.len() == 1 { "" } else { "s" }, @@ -1662,13 +1675,13 @@ pub async fn run( match &change.kind { ChangeKind::Uninstall { old_version } => { if args.dry_run { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Would remove <info>{}</info> (<comment>{}</comment>)", change.name, old_version )); } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Removing <info>{}</info> (<comment>{}</comment>)", change.name, old_version @@ -1677,13 +1690,13 @@ pub async fn run( } ChangeKind::Install { new_version } => { if args.dry_run { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Would lock <info>{}</info> (<comment>{}</comment>)", change.name, new_version )); } else { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - Locking <info>{}</info> (<comment>{}</comment>)", change.name, new_version @@ -1705,7 +1718,7 @@ pub async fn run( } else { "Upgrading" }; - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( " - {} <info>{}</info> (<comment>{}</comment> => <comment>{}</comment>)", direction, change.name, @@ -1718,7 +1731,9 @@ pub async fn run( // Step 11: Write lock file (unless --dry-run) if !args.dry_run { - console.info(&console_format!("<info>Writing lock file</info>")); + io.lock() + .unwrap() + .info(&console_format!("<info>Writing lock file</info>")); new_lock.write_to_file(&lock_path)?; } @@ -1734,7 +1749,7 @@ pub async fn run( let no_dev_only = mode == "no-dev"; let bump_composer = Composer::require(working_dir)?; let bump_exit = super::bump::do_bump( - console, + io.clone(), &bump_composer, dev_only, no_dev_only, @@ -1776,7 +1791,7 @@ pub async fn run( download_only: false, prefer_source, }, - console, + io.clone(), executor, ) .await?; @@ -1845,12 +1860,12 @@ mod tests { } } - fn test_console() -> mozart_core::console::Console { - mozart_core::console::Console { - interactive: false, - verbosity: mozart_core::console::Verbosity::Normal, - decorated: false, - } + fn test_console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { + std::sync::Arc::new(std::sync::Mutex::new( + Box::new(mozart_core::console::Console::new( + 0, false, false, false, false, + )) as Box<dyn IoInterface>, + )) } #[test] @@ -2178,7 +2193,7 @@ mod tests { .map(String::from) .collect(); let specs = vec!["psr/log".to_string(), "nonexistent/pkg".to_string()]; - let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); assert_eq!(result, vec!["psr/log", "nonexistent/pkg"]); } @@ -2191,7 +2206,7 @@ mod tests { ]); let specs = vec!["symfony/*".to_string()]; let root_requires: IndexSet<String> = IndexSet::new(); - let mut result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); + let mut result = expand_wildcards(&specs, &lock, &root_requires, test_console()); result.sort(); assert_eq!(result, vec!["symfony/console", "symfony/http-kernel"]); } @@ -2202,7 +2217,7 @@ mod tests { let specs = vec!["unknown/*".to_string()]; let root_requires: IndexSet<String> = IndexSet::new(); // Should return empty (no match), no panic - let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); assert!(result.is_empty()); } @@ -2211,7 +2226,7 @@ mod tests { 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 root_requires: IndexSet<String> = IndexSet::new(); - let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); assert_eq!(result.len(), 1); assert_eq!(result[0], "psr/log"); } @@ -2222,7 +2237,7 @@ mod tests { lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); let specs = vec!["phpunit/*".to_string()]; let root_requires: IndexSet<String> = IndexSet::new(); - let result = expand_wildcards(&specs, &lock, &root_requires, &test_console()); + let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); assert_eq!(result, vec!["phpunit/phpunit"]); } @@ -2354,7 +2369,7 @@ mod tests { false, // with_all_dependencies &IndexSet::new(), &IndexMap::new(), - &test_console(), + 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 7595ee5..a6dbf91 100644 --- a/crates/mozart/src/commands/validate.rs +++ b/crates/mozart/src/commands/validate.rs @@ -1,6 +1,7 @@ use crate::composer::Composer; use clap::Args; use mozart_core::config_validator::{ValidationResult, ValidatorOptions, validate_manifest}; +use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::package::RootPackageData; @@ -54,7 +55,7 @@ fn should_check_lock(args: &ValidateArgs, config_lock: bool) -> bool { pub async fn execute( args: &ValidateArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; @@ -134,7 +135,7 @@ pub async fn execute( let check_publish = !args.no_check_publish; let file_name = file.display().to_string(); output_result( - console, + io.clone(), &file_name, &result, check_publish, @@ -146,11 +147,12 @@ pub async fn execute( let (dep_errors, dep_warnings) = if args.with_dependencies { let vendor_dir = file.parent().unwrap_or(Path::new(".")).join("vendor"); if let Some(comp) = &composer { - validate_dependencies(comp, args, console) + validate_dependencies(comp, args, io.clone()) } else if vendor_dir.exists() { - validate_dependencies_vendor_walk(&vendor_dir, args, console) + validate_dependencies_vendor_walk(&vendor_dir, args, io.clone()) } else { - console + io.lock() + .unwrap() .info("No vendor directory found. Run `mozart install` to install dependencies."); (0, 0) } @@ -185,7 +187,7 @@ pub async fn execute( fn validate_dependencies( composer: &Composer, args: &ValidateArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> (u32, u32) { let mut dep_errors = 0u32; let mut dep_warnings = 0u32; @@ -234,7 +236,7 @@ fn validate_dependencies( // Per-dep rendering — same header format as the root file output_result( - console, + io.clone(), package.pretty_name(), &dep_result, false, // check_publish: false for deps, matching Composer @@ -251,7 +253,7 @@ fn validate_dependencies( fn validate_dependencies_vendor_walk( vendor_dir: &Path, args: &ValidateArgs, - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, ) -> (u32, u32) { let mut dep_errors = 0u32; let mut dep_warnings = 0u32; @@ -308,7 +310,7 @@ fn validate_dependencies_vendor_walk( dep_warnings += dep_result.warnings.len() as u32; } - output_result(console, &pkg_name, &dep_result, false, false, &[]); + output_result(io.clone(), &pkg_name, &dep_result, false, false, &[]); } } @@ -366,7 +368,7 @@ fn check_lock_freshness( /// (dependency), matching how Composer calls `outputResult($io, $file, …)` /// for the root and `outputResult($io, $package->getPrettyName(), …)` for deps. fn output_result( - console: &mozart_core::console::Console, + io: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>>, name: &str, result: &ValidationResult, check_publish: bool, @@ -375,34 +377,34 @@ fn output_result( ) { // Print header message if result.has_errors() { - console.error(&console_format!( + io.lock().unwrap().error(&console_format!( "<error>{name} is invalid, the following errors/warnings were found:</error>" )); } else if result.has_publish_errors() && check_publish { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>{name} is valid for simple usage with Composer but has</info>" )); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>strict errors that make it unable to be published as a package</info>" )); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>" )); } else if result.has_warnings() { - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<info>{name} is valid, but with a few warnings</info>" )); - console.info(&console_format!( + io.lock().unwrap().info(&console_format!( "<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>" )); } else if !lock_errors.is_empty() { let kind = if check_lock { "errors" } else { "warnings" }; console_writeln!( - console, + io, "<info>{name} is valid but your composer.lock has some {kind}</info>", ); } else { - console_writeln!(console, "<info>{name} i valid</info>"); + console_writeln!(io, "<info>{name} i valid</info>"); } // Collect error and warning message lines @@ -444,14 +446,16 @@ fn output_result( // Print errors for msg in &all_errors { - console.error(msg); + io.lock().unwrap().error(msg); } for msg in &all_warnings { if msg.starts_with('#') { - console.info(&console_format!("<warning>{msg}</warning>")); + io.lock() + .unwrap() + .info(&console_format!("<warning>{msg}</warning>")); } else { - console.info(msg); + io.lock().unwrap().info(msg); } } } diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 970fce4..fbeba6d 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -9,11 +9,11 @@ //! Composer's PHPUnit suite uses. use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use clap::Parser; use mozart::commands::{Cli, Commands, install, update}; -use mozart_core::console::Console; +use mozart_core::console::{Console, IoInterface}; use mozart_core::exit_code::MozartError; use mozart_core::repository::installer_executor::TraceRecorderExecutor; use mozart_core::repository::repository::RepositorySet; @@ -108,7 +108,10 @@ async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result<InProcessRu // Quiet console: assertions run against the recorder + on-disk // artifacts, not captured stdout/stderr (Console doesn't yet support // buffered sinks). EXPECT-OUTPUT enforcement is a follow-up. - let console = Console::new(0, true, false, true, true); + let console: Arc<Mutex<Box<dyn IoInterface>>> = Arc::new(Mutex::new(Box::new(Console::new( + 0, true, false, true, true, + )) + as Box<dyn IoInterface>)); let repositories = Arc::new(RepositorySet::empty()); let mut executor = TraceRecorderExecutor::new(); @@ -119,7 +122,7 @@ async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result<InProcessRu root, Some(&path_repo_base), args, - &console, + console.clone(), repositories, &mut executor, ) @@ -130,7 +133,7 @@ async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result<InProcessRu root, Some(&path_repo_base), args, - &console, + console.clone(), repositories, &mut executor, ) |
