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