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