diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 23:38:32 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 23:38:32 +0900 |
| commit | 52310761f67220c9c075cd847205825a720035ee (patch) | |
| tree | 0528fc94aea7853e41313e19964d74a958dae9c9 /crates/mozart/src/exit_code.rs | |
| parent | 92da9e37c68beb180e45e550fba5acd7d28dca27 (diff) | |
| download | php-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/exit_code.rs')
| -rw-r--r-- | crates/mozart/src/exit_code.rs | 114 |
1 files changed, 114 insertions, 0 deletions
diff --git a/crates/mozart/src/exit_code.rs b/crates/mozart/src/exit_code.rs new file mode 100644 index 0000000..bc01cfa --- /dev/null +++ b/crates/mozart/src/exit_code.rs @@ -0,0 +1,114 @@ +/// Exit code: success. +pub const OK: i32 = 0; + +/// Exit code: general / unclassified error. +pub const GENERAL_ERROR: i32 = 1; + +/// Exit code: dependency resolution failed. +pub const DEPENDENCY_RESOLUTION_FAILED: i32 = 2; + +/// Exit code: partial update requested but no lock file exists. +pub const NO_LOCK_FILE_FOR_PARTIAL_UPDATE: i32 = 3; + +/// Exit code: lock file is invalid or corrupt. +pub const LOCK_FILE_INVALID: i32 = 4; + +/// Exit code: audit found a security advisory. +pub const AUDIT_FAILED: i32 = 5; + +/// Exit code: HTTP / network transport error. +pub const TRANSPORT_ERROR: i32 = 100; + +// --------------------------------------------------------------------------- +// MozartError — carries a specific exit code through anyhow's error chain +// --------------------------------------------------------------------------- + +/// An error type that carries a specific exit code for Mozart to use on exit. +/// +/// Use [`bail`] or [`bail_silent`] to construct one wrapped in `anyhow::Error`. +#[derive(Debug)] +pub struct MozartError { + pub message: String, + pub exit_code: i32, +} + +impl std::fmt::Display for MozartError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for MozartError {} + +/// Return an `anyhow::Error` that carries `exit_code` and prints `message`. +pub fn bail(exit_code: i32, message: impl Into<String>) -> anyhow::Error { + MozartError { + message: message.into(), + exit_code, + } + .into() +} + +/// Return an `anyhow::Error` that carries `exit_code` but suppresses the +/// message (caller has already printed it). +pub fn bail_silent(exit_code: i32) -> anyhow::Error { + MozartError { + message: String::new(), + exit_code, + } + .into() +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constants_have_expected_values() { + assert_eq!(OK, 0); + assert_eq!(GENERAL_ERROR, 1); + assert_eq!(DEPENDENCY_RESOLUTION_FAILED, 2); + assert_eq!(NO_LOCK_FILE_FOR_PARTIAL_UPDATE, 3); + assert_eq!(LOCK_FILE_INVALID, 4); + assert_eq!(AUDIT_FAILED, 5); + assert_eq!(TRANSPORT_ERROR, 100); + } + + #[test] + fn test_mozart_error_display() { + let err = MozartError { + message: "something went wrong".to_string(), + exit_code: GENERAL_ERROR, + }; + assert_eq!(format!("{err}"), "something went wrong"); + } + + #[test] + fn test_bail_can_be_downcast() { + let err = bail(DEPENDENCY_RESOLUTION_FAILED, "cannot resolve"); + let me = err.downcast_ref::<MozartError>().expect("should downcast"); + assert_eq!(me.exit_code, DEPENDENCY_RESOLUTION_FAILED); + assert_eq!(me.message, "cannot resolve"); + } + + #[test] + fn test_bail_silent_has_empty_message() { + let err = bail_silent(GENERAL_ERROR); + let me = err.downcast_ref::<MozartError>().expect("should downcast"); + assert_eq!(me.exit_code, GENERAL_ERROR); + assert!(me.message.is_empty()); + } + + #[test] + fn test_mozart_error_is_std_error() { + let err: Box<dyn std::error::Error> = Box::new(MozartError { + message: "test".to_string(), + exit_code: 1, + }); + assert_eq!(err.to_string(), "test"); + } +} |
