use std::collections::BTreeMap;
use std::path::Path;
use crate::error::SkillfileError;
use crate::models::{Entry, LockEntry};
pub const LOCK_NAME: &str = "Skillfile.lock";
#[must_use]
pub fn lock_key(entry: &Entry) -> String {
format!(
"{}/{}/{}",
entry.source_type(),
entry.entity_type,
entry.name
)
}
pub fn read_lock(repo_root: &Path) -> Result<BTreeMap<String, LockEntry>, SkillfileError> {
let lock_path = repo_root.join(LOCK_NAME);
if !lock_path.exists() {
return Ok(BTreeMap::new());
}
let text = std::fs::read_to_string(&lock_path)?;
let data: BTreeMap<String, LockEntry> = serde_json::from_str(&text)
.map_err(|e| SkillfileError::Manifest(format!("invalid lock file: {e}")))?;
Ok(data)
}
pub fn write_lock(
repo_root: &Path,
locked: &BTreeMap<String, LockEntry>,
) -> Result<(), SkillfileError> {
let lock_path = repo_root.join(LOCK_NAME);
let json = serde_json::to_string_pretty(locked)
.map_err(|e| SkillfileError::Manifest(format!("failed to serialize lock: {e}")))?;
std::fs::write(&lock_path, format!("{json}\n"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_github_entry(name: &str) -> Entry {
use crate::models::{EntityType, SourceFields};
Entry {
entity_type: EntityType::Agent,
name: name.into(),
source: SourceFields::Github {
owner_repo: "owner/repo".into(),
path_in_repo: "agent.md".into(),
ref_: "main".into(),
},
}
}
#[test]
fn lock_key_format() {
let e = make_github_entry("my-agent");
assert_eq!(lock_key(&e), "github/agent/my-agent");
}
#[test]
fn write_lock_valid_json() {
let dir = tempfile::tempdir().unwrap();
let mut locked = BTreeMap::new();
locked.insert(
"github/agent/test".to_string(),
LockEntry {
sha: "abc123".into(),
raw_url: "https://example.com/file.md".into(),
},
);
write_lock(dir.path(), &locked).unwrap();
let content = std::fs::read_to_string(dir.path().join(LOCK_NAME)).unwrap();
let data: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(data["github/agent/test"]["sha"], "abc123");
assert_eq!(
data["github/agent/test"]["raw_url"],
"https://example.com/file.md"
);
}
#[test]
fn read_lock_missing_file() {
let dir = tempfile::tempdir().unwrap();
let result = read_lock(dir.path()).unwrap();
assert!(result.is_empty());
}
#[test]
fn roundtrip() {
let dir = tempfile::tempdir().unwrap();
let mut locked = BTreeMap::new();
locked.insert(
"github/agent/foo".to_string(),
LockEntry {
sha: "deadbeef".into(),
raw_url: "https://example.com/foo.md".into(),
},
);
locked.insert(
"github/skill/bar".to_string(),
LockEntry {
sha: "cafebabe".into(),
raw_url: "https://example.com/bar.md".into(),
},
);
write_lock(dir.path(), &locked).unwrap();
let result = read_lock(dir.path()).unwrap();
assert_eq!(result, locked);
}
#[test]
fn write_lock_sorted_keys() {
let dir = tempfile::tempdir().unwrap();
let mut locked = BTreeMap::new();
locked.insert(
"github/skill/zebra".to_string(),
LockEntry {
sha: "aaa".into(),
raw_url: "https://example.com/z.md".into(),
},
);
locked.insert(
"github/agent/alpha".to_string(),
LockEntry {
sha: "bbb".into(),
raw_url: "https://example.com/a.md".into(),
},
);
write_lock(dir.path(), &locked).unwrap();
let content = std::fs::read_to_string(dir.path().join(LOCK_NAME)).unwrap();
let alpha_pos = content.find("alpha").unwrap();
let zebra_pos = content.find("zebra").unwrap();
assert!(alpha_pos < zebra_pos);
}
}