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