1use 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
54pub 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 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}