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}