mod http;
mod marker;
use super::daemon_utils::daemon_base_url;
use super::reindex_engine::register_index_with_daemon;
use crate::config::GlobalConfig;
use http::{fetch_known_index_ids, wait_for_daemon_ready};
use marker::{default_scan_paths, detect_project_marker, ProjectMarker};
use std::time::Duration;
pub async fn auto_discover_and_index() {
let cfg = match GlobalConfig::load() {
Ok(c) => c,
Err(e) => {
tracing::warn!("auto-discover: could not load global config: {e:#} — skipping");
return;
}
};
let scan_paths = if cfg.scan_paths.is_empty() {
default_scan_paths()
} else {
cfg.scan_paths.clone()
};
if scan_paths.is_empty() {
tracing::debug!("auto-discover: no scan paths configured and no defaults found — skipping");
return;
}
let base = daemon_base_url();
let client = match trusty_common::server::daemon_http_client() {
Ok(c) => c,
Err(e) => {
tracing::warn!("auto-discover: could not build HTTP client: {e:#} — skipping");
return;
}
};
if !wait_for_daemon_ready(&client, &base, Duration::from_secs(15)).await {
tracing::warn!(
"auto-discover: daemon at {base} did not become ready within 15s — skipping"
);
return;
}
let known: std::collections::HashSet<String> = match fetch_known_index_ids(&client, &base).await
{
Ok(s) => s,
Err(e) => {
tracing::warn!("auto-discover: could not list indexes: {e:#} — skipping");
return;
}
};
let mut discovered = 0usize;
let mut indexed = 0usize;
for root in &scan_paths {
if !root.is_dir() {
tracing::debug!(
"auto-discover: skipping non-directory scan path {}",
root.display()
);
continue;
}
let entries = match std::fs::read_dir(root) {
Ok(it) => it,
Err(e) => {
tracing::warn!("auto-discover: could not read {}: {e}", root.display());
continue;
}
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let marker = detect_project_marker(&path);
if marker == ProjectMarker::None {
continue;
}
discovered += 1;
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) if !n.is_empty() => n.to_string(),
_ => {
tracing::debug!(
"auto-discover: skipping {} (no usable name)",
path.display()
);
continue;
}
};
if known.contains(&name) {
tracing::debug!(
"auto-discover: skipping {} (index '{}' already registered)",
path.display(),
name
);
continue;
}
tracing::info!(
"auto-discover: indexing {} as '{}' (marker={:?})",
path.display(),
name,
marker
);
match register_index_with_daemon(&name, &path).await {
Ok((_created, true)) => {
if !path.exists() {
tracing::info!(
"auto-discover: skipping reindex of '{}' — root path '{}' \
does not exist on disk (dead/moved project)",
name,
path.display()
);
continue;
}
let reindex_url = format!("{base}/indexes/{name}/reindex");
match client
.post(&reindex_url)
.json(&serde_json::json!({ "background": true }))
.send()
.await
{
Ok(resp) if resp.status().is_success() => {
indexed += 1;
}
Ok(resp) => {
tracing::warn!(
"auto-discover: reindex of '{name}' returned HTTP {}",
resp.status()
);
}
Err(e) => {
tracing::warn!(
"auto-discover: could not POST reindex for '{name}': {e}"
);
}
}
}
Ok((_, false)) => {
tracing::warn!(
"auto-discover: daemon unreachable while registering '{name}' — aborting"
);
return;
}
Err(e) => {
tracing::warn!("auto-discover: could not register '{name}': {e:#}");
}
}
}
}
if discovered > 0 {
tracing::info!(
"auto-discover: scanned {} root(s); discovered {} project(s); queued {} for indexing",
scan_paths.len(),
discovered,
indexed
);
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
fn tempdir_unique(label: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let p = std::env::temp_dir().join(format!("trusty-discover-{label}-{pid}-{nanos}"));
let _ = fs::remove_dir_all(&p);
fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn root_path_exists_true_for_real_dir() {
let dir = tempdir_unique("exists");
assert!(dir.exists(), "freshly-created dir must exist");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn root_path_exists_false_after_removal() {
let dir = tempdir_unique("gone");
fs::remove_dir_all(&dir).ok();
assert!(
!dir.exists(),
"deleted dir must not exist — the dead-root skip predicate would fire"
);
}
#[test]
fn root_path_exists_false_for_never_created() {
let phantom =
std::env::temp_dir().join("trusty-discover-phantom-path-that-will-never-exist-12345");
let _ = fs::remove_dir_all(&phantom);
assert!(
!phantom.exists(),
"phantom path must not exist — dead-root skip would fire for this index"
);
}
}