souk_core/resolution/
plugin.rs1use 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}