1use serde::{Deserialize, Serialize};
2use crate::permissions::Permission;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct PluginManifest {
7 pub id: String,
8 pub name: String,
9 pub version: String,
10 pub description: String,
11 #[serde(default)]
12 pub long_description: Option<String>,
13 pub author: String,
14 #[serde(default)]
15 pub author_url: Option<String>,
16 #[serde(default)]
17 pub homepage: Option<String>,
18 #[serde(default)]
19 pub license: Option<String>,
20 #[serde(default)]
21 pub keywords: Vec<String>,
22 #[serde(default)]
23 pub icon: Option<String>,
24
25 pub compatibility: CompatibilitySpec,
26
27 pub capability_levels: Vec<CapabilityLevel>,
29
30 #[serde(default)]
32 pub integration: IntegrationConfig,
33
34 #[serde(default)]
36 pub entry: EntryConfig,
37
38 #[serde(default)]
40 pub dependencies: Vec<PluginDependency>,
41
42 #[serde(default)]
44 pub permissions: Vec<Permission>,
45
46 #[serde(default)]
49 pub host_capabilities: Vec<HostCapability>,
50
51 #[serde(default)]
53 pub settings_schema: Option<serde_json::Value>,
54
55 #[serde(default)]
57 pub commands: Vec<CommandDeclaration>,
58
59 #[serde(default)]
61 pub checksum: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CompatibilitySpec {
66 pub min_app_version: String,
67 #[serde(default)]
68 pub min_host_api_version: Option<String>,
69 #[serde(default)]
70 pub max_app_version: Option<String>,
71 #[serde(default = "all_platforms")]
72 pub platforms: Vec<String>,
73}
74
75fn all_platforms() -> Vec<String> {
76 vec!["windows".into(), "macos".into(), "linux".into()]
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(from = "u8", into = "u8")]
82pub enum CapabilityLevel {
83 Module = 0,
85 ModuleFeature = 1,
87 UiExtension = 2,
89 AiAssistant = 3,
91 Service = 4,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum HostCapability {
99 Navigation,
100 AppState,
101 FileIntents,
102 FileDialogs,
103 #[serde(rename = "aichat")]
104 AiChat,
105 EnterpriseGateway,
106 DeepLinks,
107 ThemeRead,
108 EventSubscribe,
109}
110
111impl HostCapability {
112 pub fn as_str(&self) -> &'static str {
113 match self {
114 Self::Navigation => "navigation",
115 Self::AppState => "app_state",
116 Self::FileIntents => "file_intents",
117 Self::FileDialogs => "file_dialogs",
118 Self::AiChat => "aichat",
119 Self::EnterpriseGateway => "enterprise_gateway",
120 Self::DeepLinks => "deep_links",
121 Self::ThemeRead => "theme_read",
122 Self::EventSubscribe => "event_subscribe",
123 }
124 }
125}
126
127impl From<u8> for CapabilityLevel {
128 fn from(v: u8) -> Self {
129 match v {
130 0 => Self::Module,
131 1 => Self::ModuleFeature,
132 2 => Self::UiExtension,
133 3 => Self::AiAssistant,
134 4 => Self::Service,
135 _ => Self::Service,
136 }
137 }
138}
139
140impl From<CapabilityLevel> for u8 {
141 fn from(l: CapabilityLevel) -> u8 {
142 l as u8
143 }
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148pub struct IntegrationConfig {
149 #[serde(default)]
150 pub level0: Option<Level0Config>,
151 #[serde(default)]
152 pub level1: Option<Level1Config>,
153 #[serde(default)]
154 pub level2: Option<Level2Config>,
155 #[serde(default)]
156 pub level3: Option<Level3Config>,
157 #[serde(default)]
158 pub level4: Option<Level4Config>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Level0Config {
164 pub module_id: String,
166 pub module_label: String,
167 pub module_icon: String,
169 #[serde(default = "default_sidebar_position")]
171 pub sidebar_position: String,
172 #[serde(default = "default_sidebar_order")]
174 pub sidebar_order: u32,
175 pub panel_entry: String,
177}
178
179fn default_sidebar_position() -> String { "main".into() }
180fn default_sidebar_order() -> u32 { 100 }
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct Level1Config {
185 pub parent_module: String,
187 pub tab_id: String,
189 pub tab_label: String,
190 pub tab_icon: String,
192 #[serde(default)]
194 pub tab_position: Option<String>,
195 pub panel_entry: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct Level2Config {
202 pub slots: Vec<String>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Level3Config {
209 pub assistant_id: String,
210 pub assistant_name: String,
211 #[serde(default)]
212 pub assistant_icon: Option<String>,
213 #[serde(default)]
214 pub assistant_description: Option<String>,
215 pub system_prompt_file: String,
217 #[serde(default)]
219 pub preferred_model: Option<String>,
220}
221
222#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224pub struct Level4Config {
225 #[serde(default)]
227 pub workflow_step_types: Vec<String>,
228}
229
230#[derive(Debug, Clone, Default, Serialize, Deserialize)]
232pub struct EntryConfig {
233 #[serde(default)]
234 pub native: Option<NativeEntry>,
235 #[serde(default)]
236 pub frontend: Option<String>,
237 #[serde(default)]
238 pub frontend_styles: Option<String>,
239}
240
241#[derive(Debug, Clone, Default, Serialize, Deserialize)]
242pub struct NativeEntry {
243 #[serde(default)]
244 pub macos_arm64: Option<String>,
245 #[serde(default)]
246 pub macos_x64: Option<String>,
247 #[serde(default)]
248 pub windows_x64: Option<String>,
249 #[serde(default)]
250 pub windows_arm64: Option<String>,
251 #[serde(default)]
252 pub linux_x64: Option<String>,
253 #[serde(default)]
254 pub linux_arm64: Option<String>,
255}
256
257impl NativeEntry {
258 pub fn for_current_platform(&self) -> Option<&str> {
260 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
261 return self.macos_arm64.as_deref();
262
263 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
264 return self.macos_x64.as_deref();
265
266 #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
267 return self.windows_x64.as_deref();
268
269 #[cfg(all(target_os = "windows", target_arch = "aarch64"))]
270 return self.windows_arm64.as_deref();
271
272 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
273 return self.linux_x64.as_deref();
274
275 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
276 return self.linux_arm64.as_deref();
277
278 #[allow(unreachable_code)]
279 None
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct PluginDependency {
285 pub id: String,
286 pub version: String,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct CommandDeclaration {
292 pub id: String,
293 #[serde(default)]
294 pub description: Option<String>,
295}
296
297#[cfg(test)]
298mod tests {
299 use super::{HostCapability, PluginManifest};
300
301 #[test]
302 fn manifest_supports_public_host_api_fields() {
303 let manifest: PluginManifest = serde_json::from_value(serde_json::json!({
304 "id": "dev.haloforge.example",
305 "name": "Example",
306 "version": "0.1.0",
307 "description": "Example plugin",
308 "author": "HaloForge Team",
309 "compatibility": {
310 "min_app_version": "0.1.0",
311 "min_host_api_version": "0.1.0",
312 "platforms": ["windows"]
313 },
314 "capability_levels": [2],
315 "host_capabilities": ["navigation", "aichat"],
316 "integration": {
317 "level2": { "slots": ["devkit.toolbar"] }
318 }
319 }))
320 .expect("manifest should deserialize");
321
322 assert_eq!(
323 manifest.compatibility.min_host_api_version.as_deref(),
324 Some("0.1.0")
325 );
326 assert_eq!(
327 manifest.host_capabilities,
328 vec![HostCapability::Navigation, HostCapability::AiChat]
329 );
330 }
331
332 #[test]
333 fn host_capability_names_are_stable() {
334 assert_eq!(HostCapability::FileIntents.as_str(), "file_intents");
335 assert_eq!(HostCapability::FileDialogs.as_str(), "file_dialogs");
336 assert_eq!(HostCapability::DeepLinks.as_str(), "deep_links");
337 assert_eq!(HostCapability::ThemeRead.as_str(), "theme_read");
338 }
339}