Skip to main content

grex_cli/cli/verbs/
init.rs

1//! `grex init` — bootstrap a meta-pack at the given path (or cwd).
2//!
3//! Writes the minimal v1 manifest skeleton (`schema_version: "1"`,
4//! `name`, `type: meta`, empty `actions` + `children`) to
5//! `<path>/.grex/pack.yaml`. Refuses to overwrite an existing manifest.
6
7use crate::cli::args::{GlobalFlags, InitArgs};
8use anyhow::Result;
9use std::path::{Path, PathBuf};
10use tokio_util::sync::CancellationToken;
11
12/// Derive a pack name from a directory path that satisfies the v1
13/// validator `^[a-z][a-z0-9-]*$`. Non-conforming characters become `-`;
14/// leading non-alphabetic chars are trimmed; an empty result falls back
15/// to `"workspace"`.
16fn derive_pack_name(dir: &Path) -> String {
17    let raw = dir.file_name().and_then(|s| s.to_str()).unwrap_or("workspace");
18    let mut out = String::with_capacity(raw.len());
19    for ch in raw.chars() {
20        let c = ch.to_ascii_lowercase();
21        if c.is_ascii_lowercase() || c.is_ascii_digit() {
22            out.push(c);
23        } else {
24            out.push('-');
25        }
26    }
27    while !out.is_empty() && !out.chars().next().unwrap_or('-').is_ascii_lowercase() {
28        out.remove(0);
29    }
30    if out.is_empty() {
31        "workspace".to_string()
32    } else {
33        out
34    }
35}
36
37fn minimal_pack_yaml(name: &str) -> String {
38    format!("schema_version: \"1\"\nname: {name}\ntype: meta\nactions: []\nchildren: []\n")
39}
40
41pub fn run(args: InitArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
42    match run_impl(args.path, global.json) {
43        Outcome::Ok => Ok(()),
44        Outcome::AlreadyInitialized => std::process::exit(1),
45        Outcome::Io => std::process::exit(2),
46    }
47}
48
49enum Outcome {
50    Ok,
51    AlreadyInitialized,
52    Io,
53}
54
55fn run_impl(path: Option<PathBuf>, json: bool) -> Outcome {
56    let dir = match path {
57        Some(p) => p,
58        None => match std::env::current_dir() {
59            Ok(cwd) => cwd,
60            Err(err) => {
61                emit_error(json, "io", &format!("resolve cwd: {err}"));
62                return Outcome::Io;
63            }
64        },
65    };
66
67    if let Err(err) = std::fs::create_dir_all(&dir) {
68        emit_error(json, "io", &format!("create workspace dir {}: {err}", dir.display()));
69        return Outcome::Io;
70    }
71
72    let grex_dir = dir.join(".grex");
73    let manifest_path = grex_dir.join("pack.yaml");
74    if manifest_path.exists() {
75        emit_error(json, "already_initialized", &format!("{} already initialized", dir.display()));
76        return Outcome::AlreadyInitialized;
77    }
78
79    if let Err(err) = std::fs::create_dir_all(&grex_dir) {
80        emit_error(json, "io", &format!("create {}: {err}", grex_dir.display()));
81        return Outcome::Io;
82    }
83
84    let name = derive_pack_name(&dir);
85    if let Err(err) = std::fs::write(&manifest_path, minimal_pack_yaml(&name)) {
86        emit_error(json, "io", &format!("write {}: {err}", manifest_path.display()));
87        return Outcome::Io;
88    }
89
90    emit_ok(json, &dir, &manifest_path);
91    Outcome::Ok
92}
93
94fn emit_ok(json: bool, dir: &Path, manifest: &Path) {
95    if json {
96        let doc = serde_json::json!({
97            "verb": "init",
98            "status": "ok",
99            "path": dir.display().to_string(),
100            "manifest": manifest.display().to_string(),
101        });
102        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
103    } else {
104        println!("grex init: wrote {}", manifest.display());
105    }
106}
107
108fn emit_error(json: bool, kind: &str, msg: &str) {
109    if json {
110        let doc = serde_json::json!({
111            "verb": "init",
112            "error": { "kind": kind, "message": msg },
113        });
114        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
115    } else {
116        eprintln!("grex init: {msg}");
117    }
118}