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