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