Skip to main content

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}