Skip to main content

claude_code_status_line/
commands.rs

1//! Slash commands embedded in the binary and installed with `--install-commands`.
2//!
3//! The binary is distributed through npm and crates.io, so shipping the commands
4//! inside it keeps them in lockstep with the installed version.
5
6use std::fs;
7use std::io;
8use std::path::{Path, PathBuf};
9
10pub const COMMANDS: &[(&str, &str)] = &[
11    (
12        "install-statusline.md",
13        include_str!("../.claude/commands/install-statusline.md"),
14    ),
15    (
16        "update-statusline.md",
17        include_str!("../.claude/commands/update-statusline.md"),
18    ),
19    (
20        "customize-statusline.md",
21        include_str!("../.claude/commands/customize-statusline.md"),
22    ),
23    (
24        "uninstall-statusline.md",
25        include_str!("../.claude/commands/uninstall-statusline.md"),
26    ),
27];
28
29#[derive(Debug, PartialEq)]
30pub enum InstallOutcome {
31    Installed,
32    UpToDate,
33    /// Existing file differed; the previous content was saved to `backup`.
34    Updated {
35        backup: PathBuf,
36    },
37}
38
39/// Write every embedded command into `dir`, backing up files whose content differs.
40pub fn install_commands(dir: &Path) -> io::Result<Vec<(&'static str, InstallOutcome)>> {
41    fs::create_dir_all(dir)?;
42
43    COMMANDS
44        .iter()
45        .map(|&(name, content)| {
46            let path = dir.join(name);
47            let outcome = match fs::read_to_string(&path) {
48                Ok(existing) if existing.replace("\r\n", "\n") == content => {
49                    InstallOutcome::UpToDate
50                }
51                Ok(_) => {
52                    let backup = next_backup_path(dir, name);
53                    fs::copy(&path, &backup)?;
54                    fs::write(&path, content)?;
55                    InstallOutcome::Updated { backup }
56                }
57                Err(err) if err.kind() == io::ErrorKind::NotFound => {
58                    fs::write(&path, content)?;
59                    InstallOutcome::Installed
60                }
61                Err(err) => return Err(err),
62            };
63            Ok((name, outcome))
64        })
65        .collect()
66}
67
68/// First free backup name (`<name>.bak`, `<name>.bak.1`, ...) so an earlier
69/// backup holding user edits is never overwritten.
70pub(crate) fn next_backup_path(dir: &Path, name: &str) -> PathBuf {
71    let first = dir.join(format!("{}.bak", name));
72    if !first.exists() {
73        return first;
74    }
75    (1..)
76        .map(|n| dir.join(format!("{}.bak.{}", name, n)))
77        .find(|path| !path.exists())
78        .expect("unbounded range always yields a free name")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_install_commands_fresh_then_up_to_date() {
87        let dir = tempfile::tempdir().unwrap();
88
89        let first = install_commands(dir.path()).unwrap();
90        assert_eq!(first.len(), COMMANDS.len());
91        assert!(
92            first
93                .iter()
94                .all(|(_, outcome)| *outcome == InstallOutcome::Installed)
95        );
96
97        let second = install_commands(dir.path()).unwrap();
98        assert!(
99            second
100                .iter()
101                .all(|(_, outcome)| *outcome == InstallOutcome::UpToDate)
102        );
103    }
104
105    #[test]
106    fn test_install_commands_backs_up_modified_file() {
107        let dir = tempfile::tempdir().unwrap();
108        let (name, content) = COMMANDS[0];
109        fs::write(dir.path().join(name), "custom edits").unwrap();
110
111        let results = install_commands(dir.path()).unwrap();
112        let backup = dir.path().join(format!("{}.bak", name));
113
114        assert_eq!(
115            results[0],
116            (
117                name,
118                InstallOutcome::Updated {
119                    backup: backup.clone()
120                }
121            )
122        );
123        assert_eq!(fs::read_to_string(backup).unwrap(), "custom edits");
124        assert_eq!(fs::read_to_string(dir.path().join(name)).unwrap(), content);
125    }
126
127    #[test]
128    fn test_install_commands_keeps_earlier_backup() {
129        let dir = tempfile::tempdir().unwrap();
130        let (name, _) = COMMANDS[0];
131        fs::write(dir.path().join(name), "user edits").unwrap();
132        install_commands(dir.path()).unwrap();
133
134        fs::write(dir.path().join(name), "older stock").unwrap();
135        let results = install_commands(dir.path()).unwrap();
136
137        let second_backup = dir.path().join(format!("{}.bak.1", name));
138        assert_eq!(
139            results[0].1,
140            InstallOutcome::Updated {
141                backup: second_backup.clone()
142            }
143        );
144        assert_eq!(
145            fs::read_to_string(dir.path().join(format!("{}.bak", name))).unwrap(),
146            "user edits"
147        );
148        assert_eq!(fs::read_to_string(second_backup).unwrap(), "older stock");
149    }
150
151    #[test]
152    fn test_install_commands_treats_crlf_copy_as_up_to_date() {
153        let dir = tempfile::tempdir().unwrap();
154        let (name, content) = COMMANDS[0];
155        fs::write(dir.path().join(name), content.replace('\n', "\r\n")).unwrap();
156
157        let results = install_commands(dir.path()).unwrap();
158        assert_eq!(results[0].1, InstallOutcome::UpToDate);
159    }
160}