Skip to main content

kaizen/shell/
projects.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Registered workspace listing.
3
4use 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}