pub mod color;
pub mod commands;
pub mod output;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::cli::output::OutputConfig;
#[derive(Parser)]
#[command(
name = "openlatch",
version,
about = "The security layer for AI agents",
after_help = "Run 'openlatch <command> --help' for more information on a command."
)]
pub struct Cli {
#[command(subcommand)]
pub command: 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,
Uninstall(UninstallArgs),
Docs,
Hooks {
#[command(subcommand)]
cmd: HooksCommands,
},
#[command(hide = true)]
Daemon {
#[command(subcommand)]
cmd: DaemonCommands,
},
}
#[derive(Subcommand)]
pub enum HooksCommands {
Install(InitArgs),
Uninstall(UninstallArgs),
Status,
}
#[derive(Subcommand)]
pub enum DaemonCommands {
Start(StartArgs),
Stop,
Restart,
}
#[derive(Args, Clone)]
pub struct InitArgs {
#[arg(long)]
pub foreground: bool,
#[arg(long)]
pub reconfig: bool,
#[arg(long)]
pub no_start: bool,
}
#[derive(Args, Clone)]
pub struct StartArgs {
#[arg(long)]
pub foreground: bool,
#[arg(long)]
pub port: Option<u16>,
}
#[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,
}
#[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", ];
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());
}
}