#![warn(clippy::unwrap_used)]
mod cli;
mod collections;
mod commands;
mod core;
mod http;
mod models;
mod utils;
use anyhow::{Context, Result};
use clap::Parser;
use colored::*;
use std::env;
use std::path::PathBuf;
use std::process;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use core::api::ApiError;
const EXIT_SUCCESS: i32 = 0;
const EXIT_UNKNOWN_ERROR: i32 = 1;
const EXIT_USAGE_ERROR: i32 = 2;
const EXIT_AUTH_ERROR: i32 = 3;
const EXIT_NETWORK_ERROR: i32 = 4;
const EXIT_RATE_LIMIT_ERROR: i32 = 5;
const EXIT_SERVER_ERROR: i32 = 6;
const EXIT_VALIDATION_ERROR: i32 = 7;
fn handle_simple_commands() -> Option<i32> {
let args: Vec<String> = env::args().collect();
if args.len() == 1 {
core::banner::display_banner();
return Some(EXIT_SUCCESS);
}
if args.len() == 2 && (args[1] == "--version" || args[1] == "-V") {
println!("mrapids {}", env!("CARGO_PKG_VERSION"));
return Some(EXIT_SUCCESS);
}
None }
fn main() {
if let Some(code) = handle_simple_commands() {
process::exit(code);
}
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")))
.with(
tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(std::io::stderr),
)
.init();
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let exit_code = rt.block_on(async {
match run().await {
Ok(code) => code,
Err(e) => {
eprintln!("{}: {}", "Error".bright_red(), e);
if let Some(api_err) = e.downcast_ref::<ApiError>() {
match api_err {
ApiError::AuthError(_) => EXIT_AUTH_ERROR,
ApiError::ValidationError(_) => EXIT_VALIDATION_ERROR,
ApiError::OperationNotFound(_) => EXIT_VALIDATION_ERROR,
ApiError::NetworkError(_) => EXIT_NETWORK_ERROR,
ApiError::TimeoutError(_) => EXIT_NETWORK_ERROR,
ApiError::ServerError(_) => EXIT_SERVER_ERROR,
ApiError::ClientError(_) => EXIT_VALIDATION_ERROR,
ApiError::PolicyDeny(_) => EXIT_AUTH_ERROR,
ApiError::PayloadTooLarge(_) => EXIT_VALIDATION_ERROR,
ApiError::InternalError(_) => EXIT_UNKNOWN_ERROR,
}
} else if let Some(network_err) = e.downcast_ref::<reqwest::Error>() {
if network_err.is_timeout() || network_err.is_connect() {
EXIT_NETWORK_ERROR
} else if network_err.status() == Some(reqwest::StatusCode::TOO_MANY_REQUESTS) {
EXIT_RATE_LIMIT_ERROR
} else if network_err.is_status() {
EXIT_SERVER_ERROR
} else {
EXIT_UNKNOWN_ERROR
}
} else {
EXIT_UNKNOWN_ERROR
}
}
}
});
process::exit(exit_code);
}
async fn run() -> Result<i32> {
let args_count = env::args().count();
if args_count == 1 {
core::banner::display_banner();
return Ok(EXIT_SUCCESS);
}
let args_vec: Vec<String> = env::args().collect();
if args_vec.len() == 2 && (args_vec[1] == "--version" || args_vec[1] == "-V") {
println!("mrapids {}", env!("CARGO_PKG_VERSION"));
return Ok(EXIT_SUCCESS);
}
let args = match cli::Args::try_parse() {
Ok(args) => args,
Err(e) => {
eprintln!("{}", e);
return Ok(EXIT_USAGE_ERROR);
}
};
let no_color = args.no_color
|| env::var("NO_COLOR").is_ok()
|| env::var("MRAPIDS_NO_COLOR")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
if no_color {
colored::control::set_override(false);
}
let machine_mode = args.machine
|| env::var("MRAPIDS_MACHINE")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
if machine_mode {
colored::control::set_override(false);
}
let json_mode = args.json
|| env::var("MRAPIDS_OUTPUT")
.map(|v| v.to_lowercase() == "json")
.unwrap_or(false);
if args.trace {
env::set_var("MRAPIDS_TRACE", "true");
} else if args.verbose {
env::set_var("MRAPIDS_VERBOSE", "true");
}
if let Some(output) = &args.output_format {
env::set_var("MRAPIDS_OUTPUT", output);
}
if args.quiet {
env::set_var("MRAPIDS_QUIET", "true");
}
if json_mode {
env::set_var("MRAPIDS_JSON", "true");
}
if machine_mode {
env::set_var("MRAPIDS_MACHINE", "true");
}
match args.command {
cli::Commands::Init(cmd) => {
core::init_command(cmd)?;
}
cli::Commands::Run(cmd) => {
if !cmd.json_output && !json_mode && !args.machine {
core::display_short_banner();
}
tokio::task::spawn_blocking(move || core::run_command(cmd))
.await
.map_err(|e| ApiError::InternalError(format!("Task join error: {}", e)))??;
}
cli::Commands::Test(cmd) => {
println!("{}", "🧪 Running tests...".bright_cyan());
core::test_command(cmd)?;
}
cli::Commands::SetupTests(cmd) => {
core::setup_tests_command(cmd)?;
}
cli::Commands::List(cmd) => {
core::list_command(cmd)?;
}
cli::Commands::Show(cmd) => {
core::show_command(cmd)?;
}
cli::Commands::Cleanup(cmd) => {
core::cleanup_command(cmd)?;
}
cli::Commands::Explore(cmd) => {
core::explore_command(cmd)?;
}
cli::Commands::Auth(cmd) => {
handle_auth_command(cmd).await?;
}
cli::Commands::Env(cmd) => {
handle_env_command(cmd)?;
}
cli::Commands::Flatten(cmd) => {
core::flatten_command(cmd).await?;
}
cli::Commands::Validate(cmd) => {
core::validate_command(cmd)?;
}
cli::Commands::Diff(cmd) => {
core::diff_command(cmd)?;
}
cli::Commands::Doctor(cmd) => {
core::doctor_command(cmd, args.env.clone())?;
}
cli::Commands::Gen(cmd) => {
handle_gen_command(cmd).await?;
}
cli::Commands::Collection(cmd) => {
handle_collection_command(cmd).await?;
}
cli::Commands::Db(cmd) => {
handle_db_command(cmd)?;
}
cli::Commands::Sql(cmd) => {
handle_sql_command(cmd)?;
}
cli::Commands::Compare(cmd) => {
handle_compare_command(cmd)?;
}
cli::Commands::History(cmd) => {
handle_history_command(cmd)?;
}
cli::Commands::Export(cmd) => {
handle_export_command(cmd)?;
}
cli::Commands::Index(cmd) => {
handle_index_command(cmd)?;
}
cli::Commands::Find(cmd) => {
handle_find_command(cmd)?;
}
cli::Commands::Plan(cmd) => {
handle_plan_command(cmd)?;
}
cli::Commands::Mcp(cmd) => {
if let cli::McpSubcommand::Http {
port,
bind,
api_key,
policy,
spec,
base_url,
allow_localhost,
debug,
} = &cmd.command
{
if let Some(url) = base_url {
std::env::set_var("API_BASE_URL", url);
}
http::server::start_http_server(
bind.clone(),
*port,
api_key.clone(),
policy.clone(),
spec.clone(),
*allow_localhost,
*debug,
)
.await?;
} else {
core::mcp::execute_mcp_command(cmd)?;
}
}
cli::Commands::Policy(cmd) => {
handle_policy_command(cmd)?;
}
}
Ok(EXIT_SUCCESS)
}
async fn handle_gen_command(cmd: cli::GenCommand) -> Result<()> {
use cli::GenTarget;
match cmd.target {
GenTarget::Snippets(snippets_cmd) => {
println!("{}", "📝 Generating snippets...".bright_cyan());
let analyze_cmd = cli::AnalyzeCommand {
spec: snippets_cmd.spec,
operation: snippets_cmd.operation,
output: snippets_cmd.output,
all: true,
skip_data: false,
skip_validate: false,
force: true,
cleanup_backups: true,
};
core::analyze::analyze_command(analyze_cmd)?;
}
GenTarget::Sdk(sdk_cmd) => {
println!("{}", "🔧 Generating SDK...".bright_cyan());
let spec_path = sdk_cmd
.spec
.unwrap_or_else(|| PathBuf::from("specs/api.yaml"));
let content = std::fs::read_to_string(&spec_path)?;
let spec = core::parser::parse_spec(&content)?;
let lang_str = match sdk_cmd.language {
cli::SdkLanguage::Typescript => "typescript",
cli::SdkLanguage::Python => "python",
cli::SdkLanguage::Go => "go",
cli::SdkLanguage::Rust => "rust",
};
let output_dir = sdk_cmd
.output
.unwrap_or_else(|| PathBuf::from(format!("./sdk-{}", lang_str)));
match sdk_cmd.language {
cli::SdkLanguage::Typescript => {
core::sdk_gen::typescript::generate_typescript_sdk(
&spec,
&output_dir,
sdk_cmd.package.as_deref(),
sdk_cmd.docs,
sdk_cmd.examples,
)?;
}
cli::SdkLanguage::Python => {
core::sdk_gen::python::generate_python_sdk(
&spec,
&output_dir,
sdk_cmd.package.as_deref(),
sdk_cmd.docs,
sdk_cmd.examples,
)?;
}
cli::SdkLanguage::Go => {
core::sdk_gen::go::generate_go_sdk(
&spec,
&output_dir,
sdk_cmd.package.as_deref(),
sdk_cmd.docs,
sdk_cmd.examples,
)?;
}
cli::SdkLanguage::Rust => {
core::sdk_gen::rust_gen::generate_rust_sdk(
&spec,
&output_dir,
sdk_cmd.package.as_deref(),
sdk_cmd.docs,
sdk_cmd.examples,
)?;
}
}
println!("✅ SDK generated successfully in: {}", output_dir.display());
}
GenTarget::Stubs(stubs_cmd) => {
let spec_path = stubs_cmd
.spec
.unwrap_or_else(|| PathBuf::from("specs/api.yaml"));
let content = std::fs::read_to_string(&spec_path)?;
let spec = core::parser::parse_spec(&content)?;
let output_dir = stubs_cmd
.output
.unwrap_or_else(|| PathBuf::from("./generated"));
core::stubs::generate_server_stubs(
&spec,
&stubs_cmd.framework,
&output_dir,
stubs_cmd.with_tests,
stubs_cmd.with_validation,
)?;
}
GenTarget::Fixtures(fixtures_cmd) => {
let spec_path = fixtures_cmd
.spec
.unwrap_or_else(|| PathBuf::from("specs/api.yaml"));
let content = std::fs::read_to_string(&spec_path)?;
let spec = core::parser::parse_spec(&content)?;
let format = match fixtures_cmd.format {
cli::FixtureFormat::Json => core::fixtures::FixtureFormat::Json,
cli::FixtureFormat::Yaml => core::fixtures::FixtureFormat::Yaml,
cli::FixtureFormat::Csv => core::fixtures::FixtureFormat::Csv,
};
let variant = core::fixtures::FixtureVariant::Valid;
core::fixtures::generate_fixtures(
&spec,
&fixtures_cmd.output,
fixtures_cmd.count,
fixtures_cmd.schema,
fixtures_cmd.seed,
format,
variant,
)?;
}
}
Ok(())
}
fn handle_env_command(cmd: cli::EnvCommand) -> Result<()> {
use crate::core::config::ConfigLoader;
use cli::EnvCommands;
use colored::*;
use std::fs;
use std::path::Path;
match cmd.command {
EnvCommands::List { verbose } => {
let environments = ConfigLoader::list_environments();
if environments.is_empty() {
println!("{}", "No environment configurations found.".yellow());
println!("\nTo create an environment, run:");
println!(" mrapids env create <name> --base-url <url>");
return Ok(());
}
let manifest_path = Path::new("mrapids.yaml");
let default_env = if manifest_path.exists() {
let content = fs::read_to_string(manifest_path)?;
let manifest: serde_yaml::Value = serde_yaml::from_str(&content)?;
manifest
.get("default_env")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
} else {
None
};
println!("{}", "Available environments:".bold());
println!();
for env in &environments {
let is_default = default_env.as_ref() == Some(env);
let config_path = format!("config/{}.yaml", env);
let env_file_path = format!("env/.env.{}", env);
if verbose {
let config_exists = Path::new(&config_path).exists();
let env_exists = Path::new(&env_file_path).exists();
println!(
" {} {}",
if is_default { "→" } else { " " },
if is_default {
format!("{} (default)", env).bright_cyan().bold()
} else {
env.normal()
}
);
println!(
" Config: {} {}",
config_path,
if config_exists {
"✓".green()
} else {
"✗".red()
}
);
println!(
" Env file: {} {}",
env_file_path,
if env_exists {
"✓".green()
} else {
"✗ (optional)".yellow()
}
);
println!();
} else {
println!(
" {} {}",
if is_default { "•" } else { "-" },
if is_default {
format!("{} (default)", env).bright_cyan().bold()
} else {
env.normal()
}
);
}
}
if !verbose {
println!("\nUse --verbose to see config file paths");
}
}
EnvCommands::Show { environment, full } => {
if !ConfigLoader::environment_exists(&environment) {
let _ = ConfigLoader::validate_environment(&environment, None);
return Ok(());
}
println!("{} {}", "Environment:".bold(), environment.bright_cyan());
println!();
let config_path = format!("config/{}.yaml", environment);
let env_file_path = format!("env/.env.{}", environment);
println!("{}:", "Configuration file".bold());
println!(" Path: {}", config_path);
if full && Path::new(&config_path).exists() {
let content = fs::read_to_string(&config_path)?;
println!(" Content:");
for line in content.lines() {
println!(" {}", line);
}
}
println!("\n{}:", "Environment variables file".bold());
println!(" Path: {}", env_file_path);
if Path::new(&env_file_path).exists() {
println!(" Status: {} Found", "✓".green());
if full {
let content = fs::read_to_string(&env_file_path)?;
let var_count = content
.lines()
.filter(|l| !l.trim().is_empty() && !l.trim().starts_with('#'))
.count();
println!(" Variables: {} defined", var_count);
}
} else {
println!(" Status: {} Not found (optional)", "○".yellow());
}
}
EnvCommands::Create {
name,
from,
base_url,
} => {
let config_dir = Path::new("config");
if !config_dir.exists() {
fs::create_dir_all(config_dir)?;
}
let config_path = config_dir.join(format!("{}.yaml", name));
if config_path.exists() {
eprintln!("{} Environment '{}' already exists", "Error:".red(), name);
return Ok(());
}
let content = if let Some(source) = from {
let source_path = config_dir.join(format!("{}.yaml", source));
if !source_path.exists() {
eprintln!(
"{} Source environment '{}' not found",
"Error:".red(),
source
);
return Ok(());
}
fs::read_to_string(source_path)?
} else {
let url = base_url.unwrap_or_else(|| "https://api.example.com".to_string());
format!(
"base_url: {}\ntimeout_ms: 30000\nheaders:\n X-Environment: {}\n",
url, name
)
};
fs::write(&config_path, content)?;
println!(
"{} Created environment configuration: {}",
"✓".green(),
config_path.display()
);
let env_dir = Path::new("env");
if !env_dir.exists() {
fs::create_dir_all(env_dir)?;
}
let env_file = env_dir.join(format!(".env.{}", name));
if !env_file.exists() {
fs::write(&env_file, format!("# Environment variables for {}\n", name))?;
println!(
"{} Created environment file: {}",
"✓".green(),
env_file.display()
);
}
println!("\nTo use this environment:");
println!(" mrapids run <operation> --env {}", name);
}
EnvCommands::Validate { environment } => {
if let Some(env) = environment {
match ConfigLoader::validate_environment(&env, None) {
Ok(_) => {
println!("{} Environment '{}' is valid", "✓".green(), env);
match ConfigLoader::load(Some(&env)) {
Ok(_) => println!("{} Configuration loads successfully", "✓".green()),
Err(e) => println!("{} Configuration error: {}", "✗".red(), e),
}
}
Err(e) => {
eprintln!("{}", e);
}
}
} else {
let environments = ConfigLoader::list_environments();
if environments.is_empty() {
println!("{}", "No environments found to validate".yellow());
return Ok(());
}
println!("{}", "Validating all environments:".bold());
println!();
let mut all_valid = true;
for env in environments {
print!(" {} ... ", env);
match ConfigLoader::load(Some(&env)) {
Ok(_) => println!("{}", "✓".green()),
Err(e) => {
println!("{} - {}", "✗".red(), e.to_string().red());
all_valid = false;
}
}
}
println!();
if all_valid {
println!("{} All environments are valid", "✓".green().bold());
} else {
println!("{} Some environments have errors", "⚠".yellow().bold());
}
}
}
}
Ok(())
}
async fn handle_auth_command(cmd: cli::AuthCommand) -> Result<()> {
use crate::core::auth::{
delete_profile, list_profiles, load_tokens, oauth_login, refresh_tokens, test_auth_profile,
};
use cli::{AuthCommands, DetectOutputFormat};
match cmd.command {
AuthCommands::Detect {
spec,
format,
operations,
summary_only,
} => {
let detect_cmd = crate::commands::auth::detect::DetectCommand {
spec,
format: match format {
DetectOutputFormat::Table => crate::commands::auth::detect::OutputFormat::Table,
DetectOutputFormat::Json => crate::commands::auth::detect::OutputFormat::Json,
DetectOutputFormat::Yaml => crate::commands::auth::detect::OutputFormat::Yaml,
},
operations,
summary_only,
};
detect_cmd.execute().await?;
}
AuthCommands::Connect {
scheme,
auth_type,
api_key,
token,
username,
password,
flow: _,
client_id,
client_secret,
scopes,
non_interactive,
force,
env,
} => {
let cmd_auth_type = auth_type.map(|t| match t.as_str() {
"api-key" => crate::commands::auth::connect::AuthType::ApiKey,
"bearer" => crate::commands::auth::connect::AuthType::Bearer,
"basic" => crate::commands::auth::connect::AuthType::Basic,
"oauth2" => crate::commands::auth::connect::AuthType::OAuth2,
"oidc" => crate::commands::auth::connect::AuthType::OpenIdConnect,
"mtls" => crate::commands::auth::connect::AuthType::MutualTls,
_ => crate::commands::auth::connect::AuthType::Bearer,
});
let connect_cmd = crate::commands::auth::connect::ConnectCommand {
scheme: Some(scheme),
auth_type: cmd_auth_type,
non_interactive,
discover: false,
force,
keychain: false,
spec: None,
env: Some(env),
};
if let Some(key) = api_key {
std::env::set_var("API_KEY", key);
}
if let Some(t) = token {
std::env::set_var("BEARER_TOKEN", t);
}
if let Some(u) = username {
std::env::set_var("BASIC_USERNAME", u);
}
if let Some(p) = password {
std::env::set_var("BASIC_PASSWORD", p);
}
if let Some(cid) = client_id {
std::env::set_var("CLIENT_ID", cid);
}
if let Some(cs) = client_secret {
std::env::set_var("CLIENT_SECRET", cs);
}
if let Some(s) = scopes {
std::env::set_var("OAUTH_SCOPES", s);
}
connect_cmd.execute().await?;
}
AuthCommands::Validate {
scheme,
spec,
endpoint,
verbose,
debug,
quick,
} => {
let validate_cmd = crate::commands::auth::validate::ValidateCommand {
scheme,
spec,
endpoint,
verbose,
debug,
quick,
};
validate_cmd.execute().await?;
}
AuthCommands::Login {
provider,
client_id,
client_secret,
auth_url,
token_url,
scopes,
profile,
setup_help,
} => {
if setup_help {
println!(
"{}",
crate::core::auth::providers::get_provider_help(&provider)
);
return Ok(());
}
let profile_name = profile.unwrap_or_else(|| provider.clone());
if crate::core::auth::token_store::profile_exists(&profile_name) {
println!("⚠️ Profile '{}' already exists. Use 'mrapids auth logout {}' first to remove it.",
profile_name.bright_yellow(), profile_name);
return Ok(());
}
let config = if provider.to_lowercase() == "custom" {
if client_id.is_none() || auth_url.is_none() || token_url.is_none() {
return Err(ApiError::ValidationError(
"Custom provider requires --client-id, --auth-url, and --token-url"
.to_string(),
)
.into());
}
crate::core::auth::providers::create_custom_config(
&profile_name,
client_id.unwrap(),
client_secret,
auth_url.unwrap(),
token_url.unwrap(),
if scopes.is_empty() {
vec!["read".to_string()]
} else {
scopes
},
)
} else {
let mut config = crate::core::auth::providers::get_provider_config(&provider)?;
if let Some(id) = client_id {
config.client_id = id;
}
if let Some(secret) = client_secret {
config.client_secret = Some(secret);
}
if !scopes.is_empty() {
config.scopes = scopes;
}
if config.client_id.starts_with("YOUR_") {
println!("⚠️ {} OAuth setup required:", provider.bright_yellow());
println!(
"{}",
crate::core::auth::providers::get_provider_help(&provider)
);
return Ok(());
}
config
};
oauth_login(config, profile_name).await?;
}
AuthCommands::List { detailed } => {
let profiles = list_profiles()?;
if profiles.is_empty() {
println!(
"No auth profiles found. Use 'mrapids auth login <provider>' to create one."
);
return Ok(());
}
if detailed {
use prettytable::{row, Table};
let mut table = Table::new();
table.add_row(row!["Profile", "Provider", "Created", "Last Used"]);
for profile in profiles {
let last_used = profile
.last_used
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "Never".to_string());
table.add_row(row![
profile.name.bright_green(),
profile.provider.bright_cyan(),
profile.created_at.format("%Y-%m-%d %H:%M"),
last_used
]);
}
table.printstd();
} else {
println!("🔐 Auth Profiles:\n");
for profile in profiles {
println!(
" • {} ({})",
profile.name.bright_green(),
profile.provider.bright_cyan()
);
}
println!("\nUse 'mrapids auth list --detailed' for more information.");
}
}
AuthCommands::Show {
profile,
show_tokens,
} => {
let auth_profile = crate::core::auth::token_store::load_profile(&profile)?;
let provider_config = crate::core::auth::providers::load_provider_config(&profile)?;
println!("🔐 Auth Profile: {}\n", profile.bright_green());
println!(" Provider: {}", auth_profile.provider.bright_cyan());
println!(
" Created: {}",
auth_profile.created_at.format("%Y-%m-%d %H:%M")
);
if let Some(last_used) = auth_profile.last_used {
println!(" Last Used: {}", last_used.format("%Y-%m-%d %H:%M"));
}
println!(" Scopes: {}", provider_config.scopes.join(", "));
if show_tokens {
println!("\n⚠️ {}:", "Token Information (SENSITIVE)".bright_red());
let tokens = load_tokens(&profile)?;
println!(" Token Type: {}", tokens.token_type);
println!(
" Access Token: {}...{}",
&tokens.access_token[..10.min(tokens.access_token.len())],
&tokens.access_token[tokens.access_token.len().saturating_sub(10)..]
);
if let Some(expires_at) = tokens.expires_at {
let remaining = expires_at.signed_duration_since(chrono::Utc::now());
if remaining.num_seconds() > 0 {
println!(" Expires In: {} minutes", remaining.num_minutes());
} else {
println!(" Status: {} (refresh required)", "EXPIRED".bright_red());
}
}
println!(
" Has Refresh Token: {}",
if tokens.refresh_token.is_some() {
"Yes"
} else {
"No"
}
);
}
}
AuthCommands::Refresh { profile } => {
refresh_tokens(&profile).await?;
}
AuthCommands::Logout { profile, force } => {
if !force {
println!(
"Are you sure you want to remove auth profile '{}'? [y/N] ",
profile.bright_yellow()
);
use std::io::{self, BufRead};
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
if let Some(Ok(line)) = lines.next() {
if !line.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
}
delete_profile(&profile)?;
println!(
"✅ Auth profile '{}' removed successfully.",
profile.bright_green()
);
}
AuthCommands::Test { profile } => {
test_auth_profile(&profile).await?;
}
AuthCommands::Setup { provider } => {
println!(
"{}",
crate::core::auth::providers::get_provider_help(&provider)
);
}
}
Ok(())
}
fn handle_db_command(cmd: cli::DbCommand) -> Result<()> {
use crate::core::analytics_engine::AnalyticsEngine;
use crate::core::output::{is_json_mode, ResponseEnvelope};
use cli::DbSubcommand;
use colored::*;
let json_mode = is_json_mode();
match cmd.command {
DbSubcommand::Status { verbose } => {
let engine = AnalyticsEngine::open()?;
let status = engine.get_status()?;
if json_mode {
let mut data = serde_json::json!({
"database": {
"path": status.path.display().to_string(),
"exists": status.exists,
"size_bytes": status.size_bytes,
"size_human": status.size_human,
"last_modified": status.last_modified_human
},
"schema": {
"table_count": status.table_count
},
"engine_version": status.engine_version
});
if verbose {
let validation = engine.validate().unwrap_or(false);
let stats = engine.get_request_stats().ok();
data["validation"] = serde_json::json!({
"healthy": validation
});
if let Some(s) = stats {
data["statistics"] = s;
}
}
let envelope = ResponseEnvelope::success("db status", data);
println!("{}", envelope.to_json());
} else {
println!("{} SQLite Analytics Engine", "📊".bright_cyan());
println!();
println!("{}:", "Database Information".bold());
println!(
" Path: {}",
status.path.display().to_string().bright_yellow()
);
println!(
" Exists: {}",
if status.exists {
"Yes".green()
} else {
"No".red()
}
);
println!(" Size: {}", status.size_human.bright_cyan());
println!(" Last Modified: {}", status.last_modified_human);
println!();
println!("{}:", "Schema".bold());
println!(" Tables: {}", status.table_count);
println!();
println!("{}:", "SQLite".bold());
println!(" Engine: {}", status.engine_version.bright_green());
if verbose {
println!();
println!("{}:", "Validation".bold());
match engine.validate() {
Ok(true) => println!(" Status: {} Database is healthy", "✓".green()),
Ok(false) => println!(" Status: {} Validation failed", "✗".red()),
Err(e) => println!(" Status: {} Error: {}", "✗".red(), e),
}
println!();
println!("{}:", "Request Statistics".bold());
match engine.get_request_stats() {
Ok(stats) => {
if let Some(arr) = stats.as_array() {
if let Some(row) = arr.first() {
let total = row
.get("total_requests")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let success =
row.get("successful").and_then(|v| v.as_i64()).unwrap_or(0);
let failed =
row.get("failed").and_then(|v| v.as_i64()).unwrap_or(0);
let avg_time = row
.get("avg_response_time_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
println!(
" Total Requests: {}",
total.to_string().bright_cyan()
);
println!(" Successful: {}", success.to_string().green());
println!(" Failed: {}", failed.to_string().red());
if total > 0 {
println!(" Avg Response Time: {:.2} ms", avg_time);
}
}
}
}
Err(e) => println!(" Error loading stats: {}", e),
}
}
println!();
println!(
"{}",
"Use 'mrapids db schema' to view table structure".dimmed()
);
}
}
DbSubcommand::Schema { table, format } => {
let engine = AnalyticsEngine::open()?;
let schema = engine.get_schema()?;
let status = engine.get_status()?;
let tables: Vec<_> = if let Some(ref table_name) = table {
schema
.into_iter()
.filter(|t| t.name == *table_name)
.collect()
} else {
schema
};
if tables.is_empty() {
if let Some(table_name) = table {
println!("{} Table '{}' not found", "⚠️".yellow(), table_name);
} else {
println!("{}", "No tables found".yellow());
}
return Ok(());
}
match format {
cli::DbSchemaFormat::Json => {
let json_output: Vec<serde_json::Value> = tables
.iter()
.map(|t| {
serde_json::json!({
"table": t.name,
"columns": t.columns.iter().map(|c| {
serde_json::json!({
"name": c.name,
"type": c.data_type,
"nullable": c.nullable,
"default": c.default_value
})
}).collect::<Vec<_>>()
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"table_count": status.table_count,
"tables": json_output
}))?
);
}
cli::DbSchemaFormat::Sql => {
println!("-- mrapids database schema");
println!();
for table_info in &tables {
println!("CREATE TABLE {} (", table_info.name);
for (i, col) in table_info.columns.iter().enumerate() {
let nullable = if col.nullable { "" } else { " NOT NULL" };
let default = col
.default_value
.as_ref()
.map(|d| format!(" DEFAULT {}", d))
.unwrap_or_default();
let comma = if i < table_info.columns.len() - 1 {
","
} else {
""
};
println!(
" {} {}{}{}{}",
col.name, col.data_type, nullable, default, comma
);
}
println!(");");
println!();
}
}
cli::DbSchemaFormat::Table => {
println!("{} Database Schema", "📋".bright_cyan());
println!();
for table_info in &tables {
println!("{}:", table_info.name.bright_green().bold());
use prettytable::{row, Table};
let mut table = Table::new();
table.add_row(row!["Column", "Type", "Nullable", "Default"]);
for col in &table_info.columns {
let nullable_display = if col.nullable { "YES" } else { "NO" };
let default_display = col.default_value.as_deref().unwrap_or("-");
table.add_row(row![
col.name.bright_yellow(),
col.data_type,
nullable_display,
default_display
]);
}
table.printstd();
println!();
}
}
}
}
DbSubcommand::Query { sql, format } => {
let engine = AnalyticsEngine::open()?;
let results = engine.query_json(&sql)?;
match format {
cli::DbOutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&results)?);
}
cli::DbOutputFormat::Table => {
if let Some(arr) = results.as_array() {
if arr.is_empty() {
println!("{}", "No results".yellow());
} else {
if let Some(first) = arr.first() {
if let Some(obj) = first.as_object() {
use prettytable::{Cell, Row, Table};
let mut table = Table::new();
let headers: Vec<Cell> =
obj.keys().map(|k| Cell::new(k)).collect();
table.add_row(Row::new(headers));
for row in arr {
if let Some(obj) = row.as_object() {
let cells: Vec<Cell> = obj
.values()
.map(|v| Cell::new(&format!("{}", v)))
.collect();
table.add_row(Row::new(cells));
}
}
table.printstd();
}
}
}
}
}
}
}
DbSubcommand::Stats {
spec,
operation,
range: _,
} => {
let engine = AnalyticsEngine::open()?;
let mut conditions = vec![];
if let Some(ref s) = spec {
conditions.push(format!("spec_file = '{}'", s));
}
if let Some(ref op) = operation {
conditions.push(format!("operation_id = '{}'", op));
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
};
let sql = format!(
r#"
SELECT
operation_id,
COUNT(*) as total,
SUM(CASE WHEN success THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) as failed,
ROUND(AVG(response_time_ms), 2) as avg_ms,
ROUND(MIN(response_time_ms), 2) as min_ms,
ROUND(MAX(response_time_ms), 2) as max_ms
FROM api_requests{}
GROUP BY operation_id
ORDER BY total DESC
LIMIT 20
"#,
where_clause
);
let results = engine.query_json(&sql)?;
println!("{} API Request Statistics", "📊".bright_cyan());
println!();
if let Some(arr) = results.as_array() {
if arr.is_empty() {
println!("{}", "No request data recorded yet.".yellow());
println!("Run API requests with mrapids to start collecting analytics.");
} else {
use prettytable::{row, Table};
let mut table = Table::new();
table.add_row(row![
"Operation",
"Total",
"Success",
"Failed",
"Avg (ms)",
"Min (ms)",
"Max (ms)"
]);
for row_data in arr {
let op = row_data
.get("operation_id")
.and_then(|v| v.as_str())
.unwrap_or("-");
let total = row_data.get("total").and_then(|v| v.as_i64()).unwrap_or(0);
let success = row_data
.get("successful")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let failed = row_data.get("failed").and_then(|v| v.as_i64()).unwrap_or(0);
let avg = row_data
.get("avg_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let min = row_data
.get("min_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let max = row_data
.get("max_ms")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
table.add_row(row![
op.bright_cyan(),
total,
success.to_string().green(),
if failed > 0 {
failed.to_string().red()
} else {
"0".normal()
},
format!("{:.2}", avg),
format!("{:.2}", min),
format!("{:.2}", max)
]);
}
table.printstd();
}
}
}
DbSubcommand::Runs { limit, format } => {
let engine = AnalyticsEngine::open()?;
let runs = engine.get_runs(limit)?;
let use_json = json_mode || matches!(format, cli::DbOutputFormat::Json);
if use_json {
let envelope = ResponseEnvelope::success("db runs", runs.clone());
println!("{}", envelope.to_json());
return Ok(());
}
println!("{} Recent API Runs", "📋".bright_cyan());
println!();
match format {
cli::DbOutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&runs)?);
}
cli::DbOutputFormat::Table => {
let empty_vec = vec![];
let runs_arr = runs.as_array().unwrap_or(&empty_vec);
if runs_arr.is_empty() {
println!("{}", "No runs recorded yet.".yellow());
println!("\n💡 Run 'mrapids run <operation>' to execute an API request.");
} else {
use prettytable::{row, Table};
let mut table = Table::new();
table
.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(row![
bc -> "Run ID",
bc -> "Timestamp",
bc -> "Requests",
bc -> "✓",
bc -> "✗",
bc -> "Duration",
bc -> "Status"
]);
for run in runs_arr {
let run_id = run["run_id"].as_str().unwrap_or("-");
let timestamp = run["timestamp"].as_str().unwrap_or("-");
let total = run["total_requests"].as_i64().unwrap_or(0);
let successful = run["successful"].as_i64().unwrap_or(0);
let failed = run["failed"].as_i64().unwrap_or(0);
let duration = run["duration_ms"]
.as_f64()
.map(|d| format!("{:.0}ms", d))
.unwrap_or_else(|| "-".to_string());
let status = run["status"].as_str().unwrap_or("unknown");
let status_colored = match status {
"completed" => status.green().to_string(),
"failed" => status.red().to_string(),
"running" => status.yellow().to_string(),
_ => status.to_string(),
};
table.add_row(row![
run_id.bright_magenta(),
timestamp,
total,
successful.to_string().green(),
failed.to_string().red(),
duration,
status_colored
]);
}
table.printstd();
println!();
println!(
"{}",
"Use 'mrapids db run <run_id>' to see run details".dimmed()
);
}
}
}
}
DbSubcommand::Run { run_id, format } => {
let engine = AnalyticsEngine::open()?;
let requests = engine.get_run_requests(&run_id)?;
let use_json = json_mode || matches!(format, cli::DbOutputFormat::Json);
if use_json {
let envelope = ResponseEnvelope::success("db run", requests.clone());
println!("{}", envelope.to_json());
return Ok(());
}
println!(
"{} Run Details: {}",
"📋".bright_cyan(),
run_id.bright_magenta()
);
println!();
match format {
cli::DbOutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&requests)?);
}
cli::DbOutputFormat::Table => {
let empty_vec = vec![];
let requests_arr = requests.as_array().unwrap_or(&empty_vec);
if requests_arr.is_empty() {
println!(
"{}",
format!("No requests found for run '{}'", run_id).yellow()
);
} else {
use prettytable::{row, Table};
let mut table = Table::new();
table
.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(row![
bc -> "Request ID",
bc -> "Operation",
bc -> "Method",
bc -> "Endpoint",
bc -> "Status",
bc -> "Duration",
bc -> "Success"
]);
for req in requests_arr {
let request_id = req["request_id"].as_str().unwrap_or("-");
let operation = req["operation_id"].as_str().unwrap_or("-");
let method = req["method"].as_str().unwrap_or("-");
let endpoint = req["endpoint"].as_str().unwrap_or("-");
let status = req["status_code"].as_i64().unwrap_or(0);
let duration = req["duration_ms"]
.as_f64()
.map(|d| format!("{:.0}ms", d))
.unwrap_or_else(|| "-".to_string());
let success = req["success"].as_bool().unwrap_or(false);
let status_colored = if (200..300).contains(&(status as u16)) {
status.to_string().green().to_string()
} else if status >= 400 {
status.to_string().red().to_string()
} else {
status.to_string()
};
let success_icon = if success { "✓".green() } else { "✗".red() };
table.add_row(row![
request_id.bright_cyan(),
operation,
method.bright_yellow(),
endpoint,
status_colored,
duration,
success_icon
]);
}
table.printstd();
println!();
println!("{}", "Use 'mrapids db request <request_id>' to see full request/response details".dimmed());
}
}
}
}
DbSubcommand::Request { request_id, format } => {
let engine = AnalyticsEngine::open()?;
let detail = engine.get_request_detail(&request_id)?;
let use_json = json_mode || matches!(format, cli::DbOutputFormat::Json);
if use_json {
let envelope = ResponseEnvelope::success("db request", detail.clone());
println!("{}", envelope.to_json());
return Ok(());
}
println!(
"{} Request Details: {}",
"📋".bright_cyan(),
request_id.bright_cyan()
);
println!();
match format {
cli::DbOutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&detail)?);
}
cli::DbOutputFormat::Table => {
let empty_vec = vec![];
let details_arr = detail.as_array().unwrap_or(&empty_vec);
if details_arr.is_empty() {
println!(
"{}",
format!("No request found with ID '{}'", request_id).yellow()
);
} else {
let req = &details_arr[0];
println!("{}", "═══ Request ═══".bright_blue());
println!(
" Run ID: {}",
req["run_id"].as_str().unwrap_or("-").bright_magenta()
);
println!(
" Operation: {}",
req["operation_id"].as_str().unwrap_or("-")
);
println!(
" Method: {}",
req["method"].as_str().unwrap_or("-").bright_yellow()
);
println!(" Endpoint: {}", req["endpoint"].as_str().unwrap_or("-"));
println!(
" URL: {}",
req["url"].as_str().unwrap_or("-").bright_blue()
);
if let Some(headers) = req.get("request_headers") {
if !headers.is_null() {
println!(" Headers: {}", headers.to_string().dimmed());
}
}
if let Some(params) = req.get("query_params") {
if !params.is_null() && params.as_str() != Some("{}") {
println!(" Query: {}", params.to_string().dimmed());
}
}
if let Some(payload) = req.get("payload") {
if !payload.is_null() {
let payload_owned = payload.to_string();
let payload_str = payload.as_str().unwrap_or(&payload_owned);
if payload_str.len() > 100 {
println!(" Payload: {}...", &payload_str[..100].dimmed());
} else {
println!(" Payload: {}", payload_str.dimmed());
}
}
}
println!();
println!("{}", "═══ Response ═══".bright_green());
let status = req["status_code"].as_i64().unwrap_or(0);
let status_colored = if (200..300).contains(&(status as u16)) {
format!("{} {}", status, req["status_text"].as_str().unwrap_or(""))
.green()
} else if status >= 400 {
format!("{} {}", status, req["status_text"].as_str().unwrap_or(""))
.red()
} else {
format!("{} {}", status, req["status_text"].as_str().unwrap_or(""))
.normal()
};
println!(" Status: {}", status_colored);
println!(
" Duration: {}ms",
req["duration_ms"].as_f64().unwrap_or(0.0)
);
let success = req["success"].as_bool().unwrap_or(false);
println!(
" Success: {}",
if success { "✓".green() } else { "✗".red() }
);
if let Some(err) = req.get("error_message") {
if !err.is_null() {
println!(" Error: {}", err.as_str().unwrap_or("-").red());
}
}
if let Some(body) = req.get("body") {
if !body.is_null() {
let body_owned = body.to_string();
let body_str = body.as_str().unwrap_or(&body_owned);
println!();
println!("{}", "═══ Response Body ═══".bright_cyan());
if body_str.len() > 500 {
if let Ok(json) =
serde_json::from_str::<serde_json::Value>(body_str)
{
let pretty = serde_json::to_string_pretty(&json)
.unwrap_or_else(|_| body_str.to_string());
let lines: Vec<&str> = pretty.lines().take(20).collect();
println!("{}", lines.join("\n"));
if pretty.lines().count() > 20 {
println!("{}", "... (truncated)".dimmed());
}
} else {
println!("{}...", &body_str[..500]);
println!(
"{}",
format!("... ({} bytes total)", body_str.len())
.dimmed()
);
}
} else {
if let Ok(json) =
serde_json::from_str::<serde_json::Value>(body_str)
{
println!(
"{}",
serde_json::to_string_pretty(&json)
.unwrap_or_else(|_| body_str.to_string())
);
} else {
println!("{}", body_str);
}
}
}
}
}
}
}
}
DbSubcommand::Reset { force } => {
if !force {
println!("{} This will delete all analytics data.", "⚠️".yellow());
print!("Are you sure you want to reset the database? [y/N] ");
use std::io::{self, BufRead, Write};
io::stdout().flush()?;
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
if let Some(Ok(line)) = lines.next() {
if !line.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
}
let db_path = AnalyticsEngine::get_db_path()?;
if db_path.exists() {
std::fs::remove_file(&db_path)?;
println!("{} Database reset successfully.", "✓".green());
let _engine = AnalyticsEngine::open()?;
println!("{} New database initialized.", "✓".green());
} else {
println!("{}", "No database file found.".yellow());
}
}
DbSubcommand::Check { format, fix } => {
use cli::DbCheckFormat;
println!("{} Running database health checks...", "🔍".bright_cyan());
println!();
let mut checks: Vec<(&str, bool, String)> = Vec::new();
let mut all_passed = true;
let db_path = AnalyticsEngine::get_db_path()?;
let db_exists = db_path.exists();
checks.push((
"Database file exists",
db_exists,
if db_exists {
format!("{}", db_path.display())
} else {
"File not found".to_string()
},
));
if !db_exists {
all_passed = false;
}
if db_exists {
match AnalyticsEngine::open() {
Ok(engine) => {
checks.push((
"Database opens successfully",
true,
"Connection established".to_string(),
));
let status = engine.get_status()?;
let tables_ok = status.table_count >= 6;
checks.push((
"Schema tables present",
tables_ok,
format!("{} tables", status.table_count),
));
if !tables_ok {
all_passed = false;
}
let required_tables = vec![
"runs",
"requests",
"responses",
"comparisons",
"comparison_diffs",
];
let schema = engine.get_schema()?;
let existing_tables: Vec<String> =
schema.iter().map(|t| t.name.clone()).collect();
let mut missing_tables = Vec::new();
for table in &required_tables {
if !existing_tables.contains(&table.to_string()) {
missing_tables.push(*table);
}
}
let tables_ok = missing_tables.is_empty();
checks.push((
"Required tables exist",
tables_ok,
if tables_ok {
format!("{} tables found", existing_tables.len())
} else {
format!("Missing: {}", missing_tables.join(", "))
},
));
if !tables_ok {
all_passed = false;
}
match engine.validate() {
Ok(true) => {
checks.push((
"Database validation",
true,
"Read/write test passed".to_string(),
));
}
Ok(false) | Err(_) => {
checks.push((
"Database validation",
false,
"Read/write test failed".to_string(),
));
all_passed = false;
}
}
let orphan_check = engine.query_json(
"SELECT COUNT(*) as count FROM responses WHERE request_id NOT IN (SELECT request_id FROM requests)"
)?;
let orphan_count = orphan_check
.as_array()
.and_then(|arr| arr.first())
.and_then(|row| row.get("count"))
.and_then(|v| v.as_i64())
.unwrap_or(0);
let no_orphans = orphan_count == 0;
checks.push((
"No orphaned records",
no_orphans,
if no_orphans {
"Foreign key integrity OK".to_string()
} else {
format!("{} orphaned response records", orphan_count)
},
));
if fix && !no_orphans {
println!("{} Fixing orphaned records...", "🔧".yellow());
engine.connection().execute(
"DELETE FROM responses WHERE request_id NOT IN (SELECT request_id FROM requests)",
[]
)?;
println!(
"{} Removed {} orphaned response records",
"✓".green(),
orphan_count
);
}
}
Err(e) => {
checks.push((
"Database opens successfully",
false,
format!("Error: {}", e),
));
all_passed = false;
}
}
}
match format {
DbCheckFormat::Json => {
let results: Vec<serde_json::Value> = checks
.iter()
.map(|(name, passed, detail)| {
serde_json::json!({
"check": name,
"passed": passed,
"detail": detail
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"all_passed": all_passed,
"checks": results
}))?
);
}
DbCheckFormat::Text => {
for (name, passed, detail) in &checks {
let status_icon = if *passed { "✓".green() } else { "✗".red() };
let name_colored = if *passed { name.green() } else { name.red() };
println!(" {} {} - {}", status_icon, name_colored, detail.dimmed());
}
println!();
if all_passed {
println!("{} All health checks passed!", "🎉".bright_green());
} else {
println!(
"{} Some checks failed. Use --fix to attempt automatic repairs.",
"⚠️".yellow()
);
}
}
}
}
DbSubcommand::Migrations => {
println!("{} Database Schema Info", "📋".bright_cyan());
println!();
let engine = AnalyticsEngine::open()?;
let status = engine.get_status()?;
println!("{}:", "Current Schema".bold());
println!(" Engine: SQLite {}", status.engine_version.bright_green());
println!(" Tables: {}", status.table_count);
println!();
println!("{}:", "Tables".bold());
println!(" api_requests - Basic request logging");
println!(" runs, requests, responses - Detailed request/response tracking");
println!(" comparisons, comparison_diffs - API reconciliation");
println!();
println!(
"{}",
"Schema is initialized automatically on first use.".dimmed()
);
}
}
Ok(())
}
fn handle_sql_command(cmd: cli::SqlCommand) -> Result<()> {
use crate::core::analytics_engine::AnalyticsEngine;
use crate::core::output::{is_json_mode, ResponseEnvelope};
use cli::SqlSubcommand;
use colored::*;
use prettytable::{Cell, Row, Table};
use std::fs;
use std::path::PathBuf;
let json_mode = is_json_mode();
fn get_queries_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let queries_dir = home.join(".mrapids").join("queries");
if !queries_dir.exists() {
fs::create_dir_all(&queries_dir)?;
}
Ok(queries_dir)
}
fn execute_query(
engine: &AnalyticsEngine,
query: &str,
json: bool,
csv: bool,
no_header: bool,
) -> Result<()> {
if json {
let results = engine.query_json(query)?;
println!("{}", serde_json::to_string_pretty(&results)?);
} else if csv {
let csv_output = engine.query_csv(query, !no_header)?;
println!("{}", csv_output);
} else {
let (columns, rows) = engine.query_table(query)?;
if rows.is_empty() {
println!("{}", "No results.".dimmed());
} else {
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
if !no_header {
let header_cells: Vec<Cell> = columns
.iter()
.map(|c| Cell::new(c).style_spec("bc"))
.collect();
table.set_titles(Row::new(header_cells));
}
for row_data in rows {
let cells: Vec<Cell> = row_data.iter().map(|v| Cell::new(v)).collect();
table.add_row(Row::new(cells));
}
table.printstd();
}
}
Ok(())
}
match cmd.command {
Some(SqlSubcommand::Save {
name,
query,
description,
}) => {
let queries_dir = get_queries_dir()?;
let file_path = queries_dir.join(format!("{}.sql", name));
let content = if let Some(desc) = description {
format!("-- {}\n{}", desc, query)
} else {
query
};
fs::write(&file_path, content)?;
println!("{} Query saved as '{}'", "✓".green(), name.bright_cyan());
println!(" Path: {}", file_path.display().to_string().dimmed());
println!(
"\n Run with: {}",
format!("mrapids sql run {}", name).bright_yellow()
);
}
Some(SqlSubcommand::Run {
name,
json,
csv,
table: _,
no_header,
}) => {
let queries_dir = get_queries_dir()?;
let file_path = queries_dir.join(format!("{}.sql", name));
if !file_path.exists() {
return Err(ApiError::ValidationError(format!(
"Query '{}' not found. Use 'mrapids sql list' to see available queries.",
name
))
.into());
}
let content = fs::read_to_string(&file_path)?;
let query: String = content
.lines()
.filter(|line| !line.trim().starts_with("--"))
.collect::<Vec<_>>()
.join("\n");
let engine = AnalyticsEngine::open()?;
execute_query(&engine, &query, json, csv, no_header)?;
}
Some(SqlSubcommand::List) => {
let queries_dir = get_queries_dir()?;
let entries = fs::read_dir(&queries_dir)?;
let mut queries: Vec<(String, Option<String>)> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "sql").unwrap_or(false) {
let name = path.file_stem().unwrap().to_string_lossy().to_string();
let content = fs::read_to_string(&path).ok();
let description = content.and_then(|c| {
c.lines()
.find(|l| l.trim().starts_with("--"))
.map(|l| l.trim().trim_start_matches("--").trim().to_string())
});
queries.push((name, description));
}
}
if queries.is_empty() {
println!("{}", "No saved queries found.".yellow());
println!("\n💡 Save a query with: mrapids sql save <name> \"<query>\"");
} else {
println!("{} Saved Queries", "📋".bright_cyan());
println!();
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(prettytable::row![bc -> "Name", bc -> "Description"]);
for (name, desc) in &queries {
table.add_row(prettytable::row![
name.bright_cyan(),
desc.as_deref().unwrap_or("-").dimmed()
]);
}
table.printstd();
println!();
println!("{}", "Run a query with: mrapids sql run <name>".dimmed());
}
}
Some(SqlSubcommand::Delete { name, force }) => {
let queries_dir = get_queries_dir()?;
let file_path = queries_dir.join(format!("{}.sql", name));
if !file_path.exists() {
return Err(
ApiError::ValidationError(format!("Query '{}' not found.", name)).into(),
);
}
if !force {
println!("{} Delete query '{}'?", "⚠️".yellow(), name);
print!("Are you sure? [y/N] ");
use std::io::{self, BufRead, Write};
io::stdout().flush()?;
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
if let Some(Ok(line)) = lines.next() {
if !line.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
}
fs::remove_file(&file_path)?;
println!("{} Query '{}' deleted.", "✓".green(), name);
}
None => {
if let Some(query) = cmd.query {
let engine = AnalyticsEngine::open()?;
let use_json = json_mode || cmd.json;
if use_json && !cmd.csv {
let results = engine.query_json(&query)?;
let envelope = ResponseEnvelope::success("sql", results);
println!("{}", envelope.to_json());
} else {
execute_query(&engine, &query, cmd.json, cmd.csv, cmd.no_header)?;
}
} else {
return Err(ApiError::ValidationError(
"Please provide a SQL query or use a subcommand (save, run, list)".to_string(),
)
.into());
}
}
}
Ok(())
}
fn handle_compare_command(cmd: cli::CompareCommand) -> Result<()> {
use crate::core::output::{is_json_mode, ResponseEnvelope};
use core::analytics_engine::AnalyticsEngine;
use prettytable::{row, Table};
use std::collections::HashMap;
let json_mode = is_json_mode();
let engine = AnalyticsEngine::open()?;
if !engine.run_exists(&cmd.left)? {
return Err(ApiError::ValidationError(format!("Left run '{}' not found", cmd.left)).into());
}
if !engine.run_exists(&cmd.right)? {
return Err(
ApiError::ValidationError(format!("Right run '{}' not found", cmd.right)).into(),
);
}
if !json_mode && !cmd.json {
println!("{} Comparing runs", "🔍".bright_cyan());
println!(" Left (baseline): {}", cmd.left.bright_yellow());
println!(" Right (compare): {}", cmd.right.bright_green());
println!();
}
let comparison_id = AnalyticsEngine::generate_comparison_id();
engine.create_comparison(&comparison_id, &cmd.left, &cmd.right)?;
let left_requests = engine.get_run_requests_for_comparison(&cmd.left)?;
let right_requests = engine.get_run_requests_for_comparison(&cmd.right)?;
let left_map: HashMap<String, serde_json::Value> = left_requests
.into_iter()
.map(|(endpoint, method, data)| (format!("{} {}", method.to_uppercase(), endpoint), data))
.collect();
let right_map: HashMap<String, serde_json::Value> = right_requests
.into_iter()
.map(|(endpoint, method, data)| (format!("{} {}", method.to_uppercase(), endpoint), data))
.collect();
let mut total_diffs: i32 = 0;
let mut status_diffs = 0;
let mut body_diffs = 0;
let mut missing_left = 0;
let mut missing_right = 0;
for (key, left_data) in &left_map {
if !right_map.contains_key(key) {
missing_right += 1;
total_diffs += 1;
engine.store_diff(
&comparison_id,
key,
"missing_in_right",
Some(left_data),
None,
None,
"warning",
Some("Endpoint exists in left run but not in right run"),
)?;
}
}
for (key, right_data) in &right_map {
if !left_map.contains_key(key) {
missing_left += 1;
total_diffs += 1;
engine.store_diff(
&comparison_id,
key,
"missing_in_left",
None,
Some(right_data),
None,
"warning",
Some("Endpoint exists in right run but not in left run"),
)?;
}
}
for (key, left_data) in &left_map {
if let Some(right_data) = right_map.get(key) {
let left_status = left_data.get("status_code");
let right_status = right_data.get("status_code");
if left_status != right_status {
status_diffs += 1;
total_diffs += 1;
let severity = if cmd.breaking_only {
"error"
} else {
"warning"
};
engine.store_diff(
&comparison_id,
key,
"status_code_diff",
left_status,
right_status,
Some("status_code"),
severity,
Some(&format!(
"Status code changed from {} to {}",
left_status.map(|v| v.to_string()).unwrap_or_default(),
right_status.map(|v| v.to_string()).unwrap_or_default()
)),
)?;
}
if !cmd.breaking_only {
let left_body = left_data.get("response_body");
let right_body = right_data.get("response_body");
if left_body != right_body {
body_diffs += 1;
total_diffs += 1;
engine.store_diff(
&comparison_id,
key,
"body_diff",
left_body,
right_body,
Some("response_body"),
"info",
Some("Response body differs between runs"),
)?;
}
}
if !cmd.ignore_headers && !cmd.breaking_only {
let left_headers = left_data.get("response_headers");
let right_headers = right_data.get("response_headers");
if left_headers != right_headers {
total_diffs += 1;
engine.store_diff(
&comparison_id,
key,
"header_diff",
left_headers,
right_headers,
Some("response_headers"),
"info",
Some("Response headers differ between runs"),
)?;
}
}
if !cmd.ignore_timing && !cmd.breaking_only {
let left_duration = left_data.get("duration_ms").and_then(|v| v.as_f64());
let right_duration = right_data.get("duration_ms").and_then(|v| v.as_f64());
if let (Some(l), Some(r)) = (left_duration, right_duration) {
let diff_pct = ((r - l) / l * 100.0).abs();
if diff_pct > 50.0 {
total_diffs += 1;
engine.store_diff(
&comparison_id,
key,
"timing_diff",
Some(&serde_json::json!(l)),
Some(&serde_json::json!(r)),
Some("duration_ms"),
"info",
Some(&format!("Response time changed by {:.1}%", diff_pct)),
)?;
}
}
}
}
}
let summary = serde_json::json!({
"status_code_differences": status_diffs,
"body_differences": body_diffs,
"missing_in_left": missing_left,
"missing_in_right": missing_right,
"total_endpoints_left": left_map.len(),
"total_endpoints_right": right_map.len()
});
engine.complete_comparison(&comparison_id, total_diffs, Some(&summary))?;
let use_json = json_mode || cmd.json;
if use_json {
let result = serde_json::json!({
"comparison_id": comparison_id,
"left_run": cmd.left,
"right_run": cmd.right,
"total_diffs": total_diffs,
"summary": summary,
"diffs": engine.get_comparison_diffs(&comparison_id)?
});
let envelope = ResponseEnvelope::success("compare", result);
println!("{}", envelope.to_json());
} else {
println!(
"{} Comparison Complete: {}",
"✓".green(),
comparison_id.bright_magenta()
);
println!();
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(row![bc -> "Category", bc -> "Count"]);
if status_diffs > 0 {
table.add_row(
row![r -> "Status code differences", Fr -> status_diffs.to_string().red()],
);
}
if body_diffs > 0 {
table.add_row(row![r -> "Body differences", Fy -> body_diffs.to_string().yellow()]);
}
if missing_right > 0 {
table.add_row(
row![r -> "Missing in right run", Fy -> missing_right.to_string().yellow()],
);
}
if missing_left > 0 {
table
.add_row(row![r -> "Missing in left run", Fy -> missing_left.to_string().yellow()]);
}
table.add_row(row![bFg -> "Total differences", bFg -> total_diffs.to_string()]);
if total_diffs == 0 {
println!(
"{} No differences found! Runs are identical.",
"🎉".bright_green()
);
} else {
table.printstd();
println!();
println!("📊 View full diff details:");
println!(
" {}",
format!(
"mrapids sql \"SELECT * FROM comparison_diffs WHERE comparison_id = '{}'\"",
comparison_id
)
.dimmed()
);
}
}
Ok(())
}
fn handle_history_command(cmd: cli::HistoryCommand) -> Result<()> {
use crate::core::output::{is_json_mode, ResponseEnvelope};
use core::analytics_engine::AnalyticsEngine;
use prettytable::Table;
let json_mode = is_json_mode();
let engine = AnalyticsEngine::open()?;
let mut conditions = Vec::new();
if let Some(spec) = &cmd.spec {
conditions.push(format!("spec_path LIKE '%{}%'", spec));
}
if cmd.failed {
conditions.push("failed > 0".to_string());
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
};
let query = format!(
r#"
SELECT
run_id,
strftime('%Y-%m-%d %H:%M:%S', timestamp) as timestamp,
COALESCE(spec_path, '-') as spec,
total_requests,
successful,
failed,
ROUND(duration_ms, 0) as duration_ms,
status
FROM runs
{}
ORDER BY timestamp DESC
LIMIT {}
"#,
where_clause, cmd.limit
);
let use_json = json_mode || cmd.json;
if use_json {
let result = engine.query_json(&query)?;
let envelope = ResponseEnvelope::success("history", result);
println!("{}", envelope.to_json());
} else {
println!("{} API Run History", "📜".bright_cyan());
println!();
let (headers, rows) = engine.query_table(&query)?;
if rows.is_empty() {
println!("{}", "No runs found.".yellow());
println!("\n💡 Run an API request to create history:");
println!(" mrapids run get --url https://httpbin.org");
} else {
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(prettytable::Row::new(
headers
.iter()
.map(|h| prettytable::Cell::new(h).style_spec("bc"))
.collect(),
));
for row_data in rows {
let cells: Vec<prettytable::Cell> = row_data
.iter()
.enumerate()
.map(|(i, val)| {
let header = &headers[i];
match header.as_str() {
"run_id" => prettytable::Cell::new(val).style_spec("Fm"),
"status" => {
if val == "completed" {
prettytable::Cell::new(val).style_spec("Fg")
} else if val == "failed" {
prettytable::Cell::new(val).style_spec("Fr")
} else {
prettytable::Cell::new(val).style_spec("Fy")
}
}
"failed" => {
if val != "0" {
prettytable::Cell::new(val).style_spec("Fr")
} else {
prettytable::Cell::new(val)
}
}
"successful" => {
if val != "0" {
prettytable::Cell::new(val).style_spec("Fg")
} else {
prettytable::Cell::new(val)
}
}
_ => prettytable::Cell::new(val),
}
})
.collect();
table.add_row(prettytable::Row::new(cells));
}
table.printstd();
println!();
println!(
"{}",
format!("Showing last {} runs. Use --limit N for more.", cmd.limit).dimmed()
);
}
}
Ok(())
}
fn handle_export_command(cmd: cli::ExportCommand) -> Result<()> {
use cli::ExportFormat;
use core::analytics_engine::AnalyticsEngine;
let engine = AnalyticsEngine::open()?;
let query = if let Some(table) = &cmd.table {
let valid_tables = [
"runs",
"requests",
"responses",
"comparisons",
"comparison_diffs",
"api_requests",
"collection_runs",
];
if !valid_tables.contains(&table.as_str()) {
return Err(ApiError::ValidationError(format!(
"Invalid table '{}'. Valid tables: {}",
table,
valid_tables.join(", ")
))
.into());
}
format!("SELECT * FROM {}", table)
} else if let Some(query) = &cmd.query {
query.clone()
} else {
return Err(ApiError::ValidationError(
"Please specify either --table or --query".to_string(),
)
.into());
};
let extension = match cmd.format {
ExportFormat::Parquet => "parquet",
ExportFormat::Csv => "csv",
ExportFormat::Json => "json",
};
let output_path = if let Some(path) = cmd.output {
path
} else {
let base_name = if let Some(table) = &cmd.table {
table.clone()
} else {
"export".to_string()
};
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
PathBuf::from(format!("{}_{}.{}", base_name, timestamp, extension))
};
println!("{} Exporting data...", "📦".bright_cyan());
println!(" Format: {}", extension.bright_yellow());
println!(
" Output: {}",
output_path.display().to_string().bright_green()
);
match cmd.format {
ExportFormat::Parquet => {
return Err(ApiError::ValidationError(
"Parquet export is not supported. Use CSV or JSON format instead.".to_string(),
)
.into());
}
ExportFormat::Csv => {
let csv = engine.query_csv(&query, true)?;
std::fs::write(&output_path, csv)?;
}
ExportFormat::Json => {
let json = engine.query_json(&query)?;
let pretty = serde_json::to_string_pretty(&json)?;
std::fs::write(&output_path, pretty)?;
}
};
let metadata = std::fs::metadata(&output_path)?;
let size = metadata.len();
let size_human = if size >= 1024 * 1024 {
format!("{:.2} MB", size as f64 / (1024.0 * 1024.0))
} else if size >= 1024 {
format!("{:.2} KB", size as f64 / 1024.0)
} else {
format!("{} bytes", size)
};
println!();
println!("{} Export complete!", "✓".green());
println!(" Size: {}", size_human);
Ok(())
}
fn handle_index_command(cmd: cli::IndexCommand) -> Result<()> {
use cli::IndexSubcommand;
use mrapids::core::cards::build_cards_from_spec;
use mrapids::core::embeddings::{EmbeddingEngine, OpenAIEmbeddingEngine};
use mrapids::core::index_store::IndexStore;
use mrapids::core::parser::parse_spec;
use sha2::{Digest, Sha256};
let db_path = get_index_db_path()?;
let store = IndexStore::open(&db_path)?;
match cmd.command {
IndexSubcommand::Build {
spec,
id,
force,
embed,
} => {
let spec_path = match spec {
Some(p) => p,
None => find_spec_file()?,
};
println!(
"{} Building index from {}",
"📦".bright_cyan(),
spec_path.display()
);
let content = std::fs::read_to_string(&spec_path)?;
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
let spec_hash = hex::encode(hasher.finalize());
let spec_id = id.unwrap_or_else(|| {
spec_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("default")
.to_string()
});
if !force {
let specs = store.list_specs()?;
for existing in specs {
if existing.spec_id == spec_id && existing.spec_hash == spec_hash {
println!(
"{} Spec already indexed (hash unchanged). Use --force to rebuild.",
"ℹ️".yellow()
);
return Ok(());
}
}
}
let unified_spec = parse_spec(&content)?;
let cards = build_cards_from_spec(&spec_id, &unified_spec);
let card_count = cards.len();
println!(" {} Found {} operations", "→".dimmed(), card_count);
store.upsert_spec(
&spec_id,
&spec_path.to_string_lossy(),
&unified_spec.info.title,
&unified_spec.info.version,
&unified_spec.base_url,
card_count,
&spec_hash,
)?;
for card in &cards {
store.upsert_card(card)?;
}
let vocab_count = store.build_vocabulary(&spec_id, &cards)?;
println!(" {} Built vocabulary: {} terms", "→".dimmed(), vocab_count);
println!(
"{} Indexed {} operations from '{}' spec",
"✅".green(),
card_count,
spec_id
);
if embed == "openai" {
println!(" {} Generating embeddings with OpenAI...", "🧠".cyan());
let api_key = std::env::var("OPENAI_API_KEY")
.context("OPENAI_API_KEY environment variable required for --embed openai")?;
let engine = OpenAIEmbeddingEngine::new(api_key);
let texts: Vec<String> = cards.iter().map(|c| c.embedding_text.clone()).collect();
let batch_size = 100;
let mut embedded_count = 0;
for (batch_idx, batch) in texts.chunks(batch_size).enumerate() {
let batch_texts: Vec<String> = batch.to_vec();
match engine.embed_batch(&batch_texts) {
Ok(embeddings) => {
for (i, embedding) in embeddings.into_iter().enumerate() {
let card_idx = batch_idx * batch_size + i;
let card_id =
format!("{}:{}", spec_id, cards[card_idx].operation_id);
store.upsert_embedding(&card_id, &embedding, "openai")?;
embedded_count += 1;
}
}
Err(e) => {
eprintln!(" {} Failed to generate embeddings: {}", "⚠️".yellow(), e);
break;
}
}
print!(
"\r {} Embedded {}/{} operations",
"→".dimmed(),
embedded_count,
card_count
);
std::io::Write::flush(&mut std::io::stdout())?;
}
println!(
"\r {} Embedded {} operations with OpenAI ",
"✅".green(),
embedded_count
);
} else if embed == "local" {
#[cfg(feature = "embeddings")]
{
use mrapids::core::embeddings::LocalEmbeddingEngine;
println!(
" {} Generating embeddings with local ONNX model...",
"🧠".cyan()
);
match LocalEmbeddingEngine::new() {
Ok(engine) => {
let mut embedded_count = 0;
let pb = indicatif::ProgressBar::new(card_count as u64);
pb.set_style(
indicatif::ProgressStyle::default_bar()
.template(" {spinner:.cyan} Embedding [{bar:30}] {pos}/{len}")
.unwrap_or_else(|_| indicatif::ProgressStyle::default_bar()),
);
for card in &cards {
let card_id = format!("{}:{}", spec_id, card.operation_id);
match engine.embed(&card.embedding_text) {
Ok(embedding) => {
store.upsert_embedding(&card_id, &embedding, "local")?;
embedded_count += 1;
}
Err(e) => {
eprintln!(
" {} Failed to embed {}: {}",
"⚠️".yellow(),
card.operation_id,
e
);
}
}
pb.inc(1);
}
pb.finish_and_clear();
println!(
" {} Embedded {} operations with local ONNX model",
"✅".green(),
embedded_count
);
}
Err(e) => {
eprintln!(
" {} Failed to load local embedding model: {}",
"⚠️".yellow(),
e
);
println!(
" {} Continuing without embeddings (keyword search still works)",
"ℹ️".yellow()
);
}
}
}
#[cfg(not(feature = "embeddings"))]
{
println!(
" {} Local embeddings require the 'embeddings' feature.",
"ℹ️".yellow()
);
println!(
" {} Rebuild with: cargo build --features embeddings",
"→".dimmed()
);
}
}
}
IndexSubcommand::Add {
spec,
id,
force,
embed,
} => {
let build_cmd = cli::IndexSubcommand::Build {
spec: Some(spec),
id,
force,
embed,
};
let cmd = cli::IndexCommand { command: build_cmd };
return handle_index_command(cmd);
}
IndexSubcommand::List { format } => {
let specs = store.list_specs()?;
if specs.is_empty() {
println!(
"{} No specs indexed yet. Run: mrapids index build",
"ℹ️".yellow()
);
return Ok(());
}
let format = if crate::core::output::is_json_mode() {
cli::IndexOutputFormat::Json
} else {
format
};
match format {
cli::IndexOutputFormat::Table => {
println!("\n{}", "Indexed Specs".bold());
println!("{}", "─".repeat(80));
println!(
"{:<20} {:<30} {:<10} {:<10} {}",
"ID".bold(),
"Title".bold(),
"Version".bold(),
"Ops".bold(),
"Indexed".bold()
);
println!("{}", "─".repeat(80));
for spec in specs {
let title = if spec.title.len() > 28 {
format!("{}...", &spec.title[..25])
} else {
spec.title.clone()
};
println!(
"{:<20} {:<30} {:<10} {:<10} {}",
spec.spec_id,
title,
spec.version,
spec.operation_count,
spec.indexed_at
);
}
println!();
}
cli::IndexOutputFormat::Json => {
let json = serde_json::json!({
"specs": specs.iter().map(|s| serde_json::json!({
"spec_id": s.spec_id,
"title": s.title,
"version": s.version,
"operation_count": s.operation_count,
"indexed_at": s.indexed_at,
})).collect::<Vec<_>>()
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
}
}
IndexSubcommand::Status { format } => {
let status = store.get_status()?;
let specs = store.list_specs()?;
let format = if crate::core::output::is_json_mode() {
cli::IndexOutputFormat::Json
} else {
format
};
match format {
cli::IndexOutputFormat::Table => {
println!("\n{}", "Index Status".bold());
println!("{}", "─".repeat(40));
println!(" Database: {}", db_path.display());
println!(" Specs: {}", status.spec_count);
println!(" Operations: {}", status.card_count);
println!(
" Embeddings: {} {}",
status.embedding_count,
if status.has_embeddings {
"(semantic search enabled)".green()
} else {
"(keyword search only)".dimmed()
}
);
println!();
if !specs.is_empty() {
println!("{}", "Specs:".bold());
for spec in specs {
println!(
" {} {} ({} ops)",
"•".cyan(),
spec.spec_id,
spec.operation_count
);
}
println!();
}
}
cli::IndexOutputFormat::Json => {
let json = serde_json::json!({
"database_path": db_path.to_string_lossy(),
"spec_count": status.spec_count,
"card_count": status.card_count,
"embedding_count": status.embedding_count,
"has_embeddings": status.has_embeddings,
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
}
}
IndexSubcommand::Rebuild { spec, embed } => {
let specs = store.list_specs()?;
if specs.is_empty() {
println!(
"{} No specs to rebuild. Run: mrapids index add <spec>",
"ℹ️".yellow()
);
return Ok(());
}
let to_rebuild: Vec<_> = if let Some(ref id) = spec {
specs.into_iter().filter(|s| s.spec_id == *id).collect()
} else {
specs
};
for s in to_rebuild {
println!("{} Rebuilding '{}'...", "🔄".cyan(), s.spec_id);
let spec_path = PathBuf::from(&s.spec_path);
if spec_path.exists() {
let build_cmd = cli::IndexSubcommand::Build {
spec: Some(spec_path),
id: Some(s.spec_id),
force: true,
embed: embed.clone(),
};
let cmd = cli::IndexCommand { command: build_cmd };
handle_index_command(cmd)?;
} else {
println!(" {} Spec file not found: {}", "⚠️".yellow(), s.spec_path);
}
}
}
IndexSubcommand::Remove { spec_id, force } => {
if !force {
println!("Remove spec '{}' from index? [y/N] ", spec_id);
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}
let removed = store.remove_spec(&spec_id)?;
if removed > 0 {
println!(
"{} Removed '{}' ({} operations)",
"✅".green(),
spec_id,
removed
);
} else {
println!("{} Spec '{}' not found in index", "ℹ️".yellow(), spec_id);
}
}
}
Ok(())
}
fn handle_find_command(cmd: cli::FindCommand) -> Result<()> {
use mrapids::core::embeddings::{EmbeddingEngine, OpenAIEmbeddingEngine};
use mrapids::core::index_store::IndexStore;
let db_path = get_index_db_path()?;
let store = IndexStore::open(&db_path)?;
let status = store.get_status()?;
if status.card_count == 0 {
eprintln!(
"{} No operations indexed. Run: mrapids index build",
"⚠️".yellow()
);
return Ok(());
}
let json_mode = crate::core::output::is_json_mode();
let results = if cmd.semantic && status.has_embeddings {
if !json_mode {
println!("{} Using semantic search...", "🧠".cyan());
}
let api_key = std::env::var("OPENAI_API_KEY")
.context("OPENAI_API_KEY required for semantic search")?;
let engine = OpenAIEmbeddingEngine::new(api_key);
let query_embedding = engine.embed(&cmd.query)?;
store.vector_search(&query_embedding, cmd.limit, cmd.spec.as_deref())?
} else if cmd.semantic && !status.has_embeddings {
eprintln!(
"{} No embeddings found. Run: mrapids index build --embed openai",
"⚠️".yellow()
);
eprintln!("{} Falling back to keyword search...", "ℹ️".dimmed());
store.keyword_search(
&cmd.query,
cmd.limit,
cmd.spec.as_deref(),
cmd.method.as_deref(),
cmd.risk.as_deref(),
)?
} else {
store.keyword_search(
&cmd.query,
cmd.limit,
cmd.spec.as_deref(),
cmd.method.as_deref(),
cmd.risk.as_deref(),
)?
};
if results.is_empty() {
if json_mode {
println!(
"{}",
serde_json::json!({"query": cmd.query, "count": 0, "results": []})
);
} else {
println!(
"{} No operations found matching '{}'",
"ℹ️".yellow(),
cmd.query
);
}
return Ok(());
}
let format = if json_mode {
cli::FindOutputFormat::Json
} else {
cmd.format
};
match format {
cli::FindOutputFormat::Table => {
let search_mode = if cmd.semantic && status.has_embeddings {
"semantic"
} else {
"keyword"
};
println!(
"\n{} {} results for '{}' ({})\n",
"🔍".cyan(),
results.len(),
cmd.query,
search_mode
);
if cmd.semantic && status.has_embeddings {
println!(
"{:<6} {:<8} {:<25} {:<22} {}",
"Score".bold(),
"Method".bold(),
"Path".bold(),
"Operation".bold(),
"Summary".bold()
);
println!("{}", "─".repeat(100));
for result in results {
let method_colored = match result.method.as_str() {
"GET" => result.method.green(),
"POST" => result.method.blue(),
"PUT" | "PATCH" => result.method.yellow(),
"DELETE" => result.method.red(),
_ => result.method.normal(),
};
let path = if result.path.len() > 23 {
format!("{}...", &result.path[..20])
} else {
result.path.clone()
};
let op_id = if result.operation_id.len() > 20 {
format!("{}...", &result.operation_id[..17])
} else {
result.operation_id.clone()
};
let score_str = format!("{:.3}", result.score);
println!(
"{:<6} {:<8} {:<25} {:<22} {}",
score_str.bright_cyan(),
method_colored,
path,
op_id,
result.summary.as_deref().unwrap_or("").dimmed()
);
}
} else {
println!(
"{:<8} {:<30} {:<25} {}",
"Method".bold(),
"Path".bold(),
"Operation".bold(),
"Summary".bold()
);
println!("{}", "─".repeat(90));
for result in results {
let method_colored = match result.method.as_str() {
"GET" => result.method.green(),
"POST" => result.method.blue(),
"PUT" | "PATCH" => result.method.yellow(),
"DELETE" => result.method.red(),
_ => result.method.normal(),
};
let path = if result.path.len() > 28 {
format!("{}...", &result.path[..25])
} else {
result.path.clone()
};
let op_id = if result.operation_id.len() > 23 {
format!("{}...", &result.operation_id[..20])
} else {
result.operation_id.clone()
};
println!(
"{:<8} {:<30} {:<25} {}",
method_colored,
path,
op_id,
result.summary.as_deref().unwrap_or("").dimmed()
);
}
}
println!();
}
cli::FindOutputFormat::Json => {
let json = serde_json::json!({
"query": cmd.query,
"count": results.len(),
"results": results.iter().map(|r| r.to_json()).collect::<Vec<_>>()
});
println!("{}", serde_json::to_string_pretty(&json)?);
}
cli::FindOutputFormat::Ids => {
for result in results {
println!("{}", result.operation_id);
}
}
}
Ok(())
}
fn get_index_db_path() -> Result<PathBuf> {
let mut current = std::env::current_dir()?;
loop {
let mrapids_dir = current.join(".mrapids");
if mrapids_dir.exists() {
return Ok(mrapids_dir.join("index.db"));
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
let home = dirs::home_dir().context("Could not find home directory")?;
let global_dir = home.join(".mrapids");
std::fs::create_dir_all(&global_dir)?;
Ok(global_dir.join("index.db"))
}
fn find_spec_file() -> Result<PathBuf> {
let current_dir = std::env::current_dir()?;
let candidates = [
"openapi.yaml",
"openapi.yml",
"openapi.json",
"swagger.yaml",
"swagger.yml",
"swagger.json",
"api.yaml",
"api.yml",
"api.json",
"spec.yaml",
"spec.yml",
"spec.json",
];
let mrapids_dir = current_dir.join(".mrapids");
if mrapids_dir.exists() {
for candidate in &candidates {
let path = mrapids_dir.join(candidate);
if path.exists() {
return Ok(path);
}
}
}
let specs_dir = current_dir.join("specs");
if specs_dir.exists() {
for candidate in &candidates {
let path = specs_dir.join(candidate);
if path.exists() {
return Ok(path);
}
}
}
for candidate in &candidates {
let path = current_dir.join(candidate);
if path.exists() {
return Ok(path);
}
}
Err(ApiError::ValidationError(
"No OpenAPI spec found. Provide --spec path/to/spec.yaml or place spec in .mrapids/, specs/, or current directory".to_string()
).into())
}
async fn handle_collection_command(cmd: cli::CollectionCommand) -> Result<()> {
use cli::CollectionSubcommand;
use mrapids::collections::{
find_collection, list_collections, parse_collection, validate_collection,
CollectionExecutor, ConsoleReporter, ExecutionOptions,
};
use mrapids::core::parser::parse_spec;
use serde_json::json;
use std::collections::HashMap;
match cmd.command {
CollectionSubcommand::List { dir } => {
let collections = list_collections(&dir)?;
if collections.is_empty() {
println!("No collections found in {:?}", dir);
println!("\n💡 Create a collection YAML file in this directory to get started.");
} else {
println!("📚 Available collections:\n");
for path in collections {
let display_name = if let Ok(relative) = path.strip_prefix(&dir) {
relative
.to_string_lossy()
.trim_end_matches(".yaml")
.trim_end_matches(".yml")
.to_string()
} else if let Some(name) = path.file_stem() {
name.to_string_lossy().to_string()
} else {
continue;
};
println!(" • {}", display_name.bright_cyan());
}
println!("\nRun 'mrapids collection show <name>' for details");
}
}
CollectionSubcommand::Show { name, dir } => {
let path = find_collection(&dir, &name)?;
let collection = parse_collection(&path)?;
println!("📋 Collection: {}", collection.name.bright_cyan().bold());
if let Some(desc) = &collection.description {
println!(" {}", desc.dimmed());
}
println!("\n🔗 Requests ({}):", collection.requests.len());
for (i, request) in collection.requests.iter().enumerate() {
println!(
" {}. {} → {}",
i + 1,
request.name.bright_green(),
request.operation.dimmed()
);
}
if !collection.variables.is_empty() {
println!("\n📝 Variables:");
for (key, value) in &collection.variables {
println!(
" {} = {}",
key.bright_yellow(),
serde_json::to_string(value).unwrap_or_else(|_| "?".to_string())
);
}
}
if let Some(auth) = &collection.auth_profile {
println!("\n🔐 Auth Profile: {}", auth.bright_magenta());
}
}
CollectionSubcommand::Validate {
name,
dir,
spec: spec_path,
} => {
let path = find_collection(&dir, &name)?;
let collection = parse_collection(&path)?;
let spec = if let Some(spec_path) = spec_path {
let content = std::fs::read_to_string(&spec_path)?;
Some(parse_spec(&content)?)
} else {
None
};
let result = validate_collection(&collection, spec.as_ref());
if result.is_valid() {
println!("✅ Collection '{}' is valid!", name.bright_green());
} else {
println!("❌ Collection '{}' has errors:", name.bright_red());
for error in &result.errors {
println!(" • {}", error.red());
}
}
if !result.warnings.is_empty() {
println!("\n⚠️ Warnings:");
for warning in &result.warnings {
println!(" • {}", warning.yellow());
}
}
}
CollectionSubcommand::Run {
name,
dir,
output,
save_all,
save_summary,
variables,
auth_profile,
continue_on_error,
requests,
skip_requests,
use_env,
env_file,
spec: spec_path,
env: _,
} => {
let path = find_collection(&dir, &name)?;
let collection = parse_collection(&path)?;
let spec_path = spec_path.unwrap_or_else(|| PathBuf::from("specs/api.yaml"));
let spec_content = std::fs::read_to_string(&spec_path)?;
let spec = parse_spec(&spec_content)?;
let mut variable_map = HashMap::new();
for (key, value) in variables {
variable_map.insert(key, json!(value));
}
let options = ExecutionOptions {
continue_on_error,
skip_requests,
only_requests: if requests.is_empty() {
None
} else {
Some(requests)
},
auth_profile,
variable_overrides: variable_map,
use_env,
env_file,
save_all,
save_summary,
};
let auth = None;
let executor = CollectionExecutor::new(spec, auth);
let mut reporter = ConsoleReporter::new(output != "json");
let summary = executor
.execute(&collection, options, &mut reporter)
.await?;
if output == "json" {
println!("{}", serde_json::to_string_pretty(&summary)?);
}
if summary.failed > 0 && !continue_on_error {
std::process::exit(1);
}
}
CollectionSubcommand::Test {
name,
dir,
spec: spec_path,
auth_profile,
output,
continue_on_error,
} => {
let path = find_collection(&dir, &name)?;
let collection = parse_collection(&path)?;
let spec_path = spec_path.unwrap_or_else(|| PathBuf::from("specs/api.yaml"));
let spec_content = std::fs::read_to_string(&spec_path)?;
let spec = parse_spec(&spec_content)?;
let options = ExecutionOptions {
continue_on_error,
skip_requests: vec![],
only_requests: None,
auth_profile,
variable_overrides: HashMap::new(),
use_env: false,
env_file: None,
save_all: None,
save_summary: None,
};
let auth = None;
let executor = CollectionExecutor::new(spec, auth);
let test_results = executor.execute_as_tests(&collection, options).await?;
match output.as_str() {
"json" => {
println!("{}", serde_json::to_string_pretty(&test_results)?);
}
"junit" => {
let junit_xml = mrapids::collections::testing::to_junit_xml(&test_results);
println!("{}", junit_xml);
}
_ => {
mrapids::collections::testing::print_test_results(&test_results);
}
}
if !test_results.all_passed {
std::process::exit(1);
}
}
}
Ok(())
}
fn handle_plan_command(cmd: cli::PlanCommand) -> Result<()> {
use cli::PlanSubcommand;
use core::plan_sketch;
match cmd.command {
PlanSubcommand::Sketch {
operations,
from,
dir,
spec,
format,
show_gaps,
} => {
let format = if crate::core::output::is_json_mode() {
cli::PlanFormat::Json
} else {
format
};
let sketch = if let Some(collection_name) = from {
plan_sketch::sketch_from_collection(&collection_name, &dir, spec.as_ref())?
} else {
plan_sketch::sketch_from_operations(&operations, spec.as_ref())?
};
plan_sketch::display_sketch(&sketch, &format, show_gaps);
}
}
Ok(())
}
fn generate_preset_policy(preset: &str, spec_path: &std::path::Path) -> Result<String> {
let yaml = match preset.to_lowercase().as_str() {
"hipaa" => format!(
r#"# {preset_upper} Compliance Preset Policy
# Source: {spec}
# Generated: {date}
version: "1.0"
metadata:
name: "hipaa-compliant-policy"
description: "HIPAA-compliant agent access policy"
defaults:
allow_methods: ["GET"]
require_auth: true
audit_level: "detailed"
read_only: true
default_classification: confidential
max_calls_per_session: 50
max_calls_per_minute: 5
warn_on_confidential_to_llm: true
block_regulated_to_llm: true
rules:
- name: "allow-reads-with-audit"
pattern: "*"
allow:
methods: ["GET", "HEAD"]
audit:
level: "detailed"
include_response: true
- name: "deny-all-writes"
pattern: "*"
deny:
methods: ["POST", "PUT", "PATCH", "DELETE"]
explain: "HIPAA: Write operations require human approval and audit trail"
- name: "deny-exports"
pattern: "*"
deny:
operations: ["*export*", "*download*", "*bulk*"]
explain: "HIPAA: Data exports require explicit authorization"
- name: "health-checks"
pattern: "*"
allow:
operations: ["*health*", "*ping*", "*status*"]
# HIPAA requires masking of: SSN, DOB, medical records, diagnosis codes
# The built-in PII masking handles SSN, email, phone, credit cards automatically.
# Add field-level masking in your application for medical-specific fields.
"#,
preset_upper = "HIPAA",
spec = spec_path.display(),
date = chrono::Utc::now().format("%Y-%m-%d"),
),
"pci" => format!(
r#"# {preset_upper} Compliance Preset Policy
# Source: {spec}
# Generated: {date}
version: "1.0"
metadata:
name: "pci-dss-policy"
description: "PCI DSS compliant agent access policy"
defaults:
allow_methods: ["GET"]
require_auth: true
audit_level: "detailed"
read_only: true
default_classification: confidential
max_calls_per_session: 100
max_calls_per_minute: 10
warn_on_confidential_to_llm: true
block_regulated_to_llm: true
rules:
- name: "allow-reads"
pattern: "*"
allow:
methods: ["GET", "HEAD"]
audit:
level: "detailed"
include_response: true
- name: "deny-payment-endpoints"
pattern: "*/payment*"
deny:
all: true
explain: "PCI: Payment endpoints require dedicated secure channel"
- name: "deny-card-operations"
pattern: "*"
deny:
operations: ["*card*", "*payment*", "*billing*", "*charge*", "*refund*"]
explain: "PCI: Card operations blocked for agent access"
- name: "deny-exports"
pattern: "*"
deny:
operations: ["*export*", "*bulk*", "*download*"]
explain: "PCI: Bulk data access requires explicit authorization"
- name: "health-checks"
pattern: "*"
allow:
operations: ["*health*", "*ping*", "*status*"]
"#,
preset_upper = "PCI DSS",
spec = spec_path.display(),
date = chrono::Utc::now().format("%Y-%m-%d"),
),
"sox" => format!(
r#"# {preset_upper} Compliance Preset Policy
# Source: {spec}
# Generated: {date}
version: "1.0"
metadata:
name: "sox-compliant-policy"
description: "SOX compliant agent access policy — separation of duties"
defaults:
allow_methods: ["GET"]
require_auth: true
audit_level: "detailed"
default_classification: internal
max_calls_per_session: 200
max_calls_per_minute: 20
warn_on_confidential_to_llm: true
rules:
- name: "allow-reads"
pattern: "*"
allow:
methods: ["GET", "HEAD", "OPTIONS"]
audit:
level: "detailed"
include_body: false
include_response: true
- name: "deny-financial-writes"
pattern: "*"
deny:
operations: ["*transaction*", "*transfer*", "*payment*", "*invoice*", "*ledger*"]
methods: ["POST", "PUT", "PATCH", "DELETE"]
explain: "SOX: Financial write operations require dual approval"
- name: "deny-admin-access"
pattern: "*"
deny:
operations: ["*admin*", "*config*", "*setting*", "*role*", "*permission*"]
explain: "SOX: Administrative operations require separation of duties"
- name: "health-checks"
pattern: "*"
allow:
operations: ["*health*", "*ping*", "*status*"]
"#,
preset_upper = "SOX",
spec = spec_path.display(),
date = chrono::Utc::now().format("%Y-%m-%d"),
),
unknown => {
anyhow::bail!(
"Unknown preset: \"{}\"\nAvailable presets: hipaa, pci, sox",
unknown
);
}
};
Ok(yaml)
}
fn handle_policy_command(cmd: cli::PolicyCommand) -> Result<()> {
use cli::PolicySubcommand;
match cmd.command {
PolicySubcommand::Init {
spec,
output,
read_only,
preset,
} => {
let spec_path = match spec {
Some(p) => p,
None => find_spec_file()?,
};
let output_path = output.unwrap_or_else(|| PathBuf::from(".mrapids/policy.yaml"));
if output_path.exists() {
eprintln!(
"{} Policy file already exists: {}",
"⚠️".yellow(),
output_path.display()
);
eprintln!(" Use a different --output path or delete the existing file.");
return Ok(());
}
let content = std::fs::read_to_string(&spec_path)?;
let unified_spec = core::parser::parse_spec(&content)?;
if preset.is_some() && read_only {
eprintln!("{} --read-only is ignored when --preset is specified (presets include their own read_only setting)", "⚠".yellow());
}
if let Some(ref preset_name) = preset {
match generate_preset_policy(preset_name, &spec_path) {
Ok(yaml) => {
if let Some(parent) = output_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(&output_path, &yaml)?;
{
use mrapids::core::policy::parser::{
load_policy_from_file, validate_policy,
};
let generated = load_policy_from_file(&output_path)?;
validate_policy(&generated)?;
}
let total = unified_spec.operations.len();
println!(
"{} {} preset policy generated: {}",
"✅".green(),
preset_name.to_uppercase(),
output_path.display()
);
println!();
println!(" {} {} total operations covered", "ℹ".cyan(), total);
println!(
" {} Compliance preset: {}",
"🔒".normal(),
preset_name.to_uppercase()
);
println!();
println!("Next steps:");
println!(" 1. Review: cat {}", output_path.display());
println!(
" 2. Test: mrapids policy validate --policy {}",
output_path.display()
);
println!(
" 3. Use: mrapids mcp serve --policy {}",
output_path.display()
);
return Ok(());
}
Err(e) => {
return Err(e);
}
}
}
let mut tags: Vec<String> = unified_spec
.operations
.iter()
.flat_map(|op| op.tags.iter().cloned())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
tags.sort();
let mut write_ops: Vec<String> = Vec::new();
let mut delete_ops: Vec<String> = Vec::new();
for op in &unified_spec.operations {
match op.method.to_uppercase().as_str() {
"DELETE" => delete_ops.push(op.operation_id.clone()),
"POST" | "PUT" | "PATCH" => write_ops.push(op.operation_id.clone()),
_ => {}
}
}
let total = unified_spec.operations.len();
let read_count = total - write_ops.len() - delete_ops.len();
let mut yaml = String::new();
yaml.push_str("# Auto-generated MCP Agent Policy\n");
yaml.push_str(&format!("# Source: {}\n", spec_path.display()));
yaml.push_str(&format!(
"# Generated: {}\n",
chrono::Utc::now().format("%Y-%m-%d")
));
yaml.push_str(&format!(
"# Operations: {} total ({} read, {} write, {} delete)\n\n",
total,
read_count,
write_ops.len(),
delete_ops.len()
));
yaml.push_str("version: \"1.0\"\n\n");
yaml.push_str("metadata:\n");
yaml.push_str(&format!(
" name: \"{}-agent-policy\"\n",
unified_spec.info.title.to_lowercase().replace(' ', "-")
));
yaml.push_str(&format!(
" description: \"Agent access policy for {}\"\n\n",
unified_spec.info.title
));
yaml.push_str("defaults:\n");
yaml.push_str(" allow_methods: [\"GET\", \"HEAD\", \"OPTIONS\"]\n");
yaml.push_str(" require_auth: false\n");
yaml.push_str(" audit_level: \"basic\"\n");
if read_only {
yaml.push_str(" read_only: true # Block all POST/PUT/PATCH/DELETE globally\n");
}
yaml.push_str(" # max_calls_per_session: 100 # Uncomment to limit total calls\n");
yaml.push_str(" # max_calls_per_minute: 10 # Uncomment to rate-limit\n");
yaml.push_str(
" # default_classification: internal # Default data sensitivity level\n",
);
yaml.push_str(" # warn_on_confidential_to_llm: true # Warn when confidential data returned to agent\n");
yaml.push_str(" # block_regulated_to_llm: true # Block regulated data from reaching agent\n");
yaml.push_str("\n");
yaml.push_str("# classifications:\n");
yaml.push_str("# \"*health*\": public\n");
yaml.push_str("# \"*admin*\": regulated\n");
yaml.push_str("# # Add per-operation classifications here\n");
yaml.push_str("\n");
yaml.push_str("# credential_scopes:\n");
yaml.push_str("# - profile: \"agent-readonly\"\n");
yaml.push_str("# allowed_methods: [\"GET\"]\n");
yaml.push_str("# - profile: \"agent-full\"\n");
yaml.push_str("# allowed_methods: [\"GET\", \"POST\"]\n");
yaml.push_str("# requires_approval: [\"DELETE\"]\n");
yaml.push_str("\n");
yaml.push_str("rules:\n");
yaml.push_str(" # Allow all read operations\n");
yaml.push_str(" - name: \"allow-reads\"\n");
yaml.push_str(" pattern: \"*\"\n");
yaml.push_str(" allow:\n");
yaml.push_str(" methods: [\"GET\", \"HEAD\", \"OPTIONS\"]\n");
yaml.push_str(" audit:\n");
yaml.push_str(" level: \"basic\"\n\n");
if !delete_ops.is_empty() {
yaml.push_str(" # Block all delete operations\n");
yaml.push_str(" - name: \"deny-deletes\"\n");
yaml.push_str(" pattern: \"*\"\n");
yaml.push_str(" deny:\n");
yaml.push_str(" methods: [\"DELETE\"]\n");
yaml.push_str(" explain: \"Delete operations require human approval\"\n\n");
}
if !tags.is_empty() && !read_only {
yaml.push_str(" # Uncomment to allow writes for specific tags\n");
for tag in &tags {
yaml.push_str(&format!(
" # - name: \"allow-{}-writes\"\n",
tag.to_lowercase()
));
yaml.push_str(" # pattern: \"*\"\n");
yaml.push_str(" # allow:\n");
yaml.push_str(" # methods: [\"POST\", \"PUT\", \"PATCH\"]\n");
yaml.push_str(&format!(" # tags: [\"{}\"]\n", tag));
yaml.push_str(" # audit:\n");
yaml.push_str(" # level: \"detailed\"\n");
yaml.push_str(" # include_body: true\n\n");
}
}
yaml.push_str(" # Health checks always allowed\n");
yaml.push_str(" - name: \"health-checks\"\n");
yaml.push_str(" pattern: \"*\"\n");
yaml.push_str(" allow:\n");
yaml.push_str(" operations: [\"*health*\", \"*ping*\", \"*status*\"]\n");
if let Some(parent) = output_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(&output_path, &yaml)?;
{
use mrapids::core::policy::parser::{load_policy_from_file, validate_policy};
let generated = load_policy_from_file(&output_path)?;
validate_policy(&generated)?;
}
println!(
"{} Policy generated: {}",
"✅".green(),
output_path.display()
);
println!();
println!(
" {} {} read operations (auto-allowed)",
"✓".green(),
read_count
);
if !delete_ops.is_empty() {
println!(
" {} {} delete operations (blocked)",
"✗".red(),
delete_ops.len()
);
}
if !write_ops.is_empty() {
println!(
" {} {} write operations (blocked by default)",
"⚠".yellow(),
write_ops.len()
);
}
if !tags.is_empty() {
println!(" {} Tags: {}", "ℹ".cyan(), tags.join(", "));
}
if read_only {
println!(" {} Read-only mode enabled", "🔒".normal());
}
println!();
println!("Next steps:");
println!(" 1. Review: cat {}", output_path.display());
println!(
" 2. Test: mrapids policy validate --policy {}",
output_path.display()
);
println!(
" 3. Use: mrapids mcp serve --policy {}",
output_path.display()
);
}
PolicySubcommand::Validate { policy } => {
use mrapids::core::policy::parser::{load_policy_from_file, validate_policy};
let policy_path = policy.unwrap_or_else(|| {
if PathBuf::from(".mrapids/policy.yaml").exists() {
PathBuf::from(".mrapids/policy.yaml")
} else {
PathBuf::from("policy.yaml")
}
});
println!("Validating policy: {}", policy_path.display());
let policy_set = load_policy_from_file(&policy_path)?;
validate_policy(&policy_set)?;
println!("{} Policy is valid", "✅".green());
println!(" Version: {}", policy_set.version);
println!(" Rules: {}", policy_set.rules.len());
if let Some(meta) = &policy_set.metadata {
println!(" Name: {}", meta.name);
}
}
PolicySubcommand::Report { policy } => {
use mrapids::core::policy::explain::generate_policy_report;
use mrapids::core::policy::parser::load_policy_from_file;
let policy_path = policy.unwrap_or_else(|| {
if PathBuf::from(".mrapids/policy.yaml").exists() {
PathBuf::from(".mrapids/policy.yaml")
} else {
PathBuf::from("policy.yaml")
}
});
let policy_set = load_policy_from_file(&policy_path)?;
if crate::core::output::is_json_mode() {
println!("{}", serde_json::to_string_pretty(&policy_set)?);
} else {
let report = generate_policy_report(&policy_set);
println!("{}", report);
}
}
}
Ok(())
}