1pub mod cli;
2pub mod domain;
3pub mod git;
4pub mod mcp;
5pub mod persistence;
6pub mod services;
7pub mod tui;
8
9use anyhow::{Context, Result, anyhow};
10use std::ffi::OsString;
11use std::io::IsTerminal;
12use structopt::StructOpt;
13
14use crate::{
15 cli::{Cli, Command, ReviewCommand},
16 domain::review::ReviewState,
17 git::diff::DiffSource,
18 persistence::store::Store,
19 services::{
20 ai_session::{RunAiSessionInput, default_ai_session_mode, run_ai_session},
21 review_service::{AddCommentInput, AddReplyInput, ReviewService},
22 },
23};
24
25pub async fn run() -> Result<()> {
26 let args: Vec<OsString> = std::env::args_os().collect();
27 let command = if should_run_mcp(&args) {
28 Command::Mcp
29 } else {
30 Cli::from_args().command
31 };
32
33 let project_root =
34 std::env::current_dir().context("failed to read current working directory")?;
35 let store = Store::from_project_root(&project_root);
36 let service = ReviewService::new(store);
37
38 match command {
39 Command::Tui {
40 review,
41 no_mouse,
42 commit,
43 base,
44 head,
45 } => {
46 let diff_source = resolve_tui_diff_source(commit, base, head);
47 tui::run_tui(service, review, no_mouse, diff_source).await?;
48 }
49 Command::Review { command } => {
50 handle_review_command(command, &service).await?;
51 }
52 Command::Mcp => {
53 mcp::run_mcp(service).await?;
54 }
55 }
56
57 Ok(())
58}
59
60fn should_run_mcp(args: &[OsString]) -> bool {
61 if args.len() == 1 && !std::io::stdin().is_terminal() && !std::io::stdout().is_terminal() {
62 return true;
63 }
64
65 let first_arg = args.get(1).and_then(|value| value.to_str());
66 if matches!(first_arg, Some("mcp")) {
67 return true;
68 }
69
70 args.iter()
71 .skip(1)
72 .filter_map(|value| value.to_str())
73 .any(|value| matches!(value, "--stdio" | "--mcp"))
74}
75
76fn resolve_tui_diff_source(
77 commit: Option<String>,
78 base: Option<String>,
79 head: Option<String>,
80) -> DiffSource {
81 if let Some(rev) = commit {
82 DiffSource::Commit { rev }
83 } else if let Some(base) = base {
84 DiffSource::Range {
85 base,
86 head: head.unwrap_or_else(|| "HEAD".to_string()),
87 }
88 } else {
89 DiffSource::WorkingTree
90 }
91}
92
93async fn handle_review_command(command: ReviewCommand, service: &ReviewService) -> Result<()> {
94 match command {
95 ReviewCommand::Create { name } => {
96 let review = service.create_review(&name).await?;
97 println!("created review {} in {:?}", review.name, review.state);
98 }
99 ReviewCommand::Start { name } => {
100 let review = service.set_state(&name, ReviewState::UnderReview).await?;
101 println!("review {} started in {:?}", review.name, review.state);
102 }
103 ReviewCommand::List => {
104 for review_name in service.list_reviews().await? {
105 println!("{review_name}");
106 }
107 }
108 ReviewCommand::Show { name, json } => {
109 let review = service.load_review(&name).await?;
110 if json {
111 println!("{}", serde_json::to_string_pretty(&review)?);
112 } else {
113 println!("name: {}", review.name);
114 println!("state: {:?}", review.state);
115 println!("comments: {}", review.comments.len());
116 for comment in review.comments {
117 println!(
118 " #{} [{}] {}:{} {}",
119 comment.id,
120 match comment.status {
121 crate::domain::review::CommentStatus::Open => "open",
122 crate::domain::review::CommentStatus::Pending => "pending_human",
123 crate::domain::review::CommentStatus::Addressed => "addressed",
124 },
125 comment
126 .old_line
127 .map(|value| value.to_string())
128 .unwrap_or_else(|| "_".into()),
129 comment
130 .new_line
131 .map(|value| value.to_string())
132 .unwrap_or_else(|| "_".into()),
133 comment.body
134 );
135 }
136 }
137 }
138 ReviewCommand::SetState { name, state } => {
139 let review = service.set_state(&name, state.0).await?;
140 println!("state updated to {:?}", review.state);
141 }
142 ReviewCommand::AddComment {
143 name,
144 file,
145 side,
146 old_line,
147 new_line,
148 body,
149 author,
150 } => {
151 if old_line.is_none() && new_line.is_none() {
152 return Err(anyhow!("provide --old-line or --new-line"));
153 }
154
155 let review = service
156 .add_comment(
157 &name,
158 AddCommentInput {
159 file_path: file,
160 old_line,
161 new_line,
162 side: side.0,
163 line_anchor: None,
164 body,
165 author: author.0,
166 },
167 )
168 .await?;
169 println!("comment added. total comments: {}", review.comments.len());
170 }
171 ReviewCommand::AddReply {
172 name,
173 comment_id,
174 body,
175 author,
176 } => {
177 service
178 .add_reply(
179 &name,
180 AddReplyInput {
181 comment_id,
182 author: author.0,
183 body,
184 },
185 )
186 .await?;
187 println!("reply added to comment #{comment_id}");
188 }
189 ReviewCommand::MarkAddressed {
190 name,
191 comment_id,
192 author,
193 } => {
194 service.mark_addressed(&name, comment_id, author.0).await?;
195 println!("comment #{comment_id} marked addressed");
196 }
197 ReviewCommand::MarkOpen {
198 name,
199 comment_id,
200 author,
201 } => {
202 service.mark_open(&name, comment_id, author.0).await?;
203 println!("comment #{comment_id} marked open");
204 }
205 ReviewCommand::Done { name } => {
206 service.set_state(&name, ReviewState::Done).await?;
207 println!("review {name} marked done");
208 }
209 ReviewCommand::Resolve { name } => {
210 service.set_state(&name, ReviewState::Done).await?;
211 println!("review {name} resolved");
212 }
213 ReviewCommand::RunAiSession {
214 name,
215 provider,
216 mode,
217 comment_ids,
218 } => {
219 let mode = mode
220 .map(|value| value.0)
221 .unwrap_or_else(|| default_ai_session_mode(&comment_ids));
222 let result = run_ai_session(
223 service,
224 RunAiSessionInput {
225 review_name: name,
226 provider: provider.0,
227 comment_ids,
228 mode,
229 diff_source: DiffSource::WorkingTree,
230 },
231 )
232 .await?;
233 println!("{}", serde_json::to_string_pretty(&result)?);
234 }
235 }
236
237 Ok(())
238}
239
240#[cfg(test)]
241mod tests {
242 use super::should_run_mcp;
243 use std::ffi::OsString;
244
245 #[test]
246 fn should_run_mcp_when_first_arg_is_mcp() {
247 let args = vec![OsString::from("parley"), OsString::from("mcp")];
248 assert!(should_run_mcp(&args));
249 }
250
251 #[test]
252 fn should_run_mcp_when_stdio_flag_is_present() {
253 let args = vec![OsString::from("parley"), OsString::from("--stdio")];
254 assert!(should_run_mcp(&args));
255 }
256}