Skip to main content

souk_core/resolution/
plugin.rs

1use std::path::{Path, PathBuf};
2
3use crate::discovery::MarketplaceConfig;
4use crate::error::SoukError;
5
6pub fn resolve_plugin(
7    input: &str,
8    config: Option<&MarketplaceConfig>,
9) -> Result<PathBuf, SoukError> {
10    let direct = PathBuf::from(input);
11    if direct.is_dir() {
12        return direct.canonicalize().map_err(SoukError::Io);
13    }
14
15    if let Some(config) = config {
16        let relative = config.plugin_root_abs.join(input);
17        if relative.is_dir() {
18            return relative.canonicalize().map_err(SoukError::Io);
19        }
20
21        if let Some(entry) = config.marketplace.plugins.iter().find(|p| p.name == input) {
22            let resolved = resolve_source(&entry.source, config)?;
23            if resolved.is_dir() {
24                return resolved.canonicalize().map_err(SoukError::Io);
25            }
26        }
27    }
28
29    Err(SoukError::PluginNotFound(input.to_string()))
30}
31
32pub fn resolve_source(source: &str, config: &MarketplaceConfig) -> Result<PathBuf, SoukError> {
33    if source.starts_with('/') {
34        Ok(PathBuf::from(source))
35    } else if source.starts_with("./") || source.starts_with("../") {
36        Ok(config.project_root.join(source))
37    } else {
38        Ok(config.plugin_root_abs.join(source))
39    }
40}
41
42pub fn plugin_path_to_source(path: &Path, config: &MarketplaceConfig) -> (String, bool) {
43    let canon_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
44    let canon_root = &config.plugin_root_abs;
45
46    if let Ok(relative) = canon_path.strip_prefix(canon_root) {
47        let dir_name = relative
48            .components()
49            .next()
50            .map(|c| c.as_os_str().to_string_lossy().to_string())
51            .unwrap_or_default();
52        (dir_name, true)
53    } else {
54        (canon_path.to_string_lossy().to_string(), false)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::discovery::load_marketplace_config;
62    use tempfile::TempDir;
63
64    fn setup(tmp: &TempDir) -> MarketplaceConfig {
65        let claude_dir = tmp.path().join(".claude-plugin");
66        std::fs::create_dir_all(&claude_dir).unwrap();
67        let plugins_dir = tmp.path().join("plugins");
68        std::fs::create_dir_all(&plugins_dir).unwrap();
69
70        let plugin_dir = plugins_dir.join("my-plugin").join(".claude-plugin");
71        std::fs::create_dir_all(&plugin_dir).unwrap();
72        std::fs::write(
73            plugin_dir.join("plugin.json"),
74            r#"{"name": "my-plugin", "version": "1.0.0", "description": "test"}"#,
75        )
76        .unwrap();
77
78        let mp_path = claude_dir.join("marketplace.json");
79        std::fs::write(
80            &mp_path,
81            r#"{
82                "version": "0.1.0",
83                "pluginRoot": "./plugins",
84                "plugins": [
85                    {"name": "My Plugin", "source": "my-plugin"}
86                ]
87            }"#,
88        )
89        .unwrap();
90
91        load_marketplace_config(&mp_path).unwrap()
92    }
93
94    #[test]
95    fn resolve_by_direct_path() {
96        let tmp = TempDir::new().unwrap();
97        let config = setup(&tmp);
98        let plugin_path = tmp.path().join("plugins").join("my-plugin");
99        let result = resolve_plugin(plugin_path.to_str().unwrap(), Some(&config));
100        assert!(result.is_ok());
101    }
102
103    #[test]
104    fn resolve_by_plugin_root_relative() {
105        let tmp = TempDir::new().unwrap();
106        let config = setup(&tmp);
107        let result = resolve_plugin("my-plugin", Some(&config));
108        assert!(result.is_ok());
109    }
110
111    #[test]
112    fn resolve_by_marketplace_name() {
113        let tmp = TempDir::new().unwrap();
114        let config = setup(&tmp);
115        let result = resolve_plugin("My Plugin", Some(&config));
116        assert!(result.is_ok());
117    }
118
119    #[test]
120    fn resolve_not_found() {
121        let tmp = TempDir::new().unwrap();
122        let config = setup(&tmp);
123        let result = resolve_plugin("nonexistent", Some(&config));
124        assert!(matches!(result, Err(SoukError::PluginNotFound(_))));
125    }
126
127    #[test]
128    fn path_to_source_internal() {
129        let tmp = TempDir::new().unwrap();
130        let config = setup(&tmp);
131        let path = config.plugin_root_abs.join("my-plugin");
132        let (source, is_internal) = plugin_path_to_source(&path, &config);
133        assert_eq!(source, "my-plugin");
134        assert!(is_internal);
135    }
136
137    #[test]
138    fn path_to_source_external() {
139        let tmp = TempDir::new().unwrap();
140        let config = setup(&tmp);
141        let external = TempDir::new().unwrap();
142        let (source, is_internal) = plugin_path_to_source(external.path(), &config);
143        assert!(!is_internal);
144        assert!(std::path::Path::new(&source).is_absolute());
145    }
146}