openlatch_client/cli/
mod.rs1pub mod color;
15pub mod commands;
16pub mod output;
17
18use clap::{Args, Parser, Subcommand, ValueEnum};
19
20use crate::cli::output::OutputConfig;
21
22#[derive(Parser)]
24#[command(
25 name = "openlatch",
26 version,
27 about = "The security layer for AI agents",
28 after_help = "Run 'openlatch <command> --help' for more information on a command."
29)]
30pub struct Cli {
31 #[command(subcommand)]
32 pub command: Commands,
33
34 #[arg(long, global = true, default_value = "human")]
36 pub format: OutputFormat,
37
38 #[arg(long, global = true)]
40 pub json: bool,
41
42 #[arg(long, short = 'v', global = true)]
44 pub verbose: bool,
45
46 #[arg(long, global = true)]
48 pub debug: bool,
49
50 #[arg(long, short = 'q', global = true)]
52 pub quiet: bool,
53
54 #[arg(long, global = true)]
56 pub no_color: bool,
57}
58
59#[derive(Clone, ValueEnum)]
61pub enum OutputFormat {
62 Human,
63 Json,
64}
65
66#[derive(Subcommand)]
68pub enum Commands {
69 #[command(visible_alias = "setup")]
71 Init(InitArgs),
72
73 Status,
75
76 Start(StartArgs),
78
79 Stop,
81
82 Restart,
84
85 Logs(LogsArgs),
87
88 Doctor,
90
91 Uninstall(UninstallArgs),
93
94 Docs,
96
97 Hooks {
99 #[command(subcommand)]
100 cmd: HooksCommands,
101 },
102
103 #[command(hide = true)]
105 Daemon {
106 #[command(subcommand)]
107 cmd: DaemonCommands,
108 },
109}
110
111#[derive(Subcommand)]
113pub enum HooksCommands {
114 Install(InitArgs),
116 Uninstall(UninstallArgs),
118 Status,
120}
121
122#[derive(Subcommand)]
124pub enum DaemonCommands {
125 Start(StartArgs),
127 Stop,
129 Restart,
131}
132
133#[derive(Args, Clone)]
135pub struct InitArgs {
136 #[arg(long)]
138 pub foreground: bool,
139 #[arg(long)]
141 pub reconfig: bool,
142 #[arg(long)]
144 pub no_start: bool,
145}
146
147#[derive(Args, Clone)]
149pub struct StartArgs {
150 #[arg(long)]
152 pub foreground: bool,
153 #[arg(long)]
155 pub port: Option<u16>,
156}
157
158#[derive(Args, Clone)]
160pub struct LogsArgs {
161 #[arg(long, short = 'f')]
163 pub follow: bool,
164
165 #[arg(long)]
167 pub since: Option<String>,
168
169 #[arg(long, short = 'n', default_value = "20")]
171 pub lines: usize,
172}
173
174#[derive(Args, Clone)]
176pub struct UninstallArgs {
177 #[arg(long)]
179 pub purge: bool,
180
181 #[arg(long, short = 'y')]
183 pub yes: bool,
184}
185
186const KNOWN_SUBCOMMANDS: &[&str] = &[
188 "init",
189 "status",
190 "start",
191 "stop",
192 "restart",
193 "logs",
194 "doctor",
195 "uninstall",
196 "docs",
197 "hooks",
198 "daemon",
199 "setup", ];
201
202pub fn suggest_subcommand(input: &str) -> Option<String> {
215 let mut best_name = "";
216 let mut best_score = 0.0_f64;
217
218 for &name in KNOWN_SUBCOMMANDS {
219 let score = strsim::jaro_winkler(input, name);
220 if score > best_score {
221 best_score = score;
222 best_name = name;
223 }
224 }
225
226 if best_score > 0.7 && !best_name.is_empty() {
227 Some(best_name.to_string())
228 } else {
229 None
230 }
231}
232
233pub fn build_output_config(cli: &Cli) -> OutputConfig {
238 let format = if cli.json {
239 output::OutputFormat::Json
240 } else {
241 match cli.format {
242 OutputFormat::Json => output::OutputFormat::Json,
243 OutputFormat::Human => output::OutputFormat::Human,
244 }
245 };
246
247 let color_enabled = color::is_color_enabled(cli.no_color);
248
249 OutputConfig {
250 format,
251 verbose: cli.verbose || cli.debug,
252 debug: cli.debug,
253 quiet: cli.quiet,
254 color: color_enabled,
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_suggest_subcommand_close_match() {
264 let suggestion = suggest_subcommand("stats");
266 assert_eq!(suggestion, Some("status".to_string()));
267 }
268
269 #[test]
270 fn test_suggest_subcommand_exact_match() {
271 let suggestion = suggest_subcommand("init");
272 assert_eq!(suggestion, Some("init".to_string()));
273 }
274
275 #[test]
276 fn test_suggest_subcommand_no_match() {
277 let suggestion = suggest_subcommand("xyz");
279 assert!(suggestion.is_none());
280 }
281
282 #[test]
283 fn test_suggest_subcommand_typo() {
284 let suggestion = suggest_subcommand("unitstall");
286 assert!(suggestion.is_some());
288 }
289}