use std::fs;
use std::path::{Path, PathBuf};
use crate::validate;
use crate::validate::hosts::{HostsConfig, validate_hosts_yaml};
#[derive(Debug, thiserror::Error)]
pub enum SourceError {
#[error("source repo not found at `{0}`")]
SourceRepoNotFound(PathBuf),
#[error(
"archive clone path not found at `{0}` (set `--archive` or seed `archive_clone_path` in the local config)"
)]
ArchiveCloneMissing(PathBuf),
#[error("failed to load archive `config/hosts.yaml`: {0}")]
HostsLoadFailed(String),
#[error("failed to parse archive `config/hosts.yaml`: {0}")]
HostsParseFailed(String),
#[error("io error: {0}")]
Io(String),
}
pub fn resolve_source_repo(arg: Option<&Path>) -> Result<PathBuf, SourceError> {
let candidate = match arg {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().map_err(|e| SourceError::Io(e.to_string()))?,
};
let root = nils_common::git::repo_root_in(&candidate)
.map_err(|e| SourceError::Io(e.to_string()))?
.ok_or_else(|| SourceError::SourceRepoNotFound(candidate.clone()))?;
if !root.is_dir() {
return Err(SourceError::SourceRepoNotFound(root));
}
Ok(root)
}
pub fn resolve_archive(arg: Option<&Path>) -> Result<PathBuf, SourceError> {
let candidate = match arg {
Some(p) => p.to_path_buf(),
None => default_archive_clone_path()?,
};
if !candidate.is_dir() {
return Err(SourceError::ArchiveCloneMissing(candidate));
}
Ok(candidate)
}
pub fn default_archive_clone_path() -> Result<PathBuf, SourceError> {
let local = validate::local::validate_local_path(&local_config_path())
.map_err(|e| SourceError::Io(e.to_string()))?;
Ok(local.data.config.archive_clone_path)
}
pub fn local_config_path() -> PathBuf {
if let Some(p) = std::env::var_os("PLAN_ARCHIVE_LOCAL_CONFIG") {
return PathBuf::from(p);
}
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
return PathBuf::from(xdg)
.join("agent-plan-archive")
.join("config.yaml");
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
.join(".config")
.join("agent-plan-archive")
.join("config.yaml");
}
PathBuf::from("/nonexistent/agent-plan-archive/config.yaml")
}
pub fn hosts_path_for(archive: &Path, override_path: Option<&Path>) -> PathBuf {
match override_path {
Some(p) => p.to_path_buf(),
None => archive.join("config").join("hosts.yaml"),
}
}
pub fn load_hosts(path: &Path) -> Result<HostsConfig, SourceError> {
let raw = fs::read_to_string(path).map_err(|e| SourceError::HostsLoadFailed(e.to_string()))?;
let v = validate_hosts_yaml(&raw).map_err(|e| SourceError::HostsParseFailed(e.to_string()))?;
Ok(v.data.config)
}
pub fn has_dirty_path(repo: &Path, rel: &Path) -> Result<bool, SourceError> {
let out = nils_common::git::run_output_in(
repo,
&["status", "--porcelain", "--", &rel.to_string_lossy()],
)
.map_err(|e| SourceError::Io(e.to_string()))?;
if !out.status.success() {
return Err(SourceError::Io(format!(
"git status failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(!out.stdout.is_empty())
}