use super::daemon_utils::daemon_base_url;
use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::path::PathBuf;
pub async fn handle_index_relocate(cli_index: &Option<String>, new_path: PathBuf) -> Result<()> {
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 = resolve_index_id(&client, &base, cli_index).await?;
let canonical_new = new_path
.canonicalize()
.with_context(|| format!("new path does not exist: {}", new_path.display()))?;
let patch_url = format!("{base}/indexes/{index_id}");
let body = serde_json::json!({ "root_path": canonical_new.to_string_lossy() });
let resp = client
.patch(&patch_url)
.json(&body)
.send()
.await
.with_context(|| format!("could not reach daemon at {base}"))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
bail!("daemon returned {status} for PATCH {patch_url}: {text}");
}
let result: serde_json::Value = resp
.json()
.await
.context("could not parse PATCH response")?;
let new_root = result
.get("new_root_path")
.and_then(|v| v.as_str())
.unwrap_or(canonical_new.to_str().unwrap_or("(new path)"));
let allowlist_entry = crate::allowlist::AllowlistEntry {
path: canonical_new.clone(),
name: None,
exclude: Vec::new(),
extensions: Vec::new(),
skip_kg: false,
};
if let Err(e) = crate::allowlist::add_to_allowlist(allowlist_entry, None) {
tracing::warn!(
path = %canonical_new.display(),
error = %e,
"could not update allowlist to new path after relocation"
);
}
println!(
"{} Index '{}' relocated to {}",
"\u{2713}".green(),
index_id.bold(),
new_root.bold(),
);
println!(
" Run {} to incrementally re-embed only changed files.",
"trusty-search index".cyan()
);
Ok(())
}
async fn resolve_index_id(
client: &reqwest::Client,
base: &str,
cli_index: &Option<String>,
) -> Result<String> {
if let Some(id) = cli_index {
return Ok(id.clone());
}
let cwd = std::env::current_dir().context("could not determine current directory")?;
let canonical_cwd = std::fs::canonicalize(&cwd).unwrap_or_else(|_| cwd.clone());
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();
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(std::path::PathBuf::from);
let Some(root) = root else { continue };
let canonical_root = std::fs::canonicalize(&root).unwrap_or_else(|_| root.clone());
if canonical_cwd.starts_with(&canonical_root) {
return Ok(id);
}
}
bail!(
"no index registered for the current directory ({}); \
use -i <id> to specify an index explicitly, or run \
`trusty-search list` to see registered indexes",
cwd.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_index_id_uses_explicit_arg() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.unwrap();
let client = reqwest::Client::new();
let id = rt.block_on(resolve_index_id(
&client,
"http://127.0.0.1:0", &Some("my-index".to_string()),
));
assert!(id.is_ok());
assert_eq!(id.unwrap(), "my-index");
}
}