use crate::config;
use crate::error::CliError;
use crate::output::OutputFormat;
use crate::style::Style;
use agentero_core::features::catalog::papers::{self, PaperRecord, PaperRefLookup};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct GlobalOpts {
pub vault_flag: Option<PathBuf>,
pub yes: bool,
pub quiet: bool,
pub translator_url: Option<String>,
pub format: OutputFormat,
pub pretty: bool,
pub style: Style,
}
impl GlobalOpts {
pub fn translator_base_url(&self) -> Option<String> {
if let Some(u) = &self.translator_url {
let t = u.trim();
if !t.is_empty() {
return Some(t.to_string());
}
}
if let Ok(env) = std::env::var("AGENTERO_TRANSLATOR_URL") {
let t = env.trim();
if !t.is_empty() {
return Some(t.to_string());
}
}
config::load()
.ok()
.and_then(|c| c.translator_base_url)
.filter(|s| !s.trim().is_empty())
}
}
pub fn resolve_vault(globals: &GlobalOpts) -> Result<PathBuf, CliError> {
if let Some(p) = &globals.vault_flag {
return canonicalize_existing(p).or_else(|_| {
let abs = absolutize(p)?;
if abs.is_dir() {
Ok(abs)
} else {
Err(CliError::vault_not_found(format!(
"vault path is not a directory: {}",
abs.display()
)))
}
});
}
if let Ok(env) = std::env::var("AGENTERO_VAULT") {
let t = env.trim();
if !t.is_empty() {
let p = PathBuf::from(t);
return canonicalize_existing(&p).map_err(|_| {
CliError::vault_not_found(format!("AGENTERO_VAULT is not a directory: {t}"))
});
}
}
if let Ok(cwd) = std::env::current_dir() {
if let Some(found) = walk_up_vault(&cwd) {
return Ok(found);
}
}
if let Ok(cfg) = config::load() {
if let Some(p) = cfg.default_vault {
let path = PathBuf::from(p.trim());
if !path.as_os_str().is_empty() {
return canonicalize_existing(&path).map_err(|_| {
CliError::vault_not_found(format!(
"config default_vault is not a directory: {}",
path.display()
))
});
}
}
}
Err(CliError::vault_not_found(
"could not resolve vault (pass --vault, set AGENTERO_VAULT, or cd into a vault)",
))
}
fn absolutize(p: &Path) -> Result<PathBuf, CliError> {
if p.is_absolute() {
Ok(p.to_path_buf())
} else {
Ok(std::env::current_dir()?.join(p))
}
}
fn canonicalize_existing(p: &Path) -> Result<PathBuf, CliError> {
let abs = absolutize(p)?;
if !abs.is_dir() {
return Err(CliError::vault_not_found(format!(
"not a directory: {}",
abs.display()
)));
}
Ok(agentero_core::fs::canonicalize_best_effort(&abs))
}
pub fn looks_like_vault(root: &Path) -> bool {
let catalog = root.join(".agentero").join("catalog.sqlite");
if catalog.is_file() {
return true;
}
root.join("papers").is_dir() && root.join("notes").is_dir() && root.join(".agentero").is_dir()
}
fn walk_up_vault(start: &Path) -> Option<PathBuf> {
let mut cur = start.to_path_buf();
loop {
if looks_like_vault(&cur) {
return Some(agentero_core::fs::canonicalize_best_effort(&cur));
}
if !cur.pop() {
return None;
}
}
}
pub fn resolve_paper(
vault: &Path,
ref_: &str,
globals: &GlobalOpts,
) -> Result<PaperRecord, CliError> {
let ref_ = ref_.trim();
if ref_.is_empty() {
return Err(CliError::usage("paper ref is required"));
}
match papers::lookup_paper_ref(vault, ref_)? {
PaperRefLookup::Found(record) => Ok(*record),
PaperRefLookup::NotFound => Err(CliError::paper_not_found(ref_)),
PaperRefLookup::Ambiguous(candidates) => {
if let Some(path) = crate::prompt::select_one(
globals,
&format!("Multiple papers match id '{ref_}'. Choose a vault-relative path"),
candidates.clone(),
)? {
return papers::get_by_path(vault, &path)?
.ok_or_else(|| CliError::paper_not_found(&path));
}
Err(CliError::paper_ambiguous(ref_, &candidates))
}
}
}
pub fn paper_dir(vault: &Path, path_rel: &str) -> PathBuf {
let mut p = vault.to_path_buf();
for part in path_rel.split('/').filter(|s| !s.is_empty()) {
p.push(part);
}
p
}