diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs index f57d5fb4..2e482527 100644 --- a/crates/takana-cli/src/main.rs +++ b/crates/takana-cli/src/main.rs @@ -600,9 +600,10 @@ enum BootCmd { enum RepoCmd { /// Lista los paquetes publicados en el repositorio. List { - /// Directorio del repositorio (con `index.json`). + /// Repositorio: un directorio local (con `index.json`) o una LISTA de orígenes HTTP(S) + /// separada por comas, igual que `install --repo`. #[arg(long, default_value = DEFAULT_REPO)] - repo: PathBuf, + repo: String, }, /// Firma el ÍNDICE entero (release): ancla qué paquetes/versiones/hashes existen. Re-firmá /// tras publicar (cada `pack --repo` invalida la firma previa). @@ -618,8 +619,11 @@ enum RepoCmd { }, /// Verifica la firma del release del repositorio contra las claves de confianza. Verify { + /// Repositorio: un directorio local o una lista de orígenes HTTP(S), igual que + /// `install --repo`. Verificar un release REMOTO es justo lo que hace falta antes de + /// confiar en un espejo, y hasta el 2026-09-21 no se podía: sólo miraba directorios. #[arg(long, default_value = DEFAULT_REPO)] - repo: PathBuf, + repo: String, /// Directorio de claves de confianza. Default `/var/lib/hammer/trust`. #[arg(long)] trust: Option, @@ -2651,13 +2655,24 @@ impl RepoSource { /// Lee el índice del repo (local: `index.json`; http: `GET /index.json`). fn read_index(&self) -> anyhow::Result { + Ok(self.read_index_con_origen()?.0) + } + + /// Igual, pero dice **quién lo sirvió**. Con varios espejos, `label()` nombra a todos los + /// configurados y sólo uno respondió: atribuir el índice a la lista entera es la misma media + /// verdad que `fetch_desde_algun_origen` evita para los `.swm`, y es justo el dato que hace + /// falta cuando un espejo se queda atrás y otro no. + fn read_index_con_origen(&self) -> anyhow::Result<(takana_core::RepoIndex, String)> { match self { - RepoSource::Local(d) => Ok(takana_core::RepoIndex::load(d)?), + RepoSource::Local(d) => { + Ok((takana_core::RepoIndex::load(d)?, d.display().to_string())) + } RepoSource::Http(bases) => { let (bytes, origen) = Self::fetch_desde_algun_origen(bases, takana_core::repo::INDEX_FILE)?; - serde_json::from_slice(&bytes) - .map_err(|e| anyhow::anyhow!("índice remoto de {origen}: {e}")) + let index = serde_json::from_slice(&bytes) + .map_err(|e| anyhow::anyhow!("índice remoto de {origen}: {e}"))?; + Ok((index, origen.to_string())) } } } @@ -3466,15 +3481,18 @@ fn run_repo_sign( /// Verifica la firma del release de un repo contra el TrustStore. fn run_repo_verify( - repo_dir: &std::path::Path, + repo: &str, trust: Option<&std::path::Path>, ) -> anyhow::Result<()> { - let index = takana_core::RepoIndex::load(repo_dir)?; + let (index, origen) = RepoSource::parse(repo).read_index_con_origen()?; let trust_dir = trust.unwrap_or_else(|| std::path::Path::new(DEFAULT_TRUST_DIR)); let store = takana_core::TrustStore::load(trust_dir)?; match index.verify_signature(&store) { takana_core::SigStatus::Trusted { by } => { - println!("release: trusted (by {by}) — {} paquete(s)", index.packages.len()); + println!( + "release: trusted (by {by}) — {} paquete(s) · índice de {origen}", + index.packages.len() + ); Ok(()) } takana_core::SigStatus::UnknownKey { by } => { @@ -3495,17 +3513,50 @@ fn run_repo_verify( } /// Lista los paquetes publicados en un repositorio. -fn run_repo_list(repo_dir: &std::path::Path) -> anyhow::Result<()> { - let index = takana_core::RepoIndex::load(repo_dir)?; +/// +/// # Por qué acepta una URL (2026-09-21) +/// +/// Antes tomaba un `PathBuf` y sólo miraba directorios, así que +/// `repo list --repo https://takana-os.net/repo` trataba la URL como una ruta relativa que no +/// existe y respondía **«vacío», con salida 0**: la única forma de mirar un repo remoto era +/// `install`, que además construye. Peor que no soportarlo: decía que el repo no tenía nada +/// cuando tenía 173 paquetes. `install --repo` ya sabía hablar HTTP con `RepoSource`; esto usa +/// exactamente el mismo resolvedor, así que la misma cadena de orígenes vale en los dos verbos. +fn run_repo_list(repo: &str) -> anyhow::Result<()> { + let src = RepoSource::parse(repo); + let (index, origen) = src.read_index_con_origen()?; if index.packages.is_empty() { - println!("repo {} vacío (sin index.json o sin paquetes)", repo_dir.display()); + // Un directorio SIN `index.json` no es lo mismo que un índice con cero paquetes, y la + // salida vieja los daba por iguales. Lo primero es un repo que todavía no existe —estado + // válido: `pack --repo ` lo crea—, lo segundo es un repo publicado y vacío. + // (El caso remoto no llega acá: si el origen no sirve `index.json`, `read_index` falla.) + match &src { + // AUSENTE y VACÍO no son lo mismo (CLAUDE.md §3): el directorio que no existe suele + // ser una ruta mal escrita —o una URL pasada sin `http(s)://`, que `parse` toma por + // ruta— y decirle «repo por crear» manda a buscar el problema al sitio equivocado. + RepoSource::Local(d) if !d.exists() => { + anyhow::bail!( + "repo {}: el directorio no existe (¿ruta mal escrita? si era un repo REMOTO, la URL tiene que empezar por http(s)://)", + d.display() + ); + } + RepoSource::Local(d) if !d.join(takana_core::repo::INDEX_FILE).exists() => { + println!( + "repo {} — todavía no hay {}: es un repo por crear, no uno vacío \ + (lo crea `pack --repo`)", + d.display(), + takana_core::repo::INDEX_FILE + ); + } + _ => println!("repo {origen} — índice publicado y SIN paquetes"), + } return Ok(()); } let rel = match &index.signature { Some(s) => format!(" [release firmado por {}]", s.by), None => String::new(), }; - println!("repo {} — {} paquete(s){}:", repo_dir.display(), index.packages.len(), rel); + println!("repo {} — {} paquete(s){}:", origen, index.packages.len(), rel); for p in &index.packages { let sig = p.signed_by.as_deref().map(|b| format!(" [firmado por {b}]")).unwrap_or_default(); let anchor = p diff --git a/docs/06-swm-format.md b/docs/06-swm-format.md index d8c92616..3808a8fc 100644 --- a/docs/06-swm-format.md +++ b/docs/06-swm-format.md @@ -147,10 +147,17 @@ CLI: `.swm`; con URL baja el índice + el cierre de deps a un temporal y procede igual). `--prefix`/`--skip-source-patch` para staging y dry-run de schema. `--require-signed` exige que el release esté firmado por una clave confiada (modo estricto: no basta con reproducir). -- `takana repo list [--repo DIR]` — lista el catálogo (`/index.json`). +- `takana repo list [--repo DIR|URL]` — lista el catálogo (`/index.json`). Desde el + 2026-09-21 acepta lo MISMO que `install --repo`: un directorio o una lista de orígenes HTTP(S) + separada por comas. Antes sólo miraba directorios y ante una URL respondía **«vacío» con salida + 0** — un repo remoto con 173 paquetes se leía como uno sin nada. Y ahora distingue «todavía no + hay `index.json`» (repo por crear) de «índice publicado y sin paquetes», que también se daban + por iguales. - `takana repo sign --repo DIR --key KEY` — firma el ÍNDICE entero (release). Re-firmá tras - publicar (cada `pack --repo` invalida la firma del release). -- `takana repo verify --repo DIR [--trust DIR]` — verifica la firma del release. + publicar (cada `pack --repo` invalida la firma del release). **Sólo local:** firmar es escribir. +- `takana repo verify --repo DIR|URL [--trust DIR]` — verifica la firma del release. También + acepta orígenes HTTP(S): verificar el release de un espejo ANTES de instarle nada es justo el + caso de uso del ADR 0014, y hasta el 2026-09-21 no había forma de hacerlo sin construir. - `takana uninstall [--db FILE]` — borra los ficheros que el paquete registró (refcount: respeta los que otro paquete instalado también aporta) y lo quita de la DB de instalados. - `takana installed [--db FILE]` — lista los paquetes instalados. `install` registra cada paquete