deepstrike_sdk/runtime/skill_watcher.rs
1use std::path::Path;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher};
6
7/// Background watcher for a skill directory.
8///
9/// Uses the OS-native FS notification API (kqueue on macOS, inotify on Linux).
10/// Callers snapshot `version()` at the start of each SM iteration and re-scan
11/// the directory whenever the value has changed — no polling thread needed.
12pub struct SkillWatcher {
13 // Keep the watcher alive for the lifetime of this struct; dropping it
14 // unregisters the OS watch.
15 _watcher: RecommendedWatcher,
16 version: Arc<AtomicU64>,
17}
18
19impl SkillWatcher {
20 /// Start watching `dir`. Returns `None` if the path doesn't exist or the
21 /// OS watcher cannot be created (e.g. unavailable on this platform).
22 pub fn start(dir: &Path) -> Option<Self> {
23 let version = Arc::new(AtomicU64::new(0));
24 let version2 = Arc::clone(&version);
25
26 let mut watcher =
27 notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
28 let Ok(event) = res else { return };
29 let is_relevant = matches!(
30 event.kind,
31 EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
32 ) && event.paths.iter().any(|p| {
33 matches!(
34 p.extension().and_then(|e| e.to_str()),
35 Some("md") | Some("json") | Some("py")
36 )
37 });
38 if is_relevant {
39 version2.fetch_add(1, Ordering::Relaxed);
40 }
41 })
42 .ok()?;
43
44 watcher.watch(dir, RecursiveMode::NonRecursive).ok()?;
45
46 Some(Self {
47 _watcher: watcher,
48 version,
49 })
50 }
51
52 /// Monotonically-increasing counter; increments whenever a `.md`, `.json`,
53 /// or `.py` file in the watched directory is created, modified, or removed.
54 ///
55 /// Callers should snapshot this at the start of each loop iteration and
56 /// compare against the previous snapshot; a change means the skill catalog
57 /// should be refreshed.
58 pub fn version(&self) -> u64 {
59 self.version.load(Ordering::Relaxed)
60 }
61}