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