1use structopt::StructOpt;
2
3use super::args::{AiProviderArg, AiSessionModeArg, AuthorArg, SideArg, StateArg};
4
5#[derive(Debug, StructOpt)]
6#[structopt(
7 name = "parley",
8 about = "Local AI code review sessions for git changes"
9)]
10pub struct Cli {
11 #[structopt(subcommand)]
12 pub command: Command,
13}
14
15#[derive(Debug, StructOpt)]
16pub enum Command {
17 #[structopt(name = "tui")]
18 Tui {
19 #[structopt(long)]
21 review: String,
22 #[structopt(long)]
24 no_mouse: bool,
25 #[structopt(long, conflicts_with_all = &["base", "head"])]
27 commit: Option<String>,
28 #[structopt(long, conflicts_with = "commit")]
30 base: Option<String>,
31 #[structopt(long, requires = "base", conflicts_with = "commit")]
33 head: Option<String>,
34 },
35 #[structopt(name = "review")]
36 Review {
37 #[structopt(subcommand)]
38 command: ReviewCommand,
39 },
40 #[structopt(name = "mcp")]
41 Mcp,
42}
43
44#[derive(Debug, StructOpt)]
45pub enum ReviewCommand {
46 #[structopt(name = "create")]
47 Create { name: String },
48 #[structopt(name = "start")]
49 Start { name: String },
50 #[structopt(name = "list")]
51 List,
52 #[structopt(name = "show")]
53 Show {
54 name: String,
55 #[structopt(long)]
57 json: bool,
58 },
59 #[structopt(name = "set-state")]
60 SetState { name: String, state: StateArg },
61 #[structopt(name = "add-comment")]
62 AddComment {
63 name: String,
64 #[structopt(long)]
66 file: String,
67 #[structopt(long)]
69 side: SideArg,
70 #[structopt(long)]
72 old_line: Option<u32>,
73 #[structopt(long)]
75 new_line: Option<u32>,
76 #[structopt(long)]
78 body: String,
79 #[structopt(long, default_value = "user")]
81 author: AuthorArg,
82 },
83 #[structopt(name = "add-reply")]
84 AddReply {
85 name: String,
86 #[structopt(long)]
88 comment_id: u64,
89 #[structopt(long)]
91 body: String,
92 #[structopt(long, default_value = "ai")]
94 author: AuthorArg,
95 },
96 #[structopt(name = "mark-addressed")]
97 MarkAddressed {
98 name: String,
99 #[structopt(long)]
101 comment_id: u64,
102 #[structopt(long, default_value = "user")]
104 author: AuthorArg,
105 },
106 #[structopt(name = "mark-open")]
107 MarkOpen {
108 name: String,
109 #[structopt(long)]
111 comment_id: u64,
112 #[structopt(long, default_value = "user")]
114 author: AuthorArg,
115 },
116 #[structopt(name = "run-ai-session")]
117 RunAiSession {
118 name: String,
119 #[structopt(long)]
121 provider: AiProviderArg,
122 #[structopt(long)]
124 mode: Option<AiSessionModeArg>,
125 #[structopt(long = "comment-id")]
127 comment_ids: Vec<u64>,
128 },
129 #[structopt(name = "done")]
130 Done { name: String },
131 #[structopt(name = "resolve")]
132 Resolve { name: String },
133}
134
135#[cfg(test)]
136mod tests {
137 use structopt::StructOpt;
138
139 use super::{Cli, Command};
140
141 #[test]
142 fn tui_command_parses_no_mouse_flag() {
143 let cli =
144 Cli::from_iter_safe(["parley", "tui", "--review", "parser-cleanup", "--no-mouse"])
145 .expect("cli should parse");
146
147 match cli.command {
148 Command::Tui {
149 review,
150 no_mouse,
151 commit,
152 base,
153 head,
154 } => {
155 assert_eq!(review, "parser-cleanup");
156 assert!(no_mouse);
157 assert_eq!(commit, None);
158 assert_eq!(base, None);
159 assert_eq!(head, None);
160 }
161 other => panic!("unexpected command: {other:?}"),
162 }
163 }
164
165 #[test]
166 fn tui_command_parses_commit_source() {
167 let cli = Cli::from_iter_safe([
168 "parley",
169 "tui",
170 "--review",
171 "parser-cleanup",
172 "--commit",
173 "HEAD~2",
174 ])
175 .expect("cli should parse");
176
177 match cli.command {
178 Command::Tui {
179 commit, base, head, ..
180 } => {
181 assert_eq!(commit.as_deref(), Some("HEAD~2"));
182 assert_eq!(base, None);
183 assert_eq!(head, None);
184 }
185 other => panic!("unexpected command: {other:?}"),
186 }
187 }
188
189 #[test]
190 fn tui_command_requires_review_name() {
191 let error = Cli::from_iter_safe(["parley", "tui", "--commit", "HEAD~2"])
192 .expect_err("cli should require review name");
193
194 let message = error.to_string();
195 assert!(message.contains("--review"));
196 }
197
198 #[test]
199 fn tui_command_rejects_head_without_base() {
200 let error = Cli::from_iter_safe(["parley", "tui", "--head", "HEAD~1"])
201 .expect_err("cli should reject head without base");
202
203 let message = error.to_string();
204 assert!(message.contains("--base"));
205 }
206
207 #[test]
208 fn tui_command_rejects_commit_and_base_combination() {
209 let error = Cli::from_iter_safe(["parley", "tui", "--commit", "HEAD", "--base", "HEAD~1"])
210 .expect_err("cli should reject conflicting diff sources");
211
212 let message = error.to_string();
213 assert!(message.contains("--commit"));
214 assert!(message.contains("--base"));
215 }
216}