Add Phase 1 frxd implementation with conformance test suite

This commit is contained in:
George Coles
2026-09-15 03:07:17 -04:00
parent 81ef27179b
commit 82a95fb907
23 changed files with 7295 additions and 3 deletions
+115
View File
@@ -0,0 +1,115 @@
use std::path::Path;
use anyhow::{Context, Result};
use crate::config::Config;
use crate::index::{Collection, LocalIndex, load_collections, save_collections};
use crate::message::{EXPOSURE_FULL, EXPOSURE_METADATA};
use crate::node;
use crate::render;
pub fn add(
config_path: &Path,
path: &Path,
name: Option<String>,
shared: bool,
exposure: &str,
) -> Result<()> {
let config = Config::load(config_path)?;
let index = LocalIndex::open(&config.index_dir())?;
let name = name.unwrap_or_else(|| {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("collection")
.to_string()
});
let exposure = if exposure == EXPOSURE_FULL {
EXPOSURE_FULL
} else {
EXPOSURE_METADATA
};
let collection = Collection {
name: name.clone(),
path: path
.canonicalize()
.with_context(|| format!("resolving {}", path.display()))?
.display()
.to_string(),
shared,
exposure: exposure.to_string(),
};
let added = index.add_collection(&collection)?;
let manifest = config.collections_path();
let mut collections = load_collections(&manifest)?;
collections.retain(|existing| existing.name != name);
collections.push(collection);
save_collections(&manifest, &collections)?;
println!(
"indexed {added} file(s) from {} as collection '{name}' (shared={shared}, exposure={exposure})",
path.display()
);
Ok(())
}
pub fn reindex(config_path: &Path) -> Result<()> {
let config = Config::load(config_path)?;
let index = LocalIndex::open(&config.index_dir())?;
let collections = load_collections(&config.collections_path())?;
if collections.is_empty() {
println!("no collections registered; use `frxd add <path>`");
return Ok(());
}
for collection in collections {
let added = index.add_collection(&collection)?;
println!(
"collection '{}' ({}): {added} file(s)",
collection.name, collection.path
);
}
Ok(())
}
pub async fn search(config_path: &Path, text: &str, limit: usize) -> Result<()> {
let config = Config::load(config_path)?;
let index = LocalIndex::open(&config.index_dir())?;
let (hits, total) = index.search(text, limit, false)?;
render::local_hits(&hits, total);
Ok(())
}
pub async fn query(
config_path: &Path,
text: &str,
max_results: Option<usize>,
timeout_ms: Option<u64>,
network: bool,
) -> Result<()> {
let config = Config::load(config_path)?;
let base = format!("http://{}", config.node.listen);
let outcome = node::control_query(&base, text, max_results, timeout_ms, network).await?;
render::query_outcome(&outcome);
Ok(())
}
pub async fn status(config_path: &Path) -> Result<()> {
let config = Config::load(config_path)?;
let base = format!("http://{}", config.node.listen);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()?;
match client.get(format!("{base}/v1/local/status")).send().await {
Ok(response) if response.status().is_success() => {
let value: serde_json::Value = response.json().await?;
println!("{}", serde_json::to_string_pretty(&value)?);
}
Ok(response) => println!("node at {base} responded {}", response.status()),
Err(_) => {
println!("node not running at {base}");
println!("config: {}", config_path.display());
println!("name: {}", config.node.name);
println!("data dir: {}", config.data_dir().display());
println!("relays: {:?}", config.node.relays);
}
}
Ok(())
}