jorgex-stack 1.9.60 → 1.9.62
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.
- package/README.md +9 -7
- package/dist/chunk-KGL5VNVZ.js +305 -0
- package/dist/cli.js +6140 -689
- package/dist/pi-ci-artifact.d.ts +21 -0
- package/dist/pi-ci-artifact.js +167 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ For a fresh Engram installation, always consult the current published Stack and
|
|
|
48
48
|
pnpm --config.dlx-cache-max-age=0 dlx jorgex-stack@latest install --engram
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`dlx-cache-max-age` is separate from the pnpm 11 dependency-age filter: it controls only the cached `dlx` package, while `minimumReleaseAgeExclude` applies only to the named package resolution. An explicit Stack version such as `@1.9.30` does not reuse the cache entry for another version.
|
|
51
|
+
`dlx-cache-max-age` is separate from the pnpm 11 dependency-age filter: it controls only the cached `dlx` package, while `minimumReleaseAgeExclude` applies only to the named package resolution. An explicit Stack version such as `@1.9.30` does not reuse the cache entry for another version. Pi `install`/`update` resolve the registry's observed published `latest` dist-tag to an exact version and verify the artifact before activation; they do not install a floating `latest` alias.
|
|
52
52
|
|
|
53
53
|
Other important commands:
|
|
54
54
|
|
|
@@ -114,9 +114,9 @@ Programmatic mode does **not** provide:
|
|
|
114
114
|
|
|
115
115
|
### Pi runtime
|
|
116
116
|
|
|
117
|
-
El canon de Stack y el paquete Pi
|
|
117
|
+
El canon de Stack y el paquete Pi mantienen una snapshot de 17 árboles de skills con 89 archivos de skill. Para instalaciones deliberadas, Stack resuelve la versión publicada observada en npm y verifica el tarball y el stage; `src/lib/pi-runtime-pin.json` y el historial conservan identidades congeladas, no un selector para futuras instalaciones.
|
|
118
118
|
|
|
119
|
-
Pi combines the
|
|
119
|
+
Pi combines the **snapshot v2** package with a Stack-owned shared projection. Package resolution, verification, receipt and recovery behavior are documented in [docs/references/pi-runtime.md](docs/references/pi-runtime.md); historical pins are not a promise that personal Pi installations have migrated.
|
|
120
120
|
|
|
121
121
|
The following command block is retained as historical reference for that transition:
|
|
122
122
|
|
|
@@ -128,9 +128,11 @@ pnpm dlx jorgex-stack@1.9.7 sync --agents pi
|
|
|
128
128
|
pnpm dlx jorgex-stack@1.9.7 uninstall --agents pi
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
For the
|
|
131
|
+
For a deliberate managed install or update, Stack resolves the live published `latest` tag to an exact release, verifies its canonical tarball URL and SRI/bytes, and prepares it in an isolated stage before activation. The stage locks six dependencies and runs smoke checks; Stack then backs up state and publishes only its owned package entry. Automatic restoration covers activation/verification failures; a later projection or sync failure may need manual recovery from the retained backup. The schema 1 receipt records managed-package evidence for offline verification. `sync`, `models`, `doctor` and `uninstall` do not acquire a new version; they require an authenticated receipt/artifact, with explicit legacy recovery paths where applicable. `update --check` does not download. `--target-dir` neither downloads Pi nor touches the real HOME; with a previously verified candidate/stage injected, it can run isolated smoke/runner code inside the target. Direct Pi installation is separate and does not provide Stack's rollback guarantees.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
The artifact values in `src/lib/pi-runtime-pin.json` describe frozen historical/reference metadata, not the live install candidate; live artifact URL, version and integrity are resolved from the published registry.
|
|
134
|
+
|
|
135
|
+
Stack runs provider-owned `engram setup pi` before Pi package activation on a real managed install. It backs up and verifies Pi's `settings.json`, `mcp.json` and provider-owned `npm` tree, and restores the backup on setup failure; Pi is not activated unless setup and subsequent package lifecycle succeed. The runtime lifecycle remains authoritative in `src/lib/pi-runtime.ts`; the exact release evidence belongs to the verified receipt and tarball. Pi's own package-manager invocation is the narrow runtime exception to the repository's pnpm-only rule; the Stack lifecycle never launches npm directly. A managed install then runs provider setup, `package install → projection install → package sync`. Stack projects shared resources such as the system prompt, canonical skills and `lean-audit`; it does not inject `jorgex:engram-protocol`, project Engram tools, or filter provider tools. The behavioral `engram` role remains part of the Pi contract, while the official provider owns its setup and tools. Pi registers Context7 through an isolated in-memory HTTP bridge during bootstrap; `available` means configuration permits registration and does not imply an HTTP handshake. A conflict preserves the existing MCP file and blocks managed activation. No MCP credentials are written. For the file runtimes, Context7, Playwright and DevTools use independent managed sections. The global Playwright package and Chromium cache are shared by the machine; `--playwright-runtimes` controls which runtime receives the guide. The adopted Pi package implements and tests `playwright-handoff-v1` through `PI_CODING_AGENT_DIR/jorgex-pi/playwright.v1.json`. Package ownership is recorded separately in `~/.jorgex-stack/pi-receipt.json`; projection ownership is recorded in `~/.jorgex-stack/pi-projection-receipt.json`. Package receipts reject manual, duplicate, divergent, partial, corrupt, copied-to-another-scope, or unknown-history state. Projection cleanup requires an exact scope-bound ownership receipt; DevTools conflicts preserve the handoff for review.
|
|
134
136
|
|
|
135
137
|
The static `AGENTS.md` projected by Stack does not include Context7; Pi's native bootstrap adds that section after registering the isolated bridge. Pi never writes, owns or removes MCP files. In Claude Code, Codex and OpenCode, a compatible existing Context7 entry is preserved and removed only with explicit canonical Stack ownership. During package installation only, `initialization-diagnostics-v1` permits the exact pending `doctor` envelope described in [the Pi runtime reference](docs/references/pi-runtime.md); it is provisional and does not make Pi healthy. Projection and final `sync` must still complete, and every other unhealthy or malformed result remains blocked.
|
|
136
138
|
|
|
@@ -158,9 +160,9 @@ Para Codex, Engram 2.0 ignora `CODEX_HOME` y escribe siempre en `$HOME/.codex`;
|
|
|
158
160
|
|
|
159
161
|
La política de complementos distingue estrategias `exact` y `provider-managed`. DevTools MCP y Playwright CLI mantienen pins exactos; las integraciones oficiales de Engram son `provider-managed`/rolling, incluido Codex `main`, `pi-mcp-adapter` y el setup oficial de Pi sin pin en Stack. OpenCode 2 queda fuera de alcance y no bloquea esta integración.
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
Los receipts históricos, incluido `jorgex-pi@0.8.24`, son evidencia para recuperación/migración y no candidatos de instalación nuevos. La selección gestionada resuelve el `latest` publicado a una versión exacta y verifica el artefacto antes de activarlo. No edites receipts o hashes ni borres `HOME`, Engram o la proyección de otro runtime para forzar confianza. La migración y rollback están en [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
Stack gestiona la selección dinámica y verificación de Pi en `install`/`update`; esto no significa que una instalación personal de Pi se haya migrado. La automatización Stack ↔ Pi es snapshot-only. La instalación directa de Pi no incluye la etapa aislada ni el rollback de Stack.
|
|
164
166
|
|
|
165
167
|
`update --agents pi` only runs the Pi package lifecycle; it does not enter the global Stack updater. `update --check --agents pi` performs a read-only check of the Pi package and local registration metadata through the package runner. It does not compare Stack's projection, run the browser smoke check, or mutate Pi state; use `doctor --agents pi` for the complete package-and-projection diagnosis. Uninstall runs package cleanup, backs up Pi's settings before removal, removes only the exact receipt-owned package after verifying absence, and preserves all companion/user state. Full behavior, failure states and troubleshooting are in [docs/references/pi-runtime.md](docs/references/pi-runtime.md).
|
|
166
168
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/pi-release-resolver.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
7
|
+
var REGISTRY_URL = "https://registry.npmjs.org/jorgex-pi";
|
|
8
|
+
var REGISTRY_HOST = "registry.npmjs.org";
|
|
9
|
+
var ACCEPT = "application/vnd.npm.install-v1+json";
|
|
10
|
+
var MAX_METADATA_BYTES = 4 * 1024 * 1024;
|
|
11
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
12
|
+
var STABLE_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
13
|
+
function fail(message) {
|
|
14
|
+
throw new Error(`pi-release-resolver: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function canonicalTarballUrl(version) {
|
|
20
|
+
return `https://registry.npmjs.org/jorgex-pi/-/jorgex-pi-${version}.tgz`;
|
|
21
|
+
}
|
|
22
|
+
async function readBoundedText(response) {
|
|
23
|
+
if (response.body === null) fail("missing response body");
|
|
24
|
+
const reader = response.body.getReader();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
try {
|
|
28
|
+
for (; ; ) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done) break;
|
|
31
|
+
total += value.byteLength;
|
|
32
|
+
if (total >= MAX_METADATA_BYTES) {
|
|
33
|
+
await reader.cancel().catch(() => {
|
|
34
|
+
});
|
|
35
|
+
fail("metadata exceeds 4 MiB");
|
|
36
|
+
}
|
|
37
|
+
chunks.push(value);
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
reader.releaseLock();
|
|
41
|
+
}
|
|
42
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
43
|
+
}
|
|
44
|
+
function assertCanonicalIntegrity(integrity) {
|
|
45
|
+
if (typeof integrity !== "string" || !integrity.startsWith("sha512-")) {
|
|
46
|
+
fail("invalid integrity (expected canonical sha512 SRI)");
|
|
47
|
+
}
|
|
48
|
+
const b64 = integrity.slice("sha512-".length);
|
|
49
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) fail("invalid integrity (expected canonical sha512 SRI)");
|
|
50
|
+
let bytes;
|
|
51
|
+
try {
|
|
52
|
+
bytes = Buffer.from(b64, "base64");
|
|
53
|
+
} catch {
|
|
54
|
+
fail("invalid integrity (expected canonical sha512 SRI)");
|
|
55
|
+
}
|
|
56
|
+
if (bytes.length !== 64 || bytes.toString("base64") !== b64) {
|
|
57
|
+
fail("invalid integrity (expected canonical sha512 SRI)");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function resolveLatestPiRelease(fetchImpl) {
|
|
61
|
+
const response = await fetchImpl(REGISTRY_URL, {
|
|
62
|
+
headers: { Accept: ACCEPT },
|
|
63
|
+
redirect: "error",
|
|
64
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok) fail(`registry responded ${response.status}`);
|
|
67
|
+
if (response.url !== REGISTRY_URL) fail(`unexpected response URL ${response.url}`);
|
|
68
|
+
const lengthHeader = response.headers.get("content-length");
|
|
69
|
+
if (lengthHeader !== null) {
|
|
70
|
+
const trimmed = lengthHeader.trim();
|
|
71
|
+
if (!/^\d+$/.test(trimmed)) fail("invalid content-length");
|
|
72
|
+
if (Number(trimmed) > MAX_METADATA_BYTES) fail("metadata exceeds 4 MiB");
|
|
73
|
+
}
|
|
74
|
+
const text = await readBoundedText(response);
|
|
75
|
+
let data;
|
|
76
|
+
try {
|
|
77
|
+
data = JSON.parse(text);
|
|
78
|
+
} catch {
|
|
79
|
+
fail("malformed packument JSON");
|
|
80
|
+
}
|
|
81
|
+
if (!isRecord(data) || data["name"] !== "jorgex-pi") fail("malformed packument name");
|
|
82
|
+
const tags = data["dist-tags"];
|
|
83
|
+
if (!isRecord(tags) || typeof tags["latest"] !== "string") fail("missing dist-tags.latest");
|
|
84
|
+
const latest = tags["latest"];
|
|
85
|
+
if (!STABLE_SEMVER.test(latest)) fail(`unstable or malformed version ${latest}`);
|
|
86
|
+
const versions = data["versions"];
|
|
87
|
+
if (!isRecord(versions)) fail("missing versions map");
|
|
88
|
+
const entry = versions[latest];
|
|
89
|
+
if (!isRecord(entry)) fail(`missing versions[${latest}]`);
|
|
90
|
+
if (entry["name"] !== "jorgex-pi" || entry["version"] !== latest) {
|
|
91
|
+
fail("version mismatch in versions entry");
|
|
92
|
+
}
|
|
93
|
+
if (entry["deprecated"] !== void 0) fail(`version ${latest} is deprecated`);
|
|
94
|
+
const dist = entry["dist"];
|
|
95
|
+
if (!isRecord(dist)) fail("missing dist");
|
|
96
|
+
const { tarball, integrity } = dist;
|
|
97
|
+
if (typeof tarball !== "string" || typeof integrity !== "string") {
|
|
98
|
+
fail("missing dist.tarball/integrity");
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(tarball);
|
|
103
|
+
} catch {
|
|
104
|
+
fail("foreign tarball URL");
|
|
105
|
+
}
|
|
106
|
+
if (parsed.protocol !== "https:" || parsed.host !== REGISTRY_HOST || tarball !== canonicalTarballUrl(latest)) {
|
|
107
|
+
fail("foreign tarball URL");
|
|
108
|
+
}
|
|
109
|
+
assertCanonicalIntegrity(integrity);
|
|
110
|
+
return { version: latest, tarballUrl: tarball, integrity };
|
|
111
|
+
}
|
|
112
|
+
var MAX_TARBALL_BYTES = 128 * 1024 * 1024;
|
|
113
|
+
var TARBALL_FETCH_TIMEOUT_MS = 12e4;
|
|
114
|
+
function expectedSha512Digest(integrity) {
|
|
115
|
+
assertCanonicalIntegrity(integrity);
|
|
116
|
+
return Buffer.from(integrity.slice("sha512-".length), "base64");
|
|
117
|
+
}
|
|
118
|
+
function assertCanonicalReleaseInput(release) {
|
|
119
|
+
if (!isRecord(release)) fail("invalid release");
|
|
120
|
+
const { version, tarballUrl, integrity } = release;
|
|
121
|
+
if (typeof version !== "string" || !STABLE_SEMVER.test(version)) fail("invalid release version");
|
|
122
|
+
if (typeof tarballUrl !== "string") fail("foreign tarball URL");
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = new URL(tarballUrl);
|
|
126
|
+
} catch {
|
|
127
|
+
fail("foreign tarball URL");
|
|
128
|
+
}
|
|
129
|
+
if (parsed.protocol !== "https:" || parsed.host !== REGISTRY_HOST || tarballUrl !== canonicalTarballUrl(version)) {
|
|
130
|
+
fail("foreign tarball URL");
|
|
131
|
+
}
|
|
132
|
+
return { tarballUrl, expectedSha512: expectedSha512Digest(integrity) };
|
|
133
|
+
}
|
|
134
|
+
function readExistingArtifact(file) {
|
|
135
|
+
const stat = fs.statSync(file, { throwIfNoEntry: false });
|
|
136
|
+
if (stat === void 0) return null;
|
|
137
|
+
if (!stat.isFile()) fail("destination is not a regular file");
|
|
138
|
+
let fd;
|
|
139
|
+
try {
|
|
140
|
+
fd = fs.openSync(file, "r");
|
|
141
|
+
} catch {
|
|
142
|
+
fail("cannot read existing destination");
|
|
143
|
+
}
|
|
144
|
+
const sha256 = createHash("sha256");
|
|
145
|
+
const sha512 = createHash("sha512");
|
|
146
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
147
|
+
let bytes = 0;
|
|
148
|
+
try {
|
|
149
|
+
for (; ; ) {
|
|
150
|
+
const read = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
151
|
+
if (read === 0) break;
|
|
152
|
+
bytes += read;
|
|
153
|
+
if (bytes >= MAX_TARBALL_BYTES) fail("existing destination exceeds tarball bound");
|
|
154
|
+
sha256.update(buffer.subarray(0, read));
|
|
155
|
+
sha512.update(buffer.subarray(0, read));
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
try {
|
|
159
|
+
fs.closeSync(fd);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { bytes, sha256: sha256.digest("hex"), sha512: sha512.digest("hex") };
|
|
164
|
+
}
|
|
165
|
+
async function downloadVerifiedPiTarball(release, destination, fetchImpl) {
|
|
166
|
+
const { tarballUrl, expectedSha512 } = assertCanonicalReleaseInput(release);
|
|
167
|
+
if (typeof destination !== "string" || destination === "") fail("invalid destination");
|
|
168
|
+
const existing = readExistingArtifact(destination);
|
|
169
|
+
if (existing !== null) {
|
|
170
|
+
const existing512 = Buffer.from(existing.sha512, "hex");
|
|
171
|
+
if (existing512.length === expectedSha512.length && timingSafeEqual(existing512, expectedSha512)) {
|
|
172
|
+
return { path: destination, bytes: existing.bytes, sha256: existing.sha256, sha512: existing.sha512 };
|
|
173
|
+
}
|
|
174
|
+
fail("destination bytes do not match requested integrity");
|
|
175
|
+
}
|
|
176
|
+
let response;
|
|
177
|
+
try {
|
|
178
|
+
response = await fetchImpl(tarballUrl, {
|
|
179
|
+
headers: { accept: "application/octet-stream" },
|
|
180
|
+
redirect: "error",
|
|
181
|
+
signal: AbortSignal.timeout(TARBALL_FETCH_TIMEOUT_MS)
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
fail(`tarball fetch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
185
|
+
}
|
|
186
|
+
if (!response.ok) fail(`tarball responded ${response.status}`);
|
|
187
|
+
if (response.url !== tarballUrl) fail(`unexpected tarball response URL ${response.url}`);
|
|
188
|
+
const declared = response.headers.get("content-length");
|
|
189
|
+
if (declared !== null) {
|
|
190
|
+
const trimmed = declared.trim();
|
|
191
|
+
if (!/^\d+$/.test(trimmed)) fail("invalid tarball content-length");
|
|
192
|
+
if (Number(trimmed) > MAX_TARBALL_BYTES) fail("tarball exceeds 128 MiB");
|
|
193
|
+
}
|
|
194
|
+
if (response.body === null) fail("missing tarball body");
|
|
195
|
+
const tempPath = path.join(
|
|
196
|
+
path.dirname(destination),
|
|
197
|
+
`jorgex-pi-partial-${process.pid}-${randomBytes(8).toString("hex")}`
|
|
198
|
+
);
|
|
199
|
+
let fd;
|
|
200
|
+
try {
|
|
201
|
+
fd = fs.openSync(tempPath, "wx", 384);
|
|
202
|
+
} catch {
|
|
203
|
+
fail("cannot stage tarball temp");
|
|
204
|
+
}
|
|
205
|
+
let fdClosed = false;
|
|
206
|
+
const closeFd = () => {
|
|
207
|
+
if (fdClosed) return;
|
|
208
|
+
fdClosed = true;
|
|
209
|
+
try {
|
|
210
|
+
fs.closeSync(fd);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const discardStaged = () => {
|
|
215
|
+
closeFd();
|
|
216
|
+
try {
|
|
217
|
+
fs.rmSync(tempPath, { force: true });
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
const reader = response.body.getReader();
|
|
223
|
+
const sha256 = createHash("sha256");
|
|
224
|
+
const sha512 = createHash("sha512");
|
|
225
|
+
let bytes = 0;
|
|
226
|
+
try {
|
|
227
|
+
for (; ; ) {
|
|
228
|
+
const { done, value } = await reader.read();
|
|
229
|
+
if (done) break;
|
|
230
|
+
bytes += value.byteLength;
|
|
231
|
+
if (bytes + value.byteLength >= MAX_TARBALL_BYTES) {
|
|
232
|
+
await reader.cancel().catch(() => {
|
|
233
|
+
});
|
|
234
|
+
fail("tarball exceeds 128 MiB");
|
|
235
|
+
}
|
|
236
|
+
sha256.update(value);
|
|
237
|
+
sha512.update(value);
|
|
238
|
+
let offset = 0;
|
|
239
|
+
while (offset < value.byteLength) {
|
|
240
|
+
offset += fs.writeSync(fd, value, offset, value.byteLength - offset);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} finally {
|
|
244
|
+
reader.releaseLock();
|
|
245
|
+
}
|
|
246
|
+
closeFd();
|
|
247
|
+
const actual512 = sha512.digest();
|
|
248
|
+
if (!timingSafeEqual(actual512, expectedSha512)) fail("tarball integrity mismatch");
|
|
249
|
+
try {
|
|
250
|
+
fs.linkSync(tempPath, destination);
|
|
251
|
+
} catch {
|
|
252
|
+
fail("tarball publish raced with existing destination");
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
fs.rmSync(tempPath, { force: true });
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
return { path: destination, bytes, sha256: sha256.digest("hex"), sha512: actual512.toString("hex") };
|
|
259
|
+
} catch (error) {
|
|
260
|
+
discardStaged();
|
|
261
|
+
if (error instanceof Error && error.message.startsWith("pi-release-resolver: ")) throw error;
|
|
262
|
+
fail(`tarball download failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
var PRODUCER_REF_API = "https://api.github.com/repos/jorgehn98/jorgex-pi/git/ref/tags";
|
|
266
|
+
var PRODUCER_ACCEPT = "application/vnd.github+json";
|
|
267
|
+
var SHA40_HEX = /^[0-9a-f]{40}$/;
|
|
268
|
+
function producerTagUrl(version) {
|
|
269
|
+
return `${PRODUCER_REF_API}/v${version}`;
|
|
270
|
+
}
|
|
271
|
+
async function resolvePiProducerCommit(version, fetchImpl) {
|
|
272
|
+
if (typeof version !== "string" || !STABLE_SEMVER.test(version)) fail("invalid producer version");
|
|
273
|
+
const url = producerTagUrl(version);
|
|
274
|
+
let response;
|
|
275
|
+
try {
|
|
276
|
+
response = await fetchImpl(url, {
|
|
277
|
+
headers: { accept: PRODUCER_ACCEPT },
|
|
278
|
+
redirect: "error",
|
|
279
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
fail(`producer ref fetch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
283
|
+
}
|
|
284
|
+
if (!response.ok) fail(`producer ref responded ${response.status}`);
|
|
285
|
+
if (response.url !== url) fail(`unexpected producer ref response URL ${response.url}`);
|
|
286
|
+
const text = await readBoundedText(response);
|
|
287
|
+
let data;
|
|
288
|
+
try {
|
|
289
|
+
data = JSON.parse(text);
|
|
290
|
+
} catch {
|
|
291
|
+
fail("malformed producer ref JSON");
|
|
292
|
+
}
|
|
293
|
+
if (!isRecord(data) || data["ref"] !== `refs/tags/v${version}`) fail("producer ref mismatch");
|
|
294
|
+
const object = data["object"];
|
|
295
|
+
if (!isRecord(object) || object["type"] !== "commit") fail("producer ref is not a lightweight tag commit");
|
|
296
|
+
const sha = object["sha"];
|
|
297
|
+
if (typeof sha !== "string" || !SHA40_HEX.test(sha)) fail("invalid producer commit sha");
|
|
298
|
+
return sha;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
resolveLatestPiRelease,
|
|
303
|
+
downloadVerifiedPiTarball,
|
|
304
|
+
resolvePiProducerCommit
|
|
305
|
+
};
|