use anyhow::{Context, Result};
use colored::Colorize;
use std::path::PathBuf;
use crate::allowlist::{add_to_allowlist, AllowlistConfig, AllowlistEntry};
#[derive(Debug)]
pub enum AllowlistAddError {
Denied(String),
Io(anyhow::Error),
}
impl std::fmt::Display for AllowlistAddError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AllowlistAddError::Denied(reason) => write!(f, "denied: {reason}"),
AllowlistAddError::Io(e) => write!(f, "{e:#}"),
}
}
}
impl std::error::Error for AllowlistAddError {}
fn add_to_allowlist_checked(entry: AllowlistEntry) -> std::result::Result<(), AllowlistAddError> {
if let Some(reason) = crate::allowlist::is_denied(&entry.path) {
return Err(AllowlistAddError::Denied(reason));
}
add_to_allowlist(entry, None).map_err(AllowlistAddError::Io)
}
pub async fn handle_allowlist_add(path: PathBuf, name: Option<String>) -> Result<()> {
let absolute = if path.is_absolute() {
path.clone()
} else {
std::env::current_dir()
.context("could not determine current directory")?
.join(&path)
};
let canonical = std::fs::canonicalize(&absolute)
.with_context(|| format!("could not resolve path '{}'", absolute.display()))?;
let entry = AllowlistEntry {
path: canonical.clone(),
name: name.clone(),
exclude: vec![],
extensions: vec![],
skip_kg: false,
};
match add_to_allowlist_checked(entry) {
Ok(()) => {
let basename = canonical
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "<unnamed>".to_string());
let display_name = name.as_deref().unwrap_or(&basename);
println!(
"{} '{}' added to allowlist: {}",
"✓".green(),
display_name.bold(),
canonical.display()
);
println!(
" Run {} to register and index it.",
format!("trusty-search index {}", canonical.display()).cyan()
);
}
Err(AllowlistAddError::Denied(reason)) => {
anyhow::bail!("{} {}", "denied:".red(), reason);
}
Err(AllowlistAddError::Io(e)) => {
return Err(e).context("could not write to allowlist");
}
}
Ok(())
}
pub async fn handle_allowlist_list(json: bool) -> Result<()> {
let cfg = AllowlistConfig::load().context("could not load allowlist")?;
let allowlist_path = AllowlistConfig::default_path();
if json {
let entries: Vec<serde_json::Value> = cfg
.entries
.iter()
.map(|e| {
serde_json::json!({
"path": e.path,
"name": e.name,
"exclude": e.exclude,
"extensions": e.extensions,
"skip_kg": e.skip_kg,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}
if cfg.entries.is_empty() {
println!(
"{} Allowlist is empty — nothing can be indexed (default-deny).",
"ℹ".yellow()
);
println!(
" Use {} to approve a path.",
"trusty-search index add <path>".cyan()
);
println!(" Config: {}", allowlist_path.display());
return Ok(());
}
println!(
"{} {} path{} in allowlist ({})",
"✓".green(),
cfg.entries.len(),
if cfg.entries.len() == 1 { "" } else { "s" },
allowlist_path.display()
);
for entry in &cfg.entries {
let name_part = match &entry.name {
Some(n) => format!(" ({})", n.bold()),
None => String::new(),
};
let extras: Vec<String> = {
let mut v = Vec::new();
if entry.skip_kg {
v.push("skip_kg".into());
}
if !entry.exclude.is_empty() {
v.push(format!("exclude: {}", entry.exclude.join(",")));
}
if !entry.extensions.is_empty() {
v.push(format!("ext: {}", entry.extensions.join(",")));
}
v
};
let extras_part = if extras.is_empty() {
String::new()
} else {
format!(" [{}]", extras.join(", "))
};
println!(" {}{}{}", entry.path.display(), name_part, extras_part);
}
Ok(())
}