Skip to main content

atomcode_core/plugin/
loader.rs

1//! Helpers for downstream registries (skill / commands / hooks) to discover
2//! every installed plugin's asset directories in one pass.
3
4use std::path::PathBuf;
5
6use super::manifest::{load_plugin_manifest, PluginManifest};
7use super::paths;
8use super::state::load_installed_plugins_file;
9
10#[derive(Debug, Clone)]
11pub struct InstalledPluginAssets {
12    pub plugin: String,
13    pub marketplace: String,
14    pub plugin_dir: PathBuf,
15    pub manifest: PluginManifest,
16}
17
18impl InstalledPluginAssets {
19    /// Primary skills directory — the first entry from the manifest's
20    /// `skills` field, or the default `"skills"` when absent.
21    pub fn skills_dir(&self) -> PathBuf {
22        self.plugin_dir.join(self.manifest.skills_path())
23    }
24    /// All skills directories declared in the manifest.
25    ///
26    /// When `skills` is absent this returns a single default `"skills"`
27    /// entry (same as `skills_dir()`). When it is a CC-style array
28    /// (`["./skills/foo", "./skills/bar"]`) each entry is resolved
29    /// relative to `plugin_dir`, allowing multiple skill directories
30    /// to contribute.
31    pub fn skills_dirs(&self) -> Vec<PathBuf> {
32        self.manifest
33            .skills_paths()
34            .into_iter()
35            .map(|p| self.plugin_dir.join(p))
36            .collect()
37    }
38    pub fn commands_dir(&self) -> PathBuf {
39        self.plugin_dir.join(self.manifest.commands_path())
40    }
41    pub fn hooks_file(&self) -> PathBuf {
42        self.plugin_dir.join(self.manifest.hooks_path())
43    }
44}
45
46/// Iterate over every installed plugin. Returns empty Vec when state file is
47/// missing or the plugin home is not configured. Skips entries whose
48/// plugin_dir does not exist on disk (keeps reload resilient to deletions).
49pub fn iter_installed_plugin_assets() -> Vec<InstalledPluginAssets> {
50    let Some(state_path) = paths::installed_plugins_file() else { return vec![]; };
51    let state = match load_installed_plugins_file(&state_path) {
52        Ok(s) => s,
53        Err(_) => return vec![],
54    };
55    let Some(plugins_root) = paths::plugins_root() else { return vec![]; };
56
57    state
58        .plugins
59        .into_values()
60        .filter_map(|e| {
61            let abs = plugins_root.join(&e.plugin_dir);
62            if !abs.exists() {
63                return None;
64            }
65            let manifest = load_plugin_manifest(&abs).unwrap_or_default();
66            Some(InstalledPluginAssets {
67                plugin: e.plugin,
68                marketplace: e.marketplace,
69                plugin_dir: abs,
70                manifest,
71            })
72        })
73        .collect()
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::plugin::installer::install;
80    use crate::plugin::marketplace::add_marketplace;
81    use crate::plugin::test_support::isolated_home;
82    use std::path::PathBuf;
83    use std::process::Command;
84
85    fn make_repo(name: &str) -> PathBuf {
86        let work = tempfile::tempdir().unwrap().keep();
87        let repo = work.join(name);
88        std::fs::create_dir_all(&repo).unwrap();
89        Command::new("git").args(["init", "-q"]).current_dir(&repo).status().unwrap();
90        Command::new("git").args(["config", "user.email", "t@t"]).current_dir(&repo).status().unwrap();
91        Command::new("git").args(["config", "user.name", "t"]).current_dir(&repo).status().unwrap();
92        std::fs::create_dir_all(repo.join("skills/foo")).unwrap();
93        std::fs::write(
94            repo.join("skills/foo/SKILL.md"),
95            "---\nname: foo\ndescription: f\n---\nbody",
96        )
97        .unwrap();
98        Command::new("git").args(["add", "-A"]).current_dir(&repo).status().unwrap();
99        Command::new("git").args(["commit", "-q", "-m", "init"]).current_dir(&repo).status().unwrap();
100        repo
101    }
102
103    #[test]
104    #[serial_test::serial]
105    fn iter_yields_installed() {
106        let _home = isolated_home();
107        let repo = make_repo("p");
108        add_marketplace(&format!("file://{}", repo.display())).unwrap();
109        install("p", "p").unwrap();
110        let assets = iter_installed_plugin_assets();
111        assert_eq!(assets.len(), 1);
112        assert_eq!(assets[0].plugin, "p");
113        assert!(assets[0].skills_dir().exists());
114    }
115}