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 structopt::StructOpt;
11
12use crate::{
13 cli::{Cli, Command, ReviewCommand},
14 domain::review::ReviewState,
15 git::review_name::resolve_tui_review_name,
16 persistence::store::Store,
17 services::{
18 ai_session::{RunAiSessionInput, default_ai_session_mode, run_ai_session},
19 review_service::{AddCommentInput, AddReplyInput, ReviewService},
20 },
21};
22
23pub async fn run() -> Result<()> {
24 let cli = Cli::from_args();
25
26 let project_root =
27 std::env::current_dir().context("failed to read current working directory")?;
28 let store = Store::from_project_root(&project_root);
29 store.ensure_dirs().await?;
30 let service = ReviewService::new(store);
31
32 match cli.command {
33 Command::Tui {
34 review,
35 theme,
36 no_mouse,
37 } => {
38 let review = resolve_default_review_for_tui(&service, review.as_deref()).await?;
39 tui::run_tui(service, review, theme, no_mouse).await?;
40 }
41 Command::Review { command } => {
42 handle_review_command(command, &service).await?;
43 }
44 Command::Mcp => {
45 mcp::run_mcp(service).await?;
46 }
47 }
48
49 Ok(())
50}
51
52async fn resolve_default_review_for_tui(
53 service: &ReviewService,
54 explicit: Option<&str>,
55) -> Result<String> {
56 let resolved = resolve_tui_review_name(explicit)?;
57 if explicit.is_some() {
58 return Ok(resolved);
59 }
60
61 if service.load_review(&resolved).await.is_ok() {
62 return Ok(resolved);
63 }
64
65 let existing = service.list_reviews().await?;
66 if existing.len() == 1 {
67 return Ok(existing[0].clone());
68 }
69
70 Ok(resolved)
71}
72
73async fn handle_review_command(command: ReviewCommand, service: &ReviewService) -> Result<()> {
74 match command {
75 ReviewCommand::Create { name } => {
76 let review = service.create_review(&name).await?;
77 println!("created review {} in {:?}", review.name, review.state);
78 }
79 ReviewCommand::Start { name } => {
80 let review = service.set_state(&name, ReviewState::UnderReview).await?;
81 println!("review {} started in {:?}", review.name, review.state);
82 }
83 ReviewCommand::List => {
84 for review_name in service.list_reviews().await? {
85 println!("{review_name}");
86 }
87 }
88 ReviewCommand::Show { name, json } => {
89 let review = service.load_review(&name).await?;
90 if json {
91 println!("{}", serde_json::to_string_pretty(&review)?);
92 } else {
93 println!("name: {}", review.name);
94 println!("state: {:?}", review.state);
95 println!("comments: {}", review.comments.len());
96 for comment in review.comments {
97 println!(
98 " #{} [{}] {}:{} {}",
99 comment.id,
100 match comment.status {
101 crate::domain::review::CommentStatus::Open => "open",
102 crate::domain::review::CommentStatus::Pending => "pending_human",
103 crate::domain::review::CommentStatus::Addressed => "addressed",
104 },
105 comment
106 .old_line
107 .map(|value| value.to_string())
108 .unwrap_or_else(|| "_".into()),
109 comment
110 .new_line
111 .map(|value| value.to_string())
112 .unwrap_or_else(|| "_".into()),
113 comment.body
114 );
115 }
116 }
117 }
118 ReviewCommand::SetState { name, state } => {
119 let review = service.set_state(&name, state.0).await?;
120 println!("state updated to {:?}", review.state);
121 }
122 ReviewCommand::AddComment {
123 name,
124 file,
125 side,
126 old_line,
127 new_line,
128 body,
129 author,
130 } => {
131 if old_line.is_none() && new_line.is_none() {
132 return Err(anyhow!("provide --old-line or --new-line"));
133 }
134
135 let review = service
136 .add_comment(
137 &name,
138 AddCommentInput {
139 file_path: file,
140 old_line,
141 new_line,
142 side: side.0,
143 body,
144 author: author.0,
145 },
146 )
147 .await?;
148 println!("comment added. total comments: {}", review.comments.len());
149 }
150 ReviewCommand::AddReply {
151 name,
152 comment_id,
153 body,
154 author,
155 } => {
156 service
157 .add_reply(
158 &name,
159 AddReplyInput {
160 comment_id,
161 author: author.0,
162 body,
163 },
164 )
165 .await?;
166 println!("reply added to comment #{comment_id}");
167 }
168 ReviewCommand::MarkAddressed {
169 name,
170 comment_id,
171 author,
172 } => {
173 service.mark_addressed(&name, comment_id, author.0).await?;
174 println!("comment #{comment_id} marked addressed");
175 }
176 ReviewCommand::MarkOpen {
177 name,
178 comment_id,
179 author,
180 } => {
181 service.mark_open(&name, comment_id, author.0).await?;
182 println!("comment #{comment_id} marked open");
183 }
184 ReviewCommand::Done { name } => {
185 service.set_state(&name, ReviewState::Done).await?;
186 println!("review {name} marked done");
187 }
188 ReviewCommand::Resolve { name } => {
189 service.set_state(&name, ReviewState::Done).await?;
190 println!("review {name} resolved");
191 }
192 ReviewCommand::RunAiSession {
193 name,
194 provider,
195 mode,
196 comment_ids,
197 } => {
198 let mode = mode
199 .map(|value| value.0)
200 .unwrap_or_else(|| default_ai_session_mode(&comment_ids));
201 let result = run_ai_session(
202 service,
203 RunAiSessionInput {
204 review_name: name,
205 provider: provider.0,
206 comment_ids,
207 mode,
208 },
209 )
210 .await?;
211 println!("{}", serde_json::to_string_pretty(&result)?);
212 }
213 }
214
215 Ok(())
216}