1use std::io::IsTerminal;
4
5pub 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"; #[inline]
17pub fn is_terminal() -> bool {
18 std::io::stdout().is_terminal()
19}
20
21pub 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
28pub 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
35pub 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
42pub 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
49pub 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
57pub 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
64pub 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
73pub fn exit_usage(msg: &str) -> ! {
75 error(msg);
76 std::process::exit(EXIT_USAGE);
77}
78
79pub fn exit_file_not_found(path: &str) -> ! {
81 error(&format!("File not found: {}", path));
82 std::process::exit(EXIT_FILE_NOT_FOUND);
83}
84
85pub fn exit_database(msg: &str) -> ! {
87 error(&format!("Database error: {}", msg));
88 std::process::exit(EXIT_DATABASE);
89}
90
91pub 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
104pub 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#[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#[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 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 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 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 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}