opencode_ralph_loop_cli/manifest/
mod.rs1use std::path::Path;
2
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5
6use crate::error::CliError;
7
8pub const MANIFEST_FILENAME: &str = ".manifest.json";
9pub const SCHEMA_VERSION: u32 = 1;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Manifest {
13 pub schema_version: u32,
14 pub cli_version: String,
15 pub plugin_version: String,
16 pub template_version: String,
17 pub generated_at: String,
18 pub generator: String,
19 pub files: Vec<ManifestFile>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ManifestFile {
24 pub path: String,
25 pub sha256: String,
26 pub size_bytes: u64,
27 pub canonical: bool,
28 pub action: String,
29}
30
31impl Manifest {
32 pub fn new(plugin_version: impl Into<String>) -> Self {
33 Self {
34 schema_version: SCHEMA_VERSION,
35 cli_version: env!("CARGO_PKG_VERSION").to_string(),
36 plugin_version: plugin_version.into(),
37 template_version: crate::templates::TEMPLATE_VERSION.to_string(),
38 generated_at: Utc::now().to_rfc3339(),
39 generator: "opencode-ralph-loop-cli".to_string(),
40 files: Vec::new(),
41 }
42 }
43
44 pub fn add_file(&mut self, file: ManifestFile) {
45 self.files.push(file);
46 self.files.sort_by(|a, b| a.path.cmp(&b.path));
47 }
48
49 pub fn find_file(&self, path: &str) -> Option<&ManifestFile> {
50 self.files.iter().find(|f| f.path == path)
51 }
52}
53
54impl ManifestFile {
55 pub fn new(
56 path: impl Into<String>,
57 sha256: impl Into<String>,
58 size_bytes: u64,
59 canonical: bool,
60 action: impl Into<String>,
61 ) -> Self {
62 Self {
63 path: path.into(),
64 sha256: sha256.into(),
65 size_bytes,
66 canonical,
67 action: action.into(),
68 }
69 }
70}
71
72pub fn load(opencode_dir: &Path) -> Result<Manifest, CliError> {
74 let manifest_path = opencode_dir.join(MANIFEST_FILENAME);
75
76 if !manifest_path.exists() {
77 return Err(CliError::ManifestMissing);
78 }
79
80 let content = std::fs::read_to_string(&manifest_path)
81 .map_err(|e| CliError::io(manifest_path.to_string_lossy().into_owned(), e))?;
82
83 serde_json::from_str(&content)
84 .map_err(|e| CliError::ConfigParse(format!("invalid manifest: {e}")))
85}
86
87pub fn save(opencode_dir: &Path, manifest: &Manifest) -> Result<(), CliError> {
89 let manifest_path = opencode_dir.join(MANIFEST_FILENAME);
90
91 let mut content = serde_json::to_string_pretty(manifest)
92 .map_err(|e| CliError::Generic(format!("failed to serialize manifest: {e}")))?;
93 content.push('\n');
94
95 crate::fs_atomic::write_atomic(&manifest_path, content.as_bytes())
96}
97
98pub fn remove(opencode_dir: &Path) -> Result<(), CliError> {
100 let manifest_path = opencode_dir.join(MANIFEST_FILENAME);
101 if manifest_path.exists() {
102 std::fs::remove_file(&manifest_path)
103 .map_err(|e| CliError::io(manifest_path.to_string_lossy().into_owned(), e))?;
104 }
105 Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use tempfile::TempDir;
112
113 #[test]
114 fn saves_and_loads_manifest() {
115 let dir = TempDir::new().unwrap();
116 let opencode_dir = dir.path().join(".opencode");
117 std::fs::create_dir_all(&opencode_dir).unwrap();
118
119 let mut manifest = Manifest::new("1.4.7");
120 manifest.add_file(ManifestFile::new(
121 "plugins/ralph.ts",
122 "abc123",
123 1234,
124 true,
125 "created",
126 ));
127
128 save(&opencode_dir, &manifest).unwrap();
129
130 let loaded = load(&opencode_dir).unwrap();
131 assert_eq!(loaded.plugin_version, "1.4.7");
132 assert_eq!(loaded.files.len(), 1);
133 assert_eq!(loaded.files[0].path, "plugins/ralph.ts");
134 }
135
136 #[test]
137 fn load_returns_missing_when_absent() {
138 let dir = TempDir::new().unwrap();
139 let resultado = load(dir.path());
140 assert!(matches!(resultado, Err(CliError::ManifestMissing)));
141 }
142
143 #[test]
144 fn files_sorted_alphabetically() {
145 let mut manifest = Manifest::new("1.4.7");
146 manifest.add_file(ManifestFile::new("z.ts", "h1", 1, true, "created"));
147 manifest.add_file(ManifestFile::new("a.md", "h2", 1, true, "created"));
148 assert_eq!(manifest.files[0].path, "a.md");
149 assert_eq!(manifest.files[1].path, "z.ts");
150 }
151}