use crate::{
convert_one, daemon_base_url, find_all_mvs_configs, find_mvs_config, parse_mvs_config,
print_convert_line, ConvertResult, ConvertStatus, ConvertTarget,
};
use anyhow::Result;
use colored::Colorize;
pub async fn handle_convert(
target: ConvertTarget,
dry_run: bool,
concurrency: usize,
) -> Result<()> {
let base = daemon_base_url();
crate::commands::daemon_guard::ensure_daemon_running_or_exit(&base).await;
match target {
ConvertTarget::Project => handle_convert_project(dry_run, &base).await,
ConvertTarget::All => handle_convert_all(dry_run, concurrency, base).await,
}
}
async fn handle_convert_project(dry_run: bool, base: &str) -> Result<()> {
let cwd = std::env::current_dir()?;
let config_path = find_mvs_config(&cwd).ok_or_else(|| {
anyhow::anyhow!(
"No .mcp-vector-search/config.json found in {} or any parent directory",
cwd.display()
)
})?;
let (root, name) = parse_mvs_config(&config_path)?;
if dry_run {
println!(
"{} Dry run — would convert '{}' ({})",
"·".dimmed(),
name.bold(),
root.display()
);
return Ok(());
}
println!(
"{} Converting '{}' ({})…",
"⟳".cyan(),
name.bold(),
root.display()
);
let result = convert_one(root, name, base, false).await;
match &result.status {
ConvertStatus::Queued => {
println!(
"{} Queued for reindex — watch progress with: {}",
"✓".green(),
"trusty-search status".cyan()
);
}
ConvertStatus::AlreadyRegistered => {
println!("{} Already registered — reindex queued", "↻".cyan());
}
ConvertStatus::Failed(msg) => {
eprintln!("{} Conversion failed: {}", "✗".red(), msg);
std::process::exit(1);
}
ConvertStatus::DryRun => unreachable!(),
}
Ok(())
}
async fn handle_convert_all(dry_run: bool, concurrency: usize, base: String) -> Result<()> {
let home_display = dirs::home_dir()
.map(|h| h.display().to_string())
.unwrap_or_else(|| "$HOME".to_string());
println!(
"🔍 Scanning for mcp-vector-search projects under {}…",
home_display
);
let configs = find_all_mvs_configs();
if configs.is_empty() {
println!("{} No mcp-vector-search projects found.", "·".dimmed());
return Ok(());
}
if dry_run {
println!(
"{} Dry run — would convert {} projects:\n",
"·".dimmed(),
configs.len()
);
} else {
println!(
"{} Found {} projects. Converting (max {} concurrent)…\n",
"·".dimmed(),
configs.len(),
concurrency
);
}
let total = configs.len();
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency.max(1)));
let base = std::sync::Arc::new(base);
let mut tasks = tokio::task::JoinSet::new();
for (i, config_path) in configs.into_iter().enumerate() {
let sem = sem.clone();
let base = base.clone();
tasks.spawn(async move {
let _permit = sem.acquire_owned().await.ok();
let parsed = parse_mvs_config(&config_path);
let result = match parsed {
Ok((root, name)) => convert_one(root, name, &base, dry_run).await,
Err(e) => ConvertResult {
name: config_path.display().to_string(),
path: config_path.clone(),
status: ConvertStatus::Failed(format!("parse: {e}")),
},
};
(i + 1, result)
});
}
let mut queued = 0usize;
let mut already = 0usize;
let mut dry = 0usize;
let mut failed = 0usize;
let mut results: Vec<(usize, ConvertResult)> = Vec::with_capacity(total);
while let Some(joined) = tasks.join_next().await {
match joined {
Ok((i, r)) => results.push((i, r)),
Err(e) => eprintln!("{} task panicked: {e}", "✗".red()),
}
}
results.sort_by_key(|(i, _)| *i);
for (i, r) in &results {
print_convert_line(*i, total, r);
match r.status {
ConvertStatus::Queued => queued += 1,
ConvertStatus::AlreadyRegistered => already += 1,
ConvertStatus::DryRun => dry += 1,
ConvertStatus::Failed(_) => failed += 1,
}
}
println!();
if dry_run {
println!("{} Dry run complete: {} projects", "·".dimmed(), dry);
} else {
println!(
"{} Summary: {} queued, {} already registered (reindexing), {} failed",
"✓".green(),
queued,
already,
failed
);
println!(
" Reindexing in background. Run {} to see progress.",
"trusty-search list".cyan()
);
}
Ok(())
}