1use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result, bail};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug)]
20pub struct UnknownSourceScheme(pub String);
21
22impl std::fmt::Display for UnknownSourceScheme {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(
25 f,
26 "unknown_source_scheme: {:?} (supported: self, local:<path>)",
27 self.0
28 )
29 }
30}
31
32impl std::error::Error for UnknownSourceScheme {}
33
34const EMBEDDED_SKILL_MD: &[u8] = include_bytes!("../skills/agent-exec/SKILL.md");
40
41pub struct EmbeddedFile {
43 pub relative_path: &'static str,
44 pub content: &'static [u8],
45}
46
47pub static EMBEDDED_AGENT_EXEC_FILES: &[EmbeddedFile] = &[EmbeddedFile {
49 relative_path: "SKILL.md",
50 content: EMBEDDED_SKILL_MD,
51}];
52
53#[derive(Debug, Clone)]
59pub enum Source {
60 SelfEmbedded,
62 Local(PathBuf),
64}
65
66impl Source {
67 pub fn parse(s: &str) -> Result<Self> {
72 if s == "self" {
73 return Ok(Source::SelfEmbedded);
74 }
75 if let Some(path) = s.strip_prefix("local:") {
76 return Ok(Source::Local(PathBuf::from(path)));
77 }
78 bail!(UnknownSourceScheme(s.to_string()));
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LockEntry {
89 pub name: String,
91 pub source_type: String,
93 pub installed_at: String,
95 pub path: String,
97}
98
99#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101pub struct LockFile {
102 pub skills: Vec<LockEntry>,
104}
105
106impl LockFile {
107 pub fn read(path: &Path) -> Result<Self> {
111 if !path.exists() {
112 return Ok(LockFile::default());
113 }
114 let raw = std::fs::read_to_string(path)
115 .with_context(|| format!("read lock file {}", path.display()))?;
116 if let Ok(lock) = serde_json::from_str::<LockFile>(&raw) {
118 return Ok(lock);
119 }
120 if let Ok(map) = serde_json::from_str::<serde_json::Value>(&raw)
123 && let Some(obj) = map.get("skills").and_then(|v| v.as_object())
124 {
125 let mut skills = Vec::new();
126 for (name, val) in obj {
127 if let Ok(entry) = serde_json::from_value::<LockEntry>(val.clone()) {
128 let mut e = entry;
129 e.name = name.clone();
130 skills.push(e);
131 }
132 }
133 return Ok(LockFile { skills });
134 }
135 Ok(LockFile::default())
136 }
137
138 pub fn write(&self, path: &Path) -> Result<()> {
140 if let Some(parent) = path.parent() {
141 std::fs::create_dir_all(parent)
142 .with_context(|| format!("create dirs for {}", path.display()))?;
143 }
144 let json = serde_json::to_string_pretty(self).context("serialize lock file")?;
145 std::fs::write(path, json).with_context(|| format!("write lock file {}", path.display()))
146 }
147
148 pub fn upsert(&mut self, entry: LockEntry) {
150 if let Some(existing) = self.skills.iter_mut().find(|e| e.name == entry.name) {
151 *existing = entry;
152 } else {
153 self.skills.push(entry);
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
164pub struct InstalledSkill {
165 pub name: String,
167 pub path: PathBuf,
169 pub source_str: String,
171}
172
173pub fn install(source: &Source, agents_dir: &Path) -> Result<InstalledSkill> {
179 let skills_dir = agents_dir.join("skills");
180 match source {
181 Source::SelfEmbedded => {
182 let name = "agent-exec";
183 let dest = skills_dir.join(name);
184 std::fs::create_dir_all(&dest)
185 .with_context(|| format!("create skill dir {}", dest.display()))?;
186 for file in EMBEDDED_AGENT_EXEC_FILES {
187 let file_dest = dest.join(file.relative_path);
188 if let Some(parent) = file_dest.parent() {
189 std::fs::create_dir_all(parent)
190 .with_context(|| format!("create parent dir {}", parent.display()))?;
191 }
192 std::fs::write(&file_dest, file.content)
193 .with_context(|| format!("write embedded file {}", file_dest.display()))?;
194 }
195 Ok(InstalledSkill {
196 name: name.to_string(),
197 path: dest,
198 source_str: "self".to_string(),
199 })
200 }
201 Source::Local(src_path) => {
202 let name = src_path
203 .file_name()
204 .and_then(|n| n.to_str())
205 .ok_or_else(|| {
206 anyhow::anyhow!(
207 "cannot determine skill name from path: {}",
208 src_path.display()
209 )
210 })?;
211 let dest = skills_dir.join(name);
212 copy_dir_local(src_path, &dest)?;
213 Ok(InstalledSkill {
214 name: name.to_string(),
215 path: dest,
216 source_str: format!("local:{}", src_path.display()),
217 })
218 }
219 }
220}
221
222fn copy_dir_local(src: &Path, dst: &Path) -> Result<()> {
227 #[cfg(unix)]
228 {
229 use std::os::unix::fs::symlink;
230 if dst.exists() || dst.symlink_metadata().is_ok() {
232 if dst.is_symlink() || dst.is_file() {
233 std::fs::remove_file(dst).ok();
234 } else if dst.is_dir() {
235 std::fs::remove_dir_all(dst).ok();
236 }
237 }
238 let abs_src = src
240 .canonicalize()
241 .with_context(|| format!("canonicalize local skill source path {}", src.display()))?;
242 if symlink(&abs_src, dst).is_ok() {
243 return Ok(());
244 }
245 copy_dir_recursive(src, dst)
247 }
248 #[cfg(not(unix))]
249 {
250 copy_dir_recursive(src, dst)
251 }
252}
253
254fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
256 std::fs::create_dir_all(dst).with_context(|| format!("create dir {}", dst.display()))?;
257 for entry in std::fs::read_dir(src).with_context(|| format!("read dir {}", src.display()))? {
258 let entry = entry.with_context(|| format!("iterate dir {}", src.display()))?;
259 let file_type = entry.file_type().context("get file type")?;
260 let src_path = entry.path();
261 let dst_path = dst.join(entry.file_name());
262 if file_type.is_dir() {
263 copy_dir_recursive(&src_path, &dst_path)?;
264 } else {
265 std::fs::copy(&src_path, &dst_path).with_context(|| {
266 format!("copy {} to {}", src_path.display(), dst_path.display())
267 })?;
268 }
269 }
270 Ok(())
271}
272
273pub fn resolve_agents_dir(global: bool) -> Result<PathBuf> {
278 if global {
279 let home = directories::UserDirs::new()
280 .ok_or_else(|| anyhow::anyhow!("cannot determine home directory"))?
281 .home_dir()
282 .to_path_buf();
283 Ok(home.join(".agents"))
284 } else {
285 let cwd = std::env::current_dir().context("get current directory")?;
286 Ok(cwd.join(".agents"))
287 }
288}
289
290pub fn now_rfc3339() -> String {
292 use std::time::{SystemTime, UNIX_EPOCH};
295 let secs = SystemTime::now()
296 .duration_since(UNIX_EPOCH)
297 .unwrap_or_default()
298 .as_secs();
299 let s = secs;
301 let sec = s % 60;
302 let s = s / 60;
303 let min = s % 60;
304 let s = s / 60;
305 let hour = s % 24;
306 let days = s / 24;
307 let (year, month, day) = days_to_ymd(days);
309 format!(
310 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
311 year, month, day, hour, min, sec
312 )
313}
314
315fn days_to_ymd(mut days: u64) -> (u64, u64, u64) {
316 let mut year = 1970u64;
318 loop {
319 let leap = is_leap(year);
320 let days_in_year = if leap { 366 } else { 365 };
321 if days < days_in_year {
322 break;
323 }
324 days -= days_in_year;
325 year += 1;
326 }
327 let leap = is_leap(year);
328 let months = [
329 31u64,
330 if leap { 29 } else { 28 },
331 31,
332 30,
333 31,
334 30,
335 31,
336 31,
337 30,
338 31,
339 30,
340 31,
341 ];
342 let mut month = 1u64;
343 for &dim in &months {
344 if days < dim {
345 break;
346 }
347 days -= dim;
348 month += 1;
349 }
350 (year, month, days + 1)
351}
352
353fn is_leap(year: u64) -> bool {
354 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
355}