1use super::args::{AiProviderArg, AiSessionModeArg, AuthorArg, SideArg, StateArg};
2use clap::Parser;
3
4#[derive(Debug, Parser)]
5#[command(
6 name = "parley",
7 about = "Local AI code review sessions for git changes"
8)]
9pub struct Cli {
10 #[arg(long, global = true)]
12 pub worktree: Option<String>,
13 #[command(subcommand)]
14 pub command: Command,
15}
16
17#[derive(Debug, Parser)]
18pub enum Command {
19 #[command(name = "config")]
20 Config {
21 #[command(subcommand)]
22 command: ConfigCommand,
23 },
24 #[command(name = "tui")]
25 Tui {
26 #[arg(long, required = true)]
28 review: Option<String>,
29 #[arg(long)]
31 no_mouse: bool,
32 #[arg(long, conflicts_with_all = &["base", "head"])]
34 commit: Option<String>,
35 #[arg(long, conflicts_with_all = &["commit", "base", "head"])]
37 root: bool,
38 #[arg(long, conflicts_with = "commit")]
40 base: Option<String>,
41 #[arg(long, requires = "base", conflicts_with = "commit")]
43 head: Option<String>,
44 },
45 #[command(name = "review")]
46 Review {
47 #[command(subcommand)]
48 command: ReviewCommand,
49 },
50 #[command(name = "mcp")]
51 Mcp,
52 #[command(name = "worktree")]
53 Worktree {
54 #[command(subcommand)]
55 command: WorktreeCommand,
56 },
57}
58
59#[derive(Debug, Parser)]
60pub enum ConfigCommand {
61 #[command(name = "path")]
63 Path,
64 #[command(name = "use-local")]
66 UseLocal,
67}
68
69#[derive(Debug, Parser)]
70pub enum ReviewCommand {
71 #[command(name = "create")]
72 Create { name: String },
73 #[command(name = "start")]
74 Start { name: String },
75 #[command(name = "list")]
76 List,
77 #[command(name = "show")]
78 Show {
79 name: String,
80 #[arg(long)]
82 json: bool,
83 },
84 #[command(name = "set-state")]
85 SetState { name: String, state: StateArg },
86 #[command(name = "add-comment")]
87 AddComment {
88 name: String,
89 #[arg(long)]
91 file: String,
92 #[arg(long)]
94 side: SideArg,
95 #[arg(long)]
97 old_line: Option<u32>,
98 #[arg(long)]
100 new_line: Option<u32>,
101 #[arg(long)]
103 body: String,
104 #[arg(long, default_value = "user")]
106 author: AuthorArg,
107 },
108 #[command(name = "add-reply")]
109 AddReply {
110 name: String,
111 #[arg(long)]
113 comment_id: u64,
114 #[arg(long)]
116 body: String,
117 #[arg(long, default_value = "ai")]
119 author: AuthorArg,
120 },
121 #[command(name = "mark-addressed")]
122 MarkAddressed {
123 name: String,
124 #[arg(long)]
126 comment_id: u64,
127 #[arg(long, default_value = "user")]
129 author: AuthorArg,
130 },
131 #[command(name = "mark-open")]
132 MarkOpen {
133 name: String,
134 #[arg(long)]
136 comment_id: u64,
137 #[arg(long, default_value = "user")]
139 author: AuthorArg,
140 },
141 #[command(name = "run-ai-session")]
142 RunAiSession {
143 name: String,
144 #[arg(long)]
146 provider: AiProviderArg,
147 #[arg(long)]
149 mode: Option<AiSessionModeArg>,
150 #[arg(long = "comment-id")]
152 comment_ids: Vec<u64>,
153 },
154}
155
156#[derive(Debug, Parser)]
157pub enum WorktreeCommand {
158 #[command(name = "list")]
160 List,
161 #[command(name = "current")]
163 Current,
164}
165
166#[cfg(test)]
167mod tests {
168 use super::{Cli, Command};
169 use clap::Parser;
170
171 #[test]
172 fn tui_command_parses_no_mouse_flag() {
173 let cli = Cli::parse_from(["parley", "tui", "--review", "parser-cleanup", "--no-mouse"]);
174
175 match cli.command {
176 Command::Tui {
177 review,
178 no_mouse,
179 commit,
180 root,
181 base,
182 head,
183 } => {
184 assert_eq!(review.as_deref(), Some("parser-cleanup"));
185 assert!(no_mouse);
186 assert_eq!(commit, None);
187 assert!(!root);
188 assert_eq!(base, None);
189 assert_eq!(head, None);
190 }
191 other => panic!("unexpected command: {other:?}"),
192 }
193 }
194
195 #[test]
196 fn tui_command_parses_commit_source() {
197 let cli = Cli::parse_from([
198 "parley",
199 "tui",
200 "--review",
201 "parser-cleanup",
202 "--commit",
203 "HEAD~2",
204 ]);
205
206 match cli.command {
207 Command::Tui {
208 commit,
209 root,
210 base,
211 head,
212 ..
213 } => {
214 assert_eq!(commit.as_deref(), Some("HEAD~2"));
215 assert!(!root);
216 assert_eq!(base, None);
217 assert_eq!(head, None);
218 }
219 other => panic!("unexpected command: {other:?}"),
220 }
221 }
222
223 #[test]
224 fn tui_command_requires_review_name() {
225 let error = Cli::try_parse_from(["parley", "tui", "--commit", "HEAD~2"])
226 .expect_err("cli should require review name");
227
228 let message = error.to_string();
229 assert!(message.contains("--review"));
230 }
231
232 #[test]
233 fn tui_command_rejects_head_without_base() {
234 let error = Cli::try_parse_from(["parley", "tui", "--head", "HEAD~1"])
235 .expect_err("cli should reject head without base");
236
237 let message = error.to_string();
238 assert!(message.contains("--base"));
239 }
240
241 #[test]
242 fn tui_command_rejects_commit_and_base_combination() {
243 let error = Cli::try_parse_from(["parley", "tui", "--commit", "HEAD", "--base", "HEAD~1"])
244 .expect_err("cli should reject conflicting diff sources");
245
246 let message = error.to_string();
247 assert!(message.contains("--commit"));
248 assert!(message.contains("--base"));
249 }
250
251 #[test]
252 fn tui_command_parses_root_source() {
253 let cli = Cli::parse_from(["parley", "tui", "--review", "root-review", "--root"]);
254
255 match cli.command {
256 Command::Tui { review, root, .. } => {
257 assert_eq!(review.as_deref(), Some("root-review"));
258 assert!(root);
259 }
260 other => panic!("unexpected command: {other:?}"),
261 }
262 }
263
264 #[test]
265 fn tui_command_requires_review_name_with_root() {
266 let error = Cli::try_parse_from(["parley", "tui", "--root"])
267 .expect_err("cli should require review name with root");
268
269 let message = error.to_string();
270 assert!(message.contains("--review"));
271 }
272
273 #[test]
274 fn tui_command_rejects_root_and_commit_combination() {
275 let error = Cli::try_parse_from([
276 "parley",
277 "tui",
278 "--review",
279 "root-review",
280 "--root",
281 "--commit",
282 "HEAD",
283 ])
284 .expect_err("cli should reject conflicting root and commit sources");
285
286 let message = error.to_string();
287 assert!(message.contains("--root"));
288 assert!(message.contains("--commit"));
289 }
290
291 #[test]
292 fn config_command_parses_use_local() {
293 let cli = Cli::parse_from(["parley", "config", "use-local"]);
294
295 assert!(matches!(
296 cli.command,
297 Command::Config {
298 command: super::ConfigCommand::UseLocal
299 }
300 ));
301 }
302
303 #[test]
304 fn worktree_list_command_parses() {
305 let cli = Cli::parse_from(["parley", "worktree", "list"]);
306
307 assert!(matches!(
308 cli.command,
309 Command::Worktree {
310 command: super::WorktreeCommand::List
311 }
312 ));
313 }
314
315 #[test]
316 fn worktree_current_command_parses() {
317 let cli = Cli::parse_from(["parley", "worktree", "current"]);
318
319 assert!(matches!(
320 cli.command,
321 Command::Worktree {
322 command: super::WorktreeCommand::Current
323 }
324 ));
325 }
326}