1use std::path::PathBuf;
4
5use clap::{Args, Parser, Subcommand};
6
7#[derive(Debug, Parser)]
9#[command(name = "talon", version, about, long_about = None)]
10pub struct Cli {
11 #[arg(long, global = true, env = "TALON_PROJECT_ROOT", value_name = "DIR")]
14 pub project_root: Option<PathBuf>,
15
16 #[command(subcommand)]
18 pub command: Option<Command>,
19}
20
21#[derive(Debug, Subcommand, PartialEq, Eq)]
23pub enum Command {
24 Run,
26 Setup(SetupArgs),
29 Doctor,
32 Audit {
34 #[command(subcommand)]
36 command: AuditCommand,
37 },
38 Version,
40}
41
42#[derive(Debug, Args, PartialEq, Eq)]
44pub struct SetupArgs {
45 pub step: String,
47 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
49 pub args: Vec<String>,
50}
51
52#[derive(Debug, Subcommand, PartialEq, Eq)]
54pub enum AuditCommand {
55 Verify,
57 Tail {
59 #[arg(short = 'n', long = "lines", default_value_t = 50)]
61 lines: usize,
62 #[arg(long)]
64 kind: Option<String>,
65 },
66}
67
68impl Cli {
69 pub fn command(&self) -> &Command {
71 self.command.as_ref().unwrap_or(&Command::Run)
72 }
73
74 pub fn resolve_project_root(&self) -> std::io::Result<PathBuf> {
77 match &self.project_root {
78 Some(p) if p.is_absolute() => Ok(p.clone()),
79 Some(p) => Ok(std::env::current_dir()?.join(p)),
80 None => std::env::current_dir(),
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn parse(args: &[&str]) -> Cli {
90 Cli::try_parse_from(args).unwrap()
91 }
92
93 #[test]
94 fn defaults_to_run() {
95 let cli = parse(&["talon"]);
96 assert_eq!(cli.command(), &Command::Run);
97 assert_eq!(parse(&["talon", "run"]).command(), &Command::Run);
98 }
99
100 #[test]
101 fn setup_passes_hyphenated_args_through() {
102 let cli = parse(&[
103 "talon",
104 "setup",
105 "timezone",
106 "--tz",
107 "Europe/Berlin",
108 "--force",
109 ]);
110 assert_eq!(
111 cli.command(),
112 &Command::Setup(SetupArgs {
113 step: "timezone".into(),
114 args: vec!["--tz".into(), "Europe/Berlin".into(), "--force".into()],
115 })
116 );
117 }
118
119 #[test]
120 fn audit_subcommands() {
121 assert_eq!(
122 parse(&["talon", "audit", "verify"]).command(),
123 &Command::Audit {
124 command: AuditCommand::Verify
125 }
126 );
127 assert_eq!(
128 parse(&["talon", "audit", "tail", "-n", "5", "--kind", "ipc."]).command(),
129 &Command::Audit {
130 command: AuditCommand::Tail {
131 lines: 5,
132 kind: Some("ipc.".into())
133 }
134 }
135 );
136 assert_eq!(
137 parse(&["talon", "audit", "tail"]).command(),
138 &Command::Audit {
139 command: AuditCommand::Tail {
140 lines: 50,
141 kind: None
142 }
143 }
144 );
145 }
146
147 #[test]
148 fn project_root_flag_is_global() {
149 let cli = parse(&["talon", "doctor", "--project-root", "/srv/talon"]);
150 assert_eq!(cli.command(), &Command::Doctor);
151 assert_eq!(
152 cli.resolve_project_root().unwrap(),
153 PathBuf::from("/srv/talon")
154 );
155 }
156
157 #[test]
158 fn unknown_command_is_rejected() {
159 assert!(Cli::try_parse_from(["talon", "frobnicate"]).is_err());
160 }
161}