1use anyhow::{Context, bail};
2use serde::{Deserialize, Serialize};
3use std::{
4 collections::BTreeMap,
5 fs,
6 path::{Path, PathBuf},
7};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Project {
11 pub project: String,
12 pub environments: BTreeMap<String, Environment>,
13}
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Environment {
16 pub file: String,
17 #[serde(default = "default_schema")]
18 pub schema: String,
19 #[serde(default)]
20 pub recipients: Vec<String>,
21 #[serde(default)]
22 pub key_file: Option<String>,
23 #[serde(default)]
24 pub editor: Option<String>,
25}
26fn default_schema() -> String {
27 "config/env.schema.yaml".into()
28}
29pub fn discover(start: &Path) -> anyhow::Result<PathBuf> {
30 let mut current = start.canonicalize()?;
31 loop {
32 let candidate = current.join("open-envault.yaml");
33 if candidate.is_file() {
34 return Ok(candidate);
35 }
36 if !current.pop() {
37 bail!("open-envault.yaml not found")
38 }
39 }
40}
41pub fn load(path: &Path) -> anyhow::Result<Project> {
42 let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
43 yaml_serde::from_str(&text).context("parse open-envault.yaml")
44}
45pub fn environment<'a>(project: &'a Project, name: &str) -> anyhow::Result<&'a Environment> {
46 project
47 .environments
48 .get(name)
49 .with_context(|| format!("unknown environment: {name}"))
50}
51pub fn atomic_write(path: &Path, content: &[u8]) -> anyhow::Result<()> {
52 let tmp = path.with_extension("tmp");
53 fs::write(&tmp, content)?;
54 fs::rename(tmp, path)?;
55 Ok(())
56}