Skip to main content

mirage/output/
mod.rs

1// Output formatting utilities following Magellan's patterns
2
3use std::io::IsTerminal;
4
5// Colors for terminal output (when supported)
6pub const RED: &str = "\x1b[0;31m";
7pub const GREEN: &str = "\x1b[0;32m";
8pub const YELLOW: &str = "\x1b[1;33m";
9pub const BLUE: &str = "\x1b[0;34m";
10pub const CYAN: &str = "\x1b[0;36m";
11pub const MAGENTA: &str = "\x1b[0;35m";
12pub const BOLD: &str = "\x1b[1m";
13pub const NC: &str = "\x1b[0m"; // No Color
14
15/// Check if stdout is a terminal (for color output)
16#[inline]
17pub fn is_terminal() -> bool {
18    std::io::stdout().is_terminal()
19}
20
21/// Print info message
22pub fn info(msg: &str) {
23    let color = if is_terminal() { GREEN } else { "" };
24    let reset = if is_terminal() { NC } else { "" };
25    println!("{}[INFO]{} {}", color, reset, msg);
26}
27
28/// Print warning message
29pub fn warn(msg: &str) {
30    let color = if is_terminal() { YELLOW } else { "" };
31    let reset = if is_terminal() { NC } else { "" };
32    eprintln!("{}[WARN]{} {}", color, reset, msg);
33}
34
35/// Print error message
36pub fn error(msg: &str) {
37    let color = if is_terminal() { RED } else { "" };
38    let reset = if is_terminal() { NC } else { "" };
39    eprintln!("{}[ERROR]{} {}", color, reset, msg);
40}
41
42/// Print success message
43pub fn success(msg: &str) {
44    let color = if is_terminal() { MAGENTA } else { "" };
45    let reset = if is_terminal() { NC } else { "" };
46    println!("{}[OK]{} {}", color, reset, msg);
47}
48
49/// Print section header
50pub fn header(msg: &str) {
51    let bold = if is_terminal() { BOLD } else { "" };
52    let reset = if is_terminal() { NC } else { "" };
53    println!("{}===>{} {}", bold, reset, msg);
54    println!();
55}
56
57/// Print command being executed
58pub fn cmd(cmd: &str) {
59    let color = if is_terminal() { CYAN } else { "" };
60    let reset = if is_terminal() { NC } else { "" };
61    eprintln!("{}[CMD]{} {}", color, reset, cmd);
62}
63
64/// Exit codes (matching Magellan's conventions)
65pub const EXIT_SUCCESS: i32 = 0;
66pub const EXIT_ERROR: i32 = 1;
67pub const EXIT_USAGE: i32 = 2;
68pub const EXIT_DATABASE: i32 = 3;
69pub const EXIT_FILE_NOT_FOUND: i32 = 4;
70pub const EXIT_VALIDATION: i32 = 5;
71pub const EXIT_NOT_FOUND: i32 = 6;
72
73/// Exit with usage error
74pub fn exit_usage(msg: &str) -> ! {
75    error(msg);
76    std::process::exit(EXIT_USAGE);
77}
78
79/// Exit with file not found error
80pub fn exit_file_not_found(path: &str) -> ! {
81    error(&format!("File not found: {}", path));
82    std::process::exit(EXIT_FILE_NOT_FOUND);
83}
84
85/// Exit with database error
86pub fn exit_database(msg: &str) -> ! {
87    error(&format!("Database error: {}", msg));
88    std::process::exit(EXIT_DATABASE);
89}
90
91// ============================================================================
92// Error Codes and Remediation
93// ============================================================================
94
95/// Error codes for JSON error responses
96pub const E_DATABASE_NOT_FOUND: &str = "E001";
97pub const E_FUNCTION_NOT_FOUND: &str = "E002";
98pub const E_BLOCK_NOT_FOUND: &str = "E003";
99pub const E_PATH_NOT_FOUND: &str = "E004";
100pub const E_PATH_EXPLOSION: &str = "E005";
101pub const E_INVALID_INPUT: &str = "E006";
102pub const E_CFG_ERROR: &str = "E007";
103
104/// Common remediation messages
105pub const R_HINT_INDEX: &str = "Run 'magellan watch' to create the database";
106pub const R_HINT_LIST_FUNCTIONS: &str = "Run 'magellan find <function_name>' to find the function (or 'magellan status' to see indexed symbols)";
107pub const R_HINT_MAX_LENGTH: &str = "Use --max-length N to bound path exploration";
108pub const R_HINT_VERIFY_PATH: &str = "Use 'mirage paths --function <name>' to see available paths";
109
110/// JSON output wrapper (following Magellan's response format)
111#[derive(Debug, Clone, serde::Serialize)]
112pub struct JsonResponse<T> {
113    pub schema_version: String,
114    pub execution_id: String,
115    pub tool: String,
116    pub timestamp: String,
117    pub data: T,
118}
119
120impl<T: serde::Serialize> JsonResponse<T> {
121    pub fn new(data: T) -> Self {
122        use std::time::{SystemTime, UNIX_EPOCH};
123
124        let timestamp = chrono::Utc::now().to_rfc3339();
125        let exec_id = format!(
126            "{:x}-{}",
127            SystemTime::now()
128                .duration_since(UNIX_EPOCH)
129                .unwrap()
130                .as_secs(),
131            std::process::id()
132        );
133
134        JsonResponse {
135            schema_version: "1.0.1".to_string(),
136            execution_id: exec_id,
137            tool: "mirage".to_string(),
138            timestamp,
139            data,
140        }
141    }
142
143    pub fn to_json(&self) -> String {
144        serde_json::to_string(self).unwrap_or_default()
145    }
146
147    pub fn to_pretty_json(&self) -> String {
148        serde_json::to_string_pretty(self).unwrap_or_default()
149    }
150}
151
152/// Error response format for JSON mode
153#[derive(Debug, Clone, serde::Serialize)]
154pub struct JsonError {
155    pub error: String,
156    pub message: String,
157    pub code: String,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub remediation: Option<String>,
160}
161
162impl JsonError {
163    pub fn new(category: &str, message: &str, code: &str) -> Self {
164        JsonError {
165            error: category.to_string(),
166            message: message.to_string(),
167            code: code.to_string(),
168            remediation: None,
169        }
170    }
171
172    pub fn with_remediation(mut self, remediation: &str) -> Self {
173        self.remediation = Some(remediation.to_string());
174        self
175    }
176
177    /// Database not found error with remediation
178    pub fn database_not_found(path: &str) -> Self {
179        Self::new(
180            "DatabaseNotFound",
181            &format!("Database not found: {}", path),
182            E_DATABASE_NOT_FOUND,
183        )
184        .with_remediation(R_HINT_INDEX)
185    }
186
187    /// Function not found error with remediation
188    pub fn function_not_found(name: &str) -> Self {
189        Self::new(
190            "FunctionNotFound",
191            &format!("Function '{}' not found in database", name),
192            E_FUNCTION_NOT_FOUND,
193        )
194        .with_remediation(R_HINT_LIST_FUNCTIONS)
195    }
196
197    /// Block not found error
198    pub fn block_not_found(id: usize) -> Self {
199        Self::new(
200            "BlockNotFound",
201            &format!("Block {} not found in CFG", id),
202            E_BLOCK_NOT_FOUND,
203        )
204    }
205
206    /// Path not found error
207    pub fn path_not_found(id: &str) -> Self {
208        Self::new(
209            "PathNotFound",
210            &format!("Path '{}' not found or no longer valid", id),
211            E_PATH_NOT_FOUND,
212        )
213        .with_remediation("Run 'mirage verify --path-id ID' to check path validity")
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_json_response() {
223        let data = vec!["item1", "item2"];
224        let response = JsonResponse::new(data);
225        let json = response.to_json();
226        assert!(json.contains("\"tool\":\"mirage\""));
227        assert!(json.contains("\"data\":[\"item1\",\"item2\"]"));
228    }
229}