use std::fs;
use std::io;
use std::path::Path;
use super::Skill;
pub(crate) const SECTION_START: &str = "<!-- ARISTO-SKILLS START v1 -->";
pub(crate) const SECTION_END: &str = "<!-- ARISTO-SKILLS END -->";
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InstallOutcome {
Created,
Updated,
Unchanged,
}
#[aristo::intent(
"A second invocation with identical content leaves the target \
byte-identical and returns `Unchanged`. Created (file did not \
exist) and Updated (content differed) are distinct outcomes; \
idempotence is the Unchanged case specifically.",
verify = "test",
id = "file_copy_install_idempotent"
)]
pub(crate) fn file_copy_install(target: &Path, skill: &Skill) -> io::Result<InstallOutcome> {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let resolved = skill.resolved_content();
if target.exists() {
let existing = fs::read_to_string(target)?;
if existing == resolved {
return Ok(InstallOutcome::Unchanged);
}
fs::write(target, &resolved)?;
return Ok(InstallOutcome::Updated);
}
fs::write(target, &resolved)?;
Ok(InstallOutcome::Created)
}
#[aristo::intent(
"Removes only the file we wrote — no sibling deletion, no \
parent-dir cleanup. Absence of the target is not an error; \
uninstall-of-already-uninstalled is the idempotent case.",
verify = "test",
id = "file_copy_uninstall_idempotent"
)]
pub(crate) fn file_copy_uninstall(target: &Path) -> io::Result<bool> {
if !target.exists() {
return Ok(false);
}
fs::remove_file(target)?;
Ok(true)
}
#[aristo::intent(
"Content outside the marker boundaries is preserved byte-for-byte \
across install and update. Users who hand-edit AGENTS.md alongside \
the auto-generated block don't lose their work to a normalization \
or reformat pass.",
verify = "test",
id = "agents_md_install_preserves_outside_markers"
)]
pub(crate) fn agents_md_install(target: &Path, skills: &[&Skill]) -> io::Result<InstallOutcome> {
let new_block = render_agents_md_block(skills);
if !target.exists() {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(target, &new_block)?;
return Ok(InstallOutcome::Created);
}
let existing = fs::read_to_string(target)?;
let updated = match find_block(&existing) {
Some((start, end)) => {
let mut buf = String::with_capacity(existing.len() + new_block.len());
buf.push_str(&existing[..start]);
buf.push_str(new_block.trim_end());
buf.push_str(&existing[end..]);
buf
}
None => {
let mut buf = existing.clone();
if !buf.ends_with('\n') {
buf.push('\n');
}
buf.push('\n');
buf.push_str(&new_block);
buf
}
};
if updated == existing {
return Ok(InstallOutcome::Unchanged);
}
fs::write(target, updated)?;
Ok(InstallOutcome::Updated)
}
#[aristo::intent(
"Only the marker-delimited block is stripped; surrounding content \
is preserved byte-for-byte. Absent file or absent block is not an \
error — idempotent.",
verify = "test",
id = "agents_md_uninstall_preserves_outside_markers"
)]
pub(crate) fn agents_md_uninstall(target: &Path) -> io::Result<bool> {
if !target.exists() {
return Ok(false);
}
let existing = fs::read_to_string(target)?;
let Some((start, end)) = find_block(&existing) else {
return Ok(false);
};
let mut trim_end = end;
if existing[trim_end..].starts_with('\n') {
trim_end += 1;
}
let mut trim_start = start;
if trim_start > 0 && existing[..trim_start].ends_with('\n') {
trim_start -= 1;
}
let mut buf = String::with_capacity(existing.len());
buf.push_str(&existing[..trim_start]);
buf.push_str(&existing[trim_end..]);
fs::write(target, buf)?;
Ok(true)
}
fn render_agents_md_block(skills: &[&Skill]) -> String {
let mut buf = String::new();
buf.push_str(SECTION_START);
buf.push('\n');
for s in skills {
buf.push_str("\n## ");
buf.push_str(s.name);
buf.push_str("\n\n");
buf.push_str(s.resolved_content().trim());
buf.push('\n');
}
buf.push('\n');
buf.push_str(SECTION_END);
buf.push('\n');
buf
}
fn find_block(content: &str) -> Option<(usize, usize)> {
let start = content.find(SECTION_START)?;
let end_marker_start = content[start..].find(SECTION_END)? + start;
let end = end_marker_start + SECTION_END.len();
Some((start, end))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills;
use tempfile::TempDir;
fn skill() -> &'static Skill {
skills::bundled()
.iter()
.find(|s| s.name == "aristo-authoring")
.expect("authoring skill must be bundled")
}
#[test]
fn installed_skill_has_real_sdk_version_not_placeholder() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("SKILL.md");
file_copy_install(&target, skill()).unwrap();
let on_disk = fs::read_to_string(&target).unwrap();
assert!(
!on_disk.contains("{{SDK_VERSION}}"),
"placeholder leaked to installed file"
);
let expected = format!("sdk_version: {}", env!("CARGO_PKG_VERSION"));
assert!(
on_disk.contains(&expected),
"installed frontmatter missing `{expected}`"
);
}
#[test]
fn file_copy_creates_then_unchanged_then_updated() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("a/b/c/SKILL.md");
let r1 = file_copy_install(&target, skill()).unwrap();
assert_eq!(r1, InstallOutcome::Created);
assert!(target.is_file());
let r2 = file_copy_install(&target, skill()).unwrap();
assert_eq!(r2, InstallOutcome::Unchanged);
fs::write(&target, "tampered").unwrap();
let r3 = file_copy_install(&target, skill()).unwrap();
assert_eq!(r3, InstallOutcome::Updated);
assert_eq!(
fs::read_to_string(&target).unwrap(),
skill().resolved_content()
);
}
#[test]
fn file_copy_uninstall_idempotent() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("SKILL.md");
assert!(!file_copy_uninstall(&target).unwrap());
file_copy_install(&target, skill()).unwrap();
assert!(file_copy_uninstall(&target).unwrap());
assert!(!target.exists());
assert!(!file_copy_uninstall(&target).unwrap());
}
#[test]
fn agents_md_creates_when_file_absent() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
let r = agents_md_install(&target, &[skill()]).unwrap();
assert_eq!(r, InstallOutcome::Created);
let content = fs::read_to_string(&target).unwrap();
assert!(content.contains(SECTION_START));
assert!(content.contains(SECTION_END));
assert!(content.contains("## aristo-authoring"));
}
#[test]
fn agents_md_appends_block_to_existing_file_preserving_user_content() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
let user_text = "# My agent rules\n\nUse 4-space indent.\n";
fs::write(&target, user_text).unwrap();
agents_md_install(&target, &[skill()]).unwrap();
let content = fs::read_to_string(&target).unwrap();
assert!(
content.starts_with(user_text),
"user content must be preserved at the start"
);
assert!(content.contains(SECTION_START));
}
#[test]
fn agents_md_replaces_only_marker_block_on_update() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
let user_before = "# Before\n\n";
let stale_block =
format!("{SECTION_START}\n## aristo-authoring\n\nold content\n\n{SECTION_END}\n");
let user_after = "\n# After\n\nMore user notes.\n";
fs::write(&target, format!("{user_before}{stale_block}{user_after}")).unwrap();
let r = agents_md_install(&target, &[skill()]).unwrap();
assert_eq!(r, InstallOutcome::Updated);
let content = fs::read_to_string(&target).unwrap();
assert!(content.contains("# Before"));
assert!(content.contains("# After"));
assert!(content.contains("More user notes."));
assert!(
!content.contains("old content"),
"stale content must be replaced"
);
assert!(content.contains(SECTION_START));
}
#[test]
fn agents_md_install_idempotent() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
agents_md_install(&target, &[skill()]).unwrap();
let r = agents_md_install(&target, &[skill()]).unwrap();
assert_eq!(r, InstallOutcome::Unchanged);
}
#[test]
fn agents_md_uninstall_strips_block_preserves_surrounding() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
let user_before = "# My rules\n\nUse 4-space indent.\n";
fs::write(&target, user_before).unwrap();
agents_md_install(&target, &[skill()]).unwrap();
let removed = agents_md_uninstall(&target).unwrap();
assert!(removed);
let content = fs::read_to_string(&target).unwrap();
assert!(!content.contains(SECTION_START));
assert!(!content.contains(SECTION_END));
assert!(content.contains("# My rules"));
assert!(content.contains("Use 4-space indent."));
}
#[test]
fn agents_md_uninstall_idempotent_when_file_absent_or_block_absent() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("AGENTS.md");
assert!(!agents_md_uninstall(&target).unwrap());
fs::write(&target, "just user content\n").unwrap();
assert!(!agents_md_uninstall(&target).unwrap());
assert_eq!(fs::read_to_string(&target).unwrap(), "just user content\n");
}
}