use anyhow::Result;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info};
use crate::cache::{normalize_path, FileMetaStore};
use crate::chunker::SemanticChunker;
use crate::db_discovery::{find_best_database, register_repository, unregister_repository};
use crate::embed::{EmbeddingService, ModelType};
use crate::file::FileWalker;
use crate::fts::FtsStore;
use crate::vectordb::VectorStore;
mod manager;
pub use manager::{IndexManager, SharedStores};
fn get_db_path(path: Option<PathBuf>) -> Result<(PathBuf, PathBuf)> {
use crate::db_discovery::resolve_database_with_message;
resolve_database_with_message(path.as_deref(), "indexing")
}
fn get_db_path_smart(
path: Option<PathBuf>,
global: bool,
force: bool,
) -> Result<(PathBuf, PathBuf)> {
let target = path.as_deref();
let project_path = path.as_deref().unwrap_or(Path::new("."));
let canonical_path = PathBuf::from(normalize_path(
&project_path
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(project_path)),
));
let existing_db = find_best_database(target)?;
if force {
if let Some(ref db_info) = existing_db {
println!(
"{}",
format!(
"🗑️ Force rebuild: deleting existing database at {}",
db_info.db_path.display()
)
.yellow()
);
std::fs::remove_dir_all(&db_info.db_path)?;
std::thread::sleep(std::time::Duration::from_millis(1000));
println!("✅ Existing database deleted");
}
}
if global {
if let Some(ref db_info) = existing_db {
if !force && db_info.is_global {
println!(
"{}",
format!(
"🌍 Using existing global database: {}",
db_info.db_path.display()
)
.dimmed()
);
return Ok((db_info.db_path.clone(), db_info.project_path.clone()));
} else if !force && !db_info.is_global {
println!(
"{}",
format!(
"⚠️ Local database exists at {}\n Moving to global database...",
db_info.db_path.display()
)
.yellow()
);
std::fs::remove_dir_all(&db_info.db_path)?;
println!("✅ Local database removed");
}
}
return get_global_db_path(path);
}
if let Some(ref db_info) = existing_db {
if !db_info.is_current {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let relative_path = if let Ok(rel) = current_dir.strip_prefix(&db_info.project_path) {
format!("./{}", rel.display())
} else {
db_info.project_path.display().to_string()
};
println!(
"{}",
format!(
"📂 Using database from: {}\n (indexing from subfolder, project root: {})",
db_info.db_path.display(),
relative_path
)
.dimmed()
);
}
return Ok((db_info.db_path.clone(), db_info.project_path.clone()));
}
let project_root = find_project_root(&canonical_path);
if let Some(root) = project_root {
if root != canonical_path {
println!(
"{}",
format!(
"⚠️ You are in a subdirectory: {}\n Project root detected at: {}",
canonical_path.display(),
root.display()
)
.yellow()
);
println!(
"{}",
" Creating database at project root to avoid duplicate indexes.".yellow()
);
let db_path = root.join(".codesearch.db");
return Ok((db_path, root));
}
} else {
println!(
"{}",
format!(
"ℹ️ No project root detected (no .git, Cargo.toml, package.json, etc.)\n Creating database in: {}",
canonical_path.display()
).dimmed()
);
println!(
"{}",
" Tip: If this is a subdirectory, run 'codesearch index' from the project root."
.dimmed()
);
}
let db_path = canonical_path.join(".codesearch.db");
Ok((db_path, canonical_path))
}
fn find_project_root(start_path: &Path) -> Option<PathBuf> {
let markers = [
".git", ".hg", ".svn", "Cargo.toml", "package.json", "pyproject.toml", "go.mod", ".sln", ];
let mut current = start_path.to_path_buf();
loop {
for marker in &markers {
let marker_path = current.join(marker);
if marker_path.exists() {
return Some(current);
}
}
if let Ok(entries) = std::fs::read_dir(¤t) {
for entry in entries.flatten() {
if let Some(ext) = entry.path().extension() {
if ext == "sln" {
return Some(current);
}
}
}
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
None
}
fn get_global_db_path(path: Option<PathBuf>) -> Result<(PathBuf, PathBuf)> {
use dirs::home_dir;
let project_path = path.unwrap_or_else(|| PathBuf::from("."));
let canonical_path = project_path.canonicalize()?;
let project_name = canonical_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
let home = home_dir().ok_or_else(|| anyhow::anyhow!("No home directory found"))?;
let global_db_dir = home.join(".codesearch.dbs").join(project_name);
let db_path = global_db_dir.join(".codesearch.db");
register_repository(&canonical_path)?;
println!(
"{}",
format!(
"🌍 Using global database: {}\n (project: {})",
db_path.display(),
project_name
)
.dimmed()
);
Ok((db_path, canonical_path))
}
pub async fn index(
path: Option<PathBuf>,
dry_run: bool,
force: bool,
global: bool,
model: Option<ModelType>,
cancel_token: CancellationToken,
) -> Result<()> {
index_with_options(path, dry_run, force, global, model, false, cancel_token).await
}
pub async fn index_quiet(
path: Option<PathBuf>,
force: bool,
cancel_token: CancellationToken,
) -> Result<()> {
index_with_options(path, false, force, false, None, true, cancel_token).await
}
async fn index_with_options(
path: Option<PathBuf>,
dry_run: bool,
force: bool,
global: bool,
model: Option<ModelType>,
quiet: bool,
cancel_token: CancellationToken,
) -> Result<()> {
let (db_path, project_path) = get_db_path_smart(path, global, force)?;
let model_type = model.unwrap_or_default();
macro_rules! log_print {
($($arg:tt)*) => {
if !quiet {
println!($($arg)*);
}
};
}
log_print!("{}", "🚀 Codesearch Indexer".bright_cyan().bold());
log_print!("{}", "=".repeat(60));
log_print!("📂 Project: {}", project_path.display());
log_print!("💾 Database: {}", db_path.display());
log_print!(
"🧠 Model: {} ({} dims)",
model_type.name(),
model_type.dimensions()
);
if dry_run {
log_print!("\n{}", "🔍 DRY RUN MODE".bright_yellow());
}
log_print!("\n{}", "Phase 1: File Discovery".bright_cyan());
log_print!("{}", "-".repeat(60));
let start = Instant::now();
let walker = FileWalker::new(project_path.clone());
let (mut files, stats) = walker.walk()?;
let discovery_duration = start.elapsed();
log_print!(
"✅ Found {} indexable files in {:?}",
files.len(),
discovery_duration
);
log_print!(" Total files scanned: {}", stats.total_files);
log_print!(" Binary/skipped: {}", stats.skipped_binary);
log_print!(" Total size: {:.2} MB", stats.total_size_mb());
if files.is_empty() {
log_print!("\n{}", "No files to index!".yellow());
return Ok(());
}
if dry_run {
log_print!("\n{}", "Dry run complete!".green());
return Ok(());
}
let is_incremental = db_path.exists() && !force;
let mut file_meta_store = if is_incremental {
log_print!("\n{}", "📊 Incremental Indexing".bright_cyan());
log_print!("{}", "-".repeat(60));
Some(FileMetaStore::load_or_create(
&db_path,
model_type.name(),
model_type.dimensions(),
)?)
} else {
None
};
if is_incremental {
let file_meta_store = file_meta_store.as_mut().unwrap();
let mut changed_files = Vec::new();
let mut unchanged_files = 0;
for file in &files {
let (needs_reindex, _old_chunk_ids) = file_meta_store.check_file(&file.path)?;
if needs_reindex {
changed_files.push(file.clone());
debug!("📝 File changed (needs reindex): {}", file.path.display());
} else {
unchanged_files += 1;
debug!("✅ File unchanged: {}", file.path.display());
}
}
let deleted_files = file_meta_store.find_deleted_files();
for (file_path, _chunk_ids) in &deleted_files {
debug!("🗑️ File deleted from disk: {}", file_path);
}
log_print!(" Unchanged files: {}", unchanged_files);
log_print!(" Changed files: {}", changed_files.len());
log_print!(" Deleted files: {}", deleted_files.len());
if changed_files.is_empty() && deleted_files.is_empty() {
log_print!("\n{}", "✅ Database is up to date!".green());
return Ok(());
}
let mut total_chunks_to_delete = 0u32;
for (_, chunk_ids) in deleted_files.iter() {
total_chunks_to_delete += chunk_ids.len() as u32;
}
for file in &changed_files {
let (_, chunk_ids) = file_meta_store.check_file(&file.path)?;
total_chunks_to_delete += chunk_ids.len() as u32;
}
if total_chunks_to_delete > 0 {
log_print!("\n🔄 Deleting {} old chunks...", total_chunks_to_delete);
let mut store = VectorStore::new(&db_path, 384)?; let mut fts_store = FtsStore::new_with_writer(&db_path)?;
for (file_path, chunk_ids) in deleted_files {
if !chunk_ids.is_empty() {
info!(
"🗑️ Deleting {} chunks for deleted file: {}",
chunk_ids.len(),
file_path
);
debug!(" File path: {}", file_path);
store.delete_chunks(&chunk_ids)?;
for chunk_id in &chunk_ids {
fts_store.delete_chunk(*chunk_id)?;
}
}
file_meta_store.remove_file(Path::new(&file_path));
}
for file in &changed_files {
let (_, old_chunk_ids) = file_meta_store.check_file(&file.path)?;
if !old_chunk_ids.is_empty() {
let file_path_str = file.path.to_string_lossy().to_string();
info!(
"🔄 Deleting {} old chunks for changed file: {}",
old_chunk_ids.len(),
file_path_str
);
debug!(" File path: {}", file.path.display());
store.delete_chunks(&old_chunk_ids)?;
for chunk_id in &old_chunk_ids {
fts_store.delete_chunk(*chunk_id)?;
}
}
}
fts_store.commit()?;
log_print!("🔨 Rebuilding vector index after deletions...");
store.build_index()?;
log_print!("✅ Deleted {} chunks", total_chunks_to_delete);
drop(store);
drop(fts_store);
}
log_print!("\n🔄 Processing {} changed files...", changed_files.len());
files = changed_files;
} else {
}
log_print!(
"\n{}",
"Phase 2: Semantic Chunking, Embedding & Storage".bright_cyan()
);
log_print!("{}", "-".repeat(60));
let chunking_start = Instant::now();
let mut chunker = SemanticChunker::new(100, 2000, 10);
let mut total_chunks = 0;
let pb = ProgressBar::new(files.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
.unwrap()
.progress_chars("█▓▒░ "),
);
let cache_dir = crate::constants::get_global_models_cache_dir()?;
let mut embedding_service =
EmbeddingService::with_cache_dir(model_type, Some(cache_dir.as_path()))?;
if crate::constants::check_shutdown(&cancel_token) {
log_print!(
"\n{}",
"⚠️ Indexing cancelled during model loading".yellow()
);
return Ok(());
}
let mut store = VectorStore::new(&db_path, embedding_service.dimensions())?;
let mut fts_store = FtsStore::new_with_writer(&db_path)?;
let mut file_chunks: std::collections::HashMap<String, Vec<u32>> =
std::collections::HashMap::new();
let mut skipped_files = 0;
let mut cancelled = false;
for file in &files {
if crate::constants::check_shutdown(&cancel_token) {
cancelled = true;
break;
}
pb.set_message(format!(
"{}",
file.path.file_name().unwrap().to_string_lossy()
));
debug!("📄 Processing file: {}", file.path.display());
let source_code = match std::fs::read_to_string(&file.path) {
Ok(content) => content,
Err(_) => {
debug!("⚠️ Skipping file (invalid UTF-8): {}", file.path.display());
skipped_files += 1;
pb.inc(1);
continue;
}
};
let chunks = chunker.chunk_semantic(file.language, &file.path, &source_code)?;
let chunk_count = chunks.len();
debug!(
" Created {} chunks for {}",
chunk_count,
file.path.display()
);
if chunks.is_empty() {
pb.inc(1);
continue;
}
let embedded_chunks = match embedding_service.embed_chunks(chunks) {
Ok(chunks) => chunks,
Err(_) if crate::constants::is_shutdown_requested() => {
cancelled = true;
break;
}
Err(e) => return Err(e),
};
if crate::constants::check_shutdown(&cancel_token) {
cancelled = true;
break;
}
let fts_data: Vec<(String, String, Option<String>, String)> = embedded_chunks
.iter()
.map(|ec| {
(
ec.chunk.content.clone(),
ec.chunk.path.clone(),
ec.chunk.signature.clone(),
format!("{:?}", ec.chunk.kind),
)
})
.collect();
let chunk_ids = store.insert_chunks_with_ids(embedded_chunks)?;
for ((content, path, signature, kind), &chunk_id) in fts_data.iter().zip(chunk_ids.iter()) {
if let Err(e) = fts_store.add_chunk(chunk_id, content, path, signature.as_deref(), kind)
{
tracing::warn!(
"FTS add_chunk failed in {}: {} (continuing without FTS for this chunk)",
file.path.display(),
e
);
}
}
let file_path = file.path.to_string_lossy().to_string();
file_chunks.insert(file_path, chunk_ids.clone());
total_chunks += chunk_count;
pb.inc(1);
if total_chunks % 1000 == 0 && total_chunks > 0 {
if let Err(e) = fts_store.commit() {
tracing::warn!(
"Periodic FTS commit failed at {} chunks: {} (continuing, some FTS data may be lost)",
total_chunks,
e
);
}
}
}
if cancelled {
pb.finish_with_message("Cancelled!");
log_print!("\n{}", "⚠️ Indexing cancelled by user".yellow());
drop(embedding_service);
drop(chunker);
if total_chunks > 0 {
if let Err(e) = fts_store.commit() {
log_print!(
"{} FTS commit warning: {} (index may need recovery)",
"⚠️ ".yellow(),
e
);
log_print!(
"{} Run {} to rebuild the index cleanly if needed",
"💡 ".cyan(),
"codesearch index -f".bright_cyan()
);
} else {
log_print!(
" Partial progress: {} chunks written (re-run with --force for clean index)",
total_chunks
);
}
}
return Ok(());
}
let model_short_name = embedding_service.model_short_name().to_string();
let model_name = embedding_service.model_name().to_string();
let model_dimensions = embedding_service.dimensions();
drop(embedding_service);
drop(chunker);
if let Err(e) = fts_store.commit() {
tracing::warn!(
"Final FTS commit failed: {} (vector search will work, but hybrid/BM25 search may have gaps)",
e
);
}
if skipped_files > 0 {
log_print!(" ⚠️ Skipped {} files (invalid UTF-8)", skipped_files);
}
pb.finish_with_message("Done!");
let chunking_duration = chunking_start.elapsed();
log_print!(
"✅ Created and indexed {} chunks in {:?}",
total_chunks,
chunking_duration
);
if total_chunks == 0 {
log_print!("\n{}", "No chunks created!".yellow());
return Ok(());
}
let _fts_stats = fts_store.stats()?;
drop(fts_store);
let storage_start = Instant::now();
store.build_index()?;
let _storage_duration = storage_start.elapsed();
let metadata = serde_json::json!({
"model_short_name": model_short_name,
"model_name": model_name,
"dimensions": model_dimensions,
"indexed_at": chrono::Utc::now().to_rfc3339(),
});
std::fs::write(
db_path.join("metadata.json"),
serde_json::to_string_pretty(&metadata)?,
)?;
if is_incremental {
let mut file_meta_store = file_meta_store.take().unwrap();
let file_count = file_chunks.len();
for (file_path, chunk_ids) in file_chunks {
file_meta_store.update_file(Path::new(&file_path), chunk_ids)?;
}
file_meta_store.save(&db_path)?;
log_print!(
"✅ Updated metadata for {} changed files (unchanged files preserved)",
file_count
);
} else {
let mut file_meta_store =
FileMetaStore::new(model_type.name().to_string(), model_type.dimensions());
for (file_path, chunk_ids) in file_chunks {
file_meta_store.update_file(Path::new(&file_path), chunk_ids)?;
}
file_meta_store.save(&db_path)?;
}
let db_stats = store.stats()?;
log_print!("\n{}", "📊 Final Statistics".bright_green().bold());
log_print!("{}", "=".repeat(60));
log_print!(" Total chunks: {}", db_stats.total_chunks);
log_print!(" Total files: {}", db_stats.total_files);
log_print!(
" Indexed: {}",
if db_stats.indexed {
"✅ Yes"
} else {
"❌ No"
}
);
let mut total_size = 0u64;
for entry in std::fs::read_dir(&db_path)? {
let entry = entry?;
total_size += entry.metadata()?.len();
}
log_print!(
" Database size: {:.2} MB",
total_size as f64 / (1024.0 * 1024.0)
);
log_print!("\n{}", "✨ Indexing complete".bright_green().bold());
log_print!(
" Run {} to search your codebase",
"codesearch search <query>".bright_cyan()
);
Ok(())
}
#[allow(dead_code)] pub async fn list() -> Result<()> {
println!("{}", "📚 Indexed Repositories".bright_cyan().bold());
println!("{}", "=".repeat(60));
let current_dir = std::env::current_dir()?;
let current_db = current_dir.join(".codesearch.db");
if current_db.exists() {
println!("\n{}", "Current Directory:".bright_green());
print_repo_stats(¤t_dir, ¤t_db)?;
}
Ok(())
}
pub async fn stats(path: Option<PathBuf>) -> Result<()> {
let (db_path, project_path) = get_db_path(path)?;
if !db_path.exists() {
println!("{}", "❌ No database found!".red());
println!(" Run {} first", "codesearch index".bright_cyan());
return Ok(());
}
println!("{}", "📊 Database Statistics".bright_cyan().bold());
println!("{}", "=".repeat(60));
println!("💾 Database: {}", db_path.display());
println!("📂 Project: {}", project_path.display());
let store = VectorStore::new(&db_path, 384)?; let stats = store.stats()?;
println!("\n{}", "Vector Store:".bright_green());
println!(" Total chunks: {}", stats.total_chunks);
println!(" Total files: {}", stats.total_files);
println!(
" Indexed: {}",
if stats.indexed { "✅ Yes" } else { "❌ No" }
);
println!(" Dimensions: {}", stats.dimensions);
let mut total_size = 0u64;
for entry in std::fs::read_dir(&db_path)? {
let entry = entry?;
total_size += entry.metadata()?.len();
}
println!("\n{}", "Storage:".bright_green());
println!(
" Database size: {:.2} MB",
total_size as f64 / (1024.0 * 1024.0)
);
println!(
" Avg per chunk: {:.2} KB",
(total_size as f64 / stats.total_chunks as f64) / 1024.0
);
Ok(())
}
pub async fn clear(path: Option<PathBuf>, yes: bool) -> Result<()> {
let (db_path, project_path) = get_db_path(path)?;
if !db_path.exists() {
println!("{}", "❌ No database found!".red());
return Ok(());
}
println!("{}", "🗑️ Clear Database".bright_yellow().bold());
println!("{}", "=".repeat(60));
println!("💾 Database: {}", db_path.display());
println!("📂 Project: {}", project_path.display());
if !yes {
println!("\n{}", "⚠️ This will delete all indexed data!".yellow());
print!("Are you sure? (y/N): ");
use std::io::{self, Write};
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("{}", "Cancelled.".dimmed());
return Ok(());
}
}
println!("\n🔄 Removing database...");
std::fs::remove_dir_all(&db_path)?;
println!("{}", "✅ Database cleared!".green());
Ok(())
}
#[allow(dead_code)] fn print_repo_stats(repo_path: &Path, db_path: &Path) -> Result<()> {
println!(" 📂 {}", repo_path.display());
match VectorStore::new(db_path, 384) {
Ok(store) => match store.stats() {
Ok(stats) => {
println!(
" {} chunks in {} files",
stats.total_chunks, stats.total_files
);
}
Err(_) => {
println!(" {}", "Could not load stats".dimmed());
}
},
Err(_) => {
println!(" {}", "Could not open database".dimmed());
}
}
Ok(())
}
pub async fn add_to_index(
path: Option<PathBuf>,
global: bool,
cancel_token: CancellationToken,
) -> Result<()> {
let project_path = path.as_deref().unwrap_or_else(|| Path::new("."));
let canonical_path = project_path.canonicalize()?;
println!("{}", "➕ Add to Index".bright_green().bold());
println!("{}", "=".repeat(60));
println!("📂 Project: {}", canonical_path.display());
let db_info = find_best_database(path.as_deref())?;
if let Some(db) = db_info {
println!("\n{}", "⚠️ An index already exists!".yellow());
println!("\n{}", "Existing Index:".cyan());
println!(" Path: {}", db.db_path.display());
if db.is_global {
println!(" Type: {}", "Global".bright_green());
} else if !db.is_current {
println!(" Type: {} (parent directory)", "Local".bright_green());
} else {
println!(" Type: {}", "Local".bright_green());
}
println!(
"\n{}",
"You cannot create a separate index for a subdirectory.".yellow()
);
println!(
"{}",
if db.is_global {
"The global index will be used for all projects."
} else if !db.is_current {
"The parent directory index will be used for this subdirectory."
} else {
"An index already exists for this project."
}
);
println!("\n{}", "To use the existing index, simply run:".cyan());
println!(" codesearch index");
return Err(anyhow::anyhow!(
"Index already exists in parent or current directory"
));
}
let local_db = canonical_path.join(".codesearch.db");
let has_local = local_db.exists();
let repos_path = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?
.join(".codesearch")
.join("repos.json");
let has_global = if repos_path.exists() {
let content = fs::read_to_string(&repos_path)?;
if let Ok(repos) =
serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(&content)
{
repos.contains_key(canonical_path.to_str().unwrap_or(""))
} else {
false
}
} else {
false
};
if global && has_local {
println!("\n{}", "❌ Error: Local index already exists!".red());
println!(" A local index already exists at: {}", local_db.display());
println!(" Remove it first with: codesearch index rm");
return Err(anyhow::anyhow!("Local index exists"));
}
if has_local || has_global {
println!(
"\n{}",
"⚠️ Index already exists for this project!".yellow()
);
println!(" Local: {}", if has_local { "✅" } else { "❌" });
println!(" Global: {}", if has_global { "✅" } else { "❌" });
return Ok(());
}
if global {
println!("\n{}", "Creating global index...".cyan());
index(
Some(canonical_path.clone()),
false,
false,
true,
None,
cancel_token.clone(),
)
.await?;
println!("\n{}", "✅ Global index created!".green());
} else {
println!("\n{}", "Creating local index...".cyan());
index(
Some(canonical_path.clone()),
false,
false,
false,
None,
cancel_token,
)
.await?;
println!("\n{}", "✅ Local index created!".green());
}
Ok(())
}
pub async fn remove_from_index(path: Option<PathBuf>) -> Result<()> {
let project_path = path.unwrap_or_else(|| PathBuf::from("."));
let canonical_path = project_path.canonicalize()?;
println!("{}", "➖ Remove Index".bright_red().bold());
println!("{}", "=".repeat(60));
println!("📂 Project: {}", canonical_path.display());
let local_db = canonical_path.join(".codesearch.db");
let has_local = local_db.exists();
let repos_path = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?
.join(".codesearch")
.join("repos.json");
let has_global = if repos_path.exists() {
let content = fs::read_to_string(&repos_path)?;
if let Ok(repos) =
serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(&content)
{
repos.contains_key(canonical_path.to_str().unwrap_or(""))
} else {
false
}
} else {
false
};
if !has_local && !has_global {
println!("\n{}", "⚠️ No index found for this project.".yellow());
return Ok(());
}
if has_local && has_global {
println!(
"\n{}",
"⚠️ Warning: Both local and global indexes exist!".yellow()
);
println!(" Removing local index...");
fs::remove_dir_all(&local_db)?;
println!(" {}", "✅ Local index removed".green());
println!(" (Global index remains)");
return Ok(());
}
if has_local {
println!("\n{}", "Removing local index...".cyan());
fs::remove_dir_all(&local_db)?;
println!("{}", "✅ Local index removed!".green());
} else if has_global {
println!("\n{}", "Removing global index...".cyan());
unregister_repository(&canonical_path)?;
println!("{}", "✅ Global index removed!".green());
}
Ok(())
}
pub async fn list_index_status() -> Result<()> {
println!("{}", "📋 Index Status".bright_cyan().bold());
println!("{}", "=".repeat(60));
let db_info = find_best_database(Some(Path::new(".")))?;
if let Some(db) = db_info {
println!("\n{}", "💾 Database:".cyan());
println!(" Path: {}", db.db_path.display());
if db.is_global {
println!(" Type: {}", "Global".bright_green());
} else {
println!(" Type: {}", "Local".bright_green());
}
if !db.is_current && !db.is_global {
println!(" {}", "(from parent directory)".dimmed());
}
if let Ok(stats) = get_db_stats(&db.db_path).await {
println!(" Status: {}", "✅ Indexed".green());
println!(" Chunks: {}", stats.chunk_count);
println!(" Size: {:.2} MB", stats.size_mb);
} else {
println!(" Status: {}", "⚠️ Could not read database".yellow());
}
} else {
println!("\n{}", "No index found for this project.".dimmed());
println!("\nCreate an index with:");
println!(" codesearch index add # Create local index");
println!(" codesearch index add -g # Create global index");
}
Ok(())
}
async fn get_db_stats(db_path: &Path) -> Result<DbStats> {
use crate::vectordb::VectorStore;
if !db_path.exists() {
return Ok(DbStats {
chunk_count: 0,
size_mb: 0.0,
});
}
let store = VectorStore::new(db_path, 384)?;
let stats = store.stats()?;
let mut total_size = 0u64;
for entry in std::fs::read_dir(db_path)? {
let entry = entry?;
total_size += entry.metadata()?.len();
}
Ok(DbStats {
chunk_count: stats.total_chunks,
size_mb: total_size as f64 / (1024.0 * 1024.0),
})
}
struct DbStats {
chunk_count: usize,
size_mb: f64,
}