use std::path::{Path, PathBuf};
use clap_noun_verb::Result;
use clap_noun_verb_macros::verb;
use serde::Serialize;
#[derive(Serialize)]
pub struct StartOutput {
pub status: String,
pub transport: String,
}
#[derive(Serialize)]
pub struct CheckSummary {
pub error_count: usize,
pub warning_count: usize,
}
#[derive(Serialize)]
pub struct EmitPackOutput {
pub out_dir: String,
pub agents: Vec<String>,
pub files_written: usize,
pub pack_hash: String,
pub receipt_sig: Option<String>,
pub bound_to_scan: bool,
}
#[derive(Serialize)]
pub struct MineSummary {
pub events_analyzed: usize,
pub failure_edges: usize,
pub total_edges: usize,
pub report_path: String,
pub promoted_count: usize,
pub promoted_path: String,
}
#[derive(Serialize)]
pub struct InitOutput {
pub files_written: Vec<String>,
pub pack_dir: String,
}
#[verb]
fn start(transport: Option<String>) -> Result<StartOutput> {
let transport_val = transport.unwrap_or_else(|| "stdio".to_string());
if transport_val != "stdio" {
return Err(clap_noun_verb::NounVerbError::execution_error(
"Only stdio transport is supported; use --transport stdio".to_string(),
));
}
crate::runtime::block_on(async move { ggen_lsp::run_stdio().await })
.map_err(|e: ggen_core::utils::Error| {
clap_noun_verb::NounVerbError::execution_error(e.to_string())
})?
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
Ok(StartOutput {
status: "stopped".to_string(),
transport: transport_val,
})
}
#[verb]
fn serve(protocol: Option<String>) -> Result<StartOutput> {
let proto = protocol.unwrap_or_else(|| "lsp".to_string());
match proto.as_str() {
"lsp" => {
crate::runtime::block_on(async move { ggen_lsp::run_stdio().await })
.map_err(|e: ggen_core::utils::Error| {
clap_noun_verb::NounVerbError::execution_error(e.to_string())
})?
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
}
"mcp" => {
crate::runtime::block_on(async move {
ggen_lsp::mcp::RepairRouteServer::start_stdio().await
})
.map_err(|e: ggen_core::utils::Error| {
clap_noun_verb::NounVerbError::execution_error(e.to_string())
})?
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
}
other => {
return Err(clap_noun_verb::NounVerbError::execution_error(format!(
"unknown protocol: {other} (use lsp|mcp; A2A is consumed via the ggen-lsp-a2a bridge adapter, not a standalone server)"
)));
}
}
Ok(StartOutput {
status: "stopped".to_string(),
transport: proto,
})
}
#[verb]
fn check(
files: Option<String>, root: Option<String>, with_routes: Option<bool>,
) -> Result<CheckSummary> {
let code = run_check(
files.as_deref(),
root.as_deref(),
with_routes.unwrap_or(false),
)?;
std::process::exit(code);
}
fn run_check(files: Option<&str>, root: Option<&str>, with_routes: bool) -> Result<i32> {
let scan_root = root.unwrap_or(".");
let explicit = files.map(parse_paths).unwrap_or_default();
let paths = if explicit.is_empty() {
ggen_lsp::discover_law_surfaces(Path::new(scan_root))
} else {
explicit
};
let report = ggen_lsp::check_files_with_routes(&paths, with_routes);
report.capture(Path::new(scan_root));
let json = serde_json::to_string_pretty(&report)
.unwrap_or_else(|e| format!("{{\"error\":\"serialize failed: {e}\"}}"));
println!("{json}");
Ok(report.exit_code())
}
#[verb]
fn init(
root: Option<String>, editors: Option<String>, agents: Option<String>,
) -> Result<InitOutput> {
let root = root.unwrap_or_else(|| ".".to_string());
let editors = split_list(editors.as_deref());
let agents = split_list(agents.as_deref());
let report = ggen_lsp::init_project(Path::new(&root), &editors, &agents)
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
Ok(InitOutput {
files_written: report.files_written,
pack_dir: report.pack_dir,
})
}
fn split_list(s: Option<&str>) -> Vec<String> {
s.map(|v| {
v.split(|c: char| c.is_whitespace() || c == ',')
.filter(|x| !x.is_empty())
.map(str::to_string)
.collect()
})
.unwrap_or_default()
}
#[verb]
fn replay(case: Option<String>, root: Option<String>) -> Result<serde_json::Value> {
let root = root.unwrap_or_else(|| ".".to_string());
let value = match case {
Some(id) => serde_json::to_value(ggen_lsp::replay_case(Path::new(&root), &id)),
None => serde_json::to_value(ggen_lsp::verify_promotion(Path::new(&root))),
};
value.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
}
#[verb]
fn metrics(root: Option<String>) -> Result<serde_json::Value> {
let root = root.unwrap_or_else(|| ".".to_string());
let m = ggen_lsp::compute_metrics(Path::new(&root));
serde_json::to_value(&m)
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
}
#[verb]
fn field_status(root: Option<String>) -> Result<serde_json::Value> {
let root = root.unwrap_or_else(|| ".".to_string());
serde_json::to_value(ggen_lsp::field_status(Path::new(&root)))
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
}
#[verb]
fn mine(root: Option<String>) -> Result<MineSummary> {
let root = root.unwrap_or_else(|| ".".to_string());
let report = ggen_lsp::mine(Path::new(&root))
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
Ok(MineSummary {
events_analyzed: report.event_count,
failure_edges: report.failure_edges.len(),
total_edges: report.all_edges.len(),
report_path: report.report_path.to_string_lossy().to_string(),
promoted_count: report.promoted_count,
promoted_path: report.promoted_path.to_string_lossy().to_string(),
})
}
fn parse_paths(files: &str) -> Vec<PathBuf> {
files
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect()
}
#[verb]
fn emit_pack(
agents: Option<String>, out: Option<String>, from_scan: Option<String>,
) -> Result<EmitPackOutput> {
let agent_list = agents
.map(|a| {
a.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect::<Vec<_>>()
})
.filter(|v| !v.is_empty())
.unwrap_or_else(|| {
ggen_lsp::DEFAULT_AGENTS
.iter()
.map(|s| (*s).to_string())
.collect()
});
let scan_hash = from_scan.as_deref().and_then(read_scan_aggregate_hash);
let bound_to_scan = scan_hash.is_some();
let opts = ggen_lsp::PackOptions {
agents: agent_list,
out_dir: PathBuf::from(out.unwrap_or_else(|| ".agent-admissibility".to_string())),
scan_hash,
};
let report = ggen_lsp::emit_pack(&opts)
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
Ok(EmitPackOutput {
out_dir: report.out_dir,
agents: report.agents,
files_written: report.files_written.len(),
pack_hash: report.pack_hash,
receipt_sig: report.receipt_sig,
bound_to_scan,
})
}
fn read_scan_aggregate_hash(path: &str) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&content).ok()?;
v.get("aggregate_hash")
.and_then(|h| h.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
}
#[verb]
fn verify_pack(pack_dir: Option<String>) -> Result<serde_json::Value> {
let dir = pack_dir.unwrap_or_else(|| ".agent-admissibility".to_string());
let replay = ggen_lsp::verify_pack(Path::new(&dir));
serde_json::to_value(&replay)
.map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
}