sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ fe1601e669c584cd46f2d584416bc932f47b5a6f
crates/hammer-cli/src/main.rs
History
Who touched what
raw
Fase 2 — overlay try/commit/discard/status sobre overlayfs
1//! `hammer` — el punto de entrada de todo flujo manual. Ver `docs/`.
2//!
3//! Los subcomandos están mapeados a las fases del roadmap (`docs/10-roadmap.md`). Los que aún
4//! no están implementados devuelven un error claro indicando su fase, para que el esqueleto
5//! sea navegable desde el día uno.
6
7use std::path::PathBuf;
8
9use clap::{Parser, Subcommand};
10
11const DEFAULT_STORE: &str = "/store";
12
13#[derive(Parser)]
14#[command(
15 name = "hammer",
16 version,
17 about = "Laboratorio funcional en el sótano, terminal mutable arriba.",
18 long_about = "hammer: distro AI-nativa. Ver docs/ para el diseño completo."
19)]
20struct Cli {
21 /// Ruta del content-addressed store.
22 #[arg(long, default_value = DEFAULT_STORE, global = true)]
23 store: String,
24
25 #[command(subcommand)]
26 cmd: Cmd,
27}
28
29#[derive(Subcommand)]
30enum Cmd {
31 /// [Fase 0] Compila una receta y la sella en el store.
32 Build {
33 /// Ruta a la receta TOML.
34 recipe: String,
35 },
36 /// [Fase 1] Proyecta un artefacto del store al FHS (real o de overlay).
37 Hydrate {
38 /// Hash del artefacto (acepta prefijos cortos; con o sin `b3:`).
39 hash: String,
40 #[arg(long, default_value = "/")]
41 into: String,
42 /// Modo de enlazado: `static` (hardlink directo) | `dynamic` (requiere patchelf).
43 #[arg(long, default_value = "static")]
44 link: String,
45 },
46 /// [Fase 2] Monta un overlay de experimentación sobre los directorios del sistema.
47 /// Sin targets → cubre el FHS clásico (/usr/bin, /bin, /usr/lib, /lib, /etc, …).
48 Try {
49 /// Directorios a poner bajo overlay. Si vacío, usa la lista por defecto.
50 targets: Vec<PathBuf>,
51 /// Raíz donde viven los manifiestos y los upper/work. Default `/var/lib/hammer/overlays`.
52 #[arg(long)]
53 state_root: Option<PathBuf>,
54 },
55 /// [Fase 2] Fusiona un overlay al FHS real y desmonta. Sin diario aún (Fase 3).
56 Commit {
57 id: String,
58 #[arg(long)]
59 state_root: Option<PathBuf>,
60 },
61 /// [Fase 2] Desmonta un overlay y borra su upper. El sistema vuelve al estado base.
62 Discard {
63 id: String,
64 #[arg(long)]
65 state_root: Option<PathBuf>,
66 },
67 /// [Fase 2] Lista los overlays activos según los manifiestos persistidos.
68 Status {
69 #[arg(long)]
70 state_root: Option<PathBuf>,
71 },
72 /// [Fase 3] Ver/seguir el diario de mutaciones.
73 Journal,
74 /// [Fase 4] Aplica un manifiesto .swm (reproduce y lo deja en un overlay).
75 Apply { file: String },
76 /// [Fase 4] Exporta el delta del sistema como manifiesto .swm.
77 Export {
78 #[arg(long)]
79 base: Option<String>,
80 },
81 /// [Fase 5] Envía un comando al init (proxy a /run/init.control).
82 Ctl { line: String },
83}
84
85fn main() -> anyhow::Result<()> {
86 tracing_subscriber::fmt()
87 .with_writer(std::io::stderr)
88 .with_env_filter(
89 tracing_subscriber::EnvFilter::try_from_default_env()
90 .unwrap_or_else(|_| "info".into()),
91 )
92 .init();
93
94 let cli = Cli::parse();
95
96 // Sólo las cmds que de verdad tocan artefactos abren (y posiblemente crean) el store.
97 // Las cmds de overlay/diario/apply pueden correr en sistemas sin store local todavía.
98 match cli.cmd {
99 Cmd::Build { recipe } => {
100 let store = hammer_core::Store::open(&cli.store)?;
101 let recipe = hammer_core::Recipe::load_from_path(&recipe)?;
102 let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
103 let hash = hammer_build::build(&recipe, &cfg, &store)?;
104 println!("{hash}");
105 }
106 Cmd::Hydrate { hash, into, link } => {
107 let store = hammer_core::Store::open(&cli.store)?;
108 let mode = match link.as_str() {
109 "static" => hammer_core::LinkMode::Static,
110 "dynamic" => hammer_core::LinkMode::Dynamic,
111 other => anyhow::bail!("--link debe ser 'static' o 'dynamic', no '{other}'"),
112 };
113 let artifact_dir = store.find_by_hash(&hash)?;
114 let report = hammer_build::run_hydrate(
115 &artifact_dir,
116 std::path::Path::new(&into),
117 mode,
118 )?;
119 println!(
120 "hydrated {} archivo(s) ({} dir(s), {} symlink(s)) → {}",
121 report.files.len(),
122 report.dirs_created,
123 report.symlinks,
124 into
125 );
126 for f in &report.files {
127 println!(" {}", f.dst.display());
128 }
129 }
130 Cmd::Try { targets, state_root } => {
131 let root = state_root
132 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
133 std::fs::create_dir_all(&root)?;
134 let id = hammer_overlay::try_overlay(&targets, &root)?;
135 println!("{id}");
136 }
137 Cmd::Commit { id, state_root } => {
138 let root = state_root
139 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
140 let report = hammer_overlay::commit(&hammer_overlay::OverlayId(id), &root)?;
141 println!(
142 "commit OK — {} archivo(s) promocionados, {} eliminado(s)",
143 report.copied.len(),
144 report.removed.len()
145 );
146 }
147 Cmd::Discard { id, state_root } => {
148 let root = state_root
149 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
150 hammer_overlay::discard(&hammer_overlay::OverlayId(id), &root)?;
151 println!("discard OK");
152 }
153 Cmd::Status { state_root } => {
154 let root = state_root
155 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
156 let overlays = hammer_overlay::status(&root)?;
157 if overlays.is_empty() {
158 println!("(sin overlays activos)");
159 } else {
160 for o in overlays {
161 println!(
162 "{} created={} mounts={}",
163 o.id,
164 o.created_at,
165 o.mounts.len()
166 );
167 for m in &o.mounts {
168 println!(" └─ {} upper={}", m.target.display(), m.upper.display());
169 }
170 }
171 }
172 }
173 Cmd::Journal => {
174 println!("[fase 3 pendiente] diario — ver docs/05-journal.md");
175 }
176 Cmd::Apply { file } => {
177 println!("[fase 4 pendiente] apply {file} — ver docs/06-swm-format.md");
178 }
179 Cmd::Export { base } => {
180 println!("[fase 4 pendiente] export base={base:?} — ver docs/05-journal.md");
181 }
182 Cmd::Ctl { line } => {
183 println!("[fase 5 pendiente] ctl {line:?} — ver docs/07-agent-bus.md");
184 }
185 }
186 Ok(())
187}