1use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11
12const EMBEDDED_SKILL_MD: &[u8] = include_bytes!("../skills/agent-exec/SKILL.md");
14const EMBEDDED_CLI_CONTRACT_MD: &[u8] =
15 include_bytes!("../skills/agent-exec/references/cli-contract.md");
16const EMBEDDED_COMPLETION_EVENTS_MD: &[u8] =
17 include_bytes!("../skills/agent-exec/references/completion-events.md");
18const EMBEDDED_OPENCLAW_MD: &[u8] = include_bytes!("../skills/agent-exec/references/openclaw.md");
19
20pub struct EmbeddedFile {
22 pub relative_path: &'static str,
23 pub content: &'static [u8],
24}
25
26pub static EMBEDDED_AGENT_EXEC_FILES: &[EmbeddedFile] = &[
28 EmbeddedFile {
29 relative_path: "SKILL.md",
30 content: EMBEDDED_SKILL_MD,
31 },
32 EmbeddedFile {
33 relative_path: "references/cli-contract.md",
34 content: EMBEDDED_CLI_CONTRACT_MD,
35 },
36 EmbeddedFile {
37 relative_path: "references/completion-events.md",
38 content: EMBEDDED_COMPLETION_EVENTS_MD,
39 },
40 EmbeddedFile {
41 relative_path: "references/openclaw.md",
42 content: EMBEDDED_OPENCLAW_MD,
43 },
44];
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct LockEntry {
49 pub name: String,
51 pub source_type: String,
53 pub installed_at: String,
55 pub path: String,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct LockFile {
62 pub skills: Vec<LockEntry>,
64}
65
66impl LockFile {
67 pub fn read(path: &Path) -> Result<Self> {
68 if !path.exists() {
69 return Ok(LockFile::default());
70 }
71 let raw = std::fs::read_to_string(path)
72 .with_context(|| format!("read lock file {}", path.display()))?;
73 if let Ok(lock) = serde_json::from_str::<LockFile>(&raw) {
74 return Ok(lock);
75 }
76 if let Ok(map) = serde_json::from_str::<serde_json::Value>(&raw)
77 && let Some(obj) = map.get("skills").and_then(|v| v.as_object())
78 {
79 let mut skills = Vec::new();
80 for (name, val) in obj {
81 if let Ok(entry) = serde_json::from_value::<LockEntry>(val.clone()) {
82 let mut e = entry;
83 e.name = name.clone();
84 skills.push(e);
85 }
86 }
87 return Ok(LockFile { skills });
88 }
89 Ok(LockFile::default())
90 }
91
92 pub fn write(&self, path: &Path) -> Result<()> {
93 if let Some(parent) = path.parent() {
94 std::fs::create_dir_all(parent)
95 .with_context(|| format!("create dirs for {}", path.display()))?;
96 }
97 let json = serde_json::to_string_pretty(self).context("serialize lock file")?;
98 std::fs::write(path, json).with_context(|| format!("write lock file {}", path.display()))
99 }
100
101 pub fn upsert(&mut self, entry: LockEntry) {
102 if let Some(existing) = self.skills.iter_mut().find(|e| e.name == entry.name) {
103 *existing = entry;
104 } else {
105 self.skills.push(entry);
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct InstalledSkill {
113 pub name: String,
114 pub path: PathBuf,
115 pub source_type: String,
116}
117
118pub fn install_builtin(agents_dir: &Path) -> Result<InstalledSkill> {
120 let name = "agent-exec";
121 let dest = agents_dir.join("skills").join(name);
122 std::fs::create_dir_all(&dest)
123 .with_context(|| format!("create skill dir {}", dest.display()))?;
124 for file in EMBEDDED_AGENT_EXEC_FILES {
125 let file_dest = dest.join(file.relative_path);
126 if let Some(parent) = file_dest.parent() {
127 std::fs::create_dir_all(parent)
128 .with_context(|| format!("create parent dir {}", parent.display()))?;
129 }
130 std::fs::write(&file_dest, file.content)
131 .with_context(|| format!("write embedded file {}", file_dest.display()))?;
132 }
133 Ok(InstalledSkill {
134 name: name.to_string(),
135 path: dest,
136 source_type: "embedded".to_string(),
137 })
138}
139
140pub fn resolve_root_dir(global: bool, claude: bool) -> Result<PathBuf> {
142 let root_name = if claude { ".claude" } else { ".agents" };
143 if global {
144 let home = directories::UserDirs::new()
145 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory"))?
146 .home_dir()
147 .to_path_buf();
148 Ok(home.join(root_name))
149 } else {
150 let cwd = std::env::current_dir().context("get current directory")?;
151 Ok(cwd.join(root_name))
152 }
153}
154
155pub fn now_rfc3339() -> String {
157 use std::time::{SystemTime, UNIX_EPOCH};
158 let secs = SystemTime::now()
159 .duration_since(UNIX_EPOCH)
160 .unwrap_or_default()
161 .as_secs();
162 let s = secs;
163 let sec = s % 60;
164 let s = s / 60;
165 let min = s % 60;
166 let s = s / 60;
167 let hour = s % 24;
168 let days = s / 24;
169 let (year, month, day) = days_to_ymd(days);
170 format!(
171 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
172 year, month, day, hour, min, sec
173 )
174}
175
176fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
177 let mut year = 1970u64;
178 loop {
179 let leap = is_leap(year);
180 let days_in_year = if leap { 366 } else { 365 };
181 if days < days_in_year {
182 break;
183 }
184 days -= days_in_year;
185 year += 1;
186 }
187 let leap = is_leap(year);
188 let months = [
189 31u64,
190 if leap { 29 } else { 28 },
191 31,
192 30,
193 31,
194 30,
195 31,
196 31,
197 30,
198 31,
199 30,
200 31,
201 ];
202 let mut month = 1u64;
203 for &dim in &months {
204 if days < dim {
205 break;
206 }
207 days -= dim;
208 month += 1;
209 }
210 (year, month, days + 1)
211}
212
213fn is_leap(year: u64) -> bool {
214 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
215}