use std::path::Path;
use std::process::ExitCode;
use anyhow::Result;
use clap::{ArgMatches, Command as ClapCommand};
use serde_json::Value;
use crate::commands::describe::CommandDescriptor;
use crate::mcp::protocol::Tool;
use crate::output::OutputFormat;
use crate::paths::state::StateLayout;
use crate::state::thread_transfer::{
ThreadAttachmentExportContext, ThreadAttachmentExportResult, ThreadAttachmentImportPreview,
ThreadDiagnostic, ValidatedAttachmentMetadata,
};
mod fixity;
pub(crate) struct HealthDiagnostic {
pub check: &'static str,
pub severity: &'static str,
pub file: String,
pub message: String,
pub details: Option<serde_json::Value>,
}
pub(crate) trait Extension {
fn name(&self) -> &'static str;
fn command_groups(&self) -> &'static [&'static str];
fn cli_command(&self) -> Option<ClapCommand> {
None
}
fn dispatch_cli(
&self,
_subcommand_name: &str,
_matches: &ArgMatches,
_output: OutputFormat,
) -> Option<Result<ExitCode>> {
None
}
fn mcp_tools(&self, _commands: &[CommandDescriptor]) -> Vec<Tool> {
Vec::new()
}
fn dispatch_mcp(&self, _tool_name: &str, _args: &Value) -> Option<Result<Value>> {
None
}
fn health_diagnostics(
&self,
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
Ok(Vec::new())
}
}
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
Vec::new()
}
pub(crate) trait ThreadAttachmentProvider: Sync {
fn name(&self) -> &'static str;
fn extension_type(&self) -> &'static str;
fn export_thread_attachments(
&self,
context: &ThreadAttachmentExportContext<'_>,
) -> Result<ThreadAttachmentExportResult>;
fn preview_thread_attachment_import(
&self,
index: usize,
attachment: &ValidatedAttachmentMetadata,
) -> Option<ThreadAttachmentImportPreview>;
}
pub(crate) fn thread_attachment_providers() -> Vec<&'static dyn ThreadAttachmentProvider> {
vec![&fixity::FIXITY]
}
pub(crate) fn thread_transfer_attachment_candidates(
context: &ThreadAttachmentExportContext<'_>,
) -> (Vec<Value>, Vec<ThreadDiagnostic>) {
let mut attachments = Vec::new();
let mut diagnostics = Vec::new();
for provider in thread_attachment_providers() {
match provider.export_thread_attachments(context) {
Ok(result) => {
attachments.extend(result.attachments);
diagnostics.extend(result.diagnostics);
}
Err(error) => diagnostics.push(ThreadDiagnostic {
severity: "warning",
code: "extension_unavailable",
message: format!(
"Thread attachment provider `{}` was unavailable during export: {error}.",
provider.name()
),
}),
}
}
(attachments, diagnostics)
}
pub(crate) fn preview_thread_attachment_import(
index: usize,
attachment: &ValidatedAttachmentMetadata,
) -> Option<ThreadAttachmentImportPreview> {
for provider in thread_attachment_providers() {
if attachment.extension_type != provider.extension_type() {
continue;
}
if let Some(preview) = provider.preview_thread_attachment_import(index, attachment) {
return Some(preview);
}
}
None
}
#[cfg(test)]
pub(crate) fn owned_command_groups() -> Vec<&'static str> {
registered()
.into_iter()
.flat_map(|extension| extension.command_groups().iter().copied())
.collect()
}
pub(crate) fn augment_clap(mut command: ClapCommand) -> ClapCommand {
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
if let Some(subcommand) = extension.cli_command() {
command = command.subcommand(subcommand);
}
}
command
}
pub(crate) fn dispatch_cli(
subcommand_name: &str,
matches: &ArgMatches,
output: OutputFormat,
) -> Option<Result<ExitCode>> {
for extension in registered() {
if let Some(result) = extension.dispatch_cli(subcommand_name, matches, output) {
return Some(result);
}
}
None
}
pub(crate) fn build_mcp_tools(commands: &[CommandDescriptor]) -> Vec<Tool> {
let mut tools = Vec::new();
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
tools.extend(extension.mcp_tools(commands));
}
tools
}
pub(crate) fn dispatch_mcp(tool_name: &str, args: &Value) -> Option<Result<Value>> {
for extension in registered() {
if let Some(report) = extension.dispatch_mcp(tool_name, args) {
return Some(report);
}
}
None
}
pub(crate) fn health_diagnostics(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
let mut all = Vec::new();
for extension in registered() {
all.extend(extension.health_diagnostics(layout, repo_root, locality_id)?);
}
Ok(all)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_cli_or_mcp_extensions_are_registered_in_the_kernel() {
assert!(registered().is_empty());
assert!(owned_command_groups().is_empty());
}
#[test]
fn fixity_thread_attachment_provider_is_registered() {
let providers = thread_attachment_providers();
assert_eq!(providers.len(), 1);
assert_eq!(providers[0].name(), "fixity");
assert_eq!(providers[0].extension_type(), "fixity");
}
#[test]
fn thread_attachment_provider_extension_types_are_unique() {
let providers = thread_attachment_providers();
let mut extension_types = std::collections::BTreeSet::new();
for provider in providers {
assert!(
extension_types.insert(provider.extension_type()),
"duplicate thread attachment provider extension_type `{}`",
provider.extension_type()
);
}
}
#[test]
fn extension_mcp_tools_are_empty_in_the_kernel() {
let schema = crate::commands::describe::run();
let tools = build_mcp_tools(&schema.commands);
assert!(tools.is_empty());
}
#[test]
fn extension_cli_commands_are_empty_in_the_kernel() {
let command = augment_clap(clap::Command::new("ccd"));
assert!(command.get_subcommands().next().is_none());
}
}