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