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