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