ozali 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import {
6
6
  c, ok, warn, err, info, step,
7
7
  SKILL_SRC, COMMIT_SKILL_SRC, TEMPLATES_SRC, exists, ensureDir, copyDir, readJSON, writeJSON,
8
- ensureGitignore, tryExec, spawnCmd, which, engramAssetName,
8
+ ensureGitignore, tryExec, spawnCmd, which, engramAssetName, pickEngramAsset,
9
9
  projectName, pkgVersion, DEFAULT_KNOWLEDGE, HOME, openURL, gitInfo,
10
10
  } from "./util.mjs";
11
11
  import { detectAll, detectWorkspace, detectReferences } from "./detect.mjs";
@@ -305,19 +305,28 @@ function installEngram() {
305
305
  return false;
306
306
  }
307
307
 
308
- const ENGRAM_RELEASES_API = "https://api.github.com/repos/Gentleman-Programming/engram/releases/latest";
309
- const ENGRAM_RELEASES_DL = "https://github.com/Gentleman-Programming/engram/releases/download";
308
+ // Lista de releases (NO /releases/latest: ese endpoint puede devolver un tag especial
309
+ // sin binarios, p. ej. `pi-v*`). Recorremos la lista y elegimos el release estable.
310
+ const ENGRAM_RELEASES_LIST = "https://api.github.com/repos/Gentleman-Programming/engram/releases?per_page=30";
310
311
 
