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