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