use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use khive_runtime::{BackendId, KhiveRuntime, RuntimeConfig};
use kkernel::{coordinator::BackendRegistry, engine, kg, pack_introspect, reindex, sync, vector};
#[derive(Parser, Debug)]
#[command(
name = "kkernel",
version,
about = "khive kernel — admin/management Rust binary (ADR-076)"
)]
struct Args {
#[arg(long, env = "KHIVE_LOG", default_value = "warn", global = true)]
log: String,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Sync(SyncArgs),
#[command(subcommand)]
Pack(PackCommand),
#[command(subcommand)]
Kg(kg::KgCommand),
#[command(subcommand)]
Engine(engine::EngineCommand),
#[command(subcommand)]
Vector(vector::VectorCommand),
Reindex(reindex::ReindexArgs),
#[command(subcommand)]
Backend(BackendCommand),
}
#[derive(Parser, Debug)]
struct SyncArgs {
#[arg(long, default_value = ".")]
repo: PathBuf,
#[arg(long)]
db: PathBuf,
#[arg(long, default_value = "local")]
namespace: String,
}
#[derive(Subcommand, Debug)]
enum PackCommand {
List {
#[arg(long)]
human: bool,
},
Handler {
name: String,
#[arg(long)]
human: bool,
},
}
#[derive(Subcommand, Debug)]
enum BackendCommand {
List {
#[arg(long)]
human: bool,
},
Info {
name: String,
#[arg(long)]
human: bool,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
init_tracing(&args.log);
match args.command {
Command::Sync(s) => cmd_sync(s).await,
Command::Pack(p) => cmd_pack(p),
Command::Kg(k) => kg::run_kg(k).await,
Command::Engine(e) => engine::run_engine(e).await,
Command::Vector(v) => vector::run_vector(v),
Command::Reindex(r) => reindex::run_reindex(r).await,
Command::Backend(b) => cmd_backend(b),
}
}
fn init_tracing(level: &str) {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(level)
.with_ansi(false)
.init();
}
async fn cmd_sync(args: SyncArgs) -> Result<()> {
let report = sync::run_sync(&args.repo, &args.db, &args.namespace)
.await
.with_context(|| {
format!(
"sync failed for repo={} db={}",
args.repo.display(),
args.db.display()
)
})?;
let json = serde_json::to_string(&report).expect("serialize SyncReport");
println!("{json}");
Ok(())
}
fn cmd_pack(cmd: PackCommand) -> Result<()> {
match cmd {
PackCommand::List { human } => {
let packs = pack_introspect::list_packs()?;
if human {
for p in &packs {
println!("# {} ({} verbs)", p.name, p.verbs.len());
if !p.requires.is_empty() {
println!(" requires: {}", p.requires.join(", "));
}
if !p.note_kinds.is_empty() {
println!(" note_kinds: {}", p.note_kinds.join(", "));
}
if !p.entity_kinds.is_empty() {
println!(" entity_kinds: {}", p.entity_kinds.join(", "));
}
for v in &p.verbs {
println!(" {:<20} {}", v.name, v.description);
}
println!();
}
} else {
let json = serde_json::to_string(&packs).expect("serialize PackInfo[]");
println!("{json}");
}
Ok(())
}
PackCommand::Handler { name, human } => {
let info = pack_introspect::pack_handler(&name)?;
let info = info.with_context(|| format!("pack {name:?} is not registered"))?;
if human {
println!("# {} ({} verbs)", info.name, info.verbs.len());
if !info.requires.is_empty() {
println!("requires: {}", info.requires.join(", "));
}
if !info.note_kinds.is_empty() {
println!("note_kinds: {}", info.note_kinds.join(", "));
}
if !info.entity_kinds.is_empty() {
println!("entity_kinds: {}", info.entity_kinds.join(", "));
}
for v in &info.verbs {
println!(" {:<20} {}", v.name, v.description);
}
} else {
let json = serde_json::to_string(&info).expect("serialize PackInfo");
println!("{json}");
}
Ok(())
}
}
}
fn cmd_backend(cmd: BackendCommand) -> Result<()> {
let default_config = RuntimeConfig::default();
let default_id = default_config.backend_id.clone();
let default_path = default_config
.db_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| ":memory:".to_string());
let mut registry = BackendRegistry::new();
let rt = KhiveRuntime::new(default_config).map_err(|e| anyhow::anyhow!("{e}"))?;
registry.register(default_id.clone(), std::sync::Arc::new(rt));
match cmd {
BackendCommand::List { human } => {
let ids: Vec<_> = registry.ids();
if human {
println!("Registered backends ({}):", ids.len());
for id in &ids {
let entry = registry.get(id).unwrap();
let primary_marker = if registry.primary().map(|p| p.id == *id).unwrap_or(false)
{
" [primary]"
} else {
""
};
println!(" {}{}", id.as_str(), primary_marker);
let _ = entry; }
} else {
let names: Vec<&str> = ids.iter().map(|id| id.as_str()).collect();
let json = serde_json::json!({
"backends": names,
"primary": registry.primary().map(|e| e.id.as_str()),
"count": ids.len(),
});
println!("{}", serde_json::to_string(&json).expect("serialize"));
}
Ok(())
}
BackendCommand::Info { name, human } => {
let id = BackendId::new(&name);
let entry = registry
.get(&id)
.with_context(|| format!("backend {name:?} is not registered"))?;
if human {
let is_primary = registry
.primary()
.map(|p| p.id == entry.id)
.unwrap_or(false);
println!("backend: {}", entry.id.as_str());
println!(" primary: {is_primary}");
println!(" path: {default_path}");
} else {
let json = serde_json::json!({
"name": entry.id.as_str(),
"path": default_path,
"primary": registry.primary().map(|p| p.id == entry.id).unwrap_or(false),
});
println!("{}", serde_json::to_string(&json).expect("serialize"));
}
Ok(())
}
}
}