use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::Result;
use clap::{ArgMatches, Args, Command, FromArgMatches, Subcommand};
use serde_json::Value;
use crate::commands::describe::CommandDescriptor;
use crate::extensions::codemap_commands;
use crate::extensions::{Extension, HealthDiagnostic};
use crate::mcp::protocol::Tool;
use crate::mcp::tools::{build_tool, ToolDef};
use crate::output::OutputFormat;
use crate::paths;
use crate::paths::state::StateLayout;
#[derive(Subcommand)]
enum CodemapCommand {
Import(CodemapImportArgs),
Status(CodemapStatusArgs),
Query(CodemapQueryArgs),
}
#[derive(Args)]
struct CodemapImportArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct CodemapStatusArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
}
#[derive(Args)]
struct CodemapQueryArgs {
#[arg(long, default_value = ".")]
path: PathBuf,
#[arg(long)]
profile: Option<String>,
#[arg(long)]
prefix: Option<String>,
#[arg(long, default_value_t = 50)]
limit: usize,
}
const COMMAND_GROUPS: &[&str] = &["codemap"];
const MCP_TOOLS: &[ToolDef] = &[ToolDef {
name: "ccd_codemap",
description: "Code map: status query",
commands: &[
("codemap-status", &["codemap", "status"]),
("codemap-query", &["codemap", "query"]),
],
renames: &[],
exclude: &[],
extra_props: &[],
}];
pub(crate) struct CodemapExtension;
pub(crate) static CODEMAP_EXTENSION: CodemapExtension = CodemapExtension;
impl Extension for CodemapExtension {
fn name(&self) -> &'static str {
"codemap"
}
fn command_groups(&self) -> &'static [&'static str] {
COMMAND_GROUPS
}
fn cli_command(&self) -> Option<Command> {
Some(codemap_cli_command())
}
fn dispatch_cli(
&self,
subcommand_name: &str,
matches: &ArgMatches,
output: OutputFormat,
) -> Option<Result<ExitCode>> {
if subcommand_name != "codemap" {
return None;
}
Some(run_cli_from_matches(matches, output))
}
fn mcp_tools(&self, commands: &[CommandDescriptor]) -> Vec<Tool> {
MCP_TOOLS
.iter()
.map(|tool| build_tool(tool, commands))
.collect()
}
fn dispatch_mcp(&self, tool_name: &str, args: &Value) -> Option<Result<Value>> {
if tool_name != "ccd_codemap" {
return None;
}
let command = match get_required_string(args, "command") {
Ok(command) => command,
Err(error) => return Some(Err(error)),
};
let report = (|| -> Result<Value> {
match command.as_str() {
"codemap-status" => {
let path = resolve_path(args)?;
let profile = get_opt_str(args, "profile");
let report = codemap_commands::status(&path, profile.as_deref())?;
to_value(&report)
}
"codemap-query" => {
let path = resolve_path(args)?;
let prefix = get_opt_str(args, "prefix");
let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(50) as usize;
let profile = get_opt_str(args, "profile");
let report = codemap_commands::query_report(
&path,
profile.as_deref(),
prefix.as_deref(),
limit,
)?;
to_value(&report)
}
other => Err(anyhow::anyhow!("unknown codemap MCP command: {other}")),
}
})();
Some(report)
}
fn health_diagnostics(
&self,
layout: &StateLayout,
_repo_root: &std::path::Path,
locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
let db_root = match layout.focus_pod_name(locality_id)? {
Some(pod) => layout.pod_repo_overlay_root(&pod, locality_id)?,
None => layout.repo_overlay_root(locality_id)?,
};
let db_path = db_root.join("extensions").join("codemap").join("codemap.db");
if !db_path.exists() {
return Ok(Vec::new());
}
match codemap_commands::count_entries_at(&db_path) {
Ok(count) => Ok(vec![HealthDiagnostic {
check: "codemap_db",
severity: "info",
file: db_path.display().to_string(),
message: format!("Codemap database contains {count} entries."),
details: Some(serde_json::json!({
"entry_count": count,
"commands": [
"ccd codemap query --prefix <path>",
"ccd codemap status",
"ccd codemap import"
]
})),
}]),
Err(err) => Ok(vec![HealthDiagnostic {
check: "codemap_db",
severity: "warning",
file: db_path.display().to_string(),
message: format!("Failed to read codemap database: {err}"),
details: None,
}]),
}
}
}
fn codemap_cli_command() -> Command {
CodemapCommand::augment_subcommands(
Command::new("codemap")
.about("Structural code map commands")
.subcommand_required(true)
.arg_required_else_help(true),
)
}
fn run_cli_from_matches(matches: &ArgMatches, output: OutputFormat) -> Result<ExitCode> {
let command = CodemapCommand::from_arg_matches(matches)?;
run_cli(command, output)
}
fn run_cli(command: CodemapCommand, output: OutputFormat) -> Result<ExitCode> {
match command {
CodemapCommand::Import(args) => {
let report = codemap_commands::import(&args.path, args.profile.as_deref())?;
crate::output::render_report(output, &report)
}
CodemapCommand::Status(args) => {
let report = codemap_commands::status(&args.path, args.profile.as_deref())?;
crate::output::render_report(output, &report)
}
CodemapCommand::Query(args) => {
let report = codemap_commands::query_report(
&args.path,
args.profile.as_deref(),
args.prefix.as_deref(),
args.limit,
)?;
crate::output::render_report(output, &report)
}
}
}
fn resolve_path(args: &Value) -> Result<PathBuf> {
let path = get_opt_str(args, "path").unwrap_or_else(|| ".".to_owned());
paths::cli::resolve(&PathBuf::from(path))
}
fn get_required_string(args: &Value, key: &str) -> Result<String> {
get_opt_str(args, key).ok_or_else(|| anyhow::anyhow!("missing required argument `{key}`"))
}
fn get_opt_str(args: &Value, key: &str) -> Option<String> {
args.get(key).and_then(Value::as_str).map(str::to_owned)
}
fn to_value<T: serde::Serialize>(report: &T) -> Result<Value> {
Ok(serde_json::to_value(report)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codemap_extension_reports_metadata() {
let extension: &dyn Extension = &CODEMAP_EXTENSION;
assert_eq!(extension.name(), "codemap");
assert_eq!(extension.command_groups(), &["codemap"]);
}
}