git_iris/
cli.rs

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/// CLI structure defining the available commands and global arguments
15#[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    /// Subcommands available for the CLI
27    #[command(subcommand)]
28    pub command: Option<Commands>,
29
30    /// Log debug messages to a file
31    #[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    /// Specify a custom log file path
40    #[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    /// Suppress non-essential output (spinners, waiting messages, etc.)
48    #[arg(
49        short = 'q',
50        long = "quiet",
51        global = true,
52        help = "Suppress non-essential output"
53    )]
54    pub quiet: bool,
55
56    /// Display the version
57    #[arg(
58        short = 'v',
59        long = "version",
60        global = true,
61        help = "Display the version"
62    )]
63    pub version: bool,
64
65    /// Repository URL to use instead of local repository
66    #[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/// Enumeration of available subcommands
76#[derive(Subcommand)]
77#[command(subcommand_negates_reqs = true)]
78#[command(subcommand_precedence_over_arg = true)]
79pub enum Commands {
80    // Feature commands first
81    /// Generate a commit message using AI
82    #[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        /// Automatically commit with the generated message
92        #[arg(short, long, help = "Automatically commit with the generated message")]
93        auto_commit: bool,
94
95        /// Disable Gitmoji for this commit
96        #[arg(long, help = "Disable Gitmoji for this commit")]
97        no_gitmoji: bool,
98
99        /// Print the generated message to stdout and exit
100        #[arg(short, long, help = "Print the generated message to stdout and exit")]
101        print: bool,
102
103        /// Skip the verification step (pre/post commit hooks)
104        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
105        no_verify: bool,
106    },
107
108    /// Review staged changes and provide feedback
109    #[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        /// Print the generated review to stdout and exit
118        #[arg(short, long, help = "Print the generated review to stdout and exit")]
119        print: bool,
120
121        /// Include unstaged changes in the review
122        #[arg(long, help = "Include unstaged changes in the review")]
123        include_unstaged: bool,
124
125        /// Review a specific commit by ID (hash, branch, or reference)
126        #[arg(
127            long,
128            help = "Review a specific commit by ID (hash, branch, or reference)"
129        )]
130        commit: Option<String>,
131    },
132
133    /// Generate a changelog
134    #[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        /// Starting Git reference (commit hash, tag, or branch name)
143        #[arg(long, required = true)]
144        from: String,
145
146        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
147        #[arg(long)]
148        to: Option<String>,
149
150        /// Update the changelog file with the new changes
151        #[arg(long, help = "Update the changelog file with the new changes")]
152        update: bool,
153
154        /// Path to the changelog file
155        #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
156        file: Option<String>,
157
158        /// Explicit version name to use in the changelog instead of getting it from Git
159        #[arg(long, help = "Explicit version name to use in the changelog")]
160        version_name: Option<String>,
161    },
162
163    /// Generate release notes
164    #[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        /// Starting Git reference (commit hash, tag, or branch name)
173        #[arg(long, required = true)]
174        from: String,
175
176        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
177        #[arg(long)]
178        to: Option<String>,
179
180        /// Explicit version name to use in the release notes instead of getting it from Git
181        #[arg(long, help = "Explicit version name to use in the release notes")]
182        version_name: Option<String>,
183    },
184
185    /// Start an MCP server to provide Git-Iris functionality to AI tools
186    #[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        /// Enable development mode with more verbose logging
192        #[arg(long, help = "Enable development mode with more verbose logging")]
193        dev: bool,
194
195        /// Transport type to use (stdio, sse)
196        #[arg(
197            short,
198            long,
199            help = "Transport type to use (stdio, sse)",
200            default_value = "stdio"
201        )]
202        transport: String,
203
204        /// Port to use for network transports
205        #[arg(short, long, help = "Port to use for network transports")]
206        port: Option<u16>,
207
208        /// Listen address for network transports
209        #[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    // Configuration and utility commands
218    /// Configure the AI-assisted Git commit message generator
219    #[command(about = "Configure Git-Iris settings and providers")]
220    Config {
221        #[command(flatten)]
222        common: CommonParams,
223
224        /// Set API key for the specified provider
225        #[arg(long, help = "Set API key for the specified provider")]
226        api_key: Option<String>,
227
228        /// Set model for the specified provider
229        #[arg(long, help = "Set model for the specified provider")]
230        model: Option<String>,
231
232        /// Set token limit for the specified provider
233        #[arg(long, help = "Set token limit for the specified provider")]
234        token_limit: Option<usize>,
235
236        /// Set additional parameters for the specified provider
237        #[arg(
238            long,
239            help = "Set additional parameters for the specified provider (key=value)"
240        )]
241        param: Option<Vec<String>>,
242    },
243
244    /// Create or update a project-specific configuration file
245    #[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        /// Set model for the specified provider
254        #[arg(long, help = "Set model for the specified provider")]
255        model: Option<String>,
256
257        /// Set token limit for the specified provider
258        #[arg(long, help = "Set token limit for the specified provider")]
259        token_limit: Option<usize>,
260
261        /// Set additional parameters for the specified provider
262        #[arg(
263            long,
264            help = "Set additional parameters for the specified provider (key=value)"
265        )]
266        param: Option<Vec<String>>,
267
268        /// Print the current project configuration
269        #[arg(short, long, help = "Print the current project configuration")]
270        print: bool,
271    },
272
273    /// List available instruction presets
274    #[command(about = "List available instruction presets")]
275    ListPresets,
276}
277
278/// Define custom styles for Clap
279fn 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
290/// Parse the command-line arguments
291pub fn parse_args() -> Cli {
292    Cli::parse()
293}
294
295/// Generate dynamic help including available LLM providers
296fn get_dynamic_help() -> String {
297    let mut providers = get_available_provider_names();
298    providers.sort(); // Sort alphabetically
299
300    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
309/// Main function to parse arguments and handle the command
310pub 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    // Set quiet mode in the UI module
327    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        // If no subcommand is provided, print the help
335        let _ = Cli::parse_from(["git-iris", "--help"]);
336        Ok(())
337    }
338}
339
340/// Configuration for the Gen command
341#[allow(clippy::struct_excessive_bools)]
342struct GenConfig {
343    auto_commit: bool,
344    use_gitmoji: bool,
345    print_only: bool,
346    verify: bool,
347}
348
349/// Handle the `Gen` command
350async 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
378/// Handle the `Config` command
379fn 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
397/// Handle the `Review` command
398async 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
418/// Handle the `Changelog` command
419async 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
441/// Handle the `ReleaseNotes` command
442async 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
459/// Handle the `Serve` command
460async 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
476/// Handle the command based on parsed arguments
477pub 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}