Skip to main content

parley/cli/
command.rs

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        /// Review name to open in the TUI.
20        #[structopt(long)]
21        review: String,
22        /// Disable mouse capture and mouse interaction in the TUI.
23        #[structopt(long)]
24        no_mouse: bool,
25        /// Show diff for a single commit (against its first parent).
26        #[structopt(long, conflicts_with_all = &["base", "head"])]
27        commit: Option<String>,
28        /// Base revision for an explicit diff range.
29        #[structopt(long, conflicts_with = "commit")]
30        base: Option<String>,
31        /// Head revision for an explicit diff range (defaults to HEAD).
32        #[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        /// Print review details as pretty JSON.
56        #[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        /// File path for the comment location.
65        #[structopt(long)]
66        file: String,
67        /// Diff side for the comment location (`left` or `right`).
68        #[structopt(long)]
69        side: SideArg,
70        /// Line number on the old (left) side of the diff.
71        #[structopt(long)]
72        old_line: Option<u32>,
73        /// Line number on the new (right) side of the diff.
74        #[structopt(long)]
75        new_line: Option<u32>,
76        /// Comment text body.
77        #[structopt(long)]
78        body: String,
79        /// Comment author (`user` or `ai`, default: `user`).
80        #[structopt(long, default_value = "user")]
81        author: AuthorArg,
82    },
83    #[structopt(name = "add-reply")]
84    AddReply {
85        name: String,
86        /// Target comment id to reply to.
87        #[structopt(long)]
88        comment_id: u64,
89        /// Reply text body.
90        #[structopt(long)]
91        body: String,
92        /// Reply author (`user` or `ai`, default: `ai`).
93        #[structopt(long, default_value = "ai")]
94        author: AuthorArg,
95    },
96    #[structopt(name = "mark-addressed")]
97    MarkAddressed {
98        name: String,
99        /// Target comment id to mark as addressed.
100        #[structopt(long)]
101        comment_id: u64,
102        /// Actor marking the comment (`user` or `ai`, default: `user`).
103        #[structopt(long, default_value = "user")]
104        author: AuthorArg,
105    },
106    #[structopt(name = "mark-open")]
107    MarkOpen {
108        name: String,
109        /// Target comment id to mark as open.
110        #[structopt(long)]
111        comment_id: u64,
112        /// Actor reopening the comment (`user` or `ai`, default: `user`).
113        #[structopt(long, default_value = "user")]
114        author: AuthorArg,
115    },
116    #[structopt(name = "run-ai-session")]
117    RunAiSession {
118        name: String,
119        /// AI provider to run for the session.
120        #[structopt(long)]
121        provider: AiProviderArg,
122        /// Session mode override (for example `reply` or `refactor`).
123        #[structopt(long)]
124        mode: Option<AiSessionModeArg>,
125        /// One or more comment ids to target (repeat `--comment-id`).
126        #[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}