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