1use anyhow::{Context, Result};
4use clap::{CommandFactory, Parser, Subcommand};
5use std::path::PathBuf;
6use std::sync::{Arc, Mutex};
7use std::time::Instant;
8use indicatif::{ProgressBar, ProgressStyle};
9use owo_colors::OwoColorize;
10
11use crate::cache::CacheManager;
12use crate::indexer::Indexer;
13use crate::models::{IndexConfig, Language};
14use crate::output;
15use crate::query::{QueryEngine, QueryFilter};
16
17#[derive(Parser, Debug)]
19#[command(
20 name = "rfx",
21 version,
22 about = "A fast, deterministic code search engine built for AI",
23 long_about = "Reflex is a local-first, structure-aware code search engine that returns \
24 structured results (symbols, spans, scopes) with sub-100ms latency. \
25 Designed for AI coding agents and automation."
26)]
27pub struct Cli {
28 #[arg(short, long, action = clap::ArgAction::Count)]
30 pub verbose: u8,
31
32 #[command(subcommand)]
33 pub command: Option<Command>,
34}
35
36#[derive(Subcommand, Debug)]
37pub enum IndexSubcommand {
38 Status,
40
41 Compact {
51 #[arg(long)]
53 json: bool,
54
55 #[arg(long)]
57 pretty: bool,
58 },
59}
60
61#[derive(Subcommand, Debug)]
62pub enum Command {
63 Index {
65 #[arg(value_name = "PATH", default_value = ".")]
67 path: PathBuf,
68
69 #[arg(short, long)]
71 force: bool,
72
73 #[arg(short, long, value_delimiter = ',')]
75 languages: Vec<String>,
76
77 #[arg(short, long)]
79 quiet: bool,
80
81 #[command(subcommand)]
83 command: Option<IndexSubcommand>,
84 },
85
86 Query {
110 pattern: Option<String>,
112
113 #[arg(short, long)]
115 symbols: bool,
116
117 #[arg(short, long)]
120 lang: Option<String>,
121
122 #[arg(short, long)]
125 kind: Option<String>,
126
127 #[arg(long)]
142 ast: bool,
143
144 #[arg(short = 'r', long)]
160 regex: bool,
161
162 #[arg(long)]
164 json: bool,
165
166 #[arg(long)]
169 pretty: bool,
170
171 #[arg(long)]
175 ai: bool,
176
177 #[arg(short = 'n', long)]
179 limit: Option<usize>,
180
181 #[arg(short = 'o', long)]
184 offset: Option<usize>,
185
186 #[arg(long)]
189 expand: bool,
190
191 #[arg(short = 'f', long)]
194 file: Option<String>,
195
196 #[arg(long)]
199 exact: bool,
200
201 #[arg(long)]
216 contains: bool,
217
218 #[arg(short, long)]
220 count: bool,
221
222 #[arg(short = 't', long, default_value = "30")]
224 timeout: u64,
225
226 #[arg(long)]
228 plain: bool,
229
230 #[arg(short = 'g', long)]
244 glob: Vec<String>,
245
246 #[arg(short = 'x', long)]
255 exclude: Vec<String>,
256
257 #[arg(short = 'p', long)]
260 paths: bool,
261
262 #[arg(long)]
265 no_truncate: bool,
266
267 #[arg(short = 'a', long)]
270 all: bool,
271
272 #[arg(long)]
278 force: bool,
279
280 #[arg(long)]
283 dependencies: bool,
284 },
285
286 Serve {
288 #[arg(short, long, default_value = "7878")]
290 port: u16,
291
292 #[arg(long, default_value = "127.0.0.1")]
294 host: String,
295 },
296
297 Stats {
299 #[arg(long)]
301 json: bool,
302
303 #[arg(long)]
305 pretty: bool,
306 },
307
308 Clear {
310 #[arg(short, long)]
312 yes: bool,
313 },
314
315 ListFiles {
317 #[arg(long)]
319 json: bool,
320
321 #[arg(long)]
323 pretty: bool,
324 },
325
326 Watch {
335 #[arg(value_name = "PATH", default_value = ".")]
337 path: PathBuf,
338
339 #[arg(short, long, default_value = "15000")]
343 debounce: u64,
344
345 #[arg(short, long)]
347 quiet: bool,
348 },
349
350 Mcp,
367
368 Analyze {
384 #[arg(long)]
386 circular: bool,
387
388 #[arg(long)]
390 hotspots: bool,
391
392 #[arg(long, default_value = "2", requires = "hotspots")]
394 min_dependents: usize,
395
396 #[arg(long)]
398 unused: bool,
399
400 #[arg(long)]
402 islands: bool,
403
404 #[arg(long, default_value = "2", requires = "islands")]
406 min_island_size: usize,
407
408 #[arg(long, requires = "islands")]
410 max_island_size: Option<usize>,
411
412 #[arg(short = 'f', long, default_value = "tree")]
414 format: String,
415
416 #[arg(long)]
418 json: bool,
419
420 #[arg(long)]
422 pretty: bool,
423
424 #[arg(short, long)]
426 count: bool,
427
428 #[arg(short = 'a', long)]
431 all: bool,
432
433 #[arg(long)]
435 plain: bool,
436
437 #[arg(short = 'g', long)]
440 glob: Vec<String>,
441
442 #[arg(short = 'x', long)]
445 exclude: Vec<String>,
446
447 #[arg(long)]
450 force: bool,
451
452 #[arg(short = 'n', long)]
454 limit: Option<usize>,
455
456 #[arg(short = 'o', long)]
458 offset: Option<usize>,
459
460 #[arg(long)]
464 sort: Option<String>,
465 },
466
467 Deps {
477 file: PathBuf,
479
480 #[arg(short, long)]
482 reverse: bool,
483
484 #[arg(short, long, default_value = "1")]
486 depth: usize,
487
488 #[arg(short = 'f', long, default_value = "tree")]
490 format: String,
491
492 #[arg(long)]
494 json: bool,
495
496 #[arg(long)]
498 pretty: bool,
499 },
500
501 Ask {
527 question: Option<String>,
529
530 #[arg(short, long)]
532 execute: bool,
533
534 #[arg(short, long)]
536 provider: Option<String>,
537
538 #[arg(long)]
540 json: bool,
541
542 #[arg(long)]
544 pretty: bool,
545
546 #[arg(long)]
548 additional_context: Option<String>,
549
550 #[arg(long)]
552 configure: bool,
553
554 #[arg(long)]
556 agentic: bool,
557
558 #[arg(long, default_value = "2")]
560 max_iterations: usize,
561
562 #[arg(long)]
564 no_eval: bool,
565
566 #[arg(long)]
568 show_reasoning: bool,
569
570 #[arg(long)]
572 verbose: bool,
573
574 #[arg(long)]
576 quiet: bool,
577
578 #[arg(long)]
580 answer: bool,
581
582 #[arg(short = 'i', long)]
584 interactive: bool,
585
586 #[arg(long)]
588 debug: bool,
589 },
590
591 Context {
608 #[arg(long)]
610 structure: bool,
611
612 #[arg(short, long)]
614 path: Option<String>,
615
616 #[arg(long)]
618 file_types: bool,
619
620 #[arg(long)]
622 project_type: bool,
623
624 #[arg(long)]
626 framework: bool,
627
628 #[arg(long)]
630 entry_points: bool,
631
632 #[arg(long)]
634 test_layout: bool,
635
636 #[arg(long)]
638 config_files: bool,
639
640 #[arg(long, default_value = "1")]
642 depth: usize,
643
644 #[arg(long)]
646 json: bool,
647 },
648
649 #[command(hide = true)]
651 IndexSymbolsInternal {
652 cache_dir: PathBuf,
654 },
655}
656
657fn try_background_compact(cache: &CacheManager, command: &Command) {
669 match command {
671 Command::Clear { .. } => {
672 log::debug!("Skipping compaction for Clear command");
673 return;
674 }
675 Command::Mcp => {
676 log::debug!("Skipping compaction for Mcp command");
677 return;
678 }
679 Command::Watch { .. } => {
680 log::debug!("Skipping compaction for Watch command");
681 return;
682 }
683 Command::Serve { .. } => {
684 log::debug!("Skipping compaction for Serve command");
685 return;
686 }
687 _ => {}
688 }
689
690 let should_compact = match cache.should_compact() {
692 Ok(true) => true,
693 Ok(false) => {
694 log::debug!("Compaction not needed yet (last run <24h ago)");
695 return;
696 }
697 Err(e) => {
698 log::warn!("Failed to check compaction status: {}", e);
699 return;
700 }
701 };
702
703 if !should_compact {
704 return;
705 }
706
707 log::info!("Starting background cache compaction...");
708
709 let cache_path = cache.path().to_path_buf();
711
712 std::thread::spawn(move || {
714 let cache = CacheManager::new(cache_path.parent().expect("Cache should have parent directory"));
715
716 match cache.compact() {
717 Ok(report) => {
718 log::info!(
719 "Background compaction completed: {} files removed, {:.2} MB saved, took {}ms",
720 report.files_removed,
721 report.space_saved_bytes as f64 / 1_048_576.0,
722 report.duration_ms
723 );
724 }
725 Err(e) => {
726 log::warn!("Background compaction failed: {}", e);
727 }
728 }
729 });
730
731 log::debug!("Background compaction thread spawned - main command continuing");
732}
733
734impl Cli {
735 pub fn execute(self) -> Result<()> {
737 let log_level = match self.verbose {
739 0 => "warn", 1 => "info", 2 => "debug", _ => "trace", };
744 env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
745 .init();
746
747 if let Some(ref command) = self.command {
749 let cache = CacheManager::new(".");
751 try_background_compact(&cache, command);
752 }
753
754 match self.command {
756 None => {
757 Cli::command().print_help()?;
759 println!(); Ok(())
761 }
762 Some(Command::Index { path, force, languages, quiet, command }) => {
763 match command {
764 None => {
765 handle_index_build(&path, &force, &languages, &quiet)
767 }
768 Some(IndexSubcommand::Status) => {
769 handle_index_status()
770 }
771 Some(IndexSubcommand::Compact { json, pretty }) => {
772 handle_index_compact(&json, &pretty)
773 }
774 }
775 }
776 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 }) => {
777 match pattern {
779 None => handle_interactive(),
780 Some(pattern) => 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)
781 }
782 }
783 Some(Command::Serve { port, host }) => {
784 handle_serve(port, host)
785 }
786 Some(Command::Stats { json, pretty }) => {
787 handle_stats(json, pretty)
788 }
789 Some(Command::Clear { yes }) => {
790 handle_clear(yes)
791 }
792 Some(Command::ListFiles { json, pretty }) => {
793 handle_list_files(json, pretty)
794 }
795 Some(Command::Watch { path, debounce, quiet }) => {
796 handle_watch(path, debounce, quiet)
797 }
798 Some(Command::Mcp) => {
799 handle_mcp()
800 }
801 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 }) => {
802 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)
803 }
804 Some(Command::Deps { file, reverse, depth, format, json, pretty }) => {
805 handle_deps(file, reverse, depth, format, json, pretty)
806 }
807 Some(Command::Ask { question, execute, provider, json, pretty, additional_context, configure, agentic, max_iterations, no_eval, show_reasoning, verbose, quiet, answer, interactive, debug }) => {
808 handle_ask(question, execute, provider, json, pretty, additional_context, configure, agentic, max_iterations, no_eval, show_reasoning, verbose, quiet, answer, interactive, debug)
809 }
810 Some(Command::Context { structure, path, file_types, project_type, framework, entry_points, test_layout, config_files, depth, json }) => {
811 handle_context(structure, path, file_types, project_type, framework, entry_points, test_layout, config_files, depth, json)
812 }
813 Some(Command::IndexSymbolsInternal { cache_dir }) => {
814 handle_index_symbols_internal(cache_dir)
815 }
816 }
817 }
818}
819
820fn handle_index_status() -> Result<()> {
822 log::info!("Checking background symbol indexing status");
823
824 let cache = CacheManager::new(".");
825 let cache_path = cache.path().to_path_buf();
826
827 match crate::background_indexer::BackgroundIndexer::get_status(&cache_path) {
828 Ok(Some(status)) => {
829 println!("Background Symbol Indexing Status");
830 println!("==================================");
831 println!("State: {:?}", status.state);
832 println!("Total files: {}", status.total_files);
833 println!("Processed: {}", status.processed_files);
834 println!("Cached: {}", status.cached_files);
835 println!("Parsed: {}", status.parsed_files);
836 println!("Failed: {}", status.failed_files);
837 println!("Started: {}", status.started_at);
838 println!("Last updated: {}", status.updated_at);
839
840 if let Some(completed_at) = &status.completed_at {
841 println!("Completed: {}", completed_at);
842 }
843
844 if let Some(error) = &status.error {
845 println!("Error: {}", error);
846 }
847
848 if status.state == crate::background_indexer::IndexerState::Running && status.total_files > 0 {
850 let progress = (status.processed_files as f64 / status.total_files as f64) * 100.0;
851 println!("\nProgress: {:.1}%", progress);
852 }
853
854 Ok(())
855 }
856 Ok(None) => {
857 println!("No background symbol indexing in progress.");
858 println!("\nRun 'rfx index' to start background symbol indexing.");
859 Ok(())
860 }
861 Err(e) => {
862 anyhow::bail!("Failed to get indexing status: {}", e);
863 }
864 }
865 }
866
867fn handle_index_compact(json: &bool, pretty: &bool) -> Result<()> {
869 log::info!("Running cache compaction");
870
871 let cache = CacheManager::new(".");
872 let report = cache.compact()?;
873
874 if *json {
876 let json_str = if *pretty {
877 serde_json::to_string_pretty(&report)?
878 } else {
879 serde_json::to_string(&report)?
880 };
881 println!("{}", json_str);
882 } else {
883 println!("Cache Compaction Complete");
884 println!("=========================");
885 println!("Files removed: {}", report.files_removed);
886 println!("Space saved: {:.2} MB", report.space_saved_bytes as f64 / 1_048_576.0);
887 println!("Duration: {}ms", report.duration_ms);
888 }
889
890 Ok(())
891}
892
893fn handle_index_build(path: &PathBuf, force: &bool, languages: &[String], quiet: &bool) -> Result<()> {
894 log::info!("Starting index build");
895
896 let cache = CacheManager::new(path);
897 let cache_path = cache.path().to_path_buf();
898
899 if *force {
900 log::info!("Force rebuild requested, clearing existing cache");
901 cache.clear()?;
902 }
903
904 let lang_filters: Vec<Language> = languages
906 .iter()
907 .filter_map(|s| match s.to_lowercase().as_str() {
908 "rust" | "rs" => Some(Language::Rust),
909 "python" | "py" => Some(Language::Python),
910 "javascript" | "js" => Some(Language::JavaScript),
911 "typescript" | "ts" => Some(Language::TypeScript),
912 "go" => Some(Language::Go),
913 "java" => Some(Language::Java),
914 "php" => Some(Language::PHP),
915 "c" => Some(Language::C),
916 "cpp" | "c++" => Some(Language::Cpp),
917 _ => {
918 output::warn(&format!("Unknown language: {}", s));
919 None
920 }
921 })
922 .collect();
923
924 let config = IndexConfig {
925 languages: lang_filters,
926 ..Default::default()
927 };
928
929 let indexer = Indexer::new(cache, config);
930 let show_progress = !quiet;
932 let stats = indexer.index(path, show_progress)?;
933
934 if !quiet {
936 println!("Indexing complete!");
937 println!(" Files indexed: {}", stats.total_files);
938 println!(" Cache size: {}", format_bytes(stats.index_size_bytes));
939 println!(" Last updated: {}", stats.last_updated);
940
941 if !stats.files_by_language.is_empty() {
943 println!("\nFiles by language:");
944
945 let mut lang_vec: Vec<_> = stats.files_by_language.iter().collect();
947 lang_vec.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
948
949 let max_lang_len = lang_vec.iter().map(|(lang, _)| lang.len()).max().unwrap_or(8);
951 let lang_width = max_lang_len.max(8); println!(" {:<width$} Files Lines", "Language", width = lang_width);
955 println!(" {} ----- -------", "-".repeat(lang_width));
956
957 for (language, file_count) in lang_vec {
959 let line_count = stats.lines_by_language.get(language).copied().unwrap_or(0);
960 println!(" {:<width$} {:5} {:7}",
961 language, file_count, line_count,
962 width = lang_width);
963 }
964 }
965 }
966
967 if !crate::background_indexer::BackgroundIndexer::is_running(&cache_path) {
969 if !quiet {
970 println!("\nStarting background symbol indexing...");
971 println!(" Symbols will be cached for faster queries");
972 println!(" Check status with: rfx index status");
973 }
974
975 let current_exe = std::env::current_exe()
978 .context("Failed to get current executable path")?;
979
980 #[cfg(unix)]
981 {
982 std::process::Command::new(¤t_exe)
983 .arg("index-symbols-internal")
984 .arg(path)
985 .stdin(std::process::Stdio::null())
986 .stdout(std::process::Stdio::null())
987 .stderr(std::process::Stdio::null())
988 .spawn()
989 .context("Failed to spawn background indexing process")?;
990 }
991
992 #[cfg(windows)]
993 {
994 use std::os::windows::process::CommandExt;
995 const CREATE_NO_WINDOW: u32 = 0x08000000;
996
997 std::process::Command::new(¤t_exe)
998 .arg("index-symbols-internal")
999 .arg(&path)
1000 .creation_flags(CREATE_NO_WINDOW)
1001 .stdin(std::process::Stdio::null())
1002 .stdout(std::process::Stdio::null())
1003 .stderr(std::process::Stdio::null())
1004 .spawn()
1005 .context("Failed to spawn background indexing process")?;
1006 }
1007
1008 log::debug!("Spawned background symbol indexing process");
1009 } else if !quiet {
1010 println!("\n⚠️ Background symbol indexing already in progress");
1011 println!(" Check status with: rfx index status");
1012 }
1013
1014 Ok(())
1015}
1016
1017fn format_bytes(bytes: u64) -> String {
1019 const KB: u64 = 1024;
1020 const MB: u64 = KB * 1024;
1021 const GB: u64 = MB * 1024;
1022 const TB: u64 = GB * 1024;
1023
1024 if bytes >= TB {
1025 format!("{:.2} TB", bytes as f64 / TB as f64)
1026 } else if bytes >= GB {
1027 format!("{:.2} GB", bytes as f64 / GB as f64)
1028 } else if bytes >= MB {
1029 format!("{:.2} MB", bytes as f64 / MB as f64)
1030 } else if bytes >= KB {
1031 format!("{:.2} KB", bytes as f64 / KB as f64)
1032 } else {
1033 format!("{} bytes", bytes)
1034 }
1035}
1036
1037pub fn truncate_preview(preview: &str, max_length: usize) -> String {
1040 if preview.len() <= max_length {
1041 return preview.to_string();
1042 }
1043
1044 let truncate_at = preview.char_indices()
1046 .take(max_length)
1047 .filter(|(_, c)| c.is_whitespace())
1048 .last()
1049 .map(|(i, _)| i)
1050 .unwrap_or(max_length.min(preview.len()));
1051
1052 let mut truncated = preview[..truncate_at].to_string();
1053 truncated.push('…');
1054 truncated
1055}
1056
1057fn handle_query(
1059 pattern: String,
1060 symbols_flag: bool,
1061 lang: Option<String>,
1062 kind_str: Option<String>,
1063 use_ast: bool,
1064 use_regex: bool,
1065 as_json: bool,
1066 pretty_json: bool,
1067 ai_mode: bool,
1068 limit: Option<usize>,
1069 offset: Option<usize>,
1070 expand: bool,
1071 file_pattern: Option<String>,
1072 exact: bool,
1073 use_contains: bool,
1074 count_only: bool,
1075 timeout_secs: u64,
1076 plain: bool,
1077 glob_patterns: Vec<String>,
1078 exclude_patterns: Vec<String>,
1079 paths_only: bool,
1080 no_truncate: bool,
1081 all: bool,
1082 force: bool,
1083 include_dependencies: bool,
1084) -> Result<()> {
1085 log::info!("Starting query command");
1086
1087 let as_json = as_json || ai_mode;
1089
1090 let cache = CacheManager::new(".");
1091 let engine = QueryEngine::new(cache);
1092
1093 let language = if let Some(lang_str) = lang.as_deref() {
1095 match lang_str.to_lowercase().as_str() {
1096 "rust" | "rs" => Some(Language::Rust),
1097 "python" | "py" => Some(Language::Python),
1098 "javascript" | "js" => Some(Language::JavaScript),
1099 "typescript" | "ts" => Some(Language::TypeScript),
1100 "vue" => Some(Language::Vue),
1101 "svelte" => Some(Language::Svelte),
1102 "go" => Some(Language::Go),
1103 "java" => Some(Language::Java),
1104 "php" => Some(Language::PHP),
1105 "c" => Some(Language::C),
1106 "cpp" | "c++" => Some(Language::Cpp),
1107 "csharp" | "cs" | "c#" => Some(Language::CSharp),
1108 "ruby" | "rb" => Some(Language::Ruby),
1109 "kotlin" | "kt" => Some(Language::Kotlin),
1110 "zig" => Some(Language::Zig),
1111 _ => {
1112 anyhow::bail!(
1113 "Unknown language: '{}'\n\
1114 \n\
1115 Supported languages:\n\
1116 • rust, rs\n\
1117 • python, py\n\
1118 • javascript, js\n\
1119 • typescript, ts\n\
1120 • vue\n\
1121 • svelte\n\
1122 • go\n\
1123 • java\n\
1124 • php\n\
1125 • c\n\
1126 • c++, cpp\n\
1127 • c#, csharp, cs\n\
1128 • ruby, rb\n\
1129 • kotlin, kt\n\
1130 • zig\n\
1131 \n\
1132 Example: rfx query \"pattern\" --lang rust",
1133 lang_str
1134 );
1135 }
1136 }
1137 } else {
1138 None
1139 };
1140
1141 let kind = kind_str.as_deref().and_then(|s| {
1143 let capitalized = {
1145 let mut chars = s.chars();
1146 match chars.next() {
1147 None => String::new(),
1148 Some(first) => first.to_uppercase().chain(chars.flat_map(|c| c.to_lowercase())).collect(),
1149 }
1150 };
1151
1152 capitalized.parse::<crate::models::SymbolKind>()
1153 .ok()
1154 .or_else(|| {
1155 log::debug!("Treating '{}' as unknown symbol kind for filtering", s);
1157 Some(crate::models::SymbolKind::Unknown(s.to_string()))
1158 })
1159 });
1160
1161 let symbols_mode = symbols_flag || kind.is_some();
1163
1164 let final_limit = if count_only {
1172 None } else if all {
1174 None } else if limit == Some(0) {
1176 None } else if paths_only && limit.is_none() {
1178 None } else if let Some(user_limit) = limit {
1180 Some(user_limit) } else {
1182 Some(100) };
1184
1185 if use_ast && language.is_none() {
1187 anyhow::bail!(
1188 "AST pattern matching requires a language to be specified.\n\
1189 \n\
1190 Use --lang to specify the language for tree-sitter parsing.\n\
1191 \n\
1192 Supported languages for AST queries:\n\
1193 • rust, python, go, java, c, c++, c#, php, ruby, kotlin, zig, typescript, javascript\n\
1194 \n\
1195 Note: Vue and Svelte use line-based parsing and do not support AST queries.\n\
1196 \n\
1197 WARNING: AST queries are SLOW (500ms-2s+). Use --symbols instead for 95% of cases.\n\
1198 \n\
1199 Examples:\n\
1200 • rfx query \"(function_definition) @fn\" --ast --lang python\n\
1201 • rfx query \"(class_declaration) @class\" --ast --lang typescript --glob \"src/**/*.ts\""
1202 );
1203 }
1204
1205 if !as_json {
1208 let mut has_errors = false;
1209
1210 if use_regex && use_contains {
1212 eprintln!("{}", "ERROR: Cannot use --regex and --contains together.".red().bold());
1213 eprintln!(" {} --regex for pattern matching (alternation, wildcards, etc.)", "•".dimmed());
1214 eprintln!(" {} --contains for substring matching (expansive search)", "•".dimmed());
1215 eprintln!("\n {} Choose one based on your needs:", "Tip:".cyan().bold());
1216 eprintln!(" {} for OR logic: --regex", "pattern1|pattern2".yellow());
1217 eprintln!(" {} for substring: --contains", "partial_text".yellow());
1218 has_errors = true;
1219 }
1220
1221 if exact && use_contains {
1223 eprintln!("{}", "ERROR: Cannot use --exact and --contains together (contradictory).".red().bold());
1224 eprintln!(" {} --exact requires exact symbol name match", "•".dimmed());
1225 eprintln!(" {} --contains allows substring matching", "•".dimmed());
1226 has_errors = true;
1227 }
1228
1229 if file_pattern.is_some() && !glob_patterns.is_empty() {
1231 eprintln!("{}", "WARNING: Both --file and --glob specified.".yellow().bold());
1232 eprintln!(" {} --file does substring matching on file paths", "•".dimmed());
1233 eprintln!(" {} --glob does pattern matching with wildcards", "•".dimmed());
1234 eprintln!(" {} Both filters will apply (AND condition)", "Note:".dimmed());
1235 eprintln!("\n {} Usually you only need one:", "Tip:".cyan().bold());
1236 eprintln!(" {} for simple matching", "--file User.php".yellow());
1237 eprintln!(" {} for pattern matching", "--glob src/**/*.php".yellow());
1238 }
1239
1240 for pattern in &glob_patterns {
1242 if (pattern.starts_with('\'') && pattern.ends_with('\'')) ||
1244 (pattern.starts_with('"') && pattern.ends_with('"')) {
1245 eprintln!("{}",
1246 format!("WARNING: Glob pattern contains quotes: {}", pattern).yellow().bold()
1247 );
1248 eprintln!(" {} Shell quotes should not be part of the pattern", "Note:".dimmed());
1249 eprintln!(" {} --glob src/**/*.rs", "Correct:".green());
1250 eprintln!(" {} --glob 'src/**/*.rs'", "Wrong:".red().dimmed());
1251 }
1252
1253 if pattern.contains("*/") && !pattern.contains("**/") {
1255 eprintln!("{}",
1256 format!("INFO: Glob '{}' uses * (matches one directory level)", pattern).cyan()
1257 );
1258 eprintln!(" {} Use ** for recursive matching across subdirectories", "Tip:".cyan().bold());
1259 eprintln!(" {} → matches files in Models/ only", "app/Models/*.php".yellow());
1260 eprintln!(" {} → matches files in Models/ and subdirs", "app/Models/**/*.php".green());
1261 }
1262 }
1263
1264 if has_errors {
1265 anyhow::bail!("Invalid flag combination. Fix the errors above and try again.");
1266 }
1267 }
1268
1269 let filter = QueryFilter {
1270 language,
1271 kind,
1272 use_ast,
1273 use_regex,
1274 limit: final_limit,
1275 symbols_mode,
1276 expand,
1277 file_pattern,
1278 exact,
1279 use_contains,
1280 timeout_secs,
1281 glob_patterns: glob_patterns.clone(),
1282 exclude_patterns,
1283 paths_only,
1284 offset,
1285 force,
1286 suppress_output: as_json, include_dependencies,
1288 ..Default::default()
1289 };
1290
1291 let start = Instant::now();
1293
1294 let (query_response, mut flat_results, total_results, has_more) = if use_ast {
1297 match engine.search_ast_all_files(&pattern, filter.clone()) {
1299 Ok(ast_results) => {
1300 let count = ast_results.len();
1301 (None, ast_results, count, false)
1302 }
1303 Err(e) => {
1304 if as_json {
1305 let error_response = serde_json::json!({
1307 "error": e.to_string(),
1308 "query_too_broad": e.to_string().contains("Query too broad")
1309 });
1310 let json_output = if pretty_json {
1311 serde_json::to_string_pretty(&error_response)?
1312 } else {
1313 serde_json::to_string(&error_response)?
1314 };
1315 println!("{}", json_output);
1316 std::process::exit(1);
1317 } else {
1318 return Err(e);
1319 }
1320 }
1321 }
1322 } else {
1323 match engine.search_with_metadata(&pattern, filter.clone()) {
1325 Ok(response) => {
1326 let total = response.pagination.total;
1327 let has_more = response.pagination.has_more;
1328
1329 let flat = response.results.iter()
1331 .flat_map(|file_group| {
1332 file_group.matches.iter().map(move |m| {
1333 crate::models::SearchResult {
1334 path: file_group.path.clone(),
1335 lang: crate::models::Language::Unknown, kind: m.kind.clone(),
1337 symbol: m.symbol.clone(),
1338 span: m.span.clone(),
1339 preview: m.preview.clone(),
1340 dependencies: file_group.dependencies.clone(),
1341 }
1342 })
1343 })
1344 .collect();
1345
1346 (Some(response), flat, total, has_more)
1347 }
1348 Err(e) => {
1349 if as_json {
1350 let error_response = serde_json::json!({
1352 "error": e.to_string(),
1353 "query_too_broad": e.to_string().contains("Query too broad")
1354 });
1355 let json_output = if pretty_json {
1356 serde_json::to_string_pretty(&error_response)?
1357 } else {
1358 serde_json::to_string(&error_response)?
1359 };
1360 println!("{}", json_output);
1361 std::process::exit(1);
1362 } else {
1363 return Err(e);
1364 }
1365 }
1366 }
1367 };
1368
1369 if !no_truncate {
1371 const MAX_PREVIEW_LENGTH: usize = 100;
1372 for result in &mut flat_results {
1373 result.preview = truncate_preview(&result.preview, MAX_PREVIEW_LENGTH);
1374 }
1375 }
1376
1377 let elapsed = start.elapsed();
1378
1379 let timing_str = if elapsed.as_millis() < 1 {
1381 format!("{:.1}ms", elapsed.as_secs_f64() * 1000.0)
1382 } else {
1383 format!("{}ms", elapsed.as_millis())
1384 };
1385
1386 if as_json {
1387 if count_only {
1388 let count_response = serde_json::json!({
1390 "count": total_results,
1391 "timing_ms": elapsed.as_millis()
1392 });
1393 let json_output = if pretty_json {
1394 serde_json::to_string_pretty(&count_response)?
1395 } else {
1396 serde_json::to_string(&count_response)?
1397 };
1398 println!("{}", json_output);
1399 } else if paths_only {
1400 let locations: Vec<serde_json::Value> = flat_results.iter()
1402 .map(|r| serde_json::json!({
1403 "path": r.path,
1404 "line": r.span.start_line
1405 }))
1406 .collect();
1407 let json_output = if pretty_json {
1408 serde_json::to_string_pretty(&locations)?
1409 } else {
1410 serde_json::to_string(&locations)?
1411 };
1412 println!("{}", json_output);
1413 eprintln!("Found {} unique files in {}", locations.len(), timing_str);
1414 } else {
1415 let mut response = if let Some(resp) = query_response {
1417 let mut resp = resp;
1420
1421 if !no_truncate {
1423 const MAX_PREVIEW_LENGTH: usize = 100;
1424 for file_group in resp.results.iter_mut() {
1425 for m in file_group.matches.iter_mut() {
1426 m.preview = truncate_preview(&m.preview, MAX_PREVIEW_LENGTH);
1427 }
1428 }
1429 }
1430
1431 resp
1432 } else {
1433 use crate::models::{PaginationInfo, IndexStatus, FileGroupedResult, MatchResult};
1436 use std::collections::HashMap;
1437
1438 let mut grouped: HashMap<String, Vec<crate::models::SearchResult>> = HashMap::new();
1439 for result in &flat_results {
1440 grouped
1441 .entry(result.path.clone())
1442 .or_default()
1443 .push(result.clone());
1444 }
1445
1446 use crate::content_store::ContentReader;
1448 let local_cache = CacheManager::new(".");
1449 let content_path = local_cache.path().join("content.bin");
1450 let content_reader_opt = ContentReader::open(&content_path).ok();
1451
1452 let mut file_results: Vec<FileGroupedResult> = grouped
1453 .into_iter()
1454 .map(|(path, file_matches)| {
1455 let normalized_path = path.strip_prefix("./").unwrap_or(&path);
1459 let file_id_for_context = if let Some(reader) = &content_reader_opt {
1460 reader.get_file_id_by_path(normalized_path)
1461 } else {
1462 None
1463 };
1464
1465 let matches: Vec<MatchResult> = file_matches
1466 .into_iter()
1467 .map(|r| {
1468 let (context_before, context_after) = if let (Some(reader), Some(fid)) = (&content_reader_opt, file_id_for_context) {
1470 reader.get_context_by_line(fid as u32, r.span.start_line, 3)
1471 .unwrap_or_else(|_| (vec![], vec![]))
1472 } else {
1473 (vec![], vec![])
1474 };
1475
1476 MatchResult {
1477 kind: r.kind,
1478 symbol: r.symbol,
1479 span: r.span,
1480 preview: r.preview,
1481 context_before,
1482 context_after,
1483 }
1484 })
1485 .collect();
1486 FileGroupedResult {
1487 path,
1488 dependencies: None,
1489 matches,
1490 }
1491 })
1492 .collect();
1493
1494 file_results.sort_by(|a, b| a.path.cmp(&b.path));
1496
1497 crate::models::QueryResponse {
1498 ai_instruction: None, status: IndexStatus::Fresh,
1500 can_trust_results: true,
1501 warning: None,
1502 pagination: PaginationInfo {
1503 total: flat_results.len(),
1504 count: flat_results.len(),
1505 offset: offset.unwrap_or(0),
1506 limit,
1507 has_more: false, },
1509 results: file_results,
1510 }
1511 };
1512
1513 if ai_mode {
1515 let result_count: usize = response.results.iter().map(|fg| fg.matches.len()).sum();
1516
1517 response.ai_instruction = crate::query::generate_ai_instruction(
1518 result_count,
1519 response.pagination.total,
1520 response.pagination.has_more,
1521 symbols_mode,
1522 paths_only,
1523 use_ast,
1524 use_regex,
1525 language.is_some(),
1526 !glob_patterns.is_empty(),
1527 exact,
1528 );
1529 }
1530
1531 let json_output = if pretty_json {
1532 serde_json::to_string_pretty(&response)?
1533 } else {
1534 serde_json::to_string(&response)?
1535 };
1536 println!("{}", json_output);
1537
1538 let result_count: usize = response.results.iter().map(|fg| fg.matches.len()).sum();
1539 eprintln!("Found {} results in {}", result_count, timing_str);
1540 }
1541 } else {
1542 if count_only {
1544 println!("Found {} results in {}", flat_results.len(), timing_str);
1545 return Ok(());
1546 }
1547
1548 if paths_only {
1549 if flat_results.is_empty() {
1551 eprintln!("No results found (searched in {}).", timing_str);
1552 } else {
1553 for result in &flat_results {
1554 println!("{}", result.path);
1555 }
1556 eprintln!("Found {} unique files in {}", flat_results.len(), timing_str);
1557 }
1558 } else {
1559 if flat_results.is_empty() {
1561 println!("No results found (searched in {}).", timing_str);
1562 } else {
1563 let formatter = crate::formatter::OutputFormatter::new(plain);
1565 formatter.format_results(&flat_results, &pattern)?;
1566
1567 if total_results > flat_results.len() {
1569 println!("\nFound {} results ({} total) in {}", flat_results.len(), total_results, timing_str);
1571 if has_more {
1573 println!("Use --limit and --offset to paginate");
1574 }
1575 } else {
1576 println!("\nFound {} results in {}", flat_results.len(), timing_str);
1578 }
1579 }
1580 }
1581 }
1582
1583 Ok(())
1584}
1585
1586fn handle_serve(port: u16, host: String) -> Result<()> {
1588 log::info!("Starting HTTP server on {}:{}", host, port);
1589
1590 println!("Starting Reflex HTTP server...");
1591 println!(" Address: http://{}:{}", host, port);
1592 println!("\nEndpoints:");
1593 println!(" GET /query?q=<pattern>&lang=<lang>&kind=<kind>&limit=<n>&symbols=true®ex=true&exact=true&contains=true&expand=true&file=<pattern>&timeout=<secs>&glob=<pattern>&exclude=<pattern>&paths=true&dependencies=true");
1594 println!(" GET /stats");
1595 println!(" POST /index");
1596 println!("\nPress Ctrl+C to stop.");
1597
1598 let runtime = tokio::runtime::Runtime::new()?;
1600 runtime.block_on(async {
1601 run_server(port, host).await
1602 })
1603}
1604
1605async fn run_server(port: u16, host: String) -> Result<()> {
1607 use axum::{
1608 extract::{Query as AxumQuery, State},
1609 http::StatusCode,
1610 response::{IntoResponse, Json},
1611 routing::{get, post},
1612 Router,
1613 };
1614 use tower_http::cors::{CorsLayer, Any};
1615 use std::sync::Arc;
1616
1617 #[derive(Clone)]
1619 struct AppState {
1620 cache_path: String,
1621 }
1622
1623 #[derive(Debug, serde::Deserialize)]
1625 struct QueryParams {
1626 q: String,
1627 #[serde(default)]
1628 lang: Option<String>,
1629 #[serde(default)]
1630 kind: Option<String>,
1631 #[serde(default)]
1632 limit: Option<usize>,
1633 #[serde(default)]
1634 offset: Option<usize>,
1635 #[serde(default)]
1636 symbols: bool,
1637 #[serde(default)]
1638 regex: bool,
1639 #[serde(default)]
1640 exact: bool,
1641 #[serde(default)]
1642 contains: bool,
1643 #[serde(default)]
1644 expand: bool,
1645 #[serde(default)]
1646 file: Option<String>,
1647 #[serde(default = "default_timeout")]
1648 timeout: u64,
1649 #[serde(default)]
1650 glob: Vec<String>,
1651 #[serde(default)]
1652 exclude: Vec<String>,
1653 #[serde(default)]
1654 paths: bool,
1655 #[serde(default)]
1656 force: bool,
1657 #[serde(default)]
1658 dependencies: bool,
1659 }
1660
1661 fn default_timeout() -> u64 {
1663 30
1664 }
1665
1666 #[derive(Debug, serde::Deserialize)]
1668 struct IndexRequest {
1669 #[serde(default)]
1670 force: bool,
1671 #[serde(default)]
1672 languages: Vec<String>,
1673 }
1674
1675 async fn handle_query_endpoint(
1677 State(state): State<Arc<AppState>>,
1678 AxumQuery(params): AxumQuery<QueryParams>,
1679 ) -> Result<Json<crate::models::QueryResponse>, (StatusCode, String)> {
1680 log::info!("Query request: pattern={}", params.q);
1681
1682 let cache = CacheManager::new(&state.cache_path);
1683 let engine = QueryEngine::new(cache);
1684
1685 let language = if let Some(lang_str) = params.lang.as_deref() {
1687 match lang_str.to_lowercase().as_str() {
1688 "rust" | "rs" => Some(Language::Rust),
1689 "javascript" | "js" => Some(Language::JavaScript),
1690 "typescript" | "ts" => Some(Language::TypeScript),
1691 "vue" => Some(Language::Vue),
1692 "svelte" => Some(Language::Svelte),
1693 "php" => Some(Language::PHP),
1694 "python" | "py" => Some(Language::Python),
1695 "go" => Some(Language::Go),
1696 "java" => Some(Language::Java),
1697 "c" => Some(Language::C),
1698 "cpp" | "c++" => Some(Language::Cpp),
1699 _ => {
1700 return Err((
1701 StatusCode::BAD_REQUEST,
1702 format!("Unknown language '{}'. Supported languages: rust, javascript (js), typescript (ts), vue, svelte, php, python (py), go, java, c, cpp (c++)", lang_str)
1703 ));
1704 }
1705 }
1706 } else {
1707 None
1708 };
1709
1710 let kind = params.kind.as_deref().and_then(|s| {
1712 let capitalized = {
1713 let mut chars = s.chars();
1714 match chars.next() {
1715 None => String::new(),
1716 Some(first) => first.to_uppercase().chain(chars.flat_map(|c| c.to_lowercase())).collect(),
1717 }
1718 };
1719
1720 capitalized.parse::<crate::models::SymbolKind>()
1721 .ok()
1722 .or_else(|| {
1723 log::debug!("Treating '{}' as unknown symbol kind for filtering", s);
1724 Some(crate::models::SymbolKind::Unknown(s.to_string()))
1725 })
1726 });
1727
1728 let symbols_mode = params.symbols || kind.is_some();
1730
1731 let final_limit = if params.paths && params.limit.is_none() {
1733 None } else if let Some(user_limit) = params.limit {
1735 Some(user_limit) } else {
1737 Some(100) };
1739
1740 let filter = QueryFilter {
1741 language,
1742 kind,
1743 use_ast: false,
1744 use_regex: params.regex,
1745 limit: final_limit,
1746 symbols_mode,
1747 expand: params.expand,
1748 file_pattern: params.file,
1749 exact: params.exact,
1750 use_contains: params.contains,
1751 timeout_secs: params.timeout,
1752 glob_patterns: params.glob,
1753 exclude_patterns: params.exclude,
1754 paths_only: params.paths,
1755 offset: params.offset,
1756 force: params.force,
1757 suppress_output: true, include_dependencies: params.dependencies,
1759 ..Default::default()
1760 };
1761
1762 match engine.search_with_metadata(¶ms.q, filter) {
1763 Ok(response) => Ok(Json(response)),
1764 Err(e) => {
1765 log::error!("Query error: {}", e);
1766 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Query failed: {}", e)))
1767 }
1768 }
1769 }
1770
1771 async fn handle_stats_endpoint(
1773 State(state): State<Arc<AppState>>,
1774 ) -> Result<Json<crate::models::IndexStats>, (StatusCode, String)> {
1775 log::info!("Stats request");
1776
1777 let cache = CacheManager::new(&state.cache_path);
1778
1779 if !cache.exists() {
1780 return Err((StatusCode::NOT_FOUND, "No index found. Run 'rfx index' first.".to_string()));
1781 }
1782
1783 match cache.stats() {
1784 Ok(stats) => Ok(Json(stats)),
1785 Err(e) => {
1786 log::error!("Stats error: {}", e);
1787 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to get stats: {}", e)))
1788 }
1789 }
1790 }
1791
1792 async fn handle_index_endpoint(
1794 State(state): State<Arc<AppState>>,
1795 Json(req): Json<IndexRequest>,
1796 ) -> Result<Json<crate::models::IndexStats>, (StatusCode, String)> {
1797 log::info!("Index request: force={}, languages={:?}", req.force, req.languages);
1798
1799 let cache = CacheManager::new(&state.cache_path);
1800
1801 if req.force {
1802 log::info!("Force rebuild requested, clearing existing cache");
1803 if let Err(e) = cache.clear() {
1804 return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to clear cache: {}", e)));
1805 }
1806 }
1807
1808 let lang_filters: Vec<Language> = req.languages
1810 .iter()
1811 .filter_map(|s| match s.to_lowercase().as_str() {
1812 "rust" | "rs" => Some(Language::Rust),
1813 "python" | "py" => Some(Language::Python),
1814 "javascript" | "js" => Some(Language::JavaScript),
1815 "typescript" | "ts" => Some(Language::TypeScript),
1816 "vue" => Some(Language::Vue),
1817 "svelte" => Some(Language::Svelte),
1818 "go" => Some(Language::Go),
1819 "java" => Some(Language::Java),
1820 "php" => Some(Language::PHP),
1821 "c" => Some(Language::C),
1822 "cpp" | "c++" => Some(Language::Cpp),
1823 _ => {
1824 log::warn!("Unknown language: {}", s);
1825 None
1826 }
1827 })
1828 .collect();
1829
1830 let config = IndexConfig {
1831 languages: lang_filters,
1832 ..Default::default()
1833 };
1834
1835 let indexer = Indexer::new(cache, config);
1836 let path = std::path::PathBuf::from(&state.cache_path);
1837
1838 match indexer.index(&path, false) {
1839 Ok(stats) => Ok(Json(stats)),
1840 Err(e) => {
1841 log::error!("Index error: {}", e);
1842 Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Indexing failed: {}", e)))
1843 }
1844 }
1845 }
1846
1847 async fn handle_health() -> impl IntoResponse {
1849 (StatusCode::OK, "Reflex is running")
1850 }
1851
1852 let state = Arc::new(AppState {
1854 cache_path: ".".to_string(),
1855 });
1856
1857 let cors = CorsLayer::new()
1859 .allow_origin(Any)
1860 .allow_methods(Any)
1861 .allow_headers(Any);
1862
1863 let app = Router::new()
1865 .route("/query", get(handle_query_endpoint))
1866 .route("/stats", get(handle_stats_endpoint))
1867 .route("/index", post(handle_index_endpoint))
1868 .route("/health", get(handle_health))
1869 .layer(cors)
1870 .with_state(state);
1871
1872 let addr = format!("{}:{}", host, port);
1874 let listener = tokio::net::TcpListener::bind(&addr).await
1875 .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", addr, e))?;
1876
1877 log::info!("Server listening on {}", addr);
1878
1879 axum::serve(listener, app)
1881 .await
1882 .map_err(|e| anyhow::anyhow!("Server error: {}", e))?;
1883
1884 Ok(())
1885}
1886
1887fn handle_stats(as_json: bool, pretty_json: bool) -> Result<()> {
1889 log::info!("Showing index statistics");
1890
1891 let cache = CacheManager::new(".");
1892
1893 if !cache.exists() {
1894 anyhow::bail!(
1895 "No index found in current directory.\n\
1896 \n\
1897 Run 'rfx index' to build the code search index first.\n\
1898 This will scan all files in the current directory and create a .reflex/ cache.\n\
1899 \n\
1900 Example:\n\
1901 $ rfx index # Index current directory\n\
1902 $ rfx stats # Show index statistics"
1903 );
1904 }
1905
1906 let stats = cache.stats()?;
1907
1908 if as_json {
1909 let json_output = if pretty_json {
1910 serde_json::to_string_pretty(&stats)?
1911 } else {
1912 serde_json::to_string(&stats)?
1913 };
1914 println!("{}", json_output);
1915 } else {
1916 println!("Reflex Index Statistics");
1917 println!("=======================");
1918
1919 let root = std::env::current_dir()?;
1921 if crate::git::is_git_repo(&root) {
1922 match crate::git::get_git_state(&root) {
1923 Ok(git_state) => {
1924 let dirty_indicator = if git_state.dirty { " (uncommitted changes)" } else { " (clean)" };
1925 println!("Branch: {}@{}{}",
1926 git_state.branch,
1927 &git_state.commit[..7],
1928 dirty_indicator);
1929
1930 match cache.get_branch_info(&git_state.branch) {
1932 Ok(branch_info) => {
1933 if branch_info.commit_sha != git_state.commit {
1934 println!(" ⚠️ Index commit mismatch (indexed: {})",
1935 &branch_info.commit_sha[..7]);
1936 }
1937 if git_state.dirty && !branch_info.is_dirty {
1938 println!(" ⚠️ Uncommitted changes not indexed");
1939 }
1940 }
1941 Err(_) => {
1942 println!(" ⚠️ Branch not indexed");
1943 }
1944 }
1945 }
1946 Err(e) => {
1947 log::warn!("Failed to get git state: {}", e);
1948 }
1949 }
1950 } else {
1951 println!("Branch: (None)");
1953 }
1954
1955 println!("Files indexed: {}", stats.total_files);
1956 println!("Index size: {} bytes", stats.index_size_bytes);
1957 println!("Last updated: {}", stats.last_updated);
1958
1959 if !stats.files_by_language.is_empty() {
1961 println!("\nFiles by language:");
1962
1963 let mut lang_vec: Vec<_> = stats.files_by_language.iter().collect();
1965 lang_vec.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
1966
1967 let max_lang_len = lang_vec.iter().map(|(lang, _)| lang.len()).max().unwrap_or(8);
1969 let lang_width = max_lang_len.max(8); println!(" {:<width$} Files Lines", "Language", width = lang_width);
1973 println!(" {} ----- -------", "-".repeat(lang_width));
1974
1975 for (language, file_count) in lang_vec {
1977 let line_count = stats.lines_by_language.get(language).copied().unwrap_or(0);
1978 println!(" {:<width$} {:5} {:7}",
1979 language, file_count, line_count,
1980 width = lang_width);
1981 }
1982 }
1983 }
1984
1985 Ok(())
1986}
1987
1988fn handle_clear(skip_confirm: bool) -> Result<()> {
1990 let cache = CacheManager::new(".");
1991
1992 if !cache.exists() {
1993 println!("No cache to clear.");
1994 return Ok(());
1995 }
1996
1997 if !skip_confirm {
1998 println!("This will delete the local Reflex cache at: {:?}", cache.path());
1999 print!("Are you sure? [y/N] ");
2000 use std::io::{self, Write};
2001 io::stdout().flush()?;
2002
2003 let mut input = String::new();
2004 io::stdin().read_line(&mut input)?;
2005
2006 if !input.trim().eq_ignore_ascii_case("y") {
2007 println!("Cancelled.");
2008 return Ok(());
2009 }
2010 }
2011
2012 cache.clear()?;
2013 println!("Cache cleared successfully.");
2014
2015 Ok(())
2016}
2017
2018fn handle_list_files(as_json: bool, pretty_json: bool) -> Result<()> {
2020 let cache = CacheManager::new(".");
2021
2022 if !cache.exists() {
2023 anyhow::bail!(
2024 "No index found in current directory.\n\
2025 \n\
2026 Run 'rfx index' to build the code search index first.\n\
2027 This will scan all files in the current directory and create a .reflex/ cache.\n\
2028 \n\
2029 Example:\n\
2030 $ rfx index # Index current directory\n\
2031 $ rfx list-files # List indexed files"
2032 );
2033 }
2034
2035 let files = cache.list_files()?;
2036
2037 if as_json {
2038 let json_output = if pretty_json {
2039 serde_json::to_string_pretty(&files)?
2040 } else {
2041 serde_json::to_string(&files)?
2042 };
2043 println!("{}", json_output);
2044 } else if files.is_empty() {
2045 println!("No files indexed yet.");
2046 } else {
2047 println!("Indexed Files ({} total):", files.len());
2048 println!();
2049 for file in files {
2050 println!(" {} ({})",
2051 file.path,
2052 file.language);
2053 }
2054 }
2055
2056 Ok(())
2057}
2058
2059fn handle_watch(path: PathBuf, debounce_ms: u64, quiet: bool) -> Result<()> {
2061 log::info!("Starting watch mode for {:?}", path);
2062
2063 if !(5000..=30000).contains(&debounce_ms) {
2065 anyhow::bail!(
2066 "Debounce must be between 5000ms (5s) and 30000ms (30s). Got: {}ms",
2067 debounce_ms
2068 );
2069 }
2070
2071 if !quiet {
2072 println!("Starting Reflex watch mode...");
2073 println!(" Directory: {}", path.display());
2074 println!(" Debounce: {}ms ({}s)", debounce_ms, debounce_ms / 1000);
2075 println!(" Press Ctrl+C to stop.\n");
2076 }
2077
2078 let cache = CacheManager::new(&path);
2080
2081 if !cache.exists() {
2083 if !quiet {
2084 println!("No index found, running initial index...");
2085 }
2086 let config = IndexConfig::default();
2087 let indexer = Indexer::new(cache, config);
2088 indexer.index(&path, !quiet)?;
2089 if !quiet {
2090 println!("Initial index complete. Now watching for changes...\n");
2091 }
2092 }
2093
2094 let cache = CacheManager::new(&path);
2096 let config = IndexConfig::default();
2097 let indexer = Indexer::new(cache, config);
2098
2099 let watch_config = crate::watcher::WatchConfig {
2101 debounce_ms,
2102 quiet,
2103 };
2104
2105 crate::watcher::watch(&path, indexer, watch_config)?;
2106
2107 Ok(())
2108}
2109
2110fn handle_interactive() -> Result<()> {
2112 log::info!("Launching interactive mode");
2113 crate::interactive::run_interactive()
2114}
2115
2116fn handle_mcp() -> Result<()> {
2118 log::info!("Starting MCP server");
2119 crate::mcp::run_mcp_server()
2120}
2121
2122fn handle_index_symbols_internal(cache_dir: PathBuf) -> Result<()> {
2124 let mut indexer = crate::background_indexer::BackgroundIndexer::new(&cache_dir)?;
2125 indexer.run()?;
2126 Ok(())
2127}
2128
2129#[allow(clippy::too_many_arguments)]
2131fn handle_analyze(
2132 circular: bool,
2133 hotspots: bool,
2134 min_dependents: usize,
2135 unused: bool,
2136 islands: bool,
2137 min_island_size: usize,
2138 max_island_size: Option<usize>,
2139 format: String,
2140 as_json: bool,
2141 pretty_json: bool,
2142 count_only: bool,
2143 all: bool,
2144 plain: bool,
2145 _glob_patterns: Vec<String>,
2146 _exclude_patterns: Vec<String>,
2147 _force: bool,
2148 limit: Option<usize>,
2149 offset: Option<usize>,
2150 sort: Option<String>,
2151) -> Result<()> {
2152 use crate::dependency::DependencyIndex;
2153
2154 log::info!("Starting analyze command");
2155
2156 let cache = CacheManager::new(".");
2157
2158 if !cache.exists() {
2159 anyhow::bail!(
2160 "No index found in current directory.\n\
2161 \n\
2162 Run 'rfx index' to build the code search index first.\n\
2163 \n\
2164 Example:\n\
2165 $ rfx index # Index current directory\n\
2166 $ rfx analyze # Run dependency analysis"
2167 );
2168 }
2169
2170 let deps_index = DependencyIndex::new(cache);
2171
2172 let format = if as_json { "json" } else { &format };
2174
2175 let final_limit = if all {
2177 None } else if let Some(user_limit) = limit {
2179 Some(user_limit) } else {
2181 Some(200) };
2183
2184 if !circular && !hotspots && !unused && !islands {
2186 return handle_analyze_summary(&deps_index, min_dependents, count_only, as_json, pretty_json);
2187 }
2188
2189 if circular {
2191 handle_deps_circular(&deps_index, format, pretty_json, final_limit, offset, count_only, plain, sort.clone())?;
2192 }
2193
2194 if hotspots {
2195 handle_deps_hotspots(&deps_index, format, pretty_json, final_limit, offset, min_dependents, count_only, plain, sort.clone())?;
2196 }
2197
2198 if unused {
2199 handle_deps_unused(&deps_index, format, pretty_json, final_limit, offset, count_only, plain)?;
2200 }
2201
2202 if islands {
2203 handle_deps_islands(&deps_index, format, pretty_json, final_limit, offset, min_island_size, max_island_size, count_only, plain, sort.clone())?;
2204 }
2205
2206 Ok(())
2207}
2208
2209fn handle_analyze_summary(
2211 deps_index: &crate::dependency::DependencyIndex,
2212 min_dependents: usize,
2213 count_only: bool,
2214 as_json: bool,
2215 pretty_json: bool,
2216) -> Result<()> {
2217 let cycles = deps_index.detect_circular_dependencies()?;
2219 let hotspots = deps_index.find_hotspots(None, min_dependents)?;
2220 let unused = deps_index.find_unused_files()?;
2221 let all_islands = deps_index.find_islands()?;
2222
2223 if as_json {
2224 let summary = serde_json::json!({
2226 "circular_dependencies": cycles.len(),
2227 "hotspots": hotspots.len(),
2228 "unused_files": unused.len(),
2229 "islands": all_islands.len(),
2230 "min_dependents": min_dependents,
2231 });
2232
2233 let json_str = if pretty_json {
2234 serde_json::to_string_pretty(&summary)?
2235 } else {
2236 serde_json::to_string(&summary)?
2237 };
2238 println!("{}", json_str);
2239 } else if count_only {
2240 println!("{} circular dependencies", cycles.len());
2242 println!("{} hotspots ({}+ dependents)", hotspots.len(), min_dependents);
2243 println!("{} unused files", unused.len());
2244 println!("{} islands", all_islands.len());
2245 } else {
2246 println!("Dependency Analysis Summary\n");
2248
2249 println!("Circular Dependencies: {} cycle(s)", cycles.len());
2251
2252 println!("Hotspots: {} file(s) with {}+ dependents", hotspots.len(), min_dependents);
2254
2255 println!("Unused Files: {} file(s)", unused.len());
2257
2258 println!("Islands: {} disconnected component(s)", all_islands.len());
2260
2261 println!("\nUse specific flags for detailed results:");
2262 println!(" rfx analyze --circular");
2263 println!(" rfx analyze --hotspots");
2264 println!(" rfx analyze --unused");
2265 println!(" rfx analyze --islands");
2266 }
2267
2268 Ok(())
2269}
2270
2271fn handle_deps(
2273 file: PathBuf,
2274 reverse: bool,
2275 depth: usize,
2276 format: String,
2277 as_json: bool,
2278 pretty_json: bool,
2279) -> Result<()> {
2280 use crate::dependency::DependencyIndex;
2281
2282 log::info!("Starting deps command");
2283
2284 let cache = CacheManager::new(".");
2285
2286 if !cache.exists() {
2287 anyhow::bail!(
2288 "No index found in current directory.\n\
2289 \n\
2290 Run 'rfx index' to build the code search index first.\n\
2291 \n\
2292 Example:\n\
2293 $ rfx index # Index current directory\n\
2294 $ rfx deps <file> # Analyze dependencies"
2295 );
2296 }
2297
2298 let deps_index = DependencyIndex::new(cache);
2299
2300 let format = if as_json { "json" } else { &format };
2302
2303 let file_str = file.to_string_lossy().to_string();
2305
2306 let file_id = deps_index.get_file_id_by_path(&file_str)?
2308 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in index", file_str))?;
2309
2310 if reverse {
2311 let dependents = deps_index.get_dependents(file_id)?;
2313 let paths = deps_index.get_file_paths(&dependents)?;
2314
2315 match format.as_ref() {
2316 "json" => {
2317 let output: Vec<_> = dependents.iter()
2318 .filter_map(|id| paths.get(id).map(|path| serde_json::json!({
2319 "file_id": id,
2320 "path": path,
2321 })))
2322 .collect();
2323
2324 let json_str = if pretty_json {
2325 serde_json::to_string_pretty(&output)?
2326 } else {
2327 serde_json::to_string(&output)?
2328 };
2329 println!("{}", json_str);
2330 eprintln!("Found {} files that import {}", dependents.len(), file_str);
2331 }
2332 "tree" => {
2333 println!("Files that import {}:", file_str);
2334 for (id, path) in &paths {
2335 if dependents.contains(id) {
2336 println!(" └─ {}", path);
2337 }
2338 }
2339 eprintln!("\nFound {} dependents", dependents.len());
2340 }
2341 "table" => {
2342 println!("ID Path");
2343 println!("----- ----");
2344 for id in &dependents {
2345 if let Some(path) = paths.get(id) {
2346 println!("{:<5} {}", id, path);
2347 }
2348 }
2349 eprintln!("\nFound {} dependents", dependents.len());
2350 }
2351 _ => {
2352 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table, dot", format);
2353 }
2354 }
2355 } else {
2356 if depth == 1 {
2358 let deps = deps_index.get_dependencies(file_id)?;
2360
2361 match format.as_ref() {
2362 "json" => {
2363 let output: Vec<_> = deps.iter()
2364 .map(|dep| serde_json::json!({
2365 "imported_path": dep.imported_path,
2366 "resolved_file_id": dep.resolved_file_id,
2367 "import_type": match dep.import_type {
2368 crate::models::ImportType::Internal => "internal",
2369 crate::models::ImportType::External => "external",
2370 crate::models::ImportType::Stdlib => "stdlib",
2371 },
2372 "line": dep.line_number,
2373 "symbols": dep.imported_symbols,
2374 }))
2375 .collect();
2376
2377 let json_str = if pretty_json {
2378 serde_json::to_string_pretty(&output)?
2379 } else {
2380 serde_json::to_string(&output)?
2381 };
2382 println!("{}", json_str);
2383 eprintln!("Found {} dependencies for {}", deps.len(), file_str);
2384 }
2385 "tree" => {
2386 println!("Dependencies of {}:", file_str);
2387 for dep in &deps {
2388 let type_label = match dep.import_type {
2389 crate::models::ImportType::Internal => "[internal]",
2390 crate::models::ImportType::External => "[external]",
2391 crate::models::ImportType::Stdlib => "[stdlib]",
2392 };
2393 println!(" └─ {} {} (line {})", dep.imported_path, type_label, dep.line_number);
2394 }
2395 eprintln!("\nFound {} dependencies", deps.len());
2396 }
2397 "table" => {
2398 println!("Path Type Line");
2399 println!("---------------------------- --------- ----");
2400 for dep in &deps {
2401 let type_str = match dep.import_type {
2402 crate::models::ImportType::Internal => "internal",
2403 crate::models::ImportType::External => "external",
2404 crate::models::ImportType::Stdlib => "stdlib",
2405 };
2406 println!("{:<28} {:<9} {}", dep.imported_path, type_str, dep.line_number);
2407 }
2408 eprintln!("\nFound {} dependencies", deps.len());
2409 }
2410 _ => {
2411 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table, dot", format);
2412 }
2413 }
2414 } else {
2415 let transitive = deps_index.get_transitive_deps(file_id, depth)?;
2417 let file_ids: Vec<_> = transitive.keys().copied().collect();
2418 let paths = deps_index.get_file_paths(&file_ids)?;
2419
2420 match format.as_ref() {
2421 "json" => {
2422 let output: Vec<_> = transitive.iter()
2423 .filter_map(|(id, d)| {
2424 paths.get(id).map(|path| serde_json::json!({
2425 "file_id": id,
2426 "path": path,
2427 "depth": d,
2428 }))
2429 })
2430 .collect();
2431
2432 let json_str = if pretty_json {
2433 serde_json::to_string_pretty(&output)?
2434 } else {
2435 serde_json::to_string(&output)?
2436 };
2437 println!("{}", json_str);
2438 eprintln!("Found {} transitive dependencies (depth {})", transitive.len(), depth);
2439 }
2440 "tree" => {
2441 println!("Transitive dependencies of {} (depth {}):", file_str, depth);
2442 let mut by_depth: std::collections::HashMap<usize, Vec<i64>> = std::collections::HashMap::new();
2444 for (id, d) in &transitive {
2445 by_depth.entry(*d).or_insert_with(Vec::new).push(*id);
2446 }
2447
2448 for depth_level in 0..=depth {
2449 if let Some(ids) = by_depth.get(&depth_level) {
2450 let indent = " ".repeat(depth_level);
2451 for id in ids {
2452 if let Some(path) = paths.get(id) {
2453 if depth_level == 0 {
2454 println!("{}{} (self)", indent, path);
2455 } else {
2456 println!("{}└─ {}", indent, path);
2457 }
2458 }
2459 }
2460 }
2461 }
2462 eprintln!("\nFound {} transitive dependencies", transitive.len());
2463 }
2464 "table" => {
2465 println!("Depth File ID Path");
2466 println!("----- ------- ----");
2467 let mut sorted: Vec<_> = transitive.iter().collect();
2468 sorted.sort_by_key(|(_, d)| *d);
2469 for (id, d) in sorted {
2470 if let Some(path) = paths.get(id) {
2471 println!("{:<5} {:<7} {}", d, id, path);
2472 }
2473 }
2474 eprintln!("\nFound {} transitive dependencies", transitive.len());
2475 }
2476 _ => {
2477 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table, dot", format);
2478 }
2479 }
2480 }
2481 }
2482
2483 Ok(())
2484}
2485
2486fn handle_ask(
2488 question: Option<String>,
2489 _auto_execute: bool,
2490 provider_override: Option<String>,
2491 as_json: bool,
2492 pretty_json: bool,
2493 additional_context: Option<String>,
2494 configure: bool,
2495 agentic: bool,
2496 max_iterations: usize,
2497 no_eval: bool,
2498 show_reasoning: bool,
2499 verbose: bool,
2500 quiet: bool,
2501 answer: bool,
2502 interactive: bool,
2503 debug: bool,
2504) -> Result<()> {
2505 if configure {
2507 log::info!("Launching configuration wizard");
2508 return crate::semantic::run_configure_wizard();
2509 }
2510
2511 if !crate::semantic::is_any_api_key_configured() {
2513 anyhow::bail!(
2514 "No API key configured.\n\
2515 \n\
2516 Please run 'rfx ask --configure' to set up your API provider and key.\n\
2517 \n\
2518 Alternatively, you can set an environment variable:\n\
2519 - OPENAI_API_KEY\n\
2520 - ANTHROPIC_API_KEY\n\
2521 - GROQ_API_KEY"
2522 );
2523 }
2524
2525 if interactive || question.is_none() {
2528 log::info!("Launching interactive chat mode");
2529 let cache = CacheManager::new(".");
2530
2531 if !cache.exists() {
2532 anyhow::bail!(
2533 "No index found in current directory.\n\
2534 \n\
2535 Run 'rfx index' to build the code search index first.\n\
2536 \n\
2537 Example:\n\
2538 $ rfx index # Index current directory\n\
2539 $ rfx ask # Launch interactive chat"
2540 );
2541 }
2542
2543 return crate::semantic::run_chat_mode(cache, provider_override, None);
2544 }
2545
2546 let question = question.unwrap();
2548
2549 log::info!("Starting ask command");
2550
2551 let cache = CacheManager::new(".");
2552
2553 if !cache.exists() {
2554 anyhow::bail!(
2555 "No index found in current directory.\n\
2556 \n\
2557 Run 'rfx index' to build the code search index first.\n\
2558 \n\
2559 Example:\n\
2560 $ rfx index # Index current directory\n\
2561 $ rfx ask \"Find all TODOs\" # Ask questions"
2562 );
2563 }
2564
2565 let runtime = tokio::runtime::Runtime::new()
2567 .context("Failed to create async runtime")?;
2568
2569 let quiet = quiet || as_json;
2571
2572 let spinner = if !as_json {
2574 let s = ProgressBar::new_spinner();
2575 s.set_style(
2576 ProgressStyle::default_spinner()
2577 .template("{spinner:.cyan} {msg}")
2578 .unwrap()
2579 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
2580 );
2581 s.set_message("Generating queries...".to_string());
2582 s.enable_steady_tick(std::time::Duration::from_millis(80));
2583 Some(s)
2584 } else {
2585 None
2586 };
2587
2588 let (queries, results, total_count, count_only, gathered_context) = if agentic {
2589 let spinner_shared = if !quiet {
2593 spinner.as_ref().map(|s| Arc::new(Mutex::new(s.clone())))
2594 } else {
2595 None
2596 };
2597
2598 let reporter: Box<dyn crate::semantic::AgenticReporter> = if quiet {
2600 Box::new(crate::semantic::QuietReporter)
2601 } else {
2602 Box::new(crate::semantic::ConsoleReporter::new(show_reasoning, verbose, debug, spinner_shared))
2603 };
2604
2605 if let Some(ref s) = spinner {
2607 s.set_message("Starting agentic mode...".to_string());
2608 s.enable_steady_tick(std::time::Duration::from_millis(80));
2609 }
2610
2611 let agentic_config = crate::semantic::AgenticConfig {
2612 max_iterations,
2613 max_tools_per_phase: 5,
2614 enable_evaluation: !no_eval,
2615 eval_config: Default::default(),
2616 provider_override: provider_override.clone(),
2617 model_override: None,
2618 show_reasoning,
2619 verbose,
2620 debug,
2621 };
2622
2623 let agentic_response = runtime.block_on(async {
2624 crate::semantic::run_agentic_loop(&question, &cache, agentic_config, &*reporter).await
2625 }).context("Failed to run agentic loop")?;
2626
2627 if let Some(ref s) = spinner {
2629 s.finish_and_clear();
2630 }
2631
2632 if !as_json {
2634 reporter.clear_all();
2635 }
2636
2637 log::info!("Agentic loop completed: {} queries generated", agentic_response.queries.len());
2638
2639 let count_only_mode = agentic_response.total_count.is_none();
2641 let count = agentic_response.total_count.unwrap_or(0);
2642 (agentic_response.queries, agentic_response.results, count, count_only_mode, agentic_response.gathered_context)
2643 } else {
2644 if let Some(ref s) = spinner {
2646 s.set_message("Generating queries...".to_string());
2647 s.enable_steady_tick(std::time::Duration::from_millis(80));
2648 }
2649
2650 let semantic_response = runtime.block_on(async {
2651 crate::semantic::ask_question(&question, &cache, provider_override.clone(), additional_context, debug).await
2652 }).context("Failed to generate semantic queries")?;
2653
2654 if let Some(ref s) = spinner {
2655 s.finish_and_clear();
2656 }
2657 log::info!("LLM generated {} queries", semantic_response.queries.len());
2658
2659 let (exec_results, exec_total, exec_count_only) = runtime.block_on(async {
2661 crate::semantic::execute_queries(semantic_response.queries.clone(), &cache).await
2662 }).context("Failed to execute queries")?;
2663
2664 (semantic_response.queries, exec_results, exec_total, exec_count_only, None)
2665 };
2666
2667 let generated_answer = if answer {
2669 let answer_spinner = if !as_json {
2671 let s = ProgressBar::new_spinner();
2672 s.set_style(
2673 ProgressStyle::default_spinner()
2674 .template("{spinner:.cyan} {msg}")
2675 .unwrap()
2676 .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
2677 );
2678 s.set_message("Generating answer...".to_string());
2679 s.enable_steady_tick(std::time::Duration::from_millis(80));
2680 Some(s)
2681 } else {
2682 None
2683 };
2684
2685 let mut config = crate::semantic::config::load_config(cache.path())?;
2687 if let Some(provider) = &provider_override {
2688 config.provider = provider.clone();
2689 }
2690 let api_key = crate::semantic::config::get_api_key(&config.provider)?;
2691 let model = if config.model.is_some() {
2692 config.model.clone()
2693 } else {
2694 crate::semantic::config::get_user_model(&config.provider)
2695 };
2696 let provider_instance = crate::semantic::providers::create_provider(
2697 &config.provider,
2698 api_key,
2699 model,
2700 )?;
2701
2702 let codebase_context_str = crate::semantic::context::CodebaseContext::extract(&cache)
2704 .ok()
2705 .map(|ctx| ctx.to_prompt_string());
2706
2707 let answer_result = runtime.block_on(async {
2709 crate::semantic::generate_answer(
2710 &question,
2711 &results,
2712 total_count,
2713 gathered_context.as_deref(),
2714 codebase_context_str.as_deref(),
2715 &*provider_instance,
2716 ).await
2717 }).context("Failed to generate answer")?;
2718
2719 if let Some(s) = answer_spinner {
2720 s.finish_and_clear();
2721 }
2722
2723 Some(answer_result)
2724 } else {
2725 None
2726 };
2727
2728 if as_json {
2730 let json_response = crate::semantic::AgenticQueryResponse {
2732 queries: queries.clone(),
2733 results: results.clone(),
2734 total_count: if count_only { None } else { Some(total_count) },
2735 gathered_context: gathered_context.clone(),
2736 tools_executed: None, answer: generated_answer,
2738 };
2739
2740 let json_str = if pretty_json {
2741 serde_json::to_string_pretty(&json_response)?
2742 } else {
2743 serde_json::to_string(&json_response)?
2744 };
2745 println!("{}", json_str);
2746 return Ok(());
2747 }
2748
2749 if !answer {
2751 println!("\n{}", "Generated Queries:".bold().cyan());
2752 println!("{}", "==================".cyan());
2753 for (idx, query_cmd) in queries.iter().enumerate() {
2754 println!(
2755 "{}. {} {} {}",
2756 (idx + 1).to_string().bright_white().bold(),
2757 format!("[order: {}, merge: {}]", query_cmd.order, query_cmd.merge).dimmed(),
2758 "rfx".bright_green().bold(),
2759 query_cmd.command.bright_white()
2760 );
2761 }
2762 println!();
2763 }
2764
2765 println!();
2771 if let Some(answer_text) = generated_answer {
2772 println!("{}", "Answer:".bold().green());
2774 println!("{}", "=======".green());
2775 println!();
2776
2777 termimad::print_text(&answer_text);
2779 println!();
2780
2781 if !results.is_empty() {
2783 println!(
2784 "{}",
2785 format!(
2786 "(Based on {} matches across {} files)",
2787 total_count,
2788 results.len()
2789 ).dimmed()
2790 );
2791 }
2792 } else {
2793 if count_only {
2795 println!("{} {}", "Found".bright_green().bold(), format!("{} results", total_count).bright_white().bold());
2797 } else if results.is_empty() {
2798 println!("{}", "No results found.".yellow());
2799 } else {
2800 println!(
2801 "{} {} {} {} {}",
2802 "Found".bright_green().bold(),
2803 total_count.to_string().bright_white().bold(),
2804 "total results across".dimmed(),
2805 results.len().to_string().bright_white().bold(),
2806 "files:".dimmed()
2807 );
2808 println!();
2809
2810 for file_group in &results {
2811 println!("{}:", file_group.path.bright_cyan().bold());
2812 for match_result in &file_group.matches {
2813 println!(
2814 " {} {}-{}: {}",
2815 "Line".dimmed(),
2816 match_result.span.start_line.to_string().bright_yellow(),
2817 match_result.span.end_line.to_string().bright_yellow(),
2818 match_result.preview.lines().next().unwrap_or("")
2819 );
2820 }
2821 println!();
2822 }
2823 }
2824 }
2825
2826 Ok(())
2827}
2828
2829fn handle_context(
2831 structure: bool,
2832 path: Option<String>,
2833 file_types: bool,
2834 project_type: bool,
2835 framework: bool,
2836 entry_points: bool,
2837 test_layout: bool,
2838 config_files: bool,
2839 depth: usize,
2840 json: bool,
2841) -> Result<()> {
2842 let cache = CacheManager::new(".");
2843
2844 if !cache.exists() {
2845 anyhow::bail!(
2846 "No index found in current directory.\n\
2847 \n\
2848 Run 'rfx index' to build the code search index first.\n\
2849 \n\
2850 Example:\n\
2851 $ rfx index # Index current directory\n\
2852 $ rfx context # Generate context"
2853 );
2854 }
2855
2856 let opts = crate::context::ContextOptions {
2858 structure,
2859 path,
2860 file_types,
2861 project_type,
2862 framework,
2863 entry_points,
2864 test_layout,
2865 config_files,
2866 depth,
2867 json,
2868 };
2869
2870 let context_output = crate::context::generate_context(&cache, &opts)
2872 .context("Failed to generate codebase context")?;
2873
2874 println!("{}", context_output);
2876
2877 Ok(())
2878}
2879
2880fn handle_deps_circular(
2882 deps_index: &crate::dependency::DependencyIndex,
2883 format: &str,
2884 pretty_json: bool,
2885 limit: Option<usize>,
2886 offset: Option<usize>,
2887 count_only: bool,
2888 _plain: bool,
2889 sort: Option<String>,
2890) -> Result<()> {
2891 let mut all_cycles = deps_index.detect_circular_dependencies()?;
2892
2893 let sort_order = sort.as_deref().unwrap_or("desc");
2895 match sort_order {
2896 "asc" => {
2897 all_cycles.sort_by_key(|cycle| cycle.len());
2899 }
2900 "desc" => {
2901 all_cycles.sort_by_key(|cycle| std::cmp::Reverse(cycle.len()));
2903 }
2904 _ => {
2905 anyhow::bail!("Invalid sort order '{}'. Supported: asc, desc", sort_order);
2906 }
2907 }
2908
2909 let total_count = all_cycles.len();
2910
2911 if count_only {
2912 println!("Found {} circular dependencies", total_count);
2913 return Ok(());
2914 }
2915
2916 if all_cycles.is_empty() {
2917 println!("No circular dependencies found.");
2918 return Ok(());
2919 }
2920
2921 let offset_val = offset.unwrap_or(0);
2923 let mut cycles: Vec<_> = all_cycles.into_iter().skip(offset_val).collect();
2924
2925 if let Some(lim) = limit {
2927 cycles.truncate(lim);
2928 }
2929
2930 if cycles.is_empty() {
2931 println!("No circular dependencies found at offset {}.", offset_val);
2932 return Ok(());
2933 }
2934
2935 let count = cycles.len();
2936 let has_more = offset_val + count < total_count;
2937
2938 match format {
2939 "json" => {
2940 let file_ids: Vec<i64> = cycles.iter().flat_map(|c| c.iter()).copied().collect();
2941 let paths = deps_index.get_file_paths(&file_ids)?;
2942
2943 let results: Vec<_> = cycles.iter()
2944 .map(|cycle| {
2945 let cycle_paths: Vec<_> = cycle.iter()
2946 .filter_map(|id| paths.get(id).cloned())
2947 .collect();
2948 serde_json::json!({
2949 "paths": cycle_paths,
2950 })
2951 })
2952 .collect();
2953
2954 let output = serde_json::json!({
2955 "pagination": {
2956 "total": total_count,
2957 "count": count,
2958 "offset": offset_val,
2959 "limit": limit,
2960 "has_more": has_more,
2961 },
2962 "results": results,
2963 });
2964
2965 let json_str = if pretty_json {
2966 serde_json::to_string_pretty(&output)?
2967 } else {
2968 serde_json::to_string(&output)?
2969 };
2970 println!("{}", json_str);
2971 if total_count > count {
2972 eprintln!("Found {} circular dependencies ({} total)", count, total_count);
2973 } else {
2974 eprintln!("Found {} circular dependencies", count);
2975 }
2976 }
2977 "tree" => {
2978 println!("Circular Dependencies Found:");
2979 let file_ids: Vec<i64> = cycles.iter().flat_map(|c| c.iter()).copied().collect();
2980 let paths = deps_index.get_file_paths(&file_ids)?;
2981
2982 for (idx, cycle) in cycles.iter().enumerate() {
2983 println!("\nCycle {}:", idx + 1);
2984 for id in cycle {
2985 if let Some(path) = paths.get(id) {
2986 println!(" → {}", path);
2987 }
2988 }
2989 if let Some(first_id) = cycle.first() {
2991 if let Some(path) = paths.get(first_id) {
2992 println!(" → {} (cycle completes)", path);
2993 }
2994 }
2995 }
2996 if total_count > count {
2997 eprintln!("\nFound {} cycles ({} total)", count, total_count);
2998 if has_more {
2999 eprintln!("Use --limit and --offset to paginate");
3000 }
3001 } else {
3002 eprintln!("\nFound {} cycles", count);
3003 }
3004 }
3005 "table" => {
3006 println!("Cycle Files in Cycle");
3007 println!("----- --------------");
3008 let file_ids: Vec<i64> = cycles.iter().flat_map(|c| c.iter()).copied().collect();
3009 let paths = deps_index.get_file_paths(&file_ids)?;
3010
3011 for (idx, cycle) in cycles.iter().enumerate() {
3012 let cycle_str = cycle.iter()
3013 .filter_map(|id| paths.get(id).map(|p| p.as_str()))
3014 .collect::<Vec<_>>()
3015 .join(" → ");
3016 println!("{:<5} {}", idx + 1, cycle_str);
3017 }
3018 if total_count > count {
3019 eprintln!("\nFound {} cycles ({} total)", count, total_count);
3020 if has_more {
3021 eprintln!("Use --limit and --offset to paginate");
3022 }
3023 } else {
3024 eprintln!("\nFound {} cycles", count);
3025 }
3026 }
3027 _ => {
3028 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table", format);
3029 }
3030 }
3031
3032 Ok(())
3033}
3034
3035fn handle_deps_hotspots(
3037 deps_index: &crate::dependency::DependencyIndex,
3038 format: &str,
3039 pretty_json: bool,
3040 limit: Option<usize>,
3041 offset: Option<usize>,
3042 min_dependents: usize,
3043 count_only: bool,
3044 _plain: bool,
3045 sort: Option<String>,
3046) -> Result<()> {
3047 let mut all_hotspots = deps_index.find_hotspots(None, min_dependents)?;
3049
3050 let sort_order = sort.as_deref().unwrap_or("desc");
3052 match sort_order {
3053 "asc" => {
3054 all_hotspots.sort_by(|a, b| a.1.cmp(&b.1));
3056 }
3057 "desc" => {
3058 all_hotspots.sort_by(|a, b| b.1.cmp(&a.1));
3060 }
3061 _ => {
3062 anyhow::bail!("Invalid sort order '{}'. Supported: asc, desc", sort_order);
3063 }
3064 }
3065
3066 let total_count = all_hotspots.len();
3067
3068 if count_only {
3069 println!("Found {} hotspots with {}+ dependents", total_count, min_dependents);
3070 return Ok(());
3071 }
3072
3073 if all_hotspots.is_empty() {
3074 println!("No hotspots found.");
3075 return Ok(());
3076 }
3077
3078 let offset_val = offset.unwrap_or(0);
3080 let mut hotspots: Vec<_> = all_hotspots.into_iter().skip(offset_val).collect();
3081
3082 if let Some(lim) = limit {
3084 hotspots.truncate(lim);
3085 }
3086
3087 if hotspots.is_empty() {
3088 println!("No hotspots found at offset {}.", offset_val);
3089 return Ok(());
3090 }
3091
3092 let count = hotspots.len();
3093 let has_more = offset_val + count < total_count;
3094
3095 let file_ids: Vec<i64> = hotspots.iter().map(|(id, _)| *id).collect();
3096 let paths = deps_index.get_file_paths(&file_ids)?;
3097
3098 match format {
3099 "json" => {
3100 let results: Vec<_> = hotspots.iter()
3101 .filter_map(|(id, import_count)| {
3102 paths.get(id).map(|path| serde_json::json!({
3103 "path": path,
3104 "import_count": import_count,
3105 }))
3106 })
3107 .collect();
3108
3109 let output = serde_json::json!({
3110 "pagination": {
3111 "total": total_count,
3112 "count": count,
3113 "offset": offset_val,
3114 "limit": limit,
3115 "has_more": has_more,
3116 },
3117 "results": results,
3118 });
3119
3120 let json_str = if pretty_json {
3121 serde_json::to_string_pretty(&output)?
3122 } else {
3123 serde_json::to_string(&output)?
3124 };
3125 println!("{}", json_str);
3126 if total_count > count {
3127 eprintln!("Found {} hotspots ({} total)", count, total_count);
3128 } else {
3129 eprintln!("Found {} hotspots", count);
3130 }
3131 }
3132 "tree" => {
3133 println!("Hotspots (Most-Imported Files):");
3134 for (idx, (id, import_count)) in hotspots.iter().enumerate() {
3135 if let Some(path) = paths.get(id) {
3136 println!(" {}. {} ({} imports)", idx + 1, path, import_count);
3137 }
3138 }
3139 if total_count > count {
3140 eprintln!("\nFound {} hotspots ({} total)", count, total_count);
3141 if has_more {
3142 eprintln!("Use --limit and --offset to paginate");
3143 }
3144 } else {
3145 eprintln!("\nFound {} hotspots", count);
3146 }
3147 }
3148 "table" => {
3149 println!("Rank Imports File");
3150 println!("---- ------- ----");
3151 for (idx, (id, import_count)) in hotspots.iter().enumerate() {
3152 if let Some(path) = paths.get(id) {
3153 println!("{:<4} {:<7} {}", idx + 1, import_count, path);
3154 }
3155 }
3156 if total_count > count {
3157 eprintln!("\nFound {} hotspots ({} total)", count, total_count);
3158 if has_more {
3159 eprintln!("Use --limit and --offset to paginate");
3160 }
3161 } else {
3162 eprintln!("\nFound {} hotspots", count);
3163 }
3164 }
3165 _ => {
3166 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table", format);
3167 }
3168 }
3169
3170 Ok(())
3171}
3172
3173fn handle_deps_unused(
3175 deps_index: &crate::dependency::DependencyIndex,
3176 format: &str,
3177 pretty_json: bool,
3178 limit: Option<usize>,
3179 offset: Option<usize>,
3180 count_only: bool,
3181 _plain: bool,
3182) -> Result<()> {
3183 let all_unused = deps_index.find_unused_files()?;
3184 let total_count = all_unused.len();
3185
3186 if count_only {
3187 println!("Found {} unused files", total_count);
3188 return Ok(());
3189 }
3190
3191 if all_unused.is_empty() {
3192 println!("No unused files found (all files have incoming dependencies).");
3193 return Ok(());
3194 }
3195
3196 let offset_val = offset.unwrap_or(0);
3198 let mut unused: Vec<_> = all_unused.into_iter().skip(offset_val).collect();
3199
3200 if unused.is_empty() {
3201 println!("No unused files found at offset {}.", offset_val);
3202 return Ok(());
3203 }
3204
3205 if let Some(lim) = limit {
3207 unused.truncate(lim);
3208 }
3209
3210 let count = unused.len();
3211 let has_more = offset_val + count < total_count;
3212
3213 let paths = deps_index.get_file_paths(&unused)?;
3214
3215 match format {
3216 "json" => {
3217 let results: Vec<String> = unused.iter()
3219 .filter_map(|id| paths.get(id).cloned())
3220 .collect();
3221
3222 let output = serde_json::json!({
3223 "pagination": {
3224 "total": total_count,
3225 "count": count,
3226 "offset": offset_val,
3227 "limit": limit,
3228 "has_more": has_more,
3229 },
3230 "results": results,
3231 });
3232
3233 let json_str = if pretty_json {
3234 serde_json::to_string_pretty(&output)?
3235 } else {
3236 serde_json::to_string(&output)?
3237 };
3238 println!("{}", json_str);
3239 if total_count > count {
3240 eprintln!("Found {} unused files ({} total)", count, total_count);
3241 } else {
3242 eprintln!("Found {} unused files", count);
3243 }
3244 }
3245 "tree" => {
3246 println!("Unused Files (No Incoming Dependencies):");
3247 for (idx, id) in unused.iter().enumerate() {
3248 if let Some(path) = paths.get(id) {
3249 println!(" {}. {}", idx + 1, path);
3250 }
3251 }
3252 if total_count > count {
3253 eprintln!("\nFound {} unused files ({} total)", count, total_count);
3254 if has_more {
3255 eprintln!("Use --limit and --offset to paginate");
3256 }
3257 } else {
3258 eprintln!("\nFound {} unused files", count);
3259 }
3260 }
3261 "table" => {
3262 println!("Path");
3263 println!("----");
3264 for id in &unused {
3265 if let Some(path) = paths.get(id) {
3266 println!("{}", path);
3267 }
3268 }
3269 if total_count > count {
3270 eprintln!("\nFound {} unused files ({} total)", count, total_count);
3271 if has_more {
3272 eprintln!("Use --limit and --offset to paginate");
3273 }
3274 } else {
3275 eprintln!("\nFound {} unused files", count);
3276 }
3277 }
3278 _ => {
3279 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table", format);
3280 }
3281 }
3282
3283 Ok(())
3284}
3285
3286fn handle_deps_islands(
3288 deps_index: &crate::dependency::DependencyIndex,
3289 format: &str,
3290 pretty_json: bool,
3291 limit: Option<usize>,
3292 offset: Option<usize>,
3293 min_island_size: usize,
3294 max_island_size: Option<usize>,
3295 count_only: bool,
3296 _plain: bool,
3297 sort: Option<String>,
3298) -> Result<()> {
3299 let all_islands = deps_index.find_islands()?;
3300 let total_components = all_islands.len();
3301
3302 let cache = deps_index.get_cache();
3304 let total_files = cache.stats()?.total_files as usize;
3305
3306 let max_size = max_island_size.unwrap_or_else(|| {
3308 let fifty_percent = (total_files as f64 * 0.5) as usize;
3309 fifty_percent.min(500)
3310 });
3311
3312 let mut islands: Vec<_> = all_islands.into_iter()
3314 .filter(|island| {
3315 let size = island.len();
3316 size >= min_island_size && size <= max_size
3317 })
3318 .collect();
3319
3320 let sort_order = sort.as_deref().unwrap_or("desc");
3322 match sort_order {
3323 "asc" => {
3324 islands.sort_by_key(|island| island.len());
3326 }
3327 "desc" => {
3328 islands.sort_by_key(|island| std::cmp::Reverse(island.len()));
3330 }
3331 _ => {
3332 anyhow::bail!("Invalid sort order '{}'. Supported: asc, desc", sort_order);
3333 }
3334 }
3335
3336 let filtered_count = total_components - islands.len();
3337
3338 if count_only {
3339 if filtered_count > 0 {
3340 println!("Found {} islands (filtered {} of {} total components by size: {}-{})",
3341 islands.len(), filtered_count, total_components, min_island_size, max_size);
3342 } else {
3343 println!("Found {} islands", islands.len());
3344 }
3345 return Ok(());
3346 }
3347
3348 let offset_val = offset.unwrap_or(0);
3350 if offset_val > 0 && offset_val < islands.len() {
3351 islands = islands.into_iter().skip(offset_val).collect();
3352 } else if offset_val >= islands.len() {
3353 if filtered_count > 0 {
3354 println!("No islands found at offset {} (filtered {} of {} total components by size: {}-{}).",
3355 offset_val, filtered_count, total_components, min_island_size, max_size);
3356 } else {
3357 println!("No islands found at offset {}.", offset_val);
3358 }
3359 return Ok(());
3360 }
3361
3362 if let Some(lim) = limit {
3364 islands.truncate(lim);
3365 }
3366
3367 if islands.is_empty() {
3368 if filtered_count > 0 {
3369 println!("No islands found matching criteria (filtered {} of {} total components by size: {}-{}).",
3370 filtered_count, total_components, min_island_size, max_size);
3371 } else {
3372 println!("No islands found.");
3373 }
3374 return Ok(());
3375 }
3376
3377 let count = islands.len();
3379 let has_more = offset_val + count < total_components - filtered_count;
3380
3381 let file_ids: Vec<i64> = islands.iter().flat_map(|island| island.iter()).copied().collect();
3382 let paths = deps_index.get_file_paths(&file_ids)?;
3383
3384 match format {
3385 "json" => {
3386 let results: Vec<_> = islands.iter()
3387 .enumerate()
3388 .map(|(idx, island)| {
3389 let island_paths: Vec<_> = island.iter()
3390 .filter_map(|id| paths.get(id).cloned())
3391 .collect();
3392 serde_json::json!({
3393 "island_id": idx + 1,
3394 "size": island.len(),
3395 "paths": island_paths,
3396 })
3397 })
3398 .collect();
3399
3400 let output = serde_json::json!({
3401 "pagination": {
3402 "total": total_components - filtered_count,
3403 "count": count,
3404 "offset": offset_val,
3405 "limit": limit,
3406 "has_more": has_more,
3407 },
3408 "results": results,
3409 });
3410
3411 let json_str = if pretty_json {
3412 serde_json::to_string_pretty(&output)?
3413 } else {
3414 serde_json::to_string(&output)?
3415 };
3416 println!("{}", json_str);
3417 if filtered_count > 0 {
3418 eprintln!("Found {} islands (filtered {} of {} total components by size: {}-{})",
3419 count, filtered_count, total_components, min_island_size, max_size);
3420 } else if total_components - filtered_count > count {
3421 eprintln!("Found {} islands ({} total)", count, total_components - filtered_count);
3422 } else {
3423 eprintln!("Found {} islands (disconnected components)", count);
3424 }
3425 }
3426 "tree" => {
3427 println!("Islands (Disconnected Components):");
3428 for (idx, island) in islands.iter().enumerate() {
3429 println!("\nIsland {} ({} files):", idx + 1, island.len());
3430 for id in island {
3431 if let Some(path) = paths.get(id) {
3432 println!(" ├─ {}", path);
3433 }
3434 }
3435 }
3436 if filtered_count > 0 {
3437 eprintln!("\nFound {} islands (filtered {} of {} total components by size: {}-{})",
3438 count, filtered_count, total_components, min_island_size, max_size);
3439 if has_more {
3440 eprintln!("Use --limit and --offset to paginate");
3441 }
3442 } else if total_components - filtered_count > count {
3443 eprintln!("\nFound {} islands ({} total)", count, total_components - filtered_count);
3444 if has_more {
3445 eprintln!("Use --limit and --offset to paginate");
3446 }
3447 } else {
3448 eprintln!("\nFound {} islands", count);
3449 }
3450 }
3451 "table" => {
3452 println!("Island Size Files");
3453 println!("------ ---- -----");
3454 for (idx, island) in islands.iter().enumerate() {
3455 let island_files = island.iter()
3456 .filter_map(|id| paths.get(id).map(|p| p.as_str()))
3457 .collect::<Vec<_>>()
3458 .join(", ");
3459 println!("{:<6} {:<4} {}", idx + 1, island.len(), island_files);
3460 }
3461 if filtered_count > 0 {
3462 eprintln!("\nFound {} islands (filtered {} of {} total components by size: {}-{})",
3463 count, filtered_count, total_components, min_island_size, max_size);
3464 if has_more {
3465 eprintln!("Use --limit and --offset to paginate");
3466 }
3467 } else if total_components - filtered_count > count {
3468 eprintln!("\nFound {} islands ({} total)", count, total_components - filtered_count);
3469 if has_more {
3470 eprintln!("Use --limit and --offset to paginate");
3471 }
3472 } else {
3473 eprintln!("\nFound {} islands", count);
3474 }
3475 }
3476 _ => {
3477 anyhow::bail!("Unknown format '{}'. Supported: json, tree, table", format);
3478 }
3479 }
3480
3481 Ok(())
3482}
3483