tawasuyu

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…

TOML 1572Shell 203Rust 109Markdown 993728 Definitions

⎇ a87b16dd852a3a6e7be7ad0f080b3ad97c75fdbc crates/hammer-cli/src/main.rs Rust · 265 lines · 10.2 KB History Who touched what raw

Fase 3 — diario de mutaciones (hammer-journal + fanotify + hammer journal) a87b16dd8 · Sergio ·

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}