1use anyhow::Result;
5use serde::Serialize;
6use std::path::{Path, PathBuf};
7
8#[derive(Serialize)]
9struct ProjectRow {
10 name: String,
11 slug: String,
12 path: String,
13 status: &'static str,
14}
15
16pub fn cmd_projects_list(json: bool, include_missing: bool) -> Result<()> {
17 print!("{}", projects_text(json, include_missing)?);
18 Ok(())
19}
20
21pub fn projects_text(json: bool, include_missing: bool) -> Result<String> {
22 let rows = project_rows(include_missing)?;
23 if json {
24 return Ok(format!("{}\n", serde_json::to_string_pretty(&rows)?));
25 }
26 Ok(render_table(&rows))
27}
28
29fn project_rows(include_missing: bool) -> Result<Vec<ProjectRow>> {
30 let paths = if include_missing {
31 crate::core::machine_registry::list_paths_including_missing()?
32 } else {
33 crate::core::machine_registry::list_paths()?
34 };
35 Ok(paths.into_iter().map(project_row).collect())
36}
37
38fn project_row(path: PathBuf) -> ProjectRow {
39 ProjectRow {
40 name: segment(&path).to_string(),
41 slug: crate::core::paths::workspace_slug(&path),
42 status: status(&path),
43 path: path.to_string_lossy().to_string(),
44 }
45}
46
47fn status(path: &Path) -> &'static str {
48 if path.is_dir() {
49 "available"
50 } else {
51 "missing"
52 }
53}
54
55fn render_table(rows: &[ProjectRow]) -> String {
56 if rows.is_empty() {
57 return "no registered projects (run kaizen init inside a workspace first)\n".into();
58 }
59 let header = format!("{:<24} {:<40} {:<10} PATH", "NAME", "SLUG", "STATUS");
60 let body = rows.iter().map(table_row).collect::<Vec<_>>().join("\n");
61 format!("{header}\n{}\n{body}\n", "-".repeat(header.len()))
62}
63
64fn table_row(row: &ProjectRow) -> String {
65 format!(
66 "{:<24} {:<40} {:<10} {}",
67 row.name, row.slug, row.status, row.path
68 )
69}
70
71fn segment(path: &Path) -> &str {
72 path.file_name()
73 .and_then(|name| name.to_str())
74 .unwrap_or("?")
75}