uncomment/
cli.rs

1use clap::{Parser, Subcommand};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(
6    name = "uncomment",
7    version,
8    about = "Remove comments from code files using tree-sitter parsing",
9    long_about = "A fast, accurate CLI tool that removes comments from source code files using tree-sitter AST parsing. Automatically preserves important comments like linting directives, documentation, and metadata."
10)]
11pub struct Cli {
12    #[command(subcommand)]
13    pub command: Option<Commands>,
14
15    #[command(flatten)]
16    pub args: ProcessArgs,
17}
18
19#[derive(Subcommand, Debug)]
20pub enum Commands {
21    /// ~keep Initialize a configuration file in the current directory
22    #[command(about = "Create a template configuration file")]
23    Init {
24        /// ~keep Output file name
25        #[arg(short, long, default_value = ".uncommentrc.toml")]
26        output: PathBuf,
27
28        /// ~keep Overwrite existing file
29        #[arg(short, long)]
30        force: bool,
31
32        /// ~keep Generate configuration for all supported languages
33        #[arg(
34            long,
35            help = "Generate comprehensive config with all supported languages"
36        )]
37        comprehensive: bool,
38
39        /// ~keep Interactive mode to select languages
40        #[arg(short, long, help = "Interactive mode to select languages and options")]
41        interactive: bool,
42    },
43}
44
45#[derive(Parser, Debug)]
46pub struct ProcessArgs {
47    /// ~keep Files or directories to process (supports glob patterns)
48    #[arg(help = "Files, directories, or glob patterns to process")]
49    pub paths: Vec<String>,
50
51    /// ~keep Remove TODO comments (normally preserved)
52    #[arg(short = 'r', long, help = "Remove TODO comments (normally preserved)")]
53    pub remove_todo: bool,
54
55    /// ~keep Remove FIXME comments (normally preserved)
56    #[arg(short = 'f', long, help = "Remove FIXME comments (normally preserved)")]
57    pub remove_fixme: bool,
58
59    /// ~keep Remove documentation comments (normally preserved)
60    #[arg(
61        short = 'd',
62        long,
63        help = "Remove documentation comments and docstrings"
64    )]
65    pub remove_doc: bool,
66
67    /// ~keep Additional patterns to preserve (beyond defaults)
68    #[arg(
69        short = 'i',
70        long = "ignore",
71        help = "Additional patterns to preserve (can be used multiple times)"
72    )]
73    pub ignore_patterns: Vec<String>,
74
75    /// ~keep Disable automatic preservation of linting directives
76    #[arg(
77        long = "no-default-ignores",
78        help = "Disable built-in preservation patterns (ESLint, Clippy, etc.)"
79    )]
80    pub no_default_ignores: bool,
81
82    /// ~keep Show what would be changed without modifying files
83    #[arg(short = 'n', long, help = "Show changes without modifying files")]
84    pub dry_run: bool,
85
86    /// ~keep Show detailed processing information
87    #[arg(short = 'v', long, help = "Show detailed processing information")]
88    pub verbose: bool,
89
90    /// ~keep Ignore .gitignore rules when finding files
91    #[arg(long = "no-gitignore", help = "Process files ignored by .gitignore")]
92    pub no_gitignore: bool,
93
94    /// ~keep Process files in nested git repositories
95    #[arg(
96        long = "traverse-git-repos",
97        help = "Traverse into other git repositories (useful for monorepos)"
98    )]
99    pub traverse_git_repos: bool,
100
101    /// ~keep Number of parallel threads (0 = number of CPU cores)
102    #[arg(
103        short = 'j',
104        long = "threads",
105        help = "Number of parallel threads (0 = auto-detect)",
106        default_value = "1"
107    )]
108    pub threads: usize,
109
110    /// ~keep Path to configuration file
111    #[arg(
112        short = 'c',
113        long = "config",
114        help = "Path to configuration file (overrides automatic discovery)"
115    )]
116    pub config: Option<PathBuf>,
117}
118
119impl ProcessArgs {
120    pub fn processing_options(&self) -> crate::processor::ProcessingOptions {
121        crate::processor::ProcessingOptions {
122            remove_todo: self.remove_todo,
123            remove_fixme: self.remove_fixme,
124            remove_doc: self.remove_doc,
125            custom_preserve_patterns: self.ignore_patterns.clone(),
126            use_default_ignores: !self.no_default_ignores,
127            dry_run: self.dry_run,
128            respect_gitignore: !self.no_gitignore,
129            traverse_git_repos: self.traverse_git_repos,
130        }
131    }
132}
133
134impl Cli {
135    /// ~keep Handle the init command
136    pub fn handle_init_command(
137        output: &PathBuf,
138        force: bool,
139        comprehensive: bool,
140        interactive: bool,
141    ) -> anyhow::Result<()> {
142        if output.exists() && !force {
143            return Err(anyhow::anyhow!(
144                "Configuration file already exists: {}. Use --force to overwrite.",
145                output.display()
146            ));
147        }
148
149        let (template, detected_info) = if comprehensive {
150            (crate::config::Config::comprehensive_template_clean(), None)
151        } else if interactive {
152            (crate::config::Config::interactive_template_clean()?, None)
153        } else {
154            // Smart template based on detected files in current directory
155            let current_dir =
156                std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
157            let (template, info) = crate::config::Config::smart_template_with_info(&current_dir)?;
158            (template, Some(info))
159        };
160
161        std::fs::write(output, template)?;
162
163        println!("✓ Created configuration file: {}", output.display());
164
165        if comprehensive {
166            println!("📦 Generated comprehensive config with 15+ language configurations");
167        } else if interactive {
168            println!("🎯 Generated customized config based on your selections");
169        } else if let Some(info) = detected_info {
170            if !info.detected_languages.is_empty() {
171                println!(
172                    "🔍 Detected {} file types in your project:",
173                    info.detected_languages.len()
174                );
175                for (lang, count) in &info.detected_languages {
176                    println!("   {count} ({lang} files)");
177                }
178                println!(
179                    "📝 Configured {} languages with appropriate settings",
180                    info.configured_languages
181                );
182            } else {
183                println!("📝 No supported files detected, generated basic template");
184            }
185            if info.total_files > 0 {
186                println!("📊 Scanned {} files total", info.total_files);
187            }
188        } else {
189            println!("📝 Generated smart config based on detected files in your project");
190        }
191
192        Ok(())
193    }
194}