ccd-cli 1.0.0-beta.2

Bootstrap and validate Continuous Context Development repositories
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;

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())
    }

    fn enrich_pod_status(
        &self,
        _pod_name: &str,
        _locality_id: &str,
        _profile: &str,
        _shared_root: &Path,
    ) -> Option<Vec<(String, String)>> {
        None
    }
}

pub(crate) fn registered() -> Vec<&'static dyn Extension> {
    Vec::new()
}

#[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_extensions_are_registered_in_the_kernel() {
        assert!(registered().is_empty());
        assert!(owned_command_groups().is_empty());
    }

    #[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());
    }
}