aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src/console.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-21 23:38:32 +0900
committernsfisis <nsfisis@gmail.com>2026-02-21 23:38:32 +0900
commit52310761f67220c9c075cd847205825a720035ee (patch)
tree0528fc94aea7853e41313e19964d74a958dae9c9 /crates/mozart/src/console.rs
parent92da9e37c68beb180e45e550fba5acd7d28dca27 (diff)
downloadphp-mozart-52310761f67220c9c075cd847205825a720035ee.tar.gz
php-mozart-52310761f67220c9c075cd847205825a720035ee.tar.zst
php-mozart-52310761f67220c9c075cd847205825a720035ee.zip
feat(console): add structured error handling, verbosity, and suggestions
Implement Phase 7.2 error handling & UX infrastructure: - Add exit_code module with MozartError, bail()/bail_silent() helpers, and Composer-compatible exit code constants (0-5, 100) - Redesign Console struct with Verbosity enum (Quiet/Normal/Verbose/ VeryVerbose/Debug), ANSI auto-detection via IsTerminal, and verbosity-gated output methods (info/verbose/debug/error) - Thread Console through all 33 command execute() signatures - Replace all std::process::exit() calls with structured MozartError returns handled in main() - Migrate eprintln\! status messages to console.info() for quiet-mode suppression - Add suggest module with Levenshtein distance and "Did you mean?" formatting for future package name suggestions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src/console.rs')
-rw-r--r--crates/mozart/src/console.rs259
1 files changed, 251 insertions, 8 deletions
diff --git a/crates/mozart/src/console.rs b/crates/mozart/src/console.rs
index 863e9ba..5f108c0 100644
--- a/crates/mozart/src/console.rs
+++ b/crates/mozart/src/console.rs
@@ -1,5 +1,10 @@
use colored::{ColoredString, Colorize};
use dialoguer::{Confirm, Input};
+use std::io::IsTerminal;
+
+// ---------------------------------------------------------------------------
+// Tag-style color helpers (module-level free functions, unchanged API)
+// ---------------------------------------------------------------------------
/// `<info>` — green foreground
pub fn info(message: &str) -> ColoredString {
@@ -31,29 +36,169 @@ pub fn warning(message: &str) -> ColoredString {
message.black().on_yellow()
}
+// ---------------------------------------------------------------------------
+// Verbosity
+// ---------------------------------------------------------------------------
+
+/// Output verbosity level, ordered from least to most verbose.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum Verbosity {
+ /// `-q` / `--quiet`: suppress all non-error output.
+ Quiet,
+ /// Default: normal informational messages.
+ Normal,
+ /// `-v`: additional detail (URLs, cache hits, skips).
+ Verbose,
+ /// `-vv`: HTTP details, file operations, resolver iterations.
+ VeryVerbose,
+ /// `-vvv`: full debug output (headers, raw payloads, timing).
+ Debug,
+}
+
+impl Verbosity {
+ /// Construct a `Verbosity` from CLI flag counts.
+ ///
+ /// - `quiet == true` → `Quiet` (takes priority over `-v` flags)
+ /// - `verbose_count == 0` → `Normal`
+ /// - `verbose_count == 1` → `Verbose`
+ /// - `verbose_count == 2` → `VeryVerbose`
+ /// - `verbose_count >= 3` → `Debug`
+ pub fn from_flags(verbose_count: u8, quiet: bool) -> Self {
+ if quiet {
+ return Verbosity::Quiet;
+ }
+ match verbose_count {
+ 0 => Verbosity::Normal,
+ 1 => Verbosity::Verbose,
+ 2 => Verbosity::VeryVerbose,
+ _ => Verbosity::Debug,
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Console
+// ---------------------------------------------------------------------------
+
+/// Central IO hub for Mozart commands.
+///
+/// Constructed once in `commands::execute()` and passed as `&Console` to every
+/// command and library function that needs to produce output.
pub struct Console {
+ /// Whether the user can answer interactive prompts.
pub interactive: bool,
- pub quiet: bool,
+ /// Current verbosity level.
+ pub verbosity: Verbosity,
+ /// Whether ANSI color codes should be emitted.
+ pub decorated: bool,
}
impl Console {
- pub fn new(no_interaction: bool, quiet: bool) -> Self {
+ /// Build a `Console` from the parsed CLI.
+ ///
+ /// This is the primary constructor used in production. It reads
+ /// `cli.verbose`, `cli.quiet`, `cli.ansi`, `cli.no_ansi`, and
+ /// `cli.no_interaction` to configure all fields.
+ pub fn from_cli(cli: &crate::commands::Cli) -> Self {
+ let verbosity = Verbosity::from_flags(cli.verbose, cli.quiet);
+ let decorated = Self::resolve_decorated(cli.ansi, cli.no_ansi);
+ colored::control::set_override(decorated);
Self {
- interactive: !no_interaction,
- quiet,
+ interactive: !cli.no_interaction,
+ verbosity,
+ decorated,
}
}
- pub fn info(&self, msg: &str) {
- if !self.quiet {
+ /// Determine whether ANSI color output should be enabled.
+ ///
+ /// - `no_ansi == true` → always disable
+ /// - `ansi == true` → always enable
+ /// - Otherwise → auto-detect: enabled only when stderr is a TTY
+ pub fn resolve_decorated(ansi: bool, no_ansi: bool) -> bool {
+ if no_ansi {
+ return false;
+ }
+ if ansi {
+ return true;
+ }
+ std::io::stderr().is_terminal()
+ }
+
+ // -----------------------------------------------------------------------
+ // Output methods
+ // -----------------------------------------------------------------------
+
+ /// Write `msg` to stderr if `self.verbosity >= required`.
+ pub fn write(&self, msg: &str, required: Verbosity) {
+ if self.verbosity >= required {
eprintln!("{msg}");
}
}
- pub fn error(&self, msg: &str) {
+ /// Write `msg` to stdout if `self.verbosity >= required`.
+ pub fn write_stdout(&self, msg: &str, required: Verbosity) {
+ if self.verbosity >= required {
+ println!("{msg}");
+ }
+ }
+
+ /// Write an error to stderr. Always shown, even in quiet mode.
+ pub fn write_error(&self, msg: &str) {
eprintln!("{}", error(msg));
}
+ // Convenience verbosity-level shortcuts:
+
+ /// Normal-level message (suppressed by `--quiet`).
+ pub fn info(&self, msg: &str) {
+ self.write(msg, Verbosity::Normal);
+ }
+
+ /// Verbose-level message (shown with `-v` or higher).
+ pub fn verbose(&self, msg: &str) {
+ self.write(msg, Verbosity::Verbose);
+ }
+
+ /// Very-verbose-level message (shown with `-vv` or higher).
+ pub fn very_verbose(&self, msg: &str) {
+ self.write(msg, Verbosity::VeryVerbose);
+ }
+
+ /// Debug-level message (shown with `-vvv`).
+ pub fn debug(&self, msg: &str) {
+ self.write(msg, Verbosity::Debug);
+ }
+
+ /// Error message — always shown.
+ pub fn error(&self, msg: &str) {
+ self.write_error(msg);
+ }
+
+ // -----------------------------------------------------------------------
+ // Query methods
+ // -----------------------------------------------------------------------
+
+ pub fn is_verbose(&self) -> bool {
+ self.verbosity >= Verbosity::Verbose
+ }
+
+ pub fn is_very_verbose(&self) -> bool {
+ self.verbosity >= Verbosity::VeryVerbose
+ }
+
+ pub fn is_debug(&self) -> bool {
+ self.verbosity >= Verbosity::Debug
+ }
+
+ pub fn is_quiet(&self) -> bool {
+ self.verbosity == Verbosity::Quiet
+ }
+
+ // -----------------------------------------------------------------------
+ // Interactive prompt methods (unchanged from prior implementation)
+ // -----------------------------------------------------------------------
+
pub fn ask(&self, prompt: &str, default: &str) -> String {
if !self.interactive {
return default.to_string();
@@ -92,7 +237,7 @@ impl Console {
match validator(&input) {
Ok(()) => return Ok(input),
Err(e) => {
- self.error(&e);
+ self.write_error(&e);
}
}
}
@@ -110,3 +255,101 @@ impl Console {
.unwrap_or(true)
}
}
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // ── Verbosity::from_flags ───────────────────────────────────────────────
+
+ #[test]
+ fn test_verbosity_quiet_takes_priority() {
+ assert_eq!(Verbosity::from_flags(3, true), Verbosity::Quiet);
+ assert_eq!(Verbosity::from_flags(0, true), Verbosity::Quiet);
+ }
+
+ #[test]
+ fn test_verbosity_normal() {
+ assert_eq!(Verbosity::from_flags(0, false), Verbosity::Normal);
+ }
+
+ #[test]
+ fn test_verbosity_verbose() {
+ assert_eq!(Verbosity::from_flags(1, false), Verbosity::Verbose);
+ }
+
+ #[test]
+ fn test_verbosity_very_verbose() {
+ assert_eq!(Verbosity::from_flags(2, false), Verbosity::VeryVerbose);
+ }
+
+ #[test]
+ fn test_verbosity_debug() {
+ assert_eq!(Verbosity::from_flags(3, false), Verbosity::Debug);
+ assert_eq!(Verbosity::from_flags(10, false), Verbosity::Debug);
+ }
+
+ // ── Verbosity ordering ──────────────────────────────────────────────────
+
+ #[test]
+ fn test_verbosity_ordering() {
+ assert!(Verbosity::Quiet < Verbosity::Normal);
+ assert!(Verbosity::Normal < Verbosity::Verbose);
+ assert!(Verbosity::Verbose < Verbosity::VeryVerbose);
+ assert!(Verbosity::VeryVerbose < Verbosity::Debug);
+ }
+
+ // ── Console::resolve_decorated ──────────────────────────────────────────
+
+ #[test]
+ fn test_resolve_decorated_no_ansi_wins() {
+ assert!(!Console::resolve_decorated(true, true));
+ assert!(!Console::resolve_decorated(false, true));
+ }
+
+ #[test]
+ fn test_resolve_decorated_ansi_forces_on() {
+ assert!(Console::resolve_decorated(true, false));
+ }
+
+ // ── Console query methods ───────────────────────────────────────────────
+
+ fn make_console(verbosity: Verbosity) -> Console {
+ Console {
+ interactive: false,
+ verbosity,
+ decorated: false,
+ }
+ }
+
+ #[test]
+ fn test_is_quiet() {
+ assert!(make_console(Verbosity::Quiet).is_quiet());
+ assert!(!make_console(Verbosity::Normal).is_quiet());
+ }
+
+ #[test]
+ fn test_is_verbose() {
+ assert!(!make_console(Verbosity::Normal).is_verbose());
+ assert!(make_console(Verbosity::Verbose).is_verbose());
+ assert!(make_console(Verbosity::VeryVerbose).is_verbose());
+ assert!(make_console(Verbosity::Debug).is_verbose());
+ }
+
+ #[test]
+ fn test_is_very_verbose() {
+ assert!(!make_console(Verbosity::Verbose).is_very_verbose());
+ assert!(make_console(Verbosity::VeryVerbose).is_very_verbose());
+ assert!(make_console(Verbosity::Debug).is_very_verbose());
+ }
+
+ #[test]
+ fn test_is_debug() {
+ assert!(!make_console(Verbosity::VeryVerbose).is_debug());
+ assert!(make_console(Verbosity::Debug).is_debug());
+ }
+}