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