Skip to main content

reflex/cli/
mod.rs

1//! CLI argument parsing and command router
2
3use anyhow::Result;
4use clap::{CommandFactory, Parser, Subcommand};
5use std::path::PathBuf;
6use crate::cache::CacheManager;
7
8mod ask;
9mod deps;
10mod index;
11mod llm;
12mod misc;
13mod pulse;
14mod query;
15mod serve;
16mod snapshot;
17mod watch;
18
19pub use self::query::truncate_preview;
20
21/// Reflex: Local-first, structure-aware code search for AI agents
22#[derive(Parser, Debug)]
23#[command(
24    name = "rfx",
25    version,
26    about = "A fast, deterministic code search engine built for AI",
27    long_about = "Reflex is a local-first, structure-aware code search engine that returns \
28                  structured results (symbols, spans, scopes) with sub-100ms latency. \
29                  Designed for AI coding agents and automation."
30)]
31pub struct Cli {
32    /// Enable verbose logging (can be repeated for more verbosity)
33    #[arg(short, long, action = clap::ArgAction::Count)]
34    pub verbose: u8,
35
36    #[command(subcommand)]
37    pub command: Option<Command>,
38}
39
40#[derive(Subcommand, Debug)]
41pub enum IndexSubcommand {
42    /// Show background symbol indexing status
43    Status,
44
45    /// Compact the cache by removing deleted files
46    ///
47    /// Removes files from the cache that no longer exist on disk and reclaims
48    /// disk space using SQLite VACUUM. This operation is also performed automatically
49    /// in the background every 24 hours during normal usage.
50    ///
51    /// Examples:
52    ///   rfx index compact                # Show compaction results
53    ///   rfx index compact --json         # JSON output
54    Compact {
55        /// Output format as JSON
56        #[arg(long)]
57        json: bool,
58
59        /// Pretty-print JSON output (only with --json)
60        #[arg(long)]
61        pretty: bool,
62    },
63}
64
65#[derive(Subcommand, Debug)]
66pub enum Command {
67    /// Build or update the local code index
68    Index {
69        /// Directory to index (defaults to current directory)
70        #[arg(value_name = "PATH", default_value = ".")]
71        path: PathBuf,
72
73        /// Force full rebuild (ignore incremental cache)
74        #[arg(short, long)]
75        force: bool,
76
77        /// Languages to include (empty = all)
78        #[arg(short, long, value_delimiter = ',')]
79        languages: Vec<String>,
80
81        /// Suppress all output (no progress bar, no summary)
82        #[arg(short, long)]
83        quiet: bool,
84
85        /// Subcommand (status, compact)
86        #[command(subcommand)]
87        command: Option<IndexSubcommand>,
88    },
89
90    /// Query the code index
91    ///
92    /// If no pattern is provided, launches interactive mode (TUI).
93    ///
94    /// Search modes:
95    ///   - Default: Word-boundary matching (precise, finds complete identifiers)
96    ///     Example: rfx query "Error" → finds "Error" but not "NetworkError"
97    ///     Example: rfx query "test" → finds "test" but not "test_helper"
98    ///
99    ///   - Symbol search: Word-boundary for text, exact match for symbols
100    ///     Example: rfx query "parse" --symbols → finds only "parse" function/class
101    ///     Example: rfx query "parse" --kind function → finds only "parse" functions
102    ///
103    ///   - Substring search: Expansive matching (opt-in with --contains)
104    ///     Example: rfx query "mb" --contains → finds "mb", "kmb_dai_ops", "symbol", etc.
105    ///
106    ///   - Regex search: Pattern-controlled matching (opt-in with --regex)
107    ///     Example: rfx query "^mb_.*" --regex → finds "mb_init", "mb_start", etc.
108    ///
109    /// Interactive mode:
110    ///   - Launch with: rfx query
111    ///   - Search, filter, and navigate code results in a live TUI
112    ///   - Press '?' for help, 'q' to quit
113    Query {
114        /// Search pattern (omit to launch interactive mode)
115        pattern: Option<String>,
116
117        /// Search symbol definitions only (functions, classes, etc.)
118        #[arg(short, long)]
119        symbols: bool,
120
121        /// Filter by language
122        /// Supported: rust, python, javascript, typescript, vue, svelte, go, java, php, c, c++, c#, ruby, kotlin, zig
123        #[arg(short, long)]
124        lang: Option<String>,
125
126        /// Filter by symbol kind (implies --symbols)
127        /// Supported: function, class, struct, enum, interface, trait, constant, variable, method, module, namespace, type, macro, property, event, import, export, attribute
128        #[arg(short, long)]
129        kind: Option<String>,
130
131        /// Use AST pattern matching (SLOW: 500ms-2s+, scans all files)
132        ///
133        /// WARNING: AST queries bypass trigram optimization and scan the entire codebase.
134        /// In 95% of cases, use --symbols instead which is 10-100x faster.
135        ///
136        /// When --ast is set, the pattern parameter is interpreted as a Tree-sitter
137        /// S-expression query instead of text search.
138        ///
139        /// RECOMMENDED: Always use --glob to limit scope for better performance.
140        ///
141        /// Examples:
142        ///   Fast (2-50ms):    rfx query "fetch" --symbols --kind function --lang python
143        ///   Slow (500ms-2s):  rfx query "(function_definition) @fn" --ast --lang python
144        ///   Faster with glob: rfx query "(class_declaration) @class" --ast --lang typescript --glob "src/**/*.ts"
145        #[arg(long)]
146        ast: bool,
147
148        /// Use regex pattern matching
149        ///
150        /// Enables standard regex syntax in the search pattern:
151        ///   |  for alternation (OR) - NO backslash needed
152        ///   .  matches any character
153        ///   .*  matches zero or more characters
154        ///   ^  anchors to start of line
155        ///   $  anchors to end of line
156        ///
157        /// Examples:
158        ///   --regex "belongsTo|hasMany"       Match belongsTo OR hasMany
159        ///   --regex "^import.*from"           Lines starting with import...from
160        ///   --regex "fn.*test"                Functions containing 'test'
161        ///
162        /// Note: Cannot be combined with --contains (mutually exclusive)
163        #[arg(short = 'r', long)]
164        regex: bool,
165
166        /// Output format as JSON
167        #[arg(long)]
168        json: bool,
169
170        /// Pretty-print JSON output (only with --json)
171        /// By default, JSON is minified to reduce token usage
172        #[arg(long)]
173        pretty: bool,
174
175        /// AI-optimized mode: returns JSON with ai_instruction field
176        /// Implies --json (minified by default, use --pretty for formatted output)
177        /// Provides context-aware guidance to AI agents on response format and next actions
178        #[arg(long)]
179        ai: bool,
180
181        /// Maximum number of results
182        #[arg(short = 'n', long)]
183        limit: Option<usize>,
184
185        /// Pagination offset (skip first N results after sorting)
186        /// Use with --limit for pagination: --offset 0 --limit 10, then --offset 10 --limit 10
187        #[arg(short = 'o', long)]
188        offset: Option<usize>,
189
190        /// Show full symbol definition (entire function/class body)
191        /// Only applicable to symbol searches
192        #[arg(long)]
193        expand: bool,
194
195        /// Filter by file path (supports substring matching)
196        /// Example: --file math.rs or --file helpers/
197        #[arg(short = 'f', long)]
198        file: Option<String>,
199
200        /// Exact symbol name match (no substring matching)
201        /// Only applicable to symbol searches
202        #[arg(long)]
203        exact: bool,
204
205        /// Use substring matching for both text and symbols (expansive search)
206        ///
207        /// Default behavior uses word-boundary matching for precision:
208        ///   "Error" matches "Error" but not "NetworkError"
209        ///
210        /// With --contains, enables substring matching (expansive):
211        ///   "Error" matches "Error", "NetworkError", "error_handler", etc.
212        ///
213        /// Use cases:
214        ///   - Finding partial matches: --contains "partial"
215        ///   - When you're unsure of exact names
216        ///   - Exploratory searches
217        ///
218        /// Note: Cannot be combined with --regex or --exact (mutually exclusive)
219        #[arg(long)]
220        contains: bool,
221
222        /// Only show count and timing, not the actual results
223        #[arg(short, long)]
224        count: bool,
225
226        /// Query timeout in seconds (0 = no timeout, default: 30)
227        #[arg(short = 't', long, default_value = "30")]
228        timeout: u64,
229
230        /// Use plain text output (disable colors and syntax highlighting)
231        #[arg(long)]
232        plain: bool,
233
234        /// Include files matching glob pattern (can be repeated)
235        ///
236        /// Pattern syntax (NO shell quotes in the pattern itself):
237        ///   ** = recursive match (all subdirectories)
238        ///   *  = single level match (one directory)
239        ///
240        /// Examples:
241        ///   --glob src/**/*.rs          All .rs files under src/ (recursive)
242        ///   --glob app/Models/*.php     PHP files directly in Models/ (not subdirs)
243        ///   --glob tests/**/*_test.go   All test files under tests/
244        ///
245        /// Tip: Use --file for simple substring matching instead:
246        ///   --file User.php             Simpler than --glob **/User.php
247        #[arg(short = 'g', long)]
248        glob: Vec<String>,
249
250        /// Exclude files matching glob pattern (can be repeated)
251        ///
252        /// Same syntax as --glob (** for recursive, * for single level)
253        ///
254        /// Examples:
255        ///   --exclude target/**         Exclude all files under target/
256        ///   --exclude **/*.gen.rs       Exclude generated Rust files
257        ///   --exclude node_modules/**   Exclude npm dependencies
258        #[arg(short = 'x', long)]
259        exclude: Vec<String>,
260
261        /// Return only unique file paths (no line numbers or content)
262        /// Compatible with --json to output ["path1", "path2", ...]
263        #[arg(short = 'p', long)]
264        paths: bool,
265
266        /// Disable smart preview truncation (show full lines)
267        /// By default, previews are truncated to ~100 chars to reduce token usage
268        #[arg(long)]
269        no_truncate: bool,
270
271        /// Return all results (no limit)
272        /// Equivalent to --limit 0, convenience flag for getting unlimited results
273        #[arg(short = 'a', long)]
274        all: bool,
275
276        /// Force execution of potentially expensive queries
277        /// Bypasses broad query detection that prevents queries with:
278        /// • Short patterns (< 3 characters)
279        /// • High candidate counts (> 5,000 files for symbol/AST queries)
280        /// • AST queries without --glob restrictions
281        #[arg(long)]
282        force: bool,
283
284        /// Include dependency information (imports) in results
285        /// Currently only available for Rust files
286        #[arg(long)]
287        dependencies: bool,
288    },
289
290    /// Start a local HTTP API server
291    Serve {
292        /// Port to listen on
293        #[arg(short, long, default_value = "7878")]
294        port: u16,
295
296        /// Host to bind to
297        #[arg(long, default_value = "127.0.0.1")]
298        host: String,
299    },
300
301    /// Show index statistics and cache information
302    Stats {
303        /// Output format as JSON
304        #[arg(long)]
305        json: bool,
306
307        /// Pretty-print JSON output (only with --json)
308        #[arg(long)]
309        pretty: bool,
310    },
311
312    /// Clear the local cache
313    Clear {
314        /// Skip confirmation prompt
315        #[arg(short, long)]
316        yes: bool,
317    },
318
319    /// List all indexed files
320    ListFiles {
321        /// Output format as JSON
322        #[arg(long)]
323        json: bool,
324
325        /// Pretty-print JSON output (only with --json)
326        #[arg(long)]
327        pretty: bool,
328    },
329
330    /// Watch for file changes and auto-reindex
331    ///
332    /// Continuously monitors the workspace for changes and automatically
333    /// triggers incremental reindexing. Useful for IDE integrations and
334    /// keeping the index always fresh during active development.
335    ///
336    /// The debounce timer resets on every file change, batching rapid edits
337    /// (e.g., multi-file refactors, format-on-save) into a single reindex.
338    Watch {
339        /// Directory to watch (defaults to current directory)
340        #[arg(value_name = "PATH", default_value = ".")]
341        path: PathBuf,
342
343        /// Debounce duration in milliseconds (default: 15000 = 15s)
344        /// Waits this long after the last change before reindexing
345        /// Valid range: 5000-30000 (5-30 seconds)
346        #[arg(short, long, default_value = "15000")]
347        debounce: u64,
348
349        /// Suppress output (only log errors)
350        #[arg(short, long)]
351        quiet: bool,
352    },
353
354    /// Start MCP server for AI agent integration
355    ///
356    /// Runs Reflex as a Model Context Protocol (MCP) server using stdio transport.
357    /// This command is automatically invoked by MCP clients like Claude Code and
358    /// should not be run manually.
359    ///
360    /// Configuration example for Claude Code (~/.claude/claude_code_config.json):
361    /// {
362    ///   "mcpServers": {
363    ///     "reflex": {
364    ///       "type": "stdio",
365    ///       "command": "rfx",
366    ///       "args": ["mcp"]
367    ///     }
368    ///   }
369    /// }
370    Mcp,
371
372    /// Analyze codebase structure and dependencies
373    ///
374    /// Perform graph-wide dependency analysis to understand code architecture.
375    /// By default, shows a summary report with counts. Use specific flags for
376    /// detailed results.
377    ///
378    /// Examples:
379    ///   rfx analyze                                # Summary report
380    ///   rfx analyze --circular                     # Find cycles
381    ///   rfx analyze --hotspots                     # Most-imported files
382    ///   rfx analyze --hotspots --min-dependents 5  # Filter by minimum
383    ///   rfx analyze --unused                       # Orphaned files
384    ///   rfx analyze --islands                      # Disconnected components
385    ///   rfx analyze --hotspots --count             # Just show count
386    ///   rfx analyze --circular --glob "src/**"     # Limit to src/
387    Analyze {
388        /// Show circular dependencies
389        #[arg(long)]
390        circular: bool,
391
392        /// Show most-imported files (hotspots)
393        #[arg(long)]
394        hotspots: bool,
395
396        /// Minimum number of dependents for hotspots (default: 2)
397        #[arg(long, default_value = "2", requires = "hotspots")]
398        min_dependents: usize,
399
400        /// Show unused/orphaned files
401        #[arg(long)]
402        unused: bool,
403
404        /// Show disconnected components (islands)
405        #[arg(long)]
406        islands: bool,
407
408        /// Minimum island size (default: 2)
409        #[arg(long, default_value = "2", requires = "islands")]
410        min_island_size: usize,
411
412        /// Maximum island size (default: 500 or 50% of total files)
413        #[arg(long, requires = "islands")]
414        max_island_size: Option<usize>,
415
416        /// Output format: tree (default), table, dot
417        #[arg(short = 'f', long, default_value = "tree")]
418        format: String,
419
420        /// Output as JSON
421        #[arg(long)]
422        json: bool,
423
424        /// Pretty-print JSON output
425        #[arg(long)]
426        pretty: bool,
427
428        /// Only show count and timing, not the actual results
429        #[arg(short, long)]
430        count: bool,
431
432        /// Return all results (no limit)
433        /// Equivalent to --limit 0, convenience flag for unlimited results
434        #[arg(short = 'a', long)]
435        all: bool,
436
437        /// Use plain text output (disable colors and syntax highlighting)
438        #[arg(long)]
439        plain: bool,
440
441        /// Include files matching glob pattern (can be repeated)
442        /// Example: --glob "src/**/*.rs" --glob "tests/**/*.rs"
443        #[arg(short = 'g', long)]
444        glob: Vec<String>,
445
446        /// Exclude files matching glob pattern (can be repeated)
447        /// Example: --exclude "target/**" --exclude "*.gen.rs"
448        #[arg(short = 'x', long)]
449        exclude: Vec<String>,
450
451        /// Force execution of potentially expensive queries
452        /// Bypasses broad query detection
453        #[arg(long)]
454        force: bool,
455
456        /// Maximum number of results
457        #[arg(short = 'n', long)]
458        limit: Option<usize>,
459
460        /// Pagination offset
461        #[arg(short = 'o', long)]
462        offset: Option<usize>,
463
464        /// Sort order for results: asc (ascending) or desc (descending)
465        /// Applies to --hotspots (by import_count), --islands (by size), --circular (by cycle length)
466        /// Default: desc (most important first)
467        #[arg(long)]
468        sort: Option<String>,
469    },
470
471    /// Analyze dependencies for a specific file
472    ///
473    /// Show dependencies and dependents for a single file.
474    /// For graph-wide analysis, use 'rfx analyze' instead.
475    ///
476    /// Examples:
477    ///   rfx deps src/main.rs                  # Show dependencies
478    ///   rfx deps src/config.rs --reverse      # Show dependents
479    ///   rfx deps src/api.rs --depth 3         # Transitive deps
480    Deps {
481        /// File path to analyze
482        file: PathBuf,
483
484        /// Show files that depend on this file (reverse lookup)
485        #[arg(short, long)]
486        reverse: bool,
487
488        /// Traversal depth for transitive dependencies (default: 1)
489        #[arg(short, long, default_value = "1")]
490        depth: usize,
491
492        /// Output format: tree (default), table, dot
493        #[arg(short = 'f', long, default_value = "tree")]
494        format: String,
495
496        /// Output as JSON
497        #[arg(long)]
498        json: bool,
499
500        /// Pretty-print JSON output
501        #[arg(long)]
502        pretty: bool,
503    },
504
505    /// Ask a natural language question and generate search queries
506    ///
507    /// Uses an LLM to translate natural language questions into `rfx query` commands.
508    /// Requires API key configuration for one of: OpenAI, Anthropic, or OpenRouter.
509    ///
510    /// If no question is provided, launches interactive chat mode by default.
511    ///
512    /// Configuration:
513    ///   1. Run interactive setup wizard (recommended):
514    ///      rfx ask --configure
515    ///
516    ///   2. OR set API key via environment variable:
517    ///      - OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY
518    ///
519    ///   3. Optional: Configure provider in .reflex/config.toml:
520    ///      [semantic]
521    ///      provider = "openai"  # or anthropic, openrouter
522    ///      model = "gpt-5.1-mini"  # optional, defaults to provider default
523    ///
524    /// Examples:
525    ///   rfx ask --configure                           # Interactive setup wizard
526    ///   rfx ask                                       # Launch interactive chat (default)
527    ///   rfx ask "Find all TODOs in Rust files"
528    ///   rfx ask "Where is the main function defined?" --execute
529    ///   rfx ask "Show me error handling code" --provider openrouter
530    Ask {
531        /// Natural language question
532        question: Option<String>,
533
534        /// Execute queries immediately without confirmation
535        #[arg(short, long)]
536        execute: bool,
537
538        /// Override configured LLM provider (openai, anthropic, openrouter, openai-compatible)
539        #[arg(short, long)]
540        provider: Option<String>,
541
542        /// Output format as JSON
543        #[arg(long)]
544        json: bool,
545
546        /// Pretty-print JSON output (only with --json)
547        #[arg(long)]
548        pretty: bool,
549
550        /// Additional context to inject into prompt (e.g., from `rfx context`)
551        #[arg(long)]
552        additional_context: Option<String>,
553
554        /// Launch interactive configuration wizard to set up AI provider and API key
555        #[arg(long)]
556        configure: bool,
557
558        /// Enable agentic mode (multi-step reasoning with context gathering)
559        #[arg(long)]
560        agentic: bool,
561
562        /// Maximum iterations for query refinement in agentic mode (default: 2)
563        #[arg(long, default_value = "2")]
564        max_iterations: usize,
565
566        /// Skip result evaluation in agentic mode
567        #[arg(long)]
568        no_eval: bool,
569
570        /// Show LLM reasoning blocks at each phase (agentic mode only)
571        #[arg(long)]
572        show_reasoning: bool,
573
574        /// Verbose output: show tool results and details (agentic mode only)
575        #[arg(long)]
576        verbose: bool,
577
578        /// Quiet mode: suppress progress output (agentic mode only)
579        #[arg(long)]
580        quiet: bool,
581
582        /// Generate a conversational answer based on search results
583        #[arg(long)]
584        answer: bool,
585
586        /// Launch interactive chat mode (TUI) with conversation history
587        #[arg(short = 'i', long)]
588        interactive: bool,
589
590        /// Debug mode: output full LLM prompts and retain terminal history
591        #[arg(long)]
592        debug: bool,
593    },
594
595    /// Generate codebase context for AI prompts
596    ///
597    /// Provides structural and organizational context about the project to help
598    /// LLMs understand project layout. Use with `rfx ask --additional-context`.
599    ///
600    /// By default (no flags), shows all context types. Use individual flags to
601    /// select specific context types.
602    ///
603    /// Examples:
604    ///   rfx context                                    # Full context (all types)
605    ///   rfx context --path services/backend            # Full context for monorepo subdirectory
606    ///   rfx context --framework --entry-points         # Specific context types only
607    ///   rfx context --structure --depth 5              # Deep directory tree
608    ///
609    ///   # Use with semantic queries
610    ///   rfx ask "find auth" --additional-context "$(rfx context --framework)"
611    Context {
612        /// Show directory structure (enabled by default)
613        #[arg(long)]
614        structure: bool,
615
616        /// Focus on specific directory path
617        #[arg(short, long)]
618        path: Option<String>,
619
620        /// Show file type distribution (enabled by default)
621        #[arg(long)]
622        file_types: bool,
623
624        /// Detect project type (CLI/library/webapp/monorepo)
625        #[arg(long)]
626        project_type: bool,
627
628        /// Detect frameworks and conventions
629        #[arg(long)]
630        framework: bool,
631
632        /// Show entry point files
633        #[arg(long)]
634        entry_points: bool,
635
636        /// Show test organization pattern
637        #[arg(long)]
638        test_layout: bool,
639
640        /// List important configuration files
641        #[arg(long)]
642        config_files: bool,
643
644        /// Tree depth for --structure (default: 1)
645        #[arg(long, default_value = "1")]
646        depth: usize,
647
648        /// Output as JSON
649        #[arg(long)]
650        json: bool,
651    },
652
653    /// Internal command: Run background symbol indexing (hidden from help)
654    #[command(hide = true)]
655    IndexSymbolsInternal {
656        /// Cache directory path
657        cache_dir: PathBuf,
658    },
659
660    /// Take and manage codebase snapshots for structural tracking
661    ///
662    /// Snapshots capture the structural state of the index (files, dependencies,
663    /// metrics) for diffing and historical analysis.
664    ///
665    /// With no subcommand, creates a new snapshot.
666    ///
667    /// Examples:
668    ///   rfx snapshot               # Create a new snapshot
669    ///   rfx snapshot list           # List available snapshots
670    ///   rfx snapshot diff           # Diff latest vs previous
671    ///   rfx snapshot gc             # Run retention policy
672    Snapshot {
673        #[command(subcommand)]
674        command: Option<SnapshotSubcommand>,
675    },
676
677    /// Generate codebase intelligence surfaces (changelog, wiki, map, site)
678    ///
679    /// Pulse turns structural facts from the index into browsable documentation.
680    /// The `generate` command creates a Zola project and builds it into a static HTML site.
681    ///
682    /// Examples:
683    ///   rfx pulse changelog --no-llm         # Structural-only changelog
684    ///   rfx pulse wiki --no-llm             # Generate wiki pages
685    ///   rfx pulse map                        # Architecture map (mermaid)
686    ///   rfx pulse generate --no-llm          # Full static site (Zola)
687    Pulse {
688        #[command(subcommand)]
689        command: PulseSubcommand,
690    },
691
692    /// Manage LLM provider configuration (shared by `ask` and `pulse`)
693    ///
694    /// Examples:
695    ///   rfx llm config                       # Launch interactive setup wizard
696    ///   rfx llm status                       # Show current LLM configuration
697    Llm {
698        #[command(subcommand)]
699        command: LlmSubcommand,
700    },
701}
702
703#[derive(Subcommand, Debug)]
704pub enum SnapshotSubcommand {
705    /// Compare two snapshots
706    ///
707    /// Defaults to latest vs previous snapshot.
708    Diff {
709        /// Baseline snapshot ID (defaults to second-most-recent)
710        #[arg(long)]
711        baseline: Option<String>,
712
713        /// Current snapshot ID (defaults to most recent)
714        #[arg(long)]
715        current: Option<String>,
716
717        /// Output as JSON
718        #[arg(long)]
719        json: bool,
720
721        /// Pretty-print JSON output
722        #[arg(long)]
723        pretty: bool,
724    },
725
726    /// List available snapshots
727    List {
728        /// Output as JSON
729        #[arg(long)]
730        json: bool,
731
732        /// Pretty-print JSON output
733        #[arg(long)]
734        pretty: bool,
735    },
736
737    /// Run snapshot garbage collection
738    Gc {
739        /// Output as JSON
740        #[arg(long)]
741        json: bool,
742    },
743}
744
745#[derive(Subcommand, Debug)]
746pub enum PulseSubcommand {
747    /// Generate a product-level changelog from recent commits
748    Changelog {
749        /// Number of recent commits to include (default: 20)
750        #[arg(long, default_value = "20")]
751        count: usize,
752
753        /// Skip LLM narration (structural content only)
754        #[arg(long)]
755        no_llm: bool,
756
757        /// Output as JSON
758        #[arg(long)]
759        json: bool,
760
761        /// Pretty-print JSON output
762        #[arg(long)]
763        pretty: bool,
764    },
765
766    /// Generate living wiki pages
767    Wiki {
768        /// Skip LLM narration
769        #[arg(long)]
770        no_llm: bool,
771
772        /// Output directory for markdown files
773        #[arg(short, long)]
774        output: Option<PathBuf>,
775
776        /// Output as JSON
777        #[arg(long)]
778        json: bool,
779    },
780
781    /// Export an architecture map
782    Map {
783        /// Output format (mermaid, d2)
784        #[arg(short, long, default_value = "mermaid")]
785        format: String,
786
787        /// Output file (prints to stdout if not set)
788        #[arg(short, long)]
789        output: Option<PathBuf>,
790
791        /// Zoom level: repo (default) or module path
792        #[arg(short, long)]
793        zoom: Option<String>,
794    },
795
796    /// Generate a complete static site (Zola project + HTML build)
797    ///
798    /// Creates a Zola project with markdown content, templates, and CSS,
799    /// then downloads Zola and builds it into a static HTML site.
800    /// The --base-url maps to Zola's base_url config.
801    Generate {
802        /// Output directory for the Zola project
803        #[arg(short, long, default_value = "pulse-site")]
804        output: PathBuf,
805
806        /// Base URL for the site (maps to Zola's base_url)
807        #[arg(long, default_value = "/")]
808        base_url: String,
809
810        /// Site title
811        #[arg(long)]
812        title: Option<String>,
813
814        /// Surfaces to include (comma-separated: wiki,changelog,map,onboard,timeline,glossary,explorer)
815        #[arg(long)]
816        include: Option<String>,
817
818        /// Skip LLM narration
819        #[arg(long)]
820        no_llm: bool,
821
822        /// Clean output directory before generating
823        #[arg(long)]
824        clean: bool,
825
826        /// Force re-narration (ignore LLM cache)
827        #[arg(long)]
828        force_renarrate: bool,
829
830        /// Maximum concurrent LLM requests (0 = unlimited, default)
831        #[arg(long, default_value = "0")]
832        concurrency: usize,
833
834        /// Maximum directory depth for module discovery (1=top-level only, 2=default)
835        #[arg(long, default_value = "2")]
836        depth: u8,
837
838        /// Minimum file count for a module to be included
839        #[arg(long, default_value = "1")]
840        min_files: usize,
841    },
842
843    /// Serve the generated site locally
844    ///
845    /// Starts a local development server for the Pulse site.
846    /// Uses Zola's built-in server with live reload.
847    Serve {
848        /// Directory containing the generated Zola project
849        #[arg(short, long, default_value = "pulse-site")]
850        output: PathBuf,
851
852        /// Port to serve on
853        #[arg(short, long, default_value = "1111")]
854        port: u16,
855
856        /// Open browser automatically
857        #[arg(long, default_value = "true")]
858        open: bool,
859    },
860
861    /// Generate a developer onboarding guide
862    Onboard {
863        /// Skip LLM narration
864        #[arg(long)]
865        no_llm: bool,
866
867        /// Output as JSON
868        #[arg(long)]
869        json: bool,
870    },
871
872    /// Show development timeline from git history
873    Timeline {
874        /// Output as JSON
875        #[arg(long)]
876        json: bool,
877    },
878
879    /// Generate cross-cutting symbol glossary
880    Glossary {
881        /// Output as JSON
882        #[arg(long)]
883        json: bool,
884    },
885}
886
887#[derive(Subcommand, Debug)]
888pub enum LlmSubcommand {
889    /// Launch interactive configuration wizard for AI provider and API key
890    Config,
891    /// Show current LLM configuration status
892    Status,
893}
894
895/// Try to run background cache compaction if needed
896///
897/// Checks if 24+ hours have passed since last compaction.
898/// If yes, spawns a non-blocking background thread to compact the cache.
899/// Main command continues immediately without waiting for compaction.
900///
901/// Compaction is skipped for commands that don't need it:
902/// - Clear (will delete the cache anyway)
903/// - Mcp (long-running server process)
904/// - Watch (long-running watcher process)
905/// - Serve (long-running HTTP server)
906fn try_background_compact(cache: &CacheManager, command: &Command) {
907    // Skip compaction for certain commands
908    match command {
909        Command::Clear { .. } => {
910            log::debug!("Skipping compaction for Clear command");
911            return;
912        }
913        Command::Mcp => {
914            log::debug!("Skipping compaction for Mcp command");
915            return;
916        }
917        Command::Watch { .. } => {
918            log::debug!("Skipping compaction for Watch command");
919            return;
920        }
921        Command::Serve { .. } => {
922            log::debug!("Skipping compaction for Serve command");
923            return;
924        }
925        _ => {}
926    }
927
928    // Check if compaction should run
929    let should_compact = match cache.should_compact() {
930        Ok(true) => true,
931        Ok(false) => {
932            log::debug!("Compaction not needed yet (last run <24h ago)");
933            return;
934        }
935        Err(e) => {
936            log::warn!("Failed to check compaction status: {}", e);
937            return;
938        }
939    };
940
941    if !should_compact {
942        return;
943    }
944
945    log::info!("Starting background cache compaction...");
946
947    // Clone cache path for background thread
948    let cache_path = cache.path().to_path_buf();
949
950    // Spawn background thread for compaction
951    std::thread::spawn(move || {
952        let cache = CacheManager::new(cache_path.parent().expect("Cache should have parent directory"));
953
954        match cache.compact() {
955            Ok(report) => {
956                log::info!(
957                    "Background compaction completed: {} files removed, {:.2} MB saved, took {}ms",
958                    report.files_removed,
959                    report.space_saved_bytes as f64 / 1_048_576.0,
960                    report.duration_ms
961                );
962            }
963            Err(e) => {
964                log::warn!("Background compaction failed: {}", e);
965            }
966        }
967    });
968
969    log::debug!("Background compaction thread spawned - main command continuing");
970}
971
972impl Cli {
973    /// Execute the CLI command
974    pub fn execute(self) -> Result<()> {
975        // Setup logging based on verbosity
976        let log_level = match self.verbose {
977            0 => "warn",   // Default: only warnings and errors
978            1 => "info",   // -v: show info messages
979            2 => "debug",  // -vv: show debug messages
980            _ => "trace",  // -vvv: show trace messages
981        };
982        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
983            .init();
984
985        // Try background compaction (non-blocking) before command execution
986        if let Some(ref command) = self.command {
987            // Use current directory as default cache location
988            let cache = CacheManager::new(".");
989            try_background_compact(&cache, command);
990        }
991
992        // Execute the subcommand, or show help if no command provided
993        match self.command {
994            None => {
995                // No subcommand: show help
996                Cli::command().print_help()?;
997                println!();  // Add newline after help
998                Ok(())
999            }
1000            Some(Command::Index { path, force, languages, quiet, command }) => {
1001                match command {
1002                    None => {
1003                        // Default: run index build
1004                        index::handle_index_build(&path, &force, &languages, &quiet)
1005                    }
1006                    Some(IndexSubcommand::Status) => {
1007                        index::handle_index_status()
1008                    }
1009                    Some(IndexSubcommand::Compact { json, pretty }) => {
1010                        index::handle_index_compact(&json, &pretty)
1011                    }
1012                }
1013            }
1014            Some(Command::Query { pattern, symbols, lang, kind, ast, regex, json, pretty, ai, limit, offset, expand, file, exact, contains, count, timeout, plain, glob, exclude, paths, no_truncate, all, force, dependencies }) => {
1015                // If no pattern provided, launch interactive mode
1016                match pattern {
1017                    None => query::handle_interactive(),
1018                    Some(pattern) => query::handle_query(pattern, symbols, lang, kind, ast, regex, json, pretty, ai, limit, offset, expand, file, exact, contains, count, timeout, plain, glob, exclude, paths, no_truncate, all, force, dependencies)
1019                }
1020            }
1021            Some(Command::Serve { port, host }) => {
1022                serve::handle_serve(port, host)
1023            }
1024            Some(Command::Stats { json, pretty }) => {
1025                misc::handle_stats(json, pretty)
1026            }
1027            Some(Command::Clear { yes }) => {
1028                misc::handle_clear(yes)
1029            }
1030            Some(Command::ListFiles { json, pretty }) => {
1031                misc::handle_list_files(json, pretty)
1032            }
1033            Some(Command::Watch { path, debounce, quiet }) => {
1034                watch::handle_watch(path, debounce, quiet)
1035            }
1036            Some(Command::Mcp) => {
1037                misc::handle_mcp()
1038            }
1039            Some(Command::Analyze { circular, hotspots, min_dependents, unused, islands, min_island_size, max_island_size, format, json, pretty, count, all, plain, glob, exclude, force, limit, offset, sort }) => {
1040                deps::handle_analyze(circular, hotspots, min_dependents, unused, islands, min_island_size, max_island_size, format, json, pretty, count, all, plain, glob, exclude, force, limit, offset, sort)
1041            }
1042            Some(Command::Deps { file, reverse, depth, format, json, pretty }) => {
1043                deps::handle_deps(file, reverse, depth, format, json, pretty)
1044            }
1045            Some(Command::Ask { question, execute, provider, json, pretty, additional_context, configure, agentic, max_iterations, no_eval, show_reasoning, verbose, quiet, answer, interactive, debug }) => {
1046                ask::handle_ask(question, execute, provider, json, pretty, additional_context, configure, agentic, max_iterations, no_eval, show_reasoning, verbose, quiet, answer, interactive, debug)
1047            }
1048            Some(Command::Context { structure, path, file_types, project_type, framework, entry_points, test_layout, config_files, depth, json }) => {
1049                misc::handle_context(structure, path, file_types, project_type, framework, entry_points, test_layout, config_files, depth, json)
1050            }
1051            Some(Command::IndexSymbolsInternal { cache_dir }) => {
1052                index::handle_index_symbols_internal(cache_dir)
1053            }
1054            Some(Command::Snapshot { command }) => {
1055                match command {
1056                    None => snapshot::handle_snapshot_create(),
1057                    Some(SnapshotSubcommand::List { json, pretty }) => {
1058                        snapshot::handle_snapshot_list(json, pretty)
1059                    }
1060                    Some(SnapshotSubcommand::Diff { baseline, current, json, pretty }) => {
1061                        snapshot::handle_snapshot_diff(baseline, current, json, pretty)
1062                    }
1063                    Some(SnapshotSubcommand::Gc { json }) => {
1064                        snapshot::handle_snapshot_gc(json)
1065                    }
1066                }
1067            }
1068            Some(Command::Pulse { command }) => {
1069                match command {
1070                    PulseSubcommand::Changelog { count, no_llm, json, pretty } => {
1071                        pulse::handle_pulse_changelog(count, no_llm, json, pretty)
1072                    }
1073                    PulseSubcommand::Wiki { no_llm, output, json } => {
1074                        pulse::handle_pulse_wiki(no_llm, output, json)
1075                    }
1076                    PulseSubcommand::Map { format, output, zoom } => {
1077                        pulse::handle_pulse_map(format, output, zoom)
1078                    }
1079                    PulseSubcommand::Generate { output, base_url, title, include, no_llm, clean, force_renarrate, concurrency, depth, min_files } => {
1080                        pulse::handle_pulse_generate(output, base_url, title, include, no_llm, clean, force_renarrate, concurrency, depth, min_files)
1081                    }
1082                    PulseSubcommand::Serve { output, port, open } => {
1083                        pulse::handle_pulse_serve(output, port, open)
1084                    }
1085                    PulseSubcommand::Onboard { no_llm, json } => {
1086                        pulse::handle_pulse_onboard(no_llm, json)
1087                    }
1088                    PulseSubcommand::Timeline { json } => {
1089                        pulse::handle_pulse_timeline(json)
1090                    }
1091                    PulseSubcommand::Glossary { json } => {
1092                        pulse::handle_pulse_glossary(json)
1093                    }
1094                }
1095            }
1096            Some(Command::Llm { command }) => {
1097                match command {
1098                    LlmSubcommand::Config => llm::handle_llm_config(),
1099                    LlmSubcommand::Status => llm::handle_llm_status(),
1100                }
1101            }
1102        }
1103    }
1104}