use serde::{Deserialize, Serialize};
use crate::permissions::Permission;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginManifest {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
#[serde(default)]
pub long_description: Option<String>,
pub author: String,
#[serde(default)]
pub author_url: Option<String>,
#[serde(default)]
pub homepage: Option<String>,
#[serde(default)]
pub license: Option<String>,
#[serde(default)]
pub keywords: Vec<String>,
#[serde(default)]
pub icon: Option<String>,
pub compatibility: CompatibilitySpec,
pub capability_levels: Vec<CapabilityLevel>,
#[serde(default)]
pub integration: IntegrationConfig,
#[serde(default)]
pub entry: EntryConfig,
#[serde(default)]
pub dependencies: Vec<PluginDependency>,
#[serde(default)]
pub permissions: Vec<Permission>,
#[serde(default)]
pub host_capabilities: Vec<HostCapability>,
#[serde(default)]
pub settings_schema: Option<serde_json::Value>,
#[serde(default)]
pub commands: Vec<CommandDeclaration>,
#[serde(default)]
pub checksum: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilitySpec {
pub min_app_version: String,
#[serde(default)]
pub min_host_api_version: Option<String>,
#[serde(default)]
pub max_app_version: Option<String>,
#[serde(default = "all_platforms")]
pub platforms: Vec<String>,
}
fn all_platforms() -> Vec<String> {
vec!["windows".into(), "macos".into(), "linux".into()]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "u8", into = "u8")]
pub enum CapabilityLevel {
Module = 0,
ModuleFeature = 1,
UiExtension = 2,
AiAssistant = 3,
Service = 4,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostCapability {
Navigation,
AppState,
FileIntents,
#[serde(rename = "aichat")]
AiChat,
ThemeRead,
EventSubscribe,
}
impl HostCapability {
pub fn as_str(&self) -> &'static str {
match self {
Self::Navigation => "navigation",
Self::AppState => "app_state",
Self::FileIntents => "file_intents",
Self::AiChat => "aichat",
Self::ThemeRead => "theme_read",
Self::EventSubscribe => "event_subscribe",
}
}
}
impl From<u8> for CapabilityLevel {
fn from(v: u8) -> Self {
match v {
0 => Self::Module,
1 => Self::ModuleFeature,
2 => Self::UiExtension,
3 => Self::AiAssistant,
4 => Self::Service,
_ => Self::Service,
}
}
}
impl From<CapabilityLevel> for u8 {
fn from(l: CapabilityLevel) -> u8 {
l as u8
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IntegrationConfig {
#[serde(default)]
pub level0: Option<Level0Config>,
#[serde(default)]
pub level1: Option<Level1Config>,
#[serde(default)]
pub level2: Option<Level2Config>,
#[serde(default)]
pub level3: Option<Level3Config>,
#[serde(default)]
pub level4: Option<Level4Config>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level0Config {
pub module_id: String,
pub module_label: String,
pub module_icon: String,
#[serde(default = "default_sidebar_position")]
pub sidebar_position: String,
#[serde(default = "default_sidebar_order")]
pub sidebar_order: u32,
pub panel_entry: String,
}
fn default_sidebar_position() -> String { "main".into() }
fn default_sidebar_order() -> u32 { 100 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level1Config {
pub parent_module: String,
pub tab_id: String,
pub tab_label: String,
pub tab_icon: String,
#[serde(default)]
pub tab_position: Option<String>,
pub panel_entry: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level2Config {
pub slots: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level3Config {
pub assistant_id: String,
pub assistant_name: String,
#[serde(default)]
pub assistant_icon: Option<String>,
#[serde(default)]
pub assistant_description: Option<String>,
pub system_prompt_file: String,
#[serde(default)]
pub preferred_model: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Level4Config {
#[serde(default)]
pub workflow_step_types: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EntryConfig {
#[serde(default)]
pub native: Option<NativeEntry>,
#[serde(default)]
pub frontend: Option<String>,
#[serde(default)]
pub frontend_styles: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NativeEntry {
#[serde(default)]
pub macos_arm64: Option<String>,
#[serde(default)]
pub macos_x64: Option<String>,
#[serde(default)]
pub windows_x64: Option<String>,
#[serde(default)]
pub windows_arm64: Option<String>,
#[serde(default)]
pub linux_x64: Option<String>,
#[serde(default)]
pub linux_arm64: Option<String>,
}
impl NativeEntry {
pub fn for_current_platform(&self) -> Option<&str> {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
return self.macos_arm64.as_deref();
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
return self.macos_x64.as_deref();
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
return self.windows_x64.as_deref();
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
return self.windows_arm64.as_deref();
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
return self.linux_x64.as_deref();
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
return self.linux_arm64.as_deref();
#[allow(unreachable_code)]
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginDependency {
pub id: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandDeclaration {
pub id: String,
#[serde(default)]
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::{HostCapability, PluginManifest};
#[test]
fn manifest_supports_public_host_api_fields() {
let manifest: PluginManifest = serde_json::from_value(serde_json::json!({
"id": "dev.haloforge.example",
"name": "Example",
"version": "0.1.0",
"description": "Example plugin",
"author": "HaloForge Team",
"compatibility": {
"min_app_version": "0.1.0",
"min_host_api_version": "0.1.0",
"platforms": ["windows"]
},
"capability_levels": [2],
"host_capabilities": ["navigation", "aichat"],
"integration": {
"level2": { "slots": ["devkit.toolbar"] }
}
}))
.expect("manifest should deserialize");
assert_eq!(
manifest.compatibility.min_host_api_version.as_deref(),
Some("0.1.0")
);
assert_eq!(
manifest.host_capabilities,
vec![HostCapability::Navigation, HostCapability::AiChat]
);
}
#[test]
fn host_capability_names_are_stable() {
assert_eq!(HostCapability::FileIntents.as_str(), "file_intents");
assert_eq!(HostCapability::ThemeRead.as_str(), "theme_read");
}
}