1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use caixa_core::{Caixa, LAYOUT_DIR_LIB};
use clap::Args;
/// Scaffold a new caixa.
///
/// Creates:
/// - `./<nome>/caixa.lisp` — generated from `Caixa::template`
/// - `./<nome>/lib/<nome>.lisp` — empty library entry
/// - `./<nome>/.gitignore`
#[derive(Args)]
pub struct Init {
/// The caixa's `:nome` (also the default directory name).
pub nome: String,
/// Scaffold into this path instead of `./<nome>`. Pass `.` to use CWD.
#[arg(long)]
pub path: Option<PathBuf>,
}
impl Init {
pub fn run(self) -> Result<()> {
// Gate the positional `<nome>` against the canonical DNS-1123
// label shape *before* any directory creation / file write —
// the bare arg is used verbatim as the target dir
// (`PathBuf::from(&self.nome)`), as the lisp filename inside
// `lib/<nome>.lisp`, and as the `:nome` slot in the scaffolded
// manifest. A path-traversal shape (`"../escape"`,
// `"lib/../escape"`) silently escapes the target dir at the
// `create_dir_all` / `write` calls below; a wrong-case /
// wrong-separator shape (`"MyCaixa"`, `"my_caixa"`) lands in
// the manifest and surfaces at `feira build` time, far from
// the source `feira init` invocation, with the diagnostic
// naming `:nome` rather than the `<nome>` positional. The
// lifted gate refuses every shape the typed-slot axes
// ([`caixa_core::ManifestError::NomeInvalid`] on the top-level
// `:nome`) already refuse, single-sourced through the lifted
// [`caixa_core::is_dns_1123_label`] predicate.
super::load::validate_nome_arg(&self.nome)?;
let root = self
.path
.clone()
.unwrap_or_else(|| PathBuf::from(&self.nome));
if root.exists() && !is_empty_dir(&root)? {
bail!("target path {} is not empty", root.display());
}
std::fs::create_dir_all(&root).with_context(|| format!("creating {}", root.display()))?;
let manifest_path = root.join("caixa.lisp");
if manifest_path.exists() {
bail!("{} already exists", manifest_path.display());
}
let manifest = Caixa::template(&self.nome);
std::fs::write(&manifest_path, &manifest)
.with_context(|| format!("writing {}", manifest_path.display()))?;
let lib_dir = root.join(LAYOUT_DIR_LIB);
std::fs::create_dir_all(&lib_dir)?;
let lib_entry = lib_dir.join(format!("{}.lisp", self.nome));
let lib_src = format!(
";; {nome} — library entry point.\n\
;;\n\
;; Declare your forms here. Anything imported via\n\
;; `(importar :caixa \"{nome}\")` starts at this file.\n",
nome = self.nome
);
std::fs::write(&lib_entry, lib_src)?;
let gi = root.join(".gitignore");
if !gi.exists() {
std::fs::write(&gi, "/target\n/result\n")?;
}
// Parse back as a sanity check that the template stays in sync with the
// manifest schema — any schema drift surfaces here, not at build time.
let parsed = Caixa::from_lisp(&manifest)
.context("generated caixa.lisp failed to parse; template is out of sync")?;
eprintln!(
"initialized caixa {} v{} in {}",
parsed.nome,
parsed.versao,
root.display()
);
Ok(())
}
}
fn is_empty_dir(p: &Path) -> Result<bool> {
Ok(p.read_dir()?.next().is_none())
}