1use crate::changes;
2use crate::commands;
3use crate::commit;
4use crate::common::CommonParams;
5use crate::llm::get_available_provider_names;
6use crate::log_debug;
7use crate::ui;
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{Parser, Subcommand, crate_version};
10use colored::Colorize;
11
12const LOG_FILE: &str = "git-iris-debug.log";
13
14#[derive(Parser)]
16#[command(
17 author,
18 version = crate_version!(),
19 about = "Git-Iris: AI-powered Git workflow assistant",
20 long_about = "Git-Iris enhances your Git workflow with AI-assisted commit messages, code reviews, changelogs, and more.",
21 disable_version_flag = true,
22 after_help = get_dynamic_help(),
23 styles = get_styles(),
24)]
25pub struct Cli {
26 #[command(subcommand)]
28 pub command: Option<Commands>,
29
30 #[arg(
32 short = 'l',
33 long = "log",
34 global = true,
35 help = "Log debug messages to a file"
36 )]
37 pub log: bool,
38
39 #[arg(
41 long = "log-file",
42 global = true,
43 help = "Specify a custom log file path"
44 )]
45 pub log_file: Option<String>,
46
47 #[arg(
49 short = 'q',
50 long = "quiet",
51 global = true,
52 help = "Suppress non-essential output"
53 )]
54 pub quiet: bool,
55
56 #[arg(
58 short = 'v',
59 long = "version",
60 global = true,
61 help = "Display the version"
62 )]
63 pub version: bool,
64
65 #[arg(
67 short = 'r',
68 long = "repo",
69 global = true,
70 help = "Repository URL to use instead of local repository"
71 )]
72 pub repository_url: Option<String>,
73}
74
75#[derive(Subcommand)]
77#[command(subcommand_negates_reqs = true)]
78#[command(subcommand_precedence_over_arg = true)]
79pub enum Commands {
80 #[command(
83 about = "Generate a commit message using AI",
84 long_about = "Generate a commit message using AI based on the current Git context.",
85 after_help = get_dynamic_help()
86 )]
87 Gen {
88 #[command(flatten)]
89 common: CommonParams,
90
91 #[arg(short, long, help = "Automatically commit with the generated message")]
93 auto_commit: bool,
94
95 #[arg(long, help = "Disable Gitmoji for this commit")]
97 no_gitmoji: bool,
98
99 #[arg(short, long, help = "Print the generated message to stdout and exit")]
101 print: bool,
102
103 #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
105 no_verify: bool,
106 },
107
108 #[command(
110 about = "Review staged changes using AI",
111 long_about = "Generate a comprehensive multi-dimensional code review of staged changes using AI. Analyzes code across 10 dimensions including complexity, security, performance, and more."
112 )]
113 Review {
114 #[command(flatten)]
115 common: CommonParams,
116
117 #[arg(short, long, help = "Print the generated review to stdout and exit")]
119 print: bool,
120
121 #[arg(long, help = "Include unstaged changes in the review")]
123 include_unstaged: bool,
124
125 #[arg(
127 long,
128 help = "Review a specific commit by ID (hash, branch, or reference)"
129 )]
130 commit: Option<String>,
131 },
132
133 #[command(
135 about = "Generate a changelog",
136 long_about = "Generate a changelog between two specified Git references."
137 )]
138 Changelog {
139 #[command(flatten)]
140 common: CommonParams,
141
142 #[arg(long, required = true)]
144 from: String,
145
146 #[arg(long)]
148 to: Option<String>,
149
150 #[arg(long, help = "Update the changelog file with the new changes")]
152 update: bool,
153
154 #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
156 file: Option<String>,
157
158 #[arg(long, help = "Explicit version name to use in the changelog")]
160 version_name: Option<String>,
161 },
162
163 #[command(
165 about = "Generate release notes",
166 long_about = "Generate comprehensive release notes between two specified Git references."
167 )]
168 ReleaseNotes {
169 #[command(flatten)]
170 common: CommonParams,
171
172 #[arg(long, required = true)]
174 from: String,
175
176 #[arg(long)]
178 to: Option<String>,
179
180 #[arg(long, help = "Explicit version name to use in the release notes")]
182 version_name: Option<String>,
183 },
184
185 #[command(
187 about = "Start an MCP server",
188 long_about = "Start a Model Context Protocol (MCP) server to provide Git-Iris functionality to AI tools and assistants."
189 )]
190 Serve {
191 #[arg(long, help = "Enable development mode with more verbose logging")]
193 dev: bool,
194
195 #[arg(
197 short,
198 long,
199 help = "Transport type to use (stdio, sse)",
200 default_value = "stdio"
201 )]
202 transport: String,
203
204 #[arg(short, long, help = "Port to use for network transports")]
206 port: Option<u16>,
207
208 #[arg(
210 long,
211 help = "Listen address for network transports (e.g., '127.0.0.1', '0.0.0.0')",
212 default_value = "127.0.0.1"
213 )]
214 listen_address: Option<String>,
215 },
216
217 #[command(about = "Configure Git-Iris settings and providers")]
220 Config {
221 #[command(flatten)]
222 common: CommonParams,
223
224 #[arg(long, help = "Set API key for the specified provider")]
226 api_key: Option<String>,
227
228 #[arg(long, help = "Set model for the specified provider")]
230 model: Option<String>,
231
232 #[arg(long, help = "Set token limit for the specified provider")]
234 token_limit: Option<usize>,
235
236 #[arg(
238 long,
239 help = "Set additional parameters for the specified provider (key=value)"
240 )]
241 param: Option<Vec<String>>,
242 },
243
244 #[command(
246 about = "Manage project-specific configuration",
247 long_about = "Create or update a project-specific .irisconfig file in the repository root."
248 )]
249 ProjectConfig {
250 #[command(flatten)]
251 common: CommonParams,
252
253 #[arg(long, help = "Set model for the specified provider")]
255 model: Option<String>,
256
257 #[arg(long, help = "Set token limit for the specified provider")]
259 token_limit: Option<usize>,
260
261 #[arg(
263 long,
264 help = "Set additional parameters for the specified provider (key=value)"
265 )]
266 param: Option<Vec<String>>,
267
268 #[arg(short, long, help = "Print the current project configuration")]
270 print: bool,
271 },
272
273 #[command(about = "List available instruction presets")]
275 ListPresets,
276}
277
278fn get_styles() -> Styles {
280 Styles::styled()
281 .header(AnsiColor::Magenta.on_default().bold())
282 .usage(AnsiColor::Cyan.on_default().bold())
283 .literal(AnsiColor::Green.on_default().bold())
284 .placeholder(AnsiColor::Yellow.on_default())
285 .valid(AnsiColor::Blue.on_default().bold())
286 .invalid(AnsiColor::Red.on_default().bold())
287 .error(AnsiColor::Red.on_default().bold())
288}
289
290pub fn parse_args() -> Cli {
292 Cli::parse()
293}
294
295fn get_dynamic_help() -> String {
297 let mut providers = get_available_provider_names();
298 providers.sort(); let providers_list = providers
301 .iter()
302 .map(|p| format!("{}", p.bold()))
303 .collect::<Vec<_>>()
304 .join(" • ");
305
306 format!("\nAvailable LLM Providers: {providers_list}")
307}
308
309pub async fn main() -> anyhow::Result<()> {
311 let cli = parse_args();
312
313 if cli.version {
314 ui::print_version(crate_version!());
315 return Ok(());
316 }
317
318 if cli.log {
319 crate::logger::enable_logging();
320 let log_file = cli.log_file.as_deref().unwrap_or(LOG_FILE);
321 crate::logger::set_log_file(log_file)?;
322 } else {
323 crate::logger::disable_logging();
324 }
325
326 if cli.quiet {
328 crate::ui::set_quiet_mode(true);
329 }
330
331 if let Some(command) = cli.command {
332 handle_command(command, cli.repository_url).await
333 } else {
334 let _ = Cli::parse_from(["git-iris", "--help"]);
336 Ok(())
337 }
338}
339
340#[allow(clippy::struct_excessive_bools)]
342struct GenConfig {
343 auto_commit: bool,
344 use_gitmoji: bool,
345 print_only: bool,
346 verify: bool,
347}
348
349async fn handle_gen(
351 common: CommonParams,
352 config: GenConfig,
353 repository_url: Option<String>,
354) -> anyhow::Result<()> {
355 log_debug!(
356 "Handling 'gen' command with common: {:?}, auto_commit: {}, use_gitmoji: {}, print: {}, verify: {}",
357 common,
358 config.auto_commit,
359 config.use_gitmoji,
360 config.print_only,
361 config.verify
362 );
363
364 ui::print_version(crate_version!());
365 println!();
366
367 commit::handle_gen_command(
368 common,
369 config.auto_commit,
370 config.use_gitmoji,
371 config.print_only,
372 config.verify,
373 repository_url,
374 )
375 .await
376}
377
378fn handle_config(
380 common: &CommonParams,
381 api_key: Option<String>,
382 model: Option<String>,
383 token_limit: Option<usize>,
384 param: Option<Vec<String>>,
385) -> anyhow::Result<()> {
386 log_debug!(
387 "Handling 'config' command with common: {:?}, api_key: {:?}, model: {:?}, token_limit: {:?}, param: {:?}",
388 common,
389 api_key,
390 model,
391 token_limit,
392 param
393 );
394 commands::handle_config_command(common, api_key, model, token_limit, param)
395}
396
397async fn handle_review(
399 common: CommonParams,
400 print: bool,
401 repository_url: Option<String>,
402 include_unstaged: bool,
403 commit: Option<String>,
404) -> anyhow::Result<()> {
405 log_debug!(
406 "Handling 'review' command with common: {:?}, print: {}, include_unstaged: {}, commit: {:?}",
407 common,
408 print,
409 include_unstaged,
410 commit
411 );
412 ui::print_version(crate_version!());
413 println!();
414 commit::review::handle_review_command(common, print, repository_url, include_unstaged, commit)
415 .await
416}
417
418async fn handle_changelog(
420 common: CommonParams,
421 from: String,
422 to: Option<String>,
423 repository_url: Option<String>,
424 update: bool,
425 file: Option<String>,
426 version_name: Option<String>,
427) -> anyhow::Result<()> {
428 log_debug!(
429 "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, update: {}, file: {:?}, version_name: {:?}",
430 common,
431 from,
432 to,
433 update,
434 file,
435 version_name
436 );
437 changes::handle_changelog_command(common, from, to, repository_url, update, file, version_name)
438 .await
439}
440
441async fn handle_release_notes(
443 common: CommonParams,
444 from: String,
445 to: Option<String>,
446 repository_url: Option<String>,
447 version_name: Option<String>,
448) -> anyhow::Result<()> {
449 log_debug!(
450 "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, version_name: {:?}",
451 common,
452 from,
453 to,
454 version_name
455 );
456 changes::handle_release_notes_command(common, from, to, repository_url, version_name).await
457}
458
459async fn handle_serve(
461 dev: bool,
462 transport: String,
463 port: Option<u16>,
464 listen_address: Option<String>,
465) -> anyhow::Result<()> {
466 log_debug!(
467 "Handling 'serve' command with dev: {}, transport: {}, port: {:?}, listen_address: {:?}",
468 dev,
469 transport,
470 port,
471 listen_address
472 );
473 commands::handle_serve_command(dev, transport, port, listen_address).await
474}
475
476pub async fn handle_command(
478 command: Commands,
479 repository_url: Option<String>,
480) -> anyhow::Result<()> {
481 match command {
482 Commands::Gen {
483 common,
484 auto_commit,
485 no_gitmoji,
486 print,
487 no_verify,
488 } => {
489 handle_gen(
490 common,
491 GenConfig {
492 auto_commit,
493 use_gitmoji: !no_gitmoji,
494 print_only: print,
495 verify: !no_verify,
496 },
497 repository_url,
498 )
499 .await
500 }
501 Commands::Config {
502 common,
503 api_key,
504 model,
505 token_limit,
506 param,
507 } => handle_config(&common, api_key, model, token_limit, param),
508 Commands::Review {
509 common,
510 print,
511 include_unstaged,
512 commit,
513 } => handle_review(common, print, repository_url, include_unstaged, commit).await,
514 Commands::Changelog {
515 common,
516 from,
517 to,
518 update,
519 file,
520 version_name,
521 } => handle_changelog(common, from, to, repository_url, update, file, version_name).await,
522 Commands::ReleaseNotes {
523 common,
524 from,
525 to,
526 version_name,
527 } => handle_release_notes(common, from, to, repository_url, version_name).await,
528 Commands::Serve {
529 dev,
530 transport,
531 port,
532 listen_address,
533 } => handle_serve(dev, transport, port, listen_address).await,
534 Commands::ProjectConfig {
535 common,
536 model,
537 token_limit,
538 param,
539 print,
540 } => commands::handle_project_config_command(&common, model, token_limit, param, print),
541 Commands::ListPresets => commands::handle_list_presets_command(),
542 }
543}