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…
⎇ a87b16dd852a3a6e7be7ad0f080b3ad97c75fdbc
crates/hammer-cli/src/main.rs
History
Who touched what
raw
Fase 3 — diario de mutaciones (hammer-journal + fanotify + hammer journal)
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+3] Fusiona un overlay al FHS real y desmonta. Por defecto registra cada
56 /// archivo promocionado en el diario; usa `--no-journal` para saltarse el registro.
57 Commit {
58 id: String,
59 #[arg(long)]
60 state_root: Option<PathBuf>,
61 /// Directorio del diario donde anotar la promoción.
62 #[arg(long, default_value = "/var/lib/hammer/journal")]
63 journal: PathBuf,
64 /// Saltarse el registro en el diario (útil para experimentos efímeros).
65 #[arg(long)]
66 no_journal: bool,
67 },
68 /// [Fase 2] Desmonta un overlay y borra su upper. El sistema vuelve al estado base.
69 Discard {
70 id: String,
71 #[arg(long)]
72 state_root: Option<PathBuf>,
73 },
74 /// [Fase 2] Lista los overlays activos según los manifiestos persistidos.
75 Status {
76 #[arg(long)]
77 state_root: Option<PathBuf>,
78 },
79 /// [Fase 3] Ver/seguir el diario de mutaciones.
80 Journal {
81 /// Directorio del diario (mismo que `hammerd --journal`).
82 #[arg(long, default_value = "/var/lib/hammer/journal")]
83 dir: PathBuf,
84 /// Muestra los últimos N eventos. Default: 20.
85 #[arg(long, short, default_value = "20")]
86 tail: usize,
87 /// Sigue el archivo: imprime nuevos eventos según aparecen.
88 #[arg(long, short)]
89 follow: bool,
90 /// Formato. `pretty` (humano, default) o `json` (raw passthrough).
91 #[arg(long, default_value = "pretty")]
92 format: String,
93 },
94 /// [Fase 4] Aplica un manifiesto .swm (reproduce y lo deja en un overlay).
95 Apply { file: String },
96 /// [Fase 4] Exporta el delta del sistema como manifiesto .swm.
97 Export {
98 #[arg(long)]
99 base: Option<String>,
100 },
101 /// [Fase 5] Envía un comando al init (proxy a /run/init.control).
102 Ctl { line: String },
103}
104
105fn print_event(ev: &hammer_journal::MutationEvent, format: &str) {
106 match format {
107 "json" => {
108 // Re-serializa. No imprimimos la línea original (no la tenemos a mano), pero el
109 // formato es estable porque MutationEvent es la fuente de verdad del schema.
110 match serde_json::to_string(ev) {
111 Ok(s) => println!("{s}"),
112 Err(e) => eprintln!("journal: error serializando: {e}"),
113 }
114 }
115 _ => {
116 let actor = match &ev.by.source {
117 hammer_journal::Source::HammerHydrate { artifact } => format!("hydrate {artifact}"),
118 hammer_journal::Source::HammerCommit { overlay, artifact } => match artifact {
119 Some(a) => format!("commit {overlay} ({a})"),
120 None => format!("commit {overlay}"),
121 },
122 hammer_journal::Source::External => {
123 let pid = ev.by.pid.map(|p| format!("pid={p}")).unwrap_or_default();
124 let uid = ev.by.uid.map(|u| format!("uid={u}")).unwrap_or_default();
125 format!("external {pid} {uid}").trim().to_string()
126 }
127 };
128 println!(
129 "{ts} {op:<7} {path} ← {actor}",
130 ts = ev.ts,
131 op = ev.op.as_str(),
132 path = ev.path.display(),
133 );
134 }
135 }
136}
137
138fn main() -> anyhow::Result<()> {
139 tracing_subscriber::fmt()
140 .with_writer(std::io::stderr)
141 .with_env_filter(
142 tracing_subscriber::EnvFilter::try_from_default_env()
143 .unwrap_or_else(|_| "info".into()),
144 )
145 .init();
146
147 let cli = Cli::parse();
148
149 // Sólo las cmds que de verdad tocan artefactos abren (y posiblemente crean) el store.
150 // Las cmds de overlay/diario/apply pueden correr en sistemas sin store local todavía.
151 match cli.cmd {
152 Cmd::Build { recipe } => {
153 let store = hammer_core::Store::open(&cli.store)?;
154 let recipe = hammer_core::Recipe::load_from_path(&recipe)?;
155 let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
156 let hash = hammer_build::build(&recipe, &cfg, &store)?;
157 println!("{hash}");
158 }
159 Cmd::Hydrate { hash, into, link } => {
160 let store = hammer_core::Store::open(&cli.store)?;
161 let mode = match link.as_str() {
162 "static" => hammer_core::LinkMode::Static,
163 "dynamic" => hammer_core::LinkMode::Dynamic,
164 other => anyhow::bail!("--link debe ser 'static' o 'dynamic', no '{other}'"),
165 };
166 let artifact_dir = store.find_by_hash(&hash)?;
167 let report = hammer_build::run_hydrate(
168 &artifact_dir,
169 std::path::Path::new(&into),
170 mode,
171 )?;
172 println!(
173 "hydrated {} archivo(s) ({} dir(s), {} symlink(s)) → {}",
174 report.files.len(),
175 report.dirs_created,
176 report.symlinks,
177 into
178 );
179 for f in &report.files {
180 println!(" {}", f.dst.display());
181 }
182 }
183 Cmd::Try { targets, state_root } => {
184 let root = state_root
185 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
186 std::fs::create_dir_all(&root)?;
187 let id = hammer_overlay::try_overlay(&targets, &root)?;
188 println!("{id}");
189 }
190 Cmd::Commit { id, state_root, journal, no_journal } => {
191 let root = state_root
192 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
193 let id = hammer_overlay::OverlayId(id);
194 let report = if no_journal {
195 hammer_overlay::commit(&id, &root)?
196 } else {
197 let j = hammer_journal::Journal::open(&journal)?;
198 hammer_overlay::commit_with_journal(&id, &root, &j)?
199 };
200 println!(
201 "commit OK — {} archivo(s) promocionados, {} eliminado(s){}",
202 report.copied.len(),
203 report.removed.len(),
204 if no_journal { "" } else { " (anotado en el diario)" }
205 );
206 }
207 Cmd::Discard { id, state_root } => {
208 let root = state_root
209 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
210 hammer_overlay::discard(&hammer_overlay::OverlayId(id), &root)?;
211 println!("discard OK");
212 }
213 Cmd::Status { state_root } => {
214 let root = state_root
215 .unwrap_or_else(|| PathBuf::from(hammer_overlay::DEFAULT_STATE_ROOT));
216 let overlays = hammer_overlay::status(&root)?;
217 if overlays.is_empty() {
218 println!("(sin overlays activos)");
219 } else {
220 for o in overlays {
221 println!(
222 "{} created={} mounts={}",
223 o.id,
224 o.created_at,
225 o.mounts.len()
226 );
227 for m in &o.mounts {
228 println!(" └─ {} upper={}", m.target.display(), m.upper.display());
229 }
230 }
231 }
232 }
233 Cmd::Journal { dir, tail, follow, format } => {
234 let j = hammer_journal::Journal::open(&dir)?;
235 let initial = j.tail(tail)?;
236 for ev in &initial {
237 print_event(ev, &format);
238 }
239 if follow {
240 use std::io::Write;
241 let _ = std::io::stdout().flush();
242 let mut cursor = j.end_offset()?;
243 loop {
244 std::thread::sleep(std::time::Duration::from_millis(500));
245 let (news, new_cursor) = j.read_from_offset(cursor)?;
246 cursor = new_cursor;
247 for ev in &news {
248 print_event(ev, &format);
249 }
250 let _ = std::io::stdout().flush();
251 }
252 }
253 }
254 Cmd::Apply { file } => {
255 println!("[fase 4 pendiente] apply {file} — ver docs/06-swm-format.md");
256 }
257 Cmd::Export { base } => {
258 println!("[fase 4 pendiente] export base={base:?} — ver docs/05-journal.md");
259 }
260 Cmd::Ctl { line } => {
261 println!("[fase 5 pendiente] ctl {line:?} — ver docs/07-agent-bus.md");
262 }
263 }
264 Ok(())
265}