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 1181Shell 82Rust 62Markdown 521515 Definitions

⎇ fe1601e669c584cd46f2d584416bc932f47b5a6f crates/hammer-cli/src/main.rs Rust · 187 lines · 6.8 KB History Who touched what raw

Fase 2 — overlay try/commit/discard/status sobre overlayfs fe1601e66 · 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] 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}