use std::path::Path;
use anyhow::{Context, bail};
use super::Step;
pub(crate) struct Seeded {
pub(crate) dest: &'static str,
pub(crate) contents: &'static str,
pub(crate) executable: bool,
}
macro_rules! seeded {
($dest:literal, $asset:literal, $exec:literal) => {
Seeded {
dest: $dest,
contents: include_str!(concat!("../../../../assets/architect/", $asset)),
executable: $exec,
}
};
}
pub(crate) const FILES: &[Seeded] = &[
seeded!("CLAUDE.md", "templates/CLAUDE.md", false),
seeded!(".gitignore", "templates/gitignore", false),
seeded!("records/state.md", "templates/records/state.md", false),
seeded!("records/actions.md", "templates/records/actions.md", false),
seeded!("scripts/fleet-poll.py", "scripts/fleet-poll.py", true),
seeded!(
"scripts/fleet-classify.py",
"scripts/fleet-classify.py",
false
),
seeded!("scripts/input-state.py", "scripts/input-state.py", true),
seeded!(
"scripts/quiet-sessions.py",
"scripts/quiet-sessions.py",
false
),
seeded!("scripts/self-ctx.py", "scripts/self-ctx.py", false),
seeded!(
"scripts/start-fleet-poll.sh",
"scripts/start-fleet-poll.sh",
true
),
seeded!(
".claude/skills/tm-fleet-check/SKILL.md",
"skills/tm-fleet-check.md",
false
),
seeded!(
".claude/skills/tm-context-refresh/SKILL.md",
"skills/tm-context-refresh.md",
false
),
];
pub(crate) const DIRS: &[&str] = &["records/projects"];
pub(crate) fn deploy(dir: &Path) -> anyhow::Result<Vec<Step>> {
let mut steps = Vec::with_capacity(FILES.len() + DIRS.len());
for file in FILES {
steps.push(deploy_file(dir, file)?);
}
for rel in DIRS {
let path = dir.join(rel);
steps.push(if path.is_dir() {
Step::Unchanged(format!("{rel}/"))
} else {
std::fs::create_dir_all(&path)
.with_context(|| format!("cannot create {}", path.display()))?;
Step::Changed(format!("created {rel}/"))
});
}
Ok(steps)
}
fn deploy_file(dir: &Path, file: &Seeded) -> anyhow::Result<Step> {
let path = dir.join(file.dest);
match std::fs::read(&path) {
Ok(bytes) if bytes == file.contents.as_bytes() => Ok(Step::Unchanged(file.dest.to_owned())),
Ok(_) => Ok(Step::Skipped(format!(
"{}: differs from the shipped copy (edited), so it is left as is",
file.dest
))),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
trusty_common::atomic_file::write_atomic(&path, file.contents.as_bytes())
.with_context(|| format!("cannot write {}", path.display()))?;
if file.executable {
make_executable(&path)?;
}
Ok(Step::Changed(format!("wrote {}", file.dest)))
}
Err(err) if err.kind() == std::io::ErrorKind::IsADirectory => {
bail!(
"{} is a directory, not the file tm fleet init writes",
path.display()
)
}
Err(err) => Err(err).with_context(|| format!("cannot read {}", path.display())),
}
}
#[cfg(unix)]
fn make_executable(path: &Path) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
.with_context(|| format!("cannot make {} executable", path.display()))
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) -> anyhow::Result<()> {
Ok(())
}