Skip to main content

sqry_cli/args/
mod.rs

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