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