use std::collections::BTreeMap;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::lockfile::Integrity;
use crate::manifest::PackageType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillFile {
pub relative_path: String,
pub contents: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Skill {
pub owner: String,
pub name: String,
pub version: String,
pub files: Vec<SkillFile>,
pub integrity: Integrity,
}
impl Skill {
#[must_use]
pub fn id(&self) -> String {
format!("{}/{}", self.owner, self.name)
}
#[must_use]
pub fn lockfile_key(&self) -> String {
format!(
"{}/{}/{}@{}",
PackageType::Skills.as_str(),
self.owner,
self.name,
self.version
)
}
#[must_use]
pub fn computed_integrity(&self) -> Integrity {
compute_integrity(&self.files)
}
#[must_use]
pub fn integrity_matches(&self) -> bool {
self.computed_integrity() == self.integrity
}
}
#[must_use]
pub fn compute_integrity(files: &[SkillFile]) -> Integrity {
let mut sorted: Vec<&SkillFile> = files.iter().collect();
sorted.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
let mut hasher = Sha256::new();
for f in sorted {
hasher.update(f.relative_path.as_bytes());
hasher.update([0u8]);
hasher.update((f.contents.len() as u64).to_le_bytes());
hasher.update(&f.contents);
}
let digest = hasher.finalize();
let b64 = BASE64_STANDARD.encode(digest);
Integrity::parse(format!("sha256-{b64}"))
.expect("base64 of sha256 always matches integrity regex")
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpTransport {
Stdio {
command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
args: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
env: BTreeMap<String, String>,
},
Http {
url: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
headers: BTreeMap<String, String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpServer {
pub id: String,
pub version: String,
pub transport: McpTransport,
}
impl McpServer {
#[must_use]
pub fn lockfile_key(&self) -> String {
format!("{}/{}@{}", PackageType::Mcp.as_str(), self.id, self.version)
}
#[must_use]
pub fn computed_integrity(&self) -> Integrity {
let bytes = serde_json::to_vec(&self.transport)
.expect("McpTransport with String keys serializes infallibly");
let mut hasher = Sha256::new();
hasher.update(self.id.as_bytes());
hasher.update([0u8]);
hasher.update(self.version.as_bytes());
hasher.update([0u8]);
hasher.update(&bytes);
let digest = hasher.finalize();
let b64 = BASE64_STANDARD.encode(digest);
Integrity::parse(format!("sha256-{b64}"))
.expect("base64 of sha256 always matches integrity regex")
}
#[must_use]
pub fn short_name(&self) -> String {
self.id
.rsplit('/')
.next()
.unwrap_or(&self.id)
.to_lowercase()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subagent {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prompt {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hook {
pub id: String,
}