Skip to main content

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}