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