use std::collections::BTreeMap;
use std::path::PathBuf;
use semver::Version;
use serde::Serialize;
use nexo_plugin_manifest::PluginManifest;
use super::capability_aggregator::{AggregatedGate, PluginCapabilityAggregation, UnmetRequirement};
use super::contributes::{AgentMergeConflict, AgentMergeReport};
use super::contributes_skills::{SkillConflict, SkillsMergeReport};
use super::init_loop::InitOutcome;
#[derive(Clone, Debug, Serialize)]
pub struct DiscoveredPlugin {
pub manifest: PluginManifest,
pub root_dir: PathBuf,
pub manifest_path: PathBuf,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct PluginDiscoveryReport {
pub loaded_ids: Vec<String>,
pub diagnostics: Vec<DiscoveryDiagnostic>,
pub scanned: usize,
pub invalid: usize,
pub disabled: usize,
pub duplicates: usize,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub contributed_agents_per_plugin: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub agent_merge_conflicts: Vec<AgentMergeConflict>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub init_outcomes: BTreeMap<String, InitOutcome>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub contributed_skills_per_plugin: BTreeMap<String, Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skill_conflicts: Vec<SkillConflict>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub plugin_capability_gates: BTreeMap<String, AggregatedGate>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unmet_required_capabilities: Vec<UnmetRequirement>,
}
impl PluginDiscoveryReport {
pub fn fold_agent_merge(&mut self, m: AgentMergeReport) {
self.diagnostics.extend(m.diagnostics);
self.contributed_agents_per_plugin = m.contributed_agents_per_plugin;
self.agent_merge_conflicts.extend(m.conflicts);
}
pub fn fold_init_outcomes(&mut self, outcomes: BTreeMap<String, InitOutcome>) {
self.init_outcomes = outcomes;
}
pub fn fold_skill_merge(&mut self, m: SkillsMergeReport) {
self.diagnostics.extend(m.diagnostics);
self.contributed_skills_per_plugin = m.contributed_per_plugin;
self.skill_conflicts.extend(m.conflicts);
}
pub fn fold_capability_aggregation(&mut self, agg: PluginCapabilityAggregation) {
self.diagnostics.extend(agg.conflicts);
self.plugin_capability_gates = agg.gates;
self.unmet_required_capabilities.extend(agg.unmet_required);
}
}
#[derive(Clone, Debug, Serialize)]
pub struct DiscoveryDiagnostic {
pub level: DiagnosticLevel,
pub path: PathBuf,
pub kind: DiscoveryDiagnosticKind,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticLevel {
Warn,
Error,
}
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum DiscoveryDiagnosticKind {
SearchPathMissing {
reason: String,
},
ManifestParseError {
error: String,
},
ValidationFailed {
errors: Vec<String>,
},
SymlinkEscape {
target: PathBuf,
},
PermissionDenied,
DuplicateId {
id: String,
kept_path: PathBuf,
},
VersionMismatch {
id: String,
required: String,
current: Version,
},
Disabled {
id: String,
},
AllowlistRejected {
id: String,
},
UnresolvedEnvVar {
var_name: String,
in_path: PathBuf,
},
ChannelKindAlreadyRegistered {
channel_kind: String,
prior_registered_by: String,
attempted_by: String,
},
CapabilityGateConflictsCore {
env_var: String,
plugin_id: String,
core_extension: String,
},
CapabilityGateConflictsPlugin {
env_var: String,
plugin_a: String,
plugin_b: String,
},
RequiredCapabilityNotGranted {
plugin_id: String,
capability_name: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_serializes_to_json() {
let report = PluginDiscoveryReport {
loaded_ids: vec!["browser".into(), "email".into()],
diagnostics: vec![DiscoveryDiagnostic {
level: DiagnosticLevel::Warn,
path: PathBuf::from("/tmp/plugins/whatsapp/nexo-plugin.toml"),
kind: DiscoveryDiagnosticKind::Disabled {
id: "whatsapp".into(),
},
}],
scanned: 3,
invalid: 0,
disabled: 1,
duplicates: 0,
..Default::default()
};
let s = serde_json::to_string(&report).expect("serialize");
assert!(s.contains("\"loaded_ids\""));
assert!(s.contains("\"browser\""));
assert!(s.contains("\"kind\":\"disabled\""));
assert!(s.contains("\"level\":\"warn\""));
}
}