systemprompt_models/bridge/
plugin_bundle.rs1use serde::{Deserialize, Serialize};
23
24use crate::plugin::PluginDependency;
25
26pub const PLUGIN_MANIFEST_RELPATH: &str = ".claude-plugin/plugin.json";
27
28pub const PLUGIN_MANIFEST_DIRS: &[&str] = &[".claude-plugin", "claude-plugin"];
29
30pub const PLUGIN_MANIFEST_FILE: &str = "plugin.json";
31
32pub const NODE_PACKAGE_FILE: &str = "package.json";
33
34pub const NODE_LOCKFILES: [&str; 4] = [
38 "bun.lock",
39 "bun.lockb",
40 "npm-shrinkwrap.json",
41 "package-lock.json",
42];
43
44#[must_use]
45pub fn node_lockfile(plugin_dir: &std::path::Path) -> Option<&'static str> {
46 NODE_LOCKFILES
47 .iter()
48 .copied()
49 .find(|name| plugin_dir.join(name).is_file())
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53pub struct PluginManifest {
54 pub name: String,
55 #[serde(default)]
56 pub description: String,
57 #[serde(default)]
58 pub version: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub author: Option<ManifestAuthor>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub hooks: Option<String>,
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
64 pub keywords: Vec<String>,
65 #[serde(
66 default,
67 rename = "installationPreference",
68 skip_serializing_if = "Option::is_none"
69 )]
70 pub installation_preference: Option<String>,
71
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub skills: Option<serde_json::Value>,
75
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub agents: Option<serde_json::Value>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub commands: Option<serde_json::Value>,
83
84 #[serde(
85 default,
86 rename = "mcpServers",
87 skip_serializing_if = "Option::is_none"
88 )]
89 pub mcp_servers: Option<serde_json::Value>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub homepage: Option<String>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub repository: Option<String>,
97
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub license: Option<String>,
100
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub category: Option<String>,
103
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub dependencies: Vec<ManifestDependency>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(untagged)]
113pub enum ManifestDependency {
114 Name(String),
115 Detailed {
116 name: String,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 version: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 marketplace: Option<String>,
121 },
122}
123
124impl ManifestDependency {
125 #[must_use]
126 pub fn name(&self) -> &str {
127 match self {
128 Self::Name(name) | Self::Detailed { name, .. } => name,
129 }
130 }
131
132 #[must_use]
133 pub fn marketplace(&self) -> Option<&str> {
134 match self {
135 Self::Name(_) => None,
136 Self::Detailed { marketplace, .. } => marketplace.as_deref(),
137 }
138 }
139
140 #[must_use]
141 pub fn version(&self) -> Option<&str> {
142 match self {
143 Self::Name(_) => None,
144 Self::Detailed { version, .. } => version.as_deref(),
145 }
146 }
147}
148
149impl From<&PluginDependency> for ManifestDependency {
150 fn from(dependency: &PluginDependency) -> Self {
151 if dependency.marketplace.is_none() && dependency.version.is_none() {
152 Self::Name(dependency.name.clone())
153 } else {
154 Self::Detailed {
155 name: dependency.name.clone(),
156 version: dependency.version.clone(),
157 marketplace: dependency.marketplace.clone(),
158 }
159 }
160 }
161}
162
163impl From<&ManifestDependency> for PluginDependency {
164 fn from(dependency: &ManifestDependency) -> Self {
165 Self {
166 name: dependency.name().to_owned(),
167 marketplace: dependency.marketplace().map(str::to_owned),
168 version: dependency.version().map(str::to_owned),
169 }
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ManifestAuthor {
175 pub name: String,
176 #[serde(default, skip_serializing_if = "String::is_empty")]
177 pub email: String,
178}
179
180pub fn bundle_has_manifest<S: AsRef<str>>(paths: impl IntoIterator<Item = S>) -> bool {
181 paths
182 .into_iter()
183 .any(|path| path.as_ref() == PLUGIN_MANIFEST_RELPATH)
184}