Skip to main content

souk_core/
discovery.rs

1use std::path::{Path, PathBuf};
2
3use crate::error::SoukError;
4use crate::types::marketplace::Marketplace;
5
6#[derive(Debug, Clone)]
7pub struct MarketplaceConfig {
8    pub marketplace_path: PathBuf,
9    pub project_root: PathBuf,
10    pub plugin_root_abs: PathBuf,
11    pub marketplace: Marketplace,
12}
13
14pub fn discover_marketplace(start_dir: &Path) -> Result<PathBuf, SoukError> {
15    let mut current = start_dir.canonicalize().map_err(SoukError::Io)?;
16
17    loop {
18        let candidate = current.join(".claude-plugin").join("marketplace.json");
19        if candidate.is_file() {
20            return Ok(candidate);
21        }
22
23        if current.join(".git").exists() {
24            break;
25        }
26
27        match current.parent() {
28            Some(parent) if parent != current => {
29                current = parent.to_path_buf();
30            }
31            _ => break,
32        }
33    }
34
35    Err(SoukError::MarketplaceNotFound(start_dir.to_path_buf()))
36}
37
38pub fn load_marketplace_config(marketplace_path: &Path) -> Result<MarketplaceConfig, SoukError> {
39    let marketplace_path = marketplace_path.canonicalize().map_err(SoukError::Io)?;
40
41    let content = std::fs::read_to_string(&marketplace_path)?;
42    let marketplace: Marketplace = serde_json::from_str(&content)?;
43
44    let claude_plugin_dir = marketplace_path
45        .parent()
46        .ok_or_else(|| SoukError::Other("Invalid marketplace path".into()))?;
47    let project_root = claude_plugin_dir
48        .parent()
49        .ok_or_else(|| SoukError::Other("Invalid marketplace path".into()))?
50        .to_path_buf();
51
52    let plugin_root_rel = marketplace.normalized_plugin_root();
53    let plugin_root_abs = project_root
54        .join(&plugin_root_rel)
55        .canonicalize()
56        .map_err(|_| {
57            SoukError::Other(format!(
58                "Plugin root directory not found: {}",
59                project_root.join(&plugin_root_rel).display()
60            ))
61        })?;
62
63    Ok(MarketplaceConfig {
64        marketplace_path,
65        project_root,
66        plugin_root_abs,
67        marketplace,
68    })
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use tempfile::TempDir;
75
76    fn setup_marketplace(tmp: &TempDir) -> PathBuf {
77        let claude_dir = tmp.path().join(".claude-plugin");
78        std::fs::create_dir_all(&claude_dir).unwrap();
79        let plugins_dir = tmp.path().join("plugins");
80        std::fs::create_dir_all(&plugins_dir).unwrap();
81
82        let mp_path = claude_dir.join("marketplace.json");
83        std::fs::write(
84            &mp_path,
85            r#"{"version": "0.1.0", "pluginRoot": "./plugins", "plugins": []}"#,
86        )
87        .unwrap();
88        mp_path
89    }
90
91    #[test]
92    fn discover_from_project_root() {
93        let tmp = TempDir::new().unwrap();
94        let mp_path = setup_marketplace(&tmp);
95        let found = discover_marketplace(tmp.path()).unwrap();
96        assert_eq!(found, mp_path.canonicalize().unwrap());
97    }
98
99    #[test]
100    fn discover_from_subdirectory() {
101        let tmp = TempDir::new().unwrap();
102        setup_marketplace(&tmp);
103        let sub = tmp.path().join("plugins").join("my-plugin");
104        std::fs::create_dir_all(&sub).unwrap();
105
106        let found = discover_marketplace(&sub).unwrap();
107        assert!(found.ends_with("marketplace.json"));
108    }
109
110    #[test]
111    fn discover_not_found() {
112        let tmp = TempDir::new().unwrap();
113        std::fs::create_dir(tmp.path().join(".git")).unwrap();
114        let result = discover_marketplace(tmp.path());
115        assert!(result.is_err());
116    }
117
118    #[test]
119    fn load_marketplace_config_resolves_paths() {
120        let tmp = TempDir::new().unwrap();
121        let mp_path = setup_marketplace(&tmp);
122        let config = load_marketplace_config(&mp_path).unwrap();
123        assert_eq!(config.project_root, tmp.path().canonicalize().unwrap());
124        assert!(config.plugin_root_abs.ends_with("plugins"));
125    }
126
127    #[test]
128    fn load_marketplace_config_default_plugin_root() {
129        let tmp = TempDir::new().unwrap();
130        let claude_dir = tmp.path().join(".claude-plugin");
131        std::fs::create_dir_all(&claude_dir).unwrap();
132        let plugins_dir = tmp.path().join("plugins");
133        std::fs::create_dir_all(&plugins_dir).unwrap();
134        let mp_path = claude_dir.join("marketplace.json");
135        std::fs::write(&mp_path, r#"{"version": "0.1.0", "plugins": []}"#).unwrap();
136
137        let config = load_marketplace_config(&mp_path).unwrap();
138        assert!(config.plugin_root_abs.ends_with("plugins"));
139    }
140}