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