Skip to main content

sqry_cli/args/
mod.rs

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