KeiSeiKit-1.0/_primitives/_rust/kei-prune/src/main.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

115 lines
3.3 KiB
Rust

//! kei-prune CLI dispatcher.
//!
//! Constructor Pattern: one cube = clap wiring + three verb handlers.
//! Each handler is <15 LOC and delegates immediately to the library.
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use kei_prune::{candidates, ensure_schema, mark_retired, stats, PruneError};
use rusqlite::Connection;
/// Default ledger location — mirrors RULE 0.12 SSoT path.
const DEFAULT_DB: &str = "~/.claude/agents/ledger.sqlite";
#[derive(Parser, Debug)]
#[command(
name = "kei-prune",
version,
about = "Mark unused kei-ledger agents as retired (sidecar-only, non-destructive)"
)]
struct Cli {
/// SQLite database path. Defaults to the RULE 0.12 ledger SSoT.
#[arg(long, global = true, default_value = DEFAULT_DB)]
db: String,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand, Debug)]
enum Cmd {
/// List agents eligible for retirement (JSON array).
List {
/// Minimum idle days since `started_ts`.
#[arg(long, default_value_t = 90)]
idle_days: u32,
},
/// Mark a specific agent id as retired (idempotent).
Mark {
/// Ledger `agents.id` to retire.
#[arg(long)]
id: String,
},
/// Emit bucket counts (total / active / idle / retired) as JSON.
Stats,
}
fn main() -> ExitCode {
match run(Cli::parse()) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("kei-prune error: {e}");
ExitCode::from(1)
}
}
}
/// Dispatch to the per-verb handler after opening the DB + schema.
fn run(cli: Cli) -> Result<(), PruneError> {
let db_path = expand_tilde(&cli.db);
let conn = Connection::open(&db_path)?;
ensure_schema(&conn)?;
match cli.cmd {
Cmd::List { idle_days } => verb_list(&conn, idle_days),
Cmd::Mark { id } => verb_mark(&conn, &id),
Cmd::Stats => verb_stats(&conn),
}
}
/// `list` verb — emit JSON array of candidates to stdout.
fn verb_list(conn: &Connection, idle_days: u32) -> Result<(), PruneError> {
let now = now_seconds();
let rows = candidates(conn, now, idle_days)?;
println!("{}", serde_json::to_string_pretty(&rows)?);
Ok(())
}
/// `mark` verb — record retirement, echo one-line JSON confirmation.
fn verb_mark(conn: &Connection, id: &str) -> Result<(), PruneError> {
let now = now_seconds();
mark_retired(conn, id, now)?;
let msg = serde_json::json!({ "retired": id, "retired_ts": now });
println!("{}", msg);
Ok(())
}
/// `stats` verb — emit JSON object of fleet counts.
fn verb_stats(conn: &Connection) -> Result<(), PruneError> {
let s = stats(conn)?;
println!("{}", serde_json::to_string_pretty(&s)?);
Ok(())
}
/// Expand a leading `~/` to `$HOME/`. No-op otherwise.
fn expand_tilde(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/") {
if let Some(home) = std::env::var_os("HOME") {
let mut pb = PathBuf::from(home);
pb.push(stripped);
return pb;
}
}
PathBuf::from(path)
}
/// Current unix time in whole seconds. Isolated for test override and
/// to avoid pulling chrono for a single now() call.
fn now_seconds() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}