gitai/
app.rs

1use crate::common::CommonParams;
2use crate::core::llm::get_available_provider_names;
3use crate::features::changelog::{handle_changelog_command, handle_release_notes_command};
4use crate::features::commit;
5use crate::server::config::{MCPServerConfig, MCPTransportType};
6use crate::ui;
7use crate::{debug, server};
8use clap::builder::{Styles, styling::AnsiColor};
9use clap::{Parser, Subcommand, crate_version};
10use colored::Colorize;
11
12/// CLI structure defining the available commands and global arguments
13#[derive(Parser)]
14#[command(
15    author,
16    version = crate_version!(),
17    about = "GitAI: AI-powered Git workflow assistant",
18    disable_version_flag = true,
19    after_help = get_dynamic_help(),
20    styles = get_styles(),
21)]
22pub struct Cli {
23    /// Subcommands available for the CLI
24    #[command(subcommand)]
25    pub command: Option<GitAI>,
26
27    /// Log debug messages to a file
28    #[arg(
29        short = 'l',
30        long = "log",
31        global = true,
32        help = "Log debug messages to a file"
33    )]
34    pub log: bool,
35
36    /// Specify a custom log file path
37    #[arg(
38        long = "log-file",
39        global = true,
40        help = "Specify a custom log file path"
41    )]
42    pub log_file: Option<String>,
43
44    /// Suppress non-essential output (spinners, waiting messages, etc.)
45    #[arg(
46        short = 'q',
47        long = "quiet",
48        global = true,
49        help = "Suppress non-essential output"
50    )]
51    pub quiet: bool,
52
53    /// Display the version
54    #[arg(
55        short = 'v',
56        long = "version",
57        global = true,
58        help = "Display the version"
59    )]
60    pub version: bool,
61
62    /// Repository URL to use instead of local repository
63    #[arg(
64        short = 'r',
65        long = "repo",
66        global = true,
67        help = "Repository URL to use instead of local repository"
68    )]
69    pub repository_url: Option<String>,
70}
71
72/// Enumeration of available subcommands
73#[derive(Subcommand)]
74#[command(subcommand_negates_reqs = true)]
75#[command(subcommand_precedence_over_arg = true)]
76pub enum GitAI {
77    // Feature commands first
78    /// Generate a commit message using AI
79    #[command(
80        about = "Generate a commit message using AI",
81        long_about = "Generate a commit message using AI based on the current Git context.",
82        after_help = get_dynamic_help()
83    )]
84    Message {
85        #[command(flatten)]
86        common: CommonParams,
87
88        /// Automatically commit with the generated message
89        #[arg(short, long, help = "Automatically commit with the generated message")]
90        auto_commit: bool,
91
92        /// Disable emoji for this commit
93        #[arg(long, help = "Disable emojis for this commit")]
94        no_emoji: bool,
95
96        /// Print the generated message to stdout and exit
97        #[arg(short, long, help = "Print the generated message to stdout and exit")]
98        print: bool,
99
100        /// Skip the verification step (pre/post commit hooks)
101        #[arg(long, help = "Skip verification steps (pre/post commit hooks)")]
102        no_verify: bool,
103    },
104
105    /// Review staged changes and provide feedback
106    #[command(
107        about = "Review staged changes using AI",
108        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."
109    )]
110    Review {
111        #[command(flatten)]
112        common: CommonParams,
113
114        /// Print the generated review to stdout and exit
115        #[arg(short, long, help = "Print the generated review to stdout and exit")]
116        print: bool,
117
118        /// Include unstaged changes in the review
119        #[arg(long, help = "Include unstaged changes in the review")]
120        include_unstaged: bool,
121
122        /// Review a specific commit by ID (hash, branch, or reference)
123        #[arg(
124            long,
125            help = "Review a specific commit by ID (hash, branch, or reference)"
126        )]
127        commit: Option<String>,
128
129        /// Starting branch for comparison (defaults to 'main')
130        #[arg(
131            long,
132            help = "Starting branch for comparison (defaults to 'main'). Used with --to for branch comparison reviews"
133        )]
134        from: Option<String>,
135
136        /// Target branch for comparison (e.g., 'feature-branch', 'pr-branch')
137        #[arg(
138            long,
139            help = "Target branch for comparison (e.g., 'feature-branch', 'pr-branch'). Used with --from for branch comparison reviews"
140        )]
141        to: Option<String>,
142    },
143
144    /// Generate a pull request description
145    #[command(
146        about = "Generate a pull request description using AI",
147        long_about = "Generate a comprehensive pull request description based on commit ranges, branch differences, or single commits. Analyzes the overall changeset as an atomic unit and creates professional PR descriptions with summaries, detailed explanations, and testing notes.\\
148\\
149Usage examples:\\
150• Single commit: --from abc1234 or --to abc1234\\
151• Single commitish: --from HEAD~1 or --to HEAD~2\\
152• Multiple commits: --from HEAD~3 (reviews last 3 commits)\\
153• Commit range: --from abc1234 --to def5678\\
154• Branch comparison: --from main --to feature-branch\\
155• From main to branch: --to feature-branch\\
156\\
157Supported commitish syntax: HEAD~2, HEAD^, @~3, main~1, origin/main^, etc."
158    )]
159    Pr {
160        #[command(flatten)]
161        common: CommonParams,
162
163        /// Print the generated PR description to stdout and exit
164        #[arg(
165            short,
166            long,
167            help = "Print the generated PR description to stdout and exit"
168        )]
169        print: bool,
170
171        /// Starting branch, commit, or commitish for comparison
172        #[arg(
173            long,
174            help = "Starting branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash (e.g., --from abc1234). For reviewing multiple commits, use commitish syntax (e.g., --from HEAD~3 to review last 3 commits)"
175        )]
176        from: Option<String>,
177
178        /// Target branch, commit, or commitish for comparison
179        #[arg(
180            long,
181            help = "Target branch, commit, or commitish for comparison. For single commit analysis, specify just this parameter with a commit hash or commitish (e.g., --to HEAD~2)"
182        )]
183        to: Option<String>,
184    },
185
186    /// Generate a changelog
187    #[command(
188        about = "Generate a changelog",
189        long_about = "Generate a changelog between two specified Git references."
190    )]
191    Changelog {
192        #[command(flatten)]
193        common: CommonParams,
194
195        /// Starting Git reference (commit hash, tag, or branch name)
196        #[arg(long, required = true)]
197        from: String,
198
199        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
200        #[arg(long)]
201        to: Option<String>,
202
203        /// Update the changelog file with the new changes
204        #[arg(long, help = "Update the changelog file with the new changes")]
205        update: bool,
206
207        /// Path to the changelog file
208        #[arg(long, help = "Path to the changelog file (defaults to CHANGELOG.md)")]
209        file: Option<String>,
210
211        /// Explicit version name to use in the changelog instead of getting it from Git
212        #[arg(long, help = "Explicit version name to use in the changelog")]
213        version_name: Option<String>,
214    },
215
216    /// Generate release notes
217    #[command(
218        about = "Generate release notes",
219        long_about = "Generate comprehensive release notes between two specified Git references."
220    )]
221    ReleaseNotes {
222        #[command(flatten)]
223        common: CommonParams,
224
225        /// Starting Git reference (commit hash, tag, or branch name)
226        #[arg(long, required = true)]
227        from: String,
228
229        /// Ending Git reference (commit hash, tag, or branch name). Defaults to HEAD if not specified.
230        #[arg(long)]
231        to: Option<String>,
232
233        /// Explicit version name to use in the release notes instead of getting it from Git
234        #[arg(long, help = "Explicit version name to use in the release notes")]
235        version_name: Option<String>,
236    },
237
238    /// Start an MCP server to provide functionality to AI tools
239    #[command(
240        about = "Start an MCP server",
241        long_about = "Start a Model Context Protocol (MCP) server to provide functionality to AI tools and assistants."
242    )]
243    Serve {
244        /// Enable development mode with more verbose logging
245        #[arg(long, help = "Enable development mode with more verbose logging")]
246        dev: bool,
247
248        /// Transport type to use (stdio, sse)
249        #[arg(
250            short,
251            long,
252            help = "Transport type to use (stdio, sse)",
253            default_value = "stdio"
254        )]
255        transport: String,
256
257        /// Port to use for network transports
258        #[arg(short, long, help = "Port to use for network transports")]
259        port: Option<u16>,
260
261        /// Listen address for network transports
262        #[arg(
263            long,
264            help = "Listen address for network transports (e.g., '127.0.0.1', '0.0.0.0')",
265            default_value = "127.0.0.1"
266        )]
267        listen_address: Option<String>,
268    },
269}
270
271/// Define custom styles for Clap
272fn get_styles() -> Styles {
273    Styles::styled()
274        .header(AnsiColor::Magenta.on_default().bold())
275        .usage(AnsiColor::Cyan.on_default().bold())
276        .literal(AnsiColor::Green.on_default().bold())
277        .placeholder(AnsiColor::Yellow.on_default())
278        .valid(AnsiColor::Blue.on_default().bold())
279        .invalid(AnsiColor::Red.on_default().bold())
280        .error(AnsiColor::Red.on_default().bold())
281}
282
283/// Parse the command-line arguments
284pub fn parse_args() -> Cli {
285    Cli::parse()
286}
287
288/// Generate dynamic help including available LLM providers
289fn get_dynamic_help() -> String {
290    let mut providers = get_available_provider_names();
291    providers.sort();
292
293    let providers_list = providers
294        .iter()
295        .map(|p| format!("{}", p.bold()))
296        .collect::<Vec<_>>()
297        .join(" • ");
298
299    format!(
300        "\\
301Available LLM Providers: {providers_list}"
302    )
303}
304
305/// Configuration for the cmsg command
306#[allow(clippy::struct_excessive_bools)]
307pub struct CmsgConfig {
308    pub auto_commit: bool,
309    pub use_emoji: bool,
310    pub print_only: bool,
311    pub verify: bool,
312    pub dry_run: bool,
313}
314
315pub async fn handle_message(
316    common: CommonParams,
317    config: CmsgConfig,
318    repository_url: Option<String>,
319) -> anyhow::Result<()> {
320    debug!(
321        "Handling 'message' command with common: {:?}, auto_commit: {}, use_emoji: {}, print: {}, verify: {}",
322        common, config.auto_commit, config.use_emoji, config.print_only, config.verify
323    );
324
325    ui::print_version(crate_version!());
326    ui::print_newline();
327
328    commit::handle_message_command(
329        common,
330        config.auto_commit,
331        config.use_emoji,
332        config.print_only,
333        config.verify,
334        config.dry_run,
335        repository_url,
336    )
337    .await
338}
339
340/// Handle the `Review` command
341pub async fn handle_review(
342    common: CommonParams,
343    print: bool,
344    repository_url: Option<String>,
345    include_unstaged: bool,
346    commit: Option<String>,
347    from: Option<String>,
348    to: Option<String>,
349) -> anyhow::Result<()> {
350    debug!(
351        "Handling 'review' command with common: {:?}, print: {}, include_unstaged: {}, commit: {:?}, from: {:?}, to: {:?}",
352        common, print, include_unstaged, commit, from, to
353    );
354    ui::print_version(crate_version!());
355    ui::print_newline();
356    commit::review::handle_review_command(
357        common,
358        print,
359        repository_url,
360        include_unstaged,
361        commit,
362        from,
363        to,
364    )
365    .await
366}
367
368/// Handle the `Changelog` command
369pub async fn handle_changelog(
370    common: CommonParams,
371    from: String,
372    to: Option<String>,
373    repository_url: Option<String>,
374    update: bool,
375    file: Option<String>,
376    version_name: Option<String>,
377) -> anyhow::Result<()> {
378    debug!(
379        "Handling 'changelog' command with common: {:?}, from: {}, to: {:?}, update: {}, file: {:?}, version_name: {:?}",
380        common, from, to, update, file, version_name
381    );
382    handle_changelog_command(common, from, to, repository_url, update, file, version_name).await
383}
384
385/// Handle the `ReleaseNotes` command
386pub async fn handle_release_notes(
387    common: CommonParams,
388    from: String,
389    to: Option<String>,
390    repository_url: Option<String>,
391    version_name: Option<String>,
392) -> anyhow::Result<()> {
393    debug!(
394        "Handling 'release-notes' command with common: {:?}, from: {}, to: {:?}, version_name: {:?}",
395        common, from, to, version_name
396    );
397    handle_release_notes_command(common, from, to, repository_url, version_name).await
398}
399
400/// Handle the command based on parsed arguments
401pub async fn handle_command(command: GitAI, repository_url: Option<String>) -> anyhow::Result<()> {
402    match command {
403        GitAI::Message {
404            common,
405            auto_commit,
406            no_emoji,
407            print,
408            no_verify,
409        } => {
410            handle_message(
411                common,
412                CmsgConfig {
413                    auto_commit,
414                    use_emoji: !no_emoji,
415                    print_only: print,
416                    verify: !no_verify,
417                    dry_run: false,
418                },
419                repository_url,
420            )
421            .await
422        }
423        GitAI::Review {
424            common,
425            print,
426            include_unstaged,
427            commit,
428            from,
429            to,
430        } => {
431            handle_review(
432                common,
433                print,
434                repository_url,
435                include_unstaged,
436                commit,
437                from,
438                to,
439            )
440            .await
441        }
442        GitAI::Changelog {
443            common,
444            from,
445            to,
446            update,
447            file,
448            version_name,
449        } => handle_changelog(common, from, to, repository_url, update, file, version_name).await,
450        GitAI::ReleaseNotes {
451            common,
452            from,
453            to,
454            version_name,
455        } => handle_release_notes(common, from, to, repository_url, version_name).await,
456        GitAI::Serve {
457            dev,
458            transport,
459            port,
460            listen_address,
461        } => handle_serve_command(dev, transport, port, listen_address).await,
462        GitAI::Pr {
463            common,
464            print,
465            from,
466            to,
467        } => handle_pr_command(common, print, from, to, repository_url).await,
468    }
469}
470
471/// Handle the `Pr` command
472pub async fn handle_pr_command(
473    common: CommonParams,
474    print: bool,
475    from: Option<String>,
476    to: Option<String>,
477    repository_url: Option<String>,
478) -> anyhow::Result<()> {
479    debug!(
480        "Handling 'pr' command with common: {:?}, print: {}, from: {:?}, to: {:?}",
481        common, print, from, to
482    );
483    ui::print_version(crate_version!());
484    ui::print_newline();
485    commit::handle_pr_command(common, print, repository_url, from, to).await
486}
487
488/// Handle the 'serve' command to start an MCP server
489pub async fn handle_serve_command(
490    dev: bool,
491    transport: String,
492    port: Option<u16>,
493    listen_address: Option<String>,
494) -> anyhow::Result<()> {
495    debug!(
496        "Starting 'serve' command with dev: {}, transport: {}, port: {:?}, listen_address: {:?}",
497        dev, transport, port, listen_address
498    );
499
500    // Create MCP server configuration
501    let mut config = MCPServerConfig::default();
502
503    // Set development mode
504    if dev {
505        config = config.with_dev_mode();
506    }
507
508    // Set transport type
509    let transport_type = match transport.to_lowercase().as_str() {
510        "stdio" => MCPTransportType::StdIO,
511        "sse" => MCPTransportType::SSE,
512        _ => {
513            return Err(anyhow::anyhow!(
514                "Invalid transport type: {transport}. Valid options are: stdio, sse"
515            ));
516        }
517    };
518    config = config.with_transport(transport_type);
519
520    // Set port if provided
521    if let Some(p) = port {
522        config = config.with_port(p);
523    }
524
525    // Set listen address if provided
526    if let Some(addr) = listen_address {
527        config = config.with_listen_address(addr);
528    }
529
530    // Start the server - all UI output is now handled inside serve implementation
531    server::serve(config).await
532}