Skip to main content

krait/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5#[derive(Parser, Debug)]
6#[command(
7    name = "krait",
8    about = "Code intelligence CLI for AI agents",
9    version,
10    propagate_version = true
11)]
12pub struct Cli {
13    #[command(subcommand)]
14    pub command: Command,
15
16    /// Output format
17    #[arg(long, global = true, default_value = "compact")]
18    pub format: OutputFormat,
19
20    /// Enable verbose logging
21    #[arg(long, global = true)]
22    pub verbose: bool,
23}
24
25#[derive(Debug, Clone, Copy, ValueEnum)]
26pub enum OutputFormat {
27    Compact,
28    Json,
29    Human,
30}
31
32#[derive(Subcommand, Debug)]
33pub enum Command {
34    /// Generate krait.toml config from auto-detected workspaces
35    Init {
36        /// Overwrite existing krait.toml
37        #[arg(long)]
38        force: bool,
39
40        /// Print what would be generated without writing
41        #[arg(long)]
42        dry_run: bool,
43    },
44
45    /// Show daemon health, LSP state, cache stats
46    Status,
47
48    /// Show LSP diagnostics (errors/warnings)
49    Check {
50        /// File path to check (all files if omitted)
51        path: Option<PathBuf>,
52
53        /// Show only errors, suppress warnings/hints
54        #[arg(long)]
55        errors_only: bool,
56    },
57
58    /// Find symbols and references
59    #[command(subcommand)]
60    Find(FindCommand),
61
62    /// List symbols or other resources
63    #[command(subcommand)]
64    List(ListCommand),
65
66    /// Read files or symbol bodies
67    #[command(subcommand)]
68    Read(ReadCommand),
69
70    /// Semantic code editing via stdin
71    #[command(subcommand)]
72    Edit(EditCommand),
73
74    /// Manage the background daemon
75    #[command(subcommand)]
76    Daemon(DaemonCommand),
77
78    /// Show type info and documentation for a symbol
79    Hover {
80        /// Symbol name to hover over
81        name: String,
82    },
83
84    /// Format a file using the LSP formatter
85    Format {
86        /// File path to format
87        path: std::path::PathBuf,
88    },
89
90    /// Rename a symbol across all files
91    Rename {
92        /// Symbol name to rename
93        symbol: String,
94        /// New name for the symbol
95        new_name: String,
96    },
97
98    /// Apply LSP quick-fix code actions for current diagnostics
99    Fix {
100        /// File path to fix (all files with diagnostics if omitted)
101        path: Option<std::path::PathBuf>,
102    },
103
104    /// Poll for diagnostics and print timestamped results
105    Watch {
106        /// Path to check (all files if omitted)
107        path: Option<std::path::PathBuf>,
108        /// Polling interval in milliseconds
109        #[arg(long, default_value = "1500")]
110        interval: u64,
111    },
112
113    /// Manage LSP server installations
114    #[command(subcommand)]
115    Server(ServerCommand),
116
117    /// Search for a pattern in project files
118    Search {
119        /// Pattern to search for (regex by default)
120        pattern: String,
121
122        /// File or directory to search in (default: project root)
123        path: Option<PathBuf>,
124
125        /// Case-insensitive search
126        #[arg(short = 'i', long)]
127        ignore_case: bool,
128
129        /// Match whole words only
130        #[arg(short = 'w', long)]
131        word: bool,
132
133        /// Treat pattern as literal string (no regex)
134        #[arg(short = 'F', long)]
135        literal: bool,
136
137        /// Show N lines of context around each match
138        #[arg(short = 'C', long, default_value = "0")]
139        context: u32,
140
141        /// List only matching file paths
142        #[arg(short = 'l', long)]
143        files: bool,
144
145        /// Filter by language (ts, js, rs, go, py, java, cs, rb, lua)
146        #[arg(long, value_name = "LANG")]
147        r#type: Option<String>,
148
149        /// Max total matches (default: 200)
150        #[arg(long)]
151        max: Option<usize>,
152    },
153}
154
155#[derive(Subcommand, Debug)]
156pub enum FindCommand {
157    /// Locate symbol definition
158    Symbol {
159        /// Symbol name to find
160        name: String,
161
162        /// Filter results to paths containing this substring (for disambiguation)
163        #[arg(long, value_name = "SUBSTR")]
164        path: Option<String>,
165
166        /// Exclude noise paths (www/, dist/, `node_modules`/, .d.ts, .mdx)
167        #[arg(long)]
168        src_only: bool,
169
170        /// Include full symbol body in results (like Serena's `include_body=True`)
171        #[arg(long)]
172        include_body: bool,
173    },
174    /// Find all references to a symbol
175    Refs {
176        /// Symbol name to search for
177        name: String,
178
179        /// Enrich each reference with its containing symbol (function/class name)
180        #[arg(long)]
181        with_symbol: bool,
182    },
183    /// Navigate from interface method to concrete implementations
184    Impl {
185        /// Symbol name (interface method) to find implementations of
186        name: String,
187    },
188}
189
190#[derive(Subcommand, Debug)]
191pub enum ListCommand {
192    /// Semantic outline of a file
193    Symbols {
194        /// File path to list symbols for
195        path: PathBuf,
196
197        /// Depth of symbol tree (1=top-level, 2=methods, 3=full)
198        #[arg(long, default_value = "1")]
199        depth: u8,
200    },
201}
202
203#[derive(Subcommand, Debug)]
204pub enum ReadCommand {
205    /// Read file contents with line numbers
206    File {
207        /// File path to read
208        path: PathBuf,
209
210        /// Start line (1-indexed)
211        #[arg(long)]
212        from: Option<u32>,
213
214        /// End line (inclusive)
215        #[arg(long)]
216        to: Option<u32>,
217
218        /// Max lines to show
219        #[arg(long)]
220        max_lines: Option<u32>,
221    },
222    /// Extract symbol body/code
223    Symbol {
224        /// Symbol name to read
225        name: String,
226
227        /// Show only the signature/declaration
228        #[arg(long)]
229        signature_only: bool,
230
231        /// Max lines to show
232        #[arg(long)]
233        max_lines: Option<u32>,
234
235        /// Select the definition whose path contains this substring (for disambiguation)
236        #[arg(long, value_name = "SUBSTR")]
237        path: Option<String>,
238
239        /// Skip overload stubs (single-line `;` declarations) and return the implementation
240        #[arg(long)]
241        has_body: bool,
242    },
243}
244
245#[derive(Subcommand, Debug)]
246pub enum EditCommand {
247    /// Replace symbol body with stdin
248    Replace {
249        /// Symbol to replace
250        symbol: String,
251    },
252    /// Insert code after a symbol (stdin)
253    InsertAfter {
254        /// Symbol to insert after
255        symbol: String,
256    },
257    /// Insert code before a symbol (stdin)
258    InsertBefore {
259        /// Symbol to insert before
260        symbol: String,
261    },
262}
263
264#[derive(Subcommand, Debug)]
265pub enum ServerCommand {
266    /// List all supported LSP servers and their install status
267    List,
268    /// Install an LSP server (omit lang to install all missing)
269    Install {
270        /// Language (rust, go, python, java, cpp, csharp, ruby, lua, ts, js)
271        lang: Option<String>,
272        /// Force reinstall even if already present
273        #[arg(long)]
274        reinstall: bool,
275    },
276    /// Remove all managed servers from ~/.krait/servers/
277    Clean,
278    /// Show running LSP server status from daemon
279    Status,
280    /// Restart a language server in the running daemon
281    Restart {
282        /// Language to restart (rust, go, python, etc.)
283        lang: String,
284    },
285}
286
287#[derive(Subcommand, Debug)]
288pub enum DaemonCommand {
289    /// Start the daemon (foreground)
290    Start,
291    /// Stop the running daemon
292    Stop,
293    /// Show daemon status
294    Status,
295}
296
297#[cfg(test)]
298mod tests {
299    use clap::CommandFactory;
300
301    use super::*;
302
303    #[test]
304    fn cli_help_does_not_panic() {
305        Cli::command().debug_assert();
306    }
307
308    #[test]
309    fn cli_parses_find_symbol() {
310        let cli = Cli::try_parse_from(["krait", "find", "symbol", "MyStruct"]).unwrap();
311        assert!(matches!(
312            cli.command,
313            Command::Find(FindCommand::Symbol { name, .. }) if name == "MyStruct"
314        ));
315    }
316
317    #[test]
318    fn cli_parses_read_file_with_flags() {
319        let cli = Cli::try_parse_from([
320            "krait",
321            "read",
322            "file",
323            "src/lib.rs",
324            "--from",
325            "5",
326            "--to",
327            "10",
328            "--max-lines",
329            "20",
330        ])
331        .unwrap();
332        assert!(matches!(
333            cli.command,
334            Command::Read(ReadCommand::File {
335                ref path,
336                from: Some(5),
337                to: Some(10),
338                max_lines: Some(20),
339            }) if path == &PathBuf::from("src/lib.rs")
340        ));
341    }
342
343    #[test]
344    fn cli_parses_edit_replace() {
345        let cli = Cli::try_parse_from(["krait", "edit", "replace", "my_func"]).unwrap();
346        assert!(matches!(
347            cli.command,
348            Command::Edit(EditCommand::Replace { symbol }) if symbol == "my_func"
349        ));
350    }
351
352    #[test]
353    fn cli_rejects_unknown_command() {
354        let result = Cli::try_parse_from(["krait", "explode"]);
355        assert!(result.is_err());
356    }
357}