use std::io::IsTerminal;
pub const RED: &str = "\x1b[0;31m";
pub const GREEN: &str = "\x1b[0;32m";
pub const YELLOW: &str = "\x1b[1;33m";
pub const BLUE: &str = "\x1b[0;34m";
pub const CYAN: &str = "\x1b[0;36m";
pub const MAGENTA: &str = "\x1b[0;35m";
pub const BOLD: &str = "\x1b[1m";
pub const NC: &str = "\x1b[0m";
#[inline]
pub fn is_terminal() -> bool {
std::io::stdout().is_terminal()
}
pub fn info(msg: &str) {
let color = if is_terminal() { GREEN } else { "" };
let reset = if is_terminal() { NC } else { "" };
println!("{}[INFO]{} {}", color, reset, msg);
}
pub fn warn(msg: &str) {
let color = if is_terminal() { YELLOW } else { "" };
let reset = if is_terminal() { NC } else { "" };
eprintln!("{}[WARN]{} {}", color, reset, msg);
}
pub fn error(msg: &str) {
let color = if is_terminal() { RED } else { "" };
let reset = if is_terminal() { NC } else { "" };
eprintln!("{}[ERROR]{} {}", color, reset, msg);
}
pub fn success(msg: &str) {
let color = if is_terminal() { MAGENTA } else { "" };
let reset = if is_terminal() { NC } else { "" };
println!("{}[OK]{} {}", color, reset, msg);
}
pub fn header(msg: &str) {
let bold = if is_terminal() { BOLD } else { "" };
let reset = if is_terminal() { NC } else { "" };
println!("{}===>{} {}", bold, reset, msg);
println!();
}
pub fn cmd(cmd: &str) {
let color = if is_terminal() { CYAN } else { "" };
let reset = if is_terminal() { NC } else { "" };
eprintln!("{}[CMD]{} {}", color, reset, cmd);
}
pub const EXIT_SUCCESS: i32 = 0;
pub const EXIT_ERROR: i32 = 1;
pub const EXIT_USAGE: i32 = 2;
pub const EXIT_DATABASE: i32 = 3;
pub const EXIT_FILE_NOT_FOUND: i32 = 4;
pub const EXIT_VALIDATION: i32 = 5;
pub const EXIT_NOT_FOUND: i32 = 6;
pub fn exit_usage(msg: &str) -> ! {
error(msg);
std::process::exit(EXIT_USAGE);
}
pub fn exit_file_not_found(path: &str) -> ! {
error(&format!("File not found: {}", path));
std::process::exit(EXIT_FILE_NOT_FOUND);
}
pub fn exit_database(msg: &str) -> ! {
error(&format!("Database error: {}", msg));
std::process::exit(EXIT_DATABASE);
}
pub const E_DATABASE_NOT_FOUND: &str = "E001";
pub const E_FUNCTION_NOT_FOUND: &str = "E002";
pub const E_BLOCK_NOT_FOUND: &str = "E003";
pub const E_PATH_NOT_FOUND: &str = "E004";
pub const E_PATH_EXPLOSION: &str = "E005";
pub const E_INVALID_INPUT: &str = "E006";
pub const E_CFG_ERROR: &str = "E007";
pub const R_HINT_INDEX: &str = "Run 'magellan watch' to create the database";
pub const R_HINT_LIST_FUNCTIONS: &str = "Run 'magellan find <function_name>' to find the function (or 'magellan status' to see indexed symbols)";
pub const R_HINT_MAX_LENGTH: &str = "Use --max-length N to bound path exploration";
pub const R_HINT_VERIFY_PATH: &str = "Use 'mirage paths --function <name>' to see available paths";
#[derive(Debug, Clone, serde::Serialize)]
pub struct JsonResponse<T> {
pub schema_version: String,
pub execution_id: String,
pub tool: String,
pub timestamp: String,
pub data: T,
}
impl<T: serde::Serialize> JsonResponse<T> {
pub fn new(data: T) -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = chrono::Utc::now().to_rfc3339();
let exec_id = format!(
"{:x}-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
std::process::id()
);
JsonResponse {
schema_version: "1.0.1".to_string(),
execution_id: exec_id,
tool: "mirage".to_string(),
timestamp,
data,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
pub fn to_pretty_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct JsonError {
pub error: String,
pub message: String,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
}
impl JsonError {
pub fn new(category: &str, message: &str, code: &str) -> Self {
JsonError {
error: category.to_string(),
message: message.to_string(),
code: code.to_string(),
remediation: None,
}
}
pub fn with_remediation(mut self, remediation: &str) -> Self {
self.remediation = Some(remediation.to_string());
self
}
pub fn database_not_found(path: &str) -> Self {
Self::new(
"DatabaseNotFound",
&format!("Database not found: {}", path),
E_DATABASE_NOT_FOUND,
)
.with_remediation(R_HINT_INDEX)
}
pub fn function_not_found(name: &str) -> Self {
Self::new(
"FunctionNotFound",
&format!("Function '{}' not found in database", name),
E_FUNCTION_NOT_FOUND,
)
.with_remediation(R_HINT_LIST_FUNCTIONS)
}
pub fn block_not_found(id: usize) -> Self {
Self::new(
"BlockNotFound",
&format!("Block {} not found in CFG", id),
E_BLOCK_NOT_FOUND,
)
}
pub fn path_not_found(id: &str) -> Self {
Self::new(
"PathNotFound",
&format!("Path '{}' not found or no longer valid", id),
E_PATH_NOT_FOUND,
)
.with_remediation("Run 'mirage verify --path-id ID' to check path validity")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_response() {
let data = vec!["item1", "item2"];
let response = JsonResponse::new(data);
let json = response.to_json();
assert!(json.contains("\"tool\":\"mirage\""));
assert!(json.contains("\"data\":[\"item1\",\"item2\"]"));
}
}