use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
pub fn write_atomic(path: &Path, content: &[u8]) -> Result<()> {
let parent = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| Path::new(".").to_path_buf());
std::fs::create_dir_all(&parent)
.with_context(|| format!("criando diretório {}", parent.display()))?;
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "artifact".to_string());
let tmp = parent.join(format!(".{file_name}.sdd-tmp"));
std::fs::write(&tmp, content)
.with_context(|| format!("escrevendo arquivo temporário {}", tmp.display()))?;
std::fs::rename(&tmp, path)
.with_context(|| format!("renomeando {} -> {}", tmp.display(), path.display()))?;
Ok(())
}
pub fn save_stage_state(
exe: &Path,
root: &Path,
name: &str,
stage: &str,
file: &Path,
state: &str,
) -> Result<()> {
let output = build_save_stage_state_command(exe, root, name, stage, file, state)
.output()
.with_context(|| format!("invocando artifact save para {stage}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("artifact save falhou ({stage}): {}", stderr.trim());
}
Ok(())
}
fn build_save_stage_state_command(
exe: &Path,
root: &Path,
name: &str,
stage: &str,
file: &Path,
state: &str,
) -> Command {
let mut cmd = Command::new(exe);
cmd.arg("artifact")
.arg("--root")
.arg(root)
.arg("save")
.arg(name)
.arg(stage)
.arg("--file")
.arg(file)
.arg("--state")
.arg(state)
.arg("--force");
cmd
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn writes_content() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("a.md");
write_atomic(&target, b"hello").unwrap();
assert_eq!(fs::read_to_string(&target).unwrap(), "hello");
}
#[test]
fn overwrites_existing() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("a.md");
write_atomic(&target, b"v1").unwrap();
write_atomic(&target, b"v2").unwrap();
assert_eq!(fs::read_to_string(&target).unwrap(), "v2");
}
#[test]
fn stale_tmp_does_not_corrupt_target() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("a.md");
write_atomic(&target, b"good").unwrap();
let tmp = dir.path().join(".a.md.sdd-tmp");
fs::write(&tmp, b"garbage-interrupted").unwrap();
write_atomic(&target, b"good2").unwrap();
assert_eq!(fs::read_to_string(&target).unwrap(), "good2");
assert!(!tmp.exists(), "tmp deve ter sido consumido pelo rename");
}
#[test]
fn creates_missing_parent() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("nested/deep/a.md");
write_atomic(&target, b"x").unwrap();
assert!(target.exists());
}
#[test]
fn save_stage_state_places_root_before_save_subcommand() {
let cmd = build_save_stage_state_command(
Path::new("/usr/bin/sdd"),
Path::new("/tmp/project"),
"Minha Feature",
"idea",
Path::new("/tmp/project/docs/minha-feature/01-idea.md"),
"approved",
);
let args: Vec<String> = cmd
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
assert_eq!(args[0], "artifact");
assert_eq!(args[1], "--root");
assert_eq!(args[2], "/tmp/project");
assert_eq!(args[3], "save");
assert_eq!(args[4], "Minha Feature");
assert_eq!(args[5], "idea");
assert!(args.contains(&"--force".to_string()));
}
}