sqry_cli/args/mod.rs
1//! Command-line argument parsing for sqry
2
3pub mod headings;
4mod sort;
5
6use crate::output;
7use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum};
8pub use sort::SortField;
9use sqry_lsp::LspOptions;
10use std::path::PathBuf;
11
12/// sqry - Semantic Query for Code
13///
14/// Search code by what it means, not just what it says.
15/// Uses AST analysis to find functions, classes, and symbols with precision.
16#[derive(Parser, Debug)]
17#[command(
18 name = "sqry",
19 version,
20 about = "Semantic code search tool",
21 long_about = "sqry is a semantic code search tool that understands code structure through AST analysis.\n\
22 Find functions, classes, and symbols with precision using AST-aware queries.\n\n\
23 Examples:\n \
24 sqry main # Search for 'main' in current directory\n \
25 sqry test src/ # Search for 'test' in src/\n \
26 sqry --kind function . # Find all functions\n \
27 sqry --json main # Output as JSON\n \
28 sqry --csv --headers main # Output as CSV with headers\n \
29 sqry --preview main # Show code context around matches",
30 group = ArgGroup::new("output_format").args(["json", "csv", "tsv"]),
31 verbatim_doc_comment
32)]
33// CLI flags are intentionally modeled as independent booleans for clarity.
34#[allow(clippy::struct_excessive_bools)]
35pub struct Cli {
36 /// Subcommand (optional - defaults to search if pattern provided)
37 #[command(subcommand)]
38 pub command: Option<Box<Command>>,
39
40 /// Search pattern (shorthand for 'search' command)
41 ///
42 /// Supports regex patterns by default. Use --exact for literal matching.
43 #[arg(required = false)]
44 pub pattern: Option<String>,
45
46 /// Search path (defaults to current directory)
47 #[arg(required = false)]
48 pub path: Option<String>,
49
50 /// Output format as JSON
51 #[arg(long, short = 'j', global = true, group = "output_format", help_heading = headings::COMMON_OPTIONS, display_order = 10)]
52 pub json: bool,
53
54 /// Output format as CSV (comma-separated values)
55 ///
56 /// RFC 4180 compliant CSV output. Use with --headers to include column names.
57 /// By default, formula-triggering characters are prefixed with single quote
58 /// for Excel/LibreOffice safety. Use --raw-csv to disable this protection.
59 #[arg(long, global = true, group = "output_format", help_heading = headings::COMMON_OPTIONS, display_order = 12)]
60 pub csv: bool,
61
62 /// Output format as TSV (tab-separated values)
63 ///
64 /// Tab-delimited output for easy Unix pipeline processing.
65 /// Newlines and tabs in field values are replaced with spaces.
66 #[arg(long, global = true, group = "output_format", help_heading = headings::COMMON_OPTIONS, display_order = 13)]
67 pub tsv: bool,
68
69 /// Include header row in CSV/TSV output
70 ///
71 /// Requires --csv or --tsv to be specified.
72 #[arg(long, global = true, help_heading = headings::OUTPUT_CONTROL, display_order = 11)]
73 pub headers: bool,
74
75 /// Columns to include in CSV/TSV output (comma-separated)
76 ///
77 /// Available columns: `name`, `qualified_name`, `kind`, `file`, `line`, `column`,
78 /// `end_line`, `end_column`, `language`, `preview`
79 ///
80 /// Example: --columns name,file,line
81 ///
82 /// Requires --csv or --tsv to be specified.
83 #[arg(long, global = true, value_name = "COLUMNS", help_heading = headings::OUTPUT_CONTROL, display_order = 12)]
84 pub columns: Option<String>,
85
86 /// Output raw CSV without formula injection protection
87 ///
88 /// By default, values starting with =, +, -, @, tab, or carriage return
89 /// are prefixed with single quote to prevent Excel/LibreOffice formula
90 /// injection attacks. Use this flag to disable protection for programmatic
91 /// processing where raw values are needed.
92 ///
93 /// Requires --csv or --tsv to be specified.
94 #[arg(long, global = true, help_heading = headings::OUTPUT_CONTROL, display_order = 13)]
95 pub raw_csv: bool,
96
97 /// Show code context around matches (number of lines before/after)
98 #[arg(
99 long, short = 'p', global = true, value_name = "LINES",
100 default_missing_value = "3", num_args = 0..=1,
101 help_heading = headings::OUTPUT_CONTROL, display_order = 14,
102 long_help = "Show code context around matches (number of lines before/after)\n\n\
103 Displays source code context around each match. Use -p or --preview\n\
104 for default 3 lines, or specify a number like --preview 5.\n\
105 Use --preview 0 to show only the matched line without context.\n\n\
106 Examples:\n \
107 sqry --preview main # 3 lines context (default)\n \
108 sqry -p main # Same as above\n \
109 sqry --preview 5 main # 5 lines context\n \
110 sqry --preview 0 main # No context, just matched line"
111 )]
112 pub preview: Option<usize>,
113
114 /// Disable colored output
115 #[arg(long, global = true, help_heading = headings::COMMON_OPTIONS, display_order = 14)]
116 pub no_color: bool,
117
118 /// Select output color theme (default, dark, light, none)
119 #[arg(
120 long,
121 value_enum,
122 default_value = "default",
123 global = true,
124 help_heading = headings::COMMON_OPTIONS,
125 display_order = 15
126 )]
127 pub theme: crate::output::ThemeName,
128
129 /// Sort results (opt-in)
130 #[arg(
131 long,
132 value_enum,
133 global = true,
134 help_heading = headings::OUTPUT_CONTROL,
135 display_order = 16
136 )]
137 pub sort: Option<SortField>,
138
139 // ===== Pager Flags (P2-29) =====
140 /// Enable pager for output (auto-detected by default)
141 ///
142 /// Forces output to be piped through a pager (like `less`).
143 /// In auto mode (default), paging is enabled when:
144 /// - Output exceeds terminal height
145 /// - stdout is connected to an interactive terminal
146 #[arg(
147 long,
148 global = true,
149 conflicts_with = "no_pager",
150 help_heading = headings::OUTPUT_CONTROL,
151 display_order = 17
152 )]
153 pub pager: bool,
154
155 /// Disable pager (write directly to stdout)
156 ///
157 /// Disables auto-paging, writing all output directly to stdout.
158 /// Useful for scripting or piping to other commands.
159 #[arg(
160 long,
161 global = true,
162 conflicts_with = "pager",
163 help_heading = headings::OUTPUT_CONTROL,
164 display_order = 18
165 )]
166 pub no_pager: bool,
167
168 /// Custom pager command (overrides `$SQRY_PAGER` and `$PAGER`)
169 ///
170 /// Specify a custom pager command. Supports quoted arguments.
171 /// Examples:
172 /// --pager-cmd "less -R"
173 /// --pager-cmd "bat --style=plain"
174 /// --pager-cmd "more"
175 #[arg(
176 long,
177 value_name = "COMMAND",
178 global = true,
179 help_heading = headings::OUTPUT_CONTROL,
180 display_order = 19
181 )]
182 pub pager_cmd: Option<String>,
183
184 /// Filter by symbol type (function, class, struct, etc.)
185 ///
186 /// Applies to search mode (top-level shorthand and `sqry search`).
187 /// For structured queries, use `sqry query "kind:function AND ..."` instead.
188 #[arg(long, short = 'k', value_enum, help_heading = headings::MATCH_BEHAVIOUR, display_order = 20)]
189 pub kind: Option<SymbolKind>,
190
191 /// Filter by programming language
192 ///
193 /// Applies to search mode (top-level shorthand and `sqry search`).
194 /// For structured queries, use `sqry query "lang:rust AND ..."` instead.
195 #[arg(long, short = 'l', help_heading = headings::MATCH_BEHAVIOUR, display_order = 21)]
196 pub lang: Option<String>,
197
198 /// Case-insensitive search
199 #[arg(long, short = 'i', help_heading = headings::MATCH_BEHAVIOUR, display_order = 11)]
200 pub ignore_case: bool,
201
202 /// Exact match (disable regex)
203 ///
204 /// Applies to search mode (top-level shorthand and `sqry search`).
205 #[arg(long, short = 'x', help_heading = headings::MATCH_BEHAVIOUR, display_order = 10)]
206 pub exact: bool,
207
208 /// Show count only (number of matches)
209 #[arg(long, short = 'c', help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
210 pub count: bool,
211
212 /// Maximum directory depth to search
213 #[arg(long, default_value = "32", help_heading = headings::FILE_FILTERING, display_order = 20)]
214 pub max_depth: usize,
215
216 /// Include hidden files and directories
217 #[arg(long, help_heading = headings::FILE_FILTERING, display_order = 10)]
218 pub hidden: bool,
219
220 /// Follow symlinks
221 #[arg(long, help_heading = headings::FILE_FILTERING, display_order = 11)]
222 pub follow: bool,
223
224 /// Enable fuzzy search (requires index)
225 ///
226 /// Applies to search mode (top-level shorthand and `sqry search`).
227 #[arg(long, help_heading = headings::SEARCH_MODES_FUZZY, display_order = 20)]
228 pub fuzzy: bool,
229
230 /// Fuzzy matching algorithm (jaro-winkler or levenshtein)
231 #[arg(long, default_value = "jaro-winkler", value_name = "ALGORITHM", help_heading = headings::SEARCH_MODES_FUZZY, display_order = 30)]
232 pub fuzzy_algorithm: String,
233
234 /// Minimum similarity score for fuzzy matches (0.0-1.0)
235 #[arg(long, default_value = "0.6", value_name = "SCORE", help_heading = headings::SEARCH_MODES_FUZZY, display_order = 31)]
236 pub fuzzy_threshold: f64,
237
238 /// Maximum number of fuzzy candidates to consider
239 #[arg(long, default_value = "1000", value_name = "COUNT", help_heading = headings::SEARCH_MODES_FUZZY, display_order = 40)]
240 pub fuzzy_max_candidates: usize,
241
242 /// Enable JSON streaming mode for fuzzy search
243 ///
244 /// Emits results as JSON-lines (newline-delimited JSON).
245 /// Each line is a `StreamEvent` with either a partial result or final summary.
246 /// Requires --fuzzy (fuzzy search) and is inherently JSON output.
247 #[arg(long, requires = "fuzzy", help_heading = headings::SEARCH_MODES_FUZZY, display_order = 51)]
248 pub json_stream: bool,
249
250 /// Allow fuzzy matching for query field names (opt-in).
251 /// Applies typo correction to field names (e.g., "knd" → "kind").
252 /// Ambiguous corrections are rejected with an error.
253 #[arg(long, global = true, help_heading = headings::SEARCH_MODES_FUZZY, display_order = 52)]
254 pub fuzzy_fields: bool,
255
256 /// Maximum edit distance for fuzzy field correction
257 #[arg(
258 long,
259 default_value_t = 2,
260 global = true,
261 help_heading = headings::SEARCH_MODES_FUZZY,
262 display_order = 53
263 )]
264 pub fuzzy_field_distance: usize,
265
266 /// Maximum number of results to return
267 ///
268 /// Limits the output to a manageable size for downstream consumers.
269 /// Defaults: search=100, query=1000, fuzzy=50
270 #[arg(long, global = true, help_heading = headings::OUTPUT_CONTROL, display_order = 20)]
271 pub limit: Option<usize>,
272
273 /// List enabled languages and exit
274 #[arg(long, global = true, help_heading = headings::COMMON_OPTIONS, display_order = 30)]
275 pub list_languages: bool,
276
277 /// Print cache telemetry to stderr after the command completes
278 #[arg(long, global = true, help_heading = headings::COMMON_OPTIONS, display_order = 40)]
279 pub debug_cache: bool,
280
281 /// Display fully qualified symbol names in CLI output.
282 ///
283 /// Helpful for disambiguating relation queries (callers/callees) where
284 /// multiple namespaces define the same method name.
285 #[arg(long, global = true, help_heading = headings::OUTPUT_CONTROL, display_order = 30)]
286 pub qualified_names: bool,
287
288 // ===== Index Validation Flags (P1-14) =====
289 /// Index validation strictness level (off, warn, fail)
290 ///
291 /// Controls how to handle index corruption during load:
292 /// - off: Skip validation entirely (fastest)
293 /// - warn: Log warnings but continue (default)
294 /// - fail: Abort on validation errors
295 #[arg(long, value_enum, default_value = "warn", global = true, help_heading = headings::INDEX_CONFIGURATION, display_order = 40)]
296 pub validate: ValidationMode,
297
298 /// Automatically rebuild index if validation fails
299 ///
300 /// When set, if index validation fails in strict mode, sqry will
301 /// automatically rebuild the index once and retry. Useful for
302 /// recovering from transient corruption without manual intervention.
303 #[arg(long, requires = "validate", global = true, help_heading = headings::INDEX_CONFIGURATION, display_order = 41)]
304 pub auto_rebuild: bool,
305
306 /// Maximum ratio of dangling references before rebuild (0.0-1.0)
307 ///
308 /// Sets the threshold for dangling reference errors during validation.
309 /// Default: 0.05 (5%). If more than this ratio of symbols have dangling
310 /// references, validation will fail in strict mode.
311 #[arg(long, value_name = "RATIO", global = true, help_heading = headings::INDEX_CONFIGURATION, display_order = 42)]
312 pub threshold_dangling_refs: Option<f64>,
313
314 /// Maximum ratio of orphaned files before rebuild (0.0-1.0)
315 ///
316 /// Sets the threshold for orphaned file errors during validation.
317 /// Default: 0.20 (20%). If more than this ratio of indexed files are
318 /// orphaned (no longer exist on disk), validation will fail.
319 #[arg(long, value_name = "RATIO", global = true, help_heading = headings::INDEX_CONFIGURATION, display_order = 43)]
320 pub threshold_orphaned_files: Option<f64>,
321
322 /// Maximum ratio of ID gaps before warning (0.0-1.0)
323 ///
324 /// Sets the threshold for ID gap warnings during validation.
325 /// Default: 0.10 (10%). If more than this ratio of symbol IDs have gaps,
326 /// validation will warn or fail depending on strictness.
327 #[arg(long, value_name = "RATIO", global = true, help_heading = headings::INDEX_CONFIGURATION, display_order = 44)]
328 pub threshold_id_gaps: Option<f64>,
329
330 // ===== Hybrid Search Flags =====
331 /// Force text search mode (skip semantic, use ripgrep)
332 #[arg(long, short = 't', conflicts_with = "semantic", help_heading = headings::SEARCH_MODES, display_order = 10)]
333 pub text: bool,
334
335 /// Force semantic search mode (skip text fallback)
336 #[arg(long, short = 's', conflicts_with = "text", help_heading = headings::SEARCH_MODES, display_order = 11)]
337 pub semantic: bool,
338
339 /// Disable automatic fallback to text search
340 #[arg(long, conflicts_with_all = ["text", "semantic"], help_heading = headings::SEARCH_MODES, display_order = 20)]
341 pub no_fallback: bool,
342
343 /// Number of context lines for text search results
344 #[arg(long, default_value = "2", help_heading = headings::SEARCH_MODES, display_order = 30)]
345 pub context: usize,
346
347 /// Maximum text search results
348 #[arg(long, default_value = "1000", help_heading = headings::SEARCH_MODES, display_order = 31)]
349 pub max_text_results: usize,
350}
351
352/// Batch command arguments with taxonomy headings and workflow ordering
353#[derive(Args, Debug, Clone)]
354pub struct BatchCommand {
355 /// Directory containing the indexed codebase (`.sqry/graph/snapshot.sqry`).
356 #[arg(value_name = "PATH", help_heading = headings::BATCH_INPUTS, display_order = 10)]
357 pub path: Option<String>,
358
359 /// File containing queries (one per line).
360 #[arg(long, value_name = "FILE", help_heading = headings::BATCH_INPUTS, display_order = 20)]
361 pub queries: PathBuf,
362
363 /// Set output format for results.
364 #[arg(long, value_name = "FORMAT", default_value = "text", value_enum, help_heading = headings::BATCH_OUTPUT_TARGETS, display_order = 10)]
365 pub output: BatchFormat,
366
367 /// Write results to specified file instead of stdout.
368 #[arg(long, value_name = "FILE", help_heading = headings::BATCH_OUTPUT_TARGETS, display_order = 20)]
369 pub output_file: Option<PathBuf>,
370
371 /// Continue processing if a query fails.
372 #[arg(long, help_heading = headings::BATCH_SESSION_CONTROL, display_order = 10)]
373 pub continue_on_error: bool,
374
375 /// Print aggregate statistics after completion.
376 #[arg(long, help_heading = headings::BATCH_SESSION_CONTROL, display_order = 20)]
377 pub stats: bool,
378
379 /// Use sequential execution instead of parallel (for debugging).
380 ///
381 /// By default, batch queries execute in parallel for better performance.
382 /// Use this flag to force sequential execution for debugging or profiling.
383 #[arg(long, help_heading = headings::BATCH_SESSION_CONTROL, display_order = 30)]
384 pub sequential: bool,
385}
386
387/// Completions command arguments with taxonomy headings and workflow ordering
388#[derive(Args, Debug, Clone)]
389pub struct CompletionsCommand {
390 /// Shell to generate completions for.
391 #[arg(value_enum, help_heading = headings::COMPLETIONS_SHELL_TARGETS, display_order = 10)]
392 pub shell: Shell,
393}
394
395/// Available subcommands
396#[derive(Subcommand, Debug, Clone)]
397#[command(verbatim_doc_comment)]
398pub enum Command {
399 /// Visualize code relationships as diagrams
400 #[command(display_order = 30)]
401 Visualize(VisualizeCommand),
402
403 /// Search for symbols by pattern (simple pattern matching)
404 ///
405 /// Fast pattern-based search using regex or literal matching.
406 /// Use this for quick searches with simple text patterns.
407 ///
408 /// For complex queries with boolean logic and AST predicates, use 'query' instead.
409 ///
410 /// Examples:
411 /// sqry search "test.*" # Find symbols matching regex
412 /// sqry search "test" --save-as find-tests # Save as alias
413 /// sqry search "test" --validate fail # Strict index validation
414 ///
415 /// For kind/language/fuzzy filtering, use the top-level shorthand:
416 /// sqry --kind function "test" # Filter by kind
417 /// sqry --exact "main" # Exact match
418 /// sqry --fuzzy "config" # Fuzzy search
419 ///
420 /// See also: 'sqry query' for structured AST-aware queries
421 #[command(display_order = 1, verbatim_doc_comment)]
422 Search {
423 /// Search pattern (regex or literal with --exact).
424 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
425 pattern: String,
426
427 /// Search path. For fuzzy search, walks up directory tree to find nearest .sqry-index if needed.
428 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
429 path: Option<String>,
430
431 /// Save this search as a named alias for later reuse.
432 ///
433 /// The alias can be invoked with @name syntax:
434 /// sqry search "test" --save-as find-tests
435 /// sqry @find-tests src/
436 #[arg(long, value_name = "NAME", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 10)]
437 save_as: Option<String>,
438
439 /// Save alias to global storage (~/.config/sqry/) instead of local.
440 ///
441 /// Global aliases are available across all projects.
442 /// Local aliases (default) are project-specific.
443 #[arg(long, requires = "save_as", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 20)]
444 global: bool,
445
446 /// Optional description for the saved alias.
447 #[arg(long, requires = "save_as", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 30)]
448 description: Option<String>,
449
450 /// Index validation mode before search execution.
451 ///
452 /// Controls how sqry handles stale indices (files removed since indexing):
453 /// - `warn`: Log warning but continue (default)
454 /// - `fail`: Exit with code 2 if >20% of indexed files are missing
455 /// - `off`: Skip validation entirely
456 ///
457 /// Examples:
458 /// sqry search "test" --validate fail # Strict mode
459 /// sqry search "test" --validate off # Fast mode
460 #[arg(long, value_enum, default_value = "warn", help_heading = headings::SECURITY_LIMITS, display_order = 30)]
461 validate: ValidationMode,
462 },
463
464 /// Execute AST-aware query (structured queries with boolean logic)
465 ///
466 /// Powerful structured queries using predicates and boolean operators.
467 /// Use this for complex searches that combine multiple criteria.
468 ///
469 /// For simple pattern matching, use 'search' instead.
470 ///
471 /// Predicate examples:
472 /// - kind:function # Find functions
473 /// - name:test # Name contains 'test'
474 /// - lang:rust # Rust files only
475 /// - visibility:public # Public symbols
476 /// - async:true # Async functions
477 ///
478 /// Boolean logic:
479 /// - kind:function AND name:test # Functions with 'test' in name
480 /// - kind:class OR kind:struct # All classes or structs
481 /// - lang:rust AND visibility:public # Public Rust symbols
482 ///
483 /// Relation queries (28 languages with full support):
484 /// - callers:authenticate # Who calls authenticate?
485 /// - callees:processData # What does processData call?
486 /// - exports:UserService # What does `UserService` export?
487 /// - imports:database # What imports database?
488 ///
489 /// Supported for: C, C++, C#, CSS, Dart, Elixir, Go, Groovy, Haskell, HTML,
490 /// Java, JavaScript, Kotlin, Lua, Perl, PHP, Python, R, Ruby, Rust, Scala,
491 /// Shell, SQL, Svelte, Swift, TypeScript, Vue, Zig
492 ///
493 /// Saving as alias:
494 /// sqry query "kind:function AND name:test" --save-as test-funcs
495 /// sqry @test-funcs src/
496 ///
497 /// See also: 'sqry search' for simple pattern-based searches
498 #[command(display_order = 2, verbatim_doc_comment)]
499 Query {
500 /// Query expression with predicates.
501 #[arg(help_heading = headings::QUERY_INPUT, display_order = 10)]
502 query: String,
503
504 /// Search path. If no index exists here, walks up directory tree to find nearest .sqry-index.
505 #[arg(help_heading = headings::QUERY_INPUT, display_order = 20)]
506 path: Option<String>,
507
508 /// Use persistent session (keeps .sqry-index hot for repeated queries).
509 #[arg(long, help_heading = headings::PERFORMANCE_DEBUGGING, display_order = 10)]
510 session: bool,
511
512 /// Explain query execution (debug mode).
513 #[arg(long, help_heading = headings::PERFORMANCE_DEBUGGING, display_order = 20)]
514 explain: bool,
515
516 /// Disable parallel query execution (for A/B performance testing).
517 ///
518 /// By default, OR branches (3+) and symbol filtering (100+) use parallel execution.
519 /// Use this flag to force sequential execution for performance comparison.
520 #[arg(long, help_heading = headings::PERFORMANCE_DEBUGGING, display_order = 30)]
521 no_parallel: bool,
522
523 /// Show verbose output including cache statistics.
524 #[arg(long, short = 'v', help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
525 verbose: bool,
526
527 /// Maximum query execution time in seconds (default: 30s, max: 30s).
528 ///
529 /// Queries exceeding this limit will be terminated with partial results.
530 /// The 30-second ceiling is a NON-NEGOTIABLE security requirement.
531 /// Specify lower values for faster feedback on interactive queries.
532 ///
533 /// Examples:
534 /// sqry query --timeout 10 "impl:Debug" # 10 second timeout
535 /// sqry query --timeout 5 "kind:function" # 5 second timeout
536 #[arg(long, value_name = "SECS", help_heading = headings::SECURITY_LIMITS, display_order = 10)]
537 timeout: Option<u64>,
538
539 /// Maximum number of results to return (default: 10000).
540 ///
541 /// Queries returning more results will be truncated.
542 /// Use this to limit memory usage for large result sets.
543 ///
544 /// Examples:
545 /// sqry query --limit 100 "kind:function" # First 100 functions
546 /// sqry query --limit 1000 "impl:Debug" # First 1000 Debug impls
547 #[arg(long, value_name = "N", help_heading = headings::SECURITY_LIMITS, display_order = 20)]
548 limit: Option<usize>,
549
550 /// Save this query as a named alias for later reuse.
551 ///
552 /// The alias can be invoked with @name syntax:
553 /// sqry query "kind:function" --save-as all-funcs
554 /// sqry @all-funcs src/
555 #[arg(long, value_name = "NAME", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 10)]
556 save_as: Option<String>,
557
558 /// Save alias to global storage (~/.config/sqry/) instead of local.
559 ///
560 /// Global aliases are available across all projects.
561 /// Local aliases (default) are project-specific.
562 #[arg(long, requires = "save_as", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 20)]
563 global: bool,
564
565 /// Optional description for the saved alias.
566 #[arg(long, requires = "save_as", help_heading = headings::PERSISTENCE_OPTIONS, display_order = 30)]
567 description: Option<String>,
568
569 /// Index validation mode before query execution.
570 ///
571 /// Controls how sqry handles stale indices (files removed since indexing):
572 /// - `warn`: Log warning but continue (default)
573 /// - `fail`: Exit with code 2 if >20% of indexed files are missing
574 /// - `off`: Skip validation entirely
575 ///
576 /// Examples:
577 /// sqry query "kind:function" --validate fail # Strict mode
578 /// sqry query "kind:function" --validate off # Fast mode
579 #[arg(long, value_enum, default_value = "warn", help_heading = headings::SECURITY_LIMITS, display_order = 30)]
580 validate: ValidationMode,
581
582 /// Substitute variables in the query expression.
583 ///
584 /// Variables are referenced as $name in queries and resolved before execution.
585 /// Specify as KEY=VALUE pairs; can be repeated.
586 ///
587 /// Examples:
588 /// sqry query "kind:\$type" --var type=function
589 /// sqry query "kind:\$k AND lang:\$l" --var k=function --var l=rust
590 #[arg(long = "var", value_name = "KEY=VALUE", help_heading = headings::QUERY_INPUT, display_order = 30)]
591 var: Vec<String>,
592 },
593
594 /// Graph-based queries and analysis
595 ///
596 /// Advanced graph operations using the unified graph architecture.
597 /// All subcommands are noun-based and represent different analysis types.
598 ///
599 /// Available analyses:
600 /// - `trace-path <from> <to>` # Find shortest path between symbols
601 /// - `call-chain-depth <symbol>` # Calculate maximum call depth
602 /// - `dependency-tree <module>` # Show transitive dependencies
603 /// - nodes # List unified graph nodes
604 /// - edges # List unified graph edges
605 /// - cross-language # List cross-language relationships
606 /// - stats # Show graph statistics
607 /// - cycles # Detect circular dependencies
608 /// - complexity # Calculate code complexity
609 ///
610 /// All commands support --format json for programmatic use.
611 #[command(display_order = 20)]
612 Graph {
613 #[command(subcommand)]
614 operation: GraphOperation,
615
616 /// Search path (defaults to current directory).
617 #[arg(long, help_heading = headings::GRAPH_CONFIGURATION, display_order = 10)]
618 path: Option<String>,
619
620 /// Output format (json, text, dot, mermaid, d2).
621 #[arg(long, short = 'f', default_value = "text", help_heading = headings::GRAPH_CONFIGURATION, display_order = 20)]
622 format: String,
623
624 /// Show verbose output with detailed metadata.
625 #[arg(long, short = 'v', help_heading = headings::GRAPH_CONFIGURATION, display_order = 30)]
626 verbose: bool,
627 },
628
629 /// Start an interactive shell that keeps the session cache warm
630 #[command(display_order = 60)]
631 Shell {
632 /// Directory containing the `.sqry-index` file.
633 #[arg(value_name = "PATH", help_heading = headings::SHELL_CONFIGURATION, display_order = 10)]
634 path: Option<String>,
635 },
636
637 /// Execute multiple queries from a batch file using a warm session
638 #[command(display_order = 61)]
639 Batch(BatchCommand),
640
641 /// Build symbol index and graph analyses for fast queries
642 ///
643 /// Creates a persistent index of all symbols in the specified directory.
644 /// The index is saved to .sqry/ and includes precomputed graph analyses
645 /// for cycle detection, reachability, and path queries.
646 /// Uses parallel processing by default for faster indexing.
647 #[command(display_order = 10)]
648 Index {
649 /// Directory to index (defaults to current directory).
650 #[arg(help_heading = headings::INDEX_INPUT, display_order = 10)]
651 path: Option<String>,
652
653 /// Force rebuild even if index exists.
654 #[arg(long, short = 'f', alias = "rebuild", help_heading = headings::INDEX_CONFIGURATION, display_order = 10)]
655 force: bool,
656
657 /// Show index status without building.
658 ///
659 /// Returns metadata about the existing index (age, symbol count, languages).
660 /// Useful for programmatic consumers to check if indexing is needed.
661 #[arg(long, short = 's', help_heading = headings::INDEX_CONFIGURATION, display_order = 20)]
662 status: bool,
663
664 /// Automatically add .sqry-index/ to .gitignore if not already present.
665 #[arg(long, help_heading = headings::INDEX_CONFIGURATION, display_order = 30)]
666 add_to_gitignore: bool,
667
668 /// Number of threads for parallel indexing (default: auto-detect).
669 ///
670 /// Set to 1 for single-threaded (useful for debugging).
671 /// Defaults to number of CPU cores.
672 #[arg(long, short = 't', help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
673 threads: Option<usize>,
674
675 /// Disable incremental indexing (hash-based change detection).
676 ///
677 /// When set, indexing will skip the persistent hash index and avoid
678 /// hash-based change detection entirely. Useful for debugging or
679 /// forcing metadata-only evaluation.
680 #[arg(long = "no-incremental", help_heading = headings::PERFORMANCE_TUNING, display_order = 20)]
681 no_incremental: bool,
682
683 /// Override cache directory for incremental indexing (default: .sqry-cache).
684 ///
685 /// Points sqry at an alternate cache location for the hash index.
686 /// Handy for ephemeral or sandboxed environments.
687 #[arg(long = "cache-dir", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 10)]
688 cache_dir: Option<String>,
689
690 /// Disable index compression (P1-12: Index Compression).
691 ///
692 /// By default, indexes are compressed with zstd for faster load times
693 /// and reduced disk space. Use this flag to store uncompressed indexes
694 /// (useful for debugging or compatibility testing).
695 #[arg(long, help_heading = headings::ADVANCED_CONFIGURATION, display_order = 20)]
696 no_compress: bool,
697
698 /// Metrics export format for validation status (json or prometheus).
699 ///
700 /// Used with --status --json to export validation metrics in different
701 /// formats. Prometheus format outputs OpenMetrics-compatible text for
702 /// monitoring systems. JSON format (default) provides structured data.
703 #[arg(long, short = 'M', value_enum, default_value = "json", requires = "status", help_heading = headings::OUTPUT_CONTROL, display_order = 30)]
704 metrics_format: MetricsFormat,
705 },
706
707 /// Build precomputed graph analyses for fast query performance
708 ///
709 /// Computes CSR adjacency, SCC (Strongly Connected Components), condensation DAGs,
710 /// and 2-hop interval labels to eliminate O(V+E) query-time costs. Analysis files
711 /// are persisted to .sqry/analysis/ and enable fast cycle detection, reachability
712 /// queries, and path finding.
713 ///
714 /// Note: `sqry index` already builds a ready graph with analysis artifacts.
715 /// Run `sqry analyze` when you want to rebuild analyses with explicit
716 /// tuning controls or after changing analysis configuration.
717 ///
718 /// Examples:
719 /// sqry analyze # Rebuild analyses for current index
720 /// sqry analyze --force # Force analysis rebuild
721 #[command(display_order = 13, verbatim_doc_comment)]
722 Analyze {
723 /// Search path (defaults to current directory).
724 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
725 path: Option<String>,
726
727 /// Force rebuild even if analysis files exist.
728 #[arg(long, short = 'f', help_heading = headings::INDEX_CONFIGURATION, display_order = 10)]
729 force: bool,
730
731 /// Number of threads for parallel analysis (default: auto-detect).
732 ///
733 /// Controls the rayon thread pool size for SCC/condensation DAG
734 /// computation. Set to 1 for single-threaded (useful for debugging).
735 /// Defaults to number of CPU cores.
736 #[arg(long, short = 't', help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
737 threads: Option<usize>,
738
739 /// Override maximum 2-hop label intervals per edge kind.
740 ///
741 /// Controls the maximum number of reachability intervals computed
742 /// per edge kind. Larger budgets enable O(1) reachability queries
743 /// but use more memory. Default: from config or 15,000,000.
744 #[arg(long = "label-budget", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 30)]
745 label_budget: Option<u64>,
746
747 /// Override density gate threshold.
748 ///
749 /// Skip 2-hop label computation when `condensation_edges > threshold * scc_count`.
750 /// Prevents multi-minute hangs on dense import/reference graphs.
751 /// 0 = disabled. Default: from config or 64.
752 #[arg(long = "density-threshold", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 31)]
753 density_threshold: Option<u64>,
754
755 /// Override budget-exceeded policy: `"degrade"` (BFS fallback) or `"fail"`.
756 ///
757 /// When the label budget is exceeded for an edge kind:
758 /// - `"degrade"`: Fall back to BFS on the condensation DAG (default)
759 /// - "fail": Return an error and abort analysis
760 #[arg(long = "budget-exceeded-policy", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 32, value_parser = clap::builder::PossibleValuesParser::new(["degrade", "fail"]))]
761 budget_exceeded_policy: Option<String>,
762
763 /// Skip 2-hop interval label computation entirely.
764 ///
765 /// When set, the analysis builds CSR + SCC + Condensation DAG but skips
766 /// the expensive 2-hop label phase. Reachability queries fall back to BFS
767 /// on the condensation DAG (~10-50ms per query instead of O(1)).
768 /// Useful for very large codebases where label computation is too slow.
769 #[arg(long = "no-labels", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 33)]
770 no_labels: bool,
771 },
772
773 /// Start the sqry Language Server Protocol endpoint
774 #[command(display_order = 50)]
775 Lsp {
776 #[command(flatten)]
777 options: LspOptions,
778 },
779
780 /// Update existing symbol index
781 ///
782 /// Incrementally updates the index by re-indexing only changed files.
783 /// Much faster than a full rebuild for large codebases.
784 #[command(display_order = 11)]
785 Update {
786 /// Directory with existing index (defaults to current directory).
787 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
788 path: Option<String>,
789
790 /// Number of threads for parallel indexing (default: auto-detect).
791 ///
792 /// Set to 1 for single-threaded (useful for debugging).
793 /// Defaults to number of CPU cores.
794 #[arg(long, short = 't', help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
795 threads: Option<usize>,
796
797 /// Disable incremental indexing (force metadata-only or full updates).
798 ///
799 /// When set, the update process will not use the hash index and will
800 /// rely on metadata-only checks for staleness.
801 #[arg(long = "no-incremental", help_heading = headings::UPDATE_CONFIGURATION, display_order = 10)]
802 no_incremental: bool,
803
804 /// Override cache directory for incremental indexing (default: .sqry-cache).
805 ///
806 /// Points sqry at an alternate cache location for the hash index.
807 #[arg(long = "cache-dir", help_heading = headings::ADVANCED_CONFIGURATION, display_order = 10)]
808 cache_dir: Option<String>,
809
810 /// Show statistics about the update.
811 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
812 stats: bool,
813 },
814
815 /// Watch directory and auto-update index on file changes
816 ///
817 /// Monitors the directory for file system changes and automatically updates
818 /// the index in real-time. Uses OS-level file monitoring (inotify/FSEvents/Windows)
819 /// for <1ms change detection latency.
820 ///
821 /// Press Ctrl+C to stop watching.
822 #[command(display_order = 12)]
823 Watch {
824 /// Directory to watch (defaults to current directory).
825 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
826 path: Option<String>,
827
828 /// Number of threads for parallel indexing (default: auto-detect).
829 ///
830 /// Set to 1 for single-threaded (useful for debugging).
831 /// Defaults to number of CPU cores.
832 #[arg(long, short = 't', help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
833 threads: Option<usize>,
834
835 /// Build initial index if it doesn't exist.
836 #[arg(long, help_heading = headings::WATCH_CONFIGURATION, display_order = 10)]
837 build: bool,
838
839 /// Debounce duration in milliseconds.
840 ///
841 /// Wait time after detecting a change before processing to collect
842 /// rapid-fire changes (e.g., from editor saves).
843 ///
844 /// Default is platform-aware: 400ms on macOS, 100ms on Linux/Windows.
845 /// Can also be set via `SQRY_LIMITS__WATCH__DEBOUNCE_MS` env var.
846 #[arg(long, short = 'd', help_heading = headings::WATCH_CONFIGURATION, display_order = 20)]
847 debounce: Option<u64>,
848
849 /// Show detailed statistics for each update.
850 #[arg(long, short = 's', help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
851 stats: bool,
852 },
853
854 /// Repair corrupted index by fixing common issues
855 ///
856 /// Automatically detects and fixes common index corruption issues:
857 /// - Orphaned symbols (files no longer exist)
858 /// - Dangling references (symbols reference non-existent dependencies)
859 /// - Invalid checksums
860 ///
861 /// Use --dry-run to preview changes without modifying the index.
862 #[command(display_order = 14)]
863 Repair {
864 /// Directory with existing index (defaults to current directory).
865 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
866 path: Option<String>,
867
868 /// Remove symbols for files that no longer exist on disk.
869 #[arg(long, help_heading = headings::REPAIR_OPTIONS, display_order = 10)]
870 fix_orphans: bool,
871
872 /// Remove dangling references to non-existent symbols.
873 #[arg(long, help_heading = headings::REPAIR_OPTIONS, display_order = 20)]
874 fix_dangling: bool,
875
876 /// Recompute index checksum after repairs.
877 #[arg(long, help_heading = headings::REPAIR_OPTIONS, display_order = 30)]
878 recompute_checksum: bool,
879
880 /// Fix all detected issues (combines all repair options).
881 #[arg(long, conflicts_with_all = ["fix_orphans", "fix_dangling", "recompute_checksum"], help_heading = headings::REPAIR_OPTIONS, display_order = 5)]
882 fix_all: bool,
883
884 /// Preview changes without modifying the index (dry run).
885 #[arg(long, help_heading = headings::REPAIR_OPTIONS, display_order = 40)]
886 dry_run: bool,
887 },
888
889 /// Manage AST cache
890 ///
891 /// Control the disk-persisted AST cache that speeds up queries by avoiding
892 /// expensive tree-sitter parsing. The cache is stored in .sqry-cache/ and
893 /// is shared across all sqry processes.
894 #[command(display_order = 41)]
895 Cache {
896 #[command(subcommand)]
897 action: CacheAction,
898 },
899
900 /// Manage graph config (.sqry/graph/config/config.json)
901 ///
902 /// Configure sqry behavior through the unified config partition.
903 /// All settings are stored in `.sqry/graph/config/config.json`.
904 ///
905 /// Examples:
906 /// sqry config init # Initialize config with defaults
907 /// sqry config show # Display effective config
908 /// sqry config set `limits.max_results` 10000 # Update a setting
909 /// sqry config get `limits.max_results` # Get a single value
910 /// sqry config validate # Validate config file
911 /// sqry config alias set my-funcs "kind:function" # Create alias
912 /// sqry config alias list # List all aliases
913 #[command(display_order = 40, verbatim_doc_comment)]
914 Config {
915 #[command(subcommand)]
916 action: ConfigAction,
917 },
918
919 /// Generate shell completions
920 ///
921 /// Generate shell completion scripts for bash, zsh, fish, or `PowerShell`.
922 /// Install by redirecting output to the appropriate location for your shell.
923 ///
924 /// Examples:
925 /// sqry completions bash > /`etc/bash_completion.d/sqry`
926 /// sqry completions zsh > ~/.zfunc/_sqry
927 /// sqry completions fish > ~/.config/fish/completions/sqry.fish
928 #[command(display_order = 45, verbatim_doc_comment)]
929 Completions(CompletionsCommand),
930
931 /// Manage multi-repository workspaces
932 #[command(display_order = 42)]
933 Workspace {
934 #[command(subcommand)]
935 action: WorkspaceCommand,
936 },
937
938 /// Manage saved query aliases
939 ///
940 /// Save frequently used queries as named aliases for easy reuse.
941 /// Aliases can be stored globally (~/.config/sqry/) or locally (.sqry-index.user).
942 ///
943 /// Examples:
944 /// sqry alias list # List all aliases
945 /// sqry alias show my-funcs # Show alias details
946 /// sqry alias delete my-funcs # Delete an alias
947 /// sqry alias rename old-name new # Rename an alias
948 ///
949 /// To create an alias, use --save-as with search/query commands:
950 /// sqry query "kind:function" --save-as my-funcs
951 /// sqry search "test" --save-as find-tests --global
952 ///
953 /// To execute an alias, use @name syntax:
954 /// sqry @my-funcs
955 /// sqry @find-tests src/
956 #[command(display_order = 43, verbatim_doc_comment)]
957 Alias {
958 #[command(subcommand)]
959 action: AliasAction,
960 },
961
962 /// Manage query history
963 ///
964 /// View and manage your query history. History is recorded automatically
965 /// for search and query commands (unless disabled via `SQRY_NO_HISTORY=1`).
966 ///
967 /// Examples:
968 /// sqry history list # List recent queries
969 /// sqry history list --limit 50 # Show last 50 queries
970 /// sqry history search "function" # Search history
971 /// sqry history clear # Clear all history
972 /// sqry history clear --older 30d # Clear entries older than 30 days
973 /// sqry history stats # Show history statistics
974 ///
975 /// Sensitive data (API keys, tokens) is automatically redacted.
976 #[command(display_order = 44, verbatim_doc_comment)]
977 History {
978 #[command(subcommand)]
979 action: HistoryAction,
980 },
981
982 /// Natural language interface for sqry queries
983 ///
984 /// Translate natural language descriptions into sqry commands.
985 /// Uses a safety-focused translation pipeline that validates all
986 /// generated commands before execution.
987 ///
988 /// Response tiers based on confidence:
989 /// - Execute (≥85%): Run command automatically
990 /// - Confirm (65-85%): Ask for user confirmation
991 /// - Disambiguate (<65%): Present options to choose from
992 /// - Reject: Cannot safely translate
993 ///
994 /// Examples:
995 /// sqry ask "find all public functions in rust"
996 /// sqry ask "who calls authenticate"
997 /// sqry ask "trace path from main to database"
998 /// sqry ask --auto-execute "find all classes"
999 ///
1000 /// Safety: Commands are validated against a whitelist and checked
1001 /// for shell injection, path traversal, and other attacks.
1002 #[command(display_order = 3, verbatim_doc_comment)]
1003 Ask {
1004 /// Natural language query to translate.
1005 #[arg(help_heading = headings::NL_INPUT, display_order = 10)]
1006 query: String,
1007
1008 /// Search path (defaults to current directory).
1009 #[arg(help_heading = headings::NL_INPUT, display_order = 20)]
1010 path: Option<String>,
1011
1012 /// Auto-execute high-confidence commands without confirmation.
1013 ///
1014 /// When enabled, commands with ≥85% confidence will execute
1015 /// immediately. Otherwise, all commands require confirmation.
1016 #[arg(long, help_heading = headings::NL_CONFIGURATION, display_order = 10)]
1017 auto_execute: bool,
1018
1019 /// Show the translated command without executing.
1020 ///
1021 /// Useful for understanding what command would be generated
1022 /// from your natural language query.
1023 #[arg(long, help_heading = headings::NL_CONFIGURATION, display_order = 20)]
1024 dry_run: bool,
1025
1026 /// Minimum confidence threshold for auto-execution (0.0-1.0).
1027 ///
1028 /// Commands with confidence below this threshold will always
1029 /// require confirmation, even with --auto-execute.
1030 #[arg(long, default_value = "0.85", help_heading = headings::NL_CONFIGURATION, display_order = 30)]
1031 threshold: f32,
1032 },
1033
1034 /// View usage insights and manage local diagnostics
1035 ///
1036 /// sqry captures anonymous behavioral patterns locally to help you
1037 /// understand your usage and improve the tool. All data stays on
1038 /// your machine unless you explicitly choose to share.
1039 ///
1040 /// Examples:
1041 /// sqry insights show # Show current week's summary
1042 /// sqry insights show --week 2025-W50 # Show specific week
1043 /// sqry insights config # Show configuration
1044 /// sqry insights config --disable # Disable uses capture
1045 /// sqry insights status # Show storage status
1046 /// sqry insights prune --older 90d # Clean up old data
1047 ///
1048 /// Privacy: All data is stored locally. No network calls are made
1049 /// unless you explicitly use --share (which generates a file, not
1050 /// a network request).
1051 #[command(display_order = 62, verbatim_doc_comment)]
1052 Insights {
1053 #[command(subcommand)]
1054 action: InsightsAction,
1055 },
1056
1057 /// Generate a troubleshooting bundle for issue reporting
1058 ///
1059 /// Creates a structured bundle containing diagnostic information
1060 /// that can be shared with the sqry team. All data is sanitized -
1061 /// no code content, file paths, or secrets are included.
1062 ///
1063 /// The bundle includes:
1064 /// - System information (OS, architecture)
1065 /// - sqry version and build type
1066 /// - Sanitized configuration
1067 /// - Recent use events (last 24h)
1068 /// - Recent errors
1069 ///
1070 /// Examples:
1071 /// sqry troubleshoot # Generate to stdout
1072 /// sqry troubleshoot -o bundle.json # Save to file
1073 /// sqry troubleshoot --dry-run # Preview without generating
1074 /// sqry troubleshoot --include-trace # Include workflow trace
1075 ///
1076 /// Privacy: No paths, code content, or secrets are included.
1077 /// Review the output before sharing if you have concerns.
1078 #[command(display_order = 63, verbatim_doc_comment)]
1079 Troubleshoot {
1080 /// Output file path (default: stdout)
1081 #[arg(short = 'o', long, value_name = "FILE", help_heading = headings::INSIGHTS_OUTPUT, display_order = 10)]
1082 output: Option<String>,
1083
1084 /// Preview bundle contents without generating
1085 #[arg(long = "dry-run", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
1086 dry_run: bool,
1087
1088 /// Include workflow trace (opt-in, requires explicit consent)
1089 ///
1090 /// Adds a sequence of recent workflow steps to the bundle.
1091 /// The trace helps understand how operations were performed
1092 /// but reveals more behavioral patterns than the default bundle.
1093 #[arg(long, help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 20)]
1094 include_trace: bool,
1095
1096 /// Time window for events to include (e.g., 24h, 7d)
1097 ///
1098 /// Defaults to 24 hours. Longer windows provide more context
1099 /// but may include older events.
1100 #[arg(long, default_value = "24h", value_name = "DURATION", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 30)]
1101 window: String,
1102 },
1103
1104 /// Find duplicate code in the codebase
1105 ///
1106 /// Detects similar or identical code patterns using structural analysis.
1107 /// Supports different duplicate types:
1108 /// - body: Functions with identical/similar bodies
1109 /// - signature: Functions with identical signatures
1110 /// - struct: Structs with similar field layouts
1111 ///
1112 /// Examples:
1113 /// sqry duplicates # Find body duplicates
1114 /// sqry duplicates --type signature # Find signature duplicates
1115 /// sqry duplicates --threshold 90 # 90% similarity threshold
1116 /// sqry duplicates --exact # Exact matches only
1117 #[command(display_order = 21, verbatim_doc_comment)]
1118 Duplicates {
1119 /// Search path (defaults to current directory).
1120 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1121 path: Option<String>,
1122
1123 /// Type of duplicate detection.
1124 ///
1125 /// - body: Functions with identical/similar bodies (default)
1126 /// - signature: Functions with identical signatures
1127 /// - struct: Structs with similar field layouts
1128 #[arg(long, short = 't', default_value = "body", help_heading = headings::DUPLICATE_OPTIONS, display_order = 10)]
1129 r#type: String,
1130
1131 /// Similarity threshold (0-100, default: 80).
1132 ///
1133 /// Higher values require more similarity to be considered duplicates.
1134 /// 100 means exact matches only.
1135 #[arg(long, default_value = "80", help_heading = headings::DUPLICATE_OPTIONS, display_order = 20)]
1136 threshold: u32,
1137
1138 /// Maximum results to return.
1139 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1140 max_results: usize,
1141
1142 /// Exact matches only (equivalent to --threshold 100).
1143 #[arg(long, help_heading = headings::DUPLICATE_OPTIONS, display_order = 30)]
1144 exact: bool,
1145 },
1146
1147 /// Find circular dependencies in the codebase
1148 ///
1149 /// Detects cycles in call graphs, import graphs, or module dependencies.
1150 /// Uses Tarjan's SCC algorithm for efficient O(V+E) detection.
1151 ///
1152 /// Examples:
1153 /// sqry cycles # Find call cycles
1154 /// sqry cycles --type imports # Find import cycles
1155 /// sqry cycles --min-depth 3 # Cycles with 3+ nodes
1156 /// sqry cycles --include-self # Include self-loops
1157 #[command(display_order = 22, verbatim_doc_comment)]
1158 Cycles {
1159 /// Search path (defaults to current directory).
1160 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1161 path: Option<String>,
1162
1163 /// Type of cycle detection.
1164 ///
1165 /// - calls: Function/method call cycles (default)
1166 /// - imports: File import cycles
1167 /// - modules: Module-level cycles
1168 #[arg(long, short = 't', default_value = "calls", help_heading = headings::CYCLE_OPTIONS, display_order = 10)]
1169 r#type: String,
1170
1171 /// Minimum cycle depth (default: 2).
1172 #[arg(long, default_value = "2", help_heading = headings::CYCLE_OPTIONS, display_order = 20)]
1173 min_depth: usize,
1174
1175 /// Maximum cycle depth (default: unlimited).
1176 #[arg(long, help_heading = headings::CYCLE_OPTIONS, display_order = 30)]
1177 max_depth: Option<usize>,
1178
1179 /// Include self-loops (A → A).
1180 #[arg(long, help_heading = headings::CYCLE_OPTIONS, display_order = 40)]
1181 include_self: bool,
1182
1183 /// Maximum results to return.
1184 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1185 max_results: usize,
1186 },
1187
1188 /// Find unused/dead code in the codebase
1189 ///
1190 /// Detects symbols that are never referenced using reachability analysis.
1191 /// Entry points (main, public lib exports, tests) are considered reachable.
1192 ///
1193 /// Examples:
1194 /// sqry unused # Find all unused symbols
1195 /// sqry unused --scope public # Only public unused symbols
1196 /// sqry unused --scope function # Only unused functions
1197 /// sqry unused --lang rust # Only in Rust files
1198 #[command(display_order = 23, verbatim_doc_comment)]
1199 Unused {
1200 /// Search path (defaults to current directory).
1201 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1202 path: Option<String>,
1203
1204 /// Scope of unused detection.
1205 ///
1206 /// - all: All unused symbols (default)
1207 /// - public: Public symbols with no external references
1208 /// - private: Private symbols with no references
1209 /// - function: Unused functions only
1210 /// - struct: Unused structs/types only
1211 #[arg(long, short = 's', default_value = "all", help_heading = headings::UNUSED_OPTIONS, display_order = 10)]
1212 scope: String,
1213
1214 /// Filter by language.
1215 #[arg(long, help_heading = headings::UNUSED_OPTIONS, display_order = 20)]
1216 lang: Option<String>,
1217
1218 /// Filter by symbol kind.
1219 #[arg(long, help_heading = headings::UNUSED_OPTIONS, display_order = 30)]
1220 kind: Option<String>,
1221
1222 /// Maximum results to return.
1223 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1224 max_results: usize,
1225 },
1226
1227 /// Export the code graph in various formats
1228 ///
1229 /// Exports the unified code graph to DOT, D2, Mermaid, or JSON formats
1230 /// for visualization or further analysis.
1231 ///
1232 /// Examples:
1233 /// sqry export # DOT format to stdout
1234 /// sqry export --format mermaid # Mermaid format
1235 /// sqry export --format d2 -o graph.d2 # D2 format to file
1236 /// sqry export --highlight-cross # Highlight cross-language edges
1237 /// sqry export --filter-lang rust,python # Filter languages
1238 #[command(display_order = 31, verbatim_doc_comment)]
1239 Export {
1240 /// Search path (defaults to current directory).
1241 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1242 path: Option<String>,
1243
1244 /// Output format.
1245 ///
1246 /// - dot: Graphviz DOT format (default)
1247 /// - d2: D2 diagram format
1248 /// - mermaid: Mermaid markdown format
1249 /// - json: JSON format for programmatic use
1250 #[arg(long, short = 'f', default_value = "dot", help_heading = headings::EXPORT_OPTIONS, display_order = 10)]
1251 format: String,
1252
1253 /// Graph layout direction.
1254 ///
1255 /// - lr: Left to right (default)
1256 /// - tb: Top to bottom
1257 #[arg(long, short = 'd', default_value = "lr", help_heading = headings::EXPORT_OPTIONS, display_order = 20)]
1258 direction: String,
1259
1260 /// Filter by languages (comma-separated).
1261 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 30)]
1262 filter_lang: Option<String>,
1263
1264 /// Filter by edge types (comma-separated: calls,imports,exports).
1265 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 40)]
1266 filter_edge: Option<String>,
1267
1268 /// Highlight cross-language edges.
1269 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 50)]
1270 highlight_cross: bool,
1271
1272 /// Show node details (signatures, docs).
1273 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 60)]
1274 show_details: bool,
1275
1276 /// Show edge labels.
1277 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 70)]
1278 show_labels: bool,
1279
1280 /// Output file (default: stdout).
1281 #[arg(long, short = 'o', help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1282 output: Option<String>,
1283 },
1284
1285 /// Explain a symbol with context and relations
1286 ///
1287 /// Get detailed information about a symbol including its code context,
1288 /// callers, callees, and other relationships.
1289 ///
1290 /// Examples:
1291 /// sqry explain src/main.rs main # Explain main function
1292 /// sqry explain src/lib.rs `MyStruct` # Explain a struct
1293 /// sqry explain --no-context file.rs func # Skip code context
1294 /// sqry explain --no-relations file.rs fn # Skip relations
1295 #[command(alias = "exp", display_order = 26, verbatim_doc_comment)]
1296 Explain {
1297 /// File containing the symbol.
1298 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1299 file: String,
1300
1301 /// Symbol name to explain.
1302 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1303 symbol: String,
1304
1305 /// Search path (defaults to current directory).
1306 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1307 path: Option<String>,
1308
1309 /// Skip code context in output.
1310 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1311 no_context: bool,
1312
1313 /// Skip relation information in output.
1314 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 20)]
1315 no_relations: bool,
1316 },
1317
1318 /// Find symbols similar to a reference symbol
1319 ///
1320 /// Uses fuzzy name matching to find symbols that are similar
1321 /// to a given reference symbol.
1322 ///
1323 /// Examples:
1324 /// sqry similar src/lib.rs processData # Find similar to processData
1325 /// sqry similar --threshold 0.8 file.rs fn # 80% similarity threshold
1326 /// sqry similar --limit 20 file.rs func # Limit to 20 results
1327 #[command(alias = "sim", display_order = 27, verbatim_doc_comment)]
1328 Similar {
1329 /// File containing the reference symbol.
1330 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1331 file: String,
1332
1333 /// Reference symbol name.
1334 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1335 symbol: String,
1336
1337 /// Search path (defaults to current directory).
1338 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1339 path: Option<String>,
1340
1341 /// Minimum similarity threshold (0.0 to 1.0, default: 0.7).
1342 #[arg(long, short = 't', default_value = "0.7", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1343 threshold: f64,
1344
1345 /// Maximum results to return (default: 20).
1346 #[arg(long, short = 'l', default_value = "20", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1347 limit: usize,
1348 },
1349
1350 /// Extract a focused subgraph around seed symbols
1351 ///
1352 /// Collects nodes and edges within a specified depth from seed symbols,
1353 /// useful for understanding local code structure.
1354 ///
1355 /// Examples:
1356 /// sqry subgraph main # Subgraph around main
1357 /// sqry subgraph -d 3 func1 func2 # Depth 3, multiple seeds
1358 /// sqry subgraph --no-callers main # Only callees
1359 /// sqry subgraph --include-imports main # Include import edges
1360 #[command(alias = "sub", display_order = 28, verbatim_doc_comment)]
1361 Subgraph {
1362 /// Seed symbol names (at least one required).
1363 #[arg(required = true, help_heading = headings::SEARCH_INPUT, display_order = 10)]
1364 symbols: Vec<String>,
1365
1366 /// Search path (defaults to current directory).
1367 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1368 path: Option<String>,
1369
1370 /// Maximum traversal depth from seeds (default: 2).
1371 #[arg(long, short = 'd', default_value = "2", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1372 depth: usize,
1373
1374 /// Maximum nodes to include (default: 50).
1375 #[arg(long, short = 'n', default_value = "50", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1376 max_nodes: usize,
1377
1378 /// Exclude callers (incoming edges).
1379 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1380 no_callers: bool,
1381
1382 /// Exclude callees (outgoing edges).
1383 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1384 no_callees: bool,
1385
1386 /// Include import relationships.
1387 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
1388 include_imports: bool,
1389 },
1390
1391 /// Analyze what would break if a symbol changes
1392 ///
1393 /// Performs reverse dependency analysis to find all symbols
1394 /// that directly or indirectly depend on the target.
1395 ///
1396 /// Examples:
1397 /// sqry impact authenticate # Impact of changing authenticate
1398 /// sqry impact -d 5 `MyClass` # Deep analysis (5 levels)
1399 /// sqry impact --direct-only func # Only direct dependents
1400 /// sqry impact --show-files func # Show affected files
1401 #[command(alias = "imp", display_order = 24, verbatim_doc_comment)]
1402 Impact {
1403 /// Symbol to analyze.
1404 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1405 symbol: String,
1406
1407 /// Search path (defaults to current directory).
1408 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1409 path: Option<String>,
1410
1411 /// Maximum analysis depth (default: 3).
1412 #[arg(long, short = 'd', default_value = "3", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1413 depth: usize,
1414
1415 /// Maximum results to return (default: 100).
1416 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1417 limit: usize,
1418
1419 /// Only show direct dependents (depth 1).
1420 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1421 direct_only: bool,
1422
1423 /// Show list of affected files.
1424 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1425 show_files: bool,
1426 },
1427
1428 /// Compare semantic changes between git refs
1429 ///
1430 /// Analyzes AST differences between two git refs to detect added, removed,
1431 /// modified, and renamed symbols. Provides structured output showing what
1432 /// changed semantically, not just textually.
1433 ///
1434 /// Examples:
1435 /// sqry diff main HEAD # Compare branches
1436 /// sqry diff v1.0.0 v2.0.0 --json # Release comparison
1437 /// sqry diff HEAD~5 HEAD --kind function # Functions only
1438 /// sqry diff main feature --change-type added # New symbols only
1439 #[command(alias = "sdiff", display_order = 25, verbatim_doc_comment)]
1440 Diff {
1441 /// Base git ref (commit, branch, or tag).
1442 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1443 base: String,
1444
1445 /// Target git ref (commit, branch, or tag).
1446 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1447 target: String,
1448
1449 /// Path to git repository (defaults to current directory).
1450 ///
1451 /// Can be the repository root or any path within it - sqry will walk up
1452 /// the directory tree to find the .git directory.
1453 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1454 path: Option<String>,
1455
1456 /// Maximum total results to display (default: 100).
1457 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1458 limit: usize,
1459
1460 /// Filter by symbol kinds (comma-separated).
1461 #[arg(long, short = 'k', help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1462 kind: Option<String>,
1463
1464 /// Filter by change types (comma-separated).
1465 ///
1466 /// Valid values: `added`, `removed`, `modified`, `renamed`, `signature_changed`
1467 ///
1468 /// Example: --change-type added,modified
1469 #[arg(long, short = 'c', help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1470 change_type: Option<String>,
1471 },
1472
1473 /// Hierarchical semantic search (RAG-optimized)
1474 ///
1475 /// Performs semantic search with results grouped by file and container,
1476 /// optimized for retrieval-augmented generation (RAG) workflows.
1477 ///
1478 /// Examples:
1479 /// sqry hier "kind:function" # All functions, grouped
1480 /// sqry hier "auth" --max-files 10 # Limit file groups
1481 /// sqry hier --kind function "test" # Filter by kind
1482 /// sqry hier --context 5 "validate" # More context lines
1483 #[command(display_order = 4, verbatim_doc_comment)]
1484 Hier {
1485 /// Search query.
1486 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1487 query: String,
1488
1489 /// Search path (defaults to current directory).
1490 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1491 path: Option<String>,
1492
1493 /// Maximum symbols before grouping (default: 200).
1494 #[arg(long, short = 'l', default_value = "200", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1495 limit: usize,
1496
1497 /// Maximum files in output (default: 20).
1498 #[arg(long, default_value = "20", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1499 max_files: usize,
1500
1501 /// Context lines around matches (default: 3).
1502 #[arg(long, short = 'c', default_value = "3", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1503 context: usize,
1504
1505 /// Filter by symbol kinds (comma-separated).
1506 #[arg(long, short = 'k', help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1507 kind: Option<String>,
1508
1509 /// Filter by languages (comma-separated).
1510 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1511 lang: Option<String>,
1512 },
1513
1514 /// Configure MCP server integration for AI coding tools
1515 ///
1516 /// Auto-detect and configure sqry MCP for Claude Code, Codex, and Gemini CLI.
1517 /// The setup command writes tool-specific configuration so AI coding assistants
1518 /// can use sqry's semantic code search capabilities.
1519 ///
1520 /// Examples:
1521 /// sqry mcp setup # Auto-configure all detected tools
1522 /// sqry mcp setup --tool claude # Configure Claude Code only
1523 /// sqry mcp setup --scope global --dry-run # Preview global config changes
1524 /// sqry mcp status # Show current MCP configuration
1525 /// sqry mcp status --json # Machine-readable status
1526 #[command(display_order = 51, verbatim_doc_comment)]
1527 Mcp {
1528 #[command(subcommand)]
1529 command: McpCommand,
1530 },
1531}
1532
1533/// MCP server integration subcommands
1534#[derive(Subcommand, Debug, Clone)]
1535pub enum McpCommand {
1536 /// Auto-configure sqry MCP for detected AI tools (Claude Code, Codex, Gemini)
1537 ///
1538 /// Detects installed AI coding tools and writes configuration entries
1539 /// pointing to the sqry-mcp binary. Uses tool-appropriate scoping:
1540 /// - Claude Code: per-project entries with pinned workspace root (default)
1541 /// - Codex/Gemini: global entries using CWD-based workspace discovery
1542 ///
1543 /// Note: Codex and Gemini only support global MCP configs.
1544 /// They rely on being launched from within a project directory
1545 /// for sqry-mcp's CWD discovery to resolve the correct workspace.
1546 Setup {
1547 /// Target tool(s) to configure.
1548 #[arg(long, value_enum, default_value = "all")]
1549 tool: ToolTarget,
1550
1551 /// Configuration scope.
1552 ///
1553 /// - auto: project scope for Claude (when inside a repo), global for Codex/Gemini
1554 /// - project: per-project Claude entry with pinned workspace root
1555 /// - global: global entries for all tools (CWD-dependent for workspace resolution)
1556 ///
1557 /// Note: For Codex and Gemini, --scope project and --scope global behave
1558 /// identically because these tools only support global MCP configs.
1559 #[arg(long, value_enum, default_value = "auto")]
1560 scope: SetupScope,
1561
1562 /// Explicit workspace root path (overrides auto-detection).
1563 ///
1564 /// Only applicable for Claude Code project scope. Rejected for
1565 /// Codex/Gemini because setting a workspace root in their global
1566 /// config would pin to one repo and break multi-repo workflows.
1567 #[arg(long)]
1568 workspace_root: Option<PathBuf>,
1569
1570 /// Overwrite existing sqry configuration.
1571 #[arg(long)]
1572 force: bool,
1573
1574 /// Preview changes without writing.
1575 #[arg(long)]
1576 dry_run: bool,
1577
1578 /// Skip creating .bak backup files.
1579 #[arg(long)]
1580 no_backup: bool,
1581 },
1582
1583 /// Show current MCP configuration status across all tools
1584 ///
1585 /// Reports the sqry-mcp binary location and configuration state
1586 /// for each supported AI tool, including scope, workspace root,
1587 /// and any detected issues (shim usage, drift, missing config).
1588 Status {
1589 /// Output as JSON for programmatic use.
1590 #[arg(long)]
1591 json: bool,
1592 },
1593}
1594
1595/// Target AI tool(s) for MCP configuration
1596#[derive(Debug, Clone, ValueEnum)]
1597pub enum ToolTarget {
1598 /// Configure Claude Code only
1599 Claude,
1600 /// Configure Codex only
1601 Codex,
1602 /// Configure Gemini CLI only
1603 Gemini,
1604 /// Configure all detected tools (default)
1605 All,
1606}
1607
1608/// Configuration scope for MCP setup
1609#[derive(Debug, Clone, ValueEnum)]
1610pub enum SetupScope {
1611 /// Per-project for Claude, global for Codex/Gemini (auto-detect)
1612 Auto,
1613 /// Per-project entries with pinned workspace root
1614 Project,
1615 /// Global entries (CWD-dependent workspace resolution)
1616 Global,
1617}
1618
1619/// Graph-based query operations
1620#[derive(Subcommand, Debug, Clone)]
1621pub enum GraphOperation {
1622 /// Find shortest path between two symbols
1623 ///
1624 /// Traces the shortest execution path from one symbol to another,
1625 /// following Call, `HTTPRequest`, and `FFICall` edges.
1626 ///
1627 /// Example: sqry graph trace-path main processData
1628 TracePath {
1629 /// Source symbol name (e.g., "main", "User.authenticate").
1630 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1631 from: String,
1632
1633 /// Target symbol name.
1634 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 20)]
1635 to: String,
1636
1637 /// Filter by languages (comma-separated, e.g., "javascript,python").
1638 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1639 languages: Option<String>,
1640
1641 /// Show full file paths in output.
1642 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1643 full_paths: bool,
1644 },
1645
1646 /// Calculate maximum call chain depth from a symbol
1647 ///
1648 /// Computes the longest call chain starting from the given symbol,
1649 /// useful for complexity analysis and recursion detection.
1650 ///
1651 /// Example: sqry graph call-chain-depth main
1652 CallChainDepth {
1653 /// Symbol name to analyze.
1654 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1655 symbol: String,
1656
1657 /// Filter by languages (comma-separated).
1658 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1659 languages: Option<String>,
1660
1661 /// Show the actual call chain, not just the depth.
1662 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1663 show_chain: bool,
1664 },
1665
1666 /// Show transitive dependencies for a module
1667 ///
1668 /// Analyzes all imports transitively to build a complete dependency tree,
1669 /// including circular dependency detection.
1670 ///
1671 /// Example: sqry graph dependency-tree src/main.js
1672 #[command(alias = "deps")]
1673 DependencyTree {
1674 /// Module path or name.
1675 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1676 module: String,
1677
1678 /// Maximum depth to traverse (default: unlimited).
1679 #[arg(long, help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
1680 max_depth: Option<usize>,
1681
1682 /// Show circular dependencies only.
1683 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1684 cycles_only: bool,
1685 },
1686
1687 /// List all cross-language relationships
1688 ///
1689 /// Finds edges connecting symbols in different programming languages,
1690 /// such as TypeScript→JavaScript imports, Python→C FFI calls, SQL table
1691 /// access, Dart `MethodChannel` invocations, and Flutter widget hierarchies.
1692 ///
1693 /// Supported languages for --from-lang/--to-lang:
1694 /// js, ts, py, cpp, c, csharp (cs), java, go, ruby, php,
1695 /// swift, kotlin, scala, sql, dart, lua, perl, shell (bash),
1696 /// groovy, http
1697 ///
1698 /// Examples:
1699 /// sqry graph cross-language --from-lang dart --edge-type `channel_invoke`
1700 /// sqry graph cross-language --from-lang sql --edge-type `table_read`
1701 /// sqry graph cross-language --edge-type `widget_child`
1702 #[command(verbatim_doc_comment)]
1703 CrossLanguage {
1704 /// Filter by source language.
1705 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1706 from_lang: Option<String>,
1707
1708 /// Filter by target language.
1709 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1710 to_lang: Option<String>,
1711
1712 /// Edge type filter.
1713 ///
1714 /// Supported values:
1715 /// call, import, http, ffi,
1716 /// `table_read`, `table_write`, `triggered_by`,
1717 /// `channel_invoke`, `widget_child`
1718 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1719 edge_type: Option<String>,
1720
1721 /// Minimum confidence threshold (0.0-1.0).
1722 #[arg(long, default_value = "0.0", help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1723 min_confidence: f64,
1724 },
1725
1726 /// List unified graph nodes
1727 ///
1728 /// Enumerates nodes from the unified graph snapshot and applies filters.
1729 /// Useful for inspecting graph coverage and metadata details.
1730 Nodes {
1731 /// Filter by node kind(s) (comma-separated: function,method,macro).
1732 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1733 kind: Option<String>,
1734
1735 /// Filter by language(s) (comma-separated: rust,python).
1736 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1737 languages: Option<String>,
1738
1739 /// Filter by file path substring (case-insensitive).
1740 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1741 file: Option<String>,
1742
1743 /// Filter by name substring (case-sensitive).
1744 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1745 name: Option<String>,
1746
1747 /// Filter by qualified name substring (case-sensitive).
1748 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
1749 qualified_name: Option<String>,
1750
1751 /// Maximum results (default: 1000, max: 10000; use 0 for default).
1752 #[arg(long, default_value = "1000", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1753 limit: usize,
1754
1755 /// Skip N results.
1756 #[arg(long, default_value = "0", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
1757 offset: usize,
1758
1759 /// Show full file paths in output.
1760 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 30)]
1761 full_paths: bool,
1762 },
1763
1764 /// List unified graph edges
1765 ///
1766 /// Enumerates edges from the unified graph snapshot and applies filters.
1767 /// Useful for inspecting relationships and cross-cutting metadata.
1768 Edges {
1769 /// Filter by edge kind tag(s) (comma-separated: calls,imports).
1770 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1771 kind: Option<String>,
1772
1773 /// Filter by source label substring (case-sensitive).
1774 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1775 from: Option<String>,
1776
1777 /// Filter by target label substring (case-sensitive).
1778 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1779 to: Option<String>,
1780
1781 /// Filter by source language.
1782 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1783 from_lang: Option<String>,
1784
1785 /// Filter by target language.
1786 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
1787 to_lang: Option<String>,
1788
1789 /// Filter by file path substring (case-insensitive, source file only).
1790 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 60)]
1791 file: Option<String>,
1792
1793 /// Maximum results (default: 1000, max: 10000; use 0 for default).
1794 #[arg(long, default_value = "1000", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1795 limit: usize,
1796
1797 /// Skip N results.
1798 #[arg(long, default_value = "0", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
1799 offset: usize,
1800
1801 /// Show full file paths in output.
1802 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 30)]
1803 full_paths: bool,
1804 },
1805
1806 /// Show graph statistics and summary
1807 ///
1808 /// Displays overall graph metrics including node counts by language,
1809 /// edge counts by type, and cross-language relationship statistics.
1810 ///
1811 /// Example: sqry graph stats
1812 Stats {
1813 /// Show detailed breakdown by file.
1814 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1815 by_file: bool,
1816
1817 /// Show detailed breakdown by language.
1818 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
1819 by_language: bool,
1820 },
1821
1822 /// Show unified graph snapshot status
1823 ///
1824 /// Reports on the state of the unified graph snapshot stored in
1825 /// `.sqry/graph/` directory. Displays build timestamp, node/edge counts,
1826 /// and snapshot age.
1827 ///
1828 /// Example: sqry graph status
1829 Status,
1830
1831 /// Detect circular dependencies in the codebase
1832 ///
1833 /// Finds all cycles in the call and import graphs, which can indicate
1834 /// potential design issues or circular dependency problems.
1835 ///
1836 /// Example: sqry graph cycles
1837 #[command(alias = "cyc")]
1838 Cycles {
1839 /// Minimum cycle length to report (default: 2).
1840 #[arg(long, default_value = "2", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
1841 min_length: usize,
1842
1843 /// Maximum cycle length to report (default: unlimited).
1844 #[arg(long, help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 20)]
1845 max_length: Option<usize>,
1846
1847 /// Only analyze import edges (ignore calls).
1848 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1849 imports_only: bool,
1850
1851 /// Filter by languages (comma-separated).
1852 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1853 languages: Option<String>,
1854 },
1855
1856 /// Calculate code complexity metrics
1857 ///
1858 /// Analyzes cyclomatic complexity, call graph depth, and other
1859 /// complexity metrics for functions and modules.
1860 ///
1861 /// Example: sqry graph complexity
1862 #[command(alias = "cx")]
1863 Complexity {
1864 /// Target symbol or module (default: analyze all).
1865 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1866 target: Option<String>,
1867
1868 /// Sort by complexity score.
1869 #[arg(long = "sort-complexity", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1870 sort_complexity: bool,
1871
1872 /// Show only items above this complexity threshold.
1873 #[arg(long, default_value = "0", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1874 min_complexity: usize,
1875
1876 /// Filter by languages (comma-separated).
1877 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1878 languages: Option<String>,
1879 },
1880
1881 /// Find direct callers of a symbol
1882 ///
1883 /// Lists all symbols that directly call the specified function, method,
1884 /// or other callable. Useful for understanding symbol usage and impact analysis.
1885 ///
1886 /// Example: sqry graph direct-callers authenticate
1887 #[command(alias = "callers")]
1888 DirectCallers {
1889 /// Symbol name to find callers for.
1890 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1891 symbol: String,
1892
1893 /// Maximum results (default: 100).
1894 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1895 limit: usize,
1896
1897 /// Filter by languages (comma-separated).
1898 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1899 languages: Option<String>,
1900
1901 /// Show full file paths in output.
1902 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
1903 full_paths: bool,
1904 },
1905
1906 /// Find direct callees of a symbol
1907 ///
1908 /// Lists all symbols that are directly called by the specified function
1909 /// or method. Useful for understanding dependencies and refactoring scope.
1910 ///
1911 /// Example: sqry graph direct-callees processData
1912 #[command(alias = "callees")]
1913 DirectCallees {
1914 /// Symbol name to find callees for.
1915 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1916 symbol: String,
1917
1918 /// Maximum results (default: 100).
1919 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1920 limit: usize,
1921
1922 /// Filter by languages (comma-separated).
1923 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1924 languages: Option<String>,
1925
1926 /// Show full file paths in output.
1927 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
1928 full_paths: bool,
1929 },
1930
1931 /// Show call hierarchy for a symbol
1932 ///
1933 /// Displays incoming and/or outgoing call relationships in a tree format.
1934 /// Useful for understanding code flow and impact of changes.
1935 ///
1936 /// Example: sqry graph call-hierarchy main --depth 3
1937 #[command(alias = "ch")]
1938 CallHierarchy {
1939 /// Symbol name to show hierarchy for.
1940 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1941 symbol: String,
1942
1943 /// Maximum depth to traverse (default: 3).
1944 #[arg(long, short = 'd', default_value = "3", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
1945 depth: usize,
1946
1947 /// Direction: incoming, outgoing, or both (default: both).
1948 #[arg(long, default_value = "both", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 20)]
1949 direction: String,
1950
1951 /// Filter by languages (comma-separated).
1952 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1953 languages: Option<String>,
1954
1955 /// Show full file paths in output.
1956 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1957 full_paths: bool,
1958 },
1959
1960 /// Check if a symbol is in a cycle
1961 ///
1962 /// Determines whether a specific symbol participates in any circular
1963 /// dependency chains. Can optionally show the cycle path.
1964 ///
1965 /// Example: sqry graph is-in-cycle `UserService` --show-cycle
1966 #[command(alias = "incycle")]
1967 IsInCycle {
1968 /// Symbol name to check.
1969 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1970 symbol: String,
1971
1972 /// Cycle type to check: calls, imports, or all (default: calls).
1973 #[arg(long, default_value = "calls", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
1974 cycle_type: String,
1975
1976 /// Show the full cycle path if found.
1977 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
1978 show_cycle: bool,
1979 },
1980}
1981
1982/// Output format choices for `sqry batch`.
1983#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
1984pub enum BatchFormat {
1985 /// Human-readable text output (default)
1986 Text,
1987 /// Aggregated JSON output containing all query results
1988 Json,
1989 /// Newline-delimited JSON objects (one per query)
1990 Jsonl,
1991 /// Comma-separated summary per query
1992 Csv,
1993}
1994
1995/// Cache management actions
1996#[derive(Subcommand, Debug, Clone)]
1997pub enum CacheAction {
1998 /// Show cache statistics
1999 ///
2000 /// Display hit rate, size, and entry count for the AST cache.
2001 Stats {
2002 /// Path to check cache for (defaults to current directory).
2003 #[arg(help_heading = headings::CACHE_INPUT, display_order = 10)]
2004 path: Option<String>,
2005 },
2006
2007 /// Clear the cache
2008 ///
2009 /// Remove all cached AST data. Next queries will re-parse files.
2010 Clear {
2011 /// Path to clear cache for (defaults to current directory).
2012 #[arg(help_heading = headings::CACHE_INPUT, display_order = 10)]
2013 path: Option<String>,
2014
2015 /// Confirm deletion (required for safety).
2016 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2017 confirm: bool,
2018 },
2019
2020 /// Prune the cache
2021 ///
2022 /// Remove old or excessive cache entries to reclaim disk space.
2023 /// Supports time-based (--days) and size-based (--size) retention policies.
2024 Prune {
2025 /// Target cache directory (defaults to user cache dir).
2026 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 10)]
2027 path: Option<String>,
2028
2029 /// Remove entries older than N days.
2030 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 20)]
2031 days: Option<u64>,
2032
2033 /// Cap cache to maximum size (e.g., "1GB", "500MB").
2034 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 30)]
2035 size: Option<String>,
2036
2037 /// Preview deletions without removing files.
2038 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2039 dry_run: bool,
2040 },
2041}
2042
2043/// Config action subcommands
2044#[derive(Subcommand, Debug, Clone)]
2045pub enum ConfigAction {
2046 /// Initialize config with defaults
2047 ///
2048 /// Creates `.sqry/graph/config/config.json` with default settings.
2049 /// Use --force to overwrite existing config.
2050 ///
2051 /// Examples:
2052 /// sqry config init
2053 /// sqry config init --force
2054 #[command(verbatim_doc_comment)]
2055 Init {
2056 /// Project root path (defaults to current directory).
2057 // Path defaults to current directory if not specified
2058 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2059 path: Option<String>,
2060
2061 /// Overwrite existing config.
2062 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 20)]
2063 force: bool,
2064 },
2065
2066 /// Show effective config
2067 ///
2068 /// Displays the complete config with source annotations.
2069 /// Use --key to show a single value.
2070 ///
2071 /// Examples:
2072 /// sqry config show
2073 /// sqry config show --json
2074 /// sqry config show --key `limits.max_results`
2075 #[command(verbatim_doc_comment)]
2076 Show {
2077 /// Project root path (defaults to current directory).
2078 // Path defaults to current directory if not specified
2079 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2080 path: Option<String>,
2081
2082 /// Output as JSON.
2083 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
2084 json: bool,
2085
2086 /// Show only this config key (e.g., `limits.max_results`).
2087 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 20)]
2088 key: Option<String>,
2089 },
2090
2091 /// Set a config value
2092 ///
2093 /// Updates a config key and persists to disk.
2094 /// Shows a diff before applying (use --yes to skip).
2095 ///
2096 /// Examples:
2097 /// sqry config set `limits.max_results` 10000
2098 /// sqry config set `locking.stale_takeover_policy` warn
2099 /// sqry config set `output.page_size` 100 --yes
2100 #[command(verbatim_doc_comment)]
2101 Set {
2102 /// Project root path (defaults to current directory).
2103 // Path defaults to current directory if not specified
2104 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2105 path: Option<String>,
2106
2107 /// Config key (e.g., `limits.max_results`).
2108 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2109 key: String,
2110
2111 /// New value.
2112 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 30)]
2113 value: String,
2114
2115 /// Skip confirmation prompt.
2116 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 40)]
2117 yes: bool,
2118 },
2119
2120 /// Get a config value
2121 ///
2122 /// Retrieves a single config value.
2123 ///
2124 /// Examples:
2125 /// sqry config get `limits.max_results`
2126 /// sqry config get `locking.stale_takeover_policy`
2127 #[command(verbatim_doc_comment)]
2128 Get {
2129 /// Project root path (defaults to current directory).
2130 // Path defaults to current directory if not specified
2131 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2132 path: Option<String>,
2133
2134 /// Config key (e.g., `limits.max_results`).
2135 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2136 key: String,
2137 },
2138
2139 /// Validate config file
2140 ///
2141 /// Checks config syntax and schema validity.
2142 ///
2143 /// Examples:
2144 /// sqry config validate
2145 #[command(verbatim_doc_comment)]
2146 Validate {
2147 /// Project root path (defaults to current directory).
2148 // Path defaults to current directory if not specified
2149 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2150 path: Option<String>,
2151 },
2152
2153 /// Manage query aliases
2154 #[command(subcommand)]
2155 Alias(ConfigAliasAction),
2156}
2157
2158/// Config alias subcommands
2159#[derive(Subcommand, Debug, Clone)]
2160pub enum ConfigAliasAction {
2161 /// Create or update an alias
2162 ///
2163 /// Examples:
2164 /// sqry config alias set my-funcs "kind:function"
2165 /// sqry config alias set my-funcs "kind:function" --description "All functions"
2166 #[command(verbatim_doc_comment)]
2167 Set {
2168 /// Project root path (defaults to current directory).
2169 // Path defaults to current directory if not specified
2170 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2171 path: Option<String>,
2172
2173 /// Alias name.
2174 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2175 name: String,
2176
2177 /// Query expression.
2178 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 30)]
2179 query: String,
2180
2181 /// Optional description.
2182 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 40)]
2183 description: Option<String>,
2184 },
2185
2186 /// List all aliases
2187 ///
2188 /// Examples:
2189 /// sqry config alias list
2190 /// sqry config alias list --json
2191 #[command(verbatim_doc_comment)]
2192 List {
2193 /// Project root path (defaults to current directory).
2194 // Path defaults to current directory if not specified
2195 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2196 path: Option<String>,
2197
2198 /// Output as JSON.
2199 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
2200 json: bool,
2201 },
2202
2203 /// Remove an alias
2204 ///
2205 /// Examples:
2206 /// sqry config alias remove my-funcs
2207 #[command(verbatim_doc_comment)]
2208 Remove {
2209 /// Project root path (defaults to current directory).
2210 // Path defaults to current directory if not specified
2211 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2212 path: Option<String>,
2213
2214 /// Alias name to remove.
2215 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2216 name: String,
2217 },
2218}
2219
2220/// Visualize code relationships from relation queries.
2221///
2222/// Examples:
2223/// sqry visualize "callers:main" --format mermaid
2224/// sqry visualize "imports:std" --format graphviz --output-file deps.dot
2225/// sqry visualize "callees:process" --depth 5 --max-nodes 200
2226#[derive(Debug, Args, Clone)]
2227#[command(
2228 about = "Visualize code relationships as diagrams",
2229 long_about = "Visualize code relationships as diagrams.\n\n\
2230Examples:\n sqry visualize \"callers:main\" --format mermaid\n \
2231sqry visualize \"imports:std\" --format graphviz --output-file deps.dot\n \
2232sqry visualize \"callees:process\" --depth 5 --max-nodes 200",
2233 after_help = "Examples:\n sqry visualize \"callers:main\" --format mermaid\n \
2234sqry visualize \"imports:std\" --format graphviz --output-file deps.dot\n \
2235sqry visualize \"callees:process\" --depth 5 --max-nodes 200"
2236)]
2237pub struct VisualizeCommand {
2238 /// Relation query (e.g., callers:main, callees:helper).
2239 #[arg(help_heading = headings::VISUALIZATION_INPUT, display_order = 10)]
2240 pub query: String,
2241
2242 /// Target path (defaults to CLI positional path).
2243 #[arg(long, help_heading = headings::VISUALIZATION_INPUT, display_order = 20)]
2244 pub path: Option<String>,
2245
2246 /// Diagram syntax format (mermaid, graphviz, d2).
2247 ///
2248 /// Specifies the diagram language/syntax to generate.
2249 /// Output will be plain text in the chosen format.
2250 #[arg(long, short = 'f', value_enum, default_value = "mermaid", help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 10)]
2251 pub format: DiagramFormatArg,
2252
2253 /// Layout direction for the graph.
2254 #[arg(long, value_enum, default_value = "top-down", help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 20)]
2255 pub direction: DirectionArg,
2256
2257 /// File path to save the output (stdout when omitted).
2258 #[arg(long, help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 30)]
2259 pub output_file: Option<PathBuf>,
2260
2261 /// Maximum traversal depth for graph expansion.
2262 #[arg(long, short = 'd', default_value_t = 3, help_heading = headings::TRAVERSAL_CONTROL, display_order = 10)]
2263 pub depth: usize,
2264
2265 /// Maximum number of nodes to include in the diagram (1-500).
2266 #[arg(long, default_value_t = 100, help_heading = headings::TRAVERSAL_CONTROL, display_order = 20)]
2267 pub max_nodes: usize,
2268}
2269
2270/// Supported diagram text formats.
2271#[derive(Debug, Clone, Copy, ValueEnum)]
2272pub enum DiagramFormatArg {
2273 Mermaid,
2274 Graphviz,
2275 D2,
2276}
2277
2278/// Diagram layout direction.
2279#[derive(Debug, Clone, Copy, ValueEnum)]
2280#[value(rename_all = "kebab-case")]
2281pub enum DirectionArg {
2282 TopDown,
2283 BottomUp,
2284 LeftRight,
2285 RightLeft,
2286}
2287
2288/// Workspace management subcommands
2289#[derive(Subcommand, Debug, Clone)]
2290pub enum WorkspaceCommand {
2291 /// Initialise a new workspace registry
2292 Init {
2293 /// Directory that will contain the workspace registry.
2294 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2295 workspace: String,
2296
2297 /// Preferred discovery mode for initial scans.
2298 #[arg(long, value_enum, default_value_t = WorkspaceDiscoveryMode::IndexFiles, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2299 mode: WorkspaceDiscoveryMode,
2300
2301 /// Friendly workspace name stored in the registry metadata.
2302 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 20)]
2303 name: Option<String>,
2304 },
2305
2306 /// Scan for repositories inside the workspace root
2307 Scan {
2308 /// Workspace root containing the .sqry-workspace file.
2309 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2310 workspace: String,
2311
2312 /// Discovery mode to use when scanning for repositories.
2313 #[arg(long, value_enum, default_value_t = WorkspaceDiscoveryMode::IndexFiles, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2314 mode: WorkspaceDiscoveryMode,
2315
2316 /// Remove entries whose indexes are no longer present.
2317 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 20)]
2318 prune_stale: bool,
2319 },
2320
2321 /// Add a repository to the workspace manually
2322 Add {
2323 /// Workspace root containing the .sqry-workspace file.
2324 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2325 workspace: String,
2326
2327 /// Path to the repository root (must contain .sqry-index).
2328 #[arg(value_name = "REPO", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2329 repo: String,
2330
2331 /// Optional friendly name for the repository.
2332 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2333 name: Option<String>,
2334 },
2335
2336 /// Remove a repository from the workspace
2337 Remove {
2338 /// Workspace root containing the .sqry-workspace file.
2339 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2340 workspace: String,
2341
2342 /// Repository identifier (workspace-relative path).
2343 #[arg(value_name = "REPO_ID", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2344 repo_id: String,
2345 },
2346
2347 /// Run a workspace-level query across registered repositories
2348 Query {
2349 /// Workspace root containing the .sqry-workspace file.
2350 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2351 workspace: String,
2352
2353 /// Query expression (supports repo: predicates).
2354 #[arg(value_name = "QUERY", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2355 query: String,
2356
2357 /// Override parallel query threads.
2358 #[arg(long, help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
2359 threads: Option<usize>,
2360 },
2361
2362 /// Emit aggregate statistics for the workspace
2363 Stats {
2364 /// Workspace root containing the .sqry-workspace file.
2365 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2366 workspace: String,
2367 },
2368}
2369
2370/// CLI discovery modes converted to workspace `DiscoveryMode` values
2371#[derive(Clone, Copy, Debug, ValueEnum)]
2372pub enum WorkspaceDiscoveryMode {
2373 #[value(name = "index-files", alias = "index")]
2374 IndexFiles,
2375 #[value(name = "git-roots", alias = "git")]
2376 GitRoots,
2377}
2378
2379/// Alias management subcommands
2380#[derive(Subcommand, Debug, Clone)]
2381pub enum AliasAction {
2382 /// List all saved aliases
2383 ///
2384 /// Shows aliases from both global (~/.config/sqry/) and local (.sqry-index.user)
2385 /// storage. Local aliases take precedence over global ones with the same name.
2386 ///
2387 /// Examples:
2388 /// sqry alias list # List all aliases
2389 /// sqry alias list --local # Only local aliases
2390 /// sqry alias list --global # Only global aliases
2391 /// sqry alias list --json # JSON output
2392 #[command(verbatim_doc_comment)]
2393 List {
2394 /// Show only local aliases (project-specific).
2395 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2396 local: bool,
2397
2398 /// Show only global aliases (cross-project).
2399 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2400 global: bool,
2401 },
2402
2403 /// Show details of a specific alias
2404 ///
2405 /// Displays the command, arguments, description, and storage location
2406 /// for the named alias.
2407 ///
2408 /// Example: sqry alias show my-funcs
2409 Show {
2410 /// Name of the alias to show.
2411 #[arg(value_name = "NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2412 name: String,
2413 },
2414
2415 /// Delete a saved alias
2416 ///
2417 /// Removes an alias from storage. If the alias exists in both local
2418 /// and global storage, specify --local or --global to delete from
2419 /// a specific location.
2420 ///
2421 /// Examples:
2422 /// sqry alias delete my-funcs # Delete (prefers local)
2423 /// sqry alias delete my-funcs --global # Delete from global only
2424 /// sqry alias delete my-funcs --force # Skip confirmation
2425 #[command(verbatim_doc_comment)]
2426 Delete {
2427 /// Name of the alias to delete.
2428 #[arg(value_name = "NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2429 name: String,
2430
2431 /// Delete from local storage only.
2432 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2433 local: bool,
2434
2435 /// Delete from global storage only.
2436 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2437 global: bool,
2438
2439 /// Skip confirmation prompt.
2440 #[arg(long, short = 'f', help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2441 force: bool,
2442 },
2443
2444 /// Rename an existing alias
2445 ///
2446 /// Changes the name of an alias while preserving its command and arguments.
2447 /// The alias is renamed in the same storage location where it was found.
2448 ///
2449 /// Example: sqry alias rename old-name new-name
2450 Rename {
2451 /// Current name of the alias.
2452 #[arg(value_name = "OLD_NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2453 old_name: String,
2454
2455 /// New name for the alias.
2456 #[arg(value_name = "NEW_NAME", help_heading = headings::ALIAS_INPUT, display_order = 20)]
2457 new_name: String,
2458
2459 /// Rename in local storage only.
2460 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2461 local: bool,
2462
2463 /// Rename in global storage only.
2464 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2465 global: bool,
2466 },
2467
2468 /// Export aliases to a JSON file
2469 ///
2470 /// Exports aliases for backup or sharing. The export format is compatible
2471 /// with the import command for easy restoration.
2472 ///
2473 /// Examples:
2474 /// sqry alias export aliases.json # Export all
2475 /// sqry alias export aliases.json --local # Export local only
2476 #[command(verbatim_doc_comment)]
2477 Export {
2478 /// Output file path (use - for stdout).
2479 #[arg(value_name = "FILE", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2480 file: String,
2481
2482 /// Export only local aliases.
2483 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2484 local: bool,
2485
2486 /// Export only global aliases.
2487 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2488 global: bool,
2489 },
2490
2491 /// Import aliases from a JSON file
2492 ///
2493 /// Imports aliases from an export file. Handles conflicts with existing
2494 /// aliases using the specified strategy.
2495 ///
2496 /// Examples:
2497 /// sqry alias import aliases.json # Import to local
2498 /// sqry alias import aliases.json --global # Import to global
2499 /// sqry alias import aliases.json --on-conflict skip
2500 #[command(verbatim_doc_comment)]
2501 Import {
2502 /// Input file path (use - for stdin).
2503 #[arg(value_name = "FILE", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2504 file: String,
2505
2506 /// Import to local storage (default).
2507 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2508 local: bool,
2509
2510 /// Import to global storage.
2511 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2512 global: bool,
2513
2514 /// How to handle conflicts with existing aliases.
2515 #[arg(long, value_enum, default_value = "error", help_heading = headings::ALIAS_CONFIGURATION, display_order = 30)]
2516 on_conflict: ImportConflictArg,
2517
2518 /// Preview import without making changes.
2519 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2520 dry_run: bool,
2521 },
2522}
2523
2524/// History management subcommands
2525#[derive(Subcommand, Debug, Clone)]
2526pub enum HistoryAction {
2527 /// List recent query history
2528 ///
2529 /// Shows recently executed queries with their timestamps, commands,
2530 /// and execution status.
2531 ///
2532 /// Examples:
2533 /// sqry history list # List recent (default 100)
2534 /// sqry history list --limit 50 # Last 50 entries
2535 /// sqry history list --json # JSON output
2536 #[command(verbatim_doc_comment)]
2537 List {
2538 /// Maximum number of entries to show.
2539 #[arg(long, short = 'n', default_value = "100", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
2540 limit: usize,
2541 },
2542
2543 /// Search query history
2544 ///
2545 /// Searches history entries by pattern. The pattern is matched
2546 /// against command names and arguments.
2547 ///
2548 /// Examples:
2549 /// sqry history search "function" # Find queries with "function"
2550 /// sqry history search "callers:" # Find caller queries
2551 #[command(verbatim_doc_comment)]
2552 Search {
2553 /// Search pattern (matched against command and args).
2554 #[arg(value_name = "PATTERN", help_heading = headings::HISTORY_INPUT, display_order = 10)]
2555 pattern: String,
2556
2557 /// Maximum number of results.
2558 #[arg(long, short = 'n', default_value = "100", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
2559 limit: usize,
2560 },
2561
2562 /// Clear query history
2563 ///
2564 /// Removes history entries. Can clear all entries or only those
2565 /// older than a specified duration.
2566 ///
2567 /// Examples:
2568 /// sqry history clear # Clear all (requires --confirm)
2569 /// sqry history clear --older 30d # Clear entries older than 30 days
2570 /// sqry history clear --older 1w # Clear entries older than 1 week
2571 #[command(verbatim_doc_comment)]
2572 Clear {
2573 /// Remove only entries older than this duration (e.g., 30d, 1w, 24h).
2574 #[arg(long, value_name = "DURATION", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
2575 older: Option<String>,
2576
2577 /// Confirm clearing history (required when clearing all).
2578 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2579 confirm: bool,
2580 },
2581
2582 /// Show history statistics
2583 ///
2584 /// Displays aggregate statistics about query history including
2585 /// total entries, most used commands, and storage information.
2586 Stats,
2587}
2588
2589/// Insights management subcommands
2590#[derive(Subcommand, Debug, Clone)]
2591pub enum InsightsAction {
2592 /// Show usage summary for a time period
2593 ///
2594 /// Displays aggregated usage statistics including query counts,
2595 /// timing metrics, and workflow patterns.
2596 ///
2597 /// Examples:
2598 /// sqry insights show # Current week
2599 /// sqry insights show --week 2025-W50 # Specific week
2600 /// sqry insights show --json # JSON output
2601 #[command(verbatim_doc_comment)]
2602 Show {
2603 /// ISO week to display (e.g., 2025-W50). Defaults to current week.
2604 #[arg(long, short = 'w', value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
2605 week: Option<String>,
2606 },
2607
2608 /// Show or modify uses configuration
2609 ///
2610 /// View the current configuration or change settings like
2611 /// enabling/disabling uses capture.
2612 ///
2613 /// Examples:
2614 /// sqry insights config # Show current config
2615 /// sqry insights config --enable # Enable uses capture
2616 /// sqry insights config --disable # Disable uses capture
2617 /// sqry insights config --retention 90 # Set retention to 90 days
2618 #[command(verbatim_doc_comment)]
2619 Config {
2620 /// Enable uses capture.
2621 #[arg(long, conflicts_with = "disable", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
2622 enable: bool,
2623
2624 /// Disable uses capture.
2625 #[arg(long, conflicts_with = "enable", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 20)]
2626 disable: bool,
2627
2628 /// Set retention period in days.
2629 #[arg(long, value_name = "DAYS", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 30)]
2630 retention: Option<u32>,
2631 },
2632
2633 /// Show storage status and statistics
2634 ///
2635 /// Displays information about the uses storage including
2636 /// total size, file count, and date range of stored events.
2637 ///
2638 /// Example:
2639 /// sqry insights status
2640 Status,
2641
2642 /// Clean up old event data
2643 ///
2644 /// Removes event logs older than the specified duration.
2645 /// Uses the configured retention period if --older is not specified.
2646 ///
2647 /// Examples:
2648 /// sqry insights prune # Use configured retention
2649 /// sqry insights prune --older 90d # Prune older than 90 days
2650 /// sqry insights prune --dry-run # Preview without deleting
2651 #[command(verbatim_doc_comment)]
2652 Prune {
2653 /// Remove entries older than this duration (e.g., 30d, 90d).
2654 /// Defaults to configured retention period.
2655 #[arg(long, value_name = "DURATION", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
2656 older: Option<String>,
2657
2658 /// Preview deletions without removing files.
2659 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2660 dry_run: bool,
2661 },
2662
2663 /// Generate an anonymous usage snapshot for sharing
2664 ///
2665 /// Creates a privacy-safe snapshot of your usage patterns that you can
2666 /// share with the sqry community or attach to bug reports. All fields
2667 /// are strongly-typed enums and numerics — no code content, paths, or
2668 /// identifiers are ever included.
2669 ///
2670 /// Uses are disabled → exits 1. Empty weeks produce a valid snapshot
2671 /// with total_uses: 0 (not an error).
2672 ///
2673 /// JSON output is controlled by the global --json flag.
2674 ///
2675 /// Examples:
2676 /// sqry insights share # Current week, human-readable
2677 /// sqry --json insights share # JSON to stdout
2678 /// sqry insights share --output snap.json # Write JSON to file
2679 /// sqry insights share --week 2026-W09 # Specific week
2680 /// sqry insights share --from 2026-W07 --to 2026-W09 # Merge 3 weeks
2681 /// sqry insights share --dry-run # Preview without writing
2682 #[cfg(feature = "share")]
2683 #[command(verbatim_doc_comment)]
2684 Share {
2685 /// Specific ISO week to share (e.g., 2026-W09). Defaults to current week.
2686 /// Conflicts with --from / --to.
2687 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10,
2688 conflicts_with_all = ["from", "to"])]
2689 week: Option<String>,
2690
2691 /// Start of multi-week range (e.g., 2026-W07). Requires --to.
2692 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 11,
2693 conflicts_with = "week", requires = "to")]
2694 from: Option<String>,
2695
2696 /// End of multi-week range (e.g., 2026-W09). Requires --from.
2697 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 12,
2698 conflicts_with = "week", requires = "from")]
2699 to: Option<String>,
2700
2701 /// Write JSON snapshot to this file.
2702 #[arg(long, short = 'o', value_name = "FILE", help_heading = headings::INSIGHTS_OUTPUT, display_order = 20,
2703 conflicts_with = "dry_run")]
2704 output: Option<PathBuf>,
2705
2706 /// Preview what would be shared without writing a file.
2707 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 30,
2708 conflicts_with = "output")]
2709 dry_run: bool,
2710 },
2711}
2712
2713/// Import conflict resolution strategies
2714#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
2715#[value(rename_all = "lowercase")]
2716pub enum ImportConflictArg {
2717 /// Fail on any conflict (default)
2718 Error,
2719 /// Skip conflicting aliases
2720 Skip,
2721 /// Overwrite existing aliases
2722 Overwrite,
2723}
2724
2725/// Shell types for completions
2726#[derive(Debug, Clone, Copy, ValueEnum)]
2727#[allow(missing_docs)]
2728#[allow(clippy::enum_variant_names)]
2729pub enum Shell {
2730 Bash,
2731 Zsh,
2732 Fish,
2733 PowerShell,
2734 Elvish,
2735}
2736
2737/// Symbol types for filtering
2738#[derive(Debug, Clone, Copy, ValueEnum)]
2739#[allow(missing_docs)]
2740pub enum SymbolKind {
2741 Function,
2742 Class,
2743 Method,
2744 Struct,
2745 Enum,
2746 Interface,
2747 Trait,
2748 Variable,
2749 Constant,
2750 Type,
2751 Module,
2752 Namespace,
2753}
2754
2755impl std::fmt::Display for SymbolKind {
2756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2757 match self {
2758 SymbolKind::Function => write!(f, "function"),
2759 SymbolKind::Class => write!(f, "class"),
2760 SymbolKind::Method => write!(f, "method"),
2761 SymbolKind::Struct => write!(f, "struct"),
2762 SymbolKind::Enum => write!(f, "enum"),
2763 SymbolKind::Interface => write!(f, "interface"),
2764 SymbolKind::Trait => write!(f, "trait"),
2765 SymbolKind::Variable => write!(f, "variable"),
2766 SymbolKind::Constant => write!(f, "constant"),
2767 SymbolKind::Type => write!(f, "type"),
2768 SymbolKind::Module => write!(f, "module"),
2769 SymbolKind::Namespace => write!(f, "namespace"),
2770 }
2771 }
2772}
2773
2774/// Index validation strictness modes
2775#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
2776#[value(rename_all = "lowercase")]
2777pub enum ValidationMode {
2778 /// Skip validation entirely (fastest)
2779 Off,
2780 /// Log warnings but continue (default)
2781 Warn,
2782 /// Abort on validation errors
2783 Fail,
2784}
2785
2786/// Metrics export format for validation status
2787#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
2788#[value(rename_all = "lower")]
2789pub enum MetricsFormat {
2790 /// JSON format (default, structured data)
2791 #[value(alias = "jsn")]
2792 Json,
2793 /// Prometheus `OpenMetrics` text format
2794 #[value(alias = "prom")]
2795 Prometheus,
2796}
2797
2798// Helper function to get the command with applied taxonomy
2799impl Cli {
2800 /// Get the command with taxonomy headings applied
2801 #[must_use]
2802 pub fn command_with_taxonomy() -> clap::Command {
2803 use clap::CommandFactory;
2804 let cmd = Self::command();
2805 headings::apply_root_layout(cmd)
2806 }
2807
2808 /// Validate CLI arguments that have dependencies not enforceable via clap
2809 ///
2810 /// Returns an error message if validation fails, None if valid.
2811 #[must_use]
2812 pub fn validate(&self) -> Option<&'static str> {
2813 let tabular_mode = self.csv || self.tsv;
2814
2815 // --headers, --columns, and --raw-csv require CSV or TSV mode
2816 if self.headers && !tabular_mode {
2817 return Some("--headers requires --csv or --tsv");
2818 }
2819 if self.columns.is_some() && !tabular_mode {
2820 return Some("--columns requires --csv or --tsv");
2821 }
2822 if self.raw_csv && !tabular_mode {
2823 return Some("--raw-csv requires --csv or --tsv");
2824 }
2825
2826 if tabular_mode && let Err(msg) = output::parse_columns(self.columns.as_ref()) {
2827 return Some(Box::leak(msg.into_boxed_str()));
2828 }
2829
2830 None
2831 }
2832
2833 /// Get the search path, defaulting to current directory if not specified
2834 #[must_use]
2835 pub fn search_path(&self) -> &str {
2836 self.path.as_deref().unwrap_or(".")
2837 }
2838
2839 /// Check if tabular output mode is enabled
2840 #[allow(dead_code)]
2841 #[must_use]
2842 pub fn is_tabular_output(&self) -> bool {
2843 self.csv || self.tsv
2844 }
2845
2846 /// Create pager configuration from CLI flags
2847 ///
2848 /// Returns `PagerConfig` based on `--pager`, `--no-pager`, and `--pager-cmd` flags.
2849 ///
2850 /// # Structured Output Handling
2851 ///
2852 /// For machine-readable formats (JSON, CSV, TSV), paging is disabled by default
2853 /// to avoid breaking pipelines. Use `--pager` to explicitly enable paging for
2854 /// these formats.
2855 #[must_use]
2856 pub fn pager_config(&self) -> crate::output::PagerConfig {
2857 // Structured output bypasses pager unless --pager is explicit
2858 let is_structured_output = self.json || self.csv || self.tsv;
2859 let effective_no_pager = self.no_pager || (is_structured_output && !self.pager);
2860
2861 crate::output::PagerConfig::from_cli_flags(
2862 self.pager,
2863 effective_no_pager,
2864 self.pager_cmd.as_deref(),
2865 )
2866 }
2867}
2868
2869#[cfg(test)]
2870mod tests {
2871 use super::*;
2872 use crate::large_stack_test;
2873
2874 /// Guard: keep the `Command` enum from silently ballooning.
2875 /// If this fails, consider extracting the largest variant into a Box<T>.
2876 #[test]
2877 fn test_command_enum_size() {
2878 let size = std::mem::size_of::<Command>();
2879 assert!(
2880 size <= 256,
2881 "Command enum is {size} bytes, should be <= 256"
2882 );
2883 }
2884
2885 large_stack_test! {
2886 #[test]
2887 fn test_cli_parse_basic_search() {
2888 let cli = Cli::parse_from(["sqry", "main"]);
2889 assert!(cli.command.is_none());
2890 assert_eq!(cli.pattern, Some("main".to_string()));
2891 assert_eq!(cli.path, None); // Defaults to None, use cli.search_path() to get "."
2892 assert_eq!(cli.search_path(), ".");
2893 }
2894 }
2895
2896 large_stack_test! {
2897 #[test]
2898 fn test_cli_parse_with_path() {
2899 let cli = Cli::parse_from(["sqry", "test", "src/"]);
2900 assert_eq!(cli.pattern, Some("test".to_string()));
2901 assert_eq!(cli.path, Some("src/".to_string()));
2902 assert_eq!(cli.search_path(), "src/");
2903 }
2904 }
2905
2906 large_stack_test! {
2907 #[test]
2908 fn test_cli_parse_search_subcommand() {
2909 let cli = Cli::parse_from(["sqry", "search", "main"]);
2910 assert!(matches!(cli.command.as_deref(), Some(Command::Search { .. })));
2911 }
2912 }
2913
2914 large_stack_test! {
2915 #[test]
2916 fn test_cli_parse_query_subcommand() {
2917 let cli = Cli::parse_from(["sqry", "query", "kind:function"]);
2918 assert!(matches!(cli.command.as_deref(), Some(Command::Query { .. })));
2919 }
2920 }
2921
2922 large_stack_test! {
2923 #[test]
2924 fn test_cli_flags() {
2925 let cli = Cli::parse_from(["sqry", "main", "--json", "--no-color", "--ignore-case"]);
2926 assert!(cli.json);
2927 assert!(cli.no_color);
2928 assert!(cli.ignore_case);
2929 }
2930 }
2931
2932 large_stack_test! {
2933 #[test]
2934 fn test_validation_mode_default() {
2935 let cli = Cli::parse_from(["sqry", "index"]);
2936 assert_eq!(cli.validate, ValidationMode::Warn);
2937 assert!(!cli.auto_rebuild);
2938 }
2939 }
2940
2941 large_stack_test! {
2942 #[test]
2943 fn test_validation_mode_flags() {
2944 let cli = Cli::parse_from(["sqry", "index", "--validate", "fail", "--auto-rebuild"]);
2945 assert_eq!(cli.validate, ValidationMode::Fail);
2946 assert!(cli.auto_rebuild);
2947 }
2948 }
2949
2950 large_stack_test! {
2951 #[test]
2952 fn test_validate_rejects_invalid_columns() {
2953 let cli = Cli::parse_from([
2954 "sqry",
2955 "--csv",
2956 "--columns",
2957 "name,unknown",
2958 "query",
2959 "path",
2960 ]);
2961 let msg = cli.validate().expect("validation should fail");
2962 assert!(msg.contains("Unknown column"), "Unexpected message: {msg}");
2963 }
2964 }
2965
2966 large_stack_test! {
2967 #[test]
2968 fn test_index_rebuild_alias_sets_force() {
2969 // Verify --rebuild is an alias for --force
2970 let cli = Cli::parse_from(["sqry", "index", "--rebuild", "."]);
2971 if let Some(Command::Index { force, .. }) = cli.command.as_deref() {
2972 assert!(force, "--rebuild should set force=true");
2973 } else {
2974 panic!("Expected Index command");
2975 }
2976 }
2977 }
2978
2979 large_stack_test! {
2980 #[test]
2981 fn test_index_force_still_works() {
2982 // Ensure --force continues to work (backward compat)
2983 let cli = Cli::parse_from(["sqry", "index", "--force", "."]);
2984 if let Some(Command::Index { force, .. }) = cli.command.as_deref() {
2985 assert!(force, "--force should set force=true");
2986 } else {
2987 panic!("Expected Index command");
2988 }
2989 }
2990 }
2991
2992 large_stack_test! {
2993 #[test]
2994 fn test_graph_deps_alias() {
2995 // Verify "deps" is an alias for dependency-tree
2996 let cli = Cli::parse_from(["sqry", "graph", "deps", "main"]);
2997 assert!(matches!(
2998 cli.command.as_deref(),
2999 Some(Command::Graph {
3000 operation: GraphOperation::DependencyTree { .. },
3001 ..
3002 })
3003 ));
3004 }
3005 }
3006
3007 large_stack_test! {
3008 #[test]
3009 fn test_graph_cyc_alias() {
3010 let cli = Cli::parse_from(["sqry", "graph", "cyc"]);
3011 assert!(matches!(
3012 cli.command.as_deref(),
3013 Some(Command::Graph {
3014 operation: GraphOperation::Cycles { .. },
3015 ..
3016 })
3017 ));
3018 }
3019 }
3020
3021 large_stack_test! {
3022 #[test]
3023 fn test_graph_cx_alias() {
3024 let cli = Cli::parse_from(["sqry", "graph", "cx"]);
3025 assert!(matches!(
3026 cli.command.as_deref(),
3027 Some(Command::Graph {
3028 operation: GraphOperation::Complexity { .. },
3029 ..
3030 })
3031 ));
3032 }
3033 }
3034
3035 large_stack_test! {
3036 #[test]
3037 fn test_graph_nodes_args() {
3038 let cli = Cli::parse_from([
3039 "sqry",
3040 "graph",
3041 "nodes",
3042 "--kind",
3043 "function",
3044 "--languages",
3045 "rust",
3046 "--file",
3047 "src/",
3048 "--name",
3049 "main",
3050 "--qualified-name",
3051 "crate::main",
3052 "--limit",
3053 "5",
3054 "--offset",
3055 "2",
3056 "--full-paths",
3057 ]);
3058 if let Some(Command::Graph {
3059 operation:
3060 GraphOperation::Nodes {
3061 kind,
3062 languages,
3063 file,
3064 name,
3065 qualified_name,
3066 limit,
3067 offset,
3068 full_paths,
3069 },
3070 ..
3071 }) = cli.command.as_deref()
3072 {
3073 assert_eq!(kind, &Some("function".to_string()));
3074 assert_eq!(languages, &Some("rust".to_string()));
3075 assert_eq!(file, &Some("src/".to_string()));
3076 assert_eq!(name, &Some("main".to_string()));
3077 assert_eq!(qualified_name, &Some("crate::main".to_string()));
3078 assert_eq!(*limit, 5);
3079 assert_eq!(*offset, 2);
3080 assert!(full_paths);
3081 } else {
3082 panic!("Expected Graph Nodes command");
3083 }
3084 }
3085 }
3086
3087 large_stack_test! {
3088 #[test]
3089 fn test_graph_edges_args() {
3090 let cli = Cli::parse_from([
3091 "sqry",
3092 "graph",
3093 "edges",
3094 "--kind",
3095 "calls",
3096 "--from",
3097 "main",
3098 "--to",
3099 "worker",
3100 "--from-lang",
3101 "rust",
3102 "--to-lang",
3103 "python",
3104 "--file",
3105 "src/main.rs",
3106 "--limit",
3107 "10",
3108 "--offset",
3109 "1",
3110 "--full-paths",
3111 ]);
3112 if let Some(Command::Graph {
3113 operation:
3114 GraphOperation::Edges {
3115 kind,
3116 from,
3117 to,
3118 from_lang,
3119 to_lang,
3120 file,
3121 limit,
3122 offset,
3123 full_paths,
3124 },
3125 ..
3126 }) = cli.command.as_deref()
3127 {
3128 assert_eq!(kind, &Some("calls".to_string()));
3129 assert_eq!(from, &Some("main".to_string()));
3130 assert_eq!(to, &Some("worker".to_string()));
3131 assert_eq!(from_lang, &Some("rust".to_string()));
3132 assert_eq!(to_lang, &Some("python".to_string()));
3133 assert_eq!(file, &Some("src/main.rs".to_string()));
3134 assert_eq!(*limit, 10);
3135 assert_eq!(*offset, 1);
3136 assert!(full_paths);
3137 } else {
3138 panic!("Expected Graph Edges command");
3139 }
3140 }
3141 }
3142
3143 // ===== Pager Tests (P2-29) =====
3144
3145 large_stack_test! {
3146 #[test]
3147 fn test_pager_flag_default() {
3148 let cli = Cli::parse_from(["sqry", "query", "kind:function"]);
3149 assert!(!cli.pager);
3150 assert!(!cli.no_pager);
3151 assert!(cli.pager_cmd.is_none());
3152 }
3153 }
3154
3155 large_stack_test! {
3156 #[test]
3157 fn test_pager_flag() {
3158 let cli = Cli::parse_from(["sqry", "--pager", "query", "kind:function"]);
3159 assert!(cli.pager);
3160 assert!(!cli.no_pager);
3161 }
3162 }
3163
3164 large_stack_test! {
3165 #[test]
3166 fn test_no_pager_flag() {
3167 let cli = Cli::parse_from(["sqry", "--no-pager", "query", "kind:function"]);
3168 assert!(!cli.pager);
3169 assert!(cli.no_pager);
3170 }
3171 }
3172
3173 large_stack_test! {
3174 #[test]
3175 fn test_pager_cmd_flag() {
3176 let cli = Cli::parse_from([
3177 "sqry",
3178 "--pager-cmd",
3179 "bat --style=plain",
3180 "query",
3181 "kind:function",
3182 ]);
3183 assert_eq!(cli.pager_cmd, Some("bat --style=plain".to_string()));
3184 }
3185 }
3186
3187 large_stack_test! {
3188 #[test]
3189 fn test_pager_and_no_pager_conflict() {
3190 // These flags conflict and clap should reject
3191 let result =
3192 Cli::try_parse_from(["sqry", "--pager", "--no-pager", "query", "kind:function"]);
3193 assert!(result.is_err());
3194 }
3195 }
3196
3197 large_stack_test! {
3198 #[test]
3199 fn test_pager_flags_global() {
3200 // Pager flags work with any subcommand
3201 let cli = Cli::parse_from(["sqry", "--no-pager", "search", "test"]);
3202 assert!(cli.no_pager);
3203
3204 let cli = Cli::parse_from(["sqry", "--pager", "index"]);
3205 assert!(cli.pager);
3206 }
3207 }
3208
3209 large_stack_test! {
3210 #[test]
3211 fn test_pager_config_json_bypasses_pager() {
3212 use crate::output::pager::PagerMode;
3213
3214 // JSON output should bypass pager by default
3215 let cli = Cli::parse_from(["sqry", "--json", "search", "test"]);
3216 let config = cli.pager_config();
3217 assert_eq!(config.enabled, PagerMode::Never);
3218 }
3219 }
3220
3221 large_stack_test! {
3222 #[test]
3223 fn test_pager_config_csv_bypasses_pager() {
3224 use crate::output::pager::PagerMode;
3225
3226 // CSV output should bypass pager by default
3227 let cli = Cli::parse_from(["sqry", "--csv", "search", "test"]);
3228 let config = cli.pager_config();
3229 assert_eq!(config.enabled, PagerMode::Never);
3230 }
3231 }
3232
3233 large_stack_test! {
3234 #[test]
3235 fn test_pager_config_tsv_bypasses_pager() {
3236 use crate::output::pager::PagerMode;
3237
3238 // TSV output should bypass pager by default
3239 let cli = Cli::parse_from(["sqry", "--tsv", "search", "test"]);
3240 let config = cli.pager_config();
3241 assert_eq!(config.enabled, PagerMode::Never);
3242 }
3243 }
3244
3245 large_stack_test! {
3246 #[test]
3247 fn test_pager_config_json_with_explicit_pager() {
3248 use crate::output::pager::PagerMode;
3249
3250 // JSON with explicit --pager should enable pager
3251 let cli = Cli::parse_from(["sqry", "--json", "--pager", "search", "test"]);
3252 let config = cli.pager_config();
3253 assert_eq!(config.enabled, PagerMode::Always);
3254 }
3255 }
3256
3257 large_stack_test! {
3258 #[test]
3259 fn test_pager_config_text_output_auto() {
3260 use crate::output::pager::PagerMode;
3261
3262 // Text output (default) should use auto pager mode
3263 let cli = Cli::parse_from(["sqry", "search", "test"]);
3264 let config = cli.pager_config();
3265 assert_eq!(config.enabled, PagerMode::Auto);
3266 }
3267 }
3268}