feat(kebab-cli): kebab schema subcommand (fb-27)

Text mode: doctor-style key/value layout. JSON mode: schema.v1 wire
record. Honors `--config <path>` 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) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-07 12:24:06 +09:00
parent c91228e7d5
commit 4eb13c63ae

View File

@@ -177,6 +177,9 @@ enum Cmd {
/// Health check. /// Health check.
Doctor, Doctor,
/// Print introspection report (wire schemas, capabilities, model versions, stats).
Schema,
/// Launch the Ratatui shell (P9-1 — Library pane only; search / /// Launch the Ratatui shell (P9-1 — Library pane only; search /
/// ask / inspect panes land with p9-2 / p9-3 / p9-4). /// ask / inspect panes land with p9-2 / p9-3 / p9-4).
Tui, Tui,
@@ -604,6 +607,18 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
Ok(()) 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 => { Cmd::Doctor => {
let report = kebab_app::doctor_with_config_path(cli.config.as_deref())?; let report = kebab_app::doctor_with_config_path(cli.config.as_deref())?;
if cli.json { 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 — /// Minimal stdin/stdout confirm prompt for destructive ops. No new dep —
/// uses stdlib `IsTerminal` (the caller is expected to have already /// uses stdlib `IsTerminal` (the caller is expected to have already
/// short-circuited the non-TTY case). Returns `Ok(true)` only when the /// short-circuited the non-TTY case). Returns `Ok(true)` only when the