From 4eb13c63aea526f0f015967d41d2ac4f9a6d5ec0 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Thu, 7 May 2026 12:24:06 +0900
Subject: [PATCH] feat(kebab-cli): kebab schema subcommand (fb-27)
Text mode: doctor-style key/value layout. JSON mode: schema.v1 wire
record. Honors `--config ` via the established
`kebab_app::schema_with_config(&cfg)` facade pattern (per the P3-5 /
P4-3 regression conventions).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
crates/kebab-cli/src/main.rs | 58 ++++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
diff --git a/crates/kebab-cli/src/main.rs b/crates/kebab-cli/src/main.rs
index 71f9f52..fca4756 100644
--- a/crates/kebab-cli/src/main.rs
+++ b/crates/kebab-cli/src/main.rs
@@ -177,6 +177,9 @@ enum Cmd {
/// Health check.
Doctor,
+ /// Print introspection report (wire schemas, capabilities, model versions, stats).
+ Schema,
+
/// Launch the Ratatui shell (P9-1 — Library pane only; search /
/// ask / inspect panes land with p9-2 / p9-3 / p9-4).
Tui,
@@ -604,6 +607,18 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
Ok(())
}
+ Cmd::Schema => {
+ let cfg = kebab_config::Config::load(cli.config.as_deref())?;
+ let report = kebab_app::schema_with_config(&cfg)?;
+ if cli.json {
+ let v = wire::wire_schema(&report);
+ println!("{}", serde_json::to_string(&v)?);
+ } else {
+ print_schema_text(&report);
+ }
+ Ok(())
+ }
+
Cmd::Doctor => {
let report = kebab_app::doctor_with_config_path(cli.config.as_deref())?;
if cli.json {
@@ -720,6 +735,49 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
}
}
+fn print_schema_text(s: &kebab_app::SchemaV1) {
+ println!("kebab v{}\n", s.kebab_version);
+
+ println!("wire schemas");
+ println!(" {}", s.wire.schemas.join(", "));
+ println!();
+
+ println!("capabilities");
+ let caps = [
+ ("json_mode", s.capabilities.json_mode),
+ ("ingest_progress", s.capabilities.ingest_progress),
+ ("ingest_cancellation", s.capabilities.ingest_cancellation),
+ ("rag_multi_turn", s.capabilities.rag_multi_turn),
+ ("search_cache", s.capabilities.search_cache),
+ ("incremental_ingest", s.capabilities.incremental_ingest),
+ ("streaming_ask", s.capabilities.streaming_ask),
+ ("http_daemon", s.capabilities.http_daemon),
+ ("mcp_server", s.capabilities.mcp_server),
+ ("single_file_ingest", s.capabilities.single_file_ingest),
+ ];
+ for (name, on) in caps {
+ let mark = if on { "✓" } else { "✗" };
+ println!(" {mark} {name}");
+ }
+ println!();
+
+ println!("models");
+ println!(" parser_version {}", s.models.parser_version);
+ println!(" chunker_version {}", s.models.chunker_version);
+ println!(" embedding_version {}", s.models.embedding_version);
+ println!(" prompt_template_version {}", s.models.prompt_template_version);
+ println!(" index_version {}", s.models.index_version);
+ println!(" corpus_revision {}", s.models.corpus_revision);
+ println!();
+
+ println!("stats");
+ println!(" doc_count {}", s.stats.doc_count);
+ println!(" chunk_count {}", s.stats.chunk_count);
+ println!(" asset_count {}", s.stats.asset_count);
+ let last = s.stats.last_ingest_at.as_deref().unwrap_or("(never)");
+ println!(" last_ingest_at {last}");
+}
+
/// Minimal stdin/stdout confirm prompt for destructive ops. No new dep —
/// uses stdlib `IsTerminal` (the caller is expected to have already
/// short-circuited the non-TTY case). Returns `Ok(true)` only when the