311
- /** Última versión publicada de Engram (sin la "v" inicial), o null si no hay red. */
312
- function latestEngramVersion() {
313
- let raw = null;
314
- if (which("curl")) raw = tryExec("curl", ["-fsSL", ENGRAM_RELEASES_API]);
315
- else if (which("wget")) raw = tryExec("wget", ["-qO-", ENGRAM_RELEASES_API]);
312
+ /** GET de texto con curl (o wget). Devuelve el body o null si no hay red/herramienta. */
313
+ function fetchText(url) {
314
+ if (which("curl")) return tryExec("curl", ["-fsSL", url]);
315
+ if (which("wget")) return tryExec("wget", ["-qO-", url]);
316
+ return null;
317
+ }
318
+
319
+ /**
320
+ * Resuelve { version, url } del binario precompilado de Engram para este SO/arch,
321
+ * consultando la lista de releases y quedándose con el release estable más reciente
322
+ * que contenga el asset. Devuelve null si no hay red/herramienta o no hay binario.
323
+ */
324
+ function resolveEngramAsset(platform, arch) {
325
+ const raw = fetchText(ENGRAM_RELEASES_LIST);
316
326
  if (!raw) return null;
317
- try {
318
- const tag = JSON.parse(raw).tag_name;
319
- return tag ? String(tag).replace(/^v/, "") : null;
320
- } catch { return null; }
327
+ let releases;
328
+ try { releases = JSON.parse(raw); } catch { return null; }
329
+ return pickEngramAsset(releases, platform, arch);
321
330
  }
322
331
 
323
332
  /** Descarga url → dest con curl (o wget como fallback). Devuelve true si tuvo éxito. */
@@ -359,14 +368,13 @@ function installEngramFromTarball() {
359
368
  return false;
360
369
  }
361
370
 
362
- const version = latestEngramVersion();
363
- if (!version) {
364
- warn("No pude resolver la última versión de Engram (¿sin red o sin curl/wget?).");
371
+ const resolved = resolveEngramAsset(plat, process.arch);
372
+ if (!resolved) {
373
+ warn("No pude resolver un binario precompilado de Engram para tu SO/arch (¿sin red, sin curl/wget, o release sin assets?).");
365
374
  return false;
366
375
  }
367
-
376
+ const { version, url } = resolved;
368
377
  const asset = engramAssetName(plat, process.arch, version);
369
- const url = `${ENGRAM_RELEASES_DL}/v${version}/${asset}`;
370
378
  info(`Descargando binario precompilado de Engram ${c.bold("v" + version)} (${process.arch})…`);
371
379
 
372
380
  let tmpDir;
package/cli/lib/util.mjs CHANGED
@@ -37,6 +37,29 @@ export function engramAssetName(platform, arch, version) {
37
37
  return `engram_${version}_${os}_${a}.${ext}`;
38
38
  }
39
39
 
40
+ /**
41
+ * Elige el binario precompilado de Engram para un SO/arch dado a partir de la lista de
42
+ * releases (formato de la API de GitHub `/releases`). Se queda con el release ESTABLE
43
+ * más reciente cuyo tag sea semver `vX.Y.Z` y que **realmente contenga** el asset
44
+ * esperado, y devuelve su `browser_download_url` real. Ignora tags no-semver (p. ej.
45
+ * `pi-v*`, builds de Raspberry Pi que NO traen binarios) y draft/prerelease.
46
+ * Función pura (sin red). Devuelve { version, url } o null.
47
+ */
48
+ export function pickEngramAsset(releases, platform, arch) {
49
+ if (!Array.isArray(releases)) return null;
50
+ for (const r of releases) {
51
+ if (!r || r.draft || r.prerelease) continue;
52
+ const m = /^v(\d+\.\d+\.\d+)$/.exec(r.tag_name || "");
53
+ if (!m) continue; // salta tags no-semver (pi-v*, etc.)
54
+ const version = m[1];
55
+ const name = engramAssetName(platform, arch, version);
56
+ if (!name) return null; // SO/arch sin binario publicado
57
+ const asset = Array.isArray(r.assets) ? r.assets.find((a) => a && a.name === name) : null;
58
+ if (asset && asset.browser_download_url) return { version, url: asset.browser_download_url };
59
+ }
60
+ return null;
61
+ }
62
+
40
63
  // ---- colores (sin deps; respeta NO_COLOR y no-TTY) --------------------------
41
64
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
42
65
  const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
@@ -6,7 +6,7 @@ import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { engramAssetName } from "../lib/util.mjs";
9
+ import { engramAssetName, pickEngramAsset } from "../lib/util.mjs";
10
10
 
11
11
  const HERE = path.dirname(fileURLToPath(import.meta.url));
12
12
  const BIN = path.resolve(HERE, "..", "bin", "ozali.mjs");
@@ -335,3 +335,31 @@ test("engramAssetName respeta la convención de release por SO/arch", () => {
335
335
  assert.equal(engramAssetName("linux", "ia32", "1.17.0"), null, "arch no soportada → null");
336
336
  assert.equal(engramAssetName("freebsd", "x64", "1.17.0"), null, "SO no soportado → null");
337
337
  });
338
+
339
+ test("pickEngramAsset ignora tags no-semver sin binarios (pi-v*) y draft/prerelease", () => {
340
+ // Shape real de la API de GitHub: el 'latest' es pi-v0.1.9 (0 assets); los binarios
341
+ // viven en tags vX.Y.Z. Además metemos un prerelease más nuevo que debe saltarse.
342
+ const releases = [
343
+ { tag_name: "pi-v0.1.9", prerelease: false, draft: false, assets: [] },
344
+ { tag_name: "v2.0.0", prerelease: true, draft: false, assets: [
345
+ { name: "engram_2.0.0_linux_amd64.tar.gz", browser_download_url: "https://x/pre" },
346
+ ] },
347
+ { tag_name: "v1.17.0", prerelease: false, draft: false, assets: [
348
+ { name: "checksums.txt", browser_download_url: "https://x/sum" },
349
+ { name: "engram_1.17.0_linux_amd64.tar.gz", browser_download_url: "https://x/engram_1.17.0_linux_amd64.tar.gz" },
350
+ { name: "engram_1.17.0_darwin_arm64.tar.gz", browser_download_url: "https://x/darwin" },
351
+ ] },
352
+ ];
353
+ // Linux x64 → salta pi-v* (sin assets) y el prerelease → v1.17.0, con la URL REAL del asset.
354
+ assert.deepEqual(pickEngramAsset(releases, "linux", "x64"), {
355
+ version: "1.17.0",
356
+ url: "https://x/engram_1.17.0_linux_amd64.tar.gz",
357
+ });
358
+ // macOS arm64 → mismo release, su asset darwin_arm64.
359
+ assert.deepEqual(pickEngramAsset(releases, "darwin", "arm64"), { version: "1.17.0", url: "https://x/darwin" });
360
+ // Arch sin binario → null.
361
+ assert.equal(pickEngramAsset(releases, "linux", "ia32"), null);
362
+ // Sin releases utilizables → null.
363
+ assert.equal(pickEngramAsset([{ tag_name: "pi-v0.1.9", assets: [] }], "linux", "x64"), null);
364
+ assert.equal(pickEngramAsset(null, "linux", "x64"), null);
365
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ozali",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Bootstrap interactivo de IA por equipo: calibra el proyecto, genera la skill de ejecución (cdk) con TDD/SDD, y mantiene memoria de equipo (Engram) con histórico aislado.",
5
5
  "type": "module",
6
6
  "bin": {