use std::fmt;
use camino::Utf8PathBuf;
use owo_colors::OwoColorize;
use crate::applied::AppliedState;
use crate::config::GlobalConfig;
use crate::error::{Error, Result};
use crate::ui;
use super::resolve_pj_root;
pub fn run(at: Option<Utf8PathBuf>, all: bool, paths: bool, no_color: bool) -> Result<()> {
if all {
return run_all(paths, no_color);
}
run_single(at, no_color)
}
fn run_single(at: Option<Utf8PathBuf>, no_color: bool) -> Result<()> {
let explicit_at = at.is_some();
let cwd = resolve_pj_root(at)?;
let pj_root = match crate::paths::find_pj_root(&cwd) {
Some(p) => p,
None if explicit_at => {
return Err(Error::Config(format!(
"no .kata/applied.toml found at or above {cwd}; run `kata init` first"
)));
}
None => return run_single_pick_from_registry(no_color),
};
let applied = AppliedState::load(&pj_root)?;
println!("project: {pj_root}");
if let Some(p) = &applied.preset {
println!("preset: {p}");
}
println!();
println!("templates ({} applied):", applied.templates.len());
for t in &applied.templates {
let v = t.version.as_deref().unwrap_or("-");
println!(" - {} @ {} (manifest version: {})", t.source, t.rev, v);
}
println!();
println!("vars:");
for (k, v) in &applied.vars {
println!(" {k} = {v}");
}
println!();
println!("files ({} tracked):", applied.files.len());
for (k, fs) in &applied.files {
let once = if fs.once_applied { " (once)" } else { "" };
println!(" - {k}{once}");
}
Ok(())
}
fn run_all(show_paths: bool, no_color: bool) -> Result<()> {
let config = GlobalConfig::load()?;
if config.projects.is_empty() {
println!(
"no projects registered yet — `kata register` from inside a kata-managed PJ to add one."
);
return Ok(());
}
let rows: Vec<RegistryRow> = config
.projects
.iter()
.map(RegistryRow::from_entry)
.collect();
let color = ui::color_enabled(no_color);
let name_w = rows.iter().map(|r| r.name.len()).max().unwrap_or(4).max(4);
let path_w = rows.iter().map(|r| r.path.len()).max().unwrap_or(4).max(4);
let preset_w = rows
.iter()
.map(|r| r.preset.len())
.max()
.unwrap_or(6)
.max(6);
let templates_w = 9; let applied_w = rows
.iter()
.map(|r| r.applied_at.len())
.max()
.unwrap_or(7)
.max(7);
let mut header: Vec<(&str, usize)> = vec![("NAME", name_w)];
if show_paths {
header.push(("PATH", path_w));
}
header.extend([
("PRESET", preset_w),
("TEMPLATES", templates_w),
("APPLIED", applied_w),
("STATUS", 0),
]);
ui::print_table_header(&header, no_color);
for r in &rows {
let mut cells = vec![format!("{:<name_w$}", r.name, name_w = name_w)];
if show_paths {
cells.push(if color {
format!("{:<path_w$}", r.path, path_w = path_w)
.dimmed()
.to_string()
} else {
format!("{:<path_w$}", r.path, path_w = path_w)
});
}
cells.push(if color {
format!("{:<preset_w$}", r.preset, preset_w = preset_w)
.dimmed()
.to_string()
} else {
format!("{:<preset_w$}", r.preset, preset_w = preset_w)
});
cells.push(format!(
"{:<templates_w$}",
r.templates,
templates_w = templates_w
));
cells.push(if color {
format!("{:<applied_w$}", r.applied_at, applied_w = applied_w)
.dimmed()
.to_string()
} else {
format!("{:<applied_w$}", r.applied_at, applied_w = applied_w)
});
cells.push(ui::format_status_cell(&r.status, no_color));
println!("{}", cells.join(" "));
}
Ok(())
}
struct ProjectChoice {
name: String,
path: Utf8PathBuf,
}
impl fmt::Display for ProjectChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.path)
}
}
fn run_single_pick_from_registry(no_color: bool) -> Result<()> {
let config = GlobalConfig::load()?;
if config.projects.is_empty() {
return Err(Error::Config(
"no .kata/applied.toml in the current directory's hierarchy and no projects in the global registry — \
cd into a kata-managed PJ, or run `kata init` first."
.into(),
));
}
let choices: Vec<ProjectChoice> = config
.projects
.into_iter()
.map(|p| ProjectChoice {
name: p.name,
path: p.path,
})
.collect();
let chosen = inquire::Select::new("pick a project to inspect:", choices)
.with_help_message("\u{2191}\u{2193} to move, Enter to confirm, Esc to cancel")
.prompt()
.map_err(|e| match e {
inquire::InquireError::OperationCanceled
| inquire::InquireError::OperationInterrupted => Error::Config("cancelled".into()),
other => Error::Config(format!("prompt failed: {other}")),
})?;
run_single(Some(chosen.path), no_color)
}
struct RegistryRow {
name: String,
path: String,
preset: String,
templates: String,
applied_at: String,
status: String,
}
impl RegistryRow {
fn from_entry(entry: &crate::config::ProjectEntry) -> Self {
let path = entry.path.as_str().to_string();
if !entry.path.exists() {
return Self {
name: entry.name.clone(),
path,
preset: "-".into(),
templates: "-".into(),
applied_at: "-".into(),
status: "missing dir".into(),
};
}
match AppliedState::load(&entry.path) {
Ok(applied) if applied.templates.is_empty() => Self {
name: entry.name.clone(),
path,
preset: "(none)".into(),
templates: "0".into(),
applied_at: "never".into(),
status: "not init'd".into(),
},
Ok(applied) => {
let preset = applied
.preset
.clone()
.unwrap_or_else(|| "(none)".to_string());
let applied_at = applied
.applied_at
.map(|t| format!("{t}"))
.unwrap_or_else(|| "never".to_string());
Self {
name: entry.name.clone(),
path,
preset,
templates: applied.templates.len().to_string(),
applied_at,
status: "ok".into(),
}
}
Err(e) => Self {
name: entry.name.clone(),
path,
preset: "-".into(),
templates: "-".into(),
applied_at: "-".into(),
status: format!("error: {e}"),
},
}
}
}