1use std::{fs, path::Path};
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6const LOCK_FILE_NAME: &str = "oxide.lock";
7
8#[derive(Debug, Default, Serialize, Deserialize)]
9pub struct LockFile {
10 pub addons: Vec<LockEntry>,
11}
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct LockEntry {
15 pub id: String,
16 pub version: String,
17 pub variant: String,
18 pub commands_executed: Vec<String>,
19}
20
21impl LockFile {
22 pub fn load(project_root: &Path) -> Result<Self> {
23 let path = project_root.join(LOCK_FILE_NAME);
24 if !path.exists() {
25 return Ok(Self::default());
26 }
27 let contents = fs::read_to_string(&path)?;
28 let lock: Self = serde_json::from_str(&contents)?;
29 Ok(lock)
30 }
31
32 pub fn save(&self, project_root: &Path) -> Result<()> {
33 let path = project_root.join(LOCK_FILE_NAME);
34 let contents = serde_json::to_string_pretty(self)?;
35 fs::write(path, contents)?;
36 Ok(())
37 }
38
39 pub fn is_command_executed(&self, addon_id: &str, command: &str) -> bool {
40 self
41 .addons
42 .iter()
43 .find(|e| e.id == addon_id)
44 .map(|e| e.commands_executed.iter().any(|c| c == command))
45 .unwrap_or(false)
46 }
47
48 pub fn mark_command_executed(&mut self, addon_id: &str, command: &str) {
49 if let Some(entry) = self.addons.iter_mut().find(|e| e.id == addon_id)
50 && !entry.commands_executed.iter().any(|c| c == command)
51 {
52 entry.commands_executed.push(command.to_string());
53 }
54 }
55
56 pub fn upsert_entry(&mut self, entry: LockEntry) {
57 if let Some(existing) = self.addons.iter_mut().find(|e| e.id == entry.id) {
58 *existing = entry;
59 } else {
60 self.addons.push(entry);
61 }
62 }
63}