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}