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 #[command(subcommand)]
11 pub command: Command,
12}
13
14#[derive(Debug, Parser)]
15pub enum Command {
16 #[command(name = "tui")]
17 Tui {
18 #[arg(long, required = true)]
20 review: Option<String>,
21 #[arg(long)]
23 no_mouse: bool,
24 #[arg(long, conflicts_with_all = &["base", "head"])]
26 commit: Option<String>,
27 #[arg(long, conflicts_with_all = &["commit", "base", "head"])]
29 root: bool,
30 #[arg(long, conflicts_with = "commit")]
32 base: Option<String>,
33 #[arg(long, requires = "base", conflicts_with = "commit")]
35 head: Option<String>,
36 },
37 #[command(name = "review")]
38 Review {
39 #[command(subcommand)]
40 command: ReviewCommand,
41 },
42 #[command(name = "mcp")]
43 Mcp,
44}
45
46#[derive(Debug, Parser)]
47pub enum ReviewCommand {
48 #[command(name = "create")]
49 Create { name: String },
50 #[command(name = "start")]
51 Start { name: String },
52 #[command(name = "list")]
53 List,
54 #[command(name = "show")]
55 Show {
56 name: String,
57 #[arg(long)]
59 json: bool,
60 },
61 #[command(name = "set-state")]
62 SetState { name: String, state: StateArg },
63 #[command(name = "add-comment")]
64 AddComment {
65 name: String,
66 #[arg(long)]
68 file: String,
69 #[arg(long)]
71 side: SideArg,
72 #[arg(long)]
74 old_line: Option<u32>,
75 #[arg(long)]
77 new_line: Option<u32>,
78 #[arg(long)]
80 body: String,
81 #[arg(long, default_value = "user")]
83 author: AuthorArg,
84 },
85 #[command(name = "add-reply")]
86 AddReply {
87 name: String,
88 #[arg(long)]
90 comment_id: u64,
91 #[arg(long)]
93 body: String,
94 #[arg(long, default_value = "ai")]
96 author: AuthorArg,
97 },
98 #[command(name = "mark-addressed")]
99 MarkAddressed {
100 name: String,
101 #[arg(long)]
103 comment_id: u64,
104 #[arg(long, default_value = "user")]
106 author: AuthorArg,
107 },
108 #[command(name = "mark-open")]
109 MarkOpen {
110 name: String,
111 #[arg(long)]
113 comment_id: u64,
114 #[arg(long, default_value = "user")]
116 author: AuthorArg,
117 },
118 #[command(name = "run-ai-session")]
119 RunAiSession {
120 name: String,
121 #[arg(long)]
123 provider: AiProviderArg,
124 #[arg(long)]
126 mode: Option<AiSessionModeArg>,
127 #[arg(long = "comment-id")]
129 comment_ids: Vec<u64>,
130 },
131}
132
133#[cfg(test)]
134mod tests {
135 use super::{Cli, Command};
136 use clap::Parser;
137
138 #[test]
139 fn tui_command_parses_no_mouse_flag() {
140 let cli = Cli::parse_from(["parley", "tui", "--review", "parser-cleanup", "--no-mouse"]);
141
142 match cli.command {
143 Command::Tui {
144 review,
145 no_mouse,
146 commit,
147 root,
148 base,
149 head,
150 } => {
151 assert_eq!(review.as_deref(), Some("parser-cleanup"));
152 assert!(no_mouse);
153 assert_eq!(commit, None);
154 assert!(!root);
155 assert_eq!(base, None);
156 assert_eq!(head, None);
157 }
158 other => panic!("unexpected command: {other:?}"),
159 }
160 }
161
162 #[test]
163 fn tui_command_parses_commit_source() {
164 let cli = Cli::parse_from([
165 "parley",
166 "tui",
167 "--review",
168 "parser-cleanup",
169 "--commit",
170 "HEAD~2",
171 ]);
172
173 match cli.command {
174 Command::Tui {
175 commit,
176 root,
177 base,
178 head,
179 ..
180 } => {
181 assert_eq!(commit.as_deref(), Some("HEAD~2"));
182 assert!(!root);
183 assert_eq!(base, None);
184 assert_eq!(head, None);
185 }
186 other => panic!("unexpected command: {other:?}"),
187 }
188 }
189
190 #[test]
191 fn tui_command_requires_review_name() {
192 let error = Cli::try_parse_from(["parley", "tui", "--commit", "HEAD~2"])
193 .expect_err("cli should require review name");
194
195 let message = error.to_string();
196 assert!(message.contains("--review"));
197 }
198
199 #[test]
200 fn tui_command_rejects_head_without_base() {
201 let error = Cli::try_parse_from(["parley", "tui", "--head", "HEAD~1"])
202 .expect_err("cli should reject head without base");
203
204 let message = error.to_string();
205 assert!(message.contains("--base"));
206 }
207
208 #[test]
209 fn tui_command_rejects_commit_and_base_combination() {
210 let error = Cli::try_parse_from(["parley", "tui", "--commit", "HEAD", "--base", "HEAD~1"])
211 .expect_err("cli should reject conflicting diff sources");
212
213 let message = error.to_string();
214 assert!(message.contains("--commit"));
215 assert!(message.contains("--base"));
216 }
217
218 #[test]
219 fn tui_command_parses_root_source() {
220 let cli = Cli::parse_from(["parley", "tui", "--review", "root-review", "--root"]);
221
222 match cli.command {
223 Command::Tui { review, root, .. } => {
224 assert_eq!(review.as_deref(), Some("root-review"));
225 assert!(root);
226 }
227 other => panic!("unexpected command: {other:?}"),
228 }
229 }
230
231 #[test]
232 fn tui_command_requires_review_name_with_root() {
233 let error = Cli::try_parse_from(["parley", "tui", "--root"])
234 .expect_err("cli should require review name with root");
235
236 let message = error.to_string();
237 assert!(message.contains("--review"));
238 }
239
240 #[test]
241 fn tui_command_rejects_root_and_commit_combination() {
242 let error = Cli::try_parse_from([
243 "parley",
244 "tui",
245 "--review",
246 "root-review",
247 "--root",
248 "--commit",
249 "HEAD",
250 ])
251 .expect_err("cli should reject conflicting root and commit sources");
252
253 let message = error.to_string();
254 assert!(message.contains("--root"));
255 assert!(message.contains("--commit"));
256 }
257}