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::Mcp => {
949 log::debug!("Skipping compaction for Mcp command");
950 return;
951 }
952 Command::Watch { .. } => {
953 log::debug!("Skipping compaction for Watch command");
954 return;
955 }
956 Command::Serve { .. } => {
957 log::debug!("Skipping compaction for Serve command");
958 return;
959 }
960 _ => {}
961 }
962
963 // Check if compaction should run
964 let should_compact = match cache.should_compact() {
965 Ok(true) => true,
966 Ok(false) => {
967 log::debug!("Compaction not needed yet (last run <24h ago)");
968 return;
969 }
970 Err(e) => {
971 log::warn!("Failed to check compaction status: {}", e);
972 return;
973 }
974 };
975
976 if !should_compact {
977 return;
978 }
979
980 log::info!("Starting background cache compaction...");
981
982 // Clone cache path for background thread
983 let cache_path = cache.path().to_path_buf();
984
985 // Spawn background thread for compaction
986 std::thread::spawn(move || {
987 let cache = CacheManager::new(
988 cache_path
989 .parent()
990 .expect("Cache should have parent directory"),
991 );
992
993 match cache.compact() {
994 Ok(report) => {
995 log::info!(
996 "Background compaction completed: {} files removed, {:.2} MB saved, took {}ms",
997 report.files_removed,
998 report.space_saved_bytes as f64 / 1_048_576.0,
999 report.duration_ms
1000 );
1001 }
1002 Err(e) => {
1003 log::warn!("Background compaction failed: {}", e);
1004 }
1005 }
1006 });
1007
1008 log::debug!("Background compaction thread spawned - main command continuing");
1009}
1010
1011impl Cli {
1012 /// Execute the CLI command
1013 pub fn execute(self) -> Result<()> {
1014 // Setup logging based on verbosity
1015 let log_level = match self.verbose {
1016 0 => "warn", // Default: only warnings and errors
1017 1 => "info", // -v: show info messages
1018 2 => "debug", // -vv: show debug messages
1019 _ => "trace", // -vvv: show trace messages
1020 };
1021 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
1022 .init();
1023
1024 // Try background compaction (non-blocking) before command execution
1025 if let Some(ref command) = self.command {
1026 // Use current directory as default cache location
1027 let cache = CacheManager::new(".");
1028 try_background_compact(&cache, command);
1029 }
1030
1031 // Execute the subcommand, or show help if no command provided
1032 match self.command {
1033 None => {
1034 // No subcommand: show help
1035 Cli::command().print_help()?;
1036 println!(); // Add newline after help
1037 Ok(())
1038 }
1039 Some(Command::Index {
1040 path,
1041 force,
1042 languages,
1043 quiet,
1044 command,
1045 }) => {
1046 match command {
1047 None => {
1048 // Default: run index build
1049 index::handle_index_build(&path, &force, &languages, &quiet)
1050 }
1051 Some(IndexSubcommand::Status) => index::handle_index_status(),
1052 Some(IndexSubcommand::Compact { json, pretty }) => {
1053 index::handle_index_compact(&json, &pretty)
1054 }
1055 }
1056 }
1057 Some(Command::Query {
1058 pattern,
1059 symbols,
1060 lang,
1061 kind,
1062 ast,
1063 regex,
1064 json,
1065 pretty,
1066 ai,
1067 limit,
1068 offset,
1069 expand,
1070 file,
1071 exact,
1072 contains,
1073 count,
1074 timeout,
1075 plain,
1076 glob,
1077 exclude,
1078 paths,
1079 no_truncate,
1080 context,
1081 all,
1082 force,
1083 dependencies,
1084 }) => {
1085 // If no pattern provided, launch interactive mode (REF-68: require TTY)
1086 match pattern {
1087 None => {
1088 use crossterm::tty::IsTty;
1089 if !std::io::stdin().is_tty() {
1090 eprintln!("error: interactive mode requires a terminal (TTY).");
1091 eprintln!("Use 'rfx query <pattern>' for non-interactive search.");
1092 std::process::exit(1);
1093 }
1094 query::handle_interactive()
1095 }
1096 Some(pattern) => query::handle_query(
1097 pattern,
1098 symbols,
1099 lang,
1100 kind,
1101 ast,
1102 regex,
1103 json,
1104 pretty,
1105 ai,
1106 limit,
1107 offset,
1108 expand,
1109 file,
1110 exact,
1111 contains,
1112 count,
1113 timeout,
1114 plain,
1115 glob,
1116 exclude,
1117 paths,
1118 no_truncate,
1119 context,
1120 all,
1121 force,
1122 dependencies,
1123 ),
1124 }
1125 }
1126 Some(Command::Serve { port, host }) => serve::handle_serve(port, host),
1127 Some(Command::Stats { json, pretty }) => misc::handle_stats(json, pretty),
1128 Some(Command::Clear { yes }) => misc::handle_clear(yes),
1129 Some(Command::ListFiles {
1130 json,
1131 pretty,
1132 lang,
1133 glob,
1134 }) => misc::handle_list_files(json, pretty, lang, glob),
1135 Some(Command::Watch {
1136 path,
1137 debounce,
1138 quiet,
1139 }) => watch::handle_watch(path, debounce, quiet),
1140 Some(Command::Mcp) => misc::handle_mcp(),
1141 Some(Command::Analyze {
1142 circular,
1143 hotspots,
1144 min_dependents,
1145 unused,
1146 islands,
1147 min_island_size,
1148 max_island_size,
1149 format,
1150 json,
1151 pretty,
1152 count,
1153 all,
1154 plain,
1155 glob,
1156 exclude,
1157 force,
1158 limit,
1159 offset,
1160 sort,
1161 }) => deps::handle_analyze(
1162 circular,
1163 hotspots,
1164 min_dependents,
1165 unused,
1166 islands,
1167 min_island_size,
1168 max_island_size,
1169 format,
1170 json,
1171 pretty,
1172 count,
1173 all,
1174 plain,
1175 glob,
1176 exclude,
1177 force,
1178 limit,
1179 offset,
1180 sort,
1181 ),
1182 Some(Command::Deps {
1183 file,
1184 reverse,
1185 depth,
1186 format,
1187 json,
1188 pretty,
1189 }) => deps::handle_deps(file, reverse, depth, format, json, pretty),
1190 Some(Command::Ask {
1191 question,
1192 execute,
1193 provider,
1194 json,
1195 pretty,
1196 additional_context,
1197 configure,
1198 agentic,
1199 max_iterations,
1200 no_eval,
1201 show_reasoning,
1202 verbose,
1203 quiet,
1204 answer,
1205 interactive,
1206 debug,
1207 }) => ask::handle_ask(
1208 question,
1209 execute,
1210 provider,
1211 json,
1212 pretty,
1213 additional_context,
1214 configure,
1215 agentic,
1216 max_iterations,
1217 no_eval,
1218 show_reasoning,
1219 verbose,
1220 quiet,
1221 answer,
1222 interactive,
1223 debug,
1224 ),
1225 Some(Command::Context {
1226 structure,
1227 path,
1228 file_types,
1229 project_type,
1230 framework,
1231 entry_points,
1232 test_layout,
1233 config_files,
1234 depth,
1235 json,
1236 }) => misc::handle_context(
1237 structure,
1238 path,
1239 file_types,
1240 project_type,
1241 framework,
1242 entry_points,
1243 test_layout,
1244 config_files,
1245 depth,
1246 json,
1247 ),
1248 Some(Command::IndexSymbolsInternal { cache_dir }) => {
1249 index::handle_index_symbols_internal(cache_dir)
1250 }
1251 Some(Command::Snapshot { command }) => match command {
1252 None => snapshot::handle_snapshot_create(),
1253 Some(SnapshotSubcommand::List { json, pretty }) => {
1254 snapshot::handle_snapshot_list(json, pretty)
1255 }
1256 Some(SnapshotSubcommand::Diff {
1257 baseline,
1258 current,
1259 json,
1260 pretty,
1261 }) => snapshot::handle_snapshot_diff(baseline, current, json, pretty),
1262 Some(SnapshotSubcommand::Gc { json }) => snapshot::handle_snapshot_gc(json),
1263 },
1264 Some(Command::Pulse { command }) => match command {
1265 PulseSubcommand::Changelog {
1266 count,
1267 no_llm,
1268 json,
1269 pretty,
1270 } => pulse::handle_pulse_changelog(count, no_llm, json, pretty),
1271 PulseSubcommand::Wiki {
1272 no_llm,
1273 output,
1274 json,
1275 } => pulse::handle_pulse_wiki(no_llm, output, json),
1276 PulseSubcommand::Map {
1277 format,
1278 output,
1279 zoom,
1280 } => pulse::handle_pulse_map(format, output, zoom),
1281 PulseSubcommand::Generate {
1282 output,
1283 base_url,
1284 title,
1285 include,
1286 no_llm,
1287 clean,
1288 force_renarrate,
1289 concurrency,
1290 depth,
1291 min_files,
1292 } => pulse::handle_pulse_generate(
1293 output,
1294 base_url,
1295 title,
1296 include,
1297 no_llm,
1298 clean,
1299 force_renarrate,
1300 concurrency,
1301 depth,
1302 min_files,
1303 ),
1304 PulseSubcommand::Serve { output, port, open } => {
1305 pulse::handle_pulse_serve(output, port, open)
1306 }
1307 PulseSubcommand::Onboard { no_llm, json } => {
1308 pulse::handle_pulse_onboard(no_llm, json)
1309 }
1310 PulseSubcommand::Timeline { json } => pulse::handle_pulse_timeline(json),
1311 PulseSubcommand::Glossary { json } => pulse::handle_pulse_glossary(json),
1312 },
1313 Some(Command::Llm { command }) => match command {
1314 LlmSubcommand::Config => llm::handle_llm_config(),
1315 LlmSubcommand::Status => llm::handle_llm_status(),
1316 },
1317 }
1318 }
1319}