pub mod color;
pub mod commands;
pub mod header;
pub mod output;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::cli::output::OutputConfig;
pub const BANNER: &str = "\
\x1b[36m▄▄▄ OpenLatch ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\x1b[0m
hook events → envelope → cloud
localhost:7443 · fail-open
";
pub const BANNER_PLAIN: &str = "\
=== OpenLatch =============================
hook events -> envelope -> cloud
localhost:7443 · fail-open
";
#[derive(Parser)]
#[command(
name = "openlatch",
version,
about = "The security layer for AI agents",
before_help = BANNER_PLAIN,
after_help = "Run 'openlatch <command> --help' for more information on a command."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(long, global = true, default_value = "human")]
pub format: OutputFormat,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub debug: bool,
#[arg(long, short = 'q', global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
Human,
Json,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(visible_alias = "setup")]
Init(InitArgs),
Status,
Start(StartArgs),
Stop,
Restart,
Logs(LogsArgs),
Doctor(DoctorArgs),
Uninstall(UninstallArgs),
Docs,
Hooks {
#[command(subcommand)]
cmd: HooksCommands,
},
#[command(hide = true)]
Daemon {
#[command(subcommand)]
cmd: DaemonCommands,
},
Auth {
#[command(subcommand)]
cmd: AuthCommands,
},
Telemetry {
#[command(subcommand)]
cmd: TelemetryCommands,
},
Supervision {
#[command(subcommand)]
cmd: SupervisionCommands,
},
}
#[derive(Subcommand)]
pub enum TelemetryCommands {
Status,
Enable,
Disable,
Purge,
Debug,
}
#[derive(Subcommand)]
pub enum SupervisionCommands {
Install,
Uninstall,
Status,
Enable,
Disable,
}
#[derive(Subcommand)]
pub enum HooksCommands {
Install(InitArgs),
Uninstall(UninstallArgs),
Status,
}
#[derive(Subcommand)]
pub enum DaemonCommands {
Start(StartArgs),
Stop,
Restart,
}
#[derive(Subcommand)]
pub enum AuthCommands {
Login(AuthLoginArgs),
Logout,
Status,
}
#[derive(Args, Clone)]
pub struct AuthLoginArgs {
#[arg(long)]
pub no_browser: bool,
}
#[derive(Args, Clone)]
pub struct InitArgs {
#[arg(long)]
pub foreground: bool,
#[arg(long)]
pub reconfig: bool,
#[arg(long)]
pub no_start: bool,
#[arg(long, alias = "yes-telemetry", conflicts_with = "no_telemetry")]
pub telemetry: bool,
#[arg(long)]
pub no_telemetry: bool,
#[arg(long)]
pub no_persistence: bool,
}
#[derive(Args, Clone)]
pub struct StartArgs {
#[arg(long)]
pub foreground: bool,
#[arg(long)]
pub port: Option<u16>,
}
#[derive(Args, Clone, Default)]
pub struct DoctorArgs {
#[arg(long, hide = true)]
pub trigger_panic: bool,
#[arg(long, conflicts_with = "restore")]
pub fix: bool,
#[arg(long, conflicts_with = "fix")]
pub restore: bool,
#[arg(long)]
pub rescue: bool,
#[arg(long, value_name = "DURATION", requires = "rescue")]
pub since: Option<String>,
#[arg(long, short = 'y', requires = "rescue")]
pub yes: bool,
#[arg(long, value_name = "PATH", requires = "rescue")]
pub output: Option<std::path::PathBuf>,
}
#[derive(Args, Clone)]
pub struct LogsArgs {
#[arg(long, short = 'f')]
pub follow: bool,
#[arg(long)]
pub since: Option<String>,
#[arg(long, short = 'n', default_value = "20")]
pub lines: usize,
#[arg(long)]
pub tamper: bool,
}
#[derive(Args, Clone)]
pub struct UninstallArgs {
#[arg(long)]
pub purge: bool,
#[arg(long, short = 'y')]
pub yes: bool,
}
const KNOWN_SUBCOMMANDS: &[&str] = &[
"init",
"status",
"start",
"stop",
"restart",
"logs",
"doctor",
"uninstall",
"docs",
"hooks",
"daemon",
"setup", "auth",
"login",
"logout",
"telemetry",
"supervision",
];
pub fn suggest_subcommand(input: &str) -> Option<String> {
let mut best_name = "";
let mut best_score = 0.0_f64;
for &name in KNOWN_SUBCOMMANDS {
let score = strsim::jaro_winkler(input, name);
if score > best_score {
best_score = score;
best_name = name;
}
}
if best_score > 0.7 && !best_name.is_empty() {
Some(best_name.to_string())
} else {
None
}
}
pub fn build_output_config(cli: &Cli) -> OutputConfig {
let format = if cli.json {
output::OutputFormat::Json
} else {
match cli.format {
OutputFormat::Json => output::OutputFormat::Json,
OutputFormat::Human => output::OutputFormat::Human,
}
};
let color_enabled = color::is_color_enabled(cli.no_color);
OutputConfig {
format,
verbose: cli.verbose || cli.debug,
debug: cli.debug,
quiet: cli.quiet,
color: color_enabled,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_suggest_subcommand_close_match() {
let suggestion = suggest_subcommand("stats");
assert_eq!(suggestion, Some("status".to_string()));
}
#[test]
fn test_suggest_subcommand_exact_match() {
let suggestion = suggest_subcommand("init");
assert_eq!(suggestion, Some("init".to_string()));
}
#[test]
fn test_suggest_subcommand_no_match() {
let suggestion = suggest_subcommand("xyz");
assert!(suggestion.is_none());
}
#[test]
fn test_suggest_subcommand_typo() {
let suggestion = suggest_subcommand("unitstall");
assert!(suggestion.is_some());
}
}