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
1308 /// View usage insights and manage local diagnostics
1309 ///
1310 /// sqry captures anonymous behavioral patterns locally to help you
1311 /// understand your usage and improve the tool. All data stays on
1312 /// your machine unless you explicitly choose to share.
1313 ///
1314 /// Examples:
1315 /// sqry insights show # Show current week's summary
1316 /// sqry insights show --week 2025-W50 # Show specific week
1317 /// sqry insights config # Show configuration
1318 /// sqry insights config --disable # Disable uses capture
1319 /// sqry insights status # Show storage status
1320 /// sqry insights prune --older 90d # Clean up old data
1321 ///
1322 /// Privacy: All data is stored locally. No network calls are made
1323 /// unless you explicitly use --share (which generates a file, not
1324 /// a network request).
1325 #[command(display_order = 62, verbatim_doc_comment)]
1326 Insights {
1327 #[command(subcommand)]
1328 action: InsightsAction,
1329 },
1330
1331 /// Generate a troubleshooting bundle for issue reporting
1332 ///
1333 /// Creates a structured bundle containing diagnostic information
1334 /// that can be shared with the sqry team. All data is sanitized -
1335 /// no code content, file paths, or secrets are included.
1336 ///
1337 /// The bundle includes:
1338 /// - System information (OS, architecture)
1339 /// - sqry version and build type
1340 /// - Sanitized configuration
1341 /// - Recent use events (last 24h)
1342 /// - Recent errors
1343 ///
1344 /// Examples:
1345 /// sqry troubleshoot # Generate to stdout
1346 /// sqry troubleshoot -o bundle.json # Save to file
1347 /// sqry troubleshoot --dry-run # Preview without generating
1348 /// sqry troubleshoot --include-trace # Include workflow trace
1349 ///
1350 /// Privacy: No paths, code content, or secrets are included.
1351 /// Review the output before sharing if you have concerns.
1352 #[command(display_order = 63, verbatim_doc_comment)]
1353 Troubleshoot {
1354 /// Output file path (default: stdout)
1355 #[arg(short = 'o', long, value_name = "FILE", help_heading = headings::INSIGHTS_OUTPUT, display_order = 10)]
1356 output: Option<String>,
1357
1358 /// Preview bundle contents without generating
1359 #[arg(long = "dry-run", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
1360 dry_run: bool,
1361
1362 /// Include workflow trace (opt-in, requires explicit consent)
1363 ///
1364 /// Adds a sequence of recent workflow steps to the bundle.
1365 /// The trace helps understand how operations were performed
1366 /// but reveals more behavioral patterns than the default bundle.
1367 #[arg(long, help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 20)]
1368 include_trace: bool,
1369
1370 /// Time window for events to include (e.g., 24h, 7d)
1371 ///
1372 /// Defaults to 24 hours. Longer windows provide more context
1373 /// but may include older events.
1374 #[arg(long, default_value = "24h", value_name = "DURATION", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 30)]
1375 window: String,
1376 },
1377
1378 /// Find duplicate code in the codebase
1379 ///
1380 /// Detects similar or identical code patterns using structural analysis.
1381 /// Supports different duplicate types:
1382 /// - body: Functions with identical/similar bodies
1383 /// - signature: Functions with identical signatures
1384 /// - struct: Structs with similar field layouts
1385 ///
1386 /// Examples:
1387 /// sqry duplicates # Find body duplicates
1388 /// sqry duplicates --type signature # Find signature duplicates
1389 /// sqry duplicates --threshold 90 # 90% similarity threshold
1390 /// sqry duplicates --exact # Exact matches only
1391 #[command(display_order = 21, verbatim_doc_comment)]
1392 Duplicates {
1393 /// Search path (defaults to current directory).
1394 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1395 path: Option<String>,
1396
1397 /// Type of duplicate detection.
1398 ///
1399 /// - body: Functions with identical/similar bodies (default)
1400 /// - signature: Functions with identical signatures
1401 /// - struct: Structs with similar field layouts
1402 #[arg(long, short = 't', default_value = "body", help_heading = headings::DUPLICATE_OPTIONS, display_order = 10)]
1403 r#type: String,
1404
1405 /// Similarity threshold (0-100, default: 80).
1406 ///
1407 /// Higher values require more similarity to be considered duplicates.
1408 /// 100 means exact matches only.
1409 #[arg(long, default_value = "80", help_heading = headings::DUPLICATE_OPTIONS, display_order = 20)]
1410 threshold: u32,
1411
1412 /// Maximum results to return.
1413 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1414 max_results: usize,
1415
1416 /// Exact matches only (equivalent to --threshold 100).
1417 #[arg(long, help_heading = headings::DUPLICATE_OPTIONS, display_order = 30)]
1418 exact: bool,
1419 },
1420
1421 /// Find circular dependencies in the codebase
1422 ///
1423 /// Detects cycles in call graphs, import graphs, or module dependencies.
1424 /// Uses Tarjan's SCC algorithm for efficient O(V+E) detection.
1425 ///
1426 /// Examples:
1427 /// sqry cycles # Find call cycles
1428 /// sqry cycles --type imports # Find import cycles
1429 /// sqry cycles --min-depth 3 # Cycles with 3+ nodes
1430 /// sqry cycles --include-self # Include self-loops
1431 #[command(display_order = 22, verbatim_doc_comment)]
1432 Cycles {
1433 /// Search path (defaults to current directory).
1434 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1435 path: Option<String>,
1436
1437 /// Type of cycle detection.
1438 ///
1439 /// - calls: Function/method call cycles (default)
1440 /// - imports: File import cycles
1441 /// - modules: Module-level cycles
1442 #[arg(long, short = 't', default_value = "calls", help_heading = headings::CYCLE_OPTIONS, display_order = 10)]
1443 r#type: String,
1444
1445 /// Minimum cycle depth (default: 2).
1446 #[arg(long, default_value = "2", help_heading = headings::CYCLE_OPTIONS, display_order = 20)]
1447 min_depth: usize,
1448
1449 /// Maximum cycle depth (default: unlimited).
1450 #[arg(long, help_heading = headings::CYCLE_OPTIONS, display_order = 30)]
1451 max_depth: Option<usize>,
1452
1453 /// Include self-loops (A → A).
1454 #[arg(long, help_heading = headings::CYCLE_OPTIONS, display_order = 40)]
1455 include_self: bool,
1456
1457 /// Maximum results to return.
1458 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1459 max_results: usize,
1460 },
1461
1462 /// Find unused/dead code in the codebase
1463 ///
1464 /// Detects symbols that are never referenced using reachability analysis.
1465 /// Entry points (main, public lib exports, tests) are considered reachable.
1466 ///
1467 /// Examples:
1468 /// sqry unused # Find all unused symbols
1469 /// sqry unused --scope public # Only public unused symbols
1470 /// sqry unused --scope function # Only unused functions
1471 /// sqry unused --lang rust # Only in Rust files
1472 #[command(display_order = 23, verbatim_doc_comment)]
1473 Unused {
1474 /// Search path (defaults to current directory).
1475 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1476 path: Option<String>,
1477
1478 /// Scope of unused detection.
1479 ///
1480 /// - all: All unused symbols (default)
1481 /// - public: Public symbols with no external references
1482 /// - private: Private symbols with no references
1483 /// - function: Unused functions only
1484 /// - struct: Unused structs/types only
1485 #[arg(long, short = 's', default_value = "all", help_heading = headings::UNUSED_OPTIONS, display_order = 10)]
1486 scope: String,
1487
1488 /// Filter by language.
1489 #[arg(long, help_heading = headings::UNUSED_OPTIONS, display_order = 20)]
1490 lang: Option<String>,
1491
1492 /// Filter by symbol kind.
1493 #[arg(long, help_heading = headings::UNUSED_OPTIONS, display_order = 30)]
1494 kind: Option<String>,
1495
1496 /// Maximum results to return.
1497 #[arg(long, default_value = "100", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1498 max_results: usize,
1499 },
1500
1501 /// Export the code graph in various formats
1502 ///
1503 /// Exports the unified code graph to DOT, D2, Mermaid, or JSON formats
1504 /// for visualization or further analysis.
1505 ///
1506 /// Examples:
1507 /// sqry export # DOT format to stdout
1508 /// sqry export --format mermaid # Mermaid format
1509 /// sqry export --format d2 -o graph.d2 # D2 format to file
1510 /// sqry export --highlight-cross # Highlight cross-language edges
1511 /// sqry export --filter-lang rust,python # Filter languages
1512 #[command(display_order = 31, verbatim_doc_comment)]
1513 Export {
1514 /// Search path (defaults to current directory).
1515 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1516 path: Option<String>,
1517
1518 /// Output format.
1519 ///
1520 /// - dot: Graphviz DOT format (default)
1521 /// - d2: D2 diagram format
1522 /// - mermaid: Mermaid markdown format
1523 /// - json: JSON format for programmatic use
1524 #[arg(long, short = 'f', default_value = "dot", help_heading = headings::EXPORT_OPTIONS, display_order = 10)]
1525 format: String,
1526
1527 /// Graph layout direction.
1528 ///
1529 /// - lr: Left to right (default)
1530 /// - tb: Top to bottom
1531 #[arg(long, short = 'd', default_value = "lr", help_heading = headings::EXPORT_OPTIONS, display_order = 20)]
1532 direction: String,
1533
1534 /// Filter by languages (comma-separated).
1535 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 30)]
1536 filter_lang: Option<String>,
1537
1538 /// Filter by edge types (comma-separated: calls,imports,exports).
1539 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 40)]
1540 filter_edge: Option<String>,
1541
1542 /// Highlight cross-language edges.
1543 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 50)]
1544 highlight_cross: bool,
1545
1546 /// Show node details (signatures, docs).
1547 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 60)]
1548 show_details: bool,
1549
1550 /// Show edge labels.
1551 #[arg(long, help_heading = headings::EXPORT_OPTIONS, display_order = 70)]
1552 show_labels: bool,
1553
1554 /// Output file (default: stdout).
1555 #[arg(long, short = 'o', help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1556 output: Option<String>,
1557 },
1558
1559 /// Explain a symbol with context and relations
1560 ///
1561 /// Get detailed information about a symbol including its code context,
1562 /// callers, callees, and other relationships.
1563 ///
1564 /// Examples:
1565 /// sqry explain src/main.rs main # Explain main function
1566 /// sqry explain src/lib.rs `MyStruct` # Explain a struct
1567 /// sqry explain --no-context file.rs func # Skip code context
1568 /// sqry explain --no-relations file.rs fn # Skip relations
1569 #[command(alias = "exp", display_order = 26, verbatim_doc_comment)]
1570 Explain {
1571 /// File containing the symbol.
1572 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1573 file: String,
1574
1575 /// Symbol name to explain.
1576 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1577 symbol: String,
1578
1579 /// Search path (defaults to current directory).
1580 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1581 path: Option<String>,
1582
1583 /// Skip code context in output.
1584 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1585 no_context: bool,
1586
1587 /// Skip relation information in output.
1588 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 20)]
1589 no_relations: bool,
1590 },
1591
1592 /// Find symbols similar to a reference symbol
1593 ///
1594 /// Uses fuzzy name matching to find symbols that are similar
1595 /// to a given reference symbol.
1596 ///
1597 /// Examples:
1598 /// sqry similar src/lib.rs processData # Find similar to processData
1599 /// sqry similar --threshold 0.8 file.rs fn # 80% similarity threshold
1600 /// sqry similar --limit 20 file.rs func # Limit to 20 results
1601 #[command(alias = "sim", display_order = 27, verbatim_doc_comment)]
1602 Similar {
1603 /// File containing the reference symbol.
1604 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1605 file: String,
1606
1607 /// Reference symbol name.
1608 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1609 symbol: String,
1610
1611 /// Search path (defaults to current directory).
1612 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1613 path: Option<String>,
1614
1615 /// Minimum similarity threshold (0.0 to 1.0, default: 0.7).
1616 #[arg(long, short = 't', default_value = "0.7", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1617 threshold: f64,
1618
1619 /// Maximum results to return (default: 20).
1620 #[arg(long, short = 'l', default_value = "20", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1621 limit: usize,
1622 },
1623
1624 /// Extract a focused subgraph around seed symbols
1625 ///
1626 /// Collects nodes and edges within a specified depth from seed symbols,
1627 /// useful for understanding local code structure.
1628 ///
1629 /// Examples:
1630 /// sqry subgraph main # Subgraph around main
1631 /// sqry subgraph -d 3 func1 func2 # Depth 3, multiple seeds
1632 /// sqry subgraph --no-callers main # Only callees
1633 /// sqry subgraph --include-imports main # Include import edges
1634 #[command(alias = "sub", display_order = 28, verbatim_doc_comment)]
1635 Subgraph {
1636 /// Seed symbol names (at least one required).
1637 #[arg(required = true, help_heading = headings::SEARCH_INPUT, display_order = 10)]
1638 symbols: Vec<String>,
1639
1640 /// Search path (defaults to current directory).
1641 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1642 path: Option<String>,
1643
1644 /// Maximum traversal depth from seeds (default: 2).
1645 #[arg(long, short = 'd', default_value = "2", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1646 depth: usize,
1647
1648 /// Maximum nodes to include (default: 50).
1649 #[arg(long, short = 'n', default_value = "50", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1650 max_nodes: usize,
1651
1652 /// Exclude callers (incoming edges).
1653 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1654 no_callers: bool,
1655
1656 /// Exclude callees (outgoing edges).
1657 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1658 no_callees: bool,
1659
1660 /// Include import relationships.
1661 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
1662 include_imports: bool,
1663 },
1664
1665 /// Analyze what would break if a symbol changes
1666 ///
1667 /// Performs reverse dependency analysis to find all symbols
1668 /// that directly or indirectly depend on the target.
1669 ///
1670 /// Examples:
1671 /// sqry impact authenticate # Impact of changing authenticate
1672 /// sqry impact -d 5 `MyClass` # Deep analysis (5 levels)
1673 /// sqry impact --direct-only func # Only direct dependents
1674 /// sqry impact --show-files func # Show affected files
1675 #[command(alias = "imp", display_order = 24, verbatim_doc_comment)]
1676 Impact {
1677 /// Symbol to analyze.
1678 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1679 symbol: String,
1680
1681 /// Search path (defaults to current directory).
1682 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1683 path: Option<String>,
1684
1685 /// Maximum analysis depth (default: 3).
1686 #[arg(long, short = 'd', default_value = "3", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1687 depth: usize,
1688
1689 /// Maximum results to return (default: 100).
1690 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1691 limit: usize,
1692
1693 /// Only show direct dependents (depth 1).
1694 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1695 direct_only: bool,
1696
1697 /// Show list of affected files.
1698 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1699 show_files: bool,
1700 },
1701
1702 /// Compare semantic changes between git refs
1703 ///
1704 /// Analyzes AST differences between two git refs to detect added, removed,
1705 /// modified, and renamed symbols. Provides structured output showing what
1706 /// changed semantically, not just textually.
1707 ///
1708 /// Examples:
1709 /// sqry diff main HEAD # Compare branches
1710 /// sqry diff v1.0.0 v2.0.0 --json # Release comparison
1711 /// sqry diff HEAD~5 HEAD --kind function # Functions only
1712 /// sqry diff main feature --change-type added # New symbols only
1713 #[command(alias = "sdiff", display_order = 25, verbatim_doc_comment)]
1714 Diff {
1715 /// Base git ref (commit, branch, or tag).
1716 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1717 base: String,
1718
1719 /// Target git ref (commit, branch, or tag).
1720 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 20)]
1721 target: String,
1722
1723 /// Path to git repository (defaults to current directory).
1724 ///
1725 /// Can be the repository root or any path within it - sqry will walk up
1726 /// the directory tree to find the .git directory.
1727 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 30)]
1728 path: Option<String>,
1729
1730 /// Maximum total results to display (default: 100).
1731 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1732 limit: usize,
1733
1734 /// Filter by symbol kinds (comma-separated).
1735 #[arg(long, short = 'k', help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1736 kind: Option<String>,
1737
1738 /// Filter by change types (comma-separated).
1739 ///
1740 /// Valid values: `added`, `removed`, `modified`, `renamed`, `signature_changed`
1741 ///
1742 /// Example: --change-type added,modified
1743 #[arg(long, short = 'c', help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1744 change_type: Option<String>,
1745 },
1746
1747 /// Hierarchical semantic search (RAG-optimized)
1748 ///
1749 /// Performs semantic search with results grouped by file and container,
1750 /// optimized for retrieval-augmented generation (RAG) workflows.
1751 ///
1752 /// Examples:
1753 /// sqry hier "kind:function" # All functions, grouped
1754 /// sqry hier "auth" --max-files 10 # Limit file groups
1755 /// sqry hier --kind function "test" # Filter by kind
1756 /// sqry hier --context 5 "validate" # More context lines
1757 #[command(display_order = 4, verbatim_doc_comment)]
1758 Hier {
1759 /// Search query.
1760 #[arg(help_heading = headings::SEARCH_INPUT, display_order = 10)]
1761 query: String,
1762
1763 /// Search path (defaults to current directory).
1764 #[arg(long, help_heading = headings::SEARCH_INPUT, display_order = 20)]
1765 path: Option<String>,
1766
1767 /// Maximum symbols before grouping (default: 200).
1768 #[arg(long, short = 'l', default_value = "200", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
1769 limit: usize,
1770
1771 /// Maximum files in output (default: 20).
1772 #[arg(long, default_value = "20", help_heading = headings::GRAPH_FILTERING, display_order = 20)]
1773 max_files: usize,
1774
1775 /// Context lines around matches (default: 3).
1776 #[arg(long, short = 'c', default_value = "3", help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
1777 context: usize,
1778
1779 /// Filter by symbol kinds (comma-separated).
1780 #[arg(long, short = 'k', help_heading = headings::GRAPH_FILTERING, display_order = 30)]
1781 kind: Option<String>,
1782
1783 /// Filter by languages (comma-separated).
1784 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
1785 lang: Option<String>,
1786 },
1787
1788 /// Configure MCP server integration for AI coding tools
1789 ///
1790 /// Auto-detect and configure sqry MCP for Claude Code, Codex, and Gemini CLI.
1791 /// The setup command writes tool-specific configuration so AI coding assistants
1792 /// can use sqry's semantic code search capabilities.
1793 ///
1794 /// Examples:
1795 /// sqry mcp setup # Auto-configure all detected tools
1796 /// sqry mcp setup --tool claude # Configure Claude Code only
1797 /// sqry mcp setup --scope global --dry-run # Preview global config changes
1798 /// sqry mcp status # Show current MCP configuration
1799 /// sqry mcp status --json # Machine-readable status
1800 #[command(display_order = 51, verbatim_doc_comment)]
1801 Mcp {
1802 #[command(subcommand)]
1803 command: McpCommand,
1804 },
1805
1806 /// Manage the sqry daemon (sqryd).
1807 ///
1808 /// The daemon provides persistent, shared code-graph indexing for
1809 /// faster queries across concurrent editor sessions.
1810 ///
1811 /// Examples:
1812 /// sqry daemon start # Start the daemon in the background
1813 /// sqry daemon stop # Stop the running daemon
1814 /// sqry daemon status # Show daemon health and workspaces
1815 /// sqry daemon status --json # Machine-readable status
1816 /// sqry daemon logs --follow # Tail the daemon log
1817 #[command(display_order = 35, verbatim_doc_comment)]
1818 Daemon {
1819 #[command(subcommand)]
1820 action: Box<DaemonAction>,
1821 },
1822}
1823
1824/// Daemon management subcommands
1825#[derive(Subcommand, Debug, Clone)]
1826pub enum DaemonAction {
1827 /// Start the sqry daemon in the background.
1828 ///
1829 /// Locates the `sqryd` binary (sibling to `sqry` or on PATH),
1830 /// spawns it with `sqryd start --detach`, and waits for readiness.
1831 Start {
1832 /// Path to the sqryd binary (default: auto-detect).
1833 #[arg(long)]
1834 sqryd_path: Option<PathBuf>,
1835 /// Maximum seconds to wait for daemon readiness.
1836 #[arg(long, default_value_t = 10)]
1837 timeout: u64,
1838 },
1839 /// Stop the running sqry daemon.
1840 Stop {
1841 /// Maximum seconds to wait for graceful shutdown.
1842 #[arg(long, default_value_t = 15)]
1843 timeout: u64,
1844 },
1845 /// Show daemon status (version, uptime, memory, workspaces).
1846 Status {
1847 /// Emit machine-readable JSON instead of human-readable output.
1848 #[arg(long)]
1849 json: bool,
1850 },
1851 /// Tail the daemon log file.
1852 Logs {
1853 /// Number of lines to show from the end of the log.
1854 #[arg(long, short = 'n', default_value_t = 50)]
1855 lines: usize,
1856 /// Follow the log file for new output (like `tail -f`).
1857 #[arg(long, short = 'f')]
1858 follow: bool,
1859 },
1860 /// Load a workspace into the running daemon.
1861 ///
1862 /// Connects to the daemon and sends a `daemon/load` request with the
1863 /// canonicalized path. The daemon's `WorkspaceManager` indexes the
1864 /// workspace, caches the graph in memory, and starts watching for
1865 /// file changes to rebuild incrementally.
1866 Load {
1867 /// Workspace root directory to load.
1868 path: PathBuf,
1869 },
1870 /// Trigger an in-place graph rebuild for a loaded workspace.
1871 ///
1872 /// Sends a `daemon/rebuild` request to the running daemon for the specified
1873 /// workspace root. Once wired (CLI_REBUILD_3), the daemon will re-index the
1874 /// workspace and replace the in-memory graph atomically on completion.
1875 ///
1876 /// Use `--force` to discard any incremental state and perform a full rebuild
1877 /// from scratch (equivalent to dropping and re-loading the workspace).
1878 ///
1879 /// The command will wait up to `--timeout` seconds for the rebuild to finish
1880 /// and report the result as human-readable text or, with `--json`, as a
1881 /// machine-readable JSON object.
1882 #[command(verbatim_doc_comment)]
1883 Rebuild {
1884 /// Workspace root directory to rebuild.
1885 path: PathBuf,
1886 /// Force a full rebuild from scratch, discarding incremental state.
1887 #[arg(long)]
1888 force: bool,
1889 /// Maximum seconds to wait for the rebuild to complete.
1890 /// Default is 1800 seconds (30 minutes). Pass 0 to fire-and-forget.
1891 #[arg(long, default_value_t = 1800)]
1892 timeout: u64,
1893 /// Emit machine-readable JSON output instead of human-readable text.
1894 #[arg(long)]
1895 json: bool,
1896 },
1897}
1898
1899/// MCP server integration subcommands
1900#[derive(Subcommand, Debug, Clone)]
1901pub enum McpCommand {
1902 /// Auto-configure sqry MCP for detected AI tools (Claude Code, Codex, Gemini)
1903 ///
1904 /// Detects installed AI coding tools and writes configuration entries
1905 /// pointing to the sqry-mcp binary. Uses tool-appropriate scoping:
1906 /// - Claude Code: per-project entries with pinned workspace root (default)
1907 /// - Codex/Gemini: global entries using CWD-based workspace discovery
1908 ///
1909 /// Note: Codex and Gemini only support global MCP configs.
1910 /// They rely on being launched from within a project directory
1911 /// for sqry-mcp's CWD discovery to resolve the correct workspace.
1912 Setup {
1913 /// Target tool(s) to configure.
1914 #[arg(long, value_enum, default_value = "all")]
1915 tool: ToolTarget,
1916
1917 /// Configuration scope.
1918 ///
1919 /// - auto: project scope for Claude (when inside a repo), global for Codex/Gemini
1920 /// - project: per-project Claude entry with pinned workspace root
1921 /// - global: global entries for all tools (CWD-dependent for workspace resolution)
1922 ///
1923 /// Note: For Codex and Gemini, --scope project and --scope global behave
1924 /// identically because these tools only support global MCP configs.
1925 #[arg(long, value_enum, default_value = "auto")]
1926 scope: SetupScope,
1927
1928 /// Explicit workspace root path (overrides auto-detection).
1929 ///
1930 /// Only applicable for Claude Code project scope. Rejected for
1931 /// Codex/Gemini because setting a workspace root in their global
1932 /// config would pin to one repo and break multi-repo workflows.
1933 #[arg(long)]
1934 workspace_root: Option<PathBuf>,
1935
1936 /// Overwrite existing sqry configuration.
1937 #[arg(long)]
1938 force: bool,
1939
1940 /// Preview changes without writing.
1941 #[arg(long)]
1942 dry_run: bool,
1943
1944 /// Skip creating .bak backup files.
1945 #[arg(long)]
1946 no_backup: bool,
1947 },
1948
1949 /// Show current MCP configuration status across all tools
1950 ///
1951 /// Reports the sqry-mcp binary location and configuration state
1952 /// for each supported AI tool, including scope, workspace root,
1953 /// and any detected issues (shim usage, drift, missing config).
1954 Status {
1955 /// Output as JSON for programmatic use.
1956 #[arg(long)]
1957 json: bool,
1958 },
1959}
1960
1961/// Target AI tool(s) for MCP configuration
1962#[derive(Debug, Clone, ValueEnum)]
1963pub enum ToolTarget {
1964 /// Configure Claude Code only
1965 Claude,
1966 /// Configure Codex only
1967 Codex,
1968 /// Configure Gemini CLI only
1969 Gemini,
1970 /// Configure all detected tools (default)
1971 All,
1972}
1973
1974/// Configuration scope for MCP setup
1975#[derive(Debug, Clone, ValueEnum)]
1976pub enum SetupScope {
1977 /// Per-project for Claude, global for Codex/Gemini (auto-detect)
1978 Auto,
1979 /// Per-project entries with pinned workspace root
1980 Project,
1981 /// Global entries (CWD-dependent workspace resolution)
1982 Global,
1983}
1984
1985/// Graph-based query operations
1986#[derive(Subcommand, Debug, Clone)]
1987pub enum GraphOperation {
1988 /// Find shortest path between two symbols
1989 ///
1990 /// Traces the shortest execution path from one symbol to another,
1991 /// following Call, `HTTPRequest`, and `FFICall` edges.
1992 ///
1993 /// Example: sqry graph trace-path main processData
1994 TracePath {
1995 /// Source symbol name (e.g., "main", "User.authenticate").
1996 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
1997 from: String,
1998
1999 /// Target symbol name.
2000 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 20)]
2001 to: String,
2002
2003 /// Filter by languages (comma-separated, e.g., "javascript,python").
2004 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2005 languages: Option<String>,
2006
2007 /// Show full file paths in output.
2008 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2009 full_paths: bool,
2010 },
2011
2012 /// Calculate maximum call chain depth from a symbol
2013 ///
2014 /// Computes the longest call chain starting from the given symbol,
2015 /// useful for complexity analysis and recursion detection.
2016 ///
2017 /// Example: sqry graph call-chain-depth main
2018 CallChainDepth {
2019 /// Symbol name to analyze.
2020 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2021 symbol: String,
2022
2023 /// Filter by languages (comma-separated).
2024 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2025 languages: Option<String>,
2026
2027 /// Show the actual call chain, not just the depth.
2028 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2029 show_chain: bool,
2030 },
2031
2032 /// Show transitive dependencies for a module
2033 ///
2034 /// Analyzes all imports transitively to build a complete dependency tree,
2035 /// including circular dependency detection.
2036 ///
2037 /// Example: sqry graph dependency-tree src/main.js
2038 #[command(alias = "deps")]
2039 DependencyTree {
2040 /// Module path or name.
2041 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2042 module: String,
2043
2044 /// Maximum depth to traverse (default: unlimited).
2045 #[arg(long, help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
2046 max_depth: Option<usize>,
2047
2048 /// Show circular dependencies only.
2049 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2050 cycles_only: bool,
2051 },
2052
2053 /// List all cross-language relationships
2054 ///
2055 /// Finds edges connecting symbols in different programming languages,
2056 /// such as TypeScript→JavaScript imports, Python→C FFI calls, SQL table
2057 /// access, Dart `MethodChannel` invocations, and Flutter widget hierarchies.
2058 ///
2059 /// Supported languages for --from-lang/--to-lang:
2060 /// js, ts, py, cpp, c, csharp (cs), java, go, ruby, php,
2061 /// swift, kotlin, scala, sql, dart, lua, perl, shell (bash),
2062 /// groovy, http
2063 ///
2064 /// Examples:
2065 /// sqry graph cross-language --from-lang dart --edge-type `channel_invoke`
2066 /// sqry graph cross-language --from-lang sql --edge-type `table_read`
2067 /// sqry graph cross-language --edge-type `widget_child`
2068 #[command(verbatim_doc_comment)]
2069 CrossLanguage {
2070 /// Filter by source language.
2071 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2072 from_lang: Option<String>,
2073
2074 /// Filter by target language.
2075 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
2076 to_lang: Option<String>,
2077
2078 /// Edge type filter.
2079 ///
2080 /// Supported values:
2081 /// call, import, http, ffi,
2082 /// `table_read`, `table_write`, `triggered_by`,
2083 /// `channel_invoke`, `widget_child`
2084 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
2085 edge_type: Option<String>,
2086
2087 /// Minimum confidence threshold (0.0-1.0).
2088 #[arg(long, default_value = "0.0", help_heading = headings::GRAPH_FILTERING, display_order = 40)]
2089 min_confidence: f64,
2090 },
2091
2092 /// List unified graph nodes
2093 ///
2094 /// Enumerates nodes from the unified graph snapshot and applies filters.
2095 /// Useful for inspecting graph coverage and metadata details.
2096 Nodes {
2097 /// Filter by node kind(s) (comma-separated: function,method,macro).
2098 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2099 kind: Option<String>,
2100
2101 /// Filter by language(s) (comma-separated: rust,python).
2102 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
2103 languages: Option<String>,
2104
2105 /// Filter by file path substring (case-insensitive).
2106 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
2107 file: Option<String>,
2108
2109 /// Filter by name substring (case-sensitive).
2110 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
2111 name: Option<String>,
2112
2113 /// Filter by qualified name substring (case-sensitive).
2114 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
2115 qualified_name: Option<String>,
2116
2117 /// Maximum results (default: 1000, max: 10000; use 0 for default).
2118 #[arg(long, default_value = "1000", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2119 limit: usize,
2120
2121 /// Skip N results.
2122 #[arg(long, default_value = "0", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2123 offset: usize,
2124
2125 /// Show full file paths in output.
2126 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 30)]
2127 full_paths: bool,
2128 },
2129
2130 /// List unified graph edges
2131 ///
2132 /// Enumerates edges from the unified graph snapshot and applies filters.
2133 /// Useful for inspecting relationships and cross-cutting metadata.
2134 Edges {
2135 /// Filter by edge kind tag(s) (comma-separated: calls,imports).
2136 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2137 kind: Option<String>,
2138
2139 /// Filter by source label substring (case-sensitive).
2140 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
2141 from: Option<String>,
2142
2143 /// Filter by target label substring (case-sensitive).
2144 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 30)]
2145 to: Option<String>,
2146
2147 /// Filter by source language.
2148 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 40)]
2149 from_lang: Option<String>,
2150
2151 /// Filter by target language.
2152 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 50)]
2153 to_lang: Option<String>,
2154
2155 /// Filter by file path substring (case-insensitive, source file only).
2156 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 60)]
2157 file: Option<String>,
2158
2159 /// Maximum results (default: 1000, max: 10000; use 0 for default).
2160 #[arg(long, default_value = "1000", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2161 limit: usize,
2162
2163 /// Skip N results.
2164 #[arg(long, default_value = "0", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2165 offset: usize,
2166
2167 /// Show full file paths in output.
2168 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 30)]
2169 full_paths: bool,
2170 },
2171
2172 /// Show graph statistics and summary
2173 ///
2174 /// Displays overall graph metrics including node counts by language,
2175 /// edge counts by type, and cross-language relationship statistics.
2176 ///
2177 /// Example: sqry graph stats
2178 Stats {
2179 /// Show detailed breakdown by file.
2180 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2181 by_file: bool,
2182
2183 /// Show detailed breakdown by language.
2184 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2185 by_language: bool,
2186 },
2187
2188 /// Show unified graph snapshot status
2189 ///
2190 /// Reports on the state of the unified graph snapshot stored in
2191 /// `.sqry/graph/` directory. Displays build timestamp, node/edge counts,
2192 /// and snapshot age.
2193 ///
2194 /// Example: sqry graph status
2195 Status,
2196
2197 /// Show Phase 1 fact-layer provenance for a symbol
2198 ///
2199 /// Prints the snapshot's fact epoch, node provenance (first/last seen
2200 /// epoch, content hash), file provenance, and an edge-provenance summary
2201 /// for the matched symbol. This is the end-to-end proof that the V8
2202 /// save → load → accessor → CLI path is wired.
2203 ///
2204 /// Example: sqry graph provenance `my_function`
2205 #[command(alias = "prov")]
2206 Provenance {
2207 /// Symbol name to inspect (qualified or unqualified).
2208 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2209 symbol: String,
2210
2211 /// Output as JSON.
2212 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2213 json: bool,
2214 },
2215
2216 /// Resolve a symbol through the Phase 2 binding plane
2217 ///
2218 /// Loads the snapshot, constructs a BindingPlane facade, runs
2219 /// BindingPlane::resolve() for the given symbol, and prints the outcome
2220 /// along with the list of matched bindings. This is the end-to-end proof
2221 /// point for the Phase 2 binding plane (FR9).
2222 ///
2223 /// With `--explain` the ordered witness step trace is printed below the
2224 /// binding list, showing every bucket probe, candidate considered, and
2225 /// the terminal Chose/Ambiguous/Unresolved step.
2226 ///
2227 /// Example: sqry graph resolve my_function
2228 /// Example: sqry graph resolve my_function --explain
2229 /// Example: sqry graph resolve my_function --explain --json
2230 #[command(alias = "res")]
2231 Resolve {
2232 /// Symbol name to resolve (qualified or unqualified).
2233 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2234 symbol: String,
2235
2236 /// Print the ordered witness step trace (bucket probes, candidate
2237 /// evaluations, and the terminal Chose/Ambiguous/Unresolved step).
2238 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2239 explain: bool,
2240
2241 /// Emit a stable JSON document instead of human-readable text.
2242 /// The JSON shape (symbol/outcome/bindings/explain) is the documented
2243 /// stable external contract for scripting and tool integration.
2244 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2245 json: bool,
2246 },
2247
2248 /// Detect circular dependencies in the codebase
2249 ///
2250 /// Finds all cycles in the call and import graphs, which can indicate
2251 /// potential design issues or circular dependency problems.
2252 ///
2253 /// Example: sqry graph cycles
2254 #[command(alias = "cyc")]
2255 Cycles {
2256 /// Minimum cycle length to report (default: 2).
2257 #[arg(long, default_value = "2", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
2258 min_length: usize,
2259
2260 /// Maximum cycle length to report (default: unlimited).
2261 #[arg(long, help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 20)]
2262 max_length: Option<usize>,
2263
2264 /// Only analyze import edges (ignore calls).
2265 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2266 imports_only: bool,
2267
2268 /// Filter by languages (comma-separated).
2269 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
2270 languages: Option<String>,
2271 },
2272
2273 /// Calculate code complexity metrics
2274 ///
2275 /// Analyzes cyclomatic complexity, call graph depth, and other
2276 /// complexity metrics for functions and modules.
2277 ///
2278 /// Example: sqry graph complexity
2279 #[command(alias = "cx")]
2280 Complexity {
2281 /// Target symbol or module (default: analyze all).
2282 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2283 target: Option<String>,
2284
2285 /// Sort by complexity score.
2286 #[arg(long = "sort-complexity", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2287 sort_complexity: bool,
2288
2289 /// Show only items above this complexity threshold.
2290 #[arg(long, default_value = "0", help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2291 min_complexity: usize,
2292
2293 /// Filter by languages (comma-separated).
2294 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 20)]
2295 languages: Option<String>,
2296 },
2297
2298 /// Find direct callers of a symbol
2299 ///
2300 /// Lists all symbols that directly call the specified function, method,
2301 /// or other callable. Useful for understanding symbol usage and impact analysis.
2302 ///
2303 /// Example: sqry graph direct-callers authenticate
2304 #[command(alias = "callers")]
2305 DirectCallers {
2306 /// Symbol name to find callers for.
2307 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2308 symbol: String,
2309
2310 /// Maximum results (default: 100).
2311 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2312 limit: usize,
2313
2314 /// Filter by languages (comma-separated).
2315 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2316 languages: Option<String>,
2317
2318 /// Show full file paths in output.
2319 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2320 full_paths: bool,
2321 },
2322
2323 /// Find direct callees of a symbol
2324 ///
2325 /// Lists all symbols that are directly called by the specified function
2326 /// or method. Useful for understanding dependencies and refactoring scope.
2327 ///
2328 /// Example: sqry graph direct-callees processData
2329 #[command(alias = "callees")]
2330 DirectCallees {
2331 /// Symbol name to find callees for.
2332 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2333 symbol: String,
2334
2335 /// Maximum results (default: 100).
2336 #[arg(long, short = 'l', default_value = "100", help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2337 limit: usize,
2338
2339 /// Filter by languages (comma-separated).
2340 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2341 languages: Option<String>,
2342
2343 /// Show full file paths in output.
2344 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 20)]
2345 full_paths: bool,
2346 },
2347
2348 /// Show call hierarchy for a symbol
2349 ///
2350 /// Displays incoming and/or outgoing call relationships in a tree format.
2351 /// Useful for understanding code flow and impact of changes.
2352 ///
2353 /// Example: sqry graph call-hierarchy main --depth 3
2354 #[command(alias = "ch")]
2355 CallHierarchy {
2356 /// Symbol name to show hierarchy for.
2357 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2358 symbol: String,
2359
2360 /// Maximum depth to traverse (default: 3).
2361 #[arg(long, short = 'd', default_value = "3", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
2362 depth: usize,
2363
2364 /// Direction: incoming, outgoing, or both (default: both).
2365 #[arg(long, default_value = "both", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 20)]
2366 direction: String,
2367
2368 /// Filter by languages (comma-separated).
2369 #[arg(long, help_heading = headings::GRAPH_FILTERING, display_order = 10)]
2370 languages: Option<String>,
2371
2372 /// Show full file paths in output.
2373 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2374 full_paths: bool,
2375 },
2376
2377 /// Check if a symbol is in a cycle
2378 ///
2379 /// Determines whether a specific symbol participates in any circular
2380 /// dependency chains. Can optionally show the cycle path.
2381 ///
2382 /// Example: sqry graph is-in-cycle `UserService` --show-cycle
2383 #[command(alias = "incycle")]
2384 IsInCycle {
2385 /// Symbol name to check.
2386 #[arg(help_heading = headings::GRAPH_ANALYSIS_INPUT, display_order = 10)]
2387 symbol: String,
2388
2389 /// Cycle type to check: calls, imports, or all (default: calls).
2390 #[arg(long, default_value = "calls", help_heading = headings::GRAPH_ANALYSIS_OPTIONS, display_order = 10)]
2391 cycle_type: String,
2392
2393 /// Show the full cycle path if found.
2394 #[arg(long, help_heading = headings::GRAPH_OUTPUT_OPTIONS, display_order = 10)]
2395 show_cycle: bool,
2396 },
2397}
2398
2399/// Output format choices for `sqry batch`.
2400#[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq)]
2401pub enum BatchFormat {
2402 /// Human-readable text output (default)
2403 Text,
2404 /// Aggregated JSON output containing all query results
2405 Json,
2406 /// Newline-delimited JSON objects (one per query)
2407 Jsonl,
2408 /// Comma-separated summary per query
2409 Csv,
2410}
2411
2412/// Cache management actions
2413#[derive(Subcommand, Debug, Clone)]
2414pub enum CacheAction {
2415 /// Show cache statistics
2416 ///
2417 /// Display hit rate, size, and entry count for the AST cache.
2418 Stats {
2419 /// Path to check cache for (defaults to current directory).
2420 #[arg(help_heading = headings::CACHE_INPUT, display_order = 10)]
2421 path: Option<String>,
2422 },
2423
2424 /// Clear the cache
2425 ///
2426 /// Remove all cached AST data. Next queries will re-parse files.
2427 Clear {
2428 /// Path to clear cache for (defaults to current directory).
2429 #[arg(help_heading = headings::CACHE_INPUT, display_order = 10)]
2430 path: Option<String>,
2431
2432 /// Confirm deletion (required for safety).
2433 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2434 confirm: bool,
2435 },
2436
2437 /// Prune the cache
2438 ///
2439 /// Remove old or excessive cache entries to reclaim disk space.
2440 /// Supports time-based (--days) and size-based (--size) retention policies.
2441 Prune {
2442 /// Target cache directory (defaults to user cache dir).
2443 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 10)]
2444 path: Option<String>,
2445
2446 /// Remove entries older than N days.
2447 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 20)]
2448 days: Option<u64>,
2449
2450 /// Cap cache to maximum size (e.g., "1GB", "500MB").
2451 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 30)]
2452 size: Option<String>,
2453
2454 /// Preview deletions without removing files.
2455 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2456 dry_run: bool,
2457 },
2458
2459 /// Generate or refresh the macro expansion cache
2460 ///
2461 /// Runs `cargo expand` to generate expanded macro output, then caches
2462 /// the results for use during indexing. Requires `cargo-expand` installed.
2463 ///
2464 /// # Security
2465 ///
2466 /// This executes build scripts and proc macros. Only use on trusted codebases.
2467 Expand {
2468 /// Force regeneration even if cache is fresh.
2469 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 40)]
2470 refresh: bool,
2471
2472 /// Only expand a specific crate (default: all workspace crates).
2473 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 50)]
2474 crate_name: Option<String>,
2475
2476 /// Show what would be expanded without actually running cargo expand.
2477 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 20)]
2478 dry_run: bool,
2479
2480 /// Cache output directory (default: .sqry/expand-cache/).
2481 #[arg(long, help_heading = headings::CACHE_INPUT, display_order = 60)]
2482 output: Option<PathBuf>,
2483 },
2484}
2485
2486/// Config action subcommands
2487#[derive(Subcommand, Debug, Clone)]
2488pub enum ConfigAction {
2489 /// Initialize config with defaults
2490 ///
2491 /// Creates `.sqry/graph/config/config.json` with default settings.
2492 /// Use --force to overwrite existing config.
2493 ///
2494 /// Examples:
2495 /// sqry config init
2496 /// sqry config init --force
2497 #[command(verbatim_doc_comment)]
2498 Init {
2499 /// Project root path (defaults to current directory).
2500 // Path defaults to current directory if not specified
2501 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2502 path: Option<String>,
2503
2504 /// Overwrite existing config.
2505 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 20)]
2506 force: bool,
2507 },
2508
2509 /// Show effective config
2510 ///
2511 /// Displays the complete config with source annotations.
2512 /// Use --key to show a single value.
2513 ///
2514 /// Examples:
2515 /// sqry config show
2516 /// sqry config show --json
2517 /// sqry config show --key `limits.max_results`
2518 #[command(verbatim_doc_comment)]
2519 Show {
2520 /// Project root path (defaults to current directory).
2521 // Path defaults to current directory if not specified
2522 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2523 path: Option<String>,
2524
2525 /// Output as JSON.
2526 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
2527 json: bool,
2528
2529 /// Show only this config key (e.g., `limits.max_results`).
2530 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 20)]
2531 key: Option<String>,
2532 },
2533
2534 /// Set a config value
2535 ///
2536 /// Updates a config key and persists to disk.
2537 /// Shows a diff before applying (use --yes to skip).
2538 ///
2539 /// Examples:
2540 /// sqry config set `limits.max_results` 10000
2541 /// sqry config set `locking.stale_takeover_policy` warn
2542 /// sqry config set `output.page_size` 100 --yes
2543 #[command(verbatim_doc_comment)]
2544 Set {
2545 /// Project root path (defaults to current directory).
2546 // Path defaults to current directory if not specified
2547 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2548 path: Option<String>,
2549
2550 /// Config key (e.g., `limits.max_results`).
2551 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2552 key: String,
2553
2554 /// New value.
2555 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 30)]
2556 value: String,
2557
2558 /// Skip confirmation prompt.
2559 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 40)]
2560 yes: bool,
2561 },
2562
2563 /// Get a config value
2564 ///
2565 /// Retrieves a single config value.
2566 ///
2567 /// Examples:
2568 /// sqry config get `limits.max_results`
2569 /// sqry config get `locking.stale_takeover_policy`
2570 #[command(verbatim_doc_comment)]
2571 Get {
2572 /// Project root path (defaults to current directory).
2573 // Path defaults to current directory if not specified
2574 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2575 path: Option<String>,
2576
2577 /// Config key (e.g., `limits.max_results`).
2578 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2579 key: String,
2580 },
2581
2582 /// Validate config file
2583 ///
2584 /// Checks config syntax and schema validity.
2585 ///
2586 /// Examples:
2587 /// sqry config validate
2588 #[command(verbatim_doc_comment)]
2589 Validate {
2590 /// Project root path (defaults to current directory).
2591 // Path defaults to current directory if not specified
2592 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2593 path: Option<String>,
2594 },
2595
2596 /// Manage query aliases
2597 #[command(subcommand)]
2598 Alias(ConfigAliasAction),
2599}
2600
2601/// Config alias subcommands
2602#[derive(Subcommand, Debug, Clone)]
2603pub enum ConfigAliasAction {
2604 /// Create or update an alias
2605 ///
2606 /// Examples:
2607 /// sqry config alias set my-funcs "kind:function"
2608 /// sqry config alias set my-funcs "kind:function" --description "All functions"
2609 #[command(verbatim_doc_comment)]
2610 Set {
2611 /// Project root path (defaults to current directory).
2612 // Path defaults to current directory if not specified
2613 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2614 path: Option<String>,
2615
2616 /// Alias name.
2617 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2618 name: String,
2619
2620 /// Query expression.
2621 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 30)]
2622 query: String,
2623
2624 /// Optional description.
2625 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 40)]
2626 description: Option<String>,
2627 },
2628
2629 /// List all aliases
2630 ///
2631 /// Examples:
2632 /// sqry config alias list
2633 /// sqry config alias list --json
2634 #[command(verbatim_doc_comment)]
2635 List {
2636 /// Project root path (defaults to current directory).
2637 // Path defaults to current directory if not specified
2638 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2639 path: Option<String>,
2640
2641 /// Output as JSON.
2642 #[arg(long, help_heading = headings::OUTPUT_CONTROL, display_order = 10)]
2643 json: bool,
2644 },
2645
2646 /// Remove an alias
2647 ///
2648 /// Examples:
2649 /// sqry config alias remove my-funcs
2650 #[command(verbatim_doc_comment)]
2651 Remove {
2652 /// Project root path (defaults to current directory).
2653 // Path defaults to current directory if not specified
2654 #[arg(long, help_heading = headings::CONFIG_INPUT, display_order = 5)]
2655 path: Option<String>,
2656
2657 /// Alias name to remove.
2658 #[arg(help_heading = headings::CONFIG_INPUT, display_order = 20)]
2659 name: String,
2660 },
2661}
2662
2663/// Visualize code relationships from relation queries.
2664///
2665/// Examples:
2666/// sqry visualize "callers:main" --format mermaid
2667/// sqry visualize "imports:std" --format graphviz --output-file deps.dot
2668/// sqry visualize "callees:process" --depth 5 --max-nodes 200
2669#[derive(Debug, Args, Clone)]
2670#[command(
2671 about = "Visualize code relationships as diagrams",
2672 long_about = "Visualize code relationships as diagrams.\n\n\
2673Examples:\n sqry visualize \"callers:main\" --format mermaid\n \
2674sqry visualize \"imports:std\" --format graphviz --output-file deps.dot\n \
2675sqry visualize \"callees:process\" --depth 5 --max-nodes 200",
2676 after_help = "Examples:\n sqry visualize \"callers:main\" --format mermaid\n \
2677sqry visualize \"imports:std\" --format graphviz --output-file deps.dot\n \
2678sqry visualize \"callees:process\" --depth 5 --max-nodes 200"
2679)]
2680pub struct VisualizeCommand {
2681 /// Relation query (e.g., callers:main, callees:helper).
2682 #[arg(help_heading = headings::VISUALIZATION_INPUT, display_order = 10)]
2683 pub query: String,
2684
2685 /// Target path (defaults to CLI positional path).
2686 #[arg(long, help_heading = headings::VISUALIZATION_INPUT, display_order = 20)]
2687 pub path: Option<String>,
2688
2689 /// Diagram syntax format (mermaid, graphviz, d2).
2690 ///
2691 /// Specifies the diagram language/syntax to generate.
2692 /// Output will be plain text in the chosen format.
2693 #[arg(long, short = 'f', value_enum, default_value = "mermaid", help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 10)]
2694 pub format: DiagramFormatArg,
2695
2696 /// Layout direction for the graph.
2697 #[arg(long, value_enum, default_value = "top-down", help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 20)]
2698 pub direction: DirectionArg,
2699
2700 /// File path to save the output (stdout when omitted).
2701 #[arg(long, help_heading = headings::DIAGRAM_CONFIGURATION, display_order = 30)]
2702 pub output_file: Option<PathBuf>,
2703
2704 /// Maximum traversal depth for graph expansion.
2705 #[arg(long, short = 'd', default_value_t = 3, help_heading = headings::TRAVERSAL_CONTROL, display_order = 10)]
2706 pub depth: usize,
2707
2708 /// Maximum number of nodes to include in the diagram (1-500).
2709 #[arg(long, default_value_t = 100, help_heading = headings::TRAVERSAL_CONTROL, display_order = 20)]
2710 pub max_nodes: usize,
2711}
2712
2713/// Supported diagram text formats.
2714#[derive(Debug, Clone, Copy, ValueEnum)]
2715pub enum DiagramFormatArg {
2716 Mermaid,
2717 Graphviz,
2718 D2,
2719}
2720
2721/// Diagram layout direction.
2722#[derive(Debug, Clone, Copy, ValueEnum)]
2723#[value(rename_all = "kebab-case")]
2724pub enum DirectionArg {
2725 TopDown,
2726 BottomUp,
2727 LeftRight,
2728 RightLeft,
2729}
2730
2731/// Workspace management subcommands
2732#[derive(Subcommand, Debug, Clone)]
2733pub enum WorkspaceCommand {
2734 /// Initialise a new workspace registry
2735 Init {
2736 /// Directory that will contain the workspace registry.
2737 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2738 workspace: String,
2739
2740 /// Preferred discovery mode for initial scans.
2741 #[arg(long, value_enum, default_value_t = WorkspaceDiscoveryMode::IndexFiles, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2742 mode: WorkspaceDiscoveryMode,
2743
2744 /// Friendly workspace name stored in the registry metadata.
2745 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 20)]
2746 name: Option<String>,
2747 },
2748
2749 /// Scan for repositories inside the workspace root
2750 Scan {
2751 /// Workspace root containing the .sqry-workspace file.
2752 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2753 workspace: String,
2754
2755 /// Discovery mode to use when scanning for repositories.
2756 #[arg(long, value_enum, default_value_t = WorkspaceDiscoveryMode::IndexFiles, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2757 mode: WorkspaceDiscoveryMode,
2758
2759 /// Remove entries whose indexes are no longer present.
2760 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 20)]
2761 prune_stale: bool,
2762 },
2763
2764 /// Add a repository to the workspace manually
2765 Add {
2766 /// Workspace root containing the .sqry-workspace file.
2767 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2768 workspace: String,
2769
2770 /// Path to the repository root (must contain .sqry-index).
2771 #[arg(value_name = "REPO", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2772 repo: String,
2773
2774 /// Optional friendly name for the repository.
2775 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2776 name: Option<String>,
2777 },
2778
2779 /// Remove a repository from the workspace
2780 Remove {
2781 /// Workspace root containing the .sqry-workspace file.
2782 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2783 workspace: String,
2784
2785 /// Repository identifier (workspace-relative path).
2786 #[arg(value_name = "REPO_ID", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2787 repo_id: String,
2788 },
2789
2790 /// Run a workspace-level query across registered repositories
2791 Query {
2792 /// Workspace root containing the .sqry-workspace file.
2793 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2794 workspace: String,
2795
2796 /// Query expression (supports repo: predicates).
2797 #[arg(value_name = "QUERY", help_heading = headings::WORKSPACE_INPUT, display_order = 20)]
2798 query: String,
2799
2800 /// Override parallel query threads.
2801 #[arg(long, help_heading = headings::PERFORMANCE_TUNING, display_order = 10)]
2802 threads: Option<usize>,
2803 },
2804
2805 /// Emit aggregate statistics for the workspace
2806 Stats {
2807 /// Workspace root containing the .sqry-workspace file.
2808 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2809 workspace: String,
2810 },
2811
2812 /// Print the aggregate index status for every source root in the workspace
2813 Status {
2814 /// Workspace root containing the .sqry-workspace file.
2815 #[arg(value_name = "WORKSPACE", help_heading = headings::WORKSPACE_INPUT, display_order = 10)]
2816 workspace: String,
2817
2818 /// Emit machine-readable JSON instead of the human-friendly summary.
2819 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 10)]
2820 json: bool,
2821
2822 /// Bypass the 60-second aggregate-status cache and force a recompute.
2823 #[arg(long, help_heading = headings::WORKSPACE_CONFIGURATION, display_order = 20)]
2824 no_cache: bool,
2825 },
2826}
2827
2828/// CLI discovery modes converted to workspace `DiscoveryMode` values
2829#[derive(Clone, Copy, Debug, ValueEnum)]
2830pub enum WorkspaceDiscoveryMode {
2831 #[value(name = "index-files", alias = "index")]
2832 IndexFiles,
2833 #[value(name = "git-roots", alias = "git")]
2834 GitRoots,
2835}
2836
2837/// Alias management subcommands
2838#[derive(Subcommand, Debug, Clone)]
2839pub enum AliasAction {
2840 /// List all saved aliases
2841 ///
2842 /// Shows aliases from both global (~/.config/sqry/) and local (.sqry-index.user)
2843 /// storage. Local aliases take precedence over global ones with the same name.
2844 ///
2845 /// Examples:
2846 /// sqry alias list # List all aliases
2847 /// sqry alias list --local # Only local aliases
2848 /// sqry alias list --global # Only global aliases
2849 /// sqry alias list --json # JSON output
2850 #[command(verbatim_doc_comment)]
2851 List {
2852 /// Show only local aliases (project-specific).
2853 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2854 local: bool,
2855
2856 /// Show only global aliases (cross-project).
2857 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2858 global: bool,
2859 },
2860
2861 /// Show details of a specific alias
2862 ///
2863 /// Displays the command, arguments, description, and storage location
2864 /// for the named alias.
2865 ///
2866 /// Example: sqry alias show my-funcs
2867 Show {
2868 /// Name of the alias to show.
2869 #[arg(value_name = "NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2870 name: String,
2871 },
2872
2873 /// Delete a saved alias
2874 ///
2875 /// Removes an alias from storage. If the alias exists in both local
2876 /// and global storage, specify --local or --global to delete from
2877 /// a specific location.
2878 ///
2879 /// Examples:
2880 /// sqry alias delete my-funcs # Delete (prefers local)
2881 /// sqry alias delete my-funcs --global # Delete from global only
2882 /// sqry alias delete my-funcs --force # Skip confirmation
2883 #[command(verbatim_doc_comment)]
2884 Delete {
2885 /// Name of the alias to delete.
2886 #[arg(value_name = "NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2887 name: String,
2888
2889 /// Delete from local storage only.
2890 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2891 local: bool,
2892
2893 /// Delete from global storage only.
2894 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2895 global: bool,
2896
2897 /// Skip confirmation prompt.
2898 #[arg(long, short = 'f', help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2899 force: bool,
2900 },
2901
2902 /// Rename an existing alias
2903 ///
2904 /// Changes the name of an alias while preserving its command and arguments.
2905 /// The alias is renamed in the same storage location where it was found.
2906 ///
2907 /// Example: sqry alias rename old-name new-name
2908 Rename {
2909 /// Current name of the alias.
2910 #[arg(value_name = "OLD_NAME", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2911 old_name: String,
2912
2913 /// New name for the alias.
2914 #[arg(value_name = "NEW_NAME", help_heading = headings::ALIAS_INPUT, display_order = 20)]
2915 new_name: String,
2916
2917 /// Rename in local storage only.
2918 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2919 local: bool,
2920
2921 /// Rename in global storage only.
2922 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2923 global: bool,
2924 },
2925
2926 /// Export aliases to a JSON file
2927 ///
2928 /// Exports aliases for backup or sharing. The export format is compatible
2929 /// with the import command for easy restoration.
2930 ///
2931 /// Examples:
2932 /// sqry alias export aliases.json # Export all
2933 /// sqry alias export aliases.json --local # Export local only
2934 #[command(verbatim_doc_comment)]
2935 Export {
2936 /// Output file path (use - for stdout).
2937 #[arg(value_name = "FILE", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2938 file: String,
2939
2940 /// Export only local aliases.
2941 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2942 local: bool,
2943
2944 /// Export only global aliases.
2945 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2946 global: bool,
2947 },
2948
2949 /// Import aliases from a JSON file
2950 ///
2951 /// Imports aliases from an export file. Handles conflicts with existing
2952 /// aliases using the specified strategy.
2953 ///
2954 /// Examples:
2955 /// sqry alias import aliases.json # Import to local
2956 /// sqry alias import aliases.json --global # Import to global
2957 /// sqry alias import aliases.json --on-conflict skip
2958 #[command(verbatim_doc_comment)]
2959 Import {
2960 /// Input file path (use - for stdin).
2961 #[arg(value_name = "FILE", help_heading = headings::ALIAS_INPUT, display_order = 10)]
2962 file: String,
2963
2964 /// Import to local storage (default).
2965 #[arg(long, conflicts_with = "global", help_heading = headings::ALIAS_CONFIGURATION, display_order = 10)]
2966 local: bool,
2967
2968 /// Import to global storage.
2969 #[arg(long, conflicts_with = "local", help_heading = headings::ALIAS_CONFIGURATION, display_order = 20)]
2970 global: bool,
2971
2972 /// How to handle conflicts with existing aliases.
2973 #[arg(long, value_enum, default_value = "error", help_heading = headings::ALIAS_CONFIGURATION, display_order = 30)]
2974 on_conflict: ImportConflictArg,
2975
2976 /// Preview import without making changes.
2977 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
2978 dry_run: bool,
2979 },
2980}
2981
2982/// History management subcommands
2983#[derive(Subcommand, Debug, Clone)]
2984pub enum HistoryAction {
2985 /// List recent query history
2986 ///
2987 /// Shows recently executed queries with their timestamps, commands,
2988 /// and execution status.
2989 ///
2990 /// Examples:
2991 /// sqry history list # List recent (default 100)
2992 /// sqry history list --limit 50 # Last 50 entries
2993 /// sqry history list --json # JSON output
2994 #[command(verbatim_doc_comment)]
2995 List {
2996 /// Maximum number of entries to show.
2997 #[arg(long, short = 'n', default_value = "100", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
2998 limit: usize,
2999 },
3000
3001 /// Search query history
3002 ///
3003 /// Searches history entries by pattern. The pattern is matched
3004 /// against command names and arguments.
3005 ///
3006 /// Examples:
3007 /// sqry history search "function" # Find queries with "function"
3008 /// sqry history search "callers:" # Find caller queries
3009 #[command(verbatim_doc_comment)]
3010 Search {
3011 /// Search pattern (matched against command and args).
3012 #[arg(value_name = "PATTERN", help_heading = headings::HISTORY_INPUT, display_order = 10)]
3013 pattern: String,
3014
3015 /// Maximum number of results.
3016 #[arg(long, short = 'n', default_value = "100", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
3017 limit: usize,
3018 },
3019
3020 /// Clear query history
3021 ///
3022 /// Removes history entries. Can clear all entries or only those
3023 /// older than a specified duration.
3024 ///
3025 /// Examples:
3026 /// sqry history clear # Clear all (requires --confirm)
3027 /// sqry history clear --older 30d # Clear entries older than 30 days
3028 /// sqry history clear --older 1w # Clear entries older than 1 week
3029 #[command(verbatim_doc_comment)]
3030 Clear {
3031 /// Remove only entries older than this duration (e.g., 30d, 1w, 24h).
3032 #[arg(long, value_name = "DURATION", help_heading = headings::HISTORY_CONFIGURATION, display_order = 10)]
3033 older: Option<String>,
3034
3035 /// Confirm clearing history (required when clearing all).
3036 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
3037 confirm: bool,
3038 },
3039
3040 /// Show history statistics
3041 ///
3042 /// Displays aggregate statistics about query history including
3043 /// total entries, most used commands, and storage information.
3044 Stats,
3045}
3046
3047/// Insights management subcommands
3048#[derive(Subcommand, Debug, Clone)]
3049pub enum InsightsAction {
3050 /// Show usage summary for a time period
3051 ///
3052 /// Displays aggregated usage statistics including query counts,
3053 /// timing metrics, and workflow patterns.
3054 ///
3055 /// Examples:
3056 /// sqry insights show # Current week
3057 /// sqry insights show --week 2025-W50 # Specific week
3058 /// sqry insights show --json # JSON output
3059 #[command(verbatim_doc_comment)]
3060 Show {
3061 /// ISO week to display (e.g., 2025-W50). Defaults to current week.
3062 #[arg(long, short = 'w', value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
3063 week: Option<String>,
3064 },
3065
3066 /// Show or modify uses configuration
3067 ///
3068 /// View the current configuration or change settings like
3069 /// enabling/disabling uses capture.
3070 ///
3071 /// Examples:
3072 /// sqry insights config # Show current config
3073 /// sqry insights config --enable # Enable uses capture
3074 /// sqry insights config --disable # Disable uses capture
3075 /// sqry insights config --retention 90 # Set retention to 90 days
3076 #[command(verbatim_doc_comment)]
3077 Config {
3078 /// Enable uses capture.
3079 #[arg(long, conflicts_with = "disable", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
3080 enable: bool,
3081
3082 /// Disable uses capture.
3083 #[arg(long, conflicts_with = "enable", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 20)]
3084 disable: bool,
3085
3086 /// Set retention period in days.
3087 #[arg(long, value_name = "DAYS", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 30)]
3088 retention: Option<u32>,
3089 },
3090
3091 /// Show storage status and statistics
3092 ///
3093 /// Displays information about the uses storage including
3094 /// total size, file count, and date range of stored events.
3095 ///
3096 /// Example:
3097 /// sqry insights status
3098 Status,
3099
3100 /// Clean up old event data
3101 ///
3102 /// Removes event logs older than the specified duration.
3103 /// Uses the configured retention period if --older is not specified.
3104 ///
3105 /// Examples:
3106 /// sqry insights prune # Use configured retention
3107 /// sqry insights prune --older 90d # Prune older than 90 days
3108 /// sqry insights prune --dry-run # Preview without deleting
3109 #[command(verbatim_doc_comment)]
3110 Prune {
3111 /// Remove entries older than this duration (e.g., 30d, 90d).
3112 /// Defaults to configured retention period.
3113 #[arg(long, value_name = "DURATION", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10)]
3114 older: Option<String>,
3115
3116 /// Preview deletions without removing files.
3117 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 10)]
3118 dry_run: bool,
3119 },
3120
3121 /// Generate an anonymous usage snapshot for sharing
3122 ///
3123 /// Creates a privacy-safe snapshot of your usage patterns that you can
3124 /// share with the sqry community or attach to bug reports. All fields
3125 /// are strongly-typed enums and numerics — no code content, paths, or
3126 /// identifiers are ever included.
3127 ///
3128 /// Uses are disabled → exits 1. Empty weeks produce a valid snapshot
3129 /// with total_uses: 0 (not an error).
3130 ///
3131 /// JSON output is controlled by the global --json flag.
3132 ///
3133 /// Examples:
3134 /// sqry insights share # Current week, human-readable
3135 /// sqry --json insights share # JSON to stdout
3136 /// sqry insights share --output snap.json # Write JSON to file
3137 /// sqry insights share --week 2026-W09 # Specific week
3138 /// sqry insights share --from 2026-W07 --to 2026-W09 # Merge 3 weeks
3139 /// sqry insights share --dry-run # Preview without writing
3140 #[cfg(feature = "share")]
3141 #[command(verbatim_doc_comment)]
3142 Share {
3143 /// Specific ISO week to share (e.g., 2026-W09). Defaults to current week.
3144 /// Conflicts with --from / --to.
3145 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 10,
3146 conflicts_with_all = ["from", "to"])]
3147 week: Option<String>,
3148
3149 /// Start of multi-week range (e.g., 2026-W07). Requires --to.
3150 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 11,
3151 conflicts_with = "week", requires = "to")]
3152 from: Option<String>,
3153
3154 /// End of multi-week range (e.g., 2026-W09). Requires --from.
3155 #[arg(long, value_name = "WEEK", help_heading = headings::INSIGHTS_CONFIGURATION, display_order = 12,
3156 conflicts_with = "week", requires = "from")]
3157 to: Option<String>,
3158
3159 /// Write JSON snapshot to this file.
3160 #[arg(long, short = 'o', value_name = "FILE", help_heading = headings::INSIGHTS_OUTPUT, display_order = 20,
3161 conflicts_with = "dry_run")]
3162 output: Option<PathBuf>,
3163
3164 /// Preview what would be shared without writing a file.
3165 #[arg(long, help_heading = headings::SAFETY_CONTROL, display_order = 30,
3166 conflicts_with = "output")]
3167 dry_run: bool,
3168 },
3169}
3170
3171/// Import conflict resolution strategies
3172#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
3173#[value(rename_all = "lowercase")]
3174pub enum ImportConflictArg {
3175 /// Fail on any conflict (default)
3176 Error,
3177 /// Skip conflicting aliases
3178 Skip,
3179 /// Overwrite existing aliases
3180 Overwrite,
3181}
3182
3183/// Shell types for completions
3184#[derive(Debug, Clone, Copy, ValueEnum)]
3185#[allow(missing_docs)]
3186#[allow(clippy::enum_variant_names)]
3187pub enum Shell {
3188 Bash,
3189 Zsh,
3190 Fish,
3191 PowerShell,
3192 Elvish,
3193}
3194
3195/// Symbol types for filtering
3196#[derive(Debug, Clone, Copy, ValueEnum)]
3197#[allow(missing_docs)]
3198pub enum SymbolKind {
3199 Function,
3200 Class,
3201 Method,
3202 Struct,
3203 Enum,
3204 Interface,
3205 Trait,
3206 Variable,
3207 Constant,
3208 Type,
3209 Module,
3210 Namespace,
3211}
3212
3213impl std::fmt::Display for SymbolKind {
3214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3215 match self {
3216 SymbolKind::Function => write!(f, "function"),
3217 SymbolKind::Class => write!(f, "class"),
3218 SymbolKind::Method => write!(f, "method"),
3219 SymbolKind::Struct => write!(f, "struct"),
3220 SymbolKind::Enum => write!(f, "enum"),
3221 SymbolKind::Interface => write!(f, "interface"),
3222 SymbolKind::Trait => write!(f, "trait"),
3223 SymbolKind::Variable => write!(f, "variable"),
3224 SymbolKind::Constant => write!(f, "constant"),
3225 SymbolKind::Type => write!(f, "type"),
3226 SymbolKind::Module => write!(f, "module"),
3227 SymbolKind::Namespace => write!(f, "namespace"),
3228 }
3229 }
3230}
3231
3232/// Index validation strictness modes
3233#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
3234#[value(rename_all = "lowercase")]
3235pub enum ValidationMode {
3236 /// Skip validation entirely (fastest)
3237 Off,
3238 /// Log warnings but continue (default)
3239 Warn,
3240 /// Abort on validation errors
3241 Fail,
3242}
3243
3244/// Metrics export format for validation status
3245#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
3246#[value(rename_all = "lower")]
3247pub enum MetricsFormat {
3248 /// JSON format (default, structured data)
3249 #[value(alias = "jsn")]
3250 Json,
3251 /// Prometheus `OpenMetrics` text format
3252 #[value(alias = "prom")]
3253 Prometheus,
3254}
3255
3256/// Classpath analysis depth for the `--classpath-depth` flag.
3257#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
3258#[value(rename_all = "lower")]
3259pub enum ClasspathDepthArg {
3260 /// Include all transitive dependencies.
3261 Full,
3262 /// Only direct (compile-scope) dependencies.
3263 Shallow,
3264}
3265
3266// Helper function to get the command with applied taxonomy
3267impl Cli {
3268 /// Get the command with taxonomy headings applied
3269 #[must_use]
3270 pub fn command_with_taxonomy() -> clap::Command {
3271 use clap::CommandFactory;
3272 let cmd = Self::command();
3273 headings::apply_root_layout(cmd)
3274 }
3275
3276 /// Validate CLI arguments that have dependencies not enforceable via clap
3277 ///
3278 /// Returns an error message if validation fails, None if valid.
3279 #[must_use]
3280 pub fn validate(&self) -> Option<&'static str> {
3281 let tabular_mode = self.csv || self.tsv;
3282
3283 // --headers, --columns, and --raw-csv require CSV or TSV mode
3284 if self.headers && !tabular_mode {
3285 return Some("--headers requires --csv or --tsv");
3286 }
3287 if self.columns.is_some() && !tabular_mode {
3288 return Some("--columns requires --csv or --tsv");
3289 }
3290 if self.raw_csv && !tabular_mode {
3291 return Some("--raw-csv requires --csv or --tsv");
3292 }
3293
3294 if tabular_mode && let Err(msg) = output::parse_columns(self.columns.as_ref()) {
3295 return Some(Box::leak(msg.into_boxed_str()));
3296 }
3297
3298 None
3299 }
3300
3301 /// Get the search path, defaulting to current directory if not specified
3302 #[must_use]
3303 pub fn search_path(&self) -> &str {
3304 self.path.as_deref().unwrap_or(".")
3305 }
3306
3307 /// Resolve the path-scoped subcommand path, applying the global
3308 /// `--workspace` / `SQRY_WORKSPACE_FILE` fallback (`STEP_8`).
3309 ///
3310 /// Precedence (least-surprise, codified in
3311 /// `docs/development/workspace-aware-cross-repo/03_IMPLEMENTATION_PLAN.md`
3312 /// Step 8):
3313 /// 1. Explicit positional `<path>` on the subcommand wins.
3314 /// 2. The global `--workspace <PATH>` flag (or `SQRY_WORKSPACE_FILE`
3315 /// environment variable; CLI flag wins on conflict) is the fallback.
3316 /// 3. Otherwise, the top-level `cli.path` shorthand or `"."`.
3317 ///
3318 /// Callers pass `positional` from the subcommand's own positional argument.
3319 ///
3320 /// # Errors
3321 ///
3322 /// Returns an error if the workspace fallback (from `--workspace` or
3323 /// `SQRY_WORKSPACE_FILE`) is set but contains non-UTF-8 bytes. The
3324 /// downstream CLI pipeline (positional `<path>` arguments and
3325 /// `commands::run_index` / `commands::run_query` signatures) operates on
3326 /// `&str`, so a non-UTF-8 workspace path cannot be propagated faithfully —
3327 /// silently falling back to `"."` (or the top-level `cli.path`) would
3328 /// violate the documented precedence semantics. Surface the failure
3329 /// instead so the operator can supply a UTF-8 path. (STEP_8 codex iter1
3330 /// fix.)
3331 pub fn resolve_subcommand_path<'a>(
3332 &'a self,
3333 positional: Option<&'a str>,
3334 ) -> anyhow::Result<&'a str> {
3335 if let Some(p) = positional {
3336 return Ok(p);
3337 }
3338 if let Some(ws) = self.workspace.as_deref() {
3339 return ws.to_str().ok_or_else(|| {
3340 anyhow::anyhow!(
3341 "--workspace / SQRY_WORKSPACE_FILE path is not valid UTF-8: {}. \
3342 sqry's path-scoped subcommands require UTF-8 paths; supply a \
3343 valid UTF-8 workspace path or pass an explicit positional \
3344 argument.",
3345 ws.display()
3346 )
3347 });
3348 }
3349 Ok(self.search_path())
3350 }
3351
3352 /// Returns the workspace path supplied via `--workspace` /
3353 /// `SQRY_WORKSPACE_FILE`, if any (`STEP_8`).
3354 ///
3355 /// Surfaced for downstream consumers (LSP/MCP/test harnesses); the
3356 /// CLI binary itself currently routes through `resolve_subcommand_path`,
3357 /// so the binary build flags this as unused.
3358 #[allow(dead_code)]
3359 #[must_use]
3360 pub fn workspace_path(&self) -> Option<&std::path::Path> {
3361 self.workspace.as_deref()
3362 }
3363
3364 /// Return the plugin-selection arguments for the active subcommand.
3365 #[must_use]
3366 pub fn plugin_selection_args(&self) -> PluginSelectionArgs {
3367 match self.command.as_deref() {
3368 Some(
3369 Command::Query {
3370 plugin_selection, ..
3371 }
3372 | Command::Index {
3373 plugin_selection, ..
3374 }
3375 | Command::Update {
3376 plugin_selection, ..
3377 }
3378 | Command::Watch {
3379 plugin_selection, ..
3380 },
3381 ) => plugin_selection.clone(),
3382 _ => PluginSelectionArgs::default(),
3383 }
3384 }
3385
3386 /// Check if tabular output mode is enabled
3387 #[allow(dead_code)]
3388 #[must_use]
3389 pub fn is_tabular_output(&self) -> bool {
3390 self.csv || self.tsv
3391 }
3392
3393 /// Create pager configuration from CLI flags
3394 ///
3395 /// Returns `PagerConfig` based on `--pager`, `--no-pager`, and `--pager-cmd` flags.
3396 ///
3397 /// # Structured Output Handling
3398 ///
3399 /// For machine-readable formats (JSON, CSV, TSV), paging is disabled by default
3400 /// to avoid breaking pipelines. Use `--pager` to explicitly enable paging for
3401 /// these formats.
3402 #[must_use]
3403 pub fn pager_config(&self) -> crate::output::PagerConfig {
3404 // Structured output bypasses pager unless --pager is explicit
3405 let is_structured_output = self.json || self.csv || self.tsv;
3406 let effective_no_pager = self.no_pager || (is_structured_output && !self.pager);
3407
3408 crate::output::PagerConfig::from_cli_flags(
3409 self.pager,
3410 effective_no_pager,
3411 self.pager_cmd.as_deref(),
3412 )
3413 }
3414}
3415
3416#[cfg(test)]
3417mod tests {
3418 use super::*;
3419 use crate::large_stack_test;
3420
3421 /// Guard: keep the `Command` enum from silently ballooning.
3422 /// If this fails, consider extracting the largest variant into a Box<T>.
3423 #[test]
3424 fn test_command_enum_size() {
3425 let size = std::mem::size_of::<Command>();
3426 assert!(
3427 size <= 256,
3428 "Command enum is {size} bytes, should be <= 256"
3429 );
3430 }
3431
3432 large_stack_test! {
3433 #[test]
3434 fn test_cli_parse_basic_search() {
3435 let cli = Cli::parse_from(["sqry", "main"]);
3436 assert!(cli.command.is_none());
3437 assert_eq!(cli.pattern, Some("main".to_string()));
3438 assert_eq!(cli.path, None); // Defaults to None, use cli.search_path() to get "."
3439 assert_eq!(cli.search_path(), ".");
3440 }
3441 }
3442
3443 large_stack_test! {
3444 #[test]
3445 fn test_cli_parse_with_path() {
3446 let cli = Cli::parse_from(["sqry", "test", "src/"]);
3447 assert_eq!(cli.pattern, Some("test".to_string()));
3448 assert_eq!(cli.path, Some("src/".to_string()));
3449 assert_eq!(cli.search_path(), "src/");
3450 }
3451 }
3452
3453 large_stack_test! {
3454 #[test]
3455 fn test_cli_parse_search_subcommand() {
3456 let cli = Cli::parse_from(["sqry", "search", "main"]);
3457 assert!(matches!(cli.command.as_deref(), Some(Command::Search { .. })));
3458 }
3459 }
3460
3461 large_stack_test! {
3462 #[test]
3463 fn test_cli_parse_query_subcommand() {
3464 let cli = Cli::parse_from(["sqry", "query", "kind:function"]);
3465 assert!(matches!(cli.command.as_deref(), Some(Command::Query { .. })));
3466 }
3467 }
3468
3469 large_stack_test! {
3470 #[test]
3471 fn test_cli_flags() {
3472 let cli = Cli::parse_from(["sqry", "main", "--json", "--no-color", "--ignore-case"]);
3473 assert!(cli.json);
3474 assert!(cli.no_color);
3475 assert!(cli.ignore_case);
3476 }
3477 }
3478
3479 large_stack_test! {
3480 #[test]
3481 fn test_validation_mode_default() {
3482 let cli = Cli::parse_from(["sqry", "index"]);
3483 assert_eq!(cli.validate, ValidationMode::Warn);
3484 assert!(!cli.auto_rebuild);
3485 }
3486 }
3487
3488 large_stack_test! {
3489 #[test]
3490 fn test_validation_mode_flags() {
3491 let cli = Cli::parse_from(["sqry", "index", "--validate", "fail", "--auto-rebuild"]);
3492 assert_eq!(cli.validate, ValidationMode::Fail);
3493 assert!(cli.auto_rebuild);
3494 }
3495 }
3496
3497 large_stack_test! {
3498 #[test]
3499 fn test_plugin_selection_flags_parse() {
3500 let cli = Cli::parse_from([
3501 "sqry",
3502 "index",
3503 "--include-high-cost",
3504 "--enable-plugin",
3505 "json",
3506 "--disable-plugin",
3507 "rust",
3508 ]);
3509 let plugin_selection = cli.plugin_selection_args();
3510 assert!(plugin_selection.include_high_cost);
3511 assert_eq!(plugin_selection.enable_plugins, vec!["json".to_string()]);
3512 assert_eq!(plugin_selection.disable_plugins, vec!["rust".to_string()]);
3513 }
3514 }
3515
3516 large_stack_test! {
3517 #[test]
3518 fn test_plugin_selection_language_aliases_parse() {
3519 let cli = Cli::parse_from([
3520 "sqry",
3521 "index",
3522 "--enable-language",
3523 "json",
3524 "--disable-language",
3525 "rust",
3526 ]);
3527 let plugin_selection = cli.plugin_selection_args();
3528 assert_eq!(plugin_selection.enable_plugins, vec!["json".to_string()]);
3529 assert_eq!(plugin_selection.disable_plugins, vec!["rust".to_string()]);
3530 }
3531 }
3532
3533 large_stack_test! {
3534 #[test]
3535 fn test_validate_rejects_invalid_columns() {
3536 let cli = Cli::parse_from([
3537 "sqry",
3538 "--csv",
3539 "--columns",
3540 "name,unknown",
3541 "query",
3542 "path",
3543 ]);
3544 let msg = cli.validate().expect("validation should fail");
3545 assert!(msg.contains("Unknown column"), "Unexpected message: {msg}");
3546 }
3547 }
3548
3549 large_stack_test! {
3550 #[test]
3551 fn test_index_rebuild_alias_sets_force() {
3552 // Verify --rebuild is an alias for --force
3553 let cli = Cli::parse_from(["sqry", "index", "--rebuild", "."]);
3554 if let Some(Command::Index { force, .. }) = cli.command.as_deref() {
3555 assert!(force, "--rebuild should set force=true");
3556 } else {
3557 panic!("Expected Index command");
3558 }
3559 }
3560 }
3561
3562 large_stack_test! {
3563 #[test]
3564 fn test_index_force_still_works() {
3565 // Ensure --force continues to work (backward compat)
3566 let cli = Cli::parse_from(["sqry", "index", "--force", "."]);
3567 if let Some(Command::Index { force, .. }) = cli.command.as_deref() {
3568 assert!(force, "--force should set force=true");
3569 } else {
3570 panic!("Expected Index command");
3571 }
3572 }
3573 }
3574
3575 large_stack_test! {
3576 #[test]
3577 fn test_graph_deps_alias() {
3578 // Verify "deps" is an alias for dependency-tree
3579 let cli = Cli::parse_from(["sqry", "graph", "deps", "main"]);
3580 assert!(matches!(
3581 cli.command.as_deref(),
3582 Some(Command::Graph {
3583 operation: GraphOperation::DependencyTree { .. },
3584 ..
3585 })
3586 ));
3587 }
3588 }
3589
3590 large_stack_test! {
3591 #[test]
3592 fn test_graph_cyc_alias() {
3593 let cli = Cli::parse_from(["sqry", "graph", "cyc"]);
3594 assert!(matches!(
3595 cli.command.as_deref(),
3596 Some(Command::Graph {
3597 operation: GraphOperation::Cycles { .. },
3598 ..
3599 })
3600 ));
3601 }
3602 }
3603
3604 large_stack_test! {
3605 #[test]
3606 fn test_graph_cx_alias() {
3607 let cli = Cli::parse_from(["sqry", "graph", "cx"]);
3608 assert!(matches!(
3609 cli.command.as_deref(),
3610 Some(Command::Graph {
3611 operation: GraphOperation::Complexity { .. },
3612 ..
3613 })
3614 ));
3615 }
3616 }
3617
3618 large_stack_test! {
3619 #[test]
3620 fn test_graph_nodes_args() {
3621 let cli = Cli::parse_from([
3622 "sqry",
3623 "graph",
3624 "nodes",
3625 "--kind",
3626 "function",
3627 "--languages",
3628 "rust",
3629 "--file",
3630 "src/",
3631 "--name",
3632 "main",
3633 "--qualified-name",
3634 "crate::main",
3635 "--limit",
3636 "5",
3637 "--offset",
3638 "2",
3639 "--full-paths",
3640 ]);
3641 if let Some(Command::Graph {
3642 operation:
3643 GraphOperation::Nodes {
3644 kind,
3645 languages,
3646 file,
3647 name,
3648 qualified_name,
3649 limit,
3650 offset,
3651 full_paths,
3652 },
3653 ..
3654 }) = cli.command.as_deref()
3655 {
3656 assert_eq!(kind, &Some("function".to_string()));
3657 assert_eq!(languages, &Some("rust".to_string()));
3658 assert_eq!(file, &Some("src/".to_string()));
3659 assert_eq!(name, &Some("main".to_string()));
3660 assert_eq!(qualified_name, &Some("crate::main".to_string()));
3661 assert_eq!(*limit, 5);
3662 assert_eq!(*offset, 2);
3663 assert!(full_paths);
3664 } else {
3665 panic!("Expected Graph Nodes command");
3666 }
3667 }
3668 }
3669
3670 large_stack_test! {
3671 #[test]
3672 fn test_graph_edges_args() {
3673 let cli = Cli::parse_from([
3674 "sqry",
3675 "graph",
3676 "edges",
3677 "--kind",
3678 "calls",
3679 "--from",
3680 "main",
3681 "--to",
3682 "worker",
3683 "--from-lang",
3684 "rust",
3685 "--to-lang",
3686 "python",
3687 "--file",
3688 "src/main.rs",
3689 "--limit",
3690 "10",
3691 "--offset",
3692 "1",
3693 "--full-paths",
3694 ]);
3695 if let Some(Command::Graph {
3696 operation:
3697 GraphOperation::Edges {
3698 kind,
3699 from,
3700 to,
3701 from_lang,
3702 to_lang,
3703 file,
3704 limit,
3705 offset,
3706 full_paths,
3707 },
3708 ..
3709 }) = cli.command.as_deref()
3710 {
3711 assert_eq!(kind, &Some("calls".to_string()));
3712 assert_eq!(from, &Some("main".to_string()));
3713 assert_eq!(to, &Some("worker".to_string()));
3714 assert_eq!(from_lang, &Some("rust".to_string()));
3715 assert_eq!(to_lang, &Some("python".to_string()));
3716 assert_eq!(file, &Some("src/main.rs".to_string()));
3717 assert_eq!(*limit, 10);
3718 assert_eq!(*offset, 1);
3719 assert!(full_paths);
3720 } else {
3721 panic!("Expected Graph Edges command");
3722 }
3723 }
3724 }
3725
3726 // ===== Pager Tests (P2-29) =====
3727
3728 large_stack_test! {
3729 #[test]
3730 fn test_pager_flag_default() {
3731 let cli = Cli::parse_from(["sqry", "query", "kind:function"]);
3732 assert!(!cli.pager);
3733 assert!(!cli.no_pager);
3734 assert!(cli.pager_cmd.is_none());
3735 }
3736 }
3737
3738 large_stack_test! {
3739 #[test]
3740 fn test_pager_flag() {
3741 let cli = Cli::parse_from(["sqry", "--pager", "query", "kind:function"]);
3742 assert!(cli.pager);
3743 assert!(!cli.no_pager);
3744 }
3745 }
3746
3747 large_stack_test! {
3748 #[test]
3749 fn test_no_pager_flag() {
3750 let cli = Cli::parse_from(["sqry", "--no-pager", "query", "kind:function"]);
3751 assert!(!cli.pager);
3752 assert!(cli.no_pager);
3753 }
3754 }
3755
3756 large_stack_test! {
3757 #[test]
3758 fn test_pager_cmd_flag() {
3759 let cli = Cli::parse_from([
3760 "sqry",
3761 "--pager-cmd",
3762 "bat --style=plain",
3763 "query",
3764 "kind:function",
3765 ]);
3766 assert_eq!(cli.pager_cmd, Some("bat --style=plain".to_string()));
3767 }
3768 }
3769
3770 large_stack_test! {
3771 #[test]
3772 fn test_pager_and_no_pager_conflict() {
3773 // These flags conflict and clap should reject
3774 let result =
3775 Cli::try_parse_from(["sqry", "--pager", "--no-pager", "query", "kind:function"]);
3776 assert!(result.is_err());
3777 }
3778 }
3779
3780 large_stack_test! {
3781 #[test]
3782 fn test_pager_flags_global() {
3783 // Pager flags work with any subcommand
3784 let cli = Cli::parse_from(["sqry", "--no-pager", "search", "test"]);
3785 assert!(cli.no_pager);
3786
3787 let cli = Cli::parse_from(["sqry", "--pager", "index"]);
3788 assert!(cli.pager);
3789 }
3790 }
3791
3792 large_stack_test! {
3793 #[test]
3794 fn test_pager_config_json_bypasses_pager() {
3795 use crate::output::pager::PagerMode;
3796
3797 // JSON output should bypass pager by default
3798 let cli = Cli::parse_from(["sqry", "--json", "search", "test"]);
3799 let config = cli.pager_config();
3800 assert_eq!(config.enabled, PagerMode::Never);
3801 }
3802 }
3803
3804 large_stack_test! {
3805 #[test]
3806 fn test_pager_config_csv_bypasses_pager() {
3807 use crate::output::pager::PagerMode;
3808
3809 // CSV output should bypass pager by default
3810 let cli = Cli::parse_from(["sqry", "--csv", "search", "test"]);
3811 let config = cli.pager_config();
3812 assert_eq!(config.enabled, PagerMode::Never);
3813 }
3814 }
3815
3816 large_stack_test! {
3817 #[test]
3818 fn test_pager_config_tsv_bypasses_pager() {
3819 use crate::output::pager::PagerMode;
3820
3821 // TSV output should bypass pager by default
3822 let cli = Cli::parse_from(["sqry", "--tsv", "search", "test"]);
3823 let config = cli.pager_config();
3824 assert_eq!(config.enabled, PagerMode::Never);
3825 }
3826 }
3827
3828 large_stack_test! {
3829 #[test]
3830 fn test_pager_config_json_with_explicit_pager() {
3831 use crate::output::pager::PagerMode;
3832
3833 // JSON with explicit --pager should enable pager
3834 let cli = Cli::parse_from(["sqry", "--json", "--pager", "search", "test"]);
3835 let config = cli.pager_config();
3836 assert_eq!(config.enabled, PagerMode::Always);
3837 }
3838 }
3839
3840 large_stack_test! {
3841 #[test]
3842 fn test_pager_config_text_output_auto() {
3843 use crate::output::pager::PagerMode;
3844
3845 // Text output (default) should use auto pager mode
3846 let cli = Cli::parse_from(["sqry", "search", "test"]);
3847 let config = cli.pager_config();
3848 assert_eq!(config.enabled, PagerMode::Auto);
3849 }
3850 }
3851
3852 // ===== Macro boundary CLI tests =====
3853
3854 large_stack_test! {
3855 #[test]
3856 fn test_cache_expand_args_parsing() {
3857 let cli = Cli::parse_from([
3858 "sqry", "cache", "expand",
3859 "--refresh",
3860 "--crate-name", "my_crate",
3861 "--dry-run",
3862 "--output", "/tmp/expand-out",
3863 ]);
3864 if let Some(Command::Cache { action }) = cli.command.as_deref() {
3865 match action {
3866 CacheAction::Expand {
3867 refresh,
3868 crate_name,
3869 dry_run,
3870 output,
3871 } => {
3872 assert!(refresh);
3873 assert_eq!(crate_name.as_deref(), Some("my_crate"));
3874 assert!(dry_run);
3875 assert_eq!(output.as_deref(), Some(std::path::Path::new("/tmp/expand-out")));
3876 }
3877 _ => panic!("Expected CacheAction::Expand"),
3878 }
3879 } else {
3880 panic!("Expected Cache command");
3881 }
3882 }
3883 }
3884
3885 large_stack_test! {
3886 #[test]
3887 fn test_cache_expand_defaults() {
3888 let cli = Cli::parse_from(["sqry", "cache", "expand"]);
3889 if let Some(Command::Cache { action }) = cli.command.as_deref() {
3890 match action {
3891 CacheAction::Expand {
3892 refresh,
3893 crate_name,
3894 dry_run,
3895 output,
3896 } => {
3897 assert!(!refresh);
3898 assert!(crate_name.is_none());
3899 assert!(!dry_run);
3900 assert!(output.is_none());
3901 }
3902 _ => panic!("Expected CacheAction::Expand"),
3903 }
3904 } else {
3905 panic!("Expected Cache command");
3906 }
3907 }
3908 }
3909
3910 large_stack_test! {
3911 #[test]
3912 fn test_index_macro_flags_parsing() {
3913 let cli = Cli::parse_from([
3914 "sqry", "index",
3915 "--enable-macro-expansion",
3916 "--cfg", "test",
3917 "--cfg", "unix",
3918 "--expand-cache", "/tmp/expand",
3919 ]);
3920 if let Some(Command::Index {
3921 enable_macro_expansion,
3922 cfg_flags,
3923 expand_cache,
3924 ..
3925 }) = cli.command.as_deref()
3926 {
3927 assert!(enable_macro_expansion);
3928 assert_eq!(cfg_flags, &["test".to_string(), "unix".to_string()]);
3929 assert_eq!(expand_cache.as_deref(), Some(std::path::Path::new("/tmp/expand")));
3930 } else {
3931 panic!("Expected Index command");
3932 }
3933 }
3934 }
3935
3936 large_stack_test! {
3937 #[test]
3938 fn test_index_macro_flags_defaults() {
3939 let cli = Cli::parse_from(["sqry", "index"]);
3940 if let Some(Command::Index {
3941 enable_macro_expansion,
3942 cfg_flags,
3943 expand_cache,
3944 ..
3945 }) = cli.command.as_deref()
3946 {
3947 assert!(!enable_macro_expansion);
3948 assert!(cfg_flags.is_empty());
3949 assert!(expand_cache.is_none());
3950 } else {
3951 panic!("Expected Index command");
3952 }
3953 }
3954 }
3955
3956 large_stack_test! {
3957 #[test]
3958 fn test_search_macro_flags_parsing() {
3959 let cli = Cli::parse_from([
3960 "sqry", "search", "test_fn",
3961 "--cfg-filter", "test",
3962 "--include-generated",
3963 "--macro-boundaries",
3964 ]);
3965 if let Some(Command::Search {
3966 cfg_filter,
3967 include_generated,
3968 macro_boundaries,
3969 ..
3970 }) = cli.command.as_deref()
3971 {
3972 assert_eq!(cfg_filter.as_deref(), Some("test"));
3973 assert!(include_generated);
3974 assert!(macro_boundaries);
3975 } else {
3976 panic!("Expected Search command");
3977 }
3978 }
3979 }
3980
3981 large_stack_test! {
3982 #[test]
3983 fn test_search_macro_flags_defaults() {
3984 let cli = Cli::parse_from(["sqry", "search", "test_fn"]);
3985 if let Some(Command::Search {
3986 cfg_filter,
3987 include_generated,
3988 macro_boundaries,
3989 ..
3990 }) = cli.command.as_deref()
3991 {
3992 assert!(cfg_filter.is_none());
3993 assert!(!include_generated);
3994 assert!(!macro_boundaries);
3995 } else {
3996 panic!("Expected Search command");
3997 }
3998 }
3999 }
4000
4001 // ===== Daemon subcommand CLI tests (Task 10 U2) =====
4002
4003 large_stack_test! {
4004 #[test]
4005 fn daemon_start_parses() {
4006 let cli = Cli::parse_from(["sqry", "daemon", "start"]);
4007 if let Some(Command::Daemon { action }) = cli.command.as_deref() {
4008 match action.as_ref() {
4009 DaemonAction::Start { sqryd_path, timeout } => {
4010 assert!(sqryd_path.is_none(), "sqryd_path should default to None");
4011 assert_eq!(*timeout, 10, "default timeout should be 10");
4012 }
4013 other => panic!("Expected DaemonAction::Start, got {:?}", other),
4014 }
4015 } else {
4016 panic!("Expected Command::Daemon");
4017 }
4018 }
4019 }
4020
4021 large_stack_test! {
4022 #[test]
4023 fn daemon_stop_parses() {
4024 let cli = Cli::parse_from(["sqry", "daemon", "stop", "--timeout", "30"]);
4025 if let Some(Command::Daemon { action }) = cli.command.as_deref() {
4026 match action.as_ref() {
4027 DaemonAction::Stop { timeout } => {
4028 assert_eq!(*timeout, 30, "timeout should be 30");
4029 }
4030 other => panic!("Expected DaemonAction::Stop, got {:?}", other),
4031 }
4032 } else {
4033 panic!("Expected Command::Daemon");
4034 }
4035 }
4036 }
4037
4038 large_stack_test! {
4039 #[test]
4040 fn daemon_status_json_parses() {
4041 let cli = Cli::parse_from(["sqry", "daemon", "status", "--json"]);
4042 if let Some(Command::Daemon { action }) = cli.command.as_deref() {
4043 match action.as_ref() {
4044 DaemonAction::Status { json } => {
4045 assert!(*json, "--json flag should be true");
4046 }
4047 other => panic!("Expected DaemonAction::Status, got {:?}", other),
4048 }
4049 } else {
4050 panic!("Expected Command::Daemon");
4051 }
4052 }
4053 }
4054
4055 large_stack_test! {
4056 #[test]
4057 fn daemon_logs_follow_parses() {
4058 let cli = Cli::parse_from(["sqry", "daemon", "logs", "--follow", "--lines", "100"]);
4059 if let Some(Command::Daemon { action }) = cli.command.as_deref() {
4060 match action.as_ref() {
4061 DaemonAction::Logs { lines, follow } => {
4062 assert_eq!(*lines, 100, "lines should be 100");
4063 assert!(*follow, "--follow flag should be true");
4064 }
4065 other => panic!("Expected DaemonAction::Logs, got {:?}", other),
4066 }
4067 } else {
4068 panic!("Expected Command::Daemon");
4069 }
4070 }
4071 }
4072
4073 large_stack_test! {
4074 #[test]
4075 fn daemon_load_parses() {
4076 let cli = Cli::parse_from(["sqry", "daemon", "load", "/some/workspace"]);
4077 if let Some(Command::Daemon { action }) = cli.command.as_deref() {
4078 match action.as_ref() {
4079 DaemonAction::Load { path } => {
4080 assert_eq!(
4081 path,
4082 &std::path::PathBuf::from("/some/workspace"),
4083 "path should be /some/workspace"
4084 );
4085 }
4086 other => panic!("Expected DaemonAction::Load, got {:?}", other),
4087 }
4088 } else {
4089 panic!("Expected Command::Daemon");
4090 }
4091 }
4092 }
4093}