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