use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::{KernelError, Result};
use crate::module::{ModuleKind, ModuleMetadata, ModuleState};
use crate::registry::StateRegistry;
use crate::Kernel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleManifest {
pub id: String,
pub name: String,
pub version: String,
pub kind: ModuleKind,
pub description: Option<String>,
pub spec_kind: Option<String>,
pub binary: Option<String>,
pub skill: Option<String>,
pub mcp: Option<String>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub operations: Vec<String>,
#[serde(default)]
pub capabilities: Vec<String>,
}
impl ModuleManifest {
pub fn from_json(raw: &str) -> Result<Self> {
Ok(serde_json::from_str(raw)?)
}
pub fn load(path: &Path) -> Result<Self> {
let manifest_path = if path.is_dir() {
path.join("module.json")
} else {
path.to_path_buf()
};
let raw = std::fs::read_to_string(&manifest_path).map_err(|e| {
KernelError::Other(anyhow::anyhow!(
"failed to read manifest {}: {e}",
manifest_path.display()
))
})?;
Self::from_json(&raw)
}
pub fn metadata(&self) -> ModuleMetadata {
ModuleMetadata {
id: self.id.clone(),
name: self.name.clone(),
version: self.version.clone(),
kind: self.kind,
description: self.description.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct ResolvedManifest {
pub manifest: ModuleManifest,
pub binary_path: Option<PathBuf>,
pub skill_path: Option<PathBuf>,
pub mcp_path: Option<PathBuf>,
}
impl ResolvedManifest {
pub fn resolve(manifest: ModuleManifest, base_dir: &Path) -> Self {
let join = |relative: &Option<String>| relative.as_deref().map(|s| base_dir.join(s));
Self {
binary_path: join(&manifest.binary),
skill_path: join(&manifest.skill),
mcp_path: join(&manifest.mcp),
manifest,
}
}
}
impl Kernel {
pub async fn register_module_from_manifest(&self, path: &Path) -> Result<ResolvedManifest> {
let manifest = ModuleManifest::load(path)?;
let base_dir = if path.is_dir() {
path.to_path_buf()
} else {
path.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf()
};
let resolved = ResolvedManifest::resolve(manifest, &base_dir);
let metadata = resolved.manifest.metadata();
self.registry()
.upsert_module(&metadata, ModuleState::Loaded)
.await?;
tracing::info!(
id = %metadata.id,
kind = ?metadata.kind,
"registered module from manifest"
);
Ok(resolved)
}
}
pub async fn register_in_registry(
registry: &StateRegistry,
manifest: &ModuleManifest,
) -> Result<()> {
registry
.upsert_module(&manifest.metadata(), ModuleState::Loaded)
.await
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
{
"id": "petstore",
"name": "Pet Store",
"version": "1.0.0",
"kind": "native",
"description": "Generated client for the Pet Store API.",
"spec_kind": "openapi",
"binary": "petstore-cli",
"skill": "SKILL.md",
"mcp": "mcp.json",
"base_url": "https://petstore.example.com/v1",
"operations": ["list_pets", "create_pet", "get_pet"]
}"#;
#[test]
fn parses_sample_manifest() {
let m = ModuleManifest::from_json(SAMPLE).unwrap();
assert_eq!(m.id, "petstore");
assert_eq!(m.kind, ModuleKind::Native);
assert_eq!(m.operations.len(), 3);
}
#[tokio::test]
async fn registers_manifest_in_registry() {
let kernel = Kernel::in_memory().await.unwrap();
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("module.json");
std::fs::write(&manifest_path, SAMPLE).unwrap();
let resolved = kernel
.register_module_from_manifest(&manifest_path)
.await
.unwrap();
assert_eq!(resolved.manifest.id, "petstore");
assert_eq!(
resolved.binary_path.as_ref().unwrap(),
&dir.path().join("petstore-cli")
);
let rec = kernel
.registry()
.get_module("petstore")
.await
.unwrap()
.expect("record");
assert_eq!(rec.state, ModuleState::Loaded);
assert_eq!(rec.version, "1.0.0");
}
}