@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.sea.cjs
CHANGED
|
@@ -32,18 +32,39 @@ __export(index_exports, {
|
|
|
32
32
|
runInstallEntry: () => runInstallEntry
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
var import_node_string_decoder = require("node:string_decoder");
|
|
35
36
|
var import_node_crypto6 = require("node:crypto");
|
|
36
37
|
|
|
37
38
|
// src/acquisition.ts
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
39
|
+
var import_node_crypto4 = require("node:crypto");
|
|
40
|
+
var import_node_fs5 = require("node:fs");
|
|
41
|
+
var import_node_path4 = require("node:path");
|
|
41
42
|
|
|
42
43
|
// src/runtime.ts
|
|
43
44
|
var import_node_crypto = require("node:crypto");
|
|
44
45
|
var import_node_child_process = require("node:child_process");
|
|
45
46
|
var import_node_fs = require("node:fs");
|
|
46
47
|
var import_node_path = require("node:path");
|
|
48
|
+
|
|
49
|
+
// src/download.ts
|
|
50
|
+
async function readDownload(response, onProgress, expectedSize) {
|
|
51
|
+
const length = expectedSize ?? Number(response.headers.get("content-length"));
|
|
52
|
+
const total = Number.isSafeInteger(length) && length > 0 ? length : void 0;
|
|
53
|
+
let done = 0;
|
|
54
|
+
const report = () => onProgress?.({ done, ...total === void 0 ? {} : { total } });
|
|
55
|
+
report();
|
|
56
|
+
const chunks = [];
|
|
57
|
+
if (response.body) {
|
|
58
|
+
for await (const chunk of response.body) {
|
|
59
|
+
chunks.push(chunk);
|
|
60
|
+
done += chunk.length;
|
|
61
|
+
report();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Buffer.concat(chunks);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/runtime.ts
|
|
47
68
|
var NODE_VERSION = "24.20.0";
|
|
48
69
|
var NPM_VERSION = "12.0.2";
|
|
49
70
|
var ARTIFACTS = {
|
|
@@ -54,10 +75,10 @@ var ARTIFACTS = {
|
|
|
54
75
|
"linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
|
|
55
76
|
};
|
|
56
77
|
var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
|
|
57
|
-
async function verifiedDownload(url, path, algorithm, digest, fetchImpl) {
|
|
78
|
+
async function verifiedDownload(url, path, algorithm, digest, fetchImpl, onProgress) {
|
|
58
79
|
const response = await fetchImpl(url);
|
|
59
80
|
if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
|
|
60
|
-
const bytes =
|
|
81
|
+
const bytes = await readDownload(response, onProgress);
|
|
61
82
|
if ((0, import_node_crypto.createHash)(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
|
|
62
83
|
(0, import_node_fs.writeFileSync)(path, bytes);
|
|
63
84
|
}
|
|
@@ -66,7 +87,7 @@ function command(executable, args) {
|
|
|
66
87
|
if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
|
|
67
88
|
return result.stdout.trim();
|
|
68
89
|
}
|
|
69
|
-
async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
90
|
+
async function acquireRuntime(dir, fetchImpl = fetch, onProgress) {
|
|
70
91
|
const platform = `${process.platform}-${process.arch}`;
|
|
71
92
|
const artifact = ARTIFACTS[platform];
|
|
72
93
|
if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
|
|
@@ -84,13 +105,20 @@ async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
|
84
105
|
}
|
|
85
106
|
const root = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)(runtimeDir, "runtime-"));
|
|
86
107
|
const archive = (0, import_node_path.join)(root, artifact[0]);
|
|
87
|
-
|
|
108
|
+
let downloaded = 0;
|
|
109
|
+
let currentBytes = 0;
|
|
110
|
+
const progress = ({ done }) => {
|
|
111
|
+
currentBytes = done;
|
|
112
|
+
onProgress?.({ done: downloaded + done });
|
|
113
|
+
};
|
|
114
|
+
await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl, progress);
|
|
115
|
+
downloaded += currentBytes;
|
|
88
116
|
const tar = process.platform === "win32" ? (0, import_node_path.join)(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
|
|
89
117
|
command(tar, ["-xf", archive, "-C", root]);
|
|
90
118
|
const unpacked = (0, import_node_path.join)(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
|
|
91
119
|
const node = (0, import_node_path.join)(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
|
|
92
120
|
const npmArchive = (0, import_node_path.join)(root, "npm.tgz");
|
|
93
|
-
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl);
|
|
121
|
+
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl, progress);
|
|
94
122
|
const npmRoot = (0, import_node_path.join)(root, "npm");
|
|
95
123
|
(0, import_node_fs.mkdirSync)(npmRoot);
|
|
96
124
|
command(tar, ["-xf", npmArchive, "-C", npmRoot]);
|
|
@@ -195,7 +223,276 @@ function wipeProductDir(dir) {
|
|
|
195
223
|
(0, import_node_fs2.rmSync)(payload, { force: true, recursive: true });
|
|
196
224
|
}
|
|
197
225
|
|
|
226
|
+
// src/payload.ts
|
|
227
|
+
var import_node_crypto3 = require("node:crypto");
|
|
228
|
+
var import_node_fs4 = require("node:fs");
|
|
229
|
+
var import_node_os2 = require("node:os");
|
|
230
|
+
var import_node_path3 = require("node:path");
|
|
231
|
+
|
|
232
|
+
// src/canonical.ts
|
|
233
|
+
function canonicalJson(value) {
|
|
234
|
+
return encode(value);
|
|
235
|
+
}
|
|
236
|
+
function encode(value) {
|
|
237
|
+
if (value === null) return "null";
|
|
238
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
239
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
240
|
+
if (typeof value === "number") {
|
|
241
|
+
if (!Number.isFinite(value)) {
|
|
242
|
+
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
243
|
+
}
|
|
244
|
+
return JSON.stringify(value);
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
248
|
+
}
|
|
249
|
+
if (typeof value === "object") {
|
|
250
|
+
const record = value;
|
|
251
|
+
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
252
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
253
|
+
}
|
|
254
|
+
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/config.ts
|
|
258
|
+
var import_node_fs3 = require("node:fs");
|
|
259
|
+
var import_node_sea = require("node:sea");
|
|
260
|
+
|
|
261
|
+
// src/module-url.ts
|
|
262
|
+
var import_node_url = require("node:url");
|
|
263
|
+
var import_meta = {};
|
|
264
|
+
function moduleUrl() {
|
|
265
|
+
if (typeof __filename === "string") return (0, import_node_url.pathToFileURL)(__filename).href;
|
|
266
|
+
return import_meta.url;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/config.ts
|
|
270
|
+
function publicKeyBytes(config) {
|
|
271
|
+
const raw = Buffer.from(config.publicKey, "base64");
|
|
272
|
+
if (raw.length !== 32) {
|
|
273
|
+
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
274
|
+
}
|
|
275
|
+
return raw;
|
|
276
|
+
}
|
|
277
|
+
function loadProductConfig(options = {}) {
|
|
278
|
+
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
279
|
+
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
280
|
+
if (explicit) {
|
|
281
|
+
return parseProductConfig((0, import_node_fs3.readFileSync)(explicit, "utf8"));
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return parseProductConfig((0, import_node_fs3.readFileSync)(devFallback, "utf8"));
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const asset = (0, import_node_sea.getAsset)("product.json", "utf8");
|
|
289
|
+
if (typeof asset === "string") return parseProductConfig(asset);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
throw new Error(
|
|
293
|
+
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function parseProductConfig(text) {
|
|
297
|
+
let data;
|
|
298
|
+
try {
|
|
299
|
+
data = JSON.parse(text);
|
|
300
|
+
} catch {
|
|
301
|
+
throw new Error("product config is not valid JSON");
|
|
302
|
+
}
|
|
303
|
+
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
304
|
+
const record = data;
|
|
305
|
+
const product = field(record, "product");
|
|
306
|
+
const host = field(record, "host").replace(/\/+$/, "");
|
|
307
|
+
const loginKind = field(record, "loginKind");
|
|
308
|
+
const binName = field(record, "binName");
|
|
309
|
+
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
310
|
+
if (loginKind !== "github" && loginKind !== "google") {
|
|
311
|
+
throw new Error('product config loginKind must be "github" or "google"');
|
|
312
|
+
}
|
|
313
|
+
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
314
|
+
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
315
|
+
publicKeyBytes(config);
|
|
316
|
+
if (loginKind === "github") {
|
|
317
|
+
const clientId = record.githubClientId;
|
|
318
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
319
|
+
throw new Error("product config needs githubClientId for the github loginKind");
|
|
320
|
+
}
|
|
321
|
+
config.githubClientId = clientId;
|
|
322
|
+
} else if (typeof record.githubClientId === "string") {
|
|
323
|
+
config.githubClientId = record.githubClientId;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const url = new URL(host);
|
|
327
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
328
|
+
} catch {
|
|
329
|
+
throw new Error("product config host must be an http(s) URL");
|
|
330
|
+
}
|
|
331
|
+
return config;
|
|
332
|
+
}
|
|
333
|
+
function field(record, key) {
|
|
334
|
+
const value = record[key];
|
|
335
|
+
return typeof value === "string" ? value : "";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/payload.ts
|
|
339
|
+
var NeedsLoginError = class extends Error {
|
|
340
|
+
constructor() {
|
|
341
|
+
super("signed out \u2014 run login first");
|
|
342
|
+
this.name = "NeedsLoginError";
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
var ForbiddenError = class extends Error {
|
|
346
|
+
constructor() {
|
|
347
|
+
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
348
|
+
this.name = "ForbiddenError";
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function canonicalManifestBytes(manifest) {
|
|
352
|
+
return Buffer.from(
|
|
353
|
+
canonicalJson({
|
|
354
|
+
created: manifest.created,
|
|
355
|
+
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
356
|
+
version: manifest.version
|
|
357
|
+
}),
|
|
358
|
+
"utf8"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
function ed25519PublicKey(config) {
|
|
362
|
+
const raw = publicKeyBytes(config);
|
|
363
|
+
return (0, import_node_crypto3.createPublicKey)({
|
|
364
|
+
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
365
|
+
format: "jwk"
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function verifyManifest(manifest, signature, config) {
|
|
369
|
+
try {
|
|
370
|
+
const signatureBytes = Buffer.from(signature, "base64");
|
|
371
|
+
if (signatureBytes.length === 0) return false;
|
|
372
|
+
return (0, import_node_crypto3.verify)(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function verifyFileBytes(entry2, bytes) {
|
|
378
|
+
if (entry2.size !== bytes.length) return false;
|
|
379
|
+
return (0, import_node_crypto3.createHash)("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
380
|
+
}
|
|
381
|
+
async function readErrorCode(response) {
|
|
382
|
+
try {
|
|
383
|
+
const data = await response.json();
|
|
384
|
+
return typeof data.error === "string" ? data.error : "";
|
|
385
|
+
} catch {
|
|
386
|
+
return "";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function throwForStatus(status, errorCode) {
|
|
390
|
+
if (status === 401) throw new NeedsLoginError();
|
|
391
|
+
if (status === 403) throw new ForbiddenError();
|
|
392
|
+
throw new Error(
|
|
393
|
+
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
async function getJson(url, accessToken, fetchImpl) {
|
|
397
|
+
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
398
|
+
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
399
|
+
return { status: response.status, json: await response.json() };
|
|
400
|
+
}
|
|
401
|
+
function parseManifest(json) {
|
|
402
|
+
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
403
|
+
const record = json;
|
|
404
|
+
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
405
|
+
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
406
|
+
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
407
|
+
if (typeof record.signature !== "string" || !record.signature) {
|
|
408
|
+
throw new Error("the release manifest is unsigned");
|
|
409
|
+
}
|
|
410
|
+
const files = record.files.map((entry2) => {
|
|
411
|
+
const file = entry2;
|
|
412
|
+
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
413
|
+
throw new Error("the release manifest lists a malformed file");
|
|
414
|
+
}
|
|
415
|
+
const safe = safeManifestPath(file.path);
|
|
416
|
+
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
417
|
+
return { path: safe, sha256: file.sha256, size: file.size };
|
|
418
|
+
});
|
|
419
|
+
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
420
|
+
}
|
|
421
|
+
function safeManifestPath(rawPath) {
|
|
422
|
+
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
423
|
+
if (rawPath.startsWith("/")) return null;
|
|
424
|
+
const segments = rawPath.split("/");
|
|
425
|
+
for (const segment of segments) {
|
|
426
|
+
if (segment === "" || segment === "." || segment === "..") return null;
|
|
427
|
+
}
|
|
428
|
+
return segments.join("/");
|
|
429
|
+
}
|
|
430
|
+
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
431
|
+
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
432
|
+
const manifest = parseManifest(json);
|
|
433
|
+
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
434
|
+
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
435
|
+
}
|
|
436
|
+
return manifest;
|
|
437
|
+
}
|
|
438
|
+
async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
|
|
439
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
440
|
+
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
441
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
442
|
+
});
|
|
443
|
+
if (!response.ok) {
|
|
444
|
+
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
445
|
+
throwForStatus(response.status, await readErrorCode(response));
|
|
446
|
+
}
|
|
447
|
+
return readDownload(response, onProgress, expectedSize);
|
|
448
|
+
}
|
|
449
|
+
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
450
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
451
|
+
const staging = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
452
|
+
(0, import_node_fs4.mkdirSync)(staging, { recursive: true });
|
|
453
|
+
try {
|
|
454
|
+
const total = manifest.files.reduce((sum, entry2) => sum + entry2.size, 0);
|
|
455
|
+
let downloaded = 0;
|
|
456
|
+
for (const entry2 of manifest.files) {
|
|
457
|
+
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl, (progress) => options.onProgress?.({ done: downloaded + progress.done, total }), entry2.size);
|
|
458
|
+
downloaded += bytes.length;
|
|
459
|
+
if (!verifyFileBytes(entry2, bytes)) {
|
|
460
|
+
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
461
|
+
}
|
|
462
|
+
const dest = (0, import_node_path3.join)(staging, entry2.path);
|
|
463
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(dest), { recursive: true });
|
|
464
|
+
(0, import_node_fs4.writeFileSync)(dest, bytes);
|
|
465
|
+
}
|
|
466
|
+
const target = (0, import_node_path3.join)(dir, "payload");
|
|
467
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
468
|
+
(0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
|
|
469
|
+
(0, import_node_fs4.renameSync)(staging, target);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
(0, import_node_fs4.rmSync)(staging, { force: true, recursive: true });
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
return manifest.version;
|
|
475
|
+
}
|
|
476
|
+
|
|
198
477
|
// src/acquisition.ts
|
|
478
|
+
function reusableCandidate(dir, manifest) {
|
|
479
|
+
try {
|
|
480
|
+
const saved = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), "utf8"));
|
|
481
|
+
if (saved.signature !== manifest.signature) return null;
|
|
482
|
+
const root = candidateRoot(dir, saved.candidate);
|
|
483
|
+
if (!(0, import_node_fs5.existsSync)((0, import_node_path4.join)(root, "acquired.json"))) return null;
|
|
484
|
+
for (const file of manifest.files) {
|
|
485
|
+
if (!safePath(file.path) || !verifyFileBytes(file, (0, import_node_fs5.readFileSync)((0, import_node_path4.join)(root, "payload", file.path)))) return null;
|
|
486
|
+
}
|
|
487
|
+
return saved.candidate;
|
|
488
|
+
} catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function rememberCandidate(dir, candidate, manifest) {
|
|
493
|
+
candidateRoot(dir, candidate);
|
|
494
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), JSON.stringify({ candidate, signature: manifest.signature }), { mode: 384 });
|
|
495
|
+
}
|
|
199
496
|
function safePath(value) {
|
|
200
497
|
return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
201
498
|
}
|
|
@@ -203,7 +500,7 @@ function argumentsValid(value) {
|
|
|
203
500
|
return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
|
|
204
501
|
}
|
|
205
502
|
function readAcquisition(payload) {
|
|
206
|
-
const metadata = JSON.parse((0,
|
|
503
|
+
const metadata = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, "payload.json"), "utf8"));
|
|
207
504
|
if (metadata.acquisition === void 0) return null;
|
|
208
505
|
const a = metadata.acquisition;
|
|
209
506
|
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))) {
|
|
@@ -219,25 +516,25 @@ function runtimeEnvironment(node, inherited, platform = process.platform) {
|
|
|
219
516
|
current = (keys.length ? env[keys[0]] : "") ?? "";
|
|
220
517
|
for (const key of keys) delete env[key];
|
|
221
518
|
}
|
|
222
|
-
env.PATH = `${(0,
|
|
519
|
+
env.PATH = `${(0, import_node_path4.dirname)(node)}${platform === "win32" ? ";" : ":"}${current}`;
|
|
223
520
|
return env;
|
|
224
521
|
}
|
|
225
522
|
function lockInstallation(dir) {
|
|
226
|
-
(0,
|
|
227
|
-
const path = (0,
|
|
228
|
-
const owner = `${process.pid}:${(0,
|
|
523
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
524
|
+
const path = (0, import_node_path4.join)(dir, "installation.lock");
|
|
525
|
+
const owner = `${process.pid}:${(0, import_node_crypto4.randomUUID)()}`;
|
|
229
526
|
try {
|
|
230
|
-
(0,
|
|
527
|
+
(0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
|
|
231
528
|
} catch (error) {
|
|
232
529
|
if (error.code !== "EEXIST") throw error;
|
|
233
530
|
const recovery = `${path}.recovery`;
|
|
234
531
|
try {
|
|
235
|
-
(0,
|
|
532
|
+
(0, import_node_fs5.writeFileSync)(recovery, owner, { flag: "wx", mode: 384, flush: true });
|
|
236
533
|
} catch {
|
|
237
534
|
throw new Error("installation lock recovery is already pending");
|
|
238
535
|
}
|
|
239
536
|
try {
|
|
240
|
-
const previous = (0,
|
|
537
|
+
const previous = (0, import_node_fs5.readFileSync)(path, "utf8");
|
|
241
538
|
const pid = Number(previous.split(":")[0]);
|
|
242
539
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
|
|
243
540
|
try {
|
|
@@ -246,32 +543,32 @@ function lockInstallation(dir) {
|
|
|
246
543
|
} catch (probe) {
|
|
247
544
|
if (probe.code !== "ESRCH") throw probe;
|
|
248
545
|
}
|
|
249
|
-
(0,
|
|
250
|
-
(0,
|
|
546
|
+
(0, import_node_fs5.unlinkSync)(path);
|
|
547
|
+
(0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384, flush: true });
|
|
251
548
|
} finally {
|
|
252
|
-
(0,
|
|
549
|
+
(0, import_node_fs5.unlinkSync)(recovery);
|
|
253
550
|
}
|
|
254
551
|
}
|
|
255
552
|
let released = false;
|
|
256
553
|
return () => {
|
|
257
|
-
if (!released && (0,
|
|
554
|
+
if (!released && (0, import_node_fs5.readFileSync)(path, "utf8") === owner) (0, import_node_fs5.unlinkSync)(path);
|
|
258
555
|
released = true;
|
|
259
556
|
};
|
|
260
557
|
}
|
|
261
558
|
function pendingPath(dir) {
|
|
262
|
-
return (0,
|
|
559
|
+
return (0, import_node_path4.join)(dir, "acquisition-pending.json");
|
|
263
560
|
}
|
|
264
561
|
function candidateRoot(dir, candidate) {
|
|
265
562
|
if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
|
|
266
|
-
return (0,
|
|
563
|
+
return (0, import_node_path4.join)(dir, "candidates", candidate);
|
|
267
564
|
}
|
|
268
565
|
function packageRoot(prefix, acquisition) {
|
|
269
|
-
return (0,
|
|
566
|
+
return (0, import_node_path4.join)(prefix, "node_modules", acquisition.package);
|
|
270
567
|
}
|
|
271
568
|
function entry(root, path) {
|
|
272
|
-
const actual = (0,
|
|
273
|
-
const rel = (0,
|
|
274
|
-
if (rel.startsWith("..") || (0,
|
|
569
|
+
const actual = (0, import_node_fs5.realpathSync)((0, import_node_path4.join)(root, path));
|
|
570
|
+
const rel = (0, import_node_path4.relative)((0, import_node_fs5.realpathSync)(root), actual);
|
|
571
|
+
if (rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel)) throw new Error("acquired entry escapes package");
|
|
275
572
|
return actual;
|
|
276
573
|
}
|
|
277
574
|
function argv(node, root, file, args, prefix, version) {
|
|
@@ -281,54 +578,59 @@ function acquisitionRepairCommand(dir, version) {
|
|
|
281
578
|
const state = readState(dir);
|
|
282
579
|
if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
|
|
283
580
|
const root = candidateRoot(dir, state.acquired.candidate);
|
|
284
|
-
const acquisition = readAcquisition((0,
|
|
581
|
+
const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
|
|
285
582
|
if (!acquisition) throw new Error("installed acquisition declaration is missing");
|
|
286
|
-
const prefix = (0,
|
|
583
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
287
584
|
const productRoot = packageRoot(prefix, acquisition);
|
|
288
|
-
const pkg = JSON.parse((0,
|
|
585
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
|
|
289
586
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
|
|
290
|
-
return argv((0,
|
|
587
|
+
return argv((0, import_node_fs5.realpathSync)(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
|
|
291
588
|
}
|
|
292
589
|
async function recoverAcquisition(dir, run2, env) {
|
|
293
|
-
if (!(0,
|
|
294
|
-
const pending = JSON.parse((0,
|
|
590
|
+
if (!(0, import_node_fs5.existsSync)(pendingPath(dir))) return;
|
|
591
|
+
const pending = JSON.parse((0, import_node_fs5.readFileSync)(pendingPath(dir), "utf8"));
|
|
295
592
|
if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
|
|
296
593
|
const root = candidateRoot(dir, pending.candidate);
|
|
297
594
|
if (readState(dir)?.acquired?.candidate === pending.candidate) {
|
|
298
|
-
(0,
|
|
595
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
299
596
|
return;
|
|
300
597
|
}
|
|
301
|
-
const acquisition = readAcquisition((0,
|
|
598
|
+
const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
|
|
302
599
|
if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
|
|
303
|
-
const prefix = (0,
|
|
600
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
304
601
|
const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
|
|
305
602
|
if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
|
|
306
603
|
if (pending.previous) writeState(dir, pending.previous);
|
|
307
|
-
else if ((0,
|
|
308
|
-
(0,
|
|
604
|
+
else if ((0, import_node_fs5.existsSync)(statePath(dir))) (0, import_node_fs5.unlinkSync)(statePath(dir));
|
|
605
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
309
606
|
}
|
|
310
607
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
311
608
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
312
609
|
const root = candidateRoot(dir, candidate);
|
|
313
|
-
const payload = (0,
|
|
314
|
-
const prefix = (0,
|
|
315
|
-
(0,
|
|
316
|
-
const
|
|
610
|
+
const payload = (0, import_node_path4.join)(root, "payload");
|
|
611
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
612
|
+
const prepared = (0, import_node_path4.join)(root, "acquired.json");
|
|
613
|
+
const archiveHash = (0, import_node_crypto4.createHash)("sha256").update((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, acquisition.archive))).digest("hex");
|
|
614
|
+
const reused = (0, import_node_fs5.existsSync)(prepared);
|
|
615
|
+
if (reused && JSON.parse((0, import_node_fs5.readFileSync)(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
|
|
616
|
+
if (!reused) (0, import_node_fs5.mkdirSync)(prefix);
|
|
617
|
+
const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
|
|
317
618
|
const env = runtimeEnvironment(runtime.node, options.env);
|
|
318
619
|
const npmEnv = { ...env };
|
|
319
620
|
for (const key of Object.keys(npmEnv)) {
|
|
320
621
|
if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
|
|
321
622
|
}
|
|
322
|
-
const npmrc = (0,
|
|
323
|
-
const globalrc = (0,
|
|
324
|
-
(0,
|
|
325
|
-
(0,
|
|
623
|
+
const npmrc = (0, import_node_path4.join)(root, "public.npmrc");
|
|
624
|
+
const globalrc = (0, import_node_path4.join)(root, "global.npmrc");
|
|
625
|
+
(0, import_node_fs5.writeFileSync)(npmrc, "registry=https://registry.npmjs.org/\n");
|
|
626
|
+
(0, import_node_fs5.writeFileSync)(globalrc, "");
|
|
326
627
|
const install = [
|
|
327
628
|
runtime.node,
|
|
328
629
|
runtime.npm,
|
|
329
630
|
"install",
|
|
330
631
|
"--prefix",
|
|
331
632
|
prefix,
|
|
633
|
+
"--install-strategy=shallow",
|
|
332
634
|
"--ignore-scripts",
|
|
333
635
|
"--no-audit",
|
|
334
636
|
"--no-fund",
|
|
@@ -338,17 +640,18 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
338
640
|
globalrc,
|
|
339
641
|
"--registry",
|
|
340
642
|
"https://registry.npmjs.org/",
|
|
341
|
-
(0,
|
|
643
|
+
(0, import_node_path4.resolve)(payload, acquisition.archive)
|
|
342
644
|
];
|
|
343
|
-
if (!await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
645
|
+
if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
344
646
|
const productRoot = packageRoot(prefix, acquisition);
|
|
345
|
-
const pkg = JSON.parse((0,
|
|
647
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
|
|
346
648
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
|
|
347
649
|
const runEntry = entry(productRoot, acquisition.runEntry);
|
|
348
650
|
const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
|
|
349
651
|
entry(productRoot, acquisition.rollbackEntry);
|
|
652
|
+
if (!reused) (0, import_node_fs5.writeFileSync)(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
|
|
350
653
|
const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
|
|
351
|
-
(0,
|
|
654
|
+
(0, import_node_fs5.writeFileSync)(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
|
|
352
655
|
try {
|
|
353
656
|
if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
|
|
354
657
|
(options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
|
|
@@ -356,7 +659,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
356
659
|
await recoverAcquisition(dir, options.run, env);
|
|
357
660
|
throw error;
|
|
358
661
|
}
|
|
359
|
-
(0,
|
|
662
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
360
663
|
}
|
|
361
664
|
|
|
362
665
|
// src/index.ts
|
|
@@ -367,36 +670,36 @@ var import_node_path6 = require("node:path");
|
|
|
367
670
|
var import_node_url2 = require("node:url");
|
|
368
671
|
|
|
369
672
|
// ../face/src/face.ts
|
|
370
|
-
var
|
|
673
|
+
var import_node_fs6 = require("node:fs");
|
|
371
674
|
|
|
372
675
|
// ../face/src/products.ts
|
|
373
676
|
var PRODUCTS = Object.freeze({
|
|
374
677
|
"mm-strategy": Object.freeze({
|
|
375
678
|
name: "MM Strategy",
|
|
376
|
-
installWarm: "Welcome. Let's
|
|
679
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
377
680
|
accent: "38;2;249;115;22",
|
|
378
|
-
warm: "Welcome.
|
|
681
|
+
warm: "Welcome back. Checking for updates...",
|
|
379
682
|
doctor: "mm-strategy doctor"
|
|
380
683
|
}),
|
|
381
684
|
"mmi-hub": Object.freeze({
|
|
382
685
|
name: "mmi-hub",
|
|
383
|
-
installWarm: "Welcome.
|
|
686
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
384
687
|
accent: "38;2;125;211;252",
|
|
385
|
-
warm: "Welcome back. Checking
|
|
688
|
+
warm: "Welcome back. Checking for updates...",
|
|
386
689
|
doctor: "mmi doctor"
|
|
387
690
|
}),
|
|
388
691
|
"jerv-hub": Object.freeze({
|
|
389
692
|
name: "jerv-hub",
|
|
390
|
-
installWarm: "Welcome.
|
|
693
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
391
694
|
accent: "38;2;248;113;113",
|
|
392
|
-
warm: "Welcome back. Checking
|
|
695
|
+
warm: "Welcome back. Checking for updates...",
|
|
393
696
|
doctor: "jerv doctor"
|
|
394
697
|
}),
|
|
395
698
|
jervcode: Object.freeze({
|
|
396
699
|
name: "JervCode",
|
|
397
|
-
installWarm: "Welcome.
|
|
700
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
398
701
|
accent: "38;2;192;132;252",
|
|
399
|
-
warm: "Welcome back.
|
|
702
|
+
warm: "Welcome back. Checking for updates...",
|
|
400
703
|
doctor: "jervcode doctor"
|
|
401
704
|
})
|
|
402
705
|
});
|
|
@@ -410,6 +713,7 @@ function identityFor(product) {
|
|
|
410
713
|
|
|
411
714
|
// ../face/src/face.ts
|
|
412
715
|
var GLYPH = Object.freeze({
|
|
716
|
+
clock: "\u23F0",
|
|
413
717
|
diamond: "\u25C6",
|
|
414
718
|
hollow: "\u25C7",
|
|
415
719
|
bar: "\u2502",
|
|
@@ -481,12 +785,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
481
785
|
const continuesFace = continuedPhases.size > 0;
|
|
482
786
|
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
483
787
|
const progressFd = readProgressFd(env);
|
|
788
|
+
const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
|
|
484
789
|
const emitMilestone = (title, measure, kind) => {
|
|
485
|
-
if (progressFd === null) return false;
|
|
790
|
+
if (progressFd === null && !progressFile) return false;
|
|
486
791
|
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
487
792
|
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
793
|
+
if (kind === "running" && typeof measure === "string") record.measure = measure;
|
|
488
794
|
try {
|
|
489
|
-
(0,
|
|
795
|
+
if (progressFile) (0, import_node_fs6.appendFileSync)(progressFile, `${JSON.stringify(record)}
|
|
796
|
+
`);
|
|
797
|
+
else (0, import_node_fs6.writeSync)(progressFd, `${JSON.stringify(record)}
|
|
490
798
|
`);
|
|
491
799
|
return true;
|
|
492
800
|
} catch {
|
|
@@ -505,7 +813,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
505
813
|
};
|
|
506
814
|
const step = (title, measure = null, kind = "ok") => {
|
|
507
815
|
if (emitMilestone(title, measure, kind)) return "";
|
|
508
|
-
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
816
|
+
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);
|
|
509
817
|
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
510
818
|
const time = measured;
|
|
511
819
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
@@ -515,7 +823,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
515
823
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
516
824
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
517
825
|
};
|
|
518
|
-
const relay = (text) => String(text).
|
|
826
|
+
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");
|
|
519
827
|
const receipt = (lines, { ready = true } = {}) => {
|
|
520
828
|
if (ready && nested) return [];
|
|
521
829
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
@@ -546,14 +854,14 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
546
854
|
};
|
|
547
855
|
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
548
856
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
549
|
-
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
857
|
+
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 };
|
|
550
858
|
}
|
|
551
859
|
|
|
552
860
|
// ../face/src/shell.ts
|
|
553
861
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
554
862
|
|
|
555
863
|
// ../face/src/spinner.ts
|
|
556
|
-
var
|
|
864
|
+
var import_node_fs7 = require("node:fs");
|
|
557
865
|
var import_node_worker_threads = require("node:worker_threads");
|
|
558
866
|
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
559
867
|
var WORKER_SOURCE = `
|
|
@@ -565,7 +873,7 @@ function draw() {
|
|
|
565
873
|
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
566
874
|
try {
|
|
567
875
|
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
568
|
-
const text = frames[frame++ % frames.length];
|
|
876
|
+
const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
|
|
569
877
|
writeSync(2, text);
|
|
570
878
|
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
571
879
|
} finally {
|
|
@@ -577,7 +885,6 @@ parentPort.on('message', (next) => {
|
|
|
577
885
|
if (Atomics.load(control, 2)) return;
|
|
578
886
|
frames = next.frames;
|
|
579
887
|
frame = next.frame;
|
|
580
|
-
Atomics.store(control, 0, 0);
|
|
581
888
|
});
|
|
582
889
|
setInterval(draw, workerData.intervalMs);
|
|
583
890
|
`;
|
|
@@ -588,20 +895,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
588
895
|
let control = null;
|
|
589
896
|
let frames = [];
|
|
590
897
|
let frame = 0;
|
|
898
|
+
let started = 0;
|
|
899
|
+
let suspended = false;
|
|
591
900
|
const write = (text) => {
|
|
592
901
|
if (stream) stream.write(text);
|
|
593
902
|
else {
|
|
594
|
-
(0,
|
|
595
|
-
if (transcriptPath) (0,
|
|
903
|
+
(0, import_node_fs7.writeSync)(2, text);
|
|
904
|
+
if (transcriptPath) (0, import_node_fs7.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
596
905
|
`);
|
|
597
906
|
}
|
|
598
907
|
};
|
|
599
908
|
const render = (text, measure) => {
|
|
600
|
-
const line = face.step(text, measure, "note").split("\n")[0];
|
|
909
|
+
const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
|
|
601
910
|
frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
|
|
602
911
|
};
|
|
603
912
|
const draw = () => {
|
|
604
|
-
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
913
|
+
if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
|
|
605
914
|
};
|
|
606
915
|
const pause = () => {
|
|
607
916
|
if (!control) return;
|
|
@@ -621,6 +930,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
621
930
|
start(text, measure = null) {
|
|
622
931
|
if (!animate) return;
|
|
623
932
|
halt();
|
|
933
|
+
suspended = false;
|
|
934
|
+
started = Date.now();
|
|
624
935
|
render(text, measure);
|
|
625
936
|
frame = 0;
|
|
626
937
|
draw();
|
|
@@ -634,7 +945,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
634
945
|
frames,
|
|
635
946
|
frame,
|
|
636
947
|
intervalMs,
|
|
637
|
-
transcriptPath
|
|
948
|
+
transcriptPath,
|
|
949
|
+
started
|
|
638
950
|
} });
|
|
639
951
|
const active = worker;
|
|
640
952
|
worker.on("error", () => {
|
|
@@ -652,9 +964,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
652
964
|
render(text, measure);
|
|
653
965
|
draw();
|
|
654
966
|
worker?.postMessage({ frames, frame });
|
|
967
|
+
if (control && !suspended) Atomics.store(control, 0, 0);
|
|
968
|
+
},
|
|
969
|
+
pause() {
|
|
970
|
+
if (suspended) return;
|
|
971
|
+
suspended = true;
|
|
972
|
+
pause();
|
|
973
|
+
if (animate && frames.length) write("\r\x1B[2K");
|
|
974
|
+
},
|
|
975
|
+
resume() {
|
|
976
|
+
suspended = false;
|
|
977
|
+
if (control) Atomics.store(control, 0, 0);
|
|
978
|
+
if (timer || worker) draw();
|
|
655
979
|
},
|
|
656
980
|
stop() {
|
|
657
981
|
halt();
|
|
982
|
+
frames = [];
|
|
658
983
|
if (animate) write("\r\x1B[2K");
|
|
659
984
|
}
|
|
660
985
|
};
|
|
@@ -665,10 +990,10 @@ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
|
665
990
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
666
991
|
|
|
667
992
|
// ../face/src/run.ts
|
|
668
|
-
var
|
|
993
|
+
var import_node_fs9 = require("node:fs");
|
|
669
994
|
|
|
670
995
|
// ../face/src/outcome.ts
|
|
671
|
-
var
|
|
996
|
+
var import_node_fs8 = require("node:fs");
|
|
672
997
|
var counts = ["total", "updated", "failed"];
|
|
673
998
|
var strings = ["version", "retry", "detail", "logPath"];
|
|
674
999
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
@@ -690,12 +1015,12 @@ function validateInstallerOutcome(value) {
|
|
|
690
1015
|
return { ...facts };
|
|
691
1016
|
}
|
|
692
1017
|
function writeInstallerOutcome(path, value) {
|
|
693
|
-
(0,
|
|
1018
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
694
1019
|
}
|
|
695
1020
|
function readInstallerOutcome(path) {
|
|
696
1021
|
let text;
|
|
697
1022
|
try {
|
|
698
|
-
text = (0,
|
|
1023
|
+
text = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
699
1024
|
} catch (error) {
|
|
700
1025
|
if (error.code === "ENOENT") return void 0;
|
|
701
1026
|
throw error;
|
|
@@ -758,6 +1083,7 @@ var PHASES = {
|
|
|
758
1083
|
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
759
1084
|
verify: ["Verifying the payload", "Verified the payload"],
|
|
760
1085
|
install: ["Installing the product", "Installed the product"],
|
|
1086
|
+
configure: ["Configuring the product", "Configured the product"],
|
|
761
1087
|
activate: ["Activating surfaces", "Activated surfaces"],
|
|
762
1088
|
doctor: ["Checking health", "Checked health"],
|
|
763
1089
|
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
@@ -787,7 +1113,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
787
1113
|
if (!text) return;
|
|
788
1114
|
write(text, channel);
|
|
789
1115
|
if (env.MM_FACE_TRANSCRIPT) {
|
|
790
|
-
(0,
|
|
1116
|
+
(0, import_node_fs9.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
791
1117
|
`, "utf8");
|
|
792
1118
|
}
|
|
793
1119
|
};
|
|
@@ -802,6 +1128,17 @@ function createInstallerRun(value, options = {}) {
|
|
|
802
1128
|
return true;
|
|
803
1129
|
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
804
1130
|
});
|
|
1131
|
+
let activeTitle;
|
|
1132
|
+
let chunkOpen = false;
|
|
1133
|
+
let chunkChannel = "stdout";
|
|
1134
|
+
let chunkColumn = 0;
|
|
1135
|
+
const trailingCR = /* @__PURE__ */ new Set();
|
|
1136
|
+
let deferredActivity;
|
|
1137
|
+
const closeChunk = () => {
|
|
1138
|
+
if (chunkOpen) emit("\n", chunkChannel);
|
|
1139
|
+
chunkOpen = false;
|
|
1140
|
+
chunkColumn = 0;
|
|
1141
|
+
};
|
|
805
1142
|
let started = false;
|
|
806
1143
|
let finished = false;
|
|
807
1144
|
const start = () => {
|
|
@@ -816,6 +1153,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
816
1153
|
}
|
|
817
1154
|
};
|
|
818
1155
|
const durable = (title, measure, kind) => {
|
|
1156
|
+
closeChunk();
|
|
1157
|
+
activeTitle = void 0;
|
|
1158
|
+
deferredActivity = void 0;
|
|
819
1159
|
spinner.stop();
|
|
820
1160
|
const rendered = face.step(title, measure, kind);
|
|
821
1161
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
@@ -832,7 +1172,12 @@ function createInstallerRun(value, options = {}) {
|
|
|
832
1172
|
const state = facts.state ?? "ok";
|
|
833
1173
|
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
834
1174
|
if (state === "running") {
|
|
835
|
-
|
|
1175
|
+
if (face.progress(title, facts.measure ?? null)) return;
|
|
1176
|
+
if (activeTitle === title) spinner.say(title, facts.measure ?? null);
|
|
1177
|
+
else {
|
|
1178
|
+
activeTitle = title;
|
|
1179
|
+
spinner.start(title, facts.measure ?? null);
|
|
1180
|
+
}
|
|
836
1181
|
return;
|
|
837
1182
|
}
|
|
838
1183
|
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
@@ -852,8 +1197,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
852
1197
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
853
1198
|
if (facts.detail) run2.relay(facts.detail);
|
|
854
1199
|
},
|
|
855
|
-
milestone({ step, state, ms }) {
|
|
1200
|
+
milestone({ step, state, ms, measure }) {
|
|
856
1201
|
start();
|
|
1202
|
+
if (state === "running") {
|
|
1203
|
+
if (chunkOpen) {
|
|
1204
|
+
deferredActivity = { title: step, measure: measure ?? null };
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (activeTitle === step) spinner.say(step, measure ?? null);
|
|
1208
|
+
else {
|
|
1209
|
+
activeTitle = step;
|
|
1210
|
+
spinner.start(step, measure ?? null);
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
857
1214
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
858
1215
|
},
|
|
859
1216
|
signIn({ url, code }) {
|
|
@@ -867,7 +1224,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
867
1224
|
},
|
|
868
1225
|
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
869
1226
|
relay(text, channel = "stdout", record = true) {
|
|
870
|
-
|
|
1227
|
+
closeChunk();
|
|
1228
|
+
spinner.pause();
|
|
871
1229
|
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
872
1230
|
for (const row of rendered.split("\n")) if (row) {
|
|
873
1231
|
emit(`${row}
|
|
@@ -875,6 +1233,37 @@ function createInstallerRun(value, options = {}) {
|
|
|
875
1233
|
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
876
1234
|
`);
|
|
877
1235
|
}
|
|
1236
|
+
spinner.resume();
|
|
1237
|
+
},
|
|
1238
|
+
relayChunk(text, channel = "stdout") {
|
|
1239
|
+
spinner.pause();
|
|
1240
|
+
if (chunkOpen && chunkChannel !== channel) closeChunk();
|
|
1241
|
+
chunkChannel = channel;
|
|
1242
|
+
if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
|
|
1243
|
+
if (text.endsWith("\r")) trailingCR.add(channel);
|
|
1244
|
+
const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
|
|
1245
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1246
|
+
const characters = [...stripColor(parts[i])];
|
|
1247
|
+
while (characters.length) {
|
|
1248
|
+
if (tty && chunkColumn >= face.width - 6) closeChunk();
|
|
1249
|
+
const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
|
|
1250
|
+
emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
|
|
1251
|
+
chunkColumn += [...part].length;
|
|
1252
|
+
chunkOpen = true;
|
|
1253
|
+
}
|
|
1254
|
+
if (i < parts.length - 1) {
|
|
1255
|
+
emit("\n", channel);
|
|
1256
|
+
chunkOpen = false;
|
|
1257
|
+
chunkColumn = 0;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (!chunkOpen) {
|
|
1261
|
+
if (deferredActivity) {
|
|
1262
|
+
activeTitle = deferredActivity.title;
|
|
1263
|
+
spinner.start(deferredActivity.title, deferredActivity.measure);
|
|
1264
|
+
deferredActivity = void 0;
|
|
1265
|
+
} else spinner.resume();
|
|
1266
|
+
}
|
|
878
1267
|
},
|
|
879
1268
|
cancel() {
|
|
880
1269
|
if (finished) return;
|
|
@@ -883,6 +1272,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
883
1272
|
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
884
1273
|
);
|
|
885
1274
|
start();
|
|
1275
|
+
closeChunk();
|
|
1276
|
+
activeTitle = void 0;
|
|
886
1277
|
spinner.stop();
|
|
887
1278
|
finished = true;
|
|
888
1279
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
@@ -895,6 +1286,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
895
1286
|
validateInstallerOutcome(facts);
|
|
896
1287
|
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
897
1288
|
start();
|
|
1289
|
+
closeChunk();
|
|
1290
|
+
activeTitle = void 0;
|
|
898
1291
|
spinner.stop();
|
|
899
1292
|
finished = true;
|
|
900
1293
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
@@ -915,6 +1308,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
915
1308
|
if (tty) lines([face.signOff()]);
|
|
916
1309
|
},
|
|
917
1310
|
stop() {
|
|
1311
|
+
closeChunk();
|
|
1312
|
+
activeTitle = void 0;
|
|
918
1313
|
spinner.stop();
|
|
919
1314
|
}
|
|
920
1315
|
};
|
|
@@ -923,9 +1318,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
923
1318
|
|
|
924
1319
|
// src/autoupdate.ts
|
|
925
1320
|
var import_node_child_process2 = require("node:child_process");
|
|
926
|
-
var
|
|
927
|
-
var
|
|
928
|
-
var
|
|
1321
|
+
var import_node_fs10 = require("node:fs");
|
|
1322
|
+
var import_node_os3 = require("node:os");
|
|
1323
|
+
var import_node_path5 = require("node:path");
|
|
929
1324
|
function schedulePlatform(override) {
|
|
930
1325
|
const platform = override ?? process.platform;
|
|
931
1326
|
if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
|
|
@@ -942,8 +1337,8 @@ function defaultExec(command2, args) {
|
|
|
942
1337
|
}
|
|
943
1338
|
function homeOf(options) {
|
|
944
1339
|
if (options.homeDir) return options.homeDir;
|
|
945
|
-
if (process.platform === "win32") return process.env.USERPROFILE ?? (0,
|
|
946
|
-
return process.env.HOME ?? (0,
|
|
1340
|
+
if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os3.tmpdir)();
|
|
1341
|
+
return process.env.HOME ?? (0, import_node_os3.tmpdir)();
|
|
947
1342
|
}
|
|
948
1343
|
function scheduleName(config) {
|
|
949
1344
|
return `${config.binName} autoupdate`;
|
|
@@ -969,10 +1364,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
969
1364
|
}
|
|
970
1365
|
if (platform === "darwin") {
|
|
971
1366
|
const label2 = scheduleLabel(config);
|
|
972
|
-
const dir2 = (0,
|
|
973
|
-
(0,
|
|
974
|
-
const plist = (0,
|
|
975
|
-
(0,
|
|
1367
|
+
const dir2 = (0, import_node_path5.join)(home, "Library", "LaunchAgents");
|
|
1368
|
+
(0, import_node_fs10.mkdirSync)(dir2, { recursive: true });
|
|
1369
|
+
const plist = (0, import_node_path5.join)(dir2, `${label2}.plist`);
|
|
1370
|
+
(0, import_node_fs10.writeFileSync)(plist, darwinPlist(label2, command2));
|
|
976
1371
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
977
1372
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
978
1373
|
if (result2.code !== 0) {
|
|
@@ -981,10 +1376,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
981
1376
|
return;
|
|
982
1377
|
}
|
|
983
1378
|
const label = scheduleLabel(config);
|
|
984
|
-
const dir = (0,
|
|
985
|
-
(0,
|
|
986
|
-
(0,
|
|
987
|
-
(0,
|
|
1379
|
+
const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
|
|
1380
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1381
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.service`), linuxService(command2));
|
|
1382
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
988
1383
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
989
1384
|
if (reload.code !== 0) {
|
|
990
1385
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -1009,14 +1404,14 @@ function disableSchedule(config, options = {}) {
|
|
|
1009
1404
|
if (platform === "darwin") {
|
|
1010
1405
|
const label2 = scheduleLabel(config);
|
|
1011
1406
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1012
|
-
(0,
|
|
1407
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
1013
1408
|
return;
|
|
1014
1409
|
}
|
|
1015
1410
|
const label = scheduleLabel(config);
|
|
1016
|
-
const dir = (0,
|
|
1411
|
+
const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
|
|
1017
1412
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
1018
|
-
(0,
|
|
1019
|
-
(0,
|
|
1413
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.service`), { force: true });
|
|
1414
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.timer`), { force: true });
|
|
1020
1415
|
}
|
|
1021
1416
|
function querySchedule(config, options = {}) {
|
|
1022
1417
|
const platform = schedulePlatform(options.platform);
|
|
@@ -1034,12 +1429,12 @@ function querySchedule(config, options = {}) {
|
|
|
1034
1429
|
return state2;
|
|
1035
1430
|
}
|
|
1036
1431
|
if (platform === "darwin") {
|
|
1037
|
-
const plist = (0,
|
|
1038
|
-
if (!(0,
|
|
1432
|
+
const plist = (0, import_node_path5.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
1433
|
+
if (!(0, import_node_fs10.existsSync)(plist)) return { supported: true, enabled: false };
|
|
1039
1434
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
1040
1435
|
}
|
|
1041
|
-
const timer = (0,
|
|
1042
|
-
if (!(0,
|
|
1436
|
+
const timer = (0, import_node_path5.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
1437
|
+
if (!(0, import_node_fs10.existsSync)(timer)) return { supported: true, enabled: false };
|
|
1043
1438
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
1044
1439
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
1045
1440
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -1096,87 +1491,6 @@ function firstLine(text) {
|
|
|
1096
1491
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1097
1492
|
}
|
|
1098
1493
|
|
|
1099
|
-
// src/config.ts
|
|
1100
|
-
var import_node_fs9 = require("node:fs");
|
|
1101
|
-
var import_node_sea = require("node:sea");
|
|
1102
|
-
|
|
1103
|
-
// src/module-url.ts
|
|
1104
|
-
var import_node_url = require("node:url");
|
|
1105
|
-
var import_meta = {};
|
|
1106
|
-
function moduleUrl() {
|
|
1107
|
-
if (typeof __filename === "string") return (0, import_node_url.pathToFileURL)(__filename).href;
|
|
1108
|
-
return import_meta.url;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// src/config.ts
|
|
1112
|
-
function publicKeyBytes(config) {
|
|
1113
|
-
const raw = Buffer.from(config.publicKey, "base64");
|
|
1114
|
-
if (raw.length !== 32) {
|
|
1115
|
-
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
1116
|
-
}
|
|
1117
|
-
return raw;
|
|
1118
|
-
}
|
|
1119
|
-
function loadProductConfig(options = {}) {
|
|
1120
|
-
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
1121
|
-
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
1122
|
-
if (explicit) {
|
|
1123
|
-
return parseProductConfig((0, import_node_fs9.readFileSync)(explicit, "utf8"));
|
|
1124
|
-
}
|
|
1125
|
-
try {
|
|
1126
|
-
return parseProductConfig((0, import_node_fs9.readFileSync)(devFallback, "utf8"));
|
|
1127
|
-
} catch {
|
|
1128
|
-
}
|
|
1129
|
-
try {
|
|
1130
|
-
const asset = (0, import_node_sea.getAsset)("product.json", "utf8");
|
|
1131
|
-
if (typeof asset === "string") return parseProductConfig(asset);
|
|
1132
|
-
} catch {
|
|
1133
|
-
}
|
|
1134
|
-
throw new Error(
|
|
1135
|
-
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
function parseProductConfig(text) {
|
|
1139
|
-
let data;
|
|
1140
|
-
try {
|
|
1141
|
-
data = JSON.parse(text);
|
|
1142
|
-
} catch {
|
|
1143
|
-
throw new Error("product config is not valid JSON");
|
|
1144
|
-
}
|
|
1145
|
-
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
1146
|
-
const record = data;
|
|
1147
|
-
const product = field(record, "product");
|
|
1148
|
-
const host = field(record, "host").replace(/\/+$/, "");
|
|
1149
|
-
const loginKind = field(record, "loginKind");
|
|
1150
|
-
const binName = field(record, "binName");
|
|
1151
|
-
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
1152
|
-
if (loginKind !== "github" && loginKind !== "google") {
|
|
1153
|
-
throw new Error('product config loginKind must be "github" or "google"');
|
|
1154
|
-
}
|
|
1155
|
-
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
1156
|
-
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
1157
|
-
publicKeyBytes(config);
|
|
1158
|
-
if (loginKind === "github") {
|
|
1159
|
-
const clientId = record.githubClientId;
|
|
1160
|
-
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
1161
|
-
throw new Error("product config needs githubClientId for the github loginKind");
|
|
1162
|
-
}
|
|
1163
|
-
config.githubClientId = clientId;
|
|
1164
|
-
} else if (typeof record.githubClientId === "string") {
|
|
1165
|
-
config.githubClientId = record.githubClientId;
|
|
1166
|
-
}
|
|
1167
|
-
try {
|
|
1168
|
-
const url = new URL(host);
|
|
1169
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
1170
|
-
} catch {
|
|
1171
|
-
throw new Error("product config host must be an http(s) URL");
|
|
1172
|
-
}
|
|
1173
|
-
return config;
|
|
1174
|
-
}
|
|
1175
|
-
function field(record, key) {
|
|
1176
|
-
const value = record[key];
|
|
1177
|
-
return typeof value === "string" ? value : "";
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
1494
|
// src/login-github.ts
|
|
1181
1495
|
var import_node_child_process3 = require("node:child_process");
|
|
1182
1496
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1299,7 +1613,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
1299
1613
|
}
|
|
1300
1614
|
|
|
1301
1615
|
// src/login-google.ts
|
|
1302
|
-
var
|
|
1616
|
+
var import_node_crypto5 = require("node:crypto");
|
|
1303
1617
|
var import_node_http = require("node:http");
|
|
1304
1618
|
var b64url = (bytes) => bytes.toString("base64url");
|
|
1305
1619
|
async function loginGoogle(options) {
|
|
@@ -1321,9 +1635,9 @@ async function loginGoogle(options) {
|
|
|
1321
1635
|
if (typeof clientId !== "string" || !clientId) {
|
|
1322
1636
|
throw new Error("the sign-in server returned a malformed registration");
|
|
1323
1637
|
}
|
|
1324
|
-
const verifier = b64url((0,
|
|
1325
|
-
const challenge = b64url((0,
|
|
1326
|
-
const state = b64url((0,
|
|
1638
|
+
const verifier = b64url((0, import_node_crypto5.randomBytes)(32));
|
|
1639
|
+
const challenge = b64url((0, import_node_crypto5.createHash)("sha256").update(verifier).digest());
|
|
1640
|
+
const state = b64url((0, import_node_crypto5.randomBytes)(16));
|
|
1327
1641
|
const authorize = new URL(`${server}/oauth/authorize`);
|
|
1328
1642
|
authorize.search = new URLSearchParams({
|
|
1329
1643
|
response_type: "code",
|
|
@@ -1411,175 +1725,8 @@ function page(title, body) {
|
|
|
1411
1725
|
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>`;
|
|
1412
1726
|
}
|
|
1413
1727
|
|
|
1414
|
-
// src/payload.ts
|
|
1415
|
-
var import_node_crypto5 = require("node:crypto");
|
|
1416
|
-
var import_node_fs10 = require("node:fs");
|
|
1417
|
-
var import_node_os3 = require("node:os");
|
|
1418
|
-
var import_node_path5 = require("node:path");
|
|
1419
|
-
|
|
1420
|
-
// src/canonical.ts
|
|
1421
|
-
function canonicalJson(value) {
|
|
1422
|
-
return encode(value);
|
|
1423
|
-
}
|
|
1424
|
-
function encode(value) {
|
|
1425
|
-
if (value === null) return "null";
|
|
1426
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1427
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1428
|
-
if (typeof value === "number") {
|
|
1429
|
-
if (!Number.isFinite(value)) {
|
|
1430
|
-
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
1431
|
-
}
|
|
1432
|
-
return JSON.stringify(value);
|
|
1433
|
-
}
|
|
1434
|
-
if (Array.isArray(value)) {
|
|
1435
|
-
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
1436
|
-
}
|
|
1437
|
-
if (typeof value === "object") {
|
|
1438
|
-
const record = value;
|
|
1439
|
-
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
1440
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
1441
|
-
}
|
|
1442
|
-
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// src/payload.ts
|
|
1446
|
-
var NeedsLoginError = class extends Error {
|
|
1447
|
-
constructor() {
|
|
1448
|
-
super("signed out \u2014 run login first");
|
|
1449
|
-
this.name = "NeedsLoginError";
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
var ForbiddenError = class extends Error {
|
|
1453
|
-
constructor() {
|
|
1454
|
-
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
1455
|
-
this.name = "ForbiddenError";
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
|
-
function canonicalManifestBytes(manifest) {
|
|
1459
|
-
return Buffer.from(
|
|
1460
|
-
canonicalJson({
|
|
1461
|
-
created: manifest.created,
|
|
1462
|
-
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
1463
|
-
version: manifest.version
|
|
1464
|
-
}),
|
|
1465
|
-
"utf8"
|
|
1466
|
-
);
|
|
1467
|
-
}
|
|
1468
|
-
function ed25519PublicKey(config) {
|
|
1469
|
-
const raw = publicKeyBytes(config);
|
|
1470
|
-
return (0, import_node_crypto5.createPublicKey)({
|
|
1471
|
-
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
1472
|
-
format: "jwk"
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
function verifyManifest(manifest, signature, config) {
|
|
1476
|
-
try {
|
|
1477
|
-
const signatureBytes = Buffer.from(signature, "base64");
|
|
1478
|
-
if (signatureBytes.length === 0) return false;
|
|
1479
|
-
return (0, import_node_crypto5.verify)(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
1480
|
-
} catch {
|
|
1481
|
-
return false;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
function verifyFileBytes(entry2, bytes) {
|
|
1485
|
-
if (entry2.size !== bytes.length) return false;
|
|
1486
|
-
return (0, import_node_crypto5.createHash)("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
1487
|
-
}
|
|
1488
|
-
async function readErrorCode(response) {
|
|
1489
|
-
try {
|
|
1490
|
-
const data = await response.json();
|
|
1491
|
-
return typeof data.error === "string" ? data.error : "";
|
|
1492
|
-
} catch {
|
|
1493
|
-
return "";
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
function throwForStatus(status, errorCode) {
|
|
1497
|
-
if (status === 401) throw new NeedsLoginError();
|
|
1498
|
-
if (status === 403) throw new ForbiddenError();
|
|
1499
|
-
throw new Error(
|
|
1500
|
-
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
1501
|
-
);
|
|
1502
|
-
}
|
|
1503
|
-
async function getJson(url, accessToken, fetchImpl) {
|
|
1504
|
-
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
1505
|
-
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
1506
|
-
return { status: response.status, json: await response.json() };
|
|
1507
|
-
}
|
|
1508
|
-
function parseManifest(json) {
|
|
1509
|
-
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
1510
|
-
const record = json;
|
|
1511
|
-
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
1512
|
-
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
1513
|
-
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
1514
|
-
if (typeof record.signature !== "string" || !record.signature) {
|
|
1515
|
-
throw new Error("the release manifest is unsigned");
|
|
1516
|
-
}
|
|
1517
|
-
const files = record.files.map((entry2) => {
|
|
1518
|
-
const file = entry2;
|
|
1519
|
-
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
1520
|
-
throw new Error("the release manifest lists a malformed file");
|
|
1521
|
-
}
|
|
1522
|
-
const safe = safeManifestPath(file.path);
|
|
1523
|
-
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
1524
|
-
return { path: safe, sha256: file.sha256, size: file.size };
|
|
1525
|
-
});
|
|
1526
|
-
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
1527
|
-
}
|
|
1528
|
-
function safeManifestPath(rawPath) {
|
|
1529
|
-
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
1530
|
-
if (rawPath.startsWith("/")) return null;
|
|
1531
|
-
const segments = rawPath.split("/");
|
|
1532
|
-
for (const segment of segments) {
|
|
1533
|
-
if (segment === "" || segment === "." || segment === "..") return null;
|
|
1534
|
-
}
|
|
1535
|
-
return segments.join("/");
|
|
1536
|
-
}
|
|
1537
|
-
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
1538
|
-
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
1539
|
-
const manifest = parseManifest(json);
|
|
1540
|
-
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
1541
|
-
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
1542
|
-
}
|
|
1543
|
-
return manifest;
|
|
1544
|
-
}
|
|
1545
|
-
async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
1546
|
-
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1547
|
-
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
1548
|
-
headers: { authorization: `Bearer ${accessToken}` }
|
|
1549
|
-
});
|
|
1550
|
-
if (!response.ok) {
|
|
1551
|
-
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
1552
|
-
throwForStatus(response.status, await readErrorCode(response));
|
|
1553
|
-
}
|
|
1554
|
-
return Buffer.from(await response.arrayBuffer());
|
|
1555
|
-
}
|
|
1556
|
-
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
1557
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1558
|
-
const staging = (0, import_node_path5.join)((0, import_node_os3.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
1559
|
-
(0, import_node_fs10.mkdirSync)(staging, { recursive: true });
|
|
1560
|
-
try {
|
|
1561
|
-
for (const entry2 of manifest.files) {
|
|
1562
|
-
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl);
|
|
1563
|
-
if (!verifyFileBytes(entry2, bytes)) {
|
|
1564
|
-
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
1565
|
-
}
|
|
1566
|
-
const dest = (0, import_node_path5.join)(staging, entry2.path);
|
|
1567
|
-
(0, import_node_fs10.mkdirSync)((0, import_node_path5.dirname)(dest), { recursive: true });
|
|
1568
|
-
(0, import_node_fs10.writeFileSync)(dest, bytes);
|
|
1569
|
-
}
|
|
1570
|
-
const target = (0, import_node_path5.join)(dir, "payload");
|
|
1571
|
-
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1572
|
-
(0, import_node_fs10.rmSync)(target, { force: true, recursive: true });
|
|
1573
|
-
(0, import_node_fs10.renameSync)(staging, target);
|
|
1574
|
-
} catch (error) {
|
|
1575
|
-
(0, import_node_fs10.rmSync)(staging, { force: true, recursive: true });
|
|
1576
|
-
throw error;
|
|
1577
|
-
}
|
|
1578
|
-
return manifest.version;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
1728
|
// src/index.ts
|
|
1582
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1729
|
+
var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
|
|
1583
1730
|
function defaultPrint(message) {
|
|
1584
1731
|
process.stdout.write(`${message}
|
|
1585
1732
|
`);
|
|
@@ -1815,12 +1962,13 @@ function parseProgress(raw) {
|
|
|
1815
1962
|
const message = JSON.parse(line);
|
|
1816
1963
|
if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
|
|
1817
1964
|
const state = message.state ?? "ok";
|
|
1818
|
-
if (state !== "ok" && state !== "fail" && state !== "note") continue;
|
|
1965
|
+
if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
|
|
1819
1966
|
if (message.ms !== void 0 && typeof message.ms !== "number") continue;
|
|
1820
1967
|
progress.push({
|
|
1821
1968
|
step: message.step,
|
|
1822
1969
|
state,
|
|
1823
|
-
...typeof message.ms === "number" ? { ms: message.ms } : {}
|
|
1970
|
+
...typeof message.ms === "number" && Number.isFinite(message.ms) && message.ms >= 0 ? { ms: message.ms } : {},
|
|
1971
|
+
...typeof message.measure === "string" ? { measure: message.measure } : {}
|
|
1824
1972
|
});
|
|
1825
1973
|
} catch {
|
|
1826
1974
|
}
|
|
@@ -1889,7 +2037,7 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1889
2037
|
version = manifest.version;
|
|
1890
2038
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1891
2039
|
const current = readState(dir);
|
|
1892
|
-
const unchanged =
|
|
2040
|
+
const unchanged = current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
1893
2041
|
if (unchanged && current.acquired) {
|
|
1894
2042
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
1895
2043
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -1897,11 +2045,17 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1897
2045
|
if (!unchanged) {
|
|
1898
2046
|
installer.phase("download", { state: "running" });
|
|
1899
2047
|
started = Date.now();
|
|
1900
|
-
const
|
|
2048
|
+
const cached = reusableCandidate(dir, manifest);
|
|
2049
|
+
const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
|
|
1901
2050
|
const candidateDir = (0, import_node_path6.join)(dir, "candidates", candidate);
|
|
1902
|
-
|
|
2051
|
+
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2052
|
+
state: "running",
|
|
2053
|
+
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
|
|
2054
|
+
});
|
|
2055
|
+
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress });
|
|
1903
2056
|
const acquisition = (0, import_node_fs11.existsSync)((0, import_node_path6.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path6.join)(candidateDir, "payload")) : null;
|
|
1904
2057
|
if (acquisition) {
|
|
2058
|
+
rememberCandidate(dir, candidate, manifest);
|
|
1905
2059
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1906
2060
|
installer.phase("activate", { state: "running" });
|
|
1907
2061
|
let outcome;
|
|
@@ -1909,7 +2063,9 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1909
2063
|
await installAcquisition(dir, candidate, version, acquisition, {
|
|
1910
2064
|
fetchImpl,
|
|
1911
2065
|
env,
|
|
2066
|
+
onProgress,
|
|
1912
2067
|
run: async (command2, cwd, childEnv) => {
|
|
2068
|
+
installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
|
|
1913
2069
|
const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
|
|
1914
2070
|
if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
|
|
1915
2071
|
return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
|
|
@@ -2027,17 +2183,22 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2027
2183
|
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
2028
2184
|
}
|
|
2029
2185
|
async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
2186
|
+
installer.start();
|
|
2030
2187
|
const [command2, ...args] = entry2;
|
|
2031
2188
|
const shell = needsShell(command2);
|
|
2032
2189
|
const progress = !(process.platform === "win32" && shell);
|
|
2033
2190
|
const outcomeDir = (0, import_node_fs11.mkdtempSync)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
|
|
2034
2191
|
const outcomeFile = (0, import_node_path6.join)(outcomeDir, "outcome.json");
|
|
2192
|
+
const progressFile = (0, import_node_path6.join)(outcomeDir, "progress.jsonl");
|
|
2035
2193
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2036
2194
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2195
|
+
delete childEnv.MM_PROGRESS_FILE;
|
|
2037
2196
|
delete childEnv.MM_PROGRESS_FD;
|
|
2038
2197
|
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
2039
2198
|
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
2199
|
+
else Object.assign(childEnv, { MM_PROGRESS_FILE: progressFile, MM_PROGRESS_PROTOCOL: "1" });
|
|
2040
2200
|
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
2201
|
+
let progressError;
|
|
2041
2202
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2042
2203
|
try {
|
|
2043
2204
|
const result = await new Promise((resolve3) => {
|
|
@@ -2058,24 +2219,44 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2058
2219
|
}
|
|
2059
2220
|
partial = held ? safe.slice(-held) : "";
|
|
2060
2221
|
const visible = held ? safe.slice(0, -held) : safe;
|
|
2061
|
-
if (visible) installer.
|
|
2222
|
+
if (visible) installer.relayChunk(visible, channel2);
|
|
2062
2223
|
}).on("end", () => {
|
|
2063
|
-
if (partial) installer.
|
|
2224
|
+
if (partial) installer.relayChunk("[redacted]", channel2);
|
|
2064
2225
|
});
|
|
2065
2226
|
}
|
|
2066
2227
|
let pending = "";
|
|
2228
|
+
let progressOffset = 0;
|
|
2229
|
+
const progressDecoder = new import_node_string_decoder.StringDecoder("utf8");
|
|
2230
|
+
const receive = (text) => {
|
|
2231
|
+
pending += text;
|
|
2232
|
+
const end = pending.lastIndexOf("\n");
|
|
2233
|
+
if (end >= 0) {
|
|
2234
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step), ...record.measure ? { measure: redact(record.measure) } : {} });
|
|
2235
|
+
pending = pending.slice(end + 1);
|
|
2236
|
+
}
|
|
2237
|
+
if (pending.length > 65536) pending = "";
|
|
2238
|
+
};
|
|
2239
|
+
const pollProgress = () => {
|
|
2240
|
+
if (progressError) return;
|
|
2241
|
+
try {
|
|
2242
|
+
if (!(0, import_node_fs11.existsSync)(progressFile)) return;
|
|
2243
|
+
const bytes = (0, import_node_fs11.readFileSync)(progressFile);
|
|
2244
|
+
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2245
|
+
progressOffset = bytes.length;
|
|
2246
|
+
} catch {
|
|
2247
|
+
progressError = "installer progress: could not read child progress";
|
|
2248
|
+
if (timer) clearInterval(timer);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
const timer = progress ? void 0 : setInterval(pollProgress, 90);
|
|
2252
|
+
child.once("close", () => {
|
|
2253
|
+
if (timer) clearInterval(timer);
|
|
2254
|
+
pollProgress();
|
|
2255
|
+
});
|
|
2067
2256
|
const channel = child.stdio[3];
|
|
2068
2257
|
if (channel && "setEncoding" in channel) {
|
|
2069
2258
|
channel.setEncoding("utf8");
|
|
2070
|
-
channel.on("data",
|
|
2071
|
-
pending += text;
|
|
2072
|
-
const end = pending.lastIndexOf("\n");
|
|
2073
|
-
if (end >= 0) {
|
|
2074
|
-
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
2075
|
-
pending = pending.slice(end + 1);
|
|
2076
|
-
}
|
|
2077
|
-
if (pending.length > 65536) pending = "";
|
|
2078
|
-
});
|
|
2259
|
+
channel.on("data", receive);
|
|
2079
2260
|
}
|
|
2080
2261
|
child.on("error", (error) => resolve3({ ok: false, error: error.message }));
|
|
2081
2262
|
child.on("close", (code) => resolve3({
|
|
@@ -2084,6 +2265,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2084
2265
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
2085
2266
|
}));
|
|
2086
2267
|
});
|
|
2268
|
+
if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
|
|
2087
2269
|
try {
|
|
2088
2270
|
const outcome = readInstallerOutcome(outcomeFile);
|
|
2089
2271
|
return { ...result, ...outcome ? { outcome } : {} };
|
|
@@ -2091,6 +2273,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2091
2273
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2092
2274
|
}
|
|
2093
2275
|
} finally {
|
|
2276
|
+
if ((0, import_node_fs11.existsSync)(progressFile)) (0, import_node_fs11.unlinkSync)(progressFile);
|
|
2094
2277
|
if ((0, import_node_fs11.existsSync)(outcomeFile)) (0, import_node_fs11.unlinkSync)(outcomeFile);
|
|
2095
2278
|
(0, import_node_fs11.rmdirSync)(outcomeDir);
|
|
2096
2279
|
}
|