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}