use super::daemon_utils::daemon_base_url;
use crate::config::GlobalConfig;
use crate::detect::detect_project;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::path::{Path, PathBuf};
pub async fn handle_index_remove(cli_path: Option<PathBuf>) -> Result<()> {
let target_path = resolve_target_path(cli_path)?;
let base = daemon_base_url();
crate::commands::daemon_guard::ensure_daemon_running_or_exit(&base).await?;
let client = trusty_common::server::daemon_http_client()?;
let (index_id, registered_path) = find_index_by_path(&client, &base, &target_path).await?;
let delete_url = format!("{}/indexes/{}", base, index_id);
match client.delete(&delete_url).send().await {
Ok(resp) if resp.status().is_success() => {}
Ok(resp) => bail!(
"daemon returned {} for DELETE {}",
resp.status(),
delete_url
),
Err(e) => bail!("could not reach daemon at {}: {e}", base),
}
match GlobalConfig::load() {
Ok(mut cfg) => {
let removed = cfg.remove_collection_by_path(®istered_path);
if removed.is_some() {
if let Err(e) = cfg.save() {
tracing::warn!("could not update global config after removal: {e:#}");
}
}
}
Err(e) => {
tracing::warn!("could not load global config to remove entry: {e:#}");
}
}
println!(
"{} Removed index {} ({})",
"✓".green(),
format!("\"{index_id}\"").bold(),
registered_path.display()
);
Ok(())
}
fn resolve_target_path(cli_path: Option<PathBuf>) -> Result<PathBuf> {
if let Some(p) = cli_path {
return Ok(p);
}
let cwd = std::env::current_dir().context("could not resolve current directory")?;
let ctx = detect_project(&cwd);
Ok(ctx.root_path)
}
async fn find_index_by_path(
client: &reqwest::Client,
base: &str,
target: &Path,
) -> Result<(String, PathBuf)> {
let list_url = format!("{base}/indexes");
let list_body: serde_json::Value = client
.get(&list_url)
.send()
.await
.with_context(|| format!("could not reach daemon at {base}"))?
.error_for_status()
.with_context(|| format!("daemon error for {list_url}"))?
.json()
.await
.context("could not parse /indexes response")?;
let empty: Vec<serde_json::Value> = Vec::new();
let ids: Vec<String> = list_body
.get("indexes")
.and_then(|v| v.as_array())
.unwrap_or(&empty)
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let canonical_target = std::fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf());
for id in ids {
let url = format!("{base}/indexes/{id}/status");
let resp = match client.get(&url).send().await {
Ok(r) if r.status().is_success() => r,
_ => continue,
};
let body: serde_json::Value = match resp.json().await {
Ok(b) => b,
Err(_) => continue,
};
let root = body
.get("root_path")
.and_then(|v| v.as_str())
.map(PathBuf::from);
let Some(root) = root else {
continue;
};
let canonical_root = std::fs::canonicalize(&root).unwrap_or_else(|_| root.clone());
if canonical_root == canonical_target {
return Ok((id, root));
}
}
bail!(
"no index registered for path {}; run `trusty-search list` to see registered indexes",
target.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn index_remove_resolves_path_uses_cli() {
let p = resolve_target_path(Some(PathBuf::from("/explicit/path"))).unwrap();
assert_eq!(p, PathBuf::from("/explicit/path"));
}
#[test]
fn index_remove_resolves_path_falls_back_to_cwd() {
let p = resolve_target_path(None).unwrap();
assert!(!p.as_os_str().is_empty());
}
#[test]
fn index_remove_resolves_path_uses_detected_root() {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let tmp = std::env::temp_dir().join(format!("trusty-idxrm-{pid}-{nanos}"));
fs::create_dir_all(tmp.join(".git")).unwrap();
let nested = tmp.join("a");
fs::create_dir_all(&nested).unwrap();
let ctx = detect_project(&nested);
assert_eq!(ctx.root_path, tmp);
let _ = fs::remove_dir_all(&tmp);
}
}