@@ -23554,10 +23554,64 @@ unsafe fn c_arg(argv: *const *const u8, i: i32) -> String {
2355423554 }
2355523555}
2355623556
23557+/// Minimal ANSI styling for `db info`, applied only on a TTY (plain otherwise,
23558+/// so piped output stays clean).
23559+#[derive(Clone, Copy)]
23560+struct Style {
23561+ tty: bool,
23562+}
23563+
23564+impl Style {
23565+ fn detect() -> Self {
23566+ Style {
23567+ tty: std::io::IsTerminal::is_terminal(&std::io::stdout()),
23568+ }
23569+ }
23570+ fn wrap(&self, code: &str, s: &str) -> String {
23571+ if self.tty {
23572+ format!("\x1b[{}m{}\x1b[0m", code, s)
23573+ } else {
23574+ s.to_string()
23575+ }
23576+ }
23577+ fn bold(&self, s: &str) -> String {
23578+ self.wrap("1", s)
23579+ }
23580+ fn dim(&self, s: &str) -> String {
23581+ self.wrap("2", s)
23582+ }
23583+ fn cyan(&self, s: &str) -> String {
23584+ self.wrap("36", s)
23585+ }
23586+ fn green(&self, s: &str) -> String {
23587+ self.wrap("32", s)
23588+ }
23589+ fn yellow(&self, s: &str) -> String {
23590+ self.wrap("33", s)
23591+ }
23592+}
23593+
23594+/// Human-readable byte size (e.g. `56.0 KB`).
23595+fn human_bytes(n: u64) -> String {
23596+ const KB: u64 = 1024;
23597+ const MB: u64 = KB * 1024;
23598+ const GB: u64 = MB * 1024;
23599+ if n >= GB {
23600+ format!("{:.1} GB", n as f64 / GB as f64)
23601+ } else if n >= MB {
23602+ format!("{:.1} MB", n as f64 / MB as f64)
23603+ } else if n >= KB {
23604+ format!("{:.1} KB", n as f64 / KB as f64)
23605+ } else {
23606+ format!("{} B", n)
23607+ }
23608+}
23609+
2355723610/// `<program> db info` — print a human-readable report of the database in knot
2355823611/// terms: file summary, then per source its current schema descriptor (the db's
2355923612/// actual state), the full baked-in migration chain, and the row count.
2356023613fn db_info(db_path: &str) {
23614+ let st = Style::detect();
2356123615 let conn = match Connection::open(db_path) {
2356223616 Ok(c) => c,
2356323617 Err(e) => {
@@ -23566,7 +23620,7 @@ fn db_info(db_path: &str) {
2356623620 }
2356723621 };
2356823622
23569− // File summary.
23623+ // ── Header: file summary ──
2357023624 let size = std::fs::metadata(db_path).map(|m| m.len()).unwrap_or(0);
2357123625 let pages: i64 = conn
2357223626 .query_row("PRAGMA page_count", [], |r| r.get(0))
@@ -23577,19 +23631,26 @@ fn db_info(db_path: &str) {
2357723631 let journal: String = conn
2357823632 .query_row("PRAGMA journal_mode", [], |r| r.get(0))
2357923633 .unwrap_or_default();
23634+ println!("{}", st.bold(db_path));
2358023635 println!(
23581− "{} ({} bytes, {} pages × {} B, {})",
23582− db_path, size, pages, page_size, journal
23636+ "{}",
23637+ st.dim(&format!(
23638+ "{} · {} pages × {} B · {}",
23639+ human_bytes(size),
23640+ pages,
23641+ page_size,
23642+ journal
23643+ ))
2358323644 );
2358423645 println!();
2358523646
23586− // Per-source: name, descriptor, migration cursor, row count.
23647+ // ── Sources ──
2358723648 let mut stmt = match conn
2358823649 .prepare("SELECT name, schema, applied FROM _knot_schema ORDER BY name")
2358923650 {
2359023651 Ok(s) => s,
2359123652 Err(_) => {
23592− println!("(no sources — empty database)");
23653+ println!("{}", st.dim("(empty database — no sources)"));
2359323654 return;
2359423655 }
2359523656 };
@@ -23599,29 +23660,54 @@ fn db_info(db_path: &str) {
2359923660 .unwrap_or_default();
2360023661
2360123662 if sources.is_empty() {
23602− println!("(no sources — empty database)");
23663+ println!("{}", st.dim("(empty database — no sources)"));
2360323664 return;
2360423665 }
2360523666
23667+ println!("{}", st.bold(&format!("{} source{}", sources.len(), if sources.len() == 1 { "" } else { "s" })));
23668+ println!();
23669+
2360623670 for (name, schema, applied) in &sources {
2360723671 let table = quote_ident(&format!("_knot_{}", name));
2360823672 let rows: i64 = conn
2360923673 .query_row(&format!("SELECT count(*) FROM {}", table), [], |r| r.get(0))
2361023674 .unwrap_or(0);
23611− println!("source *{} — {} row{}, migration {}", name, rows, if rows == 1 { "" } else { "s" }, applied);
23612− println!(" {}", schema);
2361323675
23676+ // Source header: *name, row count, migration position.
23677+ println!(
23678+ "{} {}",
23679+ st.cyan(&format!("*{}", name)),
23680+ st.dim(&format!(
23681+ "{} row{} · migration {}",
23682+ rows,
23683+ if rows == 1 { "" } else { "s" },
23684+ applied
23685+ ))
23686+ );
23687+ // Current descriptor (the db's actual state).
23688+ println!("{}", schema);
23689+
23690+ // The full migration chain, each step marked applied / pending.
2361423691 let chain = migration_chain_for(name);
23615− if chain.is_empty() {
23616− println!(" migrations: none recorded");
23617− } else {
23618− println!(" migrations:");
23619− for step in &chain {
23620− let marker = if step.step_idx < *applied { "✓" } else { " " };
23621− println!(
23622− " {} step {}: {} → {}",
23623− marker, step.step_idx, step.from_schema, step.to_schema
23624− );
23692+ if !chain.is_empty() {
23693+ for (i, step) in chain.iter().enumerate() {
23694+ let last = i + 1 == chain.len();
23695+ let connector = if last { "└─" } else { "├─" };
23696+ if step.step_idx < *applied {
23697+ println!(
23698+ "{} {} {}",
23699+ st.dim(connector),
23700+ st.green("applied"),
23701+ st.dim(&format!("{} → {}", step.from_schema, step.to_schema))
23702+ );
23703+ } else {
23704+ println!(
23705+ "{} {} {}",
23706+ st.dim(connector),
23707+ st.yellow("pending"),
23708+ format!("{} → {}", step.from_schema, step.to_schema)
23709+ );
23710+ }
2362523711 }
2362623712 }
2362723713 println!();