use std::fs;
use std::io;
use std::path::{Path, PathBuf};
pub const COMMANDS: &[(&str, &str)] = &[
(
"install-statusline.md",
include_str!("../.claude/commands/install-statusline.md"),
),
(
"update-statusline.md",
include_str!("../.claude/commands/update-statusline.md"),
),
(
"customize-statusline.md",
include_str!("../.claude/commands/customize-statusline.md"),
),
(
"uninstall-statusline.md",
include_str!("../.claude/commands/uninstall-statusline.md"),
),
];
#[derive(Debug, PartialEq)]
pub enum InstallOutcome {
Installed,
UpToDate,
Updated {
backup: PathBuf,
},
}
pub fn install_commands(dir: &Path) -> io::Result<Vec<(&'static str, InstallOutcome)>> {
fs::create_dir_all(dir)?;
COMMANDS
.iter()
.map(|&(name, content)| {
let path = dir.join(name);
let outcome = match fs::read_to_string(&path) {
Ok(existing) if existing.replace("\r\n", "\n") == content => {
InstallOutcome::UpToDate
}
Ok(_) => {
let backup = next_backup_path(dir, name);
fs::copy(&path, &backup)?;
fs::write(&path, content)?;
InstallOutcome::Updated { backup }
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::write(&path, content)?;
InstallOutcome::Installed
}
Err(err) => return Err(err),
};
Ok((name, outcome))
})
.collect()
}
pub(crate) fn next_backup_path(dir: &Path, name: &str) -> PathBuf {
let first = dir.join(format!("{}.bak", name));
if !first.exists() {
return first;
}
(1..)
.map(|n| dir.join(format!("{}.bak.{}", name, n)))
.find(|path| !path.exists())
.expect("unbounded range always yields a free name")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_install_commands_fresh_then_up_to_date() {
let dir = tempfile::tempdir().unwrap();
let first = install_commands(dir.path()).unwrap();
assert_eq!(first.len(), COMMANDS.len());
assert!(
first
.iter()
.all(|(_, outcome)| *outcome == InstallOutcome::Installed)
);
let second = install_commands(dir.path()).unwrap();
assert!(
second
.iter()
.all(|(_, outcome)| *outcome == InstallOutcome::UpToDate)
);
}
#[test]
fn test_install_commands_backs_up_modified_file() {
let dir = tempfile::tempdir().unwrap();
let (name, content) = COMMANDS[0];
fs::write(dir.path().join(name), "custom edits").unwrap();
let results = install_commands(dir.path()).unwrap();
let backup = dir.path().join(format!("{}.bak", name));
assert_eq!(
results[0],
(
name,
InstallOutcome::Updated {
backup: backup.clone()
}
)
);
assert_eq!(fs::read_to_string(backup).unwrap(), "custom edits");
assert_eq!(fs::read_to_string(dir.path().join(name)).unwrap(), content);
}
#[test]
fn test_install_commands_keeps_earlier_backup() {
let dir = tempfile::tempdir().unwrap();
let (name, _) = COMMANDS[0];
fs::write(dir.path().join(name), "user edits").unwrap();
install_commands(dir.path()).unwrap();
fs::write(dir.path().join(name), "older stock").unwrap();
let results = install_commands(dir.path()).unwrap();
let second_backup = dir.path().join(format!("{}.bak.1", name));
assert_eq!(
results[0].1,
InstallOutcome::Updated {
backup: second_backup.clone()
}
);
assert_eq!(
fs::read_to_string(dir.path().join(format!("{}.bak", name))).unwrap(),
"user edits"
);
assert_eq!(fs::read_to_string(second_backup).unwrap(), "older stock");
}
#[test]
fn test_install_commands_treats_crlf_copy_as_up_to_date() {
let dir = tempfile::tempdir().unwrap();
let (name, content) = COMMANDS[0];
fs::write(dir.path().join(name), content.replace('\n', "\r\n")).unwrap();
let results = install_commands(dir.path()).unwrap();
assert_eq!(results[0].1, InstallOutcome::UpToDate);
}
}