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