reflex/cli/mod.rs
1//! CLI argument parsing and command router
2
3use crate::cache::CacheManager;
4use anyhow::Result;
5use clap::{CommandFactory, Parser, Subcommand};
6use std::path::PathBuf;
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 /// Number of context lines to show before and after each match (max: 10)
272 /// Example: -C 3 shows 3 lines before and after each match
273 #[arg(short = 'C', long, value_name = "N")]
274 context: Option<usize>,
275
276 /// Return all results (no limit)
277 #[arg(short = 'a', long)]
278 all: bool,
279
280 /// Force execution of potentially expensive queries
281 /// Bypasses broad query detection that prevents queries with:
282 /// • Short patterns (< 3 characters)
283 /// • High candidate counts (> 5,000 files for symbol/AST queries)
284 /// • AST queries without --glob restrictions
285 #[arg(long)]
286 force: bool,
287
288 /// Include dependency information (imports) in results
289 /// Currently only available for Rust files
290 #[arg(long)]
291 dependencies: bool,
292 },
293
294 /// Start a local HTTP API server
295 Serve {
296 /// Port to listen on
297 #[arg(short, long, default_value = "7878")]
298 port: u16,
299
300 /// Host to bind to
301 #[arg(long, default_value = "127.0.0.1")]
302 host: String,
303 },
304
305 /// Show index statistics and cache information
306 Stats {
307 /// Output format as JSON
308 #[arg(long)]
309 json: bool,
310
311 /// Pretty-print JSON output (only with --json)
312 #[arg(long)]
313 pretty: bool,
314 },
315
316 /// Clear the local cache
317 Clear {
318 /// Skip confirmation prompt
319 #[arg(short, long)]
320 yes: bool,
321 },
322
323 /// List all indexed files
324 ListFiles {
325 /// Output format as JSON
326 #[arg(long)]
327 json: bool,
328
329 /// Pretty-print JSON output (only with --json)
330 #[arg(long)]
331 pretty: bool,
332
333 /// Filter by language (e.g. rust, python, typescript)
334 #[arg(short, long)]
335 lang: Option<String>,
336
337 /// Include files matching glob pattern (can be repeated)
338 /// Example: --glob "src/**/*.rs"
339 #[arg(short = 'g', long)]
340 glob: Vec<String>,
341 },
342
343 /// Watch for file changes and auto-reindex
344 ///
345 /// Continuously monitors the workspace for changes and automatically
346 /// triggers incremental reindexing. Useful for IDE integrations and
347 /// keeping the index always fresh during active development.
348 ///
349 /// The debounce timer resets on every file change, batching rapid edits
350 /// (e.g., multi-file refactors, format-on-save) into a single reindex.
351 Watch {
352 /// Directory to watch (defaults to current directory)
353 #[arg(value_name = "PATH", default_value = ".")]
354 path: PathBuf,
355
356 /// Debounce duration in milliseconds (default: 15000 = 15s)
357 /// Waits this long after the last change before reindexing
358 /// Valid range: 5000-30000 (5-30 seconds)
359 #[arg(short, long, default_value = "15000")]
360 debounce: u64,
361
362 /// Suppress output (only log errors)
363 #[arg(short, long)]
364 quiet: bool,
365 },
366
367 /// Start MCP server for AI agent integration
368 ///
369 /// Runs Reflex as a Model Context Protocol (MCP) server using stdio transport.
370 /// This command is automatically invoked by MCP clients like Claude Code and
371 /// should not be run manually.
372 ///
373 /// Configuration example for Claude Code (~/.claude/claude_code_config.json):
374 /// {
375 /// "mcpServers": {
376 /// "reflex": {
377 /// "type": "stdio",
378 /// "command": "rfx",
379 /// "args": ["mcp"]
380 /// }
381 /// }
382 /// }
383 Mcp,
384
385 /// Analyze codebase structure and dependencies
386 ///
387 /// Perform graph-wide dependency analysis to understand code architecture.
388 /// By default, shows a summary report with counts. Use specific flags for
389 /// detailed results.
390 ///
391 /// Examples:
392 /// rfx analyze # Summary report
393 /// rfx analyze --circular # Find cycles
394 /// rfx analyze --hotspots # Most-imported files
395 /// rfx analyze --hotspots --min-dependents 5 # Filter by minimum
396 /// rfx analyze --unused # Orphaned files
397 /// rfx analyze --islands # Disconnected components
398 /// rfx analyze --hotspots --count # Just show count
399 /// rfx analyze --circular --glob "src/**" # Limit to src/
400 Analyze {
401 /// Show circular dependencies
402 #[arg(long)]
403 circular: bool,
404
405 /// Show most-imported files (hotspots)
406 #[arg(long)]
407 hotspots: bool,
408
409 /// Minimum number of dependents for hotspots (default: 2)
410 #[arg(long, default_value = "2", requires = "hotspots")]
411 min_dependents: usize,
412
413 /// Show unused/orphaned files
414 #[arg(long)]
415 unused: bool,
416
417 /// Show disconnected components (islands)
418 #[arg(long)]
419 islands: bool,
420
421 /// Minimum island size (default: 2)
422 #[arg(long, default_value = "2", requires = "islands")]
423 min_island_size: usize,
424
425 /// Maximum island size (default: 500 or 50% of total files)
426 #[arg(long, requires = "islands")]
427 max_island_size: Option<usize>,
428
429 /// Output format: tree (default), table, dot
430 #[arg(short = 'f', long, default_value = "tree")]
431 format: String,
432
433 /// Output as JSON
434 #[arg(long)]
435 json: bool,
436
437 /// Pretty-print JSON output
438 #[arg(long)]
439 pretty: bool,
440
441 /// Only show count and timing, not the actual results
442 #[arg(short, long)]
443 count: bool,
444
445 /// Return all results (no limit)
446 /// Equivalent to --limit 0, convenience flag for unlimited results
447 #[arg(short = 'a', long)]
448 all: bool,
449
450 /// Use plain text output (disable colors and syntax highlighting)
451 #[arg(long)]
452 plain: bool,
453
454 /// Include files matching glob pattern (can be repeated)
455 /// Example: --glob "src/**/*.rs" --glob "tests/**/*.rs"
456 #[arg(short = 'g', long)]
457 glob: Vec<String>,
458
459 /// Exclude files matching glob pattern (can be repeated)
460 /// Example: --exclude "target/**" --exclude "*.gen.rs"
461 #[arg(short = 'x', long)]
462 exclude: Vec<String>,
463
464 /// Force execution of potentially expensive queries
465 /// Bypasses broad query detection
466 #[arg(long)]
467 force: bool,
468
469 /// Maximum number of results
470 #[arg(short = 'n', long)]
471 limit: Option<usize>,
472
473 /// Pagination offset
474 #[arg(short = 'o', long)]
475 offset: Option<usize>,
476
477 /// Sort order for results: asc (ascending) or desc (descending)
478 /// Applies to --hotspots (by import_count), --islands (by size), --circular (by cycle length)
479 /// Default: desc (most important first)
480 #[arg(long)]
481 sort: Option<String>,
482 },
483
484 /// Analyze dependencies for a specific file
485 ///
486 /// Show dependencies and dependents for a single file.
487 /// For graph-wide analysis, use 'rfx analyze' instead.
488 ///
489 /// Examples:
490 /// rfx deps src/main.rs # Show dependencies
491 /// rfx deps src/config.rs --reverse # Show dependents
492 /// rfx deps src/api.rs --depth 3 # Transitive deps
493 Deps {
494 /// File path to analyze
495 file: PathBuf,
496
497 /// Show files that depend on this file (reverse lookup)
498 #[arg(short, long)]
499 reverse: bool,
500
501 /// Traversal depth for transitive dependencies (default: 1)
502 #[arg(short, long, default_value = "1")]
503 depth: usize,
504
505 /// Output format: tree (default), table, dot
506 #[arg(short = 'f', long, default_value = "tree")]
507 format: String,
508
509 /// Output as JSON
510 #[arg(long)]
511 json: bool,
512
513 /// Pretty-print JSON output
514 #[arg(long)]
515 pretty: bool,
516 },
517
518 /// Ask a natural language question and generate search queries
519 ///
520 /// Uses an LLM to translate natural language questions into `rfx query` commands.
521 /// Requires API key configuration for one of: OpenAI, Anthropic, or OpenRouter.
522 ///
523 /// If no question is provided, launches interactive chat mode by default.
524 ///
525 /// Configuration:
526 /// 1. Run interactive setup wizard (recommended):
527 /// rfx ask --configure
528 ///
529 /// 2. OR set API key via environment variable:
530 /// - OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY
531 ///
532 /// 3. Optional: Configure provider in .reflex/config.toml:
533 /// [semantic]
534 /// provider = "openai" # or anthropic, openrouter
535 /// model = "gpt-5.1-mini" # optional, defaults to provider default
536 ///
537 /// Examples:
538 /// rfx ask --configure # Interactive setup wizard
539 /// rfx ask # Launch interactive chat (default)
540 /// rfx ask "Find all TODOs in Rust files"
541 /// rfx ask "Where is the main function defined?" --execute
542 /// rfx ask "Show me error handling code" --provider openrouter
543 Ask {
544 /// Natural language question
545 question: Option<String>,
546
547 /// Execute queries immediately without confirmation
548 #[arg(short, long)]
549 execute: bool,
550
551 /// Override configured LLM provider (openai, anthropic, openrouter, openai-compatible)
552 #[arg(short, long)]
553 provider: Option<String>,
554
555 /// Output format as JSON
556 #[arg(long)]
557 json: bool,
558
559 /// Pretty-print JSON output (only with --json)
560 #[arg(long)]
561 pretty: bool,
562
563 /// Additional context to inject into prompt (e.g., from `rfx context`)
564 #[arg(long)]
565 additional_context: Option<String>,
566
567 /// Launch interactive configuration wizard to set up AI provider and API key
568 #[arg(long)]
569 configure: bool,
570
571 /// Enable agentic mode (multi-step reasoning with context gathering)
572 #[arg(long)]
573 agentic: bool,
574
575 /// Maximum iterations for query refinement in agentic mode (default: 2)
576 #[arg(long, default_value = "2")]
577 max_iterations: usize,
578
579 /// Skip result evaluation in agentic mode
580 #[arg(long)]
581 no_eval: bool,
582
583 /// Show LLM reasoning blocks at each phase (agentic mode only)
584 #[arg(long)]
585 show_reasoning: bool,
586
587 /// Verbose output: show tool results and details (agentic mode only)
588 #[arg(long)]
589 verbose: bool,
590
591 /// Quiet mode: suppress progress output (agentic mode only)
592 #[arg(long)]
593 quiet: bool,
594
595 /// Generate a conversational answer based on search results
596 #[arg(long)]
597 answer: bool,
598
599 /// Launch interactive chat mode (TUI) with conversation history
600 #[arg(short = 'i', long)]
601 interactive: bool,
602
603 /// Debug mode: output full LLM prompts and retain terminal history
604 #[arg(long)]
605 debug: bool,
606 },
607
608 /// Generate codebase context for AI prompts
609 ///
610 /// Provides structural and organizational context about the project to help
611 /// LLMs understand project layout. Use with `rfx ask --additional-context`.
612 ///
613 /// By default (no flags), shows all context types. Use individual flags to
614 /// select specific context types.
615 ///
616 /// Examples:
617 /// rfx context # Full context (all types)
618 /// rfx context --path services/backend # Full context for monorepo subdirectory
619 /// rfx context --framework --entry-points # Specific context types only
620 /// rfx context --structure --depth 5 # Deep directory tree
621 ///
622 /// # Use with semantic queries
623 /// rfx ask "find auth" --additional-context "$(rfx context --framework)"
624 Context {
625 /// Show directory structure (enabled by default)
626 #[arg(long)]
627 structure: bool,
628
629 /// Focus on specific directory path
630 #[arg(short, long)]
631 path: Option<String>,
632
633 /// Show file type distribution (enabled by default)
634 #[arg(long)]
635 file_types: bool,
636
637 /// Detect project type (CLI/library/webapp/monorepo)
638 #[arg(long)]
639 project_type: bool,
640
641 /// Detect frameworks and conventions
642 #[arg(long)]
643 framework: bool,
644
645 /// Show entry point files
646 #[arg(long)]
647 entry_points: bool,
648
649 /// Show test organization pattern
650 #[arg(long)]
651 test_layout: bool,
652
653 /// List important configuration files
654 #[arg(long)]
655 config_files: bool,
656
657 /// Tree depth for --structure (default: 1)
658 #[arg(long, default_value = "1")]
659 depth: usize,
660
661 /// Output as JSON
662 #[arg(long)]
663 json: bool,
664 },
665
666 /// Internal command: Run background symbol indexing (hidden from help)
667 #[command(hide = true)]
668 IndexSymbolsInternal {
669 /// Cache directory path
670 cache_dir: PathBuf,
671 },
672
673 /// Take and manage codebase snapshots for structural tracking
674 ///
675 /// Snapshots capture the structural state of the index (files, dependencies,
676 /// metrics) for diffing and historical analysis.
677 ///
678 /// With no subcommand, creates a new snapshot.
679 ///
680 /// Examples:
681 /// rfx snapshot # Create a new snapshot
682 /// rfx snapshot list # List available snapshots
683 /// rfx snapshot diff # Diff latest vs previous
684 /// rfx snapshot gc # Run retention policy
685 Snapshot {
686 #[command(subcommand)]
687 command: Option<SnapshotSubcommand>,
688 },
689
690 /// Generate codebase intelligence surfaces (changelog, wiki, map, site)
691 ///
692 /// Pulse turns structural facts from the index into browsable documentation.
693 /// The `generate` command creates a Zola project and builds it into a static HTML site.
694 ///
695 /// Examples:
696 /// rfx pulse changelog --no-llm # Structural-only changelog
697 /// rfx pulse wiki --no-llm # Generate wiki pages
698 /// rfx pulse map # Architecture map (mermaid)
699 /// rfx pulse generate --no-llm # Full static site (Zola)
700 Pulse {
701 #[command(subcommand)]
702 command: PulseSubcommand,
703 },
704
705 /// Manage LLM provider configuration (shared by `ask` and `pulse`)
706 ///
707 /// Examples:
708 /// rfx llm config # Launch interactive setup wizard
709 /// rfx llm status # Show current LLM configuration
710 Llm {
711 #[command(subcommand)]
712 command: LlmSubcommand,
713 },
714}
715
716#[derive(Subcommand, Debug)]
717pub enum SnapshotSubcommand {
718 /// Compare two snapshots
719 ///
720 /// Defaults to latest vs previous snapshot.
721 Diff {
722 /// Baseline snapshot ID (defaults to second-most-recent)
723 #[arg(long)]
724 baseline: Option<String>,
725
726 /// Current snapshot ID (defaults to most recent)
727 #[arg(long)]
728 current: Option<String>,
729
730 /// Output as JSON
731 #[arg(long)]
732 json: bool,
733
734 /// Pretty-print JSON output
735 #[arg(long)]
736 pretty: bool,
737 },
738
739 /// List available snapshots
740 List {
741 /// Output as JSON
742 #[arg(long)]
743 json: bool,
744
745 /// Pretty-print JSON output
746 #[arg(long)]
747 pretty: bool,
748 },
749
750 /// Run snapshot garbage collection
751 Gc {
752 /// Output as JSON
753 #[arg(long)]
754 json: bool,
755 },
756}
757
758#[derive(Subcommand, Debug)]
759pub enum PulseSubcommand {
760 /// Generate a product-level changelog from recent commits
761 Changelog {
762 /// Number of recent commits to include (default: 20)
763 #[arg(long, default_value = "20")]
764 count: usize,
765
766 /// Skip LLM narration (structural content only)
767 #[arg(long)]
768 no_llm: bool,
769
770 /// Output as JSON
771 #[arg(long)]
772 json: bool,
773
774 /// Pretty-print JSON output
775 #[arg(long)]
776 pretty: bool,
777 },
778
779 /// Generate living wiki pages
780 Wiki {
781 /// Skip LLM narration
782 #[arg(long)]
783 no_llm: bool,
784
785 /// Output directory for markdown files
786 #[arg(short, long)]
787 output: Option<PathBuf>,
788
789 /// Output as JSON
790 #[arg(long)]
791 json: bool,
792 },
793
794 /// Export an architecture map
795 Map {
796 /// Output format (mermaid, d2)
797 #[arg(short, long, default_value = "mermaid")]
798 format: String,
799
800 /// Output file (prints to stdout if not set)
801 #[arg(short, long)]
802 output: Option<PathBuf>,
803
804 /// Zoom level: repo (default) or module path
805 #[arg(short, long)]
806 zoom: Option<String>,
807 },
808
809 /// Generate a complete static site (Zola project + HTML build)
810 ///
811 /// Creates a Zola project with markdown content, templates, and CSS,
812 /// then downloads Zola and builds it into a static HTML site.
813 /// The --base-url maps to Zola's base_url config.
814 Generate {
815 /// Output directory for the Zola project
816 #[arg(short, long, default_value = "pulse-site")]
817 output: PathBuf,
818
819 /// Base URL for the site (maps to Zola's base_url)
820 #[arg(long, default_value = "/")]
821 base_url: String,
822
823 /// Site title
824 #[arg(long)]
825 title: Option<String>,
826
827 /// Surfaces to include (comma-separated: wiki,changelog,map,onboard,timeline,glossary,explorer)
828 #[arg(long)]
829 include: Option<String>,
830
831 /// Skip LLM narration
832 #[arg(long)]
833 no_llm: bool,
834
835 /// Clean output directory before generating
836 #[arg(long)]
837 clean: bool,
838
839 /// Force re-narration (ignore LLM cache)
840 #[arg(long)]
841 force_renarrate: bool,
842
843 /// Maximum concurrent LLM requests (0 = unlimited, default)
844 #[arg(long, default_value = "0")]
845 concurrency: usize,
846
847 /// Maximum directory depth for module discovery (1=top-level only, 2=default)
848 #[arg(long, default_value = "2")]
849 depth: u8,
850
851 /// Minimum file count for a module to be included
852 #[arg(long, default_value = "1")]
853 min_files: usize,
854 },
855
856 /// Serve the generated site locally
857 ///
858 /// Starts a local development server for the Pulse site.
859 /// Uses Zola's built-in server with live reload.
860 Serve {
861 /// Directory containing the generated Zola project
862 #[arg(short, long, default_value = "pulse-site")]
863 output: PathBuf,
864
865 /// Port to serve on
866 #[arg(short, long, default_value = "1111")]
867 port: u16,
868
869 /// Open browser automatically
870 #[arg(long, default_value = "true")]
871 open: bool,
872 },
873
874 /// Generate a developer onboarding guide
875 Onboard {
876 /// Skip LLM narration
877 #[arg(long)]
878 no_llm: bool,
879
880 /// Output as JSON
881 #[arg(long)]
882 json: bool,
883 },
884
885 /// Show development timeline from git history
886 Timeline {
887 /// Output as JSON
888 #[arg(long)]
889 json: bool,
890 },
891
892 /// Generate cross-cutting symbol glossary
893 Glossary {
894 /// Output as JSON
895 #[arg(long)]
896 json: bool,
897 },
898}
899
900#[derive(Subcommand, Debug)]
901pub enum LlmSubcommand {
902 /// Launch interactive configuration wizard for AI provider and API key
903 Config,
904 /// Show current LLM configuration status
905 Status,
906}
907
908/// Format a byte count into a human-readable string (B, KB, MB, GB, TB).
909fn format_bytes(bytes: u64) -> String {
910 const KB: u64 = 1024;
911 const MB: u64 = KB * 1024;
912 const GB: u64 = MB * 1024;
913 const TB: u64 = GB * 1024;
914
915 if bytes >= TB {
916 format!("{:.2} TB", bytes as f64 / TB as f64)
917 } else if bytes >= GB {
918 format!("{:.2} GB", bytes as f64 / GB as f64)
919 } else if bytes >= MB {
920 format!("{:.2} MB", bytes as f64 / MB as f64)
921 } else if bytes >= KB {
922 format!("{:.2} KB", bytes as f64 / KB as f64)
923 } else if bytes > 0 {
924 format!("{} bytes", bytes)
925 } else {
926 "< 1 KB".to_string()
927 }
928}
929
930/// Try to run background cache compaction if needed
931///
932/// Checks if 24+ hours have passed since last compaction.
933/// If yes, spawns a non-blocking background thread to compact the cache.
934/// Main command continues immediately without waiting for compaction.
935///
936/// Compaction is skipped for commands that don't need it:
937/// - Clear (will delete the cache anyway)
938/// - Mcp (long-running server process)
939/// - Watch (long-running watcher process)
940/// - Serve (long-running HTTP server)
941fn try_background_compact(cache: &CacheManager, command: &Command) {
942 // Skip compaction for certain commands
943 match command {
944 Command::Clear { .. } => {
945 log::debug!("Skipping compaction for Clear command");
946 return;
947 }
948 Command::Watch { .. } => {
949 log::debug!("Skipping compaction for Watch command");
950 return;
951 }
952 Command::Serve { .. } => {
953 log::debug!("Skipping compaction for Serve command");
954 return;
955 }
956 _ => {}
957 }
958
959 // Check if compaction should run
960 let should_compact = match cache.should_compact() {
961 Ok(true) => true,
962 Ok(false) => {
963 log::debug!("Compaction not needed yet (last run <24h ago)");
964 return;
965 }
966 Err(e) => {
967 log::warn!("Failed to check compaction status: {}", e);
968 return;
969 }
970 };
971
972 if !should_compact {
973 return;
974 }
975
976 log::info!("Starting background cache compaction...");
977
978 // Clone cache path for background thread
979 let cache_path = cache.path().to_path_buf();
980
981 // Spawn background thread for compaction
982 std::thread::spawn(move || {
983 let cache = CacheManager::new(
984 cache_path
985 .parent()
986 .expect("Cache should have parent directory"),
987 );
988
989 match cache.compact() {
990 Ok(report) => {
991 log::info!(
992 "Background compaction completed: {} files removed, {:.2} MB saved, took {}ms",
993 report.files_removed,
994 report.space_saved_bytes as f64 / 1_048_576.0,
995 report.duration_ms
996 );
997 }
998 Err(e) => {
999 log::warn!("Background compaction failed: {}", e);
1000 }
1001 }
1002 });
1003
1004 log::debug!("Background compaction thread spawned - main command continuing");
1005}
1006
1007impl Cli {
1008 /// Execute the CLI command
1009 pub fn execute(self) -> Result<()> {
1010 // Setup logging based on verbosity
1011 let log_level = match self.verbose {
1012 0 => "warn", // Default: only warnings and errors
1013 1 => "info", // -v: show info messages
1014 2 => "debug", // -vv: show debug messages
1015 _ => "trace", // -vvv: show trace messages
1016 };
1017 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
1018 .init();
1019
1020 // Try background compaction (non-blocking) before command execution
1021 if let Some(ref command) = self.command {
1022 // Use current directory as default cache location
1023 let cache = CacheManager::new(".");
1024 try_background_compact(&cache, command);
1025 }
1026
1027 // Execute the subcommand, or show help if no command provided
1028 match self.command {
1029 None => {
1030 // No subcommand: show help
1031 Cli::command().print_help()?;
1032 println!(); // Add newline after help
1033 Ok(())
1034 }
1035 Some(Command::Index {
1036 path,
1037 force,
1038 languages,
1039 quiet,
1040 command,
1041 }) => {
1042 match command {
1043 None => {
1044 // Default: run index build
1045 index::handle_index_build(&path, &force, &languages, &quiet)
1046 }
1047 Some(IndexSubcommand::Status) => index::handle_index_status(),
1048 Some(IndexSubcommand::Compact { json, pretty }) => {
1049 index::handle_index_compact(&json, &pretty)
1050 }
1051 }
1052 }
1053 Some(Command::Query {
1054 pattern,
1055 symbols,
1056 lang,
1057 kind,
1058 ast,
1059 regex,
1060 json,
1061 pretty,
1062 ai,
1063 limit,
1064 offset,
1065 expand,
1066 file,
1067 exact,
1068 contains,
1069 count,
1070 timeout,
1071 plain,
1072 glob,
1073 exclude,
1074 paths,
1075 no_truncate,
1076 context,
1077 all,
1078 force,
1079 dependencies,
1080 }) => {
1081 // If no pattern provided, launch interactive mode (REF-68: require TTY)
1082 match pattern {
1083 None => {
1084 use crossterm::tty::IsTty;
1085 if !std::io::stdin().is_tty() {
1086 eprintln!("error: interactive mode requires a terminal (TTY).");
1087 eprintln!("Use 'rfx query <pattern>' for non-interactive search.");
1088 std::process::exit(1);
1089 }
1090 query::handle_interactive()
1091 }
1092 Some(pattern) => query::handle_query(
1093 pattern,
1094 symbols,
1095 lang,
1096 kind,
1097 ast,
1098 regex,
1099 json,
1100 pretty,
1101 ai,
1102 limit,
1103 offset,
1104 expand,
1105 file,
1106 exact,
1107 contains,
1108 count,
1109 timeout,
1110 plain,
1111 glob,
1112 exclude,
1113 paths,
1114 no_truncate,
1115 context,
1116 all,
1117 force,
1118 dependencies,
1119 ),
1120 }
1121 }
1122 Some(Command::Serve { port, host }) => serve::handle_serve(port, host),
1123 Some(Command::Stats { json, pretty }) => misc::handle_stats(json, pretty),
1124 Some(Command::Clear { yes }) => misc::handle_clear(yes),
1125 Some(Command::ListFiles {
1126 json,
1127 pretty,
1128 lang,
1129 glob,
1130 }) => misc::handle_list_files(json, pretty, lang, glob),
1131 Some(Command::Watch {
1132 path,
1133 debounce,
1134 quiet,
1135 }) => watch::handle_watch(path, debounce, quiet),
1136 Some(Command::Mcp) => misc::handle_mcp(),
1137 Some(Command::Analyze {
1138 circular,
1139 hotspots,
1140 min_dependents,
1141 unused,
1142 islands,
1143 min_island_size,
1144 max_island_size,
1145 format,
1146 json,
1147 pretty,
1148 count,
1149 all,
1150 plain,
1151 glob,
1152 exclude,
1153 force,
1154 limit,
1155 offset,
1156 sort,
1157 }) => deps::handle_analyze(
1158 circular,
1159 hotspots,
1160 min_dependents,
1161 unused,
1162 islands,
1163 min_island_size,
1164 max_island_size,
1165 format,
1166 json,
1167 pretty,
1168 count,
1169 all,
1170 plain,
1171 glob,
1172 exclude,
1173 force,
1174 limit,
1175 offset,
1176 sort,
1177 ),
1178 Some(Command::Deps {
1179 file,
1180 reverse,
1181 depth,
1182 format,
1183 json,
1184 pretty,
1185 }) => deps::handle_deps(file, reverse, depth, format, json, pretty),
1186 Some(Command::Ask {
1187 question,
1188 execute,
1189 provider,
1190 json,
1191 pretty,
1192 additional_context,
1193 configure,
1194 agentic,
1195 max_iterations,
1196 no_eval,
1197 show_reasoning,
1198 verbose,
1199 quiet,
1200 answer,
1201 interactive,
1202 debug,
1203 }) => ask::handle_ask(
1204 question,
1205 execute,
1206 provider,
1207 json,
1208 pretty,
1209 additional_context,
1210 configure,
1211 agentic,
1212 max_iterations,
1213 no_eval,
1214 show_reasoning,
1215 verbose,
1216 quiet,
1217 answer,
1218 interactive,
1219 debug,
1220 ),
1221 Some(Command::Context {
1222 structure,
1223 path,
1224 file_types,
1225 project_type,
1226 framework,
1227 entry_points,
1228 test_layout,
1229 config_files,
1230 depth,
1231 json,
1232 }) => misc::handle_context(
1233 structure,
1234 path,
1235 file_types,
1236 project_type,
1237 framework,
1238 entry_points,
1239 test_layout,
1240 config_files,
1241 depth,
1242 json,
1243 ),
1244 Some(Command::IndexSymbolsInternal { cache_dir }) => {
1245 index::handle_index_symbols_internal(cache_dir)
1246 }
1247 Some(Command::Snapshot { command }) => match command {
1248 None => snapshot::handle_snapshot_create(),
1249 Some(SnapshotSubcommand::List { json, pretty }) => {
1250 snapshot::handle_snapshot_list(json, pretty)
1251 }
1252 Some(SnapshotSubcommand::Diff {
1253 baseline,
1254 current,
1255 json,
1256 pretty,
1257 }) => snapshot::handle_snapshot_diff(baseline, current, json, pretty),
1258 Some(SnapshotSubcommand::Gc { json }) => snapshot::handle_snapshot_gc(json),
1259 },
1260 Some(Command::Pulse { command }) => match command {
1261 PulseSubcommand::Changelog {
1262 count,
1263 no_llm,
1264 json,
1265 pretty,
1266 } => pulse::handle_pulse_changelog(count, no_llm, json, pretty),
1267 PulseSubcommand::Wiki {
1268 no_llm,
1269 output,
1270 json,
1271 } => pulse::handle_pulse_wiki(no_llm, output, json),
1272 PulseSubcommand::Map {
1273 format,
1274 output,
1275 zoom,
1276 } => pulse::handle_pulse_map(format, output, zoom),
1277 PulseSubcommand::Generate {
1278 output,
1279 base_url,
1280 title,
1281 include,
1282 no_llm,
1283 clean,
1284 force_renarrate,
1285 concurrency,
1286 depth,
1287 min_files,
1288 } => pulse::handle_pulse_generate(
1289 output,
1290 base_url,
1291 title,
1292 include,
1293 no_llm,
1294 clean,
1295 force_renarrate,
1296 concurrency,
1297 depth,
1298 min_files,
1299 ),
1300 PulseSubcommand::Serve { output, port, open } => {
1301 pulse::handle_pulse_serve(output, port, open)
1302 }
1303 PulseSubcommand::Onboard { no_llm, json } => {
1304 pulse::handle_pulse_onboard(no_llm, json)
1305 }
1306 PulseSubcommand::Timeline { json } => pulse::handle_pulse_timeline(json),
1307 PulseSubcommand::Glossary { json } => pulse::handle_pulse_glossary(json),
1308 },
1309 Some(Command::Llm { command }) => match command {
1310 LlmSubcommand::Config => llm::handle_llm_config(),
1311 LlmSubcommand::Status => llm::handle_llm_status(),
1312 },
1313 }
1314 }
1315}