aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-10 15:29:19 +0900
committernsfisis <nsfisis@gmail.com>2026-05-10 15:29:19 +0900
commit46845eff8d1398f35099a0ef914f77bcaf473287 (patch)
tree12c4850f1d2f438d0ba6c363fdc0e5036cd4601d /crates
parent212506c364b2342dd9e5fa789e8cff38835dfe52 (diff)
downloadphp-mozart-46845eff8d1398f35099a0ef914f77bcaf473287.tar.gz
php-mozart-46845eff8d1398f35099a0ef914f77bcaf473287.tar.zst
php-mozart-46845eff8d1398f35099a0ef914f77bcaf473287.zip
refactor(io): introduce IoInterface trait mirroring Composer IOInterface
Add an `IoInterface` trait in mozart-core::console that mirrors `\Composer\IO\IOInterface`, implement it for `Console`, and switch commands, the auditor, and the suggested-packages reporter to accept the abstracted IO (typically `Arc<Mutex<Box<dyn IoInterface>>>` at the command boundary, `&dyn IoInterface` deeper down) instead of `&Console`. The console_writeln\!/write\! macros now go through `IoInterface::verbosity()` via the lock so any implementor works.
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart-core/src/console.rs194
-rw-r--r--crates/mozart-core/src/installer/suggested_packages_reporter.rs12
-rw-r--r--crates/mozart-core/src/repository/advisory.rs127
-rw-r--r--crates/mozart/src/commands.rs73
-rw-r--r--crates/mozart/src/commands/about.rs6
-rw-r--r--crates/mozart/src/commands/archive.rs23
-rw-r--r--crates/mozart/src/commands/audit.rs7
-rw-r--r--crates/mozart/src/commands/browse.rs56
-rw-r--r--crates/mozart/src/commands/bump.rs57
-rw-r--r--crates/mozart/src/commands/check_platform_reqs.rs35
-rw-r--r--crates/mozart/src/commands/clear_cache.rs18
-rw-r--r--crates/mozart/src/commands/completion.rs3
-rw-r--r--crates/mozart/src/commands/config.rs17
-rw-r--r--crates/mozart/src/commands/create_project.rs68
-rw-r--r--crates/mozart/src/commands/dependency.rs59
-rw-r--r--crates/mozart/src/commands/depends.rs5
-rw-r--r--crates/mozart/src/commands/diagnose.rs63
-rw-r--r--crates/mozart/src/commands/dump_autoload.rs17
-rw-r--r--crates/mozart/src/commands/exec.rs10
-rw-r--r--crates/mozart/src/commands/fund.rs44
-rw-r--r--crates/mozart/src/commands/global.rs8
-rw-r--r--crates/mozart/src/commands/init.rs140
-rw-r--r--crates/mozart/src/commands/install.rs86
-rw-r--r--crates/mozart/src/commands/licenses.rs49
-rw-r--r--crates/mozart/src/commands/outdated.rs5
-rw-r--r--crates/mozart/src/commands/prohibits.rs5
-rw-r--r--crates/mozart/src/commands/reinstall.rs11
-rw-r--r--crates/mozart/src/commands/remove.rs81
-rw-r--r--crates/mozart/src/commands/repository.rs178
-rw-r--r--crates/mozart/src/commands/require.rs133
-rw-r--r--crates/mozart/src/commands/run_script.rs56
-rw-r--r--crates/mozart/src/commands/search.rs30
-rw-r--r--crates/mozart/src/commands/self_update.rs37
-rw-r--r--crates/mozart/src/commands/show.rs274
-rw-r--r--crates/mozart/src/commands/status.rs34
-rw-r--r--crates/mozart/src/commands/suggests.rs10
-rw-r--r--crates/mozart/src/commands/update.rs107
-rw-r--r--crates/mozart/src/commands/validate.rs46
-rw-r--r--crates/mozart/tests/installer.rs13
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(&current_exe, &data_dir, console)
+ rollback(&current_exe, &data_dir, io.clone())
} else {
- update(args, &current_exe, &data_dir, console).await
+ update(args, &current_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,
)