gioser

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114554 Definitions

⎇ e77a32f4d6a09fe29701811f4d2e60dd2cb678db crates/modules/semantic_dht/minga-cli/src/commands.rs Rust · 286 lines · 9.4 KB History Who touched what raw

feat(minga): minga-vfs — proyecta el repo como filesystem FUSE e77a32f4d · sergio ·

1//! Implementaciones de los subcomandos. Funciones puras que retornan
2//! datos estructurados — el binario las llama y formatea la salida.
3//!
4//! Layout en disco bajo `repo_path/`:
5//! - `keypair`     — la `Keypair` del peer cifrada con passphrase.
6//! - `repo/`       — directorio sled con `nodes`, `attestations`, `mst`.
7
8use std::fs;
9use std::path::Path;
10use std::time::Duration;
11
12use libp2p::{multiaddr::Protocol, Multiaddr, PeerId};
13use minga_core::{parse, Attestation, ContentHash, Did, Keypair};
14use minga_p2p::MingaPeer;
15use minga_store::{keypair_file, PersistentRepo};
16
17use crate::error::CliError;
18
19pub const KEYPAIR_FILENAME: &str = "keypair";
20pub const REPO_DIRNAME: &str = "repo";
21
22#[derive(Debug, Clone)]
23pub struct RepoStatus {
24    pub did: Did,
25    pub mst_len: usize,
26    pub nodes_len: usize,
27    pub attestations_len: usize,
28}
29
30#[derive(Debug, Clone)]
31pub struct IngestResult {
32    pub hash: ContentHash,
33    pub did: Did,
34}
35
36/// `minga init`: genera un keypair fresco, crea el repo persistente,
37/// y guarda el keypair cifrado.
38pub fn cmd_init(repo_path: &Path, passphrase: &str) -> Result<Did, CliError> {
39    if repo_path.exists() {
40        // Si el directorio existe pero está vacío, lo aceptamos.
41        // Si tiene cualquier cosa, abortamos para no pisar un repo.
42        let mut entries = fs::read_dir(repo_path)?;
43        if entries.next().is_some() {
44            return Err(CliError::AlreadyExists(repo_path.to_path_buf()));
45        }
46    } else {
47        fs::create_dir_all(repo_path)?;
48    }
49
50    let keypair = Keypair::generate();
51    keypair_file::save(&keypair, repo_path.join(KEYPAIR_FILENAME), passphrase)?;
52
53    // Crear el repo sled vacío. Se cierra al final del scope; el
54    // siguiente comando lo reabre.
55    let _ = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
56
57    Ok(keypair.did())
58}
59
60/// `minga status`: descifra el keypair, abre el repo, devuelve
61/// estadísticas básicas.
62pub fn cmd_status(repo_path: &Path, passphrase: &str) -> Result<RepoStatus, CliError> {
63    let keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
64    let repo = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
65
66    Ok(RepoStatus {
67        did: keypair.did(),
68        mst_len: repo.mst.len(),
69        nodes_len: repo.nodes.len(),
70        attestations_len: repo.attestations.len(),
71    })
72}
73
74/// `minga ingest <file>`: parsea un archivo Rust con tree-sitter,
75/// inserta el AST en el store, lo añade al MST, y crea una atestación
76/// firmada por el dueño del keypair (auto-firma de autoría).
77pub fn cmd_ingest(
78    repo_path: &Path,
79    passphrase: &str,
80    file: &Path,
81) -> Result<IngestResult, CliError> {
82    let keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
83    let repo = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
84
85    let source = fs::read_to_string(file)?;
86    let dialect = detect_dialect(file)?;
87    let node = dialect.parse(&source)?;
88    let hash = repo.nodes.put(&node)?;
89    repo.mst.insert(hash)?;
90    repo.attestations
91        .add(Attestation::create(&keypair, hash))?;
92    repo.flush()?;
93
94    Ok(IngestResult {
95        hash,
96        did: keypair.did(),
97    })
98}
99
100/// `minga mount <punto>`: monta el repositorio como un filesystem FUSE
101/// de sólo lectura. Cada hash del store se vuelve un archivo
102/// navegable con `ls`/`cat`. Bloquea hasta que se desmonte el punto
103/// (`fusermount -u <punto>` o una señal al proceso).
104pub fn cmd_mount(
105    repo_path: &Path,
106    passphrase: &str,
107    mountpoint: &Path,
108) -> Result<(), CliError> {
109    // Cargar el keypair valida la passphrase: montar es navegar el
110    // repo, así que pedimos la misma credencial que `status`/`watch`.
111    let _keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
112    let repo = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
113    minga_vfs::mount(minga_vfs::RepoSource::new(repo), mountpoint)?;
114    Ok(())
115}
116
117/// Detecta el dialecto desde la extensión del archivo. Error si la
118/// extensión no corresponde a un lenguaje soportado.
119fn detect_dialect(file: &Path) -> Result<parse::Dialect, CliError> {
120    let ext = file
121        .extension()
122        .and_then(|e| e.to_str())
123        .unwrap_or("");
124    parse::detect_by_extension(ext).ok_or_else(|| {
125        CliError::UnsupportedLanguage {
126            path: file.to_path_buf(),
127            extension: ext.to_string(),
128        }
129    })
130}
131
132/// `minga listen <addr>`: arranca el peer, escucha en `addr`, y
133/// acepta sincronizaciones entrantes hasta que el proceso se cierre.
134pub async fn cmd_listen(
135    repo_path: &Path,
136    passphrase: &str,
137    addr: &str,
138) -> Result<Multiaddr, CliError> {
139    let keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
140    let did = keypair.did();
141    let peer = MingaPeer::open(keypair, repo_path.join(REPO_DIRNAME))?;
142    let multi: Multiaddr = addr
143        .parse()
144        .map_err(|e: libp2p::multiaddr::Error| CliError::Multiaddr(e.to_string()))?;
145    let actual = peer.listen(multi).await;
146    let _accept = peer.run_passive_accept();
147
148    // Bloqueamos para siempre mientras la task de accept procesa
149    // sincronizaciones. El usuario cierra con Ctrl+C.
150    println!("Escuchando en: {}", actual);
151    println!("DID Minga: {}", did);
152    println!("PeerID libp2p: {}", peer.peer_id());
153    futures::future::pending::<()>().await;
154
155    Ok(actual)
156}
157
158/// `minga sync <multiaddr>`: dializa al peer y ejecuta una
159/// sincronización completa con él.
160pub async fn cmd_sync(
161    repo_path: &Path,
162    passphrase: &str,
163    target: &str,
164) -> Result<(), CliError> {
165    let keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
166    let peer = MingaPeer::open(keypair, repo_path.join(REPO_DIRNAME))?;
167
168    let multi: Multiaddr = target
169        .parse()
170        .map_err(|e: libp2p::multiaddr::Error| CliError::Multiaddr(e.to_string()))?;
171    let peer_id = extract_peer_id(&multi).ok_or(CliError::NoPeerIdInMultiaddr)?;
172
173    peer.dial(multi);
174
175    let deadline = std::time::Instant::now() + Duration::from_secs(10);
176    loop {
177        if peer.sync_with(peer_id).await.is_ok() {
178            return Ok(());
179        }
180        if std::time::Instant::now() >= deadline {
181            return Err(CliError::SyncTimeout);
182        }
183        tokio::time::sleep(Duration::from_millis(100)).await;
184    }
185}
186
187fn extract_peer_id(addr: &Multiaddr) -> Option<PeerId> {
188    addr.iter().find_map(|p| match p {
189        Protocol::P2p(peer_id) => Some(peer_id),
190        _ => None,
191    })
192}
193
194/// `minga watch <dir>`: vigila un directorio, re-parsea y re-ingesta
195/// cualquier archivo `.rs` que se cree o modifique. Convierte Minga en
196/// un VCS de fondo: el usuario escribe en su editor habitual y el
197/// código queda versionado y firmado en el repo automáticamente.
198pub async fn cmd_watch(
199    repo_path: &Path,
200    passphrase: &str,
201    watch_dir: &Path,
202) -> Result<(), CliError> {
203    let keypair = keypair_file::load(repo_path.join(KEYPAIR_FILENAME), passphrase)?;
204    let repo = PersistentRepo::open(repo_path.join(REPO_DIRNAME))?;
205
206    // Pasada inicial: ingerimos todos los .rs ya presentes para que
207    // el repo arranque sincronizado con el contenido actual del
208    // directorio (no solo con cambios futuros).
209    initial_scan(&repo, &keypair, watch_dir);
210
211    // Canal entre el callback síncrono de notify y el bucle async.
212    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
213    let mut watcher = notify::recommended_watcher(
214        move |res: Result<notify::Event, notify::Error>| {
215            if let Ok(event) = res {
216                let _ = tx.send(event);
217            }
218        },
219    )?;
220    notify::Watcher::watch(&mut watcher, watch_dir, notify::RecursiveMode::Recursive)?;
221
222    while let Some(event) = rx.recv().await {
223        if !is_relevant_event(&event) {
224            continue;
225        }
226        for path in &event.paths {
227            if is_supported_source(path) {
228                match ingest_into_repo(&repo, &keypair, path) {
229                    Ok(hash) => {
230                        eprintln!("ingerido: {} → {}", path.display(), hash);
231                    }
232                    Err(e) => {
233                        eprintln!("warning: {} no se pudo ingerir: {}", path.display(), e);
234                    }
235                }
236            }
237        }
238    }
239
240    Ok(())
241}
242
243fn initial_scan(repo: &PersistentRepo, keypair: &Keypair, dir: &Path) {
244    let Ok(entries) = fs::read_dir(dir) else {
245        return;
246    };
247    for entry in entries.flatten() {
248        let p = entry.path();
249        if is_supported_source(&p) {
250            let _ = ingest_into_repo(repo, keypair, &p);
251        }
252    }
253}
254
255fn ingest_into_repo(
256    repo: &PersistentRepo,
257    keypair: &Keypair,
258    file: &Path,
259) -> Result<ContentHash, CliError> {
260    let source = fs::read_to_string(file)?;
261    let dialect = detect_dialect(file)?;
262    let node = dialect.parse(&source)?;
263    let hash = repo.nodes.put(&node)?;
264    repo.mst.insert(hash)?;
265    repo.attestations
266        .add(Attestation::create(keypair, hash))?;
267    repo.flush()?;
268    Ok(hash)
269}
270
271/// Detecta si un archivo debe ingerirse: existe, es regular, y su
272/// extensión corresponde a un dialecto soportado.
273fn is_supported_source(path: &Path) -> bool {
274    if !path.is_file() {
275        return false;
276    }
277    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
278    parse::detect_by_extension(ext).is_some()
279}
280
281fn is_relevant_event(event: &notify::Event) -> bool {
282    matches!(
283        event.kind,
284        notify::EventKind::Create(_) | notify::EventKind::Modify(_)
285    )
286}