feat(cli): kebab config migrate 서브커맨드 + wire config_migration.v1

- Cmd::Config { Migrate { --dry-run } }, --json 시 config_migration.v1.
- wire_config_migration (ConfigMigrationReport 가 schema_version 자체 보유).
- schema.rs WIRE_SCHEMAS 에 config_migration.v1 등록 + JSON schema 파일.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:09:31 +00:00
parent b7e022a5e3
commit f2cc325cf3
4 changed files with 97 additions and 0 deletions

View File

@@ -108,6 +108,7 @@ const WIRE_SCHEMAS: &[&str] = &[
"doc_summary.v1",
"chunk_inspection.v1",
"doctor.v1",
"config_migration.v1",
"ingest_report.v1",
"ingest_progress.v1",
"reset_report.v1",

View File

@@ -60,6 +60,12 @@ enum Cmd {
force: bool,
},
/// config.toml 관리 (스키마 마이그레이션 등).
Config {
#[command(subcommand)]
what: ConfigWhat,
},
/// Scan the workspace and ingest new/updated documents.
Ingest {
/// Workspace root override.
@@ -346,6 +352,16 @@ enum Cmd {
},
}
#[derive(Subcommand, Debug)]
enum ConfigWhat {
/// 기존 config.toml 을 새 스키마로 마이그레이션(빠진 섹션 추가 + 멱등 + .bak 백업).
Migrate {
/// 변경만 출력하고 파일은 수정하지 않는다.
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand, Debug)]
enum ListWhat {
/// List documents currently indexed.
@@ -1310,6 +1326,42 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
Ok(())
}
Cmd::Config { what } => match what {
ConfigWhat::Migrate { dry_run } => {
let report =
kebab_app::config_migrate_with_config_path(cli.config.as_deref(), *dry_run)?;
if cli.json {
println!(
"{}",
serde_json::to_string(&wire::wire_config_migration(&report))?
);
} else if !report.changed {
println!(
"config 이미 최신입니다 (schema v{}).",
report.to_schema_version
);
} else {
let verb = if report.dry_run { "변경 예정" } else { "적용됨" };
println!(
"config 마이그레이션 {verb}: v{} → v{} ({} changes)",
report.from_schema_version,
report.to_schema_version,
report.changes.len()
);
for c in &report.changes {
println!(" - [{:?}] {}{}", c.kind, c.path, c.detail);
}
if let Some(bak) = &report.backup_path {
println!("백업: {bak}");
}
if report.dry_run {
println!("(--dry-run: 파일 미수정. 적용하려면 --dry-run 없이 재실행)");
}
}
Ok(())
}
},
Cmd::Doctor => {
let report = kebab_app::doctor_with_config_path(cli.config.as_deref())?;
if cli.json {

View File

@@ -225,6 +225,12 @@ pub fn wire_bulk_search_item(item: &kebab_core::BulkSearchItem) -> Value {
v
}
/// `config_migration.v1` 직렬화. `ConfigMigrationReport` 가 `schema_version`
/// 필드를 자체 보유하므로(doctor 와 동일) 그대로 직렬화한다.
pub fn wire_config_migration(r: &kebab_app::ConfigMigrationReport) -> Value {
serde_json::to_value(r).expect("ConfigMigrationReport serializes")
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "config_migration.v1",
"description": "Result of `kebab config migrate` — schema reconciliation of a user's config.toml.",
"type": "object",
"required": [
"schema_version",
"config_path",
"dry_run",
"from_schema_version",
"to_schema_version",
"changed",
"changes"
],
"properties": {
"schema_version": { "const": "config_migration.v1" },
"config_path": { "type": "string" },
"dry_run": { "type": "boolean" },
"from_schema_version": { "type": "integer" },
"to_schema_version": { "type": "integer" },
"changed": { "type": "boolean" },
"backup_path": { "type": ["string", "null"] },
"changes": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "path", "detail"],
"properties": {
"kind": {
"enum": ["added_section", "added_key", "removed_deprecated"]
},
"path": { "type": "string" },
"detail": { "type": "string" }
}
}
}
}
}