@mutmutco/installer-launcher 0.1.13 → 0.1.14
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/dist/launcher.js +539 -356
- package/dist/launcher.sea.cjs +553 -370
- package/package.json +1 -1
package/dist/launcher.js
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
// src/acquisition.ts
|
|
7
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { join as
|
|
8
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
9
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
10
|
+
import { join as join4, resolve, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "node:path";
|
|
10
11
|
|
|
11
12
|
// src/runtime.ts
|
|
12
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
14
|
import { spawnSync } from "node:child_process";
|
|
14
15
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync, renameSync, rmSync, realpathSync } from "node:fs";
|
|
15
16
|
import { join, relative, isAbsolute } from "node:path";
|
|
17
|
+
|
|
18
|
+
// src/download.ts
|
|
19
|
+
async function readDownload(response, onProgress, expectedSize) {
|
|
20
|
+
const length = expectedSize ?? Number(response.headers.get("content-length"));
|
|
21
|
+
const total = Number.isSafeInteger(length) && length > 0 ? length : void 0;
|
|
22
|
+
let done = 0;
|
|
23
|
+
const report = () => onProgress?.({ done, ...total === void 0 ? {} : { total } });
|
|
24
|
+
report();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
if (response.body) {
|
|
27
|
+
for await (const chunk of response.body) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
done += chunk.length;
|
|
30
|
+
report();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Buffer.concat(chunks);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/runtime.ts
|
|
16
37
|
var NODE_VERSION = "24.20.0";
|
|
17
38
|
var NPM_VERSION = "12.0.2";
|
|
18
39
|
var ARTIFACTS = {
|
|
@@ -23,10 +44,10 @@ var ARTIFACTS = {
|
|
|
23
44
|
"linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
|
|
24
45
|
};
|
|
25
46
|
var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
|
|
26
|
-
async function verifiedDownload(url, path, algorithm, digest, fetchImpl) {
|
|
47
|
+
async function verifiedDownload(url, path, algorithm, digest, fetchImpl, onProgress) {
|
|
27
48
|
const response = await fetchImpl(url);
|
|
28
49
|
if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
|
|
29
|
-
const bytes =
|
|
50
|
+
const bytes = await readDownload(response, onProgress);
|
|
30
51
|
if (createHash(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
|
|
31
52
|
writeFileSync(path, bytes);
|
|
32
53
|
}
|
|
@@ -35,7 +56,7 @@ function command(executable, args) {
|
|
|
35
56
|
if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
|
|
36
57
|
return result.stdout.trim();
|
|
37
58
|
}
|
|
38
|
-
async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
59
|
+
async function acquireRuntime(dir, fetchImpl = fetch, onProgress) {
|
|
39
60
|
const platform = `${process.platform}-${process.arch}`;
|
|
40
61
|
const artifact = ARTIFACTS[platform];
|
|
41
62
|
if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
|
|
@@ -53,13 +74,20 @@ async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
|
53
74
|
}
|
|
54
75
|
const root = mkdtempSync(join(runtimeDir, "runtime-"));
|
|
55
76
|
const archive = join(root, artifact[0]);
|
|
56
|
-
|
|
77
|
+
let downloaded = 0;
|
|
78
|
+
let currentBytes = 0;
|
|
79
|
+
const progress = ({ done }) => {
|
|
80
|
+
currentBytes = done;
|
|
81
|
+
onProgress?.({ done: downloaded + done });
|
|
82
|
+
};
|
|
83
|
+
await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl, progress);
|
|
84
|
+
downloaded += currentBytes;
|
|
57
85
|
const tar = process.platform === "win32" ? join(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
|
|
58
86
|
command(tar, ["-xf", archive, "-C", root]);
|
|
59
87
|
const unpacked = join(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
|
|
60
88
|
const node = join(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
|
|
61
89
|
const npmArchive = join(root, "npm.tgz");
|
|
62
|
-
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl);
|
|
90
|
+
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl, progress);
|
|
63
91
|
const npmRoot = join(root, "npm");
|
|
64
92
|
mkdirSync(npmRoot);
|
|
65
93
|
command(tar, ["-xf", npmArchive, "-C", npmRoot]);
|
|
@@ -164,7 +192,275 @@ function wipeProductDir(dir) {
|
|
|
164
192
|
rmSync2(payload, { force: true, recursive: true });
|
|
165
193
|
}
|
|
166
194
|
|
|
195
|
+
// src/payload.ts
|
|
196
|
+
import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
|
|
197
|
+
import { mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
198
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
199
|
+
import { dirname, join as join3 } from "node:path";
|
|
200
|
+
|
|
201
|
+
// src/canonical.ts
|
|
202
|
+
function canonicalJson(value) {
|
|
203
|
+
return encode(value);
|
|
204
|
+
}
|
|
205
|
+
function encode(value) {
|
|
206
|
+
if (value === null) return "null";
|
|
207
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
208
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
209
|
+
if (typeof value === "number") {
|
|
210
|
+
if (!Number.isFinite(value)) {
|
|
211
|
+
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(value);
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
217
|
+
}
|
|
218
|
+
if (typeof value === "object") {
|
|
219
|
+
const record = value;
|
|
220
|
+
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
221
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
222
|
+
}
|
|
223
|
+
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/config.ts
|
|
227
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
228
|
+
import { getAsset } from "node:sea";
|
|
229
|
+
|
|
230
|
+
// src/module-url.ts
|
|
231
|
+
import { pathToFileURL } from "node:url";
|
|
232
|
+
function moduleUrl() {
|
|
233
|
+
if (typeof __filename === "string") return pathToFileURL(__filename).href;
|
|
234
|
+
return import.meta.url;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/config.ts
|
|
238
|
+
function publicKeyBytes(config) {
|
|
239
|
+
const raw = Buffer.from(config.publicKey, "base64");
|
|
240
|
+
if (raw.length !== 32) {
|
|
241
|
+
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
242
|
+
}
|
|
243
|
+
return raw;
|
|
244
|
+
}
|
|
245
|
+
function loadProductConfig(options = {}) {
|
|
246
|
+
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
247
|
+
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
248
|
+
if (explicit) {
|
|
249
|
+
return parseProductConfig(readFileSync3(explicit, "utf8"));
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
return parseProductConfig(readFileSync3(devFallback, "utf8"));
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const asset = getAsset("product.json", "utf8");
|
|
257
|
+
if (typeof asset === "string") return parseProductConfig(asset);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function parseProductConfig(text) {
|
|
265
|
+
let data;
|
|
266
|
+
try {
|
|
267
|
+
data = JSON.parse(text);
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error("product config is not valid JSON");
|
|
270
|
+
}
|
|
271
|
+
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
272
|
+
const record = data;
|
|
273
|
+
const product = field(record, "product");
|
|
274
|
+
const host = field(record, "host").replace(/\/+$/, "");
|
|
275
|
+
const loginKind = field(record, "loginKind");
|
|
276
|
+
const binName = field(record, "binName");
|
|
277
|
+
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
278
|
+
if (loginKind !== "github" && loginKind !== "google") {
|
|
279
|
+
throw new Error('product config loginKind must be "github" or "google"');
|
|
280
|
+
}
|
|
281
|
+
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
282
|
+
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
283
|
+
publicKeyBytes(config);
|
|
284
|
+
if (loginKind === "github") {
|
|
285
|
+
const clientId = record.githubClientId;
|
|
286
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
287
|
+
throw new Error("product config needs githubClientId for the github loginKind");
|
|
288
|
+
}
|
|
289
|
+
config.githubClientId = clientId;
|
|
290
|
+
} else if (typeof record.githubClientId === "string") {
|
|
291
|
+
config.githubClientId = record.githubClientId;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const url = new URL(host);
|
|
295
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error("product config host must be an http(s) URL");
|
|
298
|
+
}
|
|
299
|
+
return config;
|
|
300
|
+
}
|
|
301
|
+
function field(record, key) {
|
|
302
|
+
const value = record[key];
|
|
303
|
+
return typeof value === "string" ? value : "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/payload.ts
|
|
307
|
+
var NeedsLoginError = class extends Error {
|
|
308
|
+
constructor() {
|
|
309
|
+
super("signed out \u2014 run login first");
|
|
310
|
+
this.name = "NeedsLoginError";
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var ForbiddenError = class extends Error {
|
|
314
|
+
constructor() {
|
|
315
|
+
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
316
|
+
this.name = "ForbiddenError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function canonicalManifestBytes(manifest) {
|
|
320
|
+
return Buffer.from(
|
|
321
|
+
canonicalJson({
|
|
322
|
+
created: manifest.created,
|
|
323
|
+
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
324
|
+
version: manifest.version
|
|
325
|
+
}),
|
|
326
|
+
"utf8"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
function ed25519PublicKey(config) {
|
|
330
|
+
const raw = publicKeyBytes(config);
|
|
331
|
+
return createPublicKey({
|
|
332
|
+
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
333
|
+
format: "jwk"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function verifyManifest(manifest, signature, config) {
|
|
337
|
+
try {
|
|
338
|
+
const signatureBytes = Buffer.from(signature, "base64");
|
|
339
|
+
if (signatureBytes.length === 0) return false;
|
|
340
|
+
return verify(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function verifyFileBytes(entry2, bytes) {
|
|
346
|
+
if (entry2.size !== bytes.length) return false;
|
|
347
|
+
return createHash2("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
348
|
+
}
|
|
349
|
+
async function readErrorCode(response) {
|
|
350
|
+
try {
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
return typeof data.error === "string" ? data.error : "";
|
|
353
|
+
} catch {
|
|
354
|
+
return "";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function throwForStatus(status, errorCode) {
|
|
358
|
+
if (status === 401) throw new NeedsLoginError();
|
|
359
|
+
if (status === 403) throw new ForbiddenError();
|
|
360
|
+
throw new Error(
|
|
361
|
+
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
async function getJson(url, accessToken, fetchImpl) {
|
|
365
|
+
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
366
|
+
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
367
|
+
return { status: response.status, json: await response.json() };
|
|
368
|
+
}
|
|
369
|
+
function parseManifest(json) {
|
|
370
|
+
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
371
|
+
const record = json;
|
|
372
|
+
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
373
|
+
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
374
|
+
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
375
|
+
if (typeof record.signature !== "string" || !record.signature) {
|
|
376
|
+
throw new Error("the release manifest is unsigned");
|
|
377
|
+
}
|
|
378
|
+
const files = record.files.map((entry2) => {
|
|
379
|
+
const file = entry2;
|
|
380
|
+
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
381
|
+
throw new Error("the release manifest lists a malformed file");
|
|
382
|
+
}
|
|
383
|
+
const safe = safeManifestPath(file.path);
|
|
384
|
+
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
385
|
+
return { path: safe, sha256: file.sha256, size: file.size };
|
|
386
|
+
});
|
|
387
|
+
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
388
|
+
}
|
|
389
|
+
function safeManifestPath(rawPath) {
|
|
390
|
+
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
391
|
+
if (rawPath.startsWith("/")) return null;
|
|
392
|
+
const segments = rawPath.split("/");
|
|
393
|
+
for (const segment of segments) {
|
|
394
|
+
if (segment === "" || segment === "." || segment === "..") return null;
|
|
395
|
+
}
|
|
396
|
+
return segments.join("/");
|
|
397
|
+
}
|
|
398
|
+
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
399
|
+
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
400
|
+
const manifest = parseManifest(json);
|
|
401
|
+
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
402
|
+
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
403
|
+
}
|
|
404
|
+
return manifest;
|
|
405
|
+
}
|
|
406
|
+
async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
|
|
407
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
408
|
+
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
409
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
410
|
+
});
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
413
|
+
throwForStatus(response.status, await readErrorCode(response));
|
|
414
|
+
}
|
|
415
|
+
return readDownload(response, onProgress, expectedSize);
|
|
416
|
+
}
|
|
417
|
+
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
418
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
419
|
+
const staging = join3(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
420
|
+
mkdirSync3(staging, { recursive: true });
|
|
421
|
+
try {
|
|
422
|
+
const total = manifest.files.reduce((sum, entry2) => sum + entry2.size, 0);
|
|
423
|
+
let downloaded = 0;
|
|
424
|
+
for (const entry2 of manifest.files) {
|
|
425
|
+
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl, (progress) => options.onProgress?.({ done: downloaded + progress.done, total }), entry2.size);
|
|
426
|
+
downloaded += bytes.length;
|
|
427
|
+
if (!verifyFileBytes(entry2, bytes)) {
|
|
428
|
+
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
429
|
+
}
|
|
430
|
+
const dest = join3(staging, entry2.path);
|
|
431
|
+
mkdirSync3(dirname(dest), { recursive: true });
|
|
432
|
+
writeFileSync3(dest, bytes);
|
|
433
|
+
}
|
|
434
|
+
const target = join3(dir, "payload");
|
|
435
|
+
mkdirSync3(dir, { recursive: true });
|
|
436
|
+
rmSync3(target, { force: true, recursive: true });
|
|
437
|
+
renameSync3(staging, target);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
rmSync3(staging, { force: true, recursive: true });
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
return manifest.version;
|
|
443
|
+
}
|
|
444
|
+
|
|
167
445
|
// src/acquisition.ts
|
|
446
|
+
function reusableCandidate(dir, manifest) {
|
|
447
|
+
try {
|
|
448
|
+
const saved = JSON.parse(readFileSync4(join4(dir, "acquisition-cache.json"), "utf8"));
|
|
449
|
+
if (saved.signature !== manifest.signature) return null;
|
|
450
|
+
const root = candidateRoot(dir, saved.candidate);
|
|
451
|
+
if (!existsSync4(join4(root, "acquired.json"))) return null;
|
|
452
|
+
for (const file of manifest.files) {
|
|
453
|
+
if (!safePath(file.path) || !verifyFileBytes(file, readFileSync4(join4(root, "payload", file.path)))) return null;
|
|
454
|
+
}
|
|
455
|
+
return saved.candidate;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function rememberCandidate(dir, candidate, manifest) {
|
|
461
|
+
candidateRoot(dir, candidate);
|
|
462
|
+
writeFileSync4(join4(dir, "acquisition-cache.json"), JSON.stringify({ candidate, signature: manifest.signature }), { mode: 384 });
|
|
463
|
+
}
|
|
168
464
|
function safePath(value) {
|
|
169
465
|
return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
170
466
|
}
|
|
@@ -172,7 +468,7 @@ function argumentsValid(value) {
|
|
|
172
468
|
return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
|
|
173
469
|
}
|
|
174
470
|
function readAcquisition(payload) {
|
|
175
|
-
const metadata = JSON.parse(
|
|
471
|
+
const metadata = JSON.parse(readFileSync4(join4(payload, "payload.json"), "utf8"));
|
|
176
472
|
if (metadata.acquisition === void 0) return null;
|
|
177
473
|
const a = metadata.acquisition;
|
|
178
474
|
if (!a || a.schema !== 1 || !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(a.package) || !safePath(a.archive) || !a.archive.endsWith(".tgz") || !safePath(a.convergeEntry) || !safePath(a.rollbackEntry) || !safePath(a.runEntry) || !argumentsValid(a.convergeArgs) || !argumentsValid(a.rollbackArgs) || !argumentsValid(a.repairArgs) || !Array.isArray(a.platforms) || !a.platforms.length || a.platforms.some((p) => !["win32-x64", "win32-arm64", "darwin-arm64", "linux-x64", "linux-arm64"].includes(p))) {
|
|
@@ -188,25 +484,25 @@ function runtimeEnvironment(node, inherited, platform = process.platform) {
|
|
|
188
484
|
current = (keys.length ? env[keys[0]] : "") ?? "";
|
|
189
485
|
for (const key of keys) delete env[key];
|
|
190
486
|
}
|
|
191
|
-
env.PATH = `${
|
|
487
|
+
env.PATH = `${dirname2(node)}${platform === "win32" ? ";" : ":"}${current}`;
|
|
192
488
|
return env;
|
|
193
489
|
}
|
|
194
490
|
function lockInstallation(dir) {
|
|
195
|
-
|
|
196
|
-
const path =
|
|
491
|
+
mkdirSync4(dir, { recursive: true });
|
|
492
|
+
const path = join4(dir, "installation.lock");
|
|
197
493
|
const owner = `${process.pid}:${randomUUID3()}`;
|
|
198
494
|
try {
|
|
199
|
-
|
|
495
|
+
writeFileSync4(path, owner, { flag: "wx", mode: 384 });
|
|
200
496
|
} catch (error) {
|
|
201
497
|
if (error.code !== "EEXIST") throw error;
|
|
202
498
|
const recovery = `${path}.recovery`;
|
|
203
499
|
try {
|
|
204
|
-
|
|
500
|
+
writeFileSync4(recovery, owner, { flag: "wx", mode: 384, flush: true });
|
|
205
501
|
} catch {
|
|
206
502
|
throw new Error("installation lock recovery is already pending");
|
|
207
503
|
}
|
|
208
504
|
try {
|
|
209
|
-
const previous =
|
|
505
|
+
const previous = readFileSync4(path, "utf8");
|
|
210
506
|
const pid = Number(previous.split(":")[0]);
|
|
211
507
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
|
|
212
508
|
try {
|
|
@@ -216,29 +512,29 @@ function lockInstallation(dir) {
|
|
|
216
512
|
if (probe.code !== "ESRCH") throw probe;
|
|
217
513
|
}
|
|
218
514
|
unlinkSync(path);
|
|
219
|
-
|
|
515
|
+
writeFileSync4(path, owner, { flag: "wx", mode: 384, flush: true });
|
|
220
516
|
} finally {
|
|
221
517
|
unlinkSync(recovery);
|
|
222
518
|
}
|
|
223
519
|
}
|
|
224
520
|
let released = false;
|
|
225
521
|
return () => {
|
|
226
|
-
if (!released &&
|
|
522
|
+
if (!released && readFileSync4(path, "utf8") === owner) unlinkSync(path);
|
|
227
523
|
released = true;
|
|
228
524
|
};
|
|
229
525
|
}
|
|
230
526
|
function pendingPath(dir) {
|
|
231
|
-
return
|
|
527
|
+
return join4(dir, "acquisition-pending.json");
|
|
232
528
|
}
|
|
233
529
|
function candidateRoot(dir, candidate) {
|
|
234
530
|
if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
|
|
235
|
-
return
|
|
531
|
+
return join4(dir, "candidates", candidate);
|
|
236
532
|
}
|
|
237
533
|
function packageRoot(prefix, acquisition) {
|
|
238
|
-
return
|
|
534
|
+
return join4(prefix, "node_modules", acquisition.package);
|
|
239
535
|
}
|
|
240
536
|
function entry(root, path) {
|
|
241
|
-
const actual = realpathSync2(
|
|
537
|
+
const actual = realpathSync2(join4(root, path));
|
|
242
538
|
const rel = relative2(realpathSync2(root), actual);
|
|
243
539
|
if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("acquired entry escapes package");
|
|
244
540
|
return actual;
|
|
@@ -250,54 +546,59 @@ function acquisitionRepairCommand(dir, version) {
|
|
|
250
546
|
const state = readState(dir);
|
|
251
547
|
if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
|
|
252
548
|
const root = candidateRoot(dir, state.acquired.candidate);
|
|
253
|
-
const acquisition = readAcquisition(
|
|
549
|
+
const acquisition = readAcquisition(join4(root, "payload"));
|
|
254
550
|
if (!acquisition) throw new Error("installed acquisition declaration is missing");
|
|
255
|
-
const prefix =
|
|
551
|
+
const prefix = join4(root, "prefix");
|
|
256
552
|
const productRoot = packageRoot(prefix, acquisition);
|
|
257
|
-
const pkg = JSON.parse(
|
|
553
|
+
const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
|
|
258
554
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
|
|
259
555
|
return argv(realpathSync2(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
|
|
260
556
|
}
|
|
261
557
|
async function recoverAcquisition(dir, run2, env) {
|
|
262
|
-
if (!
|
|
263
|
-
const pending = JSON.parse(
|
|
558
|
+
if (!existsSync4(pendingPath(dir))) return;
|
|
559
|
+
const pending = JSON.parse(readFileSync4(pendingPath(dir), "utf8"));
|
|
264
560
|
if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
|
|
265
561
|
const root = candidateRoot(dir, pending.candidate);
|
|
266
562
|
if (readState(dir)?.acquired?.candidate === pending.candidate) {
|
|
267
563
|
unlinkSync(pendingPath(dir));
|
|
268
564
|
return;
|
|
269
565
|
}
|
|
270
|
-
const acquisition = readAcquisition(
|
|
566
|
+
const acquisition = readAcquisition(join4(root, "payload"));
|
|
271
567
|
if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
|
|
272
|
-
const prefix =
|
|
568
|
+
const prefix = join4(root, "prefix");
|
|
273
569
|
const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
|
|
274
570
|
if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
|
|
275
571
|
if (pending.previous) writeState(dir, pending.previous);
|
|
276
|
-
else if (
|
|
572
|
+
else if (existsSync4(statePath(dir))) unlinkSync(statePath(dir));
|
|
277
573
|
unlinkSync(pendingPath(dir));
|
|
278
574
|
}
|
|
279
575
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
280
576
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
281
577
|
const root = candidateRoot(dir, candidate);
|
|
282
|
-
const payload =
|
|
283
|
-
const prefix =
|
|
284
|
-
|
|
285
|
-
const
|
|
578
|
+
const payload = join4(root, "payload");
|
|
579
|
+
const prefix = join4(root, "prefix");
|
|
580
|
+
const prepared = join4(root, "acquired.json");
|
|
581
|
+
const archiveHash = createHash3("sha256").update(readFileSync4(join4(payload, acquisition.archive))).digest("hex");
|
|
582
|
+
const reused = existsSync4(prepared);
|
|
583
|
+
if (reused && JSON.parse(readFileSync4(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
|
|
584
|
+
if (!reused) mkdirSync4(prefix);
|
|
585
|
+
const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
|
|
286
586
|
const env = runtimeEnvironment(runtime.node, options.env);
|
|
287
587
|
const npmEnv = { ...env };
|
|
288
588
|
for (const key of Object.keys(npmEnv)) {
|
|
289
589
|
if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
|
|
290
590
|
}
|
|
291
|
-
const npmrc =
|
|
292
|
-
const globalrc =
|
|
293
|
-
|
|
294
|
-
|
|
591
|
+
const npmrc = join4(root, "public.npmrc");
|
|
592
|
+
const globalrc = join4(root, "global.npmrc");
|
|
593
|
+
writeFileSync4(npmrc, "registry=https://registry.npmjs.org/\n");
|
|
594
|
+
writeFileSync4(globalrc, "");
|
|
295
595
|
const install = [
|
|
296
596
|
runtime.node,
|
|
297
597
|
runtime.npm,
|
|
298
598
|
"install",
|
|
299
599
|
"--prefix",
|
|
300
600
|
prefix,
|
|
601
|
+
"--install-strategy=shallow",
|
|
301
602
|
"--ignore-scripts",
|
|
302
603
|
"--no-audit",
|
|
303
604
|
"--no-fund",
|
|
@@ -309,15 +610,16 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
309
610
|
"https://registry.npmjs.org/",
|
|
310
611
|
resolve(payload, acquisition.archive)
|
|
311
612
|
];
|
|
312
|
-
if (!await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
613
|
+
if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
313
614
|
const productRoot = packageRoot(prefix, acquisition);
|
|
314
|
-
const pkg = JSON.parse(
|
|
615
|
+
const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
|
|
315
616
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
|
|
316
617
|
const runEntry = entry(productRoot, acquisition.runEntry);
|
|
317
618
|
const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
|
|
318
619
|
entry(productRoot, acquisition.rollbackEntry);
|
|
620
|
+
if (!reused) writeFileSync4(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
|
|
319
621
|
const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
|
|
320
|
-
|
|
622
|
+
writeFileSync4(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
|
|
321
623
|
try {
|
|
322
624
|
if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
|
|
323
625
|
(options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
|
|
@@ -336,36 +638,36 @@ import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:pa
|
|
|
336
638
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
337
639
|
|
|
338
640
|
// ../face/src/face.ts
|
|
339
|
-
import { writeSync } from "node:fs";
|
|
641
|
+
import { appendFileSync, writeSync } from "node:fs";
|
|
340
642
|
|
|
341
643
|
// ../face/src/products.ts
|
|
342
644
|
var PRODUCTS = Object.freeze({
|
|
343
645
|
"mm-strategy": Object.freeze({
|
|
344
646
|
name: "MM Strategy",
|
|
345
|
-
installWarm: "Welcome. Let's
|
|
647
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
346
648
|
accent: "38;2;249;115;22",
|
|
347
|
-
warm: "Welcome.
|
|
649
|
+
warm: "Welcome back. Checking for updates...",
|
|
348
650
|
doctor: "mm-strategy doctor"
|
|
349
651
|
}),
|
|
350
652
|
"mmi-hub": Object.freeze({
|
|
351
653
|
name: "mmi-hub",
|
|
352
|
-
installWarm: "Welcome.
|
|
654
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
353
655
|
accent: "38;2;125;211;252",
|
|
354
|
-
warm: "Welcome back. Checking
|
|
656
|
+
warm: "Welcome back. Checking for updates...",
|
|
355
657
|
doctor: "mmi doctor"
|
|
356
658
|
}),
|
|
357
659
|
"jerv-hub": Object.freeze({
|
|
358
660
|
name: "jerv-hub",
|
|
359
|
-
installWarm: "Welcome.
|
|
661
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
360
662
|
accent: "38;2;248;113;113",
|
|
361
|
-
warm: "Welcome back. Checking
|
|
663
|
+
warm: "Welcome back. Checking for updates...",
|
|
362
664
|
doctor: "jerv doctor"
|
|
363
665
|
}),
|
|
364
666
|
jervcode: Object.freeze({
|
|
365
667
|
name: "JervCode",
|
|
366
|
-
installWarm: "Welcome.
|
|
668
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
367
669
|
accent: "38;2;192;132;252",
|
|
368
|
-
warm: "Welcome back.
|
|
670
|
+
warm: "Welcome back. Checking for updates...",
|
|
369
671
|
doctor: "jervcode doctor"
|
|
370
672
|
})
|
|
371
673
|
});
|
|
@@ -379,6 +681,7 @@ function identityFor(product) {
|
|
|
379
681
|
|
|
380
682
|
// ../face/src/face.ts
|
|
381
683
|
var GLYPH = Object.freeze({
|
|
684
|
+
clock: "\u23F0",
|
|
382
685
|
diamond: "\u25C6",
|
|
383
686
|
hollow: "\u25C7",
|
|
384
687
|
bar: "\u2502",
|
|
@@ -450,12 +753,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
450
753
|
const continuesFace = continuedPhases.size > 0;
|
|
451
754
|
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
452
755
|
const progressFd = readProgressFd(env);
|
|
756
|
+
const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
|
|
453
757
|
const emitMilestone = (title, measure, kind) => {
|
|
454
|
-
if (progressFd === null) return false;
|
|
758
|
+
if (progressFd === null && !progressFile) return false;
|
|
455
759
|
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
456
760
|
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
761
|
+
if (kind === "running" && typeof measure === "string") record.measure = measure;
|
|
457
762
|
try {
|
|
458
|
-
|
|
763
|
+
if (progressFile) appendFileSync(progressFile, `${JSON.stringify(record)}
|
|
764
|
+
`);
|
|
765
|
+
else writeSync(progressFd, `${JSON.stringify(record)}
|
|
459
766
|
`);
|
|
460
767
|
return true;
|
|
461
768
|
} catch {
|
|
@@ -474,7 +781,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
474
781
|
};
|
|
475
782
|
const step = (title, measure = null, kind = "ok") => {
|
|
476
783
|
if (emitMilestone(title, measure, kind)) return "";
|
|
477
|
-
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
784
|
+
const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
478
785
|
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
479
786
|
const time = measured;
|
|
480
787
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
@@ -484,7 +791,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
484
791
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
485
792
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
486
793
|
};
|
|
487
|
-
const relay = (text) => String(text).
|
|
794
|
+
const relay = (text) => String(text).replace(/\r?\n$/, "").split(/\r?\n/).flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
|
|
488
795
|
const receipt = (lines, { ready = true } = {}) => {
|
|
489
796
|
if (ready && nested) return [];
|
|
490
797
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
@@ -515,14 +822,14 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
515
822
|
};
|
|
516
823
|
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
517
824
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
518
|
-
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
825
|
+
return { identity, width, nested, emitsProgress: progressFd !== null || Boolean(progressFile), welcome, continues, step, progress: (title, measure = null) => emitMilestone(title, measure, "running"), relay, receipt, outcome, signOff, refusal, paint };
|
|
519
826
|
}
|
|
520
827
|
|
|
521
828
|
// ../face/src/shell.ts
|
|
522
829
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
523
830
|
|
|
524
831
|
// ../face/src/spinner.ts
|
|
525
|
-
import { appendFileSync, writeSync as writeSync2 } from "node:fs";
|
|
832
|
+
import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
|
|
526
833
|
import { Worker } from "node:worker_threads";
|
|
527
834
|
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
528
835
|
var WORKER_SOURCE = `
|
|
@@ -534,7 +841,7 @@ function draw() {
|
|
|
534
841
|
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
535
842
|
try {
|
|
536
843
|
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
537
|
-
const text = frames[frame++ % frames.length];
|
|
844
|
+
const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
|
|
538
845
|
writeSync(2, text);
|
|
539
846
|
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
540
847
|
} finally {
|
|
@@ -546,7 +853,6 @@ parentPort.on('message', (next) => {
|
|
|
546
853
|
if (Atomics.load(control, 2)) return;
|
|
547
854
|
frames = next.frames;
|
|
548
855
|
frame = next.frame;
|
|
549
|
-
Atomics.store(control, 0, 0);
|
|
550
856
|
});
|
|
551
857
|
setInterval(draw, workerData.intervalMs);
|
|
552
858
|
`;
|
|
@@ -557,20 +863,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
557
863
|
let control = null;
|
|
558
864
|
let frames = [];
|
|
559
865
|
let frame = 0;
|
|
866
|
+
let started = 0;
|
|
867
|
+
let suspended = false;
|
|
560
868
|
const write = (text) => {
|
|
561
869
|
if (stream) stream.write(text);
|
|
562
870
|
else {
|
|
563
871
|
writeSync2(2, text);
|
|
564
|
-
if (transcriptPath)
|
|
872
|
+
if (transcriptPath) appendFileSync2(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
565
873
|
`);
|
|
566
874
|
}
|
|
567
875
|
};
|
|
568
876
|
const render = (text, measure) => {
|
|
569
|
-
const line = face.step(text, measure, "note").split("\n")[0];
|
|
877
|
+
const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
|
|
570
878
|
frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
|
|
571
879
|
};
|
|
572
880
|
const draw = () => {
|
|
573
|
-
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
881
|
+
if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
|
|
574
882
|
};
|
|
575
883
|
const pause = () => {
|
|
576
884
|
if (!control) return;
|
|
@@ -590,6 +898,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
590
898
|
start(text, measure = null) {
|
|
591
899
|
if (!animate) return;
|
|
592
900
|
halt();
|
|
901
|
+
suspended = false;
|
|
902
|
+
started = Date.now();
|
|
593
903
|
render(text, measure);
|
|
594
904
|
frame = 0;
|
|
595
905
|
draw();
|
|
@@ -603,7 +913,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
603
913
|
frames,
|
|
604
914
|
frame,
|
|
605
915
|
intervalMs,
|
|
606
|
-
transcriptPath
|
|
916
|
+
transcriptPath,
|
|
917
|
+
started
|
|
607
918
|
} });
|
|
608
919
|
const active = worker;
|
|
609
920
|
worker.on("error", () => {
|
|
@@ -621,9 +932,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
621
932
|
render(text, measure);
|
|
622
933
|
draw();
|
|
623
934
|
worker?.postMessage({ frames, frame });
|
|
935
|
+
if (control && !suspended) Atomics.store(control, 0, 0);
|
|
936
|
+
},
|
|
937
|
+
pause() {
|
|
938
|
+
if (suspended) return;
|
|
939
|
+
suspended = true;
|
|
940
|
+
pause();
|
|
941
|
+
if (animate && frames.length) write("\r\x1B[2K");
|
|
942
|
+
},
|
|
943
|
+
resume() {
|
|
944
|
+
suspended = false;
|
|
945
|
+
if (control) Atomics.store(control, 0, 0);
|
|
946
|
+
if (timer || worker) draw();
|
|
624
947
|
},
|
|
625
948
|
stop() {
|
|
626
949
|
halt();
|
|
950
|
+
frames = [];
|
|
627
951
|
if (animate) write("\r\x1B[2K");
|
|
628
952
|
}
|
|
629
953
|
};
|
|
@@ -634,10 +958,10 @@ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
|
634
958
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
635
959
|
|
|
636
960
|
// ../face/src/run.ts
|
|
637
|
-
import { appendFileSync as
|
|
961
|
+
import { appendFileSync as appendFileSync3 } from "node:fs";
|
|
638
962
|
|
|
639
963
|
// ../face/src/outcome.ts
|
|
640
|
-
import { readFileSync as
|
|
964
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
641
965
|
var counts = ["total", "updated", "failed"];
|
|
642
966
|
var strings = ["version", "retry", "detail", "logPath"];
|
|
643
967
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
@@ -659,12 +983,12 @@ function validateInstallerOutcome(value) {
|
|
|
659
983
|
return { ...facts };
|
|
660
984
|
}
|
|
661
985
|
function writeInstallerOutcome(path, value) {
|
|
662
|
-
|
|
986
|
+
writeFileSync5(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
663
987
|
}
|
|
664
988
|
function readInstallerOutcome(path) {
|
|
665
989
|
let text;
|
|
666
990
|
try {
|
|
667
|
-
text =
|
|
991
|
+
text = readFileSync5(path, "utf8");
|
|
668
992
|
} catch (error) {
|
|
669
993
|
if (error.code === "ENOENT") return void 0;
|
|
670
994
|
throw error;
|
|
@@ -727,6 +1051,7 @@ var PHASES = {
|
|
|
727
1051
|
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
728
1052
|
verify: ["Verifying the payload", "Verified the payload"],
|
|
729
1053
|
install: ["Installing the product", "Installed the product"],
|
|
1054
|
+
configure: ["Configuring the product", "Configured the product"],
|
|
730
1055
|
activate: ["Activating surfaces", "Activated surfaces"],
|
|
731
1056
|
doctor: ["Checking health", "Checked health"],
|
|
732
1057
|
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
@@ -756,7 +1081,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
756
1081
|
if (!text) return;
|
|
757
1082
|
write(text, channel);
|
|
758
1083
|
if (env.MM_FACE_TRANSCRIPT) {
|
|
759
|
-
|
|
1084
|
+
appendFileSync3(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
760
1085
|
`, "utf8");
|
|
761
1086
|
}
|
|
762
1087
|
};
|
|
@@ -771,6 +1096,17 @@ function createInstallerRun(value, options = {}) {
|
|
|
771
1096
|
return true;
|
|
772
1097
|
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
773
1098
|
});
|
|
1099
|
+
let activeTitle;
|
|
1100
|
+
let chunkOpen = false;
|
|
1101
|
+
let chunkChannel = "stdout";
|
|
1102
|
+
let chunkColumn = 0;
|
|
1103
|
+
const trailingCR = /* @__PURE__ */ new Set();
|
|
1104
|
+
let deferredActivity;
|
|
1105
|
+
const closeChunk = () => {
|
|
1106
|
+
if (chunkOpen) emit("\n", chunkChannel);
|
|
1107
|
+
chunkOpen = false;
|
|
1108
|
+
chunkColumn = 0;
|
|
1109
|
+
};
|
|
774
1110
|
let started = false;
|
|
775
1111
|
let finished = false;
|
|
776
1112
|
const start = () => {
|
|
@@ -785,6 +1121,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
785
1121
|
}
|
|
786
1122
|
};
|
|
787
1123
|
const durable = (title, measure, kind) => {
|
|
1124
|
+
closeChunk();
|
|
1125
|
+
activeTitle = void 0;
|
|
1126
|
+
deferredActivity = void 0;
|
|
788
1127
|
spinner.stop();
|
|
789
1128
|
const rendered = face.step(title, measure, kind);
|
|
790
1129
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
@@ -801,7 +1140,12 @@ function createInstallerRun(value, options = {}) {
|
|
|
801
1140
|
const state = facts.state ?? "ok";
|
|
802
1141
|
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
803
1142
|
if (state === "running") {
|
|
804
|
-
|
|
1143
|
+
if (face.progress(title, facts.measure ?? null)) return;
|
|
1144
|
+
if (activeTitle === title) spinner.say(title, facts.measure ?? null);
|
|
1145
|
+
else {
|
|
1146
|
+
activeTitle = title;
|
|
1147
|
+
spinner.start(title, facts.measure ?? null);
|
|
1148
|
+
}
|
|
805
1149
|
return;
|
|
806
1150
|
}
|
|
807
1151
|
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
@@ -821,8 +1165,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
821
1165
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
822
1166
|
if (facts.detail) run2.relay(facts.detail);
|
|
823
1167
|
},
|
|
824
|
-
milestone({ step, state, ms }) {
|
|
1168
|
+
milestone({ step, state, ms, measure }) {
|
|
825
1169
|
start();
|
|
1170
|
+
if (state === "running") {
|
|
1171
|
+
if (chunkOpen) {
|
|
1172
|
+
deferredActivity = { title: step, measure: measure ?? null };
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (activeTitle === step) spinner.say(step, measure ?? null);
|
|
1176
|
+
else {
|
|
1177
|
+
activeTitle = step;
|
|
1178
|
+
spinner.start(step, measure ?? null);
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
826
1182
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
827
1183
|
},
|
|
828
1184
|
signIn({ url, code }) {
|
|
@@ -836,7 +1192,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
836
1192
|
},
|
|
837
1193
|
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
838
1194
|
relay(text, channel = "stdout", record = true) {
|
|
839
|
-
|
|
1195
|
+
closeChunk();
|
|
1196
|
+
spinner.pause();
|
|
840
1197
|
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
841
1198
|
for (const row of rendered.split("\n")) if (row) {
|
|
842
1199
|
emit(`${row}
|
|
@@ -844,6 +1201,37 @@ function createInstallerRun(value, options = {}) {
|
|
|
844
1201
|
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
845
1202
|
`);
|
|
846
1203
|
}
|
|
1204
|
+
spinner.resume();
|
|
1205
|
+
},
|
|
1206
|
+
relayChunk(text, channel = "stdout") {
|
|
1207
|
+
spinner.pause();
|
|
1208
|
+
if (chunkOpen && chunkChannel !== channel) closeChunk();
|
|
1209
|
+
chunkChannel = channel;
|
|
1210
|
+
if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
|
|
1211
|
+
if (text.endsWith("\r")) trailingCR.add(channel);
|
|
1212
|
+
const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
|
|
1213
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1214
|
+
const characters = [...stripColor(parts[i])];
|
|
1215
|
+
while (characters.length) {
|
|
1216
|
+
if (tty && chunkColumn >= face.width - 6) closeChunk();
|
|
1217
|
+
const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
|
|
1218
|
+
emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
|
|
1219
|
+
chunkColumn += [...part].length;
|
|
1220
|
+
chunkOpen = true;
|
|
1221
|
+
}
|
|
1222
|
+
if (i < parts.length - 1) {
|
|
1223
|
+
emit("\n", channel);
|
|
1224
|
+
chunkOpen = false;
|
|
1225
|
+
chunkColumn = 0;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (!chunkOpen) {
|
|
1229
|
+
if (deferredActivity) {
|
|
1230
|
+
activeTitle = deferredActivity.title;
|
|
1231
|
+
spinner.start(deferredActivity.title, deferredActivity.measure);
|
|
1232
|
+
deferredActivity = void 0;
|
|
1233
|
+
} else spinner.resume();
|
|
1234
|
+
}
|
|
847
1235
|
},
|
|
848
1236
|
cancel() {
|
|
849
1237
|
if (finished) return;
|
|
@@ -852,6 +1240,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
852
1240
|
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
853
1241
|
);
|
|
854
1242
|
start();
|
|
1243
|
+
closeChunk();
|
|
1244
|
+
activeTitle = void 0;
|
|
855
1245
|
spinner.stop();
|
|
856
1246
|
finished = true;
|
|
857
1247
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
@@ -864,6 +1254,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
864
1254
|
validateInstallerOutcome(facts);
|
|
865
1255
|
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
866
1256
|
start();
|
|
1257
|
+
closeChunk();
|
|
1258
|
+
activeTitle = void 0;
|
|
867
1259
|
spinner.stop();
|
|
868
1260
|
finished = true;
|
|
869
1261
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
@@ -884,6 +1276,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
884
1276
|
if (tty) lines([face.signOff()]);
|
|
885
1277
|
},
|
|
886
1278
|
stop() {
|
|
1279
|
+
closeChunk();
|
|
1280
|
+
activeTitle = void 0;
|
|
887
1281
|
spinner.stop();
|
|
888
1282
|
}
|
|
889
1283
|
};
|
|
@@ -892,9 +1286,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
892
1286
|
|
|
893
1287
|
// src/autoupdate.ts
|
|
894
1288
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
895
|
-
import { existsSync as
|
|
896
|
-
import { tmpdir as
|
|
897
|
-
import { join as
|
|
1289
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1290
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1291
|
+
import { join as join5 } from "node:path";
|
|
898
1292
|
function schedulePlatform(override) {
|
|
899
1293
|
const platform = override ?? process.platform;
|
|
900
1294
|
if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
|
|
@@ -911,8 +1305,8 @@ function defaultExec(command2, args) {
|
|
|
911
1305
|
}
|
|
912
1306
|
function homeOf(options) {
|
|
913
1307
|
if (options.homeDir) return options.homeDir;
|
|
914
|
-
if (process.platform === "win32") return process.env.USERPROFILE ??
|
|
915
|
-
return process.env.HOME ??
|
|
1308
|
+
if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir3();
|
|
1309
|
+
return process.env.HOME ?? tmpdir3();
|
|
916
1310
|
}
|
|
917
1311
|
function scheduleName(config) {
|
|
918
1312
|
return `${config.binName} autoupdate`;
|
|
@@ -938,10 +1332,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
938
1332
|
}
|
|
939
1333
|
if (platform === "darwin") {
|
|
940
1334
|
const label2 = scheduleLabel(config);
|
|
941
|
-
const dir2 =
|
|
942
|
-
|
|
943
|
-
const plist =
|
|
944
|
-
|
|
1335
|
+
const dir2 = join5(home, "Library", "LaunchAgents");
|
|
1336
|
+
mkdirSync5(dir2, { recursive: true });
|
|
1337
|
+
const plist = join5(dir2, `${label2}.plist`);
|
|
1338
|
+
writeFileSync6(plist, darwinPlist(label2, command2));
|
|
945
1339
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
946
1340
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
947
1341
|
if (result2.code !== 0) {
|
|
@@ -950,10 +1344,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
950
1344
|
return;
|
|
951
1345
|
}
|
|
952
1346
|
const label = scheduleLabel(config);
|
|
953
|
-
const dir =
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1347
|
+
const dir = join5(home, ".config", "systemd", "user");
|
|
1348
|
+
mkdirSync5(dir, { recursive: true });
|
|
1349
|
+
writeFileSync6(join5(dir, `${label}.service`), linuxService(command2));
|
|
1350
|
+
writeFileSync6(join5(dir, `${label}.timer`), linuxTimer(label));
|
|
957
1351
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
958
1352
|
if (reload.code !== 0) {
|
|
959
1353
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -978,14 +1372,14 @@ function disableSchedule(config, options = {}) {
|
|
|
978
1372
|
if (platform === "darwin") {
|
|
979
1373
|
const label2 = scheduleLabel(config);
|
|
980
1374
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
981
|
-
|
|
1375
|
+
rmSync4(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
982
1376
|
return;
|
|
983
1377
|
}
|
|
984
1378
|
const label = scheduleLabel(config);
|
|
985
|
-
const dir =
|
|
1379
|
+
const dir = join5(home, ".config", "systemd", "user");
|
|
986
1380
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
987
|
-
|
|
988
|
-
|
|
1381
|
+
rmSync4(join5(dir, `${label}.service`), { force: true });
|
|
1382
|
+
rmSync4(join5(dir, `${label}.timer`), { force: true });
|
|
989
1383
|
}
|
|
990
1384
|
function querySchedule(config, options = {}) {
|
|
991
1385
|
const platform = schedulePlatform(options.platform);
|
|
@@ -1003,12 +1397,12 @@ function querySchedule(config, options = {}) {
|
|
|
1003
1397
|
return state2;
|
|
1004
1398
|
}
|
|
1005
1399
|
if (platform === "darwin") {
|
|
1006
|
-
const plist =
|
|
1007
|
-
if (!
|
|
1400
|
+
const plist = join5(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
1401
|
+
if (!existsSync5(plist)) return { supported: true, enabled: false };
|
|
1008
1402
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
1009
1403
|
}
|
|
1010
|
-
const timer =
|
|
1011
|
-
if (!
|
|
1404
|
+
const timer = join5(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
1405
|
+
if (!existsSync5(timer)) return { supported: true, enabled: false };
|
|
1012
1406
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
1013
1407
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
1014
1408
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -1065,86 +1459,6 @@ function firstLine(text) {
|
|
|
1065
1459
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1066
1460
|
}
|
|
1067
1461
|
|
|
1068
|
-
// src/config.ts
|
|
1069
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1070
|
-
import { getAsset } from "node:sea";
|
|
1071
|
-
|
|
1072
|
-
// src/module-url.ts
|
|
1073
|
-
import { pathToFileURL } from "node:url";
|
|
1074
|
-
function moduleUrl() {
|
|
1075
|
-
if (typeof __filename === "string") return pathToFileURL(__filename).href;
|
|
1076
|
-
return import.meta.url;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// src/config.ts
|
|
1080
|
-
function publicKeyBytes(config) {
|
|
1081
|
-
const raw = Buffer.from(config.publicKey, "base64");
|
|
1082
|
-
if (raw.length !== 32) {
|
|
1083
|
-
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
1084
|
-
}
|
|
1085
|
-
return raw;
|
|
1086
|
-
}
|
|
1087
|
-
function loadProductConfig(options = {}) {
|
|
1088
|
-
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
1089
|
-
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
1090
|
-
if (explicit) {
|
|
1091
|
-
return parseProductConfig(readFileSync6(explicit, "utf8"));
|
|
1092
|
-
}
|
|
1093
|
-
try {
|
|
1094
|
-
return parseProductConfig(readFileSync6(devFallback, "utf8"));
|
|
1095
|
-
} catch {
|
|
1096
|
-
}
|
|
1097
|
-
try {
|
|
1098
|
-
const asset = getAsset("product.json", "utf8");
|
|
1099
|
-
if (typeof asset === "string") return parseProductConfig(asset);
|
|
1100
|
-
} catch {
|
|
1101
|
-
}
|
|
1102
|
-
throw new Error(
|
|
1103
|
-
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
function parseProductConfig(text) {
|
|
1107
|
-
let data;
|
|
1108
|
-
try {
|
|
1109
|
-
data = JSON.parse(text);
|
|
1110
|
-
} catch {
|
|
1111
|
-
throw new Error("product config is not valid JSON");
|
|
1112
|
-
}
|
|
1113
|
-
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
1114
|
-
const record = data;
|
|
1115
|
-
const product = field(record, "product");
|
|
1116
|
-
const host = field(record, "host").replace(/\/+$/, "");
|
|
1117
|
-
const loginKind = field(record, "loginKind");
|
|
1118
|
-
const binName = field(record, "binName");
|
|
1119
|
-
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
1120
|
-
if (loginKind !== "github" && loginKind !== "google") {
|
|
1121
|
-
throw new Error('product config loginKind must be "github" or "google"');
|
|
1122
|
-
}
|
|
1123
|
-
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
1124
|
-
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
1125
|
-
publicKeyBytes(config);
|
|
1126
|
-
if (loginKind === "github") {
|
|
1127
|
-
const clientId = record.githubClientId;
|
|
1128
|
-
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
1129
|
-
throw new Error("product config needs githubClientId for the github loginKind");
|
|
1130
|
-
}
|
|
1131
|
-
config.githubClientId = clientId;
|
|
1132
|
-
} else if (typeof record.githubClientId === "string") {
|
|
1133
|
-
config.githubClientId = record.githubClientId;
|
|
1134
|
-
}
|
|
1135
|
-
try {
|
|
1136
|
-
const url = new URL(host);
|
|
1137
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
1138
|
-
} catch {
|
|
1139
|
-
throw new Error("product config host must be an http(s) URL");
|
|
1140
|
-
}
|
|
1141
|
-
return config;
|
|
1142
|
-
}
|
|
1143
|
-
function field(record, key) {
|
|
1144
|
-
const value = record[key];
|
|
1145
|
-
return typeof value === "string" ? value : "";
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
1462
|
// src/login-github.ts
|
|
1149
1463
|
import { spawn } from "node:child_process";
|
|
1150
1464
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1267,7 +1581,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
1267
1581
|
}
|
|
1268
1582
|
|
|
1269
1583
|
// src/login-google.ts
|
|
1270
|
-
import { createHash as
|
|
1584
|
+
import { createHash as createHash4, randomBytes } from "node:crypto";
|
|
1271
1585
|
import { createServer } from "node:http";
|
|
1272
1586
|
var b64url = (bytes) => bytes.toString("base64url");
|
|
1273
1587
|
async function loginGoogle(options) {
|
|
@@ -1290,7 +1604,7 @@ async function loginGoogle(options) {
|
|
|
1290
1604
|
throw new Error("the sign-in server returned a malformed registration");
|
|
1291
1605
|
}
|
|
1292
1606
|
const verifier = b64url(randomBytes(32));
|
|
1293
|
-
const challenge = b64url(
|
|
1607
|
+
const challenge = b64url(createHash4("sha256").update(verifier).digest());
|
|
1294
1608
|
const state = b64url(randomBytes(16));
|
|
1295
1609
|
const authorize = new URL(`${server}/oauth/authorize`);
|
|
1296
1610
|
authorize.search = new URLSearchParams({
|
|
@@ -1379,175 +1693,8 @@ function page(title, body) {
|
|
|
1379
1693
|
return `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="dark"><title>${title}</title><style>${style}</style><main><h1>${title}</h1><p>${body}</p></main>`;
|
|
1380
1694
|
}
|
|
1381
1695
|
|
|
1382
|
-
// src/payload.ts
|
|
1383
|
-
import { createHash as createHash3, createPublicKey, verify } from "node:crypto";
|
|
1384
|
-
import { mkdirSync as mkdirSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1385
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
1386
|
-
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1387
|
-
|
|
1388
|
-
// src/canonical.ts
|
|
1389
|
-
function canonicalJson(value) {
|
|
1390
|
-
return encode(value);
|
|
1391
|
-
}
|
|
1392
|
-
function encode(value) {
|
|
1393
|
-
if (value === null) return "null";
|
|
1394
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1395
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1396
|
-
if (typeof value === "number") {
|
|
1397
|
-
if (!Number.isFinite(value)) {
|
|
1398
|
-
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
1399
|
-
}
|
|
1400
|
-
return JSON.stringify(value);
|
|
1401
|
-
}
|
|
1402
|
-
if (Array.isArray(value)) {
|
|
1403
|
-
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
1404
|
-
}
|
|
1405
|
-
if (typeof value === "object") {
|
|
1406
|
-
const record = value;
|
|
1407
|
-
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
1408
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
1409
|
-
}
|
|
1410
|
-
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// src/payload.ts
|
|
1414
|
-
var NeedsLoginError = class extends Error {
|
|
1415
|
-
constructor() {
|
|
1416
|
-
super("signed out \u2014 run login first");
|
|
1417
|
-
this.name = "NeedsLoginError";
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
var ForbiddenError = class extends Error {
|
|
1421
|
-
constructor() {
|
|
1422
|
-
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
1423
|
-
this.name = "ForbiddenError";
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
function canonicalManifestBytes(manifest) {
|
|
1427
|
-
return Buffer.from(
|
|
1428
|
-
canonicalJson({
|
|
1429
|
-
created: manifest.created,
|
|
1430
|
-
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
1431
|
-
version: manifest.version
|
|
1432
|
-
}),
|
|
1433
|
-
"utf8"
|
|
1434
|
-
);
|
|
1435
|
-
}
|
|
1436
|
-
function ed25519PublicKey(config) {
|
|
1437
|
-
const raw = publicKeyBytes(config);
|
|
1438
|
-
return createPublicKey({
|
|
1439
|
-
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
1440
|
-
format: "jwk"
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
function verifyManifest(manifest, signature, config) {
|
|
1444
|
-
try {
|
|
1445
|
-
const signatureBytes = Buffer.from(signature, "base64");
|
|
1446
|
-
if (signatureBytes.length === 0) return false;
|
|
1447
|
-
return verify(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
1448
|
-
} catch {
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function verifyFileBytes(entry2, bytes) {
|
|
1453
|
-
if (entry2.size !== bytes.length) return false;
|
|
1454
|
-
return createHash3("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
1455
|
-
}
|
|
1456
|
-
async function readErrorCode(response) {
|
|
1457
|
-
try {
|
|
1458
|
-
const data = await response.json();
|
|
1459
|
-
return typeof data.error === "string" ? data.error : "";
|
|
1460
|
-
} catch {
|
|
1461
|
-
return "";
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
function throwForStatus(status, errorCode) {
|
|
1465
|
-
if (status === 401) throw new NeedsLoginError();
|
|
1466
|
-
if (status === 403) throw new ForbiddenError();
|
|
1467
|
-
throw new Error(
|
|
1468
|
-
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
async function getJson(url, accessToken, fetchImpl) {
|
|
1472
|
-
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
1473
|
-
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
1474
|
-
return { status: response.status, json: await response.json() };
|
|
1475
|
-
}
|
|
1476
|
-
function parseManifest(json) {
|
|
1477
|
-
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
1478
|
-
const record = json;
|
|
1479
|
-
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
1480
|
-
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
1481
|
-
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
1482
|
-
if (typeof record.signature !== "string" || !record.signature) {
|
|
1483
|
-
throw new Error("the release manifest is unsigned");
|
|
1484
|
-
}
|
|
1485
|
-
const files = record.files.map((entry2) => {
|
|
1486
|
-
const file = entry2;
|
|
1487
|
-
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
1488
|
-
throw new Error("the release manifest lists a malformed file");
|
|
1489
|
-
}
|
|
1490
|
-
const safe = safeManifestPath(file.path);
|
|
1491
|
-
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
1492
|
-
return { path: safe, sha256: file.sha256, size: file.size };
|
|
1493
|
-
});
|
|
1494
|
-
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
1495
|
-
}
|
|
1496
|
-
function safeManifestPath(rawPath) {
|
|
1497
|
-
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
1498
|
-
if (rawPath.startsWith("/")) return null;
|
|
1499
|
-
const segments = rawPath.split("/");
|
|
1500
|
-
for (const segment of segments) {
|
|
1501
|
-
if (segment === "" || segment === "." || segment === "..") return null;
|
|
1502
|
-
}
|
|
1503
|
-
return segments.join("/");
|
|
1504
|
-
}
|
|
1505
|
-
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
1506
|
-
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
1507
|
-
const manifest = parseManifest(json);
|
|
1508
|
-
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
1509
|
-
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
1510
|
-
}
|
|
1511
|
-
return manifest;
|
|
1512
|
-
}
|
|
1513
|
-
async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
1514
|
-
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1515
|
-
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
1516
|
-
headers: { authorization: `Bearer ${accessToken}` }
|
|
1517
|
-
});
|
|
1518
|
-
if (!response.ok) {
|
|
1519
|
-
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
1520
|
-
throwForStatus(response.status, await readErrorCode(response));
|
|
1521
|
-
}
|
|
1522
|
-
return Buffer.from(await response.arrayBuffer());
|
|
1523
|
-
}
|
|
1524
|
-
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
1525
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1526
|
-
const staging = join5(tmpdir3(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
1527
|
-
mkdirSync5(staging, { recursive: true });
|
|
1528
|
-
try {
|
|
1529
|
-
for (const entry2 of manifest.files) {
|
|
1530
|
-
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl);
|
|
1531
|
-
if (!verifyFileBytes(entry2, bytes)) {
|
|
1532
|
-
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
1533
|
-
}
|
|
1534
|
-
const dest = join5(staging, entry2.path);
|
|
1535
|
-
mkdirSync5(dirname2(dest), { recursive: true });
|
|
1536
|
-
writeFileSync6(dest, bytes);
|
|
1537
|
-
}
|
|
1538
|
-
const target = join5(dir, "payload");
|
|
1539
|
-
mkdirSync5(dir, { recursive: true });
|
|
1540
|
-
rmSync4(target, { force: true, recursive: true });
|
|
1541
|
-
renameSync3(staging, target);
|
|
1542
|
-
} catch (error) {
|
|
1543
|
-
rmSync4(staging, { force: true, recursive: true });
|
|
1544
|
-
throw error;
|
|
1545
|
-
}
|
|
1546
|
-
return manifest.version;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
1696
|
// src/index.ts
|
|
1550
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1697
|
+
var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
|
|
1551
1698
|
function defaultPrint(message) {
|
|
1552
1699
|
process.stdout.write(`${message}
|
|
1553
1700
|
`);
|
|
@@ -1783,12 +1930,13 @@ function parseProgress(raw) {
|
|
|
1783
1930
|
const message = JSON.parse(line);
|
|
1784
1931
|
if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
|
|
1785
1932
|
const state = message.state ?? "ok";
|
|
1786
|
-
if (state !== "ok" && state !== "fail" && state !== "note") continue;
|
|
1933
|
+
if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
|
|
1787
1934
|
if (message.ms !== void 0 && typeof message.ms !== "number") continue;
|
|
1788
1935
|
progress.push({
|
|
1789
1936
|
step: message.step,
|
|
1790
1937
|
state,
|
|
1791
|
-
...typeof message.ms === "number" ? { ms: message.ms } : {}
|
|
1938
|
+
...typeof message.ms === "number" && Number.isFinite(message.ms) && message.ms >= 0 ? { ms: message.ms } : {},
|
|
1939
|
+
...typeof message.measure === "string" ? { measure: message.measure } : {}
|
|
1792
1940
|
});
|
|
1793
1941
|
} catch {
|
|
1794
1942
|
}
|
|
@@ -1857,7 +2005,7 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1857
2005
|
version = manifest.version;
|
|
1858
2006
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1859
2007
|
const current = readState(dir);
|
|
1860
|
-
const unchanged =
|
|
2008
|
+
const unchanged = current?.version === version && existsSync6(payloadDir(dir));
|
|
1861
2009
|
if (unchanged && current.acquired) {
|
|
1862
2010
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
1863
2011
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -1865,11 +2013,17 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1865
2013
|
if (!unchanged) {
|
|
1866
2014
|
installer.phase("download", { state: "running" });
|
|
1867
2015
|
started = Date.now();
|
|
1868
|
-
const
|
|
2016
|
+
const cached = reusableCandidate(dir, manifest);
|
|
2017
|
+
const candidate = cached ?? randomUUID4();
|
|
1869
2018
|
const candidateDir = join6(dir, "candidates", candidate);
|
|
1870
|
-
|
|
2019
|
+
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2020
|
+
state: "running",
|
|
2021
|
+
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
|
|
2022
|
+
});
|
|
2023
|
+
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress });
|
|
1871
2024
|
const acquisition = existsSync6(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
|
|
1872
2025
|
if (acquisition) {
|
|
2026
|
+
rememberCandidate(dir, candidate, manifest);
|
|
1873
2027
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1874
2028
|
installer.phase("activate", { state: "running" });
|
|
1875
2029
|
let outcome;
|
|
@@ -1877,7 +2031,9 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1877
2031
|
await installAcquisition(dir, candidate, version, acquisition, {
|
|
1878
2032
|
fetchImpl,
|
|
1879
2033
|
env,
|
|
2034
|
+
onProgress,
|
|
1880
2035
|
run: async (command2, cwd, childEnv) => {
|
|
2036
|
+
installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
|
|
1881
2037
|
const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
|
|
1882
2038
|
if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
|
|
1883
2039
|
return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
|
|
@@ -1995,17 +2151,22 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
1995
2151
|
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1996
2152
|
}
|
|
1997
2153
|
async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
2154
|
+
installer.start();
|
|
1998
2155
|
const [command2, ...args] = entry2;
|
|
1999
2156
|
const shell = needsShell(command2);
|
|
2000
2157
|
const progress = !(process.platform === "win32" && shell);
|
|
2001
2158
|
const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
|
|
2002
2159
|
const outcomeFile = join6(outcomeDir, "outcome.json");
|
|
2160
|
+
const progressFile = join6(outcomeDir, "progress.jsonl");
|
|
2003
2161
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2004
2162
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2163
|
+
delete childEnv.MM_PROGRESS_FILE;
|
|
2005
2164
|
delete childEnv.MM_PROGRESS_FD;
|
|
2006
2165
|
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
2007
2166
|
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
2167
|
+
else Object.assign(childEnv, { MM_PROGRESS_FILE: progressFile, MM_PROGRESS_PROTOCOL: "1" });
|
|
2008
2168
|
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
2169
|
+
let progressError;
|
|
2009
2170
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2010
2171
|
try {
|
|
2011
2172
|
const result = await new Promise((resolve3) => {
|
|
@@ -2026,24 +2187,44 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2026
2187
|
}
|
|
2027
2188
|
partial = held ? safe.slice(-held) : "";
|
|
2028
2189
|
const visible = held ? safe.slice(0, -held) : safe;
|
|
2029
|
-
if (visible) installer.
|
|
2190
|
+
if (visible) installer.relayChunk(visible, channel2);
|
|
2030
2191
|
}).on("end", () => {
|
|
2031
|
-
if (partial) installer.
|
|
2192
|
+
if (partial) installer.relayChunk("[redacted]", channel2);
|
|
2032
2193
|
});
|
|
2033
2194
|
}
|
|
2034
2195
|
let pending = "";
|
|
2196
|
+
let progressOffset = 0;
|
|
2197
|
+
const progressDecoder = new StringDecoder("utf8");
|
|
2198
|
+
const receive = (text) => {
|
|
2199
|
+
pending += text;
|
|
2200
|
+
const end = pending.lastIndexOf("\n");
|
|
2201
|
+
if (end >= 0) {
|
|
2202
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step), ...record.measure ? { measure: redact(record.measure) } : {} });
|
|
2203
|
+
pending = pending.slice(end + 1);
|
|
2204
|
+
}
|
|
2205
|
+
if (pending.length > 65536) pending = "";
|
|
2206
|
+
};
|
|
2207
|
+
const pollProgress = () => {
|
|
2208
|
+
if (progressError) return;
|
|
2209
|
+
try {
|
|
2210
|
+
if (!existsSync6(progressFile)) return;
|
|
2211
|
+
const bytes = readFileSync7(progressFile);
|
|
2212
|
+
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2213
|
+
progressOffset = bytes.length;
|
|
2214
|
+
} catch {
|
|
2215
|
+
progressError = "installer progress: could not read child progress";
|
|
2216
|
+
if (timer) clearInterval(timer);
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
const timer = progress ? void 0 : setInterval(pollProgress, 90);
|
|
2220
|
+
child.once("close", () => {
|
|
2221
|
+
if (timer) clearInterval(timer);
|
|
2222
|
+
pollProgress();
|
|
2223
|
+
});
|
|
2035
2224
|
const channel = child.stdio[3];
|
|
2036
2225
|
if (channel && "setEncoding" in channel) {
|
|
2037
2226
|
channel.setEncoding("utf8");
|
|
2038
|
-
channel.on("data",
|
|
2039
|
-
pending += text;
|
|
2040
|
-
const end = pending.lastIndexOf("\n");
|
|
2041
|
-
if (end >= 0) {
|
|
2042
|
-
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
2043
|
-
pending = pending.slice(end + 1);
|
|
2044
|
-
}
|
|
2045
|
-
if (pending.length > 65536) pending = "";
|
|
2046
|
-
});
|
|
2227
|
+
channel.on("data", receive);
|
|
2047
2228
|
}
|
|
2048
2229
|
child.on("error", (error) => resolve3({ ok: false, error: error.message }));
|
|
2049
2230
|
child.on("close", (code) => resolve3({
|
|
@@ -2052,6 +2233,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2052
2233
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
2053
2234
|
}));
|
|
2054
2235
|
});
|
|
2236
|
+
if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
|
|
2055
2237
|
try {
|
|
2056
2238
|
const outcome = readInstallerOutcome(outcomeFile);
|
|
2057
2239
|
return { ...result, ...outcome ? { outcome } : {} };
|
|
@@ -2059,6 +2241,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2059
2241
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2060
2242
|
}
|
|
2061
2243
|
} finally {
|
|
2244
|
+
if (existsSync6(progressFile)) unlinkSync2(progressFile);
|
|
2062
2245
|
if (existsSync6(outcomeFile)) unlinkSync2(outcomeFile);
|
|
2063
2246
|
rmdirSync(outcomeDir);
|
|
2064
2247
|
}
|