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