use std::path::PathBuf;
use anyhow::{anyhow, Result};
use clap::Subcommand;
use serde::Serialize;
use khive_runtime::{KhiveRuntime, RuntimeConfig};
#[derive(Subcommand, Debug)]
pub enum EngineCommand {
List(EngineListArgs),
Status(EngineStatusArgs),
Migrate(EngineMigrateArgs),
DriftCheck(EngineDriftCheckArgs),
}
#[derive(clap::Parser, Debug)]
pub struct EngineListArgs {
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineStatusArgs {
pub engine: String,
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineMigrateArgs {
pub engine: String,
#[arg(long, conflicts_with_all = &["resume", "abort"])]
pub to: Option<String>,
#[arg(long, conflicts_with_all = &["to", "abort"])]
pub resume: bool,
#[arg(long, conflicts_with_all = &["to", "resume"])]
pub abort: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(clap::Parser, Debug)]
pub struct EngineDriftCheckArgs {
pub engine: String,
#[arg(long, default_value = "1000")]
pub sample: usize,
#[arg(long)]
pub human: bool,
#[arg(long)]
pub db: Option<PathBuf>,
}
#[derive(Clone, Debug, Serialize)]
pub struct EngineModelRecord {
pub engine_name: String,
pub model_id: String,
pub key_version: String,
pub dimensions: u32,
pub status: String,
pub activated_at: Option<i64>,
pub superseded_at: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct EngineStatus {
pub engine_name: String,
pub active_model: Option<EngineModelRecord>,
pub migration_in_progress: bool,
pub pending_model: Option<EngineModelRecord>,
}
pub async fn run_engine(cmd: EngineCommand) -> Result<()> {
match cmd {
EngineCommand::List(args) => cmd_engine_list(args).await,
EngineCommand::Status(args) => cmd_engine_status(args).await,
EngineCommand::Migrate(args) => cmd_engine_migrate(args),
EngineCommand::DriftCheck(args) => cmd_engine_drift_check(args),
}
}
async fn cmd_engine_list(args: EngineListArgs) -> Result<()> {
let records = fetch_model_records(args.db.as_deref(), None).await?;
if args.human {
for r in &records {
println!(
" {:<20} model={:<30} status={} key_version={} dim={}",
r.engine_name, r.model_id, r.status, r.key_version, r.dimensions
);
}
} else {
let json = serde_json::to_string(&records).expect("serialize EngineModelRecord[]");
println!("{json}");
}
Ok(())
}
async fn cmd_engine_status(args: EngineStatusArgs) -> Result<()> {
let all = fetch_model_records(args.db.as_deref(), Some(&args.engine)).await?;
let active = all.iter().find(|r| r.status == "active").cloned();
let pending = all.iter().find(|r| r.status == "pending").cloned();
let status = EngineStatus {
engine_name: args.engine.clone(),
migration_in_progress: pending.is_some(),
active_model: active,
pending_model: pending,
};
if args.human {
if let Some(ref m) = status.active_model {
println!("engine: {}", status.engine_name);
println!(" active model: {}", m.model_id);
println!(" key_version: {}", m.key_version);
println!(" dimensions: {}", m.dimensions);
println!(" migration_in_progress:{}", status.migration_in_progress);
} else {
println!(
"engine: {} — no active model registered",
status.engine_name
);
}
} else {
let json = serde_json::to_string(&status).expect("serialize EngineStatus");
println!("{json}");
}
Ok(())
}
fn cmd_engine_migrate(_args: EngineMigrateArgs) -> Result<()> {
Err(anyhow!(
"engine migrate is not yet implemented (EmbedMigrationWorker deferred \
to follow-up #380). Use 'kkernel engine list' / 'status' to inspect registered models."
))
}
fn cmd_engine_drift_check(_args: EngineDriftCheckArgs) -> Result<()> {
Err(anyhow!(
"engine drift-check is not yet implemented (lattice_transport integration \
deferred). Track follow-up #380."
))
}
async fn fetch_model_records(
db: Option<&std::path::Path>,
engine_filter: Option<&str>,
) -> Result<Vec<EngineModelRecord>> {
let db_path: Option<PathBuf> = match db {
Some(p) if p.exists() => Some(p.to_path_buf()),
Some(p) => anyhow::bail!("database file does not exist: {}", p.display()),
None => {
let default = std::env::var("HOME")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.join(".khive/khive.db");
if default.exists() {
Some(default)
} else {
return Ok(Vec::new());
}
}
};
let cfg = RuntimeConfig {
db_path,
..RuntimeConfig::default()
};
let rt = KhiveRuntime::new_readonly(cfg).map_err(|e| anyhow!("{e}"))?;
let raw = rt
.list_embedding_models(engine_filter)
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(raw
.into_iter()
.map(|r| EngineModelRecord {
engine_name: r.engine_name,
model_id: r.model_id,
key_version: r.key_version,
dimensions: r.dimensions,
status: r.status,
activated_at: r.activated_at,
superseded_at: r.superseded_at,
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn temp_db() -> (TempDir, Option<std::path::PathBuf>) {
let tmp = TempDir::new().expect("temp dir");
let path = tmp.path().join("engine_test.db");
std::fs::File::create(&path).expect("create empty db file");
(tmp, Some(path))
}
#[tokio::test]
async fn engine_list_empty_ok() {
let (_tmp, db) = temp_db();
let args = EngineListArgs { human: false, db };
cmd_engine_list(args)
.await
.expect("engine list succeeds on empty registry");
}
#[tokio::test]
async fn engine_status_empty_ok() {
let (_tmp, db) = temp_db();
let args = EngineStatusArgs {
engine: "mE5-small".into(),
human: false,
db,
};
cmd_engine_status(args)
.await
.expect("engine status succeeds on empty registry");
}
#[test]
fn engine_migrate_returns_not_implemented() {
let (_tmp, db) = temp_db();
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: Some("bge-small-en-v1.5".into()),
resume: false,
abort: false,
db,
};
let err = cmd_engine_migrate(args).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not yet implemented"),
"expected 'not yet implemented' in error, got: {msg}"
);
assert!(
msg.contains("#380"),
"expected follow-up issue reference in error, got: {msg}"
);
}
#[test]
fn engine_migrate_resume_returns_not_implemented() {
let (_tmp, db) = temp_db();
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: None,
resume: true,
abort: false,
db,
};
let err = cmd_engine_migrate(args).unwrap_err();
assert!(err.to_string().contains("not yet implemented"));
}
#[test]
fn engine_migrate_abort_returns_not_implemented() {
let (_tmp, db) = temp_db();
let args = EngineMigrateArgs {
engine: "mE5-small".into(),
to: None,
resume: false,
abort: true,
db,
};
let err = cmd_engine_migrate(args).unwrap_err();
assert!(err.to_string().contains("not yet implemented"));
}
#[test]
fn drift_check_returns_not_implemented() {
let (_tmp, db) = temp_db();
let args = EngineDriftCheckArgs {
engine: "mE5-small".into(),
sample: 500,
human: false,
db,
};
let err = cmd_engine_drift_check(args).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not yet implemented"),
"expected 'not yet implemented' in error, got: {msg}"
);
assert!(
msg.contains("#380"),
"expected follow-up issue reference in error, got: {msg}"
);
}
}