Skip to main content

agent_fleet/
enroll.rs

1//! enroll — write the typescript-bun kit into a target repo (SPEC §3.1).
2
3use std::path::{Path, PathBuf};
4
5use thiserror::Error;
6
7use crate::config::FleetEntry;
8
9#[derive(Debug, Error)]
10pub enum EnrollError {
11    #[error("target path does not exist or is not a directory: {0}")]
12    BadTarget(String),
13    #[error("template not found: {template} (looked in {dir})")]
14    MissingTemplate { template: String, dir: String },
15    #[error("io error at {path}: {source}")]
16    Io {
17        path: String,
18        #[source]
19        source: std::io::Error,
20    },
21}
22
23fn walk(root: &Path) -> Result<Vec<PathBuf>, EnrollError> {
24    let mut out = Vec::new();
25    visit(root, &mut out)?;
26    out.sort();
27    Ok(out)
28}
29
30fn visit(dir: &Path, out: &mut Vec<PathBuf>) -> Result<(), EnrollError> {
31    let entries = std::fs::read_dir(dir).map_err(|e| EnrollError::Io {
32        path: dir.display().to_string(),
33        source: e,
34    })?;
35    for entry in entries {
36        let entry = entry.map_err(|e| EnrollError::Io {
37            path: dir.display().to_string(),
38            source: e,
39        })?;
40        let p = entry.path();
41        let ft = entry.file_type().map_err(|e| EnrollError::Io {
42            path: p.display().to_string(),
43            source: e,
44        })?;
45        if ft.is_dir() {
46            visit(&p, out)?;
47        } else {
48            out.push(p);
49        }
50    }
51    Ok(())
52}
53
54/// Render and write the kit. Returns a list of relative paths written.
55pub fn enroll(entry: &FleetEntry, templates_root: &Path) -> Result<Vec<String>, EnrollError> {
56    let target = Path::new(&entry.path);
57    let meta = std::fs::metadata(target).ok();
58    let is_dir = meta.as_ref().map(|m| m.is_dir()).unwrap_or(false);
59    if !is_dir {
60        return Err(EnrollError::BadTarget(entry.path.clone()));
61    }
62    let tpl_dir = templates_root.join(&entry.template);
63    if !tpl_dir.exists() {
64        return Err(EnrollError::MissingTemplate {
65            template: entry.template.clone(),
66            dir: tpl_dir.display().to_string(),
67        });
68    }
69
70    let vars = [("name", entry.name.as_str()), ("repo", entry.repo.as_str())];
71    let mut written = Vec::new();
72    for src in walk(&tpl_dir)? {
73        let rel = src.strip_prefix(&tpl_dir).expect("walk under tpl_dir");
74        let dest = target.join(rel);
75        let raw = std::fs::read_to_string(&src).map_err(|e| EnrollError::Io {
76            path: src.display().to_string(),
77            source: e,
78        })?;
79        let mut rendered = raw;
80        for (k, v) in &vars {
81            let key = format!("{{{{{}}}}}", k);
82            rendered = rendered.replace(&key, v);
83        }
84        if let Some(parent) = dest.parent() {
85            std::fs::create_dir_all(parent).map_err(|e| EnrollError::Io {
86                path: parent.display().to_string(),
87                source: e,
88            })?;
89        }
90        std::fs::write(&dest, &rendered).map_err(|e| EnrollError::Io {
91            path: dest.display().to_string(),
92            source: e,
93        })?;
94        written.push(rel.to_string_lossy().replace('\\', "/"));
95    }
96
97    // Bootstrap release-please manifest from target's current package.json version.
98    let pkg_path = target.join("package.json");
99    if pkg_path.exists() {
100        if let Ok(text) = std::fs::read_to_string(&pkg_path) {
101            if let Ok(pkg) = serde_json::from_str::<serde_json::Value>(&text) {
102                let version = pkg
103                    .get("version")
104                    .and_then(|v| v.as_str())
105                    .unwrap_or("0.0.0")
106                    .to_string();
107                let manifest_path = target.join(".release-please-manifest.json");
108                let manifest = serde_json::json!({ ".": version });
109                let body = serde_json::to_string_pretty(&manifest).unwrap_or_else(|_| "{}".into());
110                let _ = std::fs::write(&manifest_path, format!("{}\n", body));
111                written.push(".release-please-manifest.json".to_string());
112            }
113        }
114    }
115
116    Ok(written)
117}