use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use toml::Table;
pub const DEFAULTS: &str = "defaults.toml";
pub const MANAGED: &str = "managed.toml";
pub fn system_dir() -> PathBuf {
if let Some(d) = std::env::var_os("INDRA_SYSTEM_DIR").filter(|v| !v.is_empty()) {
return PathBuf::from(d);
}
if cfg!(windows) {
let base = std::env::var_os("ProgramData")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
return base.join("indra");
}
if cfg!(target_os = "macos") {
return PathBuf::from("/Library/Application Support/indra");
}
PathBuf::from("/etc/indra")
}
fn read(path: &Path) -> Result<Option<Table>, String> {
match std::fs::read_to_string(path) {
Ok(t) => t
.parse::<Table>()
.map(Some)
.map_err(|e| format!("{}: {}", path.display(), e)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("cannot read {}: {}", path.display(), e)),
}
}
pub struct Layers {
pub defaults: Option<Table>,
pub managed: Option<Table>,
}
pub fn layers() -> Result<Layers, String> {
let dir = system_dir();
Ok(Layers {
defaults: read(&dir.join(DEFAULTS))?,
managed: read(&dir.join(MANAGED))?,
})
}
pub fn apply(user: Table, l: &Layers) -> Table {
let mut out = l.defaults.clone().unwrap_or_default();
merge(&mut out, user);
if let Some(m) = &l.managed {
merge(&mut out, m.clone());
}
out
}
pub fn merge(base: &mut Table, top: Table) {
for (k, v) in top {
match (base.get_mut(&k), v) {
(Some(toml::Value::Table(b)), toml::Value::Table(t)) => merge(b, t),
(Some(toml::Value::Array(b)), toml::Value::Array(t)) if named(b) && named(&t) => {
for item in t {
let name = name_of(&item);
match b.iter_mut().find(|x| name_of(x) == name) {
Some(slot) => *slot = item,
None => b.push(item),
}
}
}
(_, v) => {
base.insert(k, v);
}
}
}
}
fn name_of(v: &toml::Value) -> Option<&str> {
v.as_table()?.get("name")?.as_str()
}
fn named(a: &[toml::Value]) -> bool {
a.iter().all(|x| name_of(x).is_some())
}
pub fn enforced(m: &Table) -> Vec<String> {
let mut out = Vec::new();
for (k, v) in m {
match v {
toml::Value::Table(t) => {
for kk in t.keys() {
out.push(format!("{}.{}", k, kk));
}
}
toml::Value::Array(a) if named(a) => {
for x in a {
out.push(format!("{}.{}", k, name_of(x).unwrap_or("?")));
}
}
_ => out.push(k.clone()),
}
}
out
}
pub fn view() -> Value {
let dir = system_dir();
let d = dir.join(DEFAULTS);
let m = dir.join(MANAGED);
let (enforced, error) = match read(&m) {
Ok(Some(t)) => (enforced(&t), None),
Ok(None) => (vec![], None),
Err(e) => (vec![], Some(e)),
};
json!({
"dir": dir.display().to_string(),
"defaults": d.is_file(), "managed": m.is_file(),
"enforced": enforced, "error": error,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn t(s: &str) -> Table {
s.parse().unwrap()
}
#[test]
fn defaults_under_user_under_managed() {
let l = Layers {
defaults: Some(t(
"[local]\nmax_parallel = 2\nshell_steps = true\n[[profile]]\nname = \"reviewer\"\nmodel = \"sonnet\"\n[[profile]]\nname = \"fast\"\nmodel = \"haiku\"\n",
)),
managed: Some(t(
"[routing]\nstrategy = \"in_order\"\n[[profile]]\nname = \"reviewer\"\nmodel = \"opus\"\n[[runner]]\nname = \"claude\"\ncommand = \"claude\"\n",
)),
};
let user = t("[local]\nmax_parallel = 8\n[routing]\nstrategy = \"round_robin\"\n[[profile]]\nname = \"fast\"\nmodel = \"sonnet\"\n[[profile]]\nname = \"mine\"\n");
let out = apply(user, &l);
assert_eq!(out["local"]["max_parallel"].as_integer(), Some(8));
assert_eq!(out["local"]["shell_steps"].as_bool(), Some(true));
assert_eq!(out["routing"]["strategy"].as_str(), Some("in_order"));
let p = out["profile"].as_array().unwrap();
let model = |n: &str| {
p.iter()
.find(|x| x["name"].as_str() == Some(n))
.and_then(|x| x.get("model"))
.and_then(|m| m.as_str())
.map(String::from)
};
assert_eq!(model("reviewer").as_deref(), Some("opus"));
assert_eq!(model("fast").as_deref(), Some("sonnet"));
assert!(p.iter().any(|x| x["name"].as_str() == Some("mine")));
assert_eq!(out["runner"].as_array().unwrap().len(), 1);
let mut e = enforced(l.managed.as_ref().unwrap());
e.sort();
assert_eq!(
e,
vec!["profile.reviewer", "routing.strategy", "runner.claude"]
);
}
#[test]
fn no_files_change_nothing() {
let user = t("[local]\nmax_parallel = 3\n");
let out = apply(
user.clone(),
&Layers {
defaults: None,
managed: None,
},
);
assert_eq!(out, user);
}
}