lib.rs 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  1. use std::collections::BTreeMap;
  2. use std::fmt::{Display, Formatter};
  3. use std::fs;
  4. use std::path::{Path, PathBuf};
  5. use std::process::Command;
  6. use std::time::{SystemTime, UNIX_EPOCH};
  7. use serde::{Deserialize, Serialize};
  8. use serde_json::{Map, Value};
  9. const EXTERNAL_MARKETPLACE: &str = "external";
  10. const BUILTIN_MARKETPLACE: &str = "builtin";
  11. const BUNDLED_MARKETPLACE: &str = "bundled";
  12. const SETTINGS_FILE_NAME: &str = "settings.json";
  13. const REGISTRY_FILE_NAME: &str = "installed.json";
  14. const MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
  15. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
  16. #[serde(rename_all = "lowercase")]
  17. pub enum PluginKind {
  18. Builtin,
  19. Bundled,
  20. External,
  21. }
  22. impl Display for PluginKind {
  23. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  24. match self {
  25. Self::Builtin => write!(f, "builtin"),
  26. Self::Bundled => write!(f, "bundled"),
  27. Self::External => write!(f, "external"),
  28. }
  29. }
  30. }
  31. #[derive(Debug, Clone, PartialEq, Eq)]
  32. pub struct PluginMetadata {
  33. pub id: String,
  34. pub name: String,
  35. pub version: String,
  36. pub description: String,
  37. pub kind: PluginKind,
  38. pub source: String,
  39. pub default_enabled: bool,
  40. pub root: Option<PathBuf>,
  41. }
  42. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  43. pub struct PluginHooks {
  44. #[serde(rename = "PreToolUse", default)]
  45. pub pre_tool_use: Vec<String>,
  46. #[serde(rename = "PostToolUse", default)]
  47. pub post_tool_use: Vec<String>,
  48. }
  49. impl PluginHooks {
  50. #[must_use]
  51. pub fn is_empty(&self) -> bool {
  52. self.pre_tool_use.is_empty() && self.post_tool_use.is_empty()
  53. }
  54. #[must_use]
  55. pub fn merged_with(&self, other: &Self) -> Self {
  56. let mut merged = self.clone();
  57. merged
  58. .pre_tool_use
  59. .extend(other.pre_tool_use.iter().cloned());
  60. merged
  61. .post_tool_use
  62. .extend(other.post_tool_use.iter().cloned());
  63. merged
  64. }
  65. }
  66. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  67. pub struct PluginManifest {
  68. pub name: String,
  69. pub version: String,
  70. pub description: String,
  71. #[serde(rename = "defaultEnabled", default)]
  72. pub default_enabled: bool,
  73. #[serde(default)]
  74. pub hooks: PluginHooks,
  75. }
  76. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  77. #[serde(tag = "type", rename_all = "snake_case")]
  78. pub enum PluginInstallSource {
  79. LocalPath { path: PathBuf },
  80. GitUrl { url: String },
  81. }
  82. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  83. pub struct InstalledPluginRecord {
  84. pub id: String,
  85. pub name: String,
  86. pub version: String,
  87. pub description: String,
  88. pub install_path: PathBuf,
  89. pub source: PluginInstallSource,
  90. pub installed_at_unix_ms: u128,
  91. pub updated_at_unix_ms: u128,
  92. }
  93. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
  94. pub struct InstalledPluginRegistry {
  95. #[serde(default)]
  96. pub plugins: BTreeMap<String, InstalledPluginRecord>,
  97. }
  98. #[derive(Debug, Clone, PartialEq, Eq)]
  99. pub struct BuiltinPlugin {
  100. metadata: PluginMetadata,
  101. hooks: PluginHooks,
  102. }
  103. #[derive(Debug, Clone, PartialEq, Eq)]
  104. pub struct BundledPlugin {
  105. metadata: PluginMetadata,
  106. hooks: PluginHooks,
  107. }
  108. #[derive(Debug, Clone, PartialEq, Eq)]
  109. pub struct ExternalPlugin {
  110. metadata: PluginMetadata,
  111. hooks: PluginHooks,
  112. }
  113. pub trait Plugin {
  114. fn metadata(&self) -> &PluginMetadata;
  115. fn hooks(&self) -> &PluginHooks;
  116. fn validate(&self) -> Result<(), PluginError>;
  117. }
  118. #[derive(Debug, Clone, PartialEq, Eq)]
  119. pub enum PluginDefinition {
  120. Builtin(BuiltinPlugin),
  121. Bundled(BundledPlugin),
  122. External(ExternalPlugin),
  123. }
  124. impl Plugin for BuiltinPlugin {
  125. fn metadata(&self) -> &PluginMetadata {
  126. &self.metadata
  127. }
  128. fn hooks(&self) -> &PluginHooks {
  129. &self.hooks
  130. }
  131. fn validate(&self) -> Result<(), PluginError> {
  132. Ok(())
  133. }
  134. }
  135. impl Plugin for BundledPlugin {
  136. fn metadata(&self) -> &PluginMetadata {
  137. &self.metadata
  138. }
  139. fn hooks(&self) -> &PluginHooks {
  140. &self.hooks
  141. }
  142. fn validate(&self) -> Result<(), PluginError> {
  143. validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)
  144. }
  145. }
  146. impl Plugin for ExternalPlugin {
  147. fn metadata(&self) -> &PluginMetadata {
  148. &self.metadata
  149. }
  150. fn hooks(&self) -> &PluginHooks {
  151. &self.hooks
  152. }
  153. fn validate(&self) -> Result<(), PluginError> {
  154. validate_hook_paths(self.metadata.root.as_deref(), &self.hooks)
  155. }
  156. }
  157. impl Plugin for PluginDefinition {
  158. fn metadata(&self) -> &PluginMetadata {
  159. match self {
  160. Self::Builtin(plugin) => plugin.metadata(),
  161. Self::Bundled(plugin) => plugin.metadata(),
  162. Self::External(plugin) => plugin.metadata(),
  163. }
  164. }
  165. fn hooks(&self) -> &PluginHooks {
  166. match self {
  167. Self::Builtin(plugin) => plugin.hooks(),
  168. Self::Bundled(plugin) => plugin.hooks(),
  169. Self::External(plugin) => plugin.hooks(),
  170. }
  171. }
  172. fn validate(&self) -> Result<(), PluginError> {
  173. match self {
  174. Self::Builtin(plugin) => plugin.validate(),
  175. Self::Bundled(plugin) => plugin.validate(),
  176. Self::External(plugin) => plugin.validate(),
  177. }
  178. }
  179. }
  180. #[derive(Debug, Clone, PartialEq, Eq)]
  181. pub struct RegisteredPlugin {
  182. definition: PluginDefinition,
  183. enabled: bool,
  184. }
  185. impl RegisteredPlugin {
  186. #[must_use]
  187. pub fn new(definition: PluginDefinition, enabled: bool) -> Self {
  188. Self {
  189. definition,
  190. enabled,
  191. }
  192. }
  193. #[must_use]
  194. pub fn metadata(&self) -> &PluginMetadata {
  195. self.definition.metadata()
  196. }
  197. #[must_use]
  198. pub fn hooks(&self) -> &PluginHooks {
  199. self.definition.hooks()
  200. }
  201. #[must_use]
  202. pub fn is_enabled(&self) -> bool {
  203. self.enabled
  204. }
  205. pub fn validate(&self) -> Result<(), PluginError> {
  206. self.definition.validate()
  207. }
  208. #[must_use]
  209. pub fn summary(&self) -> PluginSummary {
  210. PluginSummary {
  211. metadata: self.metadata().clone(),
  212. enabled: self.enabled,
  213. }
  214. }
  215. }
  216. #[derive(Debug, Clone, PartialEq, Eq)]
  217. pub struct PluginSummary {
  218. pub metadata: PluginMetadata,
  219. pub enabled: bool,
  220. }
  221. #[derive(Debug, Clone, Default, PartialEq, Eq)]
  222. pub struct PluginRegistry {
  223. plugins: Vec<RegisteredPlugin>,
  224. }
  225. impl PluginRegistry {
  226. #[must_use]
  227. pub fn new(mut plugins: Vec<RegisteredPlugin>) -> Self {
  228. plugins.sort_by(|left, right| left.metadata().id.cmp(&right.metadata().id));
  229. Self { plugins }
  230. }
  231. #[must_use]
  232. pub fn plugins(&self) -> &[RegisteredPlugin] {
  233. &self.plugins
  234. }
  235. #[must_use]
  236. pub fn get(&self, plugin_id: &str) -> Option<&RegisteredPlugin> {
  237. self.plugins
  238. .iter()
  239. .find(|plugin| plugin.metadata().id == plugin_id)
  240. }
  241. #[must_use]
  242. pub fn contains(&self, plugin_id: &str) -> bool {
  243. self.get(plugin_id).is_some()
  244. }
  245. #[must_use]
  246. pub fn summaries(&self) -> Vec<PluginSummary> {
  247. self.plugins.iter().map(RegisteredPlugin::summary).collect()
  248. }
  249. pub fn aggregated_hooks(&self) -> Result<PluginHooks, PluginError> {
  250. self.plugins
  251. .iter()
  252. .filter(|plugin| plugin.is_enabled())
  253. .try_fold(PluginHooks::default(), |acc, plugin| {
  254. plugin.validate()?;
  255. Ok(acc.merged_with(plugin.hooks()))
  256. })
  257. }
  258. }
  259. #[derive(Debug, Clone, PartialEq, Eq)]
  260. pub struct PluginManagerConfig {
  261. pub config_home: PathBuf,
  262. pub enabled_plugins: BTreeMap<String, bool>,
  263. pub external_dirs: Vec<PathBuf>,
  264. pub install_root: Option<PathBuf>,
  265. pub registry_path: Option<PathBuf>,
  266. pub bundled_root: Option<PathBuf>,
  267. }
  268. impl PluginManagerConfig {
  269. #[must_use]
  270. pub fn new(config_home: impl Into<PathBuf>) -> Self {
  271. Self {
  272. config_home: config_home.into(),
  273. enabled_plugins: BTreeMap::new(),
  274. external_dirs: Vec::new(),
  275. install_root: None,
  276. registry_path: None,
  277. bundled_root: None,
  278. }
  279. }
  280. }
  281. #[derive(Debug, Clone, PartialEq, Eq)]
  282. pub struct PluginManager {
  283. config: PluginManagerConfig,
  284. }
  285. #[derive(Debug, Clone, PartialEq, Eq)]
  286. pub struct InstallOutcome {
  287. pub plugin_id: String,
  288. pub version: String,
  289. pub install_path: PathBuf,
  290. }
  291. #[derive(Debug, Clone, PartialEq, Eq)]
  292. pub struct UpdateOutcome {
  293. pub plugin_id: String,
  294. pub old_version: String,
  295. pub new_version: String,
  296. pub install_path: PathBuf,
  297. }
  298. #[derive(Debug)]
  299. pub enum PluginError {
  300. Io(std::io::Error),
  301. Json(serde_json::Error),
  302. InvalidManifest(String),
  303. NotFound(String),
  304. CommandFailed(String),
  305. }
  306. impl Display for PluginError {
  307. fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
  308. match self {
  309. Self::Io(error) => write!(f, "{error}"),
  310. Self::Json(error) => write!(f, "{error}"),
  311. Self::InvalidManifest(message)
  312. | Self::NotFound(message)
  313. | Self::CommandFailed(message) => write!(f, "{message}"),
  314. }
  315. }
  316. }
  317. impl std::error::Error for PluginError {}
  318. impl From<std::io::Error> for PluginError {
  319. fn from(value: std::io::Error) -> Self {
  320. Self::Io(value)
  321. }
  322. }
  323. impl From<serde_json::Error> for PluginError {
  324. fn from(value: serde_json::Error) -> Self {
  325. Self::Json(value)
  326. }
  327. }
  328. impl PluginManager {
  329. #[must_use]
  330. pub fn new(config: PluginManagerConfig) -> Self {
  331. Self { config }
  332. }
  333. #[must_use]
  334. pub fn bundled_root() -> PathBuf {
  335. PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("bundled")
  336. }
  337. #[must_use]
  338. pub fn install_root(&self) -> PathBuf {
  339. self.config
  340. .install_root
  341. .clone()
  342. .unwrap_or_else(|| self.config.config_home.join("plugins").join("installed"))
  343. }
  344. #[must_use]
  345. pub fn registry_path(&self) -> PathBuf {
  346. self.config.registry_path.clone().unwrap_or_else(|| {
  347. self.config
  348. .config_home
  349. .join("plugins")
  350. .join(REGISTRY_FILE_NAME)
  351. })
  352. }
  353. #[must_use]
  354. pub fn settings_path(&self) -> PathBuf {
  355. self.config.config_home.join(SETTINGS_FILE_NAME)
  356. }
  357. pub fn plugin_registry(&self) -> Result<PluginRegistry, PluginError> {
  358. Ok(PluginRegistry::new(
  359. self.discover_plugins()?
  360. .into_iter()
  361. .map(|plugin| {
  362. let enabled = self.is_enabled(plugin.metadata());
  363. RegisteredPlugin::new(plugin, enabled)
  364. })
  365. .collect(),
  366. ))
  367. }
  368. pub fn list_plugins(&self) -> Result<Vec<PluginSummary>, PluginError> {
  369. Ok(self.plugin_registry()?.summaries())
  370. }
  371. pub fn discover_plugins(&self) -> Result<Vec<PluginDefinition>, PluginError> {
  372. let mut plugins = builtin_plugins();
  373. plugins.extend(self.discover_bundled_plugins()?);
  374. plugins.extend(self.discover_external_plugins()?);
  375. Ok(plugins)
  376. }
  377. pub fn aggregated_hooks(&self) -> Result<PluginHooks, PluginError> {
  378. self.plugin_registry()?.aggregated_hooks()
  379. }
  380. pub fn validate_plugin_source(&self, source: &str) -> Result<PluginManifest, PluginError> {
  381. let path = resolve_local_source(source)?;
  382. load_validated_manifest_from_root(&path)
  383. }
  384. pub fn install(&mut self, source: &str) -> Result<InstallOutcome, PluginError> {
  385. let install_source = parse_install_source(source)?;
  386. let temp_root = self.install_root().join(".tmp");
  387. let staged_source = materialize_source(&install_source, &temp_root)?;
  388. let cleanup_source = matches!(install_source, PluginInstallSource::GitUrl { .. });
  389. let manifest = load_validated_manifest_from_root(&staged_source)?;
  390. let plugin_id = plugin_id(&manifest.name, EXTERNAL_MARKETPLACE);
  391. let install_path = self.install_root().join(sanitize_plugin_id(&plugin_id));
  392. if install_path.exists() {
  393. fs::remove_dir_all(&install_path)?;
  394. }
  395. copy_dir_all(&staged_source, &install_path)?;
  396. if cleanup_source {
  397. let _ = fs::remove_dir_all(&staged_source);
  398. }
  399. let now = unix_time_ms();
  400. let record = InstalledPluginRecord {
  401. id: plugin_id.clone(),
  402. name: manifest.name,
  403. version: manifest.version.clone(),
  404. description: manifest.description,
  405. install_path: install_path.clone(),
  406. source: install_source,
  407. installed_at_unix_ms: now,
  408. updated_at_unix_ms: now,
  409. };
  410. let mut registry = self.load_registry()?;
  411. registry.plugins.insert(plugin_id.clone(), record);
  412. self.store_registry(&registry)?;
  413. self.write_enabled_state(&plugin_id, Some(true))?;
  414. self.config.enabled_plugins.insert(plugin_id.clone(), true);
  415. Ok(InstallOutcome {
  416. plugin_id,
  417. version: manifest.version,
  418. install_path,
  419. })
  420. }
  421. pub fn enable(&mut self, plugin_id: &str) -> Result<(), PluginError> {
  422. self.ensure_known_plugin(plugin_id)?;
  423. self.write_enabled_state(plugin_id, Some(true))?;
  424. self.config
  425. .enabled_plugins
  426. .insert(plugin_id.to_string(), true);
  427. Ok(())
  428. }
  429. pub fn disable(&mut self, plugin_id: &str) -> Result<(), PluginError> {
  430. self.ensure_known_plugin(plugin_id)?;
  431. self.write_enabled_state(plugin_id, Some(false))?;
  432. self.config
  433. .enabled_plugins
  434. .insert(plugin_id.to_string(), false);
  435. Ok(())
  436. }
  437. pub fn uninstall(&mut self, plugin_id: &str) -> Result<(), PluginError> {
  438. let mut registry = self.load_registry()?;
  439. let record = registry.plugins.remove(plugin_id).ok_or_else(|| {
  440. PluginError::NotFound(format!("plugin `{plugin_id}` is not installed"))
  441. })?;
  442. if record.install_path.exists() {
  443. fs::remove_dir_all(&record.install_path)?;
  444. }
  445. self.store_registry(&registry)?;
  446. self.write_enabled_state(plugin_id, None)?;
  447. self.config.enabled_plugins.remove(plugin_id);
  448. Ok(())
  449. }
  450. pub fn update(&mut self, plugin_id: &str) -> Result<UpdateOutcome, PluginError> {
  451. let mut registry = self.load_registry()?;
  452. let record = registry.plugins.get(plugin_id).cloned().ok_or_else(|| {
  453. PluginError::NotFound(format!("plugin `{plugin_id}` is not installed"))
  454. })?;
  455. let temp_root = self.install_root().join(".tmp");
  456. let staged_source = materialize_source(&record.source, &temp_root)?;
  457. let cleanup_source = matches!(record.source, PluginInstallSource::GitUrl { .. });
  458. let manifest = load_validated_manifest_from_root(&staged_source)?;
  459. if record.install_path.exists() {
  460. fs::remove_dir_all(&record.install_path)?;
  461. }
  462. copy_dir_all(&staged_source, &record.install_path)?;
  463. if cleanup_source {
  464. let _ = fs::remove_dir_all(&staged_source);
  465. }
  466. let updated_record = InstalledPluginRecord {
  467. version: manifest.version.clone(),
  468. description: manifest.description,
  469. updated_at_unix_ms: unix_time_ms(),
  470. ..record.clone()
  471. };
  472. registry
  473. .plugins
  474. .insert(plugin_id.to_string(), updated_record);
  475. self.store_registry(&registry)?;
  476. Ok(UpdateOutcome {
  477. plugin_id: plugin_id.to_string(),
  478. old_version: record.version,
  479. new_version: manifest.version,
  480. install_path: record.install_path,
  481. })
  482. }
  483. fn discover_bundled_plugins(&self) -> Result<Vec<PluginDefinition>, PluginError> {
  484. discover_plugin_dirs(
  485. &self
  486. .config
  487. .bundled_root
  488. .clone()
  489. .unwrap_or_else(Self::bundled_root),
  490. )?
  491. .into_iter()
  492. .map(|root| {
  493. load_plugin_definition(
  494. &root,
  495. PluginKind::Bundled,
  496. format!("{BUNDLED_MARKETPLACE}:{}", root.display()),
  497. BUNDLED_MARKETPLACE,
  498. )
  499. })
  500. .collect()
  501. }
  502. fn discover_external_plugins(&self) -> Result<Vec<PluginDefinition>, PluginError> {
  503. let registry = self.load_registry()?;
  504. let mut plugins = registry
  505. .plugins
  506. .values()
  507. .map(|record| {
  508. load_plugin_definition(
  509. &record.install_path,
  510. PluginKind::External,
  511. describe_install_source(&record.source),
  512. EXTERNAL_MARKETPLACE,
  513. )
  514. })
  515. .collect::<Result<Vec<_>, _>>()?;
  516. for directory in &self.config.external_dirs {
  517. for root in discover_plugin_dirs(directory)? {
  518. let plugin = load_plugin_definition(
  519. &root,
  520. PluginKind::External,
  521. root.display().to_string(),
  522. EXTERNAL_MARKETPLACE,
  523. )?;
  524. if plugins
  525. .iter()
  526. .all(|existing| existing.metadata().id != plugin.metadata().id)
  527. {
  528. plugins.push(plugin);
  529. }
  530. }
  531. }
  532. Ok(plugins)
  533. }
  534. fn is_enabled(&self, metadata: &PluginMetadata) -> bool {
  535. self.config
  536. .enabled_plugins
  537. .get(&metadata.id)
  538. .copied()
  539. .unwrap_or(match metadata.kind {
  540. PluginKind::External => false,
  541. PluginKind::Builtin | PluginKind::Bundled => metadata.default_enabled,
  542. })
  543. }
  544. fn ensure_known_plugin(&self, plugin_id: &str) -> Result<(), PluginError> {
  545. if self.plugin_registry()?.contains(plugin_id) {
  546. Ok(())
  547. } else {
  548. Err(PluginError::NotFound(format!(
  549. "plugin `{plugin_id}` is not installed or discoverable"
  550. )))
  551. }
  552. }
  553. fn load_registry(&self) -> Result<InstalledPluginRegistry, PluginError> {
  554. let path = self.registry_path();
  555. match fs::read_to_string(&path) {
  556. Ok(contents) => Ok(serde_json::from_str(&contents)?),
  557. Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
  558. Ok(InstalledPluginRegistry::default())
  559. }
  560. Err(error) => Err(PluginError::Io(error)),
  561. }
  562. }
  563. fn store_registry(&self, registry: &InstalledPluginRegistry) -> Result<(), PluginError> {
  564. let path = self.registry_path();
  565. if let Some(parent) = path.parent() {
  566. fs::create_dir_all(parent)?;
  567. }
  568. fs::write(path, serde_json::to_string_pretty(registry)?)?;
  569. Ok(())
  570. }
  571. fn write_enabled_state(
  572. &self,
  573. plugin_id: &str,
  574. enabled: Option<bool>,
  575. ) -> Result<(), PluginError> {
  576. update_settings_json(&self.settings_path(), |root| {
  577. let enabled_plugins = ensure_object(root, "enabledPlugins");
  578. match enabled {
  579. Some(value) => {
  580. enabled_plugins.insert(plugin_id.to_string(), Value::Bool(value));
  581. }
  582. None => {
  583. enabled_plugins.remove(plugin_id);
  584. }
  585. }
  586. })
  587. }
  588. }
  589. #[must_use]
  590. pub fn builtin_plugins() -> Vec<PluginDefinition> {
  591. vec![PluginDefinition::Builtin(BuiltinPlugin {
  592. metadata: PluginMetadata {
  593. id: plugin_id("example-builtin", BUILTIN_MARKETPLACE),
  594. name: "example-builtin".to_string(),
  595. version: "0.1.0".to_string(),
  596. description: "Example built-in plugin scaffold for the Rust plugin system".to_string(),
  597. kind: PluginKind::Builtin,
  598. source: BUILTIN_MARKETPLACE.to_string(),
  599. default_enabled: false,
  600. root: None,
  601. },
  602. hooks: PluginHooks::default(),
  603. })]
  604. }
  605. fn load_plugin_definition(
  606. root: &Path,
  607. kind: PluginKind,
  608. source: String,
  609. marketplace: &str,
  610. ) -> Result<PluginDefinition, PluginError> {
  611. let manifest = load_validated_manifest_from_root(root)?;
  612. let metadata = PluginMetadata {
  613. id: plugin_id(&manifest.name, marketplace),
  614. name: manifest.name,
  615. version: manifest.version,
  616. description: manifest.description,
  617. kind,
  618. source,
  619. default_enabled: manifest.default_enabled,
  620. root: Some(root.to_path_buf()),
  621. };
  622. let hooks = resolve_hooks(root, &manifest.hooks);
  623. Ok(match kind {
  624. PluginKind::Builtin => PluginDefinition::Builtin(BuiltinPlugin { metadata, hooks }),
  625. PluginKind::Bundled => PluginDefinition::Bundled(BundledPlugin { metadata, hooks }),
  626. PluginKind::External => PluginDefinition::External(ExternalPlugin { metadata, hooks }),
  627. })
  628. }
  629. fn load_validated_manifest_from_root(root: &Path) -> Result<PluginManifest, PluginError> {
  630. let manifest = load_manifest_from_root(root)?;
  631. validate_manifest(&manifest)?;
  632. validate_hook_paths(Some(root), &manifest.hooks)?;
  633. Ok(manifest)
  634. }
  635. fn validate_manifest(manifest: &PluginManifest) -> Result<(), PluginError> {
  636. if manifest.name.trim().is_empty() {
  637. return Err(PluginError::InvalidManifest(
  638. "plugin manifest name cannot be empty".to_string(),
  639. ));
  640. }
  641. if manifest.version.trim().is_empty() {
  642. return Err(PluginError::InvalidManifest(
  643. "plugin manifest version cannot be empty".to_string(),
  644. ));
  645. }
  646. if manifest.description.trim().is_empty() {
  647. return Err(PluginError::InvalidManifest(
  648. "plugin manifest description cannot be empty".to_string(),
  649. ));
  650. }
  651. Ok(())
  652. }
  653. fn load_manifest_from_root(root: &Path) -> Result<PluginManifest, PluginError> {
  654. let manifest_path = root.join(MANIFEST_RELATIVE_PATH);
  655. let contents = fs::read_to_string(&manifest_path).map_err(|error| {
  656. PluginError::NotFound(format!(
  657. "plugin manifest not found at {}: {error}",
  658. manifest_path.display()
  659. ))
  660. })?;
  661. Ok(serde_json::from_str(&contents)?)
  662. }
  663. fn resolve_hooks(root: &Path, hooks: &PluginHooks) -> PluginHooks {
  664. PluginHooks {
  665. pre_tool_use: hooks
  666. .pre_tool_use
  667. .iter()
  668. .map(|entry| resolve_hook_entry(root, entry))
  669. .collect(),
  670. post_tool_use: hooks
  671. .post_tool_use
  672. .iter()
  673. .map(|entry| resolve_hook_entry(root, entry))
  674. .collect(),
  675. }
  676. }
  677. fn validate_hook_paths(root: Option<&Path>, hooks: &PluginHooks) -> Result<(), PluginError> {
  678. let Some(root) = root else {
  679. return Ok(());
  680. };
  681. for entry in hooks.pre_tool_use.iter().chain(hooks.post_tool_use.iter()) {
  682. if is_literal_command(entry) {
  683. continue;
  684. }
  685. let path = if Path::new(entry).is_absolute() {
  686. PathBuf::from(entry)
  687. } else {
  688. root.join(entry)
  689. };
  690. if !path.exists() {
  691. return Err(PluginError::InvalidManifest(format!(
  692. "hook path `{}` does not exist",
  693. path.display()
  694. )));
  695. }
  696. }
  697. Ok(())
  698. }
  699. fn resolve_hook_entry(root: &Path, entry: &str) -> String {
  700. if is_literal_command(entry) {
  701. entry.to_string()
  702. } else {
  703. root.join(entry).display().to_string()
  704. }
  705. }
  706. fn is_literal_command(entry: &str) -> bool {
  707. !entry.starts_with("./") && !entry.starts_with("../")
  708. }
  709. fn resolve_local_source(source: &str) -> Result<PathBuf, PluginError> {
  710. let path = PathBuf::from(source);
  711. if path.exists() {
  712. Ok(path)
  713. } else {
  714. Err(PluginError::NotFound(format!(
  715. "plugin source `{source}` was not found"
  716. )))
  717. }
  718. }
  719. fn parse_install_source(source: &str) -> Result<PluginInstallSource, PluginError> {
  720. if source.starts_with("http://")
  721. || source.starts_with("https://")
  722. || source.starts_with("git@")
  723. || Path::new(source)
  724. .extension()
  725. .is_some_and(|extension| extension.eq_ignore_ascii_case("git"))
  726. {
  727. Ok(PluginInstallSource::GitUrl {
  728. url: source.to_string(),
  729. })
  730. } else {
  731. Ok(PluginInstallSource::LocalPath {
  732. path: resolve_local_source(source)?,
  733. })
  734. }
  735. }
  736. fn materialize_source(
  737. source: &PluginInstallSource,
  738. temp_root: &Path,
  739. ) -> Result<PathBuf, PluginError> {
  740. fs::create_dir_all(temp_root)?;
  741. match source {
  742. PluginInstallSource::LocalPath { path } => Ok(path.clone()),
  743. PluginInstallSource::GitUrl { url } => {
  744. let destination = temp_root.join(format!("plugin-{}", unix_time_ms()));
  745. let output = Command::new("git")
  746. .arg("clone")
  747. .arg("--depth")
  748. .arg("1")
  749. .arg(url)
  750. .arg(&destination)
  751. .output()?;
  752. if !output.status.success() {
  753. return Err(PluginError::CommandFailed(format!(
  754. "git clone failed for `{url}`: {}",
  755. String::from_utf8_lossy(&output.stderr).trim()
  756. )));
  757. }
  758. Ok(destination)
  759. }
  760. }
  761. }
  762. fn discover_plugin_dirs(root: &Path) -> Result<Vec<PathBuf>, PluginError> {
  763. match fs::read_dir(root) {
  764. Ok(entries) => {
  765. let mut paths = Vec::new();
  766. for entry in entries {
  767. let path = entry?.path();
  768. if path.join(MANIFEST_RELATIVE_PATH).exists() {
  769. paths.push(path);
  770. }
  771. }
  772. Ok(paths)
  773. }
  774. Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
  775. Err(error) => Err(PluginError::Io(error)),
  776. }
  777. }
  778. fn plugin_id(name: &str, marketplace: &str) -> String {
  779. format!("{name}@{marketplace}")
  780. }
  781. fn sanitize_plugin_id(plugin_id: &str) -> String {
  782. plugin_id
  783. .chars()
  784. .map(|ch| match ch {
  785. '/' | '\\' | '@' | ':' => '-',
  786. other => other,
  787. })
  788. .collect()
  789. }
  790. fn describe_install_source(source: &PluginInstallSource) -> String {
  791. match source {
  792. PluginInstallSource::LocalPath { path } => path.display().to_string(),
  793. PluginInstallSource::GitUrl { url } => url.clone(),
  794. }
  795. }
  796. fn unix_time_ms() -> u128 {
  797. SystemTime::now()
  798. .duration_since(UNIX_EPOCH)
  799. .expect("time should be after epoch")
  800. .as_millis()
  801. }
  802. fn copy_dir_all(source: &Path, destination: &Path) -> Result<(), PluginError> {
  803. fs::create_dir_all(destination)?;
  804. for entry in fs::read_dir(source)? {
  805. let entry = entry?;
  806. let target = destination.join(entry.file_name());
  807. if entry.file_type()?.is_dir() {
  808. copy_dir_all(&entry.path(), &target)?;
  809. } else {
  810. fs::copy(entry.path(), target)?;
  811. }
  812. }
  813. Ok(())
  814. }
  815. fn update_settings_json(
  816. path: &Path,
  817. mut update: impl FnMut(&mut Map<String, Value>),
  818. ) -> Result<(), PluginError> {
  819. if let Some(parent) = path.parent() {
  820. fs::create_dir_all(parent)?;
  821. }
  822. let mut root = match fs::read_to_string(path) {
  823. Ok(contents) if !contents.trim().is_empty() => serde_json::from_str::<Value>(&contents)?,
  824. Ok(_) => Value::Object(Map::new()),
  825. Err(error) if error.kind() == std::io::ErrorKind::NotFound => Value::Object(Map::new()),
  826. Err(error) => return Err(PluginError::Io(error)),
  827. };
  828. let object = root.as_object_mut().ok_or_else(|| {
  829. PluginError::InvalidManifest(format!(
  830. "settings file {} must contain a JSON object",
  831. path.display()
  832. ))
  833. })?;
  834. update(object);
  835. fs::write(path, serde_json::to_string_pretty(&root)?)?;
  836. Ok(())
  837. }
  838. fn ensure_object<'a>(root: &'a mut Map<String, Value>, key: &str) -> &'a mut Map<String, Value> {
  839. if !root.get(key).is_some_and(Value::is_object) {
  840. root.insert(key.to_string(), Value::Object(Map::new()));
  841. }
  842. root.get_mut(key)
  843. .and_then(Value::as_object_mut)
  844. .expect("object should exist")
  845. }
  846. #[cfg(test)]
  847. mod tests {
  848. use super::*;
  849. fn temp_dir(label: &str) -> PathBuf {
  850. std::env::temp_dir().join(format!("plugins-{label}-{}", unix_time_ms()))
  851. }
  852. fn write_external_plugin(root: &Path, name: &str, version: &str) {
  853. fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
  854. fs::create_dir_all(root.join("hooks")).expect("hooks dir");
  855. fs::write(
  856. root.join("hooks").join("pre.sh"),
  857. "#!/bin/sh\nprintf 'pre'\n",
  858. )
  859. .expect("write pre hook");
  860. fs::write(
  861. root.join("hooks").join("post.sh"),
  862. "#!/bin/sh\nprintf 'post'\n",
  863. )
  864. .expect("write post hook");
  865. fs::write(
  866. root.join(MANIFEST_RELATIVE_PATH),
  867. format!(
  868. "{{\n \"name\": \"{name}\",\n \"version\": \"{version}\",\n \"description\": \"test plugin\",\n \"hooks\": {{\n \"PreToolUse\": [\"./hooks/pre.sh\"],\n \"PostToolUse\": [\"./hooks/post.sh\"]\n }}\n}}"
  869. ),
  870. )
  871. .expect("write manifest");
  872. }
  873. fn write_broken_plugin(root: &Path, name: &str) {
  874. fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
  875. fs::write(
  876. root.join(MANIFEST_RELATIVE_PATH),
  877. format!(
  878. "{{\n \"name\": \"{name}\",\n \"version\": \"1.0.0\",\n \"description\": \"broken plugin\",\n \"hooks\": {{\n \"PreToolUse\": [\"./hooks/missing.sh\"]\n }}\n}}"
  879. ),
  880. )
  881. .expect("write broken manifest");
  882. }
  883. #[test]
  884. fn validates_manifest_shape() {
  885. let error = validate_manifest(&PluginManifest {
  886. name: String::new(),
  887. version: "1.0.0".to_string(),
  888. description: "desc".to_string(),
  889. default_enabled: false,
  890. hooks: PluginHooks::default(),
  891. })
  892. .expect_err("empty name should fail");
  893. assert!(error.to_string().contains("name cannot be empty"));
  894. }
  895. #[test]
  896. fn discovers_builtin_and_bundled_plugins() {
  897. let manager = PluginManager::new(PluginManagerConfig::new(temp_dir("discover")));
  898. let plugins = manager.list_plugins().expect("plugins should list");
  899. assert!(plugins
  900. .iter()
  901. .any(|plugin| plugin.metadata.kind == PluginKind::Builtin));
  902. assert!(plugins
  903. .iter()
  904. .any(|plugin| plugin.metadata.kind == PluginKind::Bundled));
  905. }
  906. #[test]
  907. fn installs_enables_updates_and_uninstalls_external_plugins() {
  908. let config_home = temp_dir("home");
  909. let source_root = temp_dir("source");
  910. write_external_plugin(&source_root, "demo", "1.0.0");
  911. let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
  912. let install = manager
  913. .install(source_root.to_str().expect("utf8 path"))
  914. .expect("install should succeed");
  915. assert_eq!(install.plugin_id, "demo@external");
  916. assert!(manager
  917. .list_plugins()
  918. .expect("list plugins")
  919. .iter()
  920. .any(|plugin| plugin.metadata.id == "demo@external" && plugin.enabled));
  921. let hooks = manager.aggregated_hooks().expect("hooks should aggregate");
  922. assert_eq!(hooks.pre_tool_use.len(), 1);
  923. assert!(hooks.pre_tool_use[0].contains("pre.sh"));
  924. manager
  925. .disable("demo@external")
  926. .expect("disable should work");
  927. assert!(manager
  928. .aggregated_hooks()
  929. .expect("hooks after disable")
  930. .is_empty());
  931. manager.enable("demo@external").expect("enable should work");
  932. write_external_plugin(&source_root, "demo", "2.0.0");
  933. let update = manager.update("demo@external").expect("update should work");
  934. assert_eq!(update.old_version, "1.0.0");
  935. assert_eq!(update.new_version, "2.0.0");
  936. manager
  937. .uninstall("demo@external")
  938. .expect("uninstall should work");
  939. assert!(!manager
  940. .list_plugins()
  941. .expect("list plugins")
  942. .iter()
  943. .any(|plugin| plugin.metadata.id == "demo@external"));
  944. let _ = fs::remove_dir_all(config_home);
  945. let _ = fs::remove_dir_all(source_root);
  946. }
  947. #[test]
  948. fn validates_plugin_source_before_install() {
  949. let config_home = temp_dir("validate-home");
  950. let source_root = temp_dir("validate-source");
  951. write_external_plugin(&source_root, "validator", "1.0.0");
  952. let manager = PluginManager::new(PluginManagerConfig::new(&config_home));
  953. let manifest = manager
  954. .validate_plugin_source(source_root.to_str().expect("utf8 path"))
  955. .expect("manifest should validate");
  956. assert_eq!(manifest.name, "validator");
  957. let _ = fs::remove_dir_all(config_home);
  958. let _ = fs::remove_dir_all(source_root);
  959. }
  960. #[test]
  961. fn plugin_registry_tracks_enabled_state_and_lookup() {
  962. let config_home = temp_dir("registry-home");
  963. let source_root = temp_dir("registry-source");
  964. write_external_plugin(&source_root, "registry-demo", "1.0.0");
  965. let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
  966. manager
  967. .install(source_root.to_str().expect("utf8 path"))
  968. .expect("install should succeed");
  969. manager
  970. .disable("registry-demo@external")
  971. .expect("disable should succeed");
  972. let registry = manager.plugin_registry().expect("registry should build");
  973. let plugin = registry
  974. .get("registry-demo@external")
  975. .expect("installed plugin should be discoverable");
  976. assert_eq!(plugin.metadata().name, "registry-demo");
  977. assert!(!plugin.is_enabled());
  978. assert!(registry.contains("registry-demo@external"));
  979. assert!(!registry.contains("missing@external"));
  980. let _ = fs::remove_dir_all(config_home);
  981. let _ = fs::remove_dir_all(source_root);
  982. }
  983. #[test]
  984. fn rejects_plugin_sources_with_missing_hook_paths() {
  985. let config_home = temp_dir("broken-home");
  986. let source_root = temp_dir("broken-source");
  987. write_broken_plugin(&source_root, "broken");
  988. let manager = PluginManager::new(PluginManagerConfig::new(&config_home));
  989. let error = manager
  990. .validate_plugin_source(source_root.to_str().expect("utf8 path"))
  991. .expect_err("missing hook file should fail validation");
  992. assert!(error.to_string().contains("does not exist"));
  993. let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
  994. let install_error = manager
  995. .install(source_root.to_str().expect("utf8 path"))
  996. .expect_err("install should reject invalid hook paths");
  997. assert!(install_error.to_string().contains("does not exist"));
  998. let _ = fs::remove_dir_all(config_home);
  999. let _ = fs::remove_dir_all(source_root);
  1000. }
  1001. }