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