@mutmutco/installer-launcher 0.1.13 → 0.1.15
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 +654 -371
- package/dist/launcher.sea.cjs +660 -377
- 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,316 @@ 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 fetchReleaseVersion(config, fetchImpl = fetch) {
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetchImpl(`${config.host}/release/version`);
|
|
441
|
+
if (!response.ok) return null;
|
|
442
|
+
const json = await response.json();
|
|
443
|
+
const version = typeof json?.version === "string" ? json.version.trim() : "";
|
|
444
|
+
return version || null;
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
|
|
450
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
451
|
+
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
452
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
453
|
+
});
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
456
|
+
throwForStatus(response.status, await readErrorCode(response));
|
|
457
|
+
}
|
|
458
|
+
return readDownload(response, onProgress, expectedSize);
|
|
459
|
+
}
|
|
460
|
+
var VERSION_CACHE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
461
|
+
function versionCacheRoot(dir, version) {
|
|
462
|
+
return VERSION_CACHE_RE.test(version) ? (0, import_node_path3.join)(dir, "payload-cache", version) : "";
|
|
463
|
+
}
|
|
464
|
+
function cachedPayloadForVersion(dir, version) {
|
|
465
|
+
const root = versionCacheRoot(dir, version);
|
|
466
|
+
if (!root) return null;
|
|
467
|
+
const payload = (0, import_node_path3.join)(root, "payload");
|
|
468
|
+
return (0, import_node_fs4.existsSync)(payload) ? payload : null;
|
|
469
|
+
}
|
|
470
|
+
function rememberPayloadVersion(dir, version, payload) {
|
|
471
|
+
try {
|
|
472
|
+
const root = versionCacheRoot(dir, version);
|
|
473
|
+
if (!root) return;
|
|
474
|
+
const dest = (0, import_node_path3.join)(root, "payload");
|
|
475
|
+
if ((0, import_node_fs4.existsSync)(dest)) return;
|
|
476
|
+
(0, import_node_fs4.mkdirSync)(root, { recursive: true });
|
|
477
|
+
(0, import_node_fs4.cpSync)(payload, dest, { recursive: true });
|
|
478
|
+
} catch {
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
482
|
+
const target = (0, import_node_path3.join)(dir, "payload");
|
|
483
|
+
const cached = options.cacheDir ? cachedPayloadForVersion(options.cacheDir, manifest.version) : null;
|
|
484
|
+
if (cached) {
|
|
485
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
486
|
+
(0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
|
|
487
|
+
(0, import_node_fs4.cpSync)(cached, target, { recursive: true });
|
|
488
|
+
return manifest.version;
|
|
489
|
+
}
|
|
490
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
491
|
+
const staging = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
492
|
+
(0, import_node_fs4.mkdirSync)(staging, { recursive: true });
|
|
493
|
+
try {
|
|
494
|
+
const total = manifest.files.reduce((sum, entry2) => sum + entry2.size, 0);
|
|
495
|
+
let downloaded = 0;
|
|
496
|
+
for (const entry2 of manifest.files) {
|
|
497
|
+
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl, (progress) => options.onProgress?.({ done: downloaded + progress.done, total }), entry2.size);
|
|
498
|
+
downloaded += bytes.length;
|
|
499
|
+
if (!verifyFileBytes(entry2, bytes)) {
|
|
500
|
+
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
501
|
+
}
|
|
502
|
+
const dest = (0, import_node_path3.join)(staging, entry2.path);
|
|
503
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(dest), { recursive: true });
|
|
504
|
+
(0, import_node_fs4.writeFileSync)(dest, bytes);
|
|
505
|
+
}
|
|
506
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
507
|
+
(0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
|
|
508
|
+
(0, import_node_fs4.renameSync)(staging, target);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
(0, import_node_fs4.rmSync)(staging, { force: true, recursive: true });
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
if (options.cacheDir) rememberPayloadVersion(options.cacheDir, manifest.version, target);
|
|
514
|
+
return manifest.version;
|
|
515
|
+
}
|
|
516
|
+
|
|
198
517
|
// src/acquisition.ts
|
|
518
|
+
function reusableCandidate(dir, manifest) {
|
|
519
|
+
try {
|
|
520
|
+
const saved = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), "utf8"));
|
|
521
|
+
if (saved.signature !== manifest.signature) return null;
|
|
522
|
+
const root = candidateRoot(dir, saved.candidate);
|
|
523
|
+
if (!(0, import_node_fs5.existsSync)((0, import_node_path4.join)(root, "acquired.json"))) return null;
|
|
524
|
+
for (const file of manifest.files) {
|
|
525
|
+
if (!safePath(file.path) || !verifyFileBytes(file, (0, import_node_fs5.readFileSync)((0, import_node_path4.join)(root, "payload", file.path)))) return null;
|
|
526
|
+
}
|
|
527
|
+
return saved.candidate;
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function rememberCandidate(dir, candidate, manifest) {
|
|
533
|
+
candidateRoot(dir, candidate);
|
|
534
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), JSON.stringify({ candidate, signature: manifest.signature }), { mode: 384 });
|
|
535
|
+
}
|
|
199
536
|
function safePath(value) {
|
|
200
537
|
return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
201
538
|
}
|
|
@@ -203,7 +540,7 @@ function argumentsValid(value) {
|
|
|
203
540
|
return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
|
|
204
541
|
}
|
|
205
542
|
function readAcquisition(payload) {
|
|
206
|
-
const metadata = JSON.parse((0,
|
|
543
|
+
const metadata = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, "payload.json"), "utf8"));
|
|
207
544
|
if (metadata.acquisition === void 0) return null;
|
|
208
545
|
const a = metadata.acquisition;
|
|
209
546
|
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 +556,25 @@ function runtimeEnvironment(node, inherited, platform = process.platform) {
|
|
|
219
556
|
current = (keys.length ? env[keys[0]] : "") ?? "";
|
|
220
557
|
for (const key of keys) delete env[key];
|
|
221
558
|
}
|
|
222
|
-
env.PATH = `${(0,
|
|
559
|
+
env.PATH = `${(0, import_node_path4.dirname)(node)}${platform === "win32" ? ";" : ":"}${current}`;
|
|
223
560
|
return env;
|
|
224
561
|
}
|
|
225
562
|
function lockInstallation(dir) {
|
|
226
|
-
(0,
|
|
227
|
-
const path = (0,
|
|
228
|
-
const owner = `${process.pid}:${(0,
|
|
563
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
564
|
+
const path = (0, import_node_path4.join)(dir, "installation.lock");
|
|
565
|
+
const owner = `${process.pid}:${(0, import_node_crypto4.randomUUID)()}`;
|
|
229
566
|
try {
|
|
230
|
-
(0,
|
|
567
|
+
(0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
|
|
231
568
|
} catch (error) {
|
|
232
569
|
if (error.code !== "EEXIST") throw error;
|
|
233
570
|
const recovery = `${path}.recovery`;
|
|
234
571
|
try {
|
|
235
|
-
(0,
|
|
572
|
+
(0, import_node_fs5.writeFileSync)(recovery, owner, { flag: "wx", mode: 384, flush: true });
|
|
236
573
|
} catch {
|
|
237
574
|
throw new Error("installation lock recovery is already pending");
|
|
238
575
|
}
|
|
239
576
|
try {
|
|
240
|
-
const previous = (0,
|
|
577
|
+
const previous = (0, import_node_fs5.readFileSync)(path, "utf8");
|
|
241
578
|
const pid = Number(previous.split(":")[0]);
|
|
242
579
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
|
|
243
580
|
try {
|
|
@@ -246,32 +583,32 @@ function lockInstallation(dir) {
|
|
|
246
583
|
} catch (probe) {
|
|
247
584
|
if (probe.code !== "ESRCH") throw probe;
|
|
248
585
|
}
|
|
249
|
-
(0,
|
|
250
|
-
(0,
|
|
586
|
+
(0, import_node_fs5.unlinkSync)(path);
|
|
587
|
+
(0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384, flush: true });
|
|
251
588
|
} finally {
|
|
252
|
-
(0,
|
|
589
|
+
(0, import_node_fs5.unlinkSync)(recovery);
|
|
253
590
|
}
|
|
254
591
|
}
|
|
255
592
|
let released = false;
|
|
256
593
|
return () => {
|
|
257
|
-
if (!released && (0,
|
|
594
|
+
if (!released && (0, import_node_fs5.readFileSync)(path, "utf8") === owner) (0, import_node_fs5.unlinkSync)(path);
|
|
258
595
|
released = true;
|
|
259
596
|
};
|
|
260
597
|
}
|
|
261
598
|
function pendingPath(dir) {
|
|
262
|
-
return (0,
|
|
599
|
+
return (0, import_node_path4.join)(dir, "acquisition-pending.json");
|
|
263
600
|
}
|
|
264
601
|
function candidateRoot(dir, candidate) {
|
|
265
602
|
if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
|
|
266
|
-
return (0,
|
|
603
|
+
return (0, import_node_path4.join)(dir, "candidates", candidate);
|
|
267
604
|
}
|
|
268
605
|
function packageRoot(prefix, acquisition) {
|
|
269
|
-
return (0,
|
|
606
|
+
return (0, import_node_path4.join)(prefix, "node_modules", acquisition.package);
|
|
270
607
|
}
|
|
271
608
|
function entry(root, path) {
|
|
272
|
-
const actual = (0,
|
|
273
|
-
const rel = (0,
|
|
274
|
-
if (rel.startsWith("..") || (0,
|
|
609
|
+
const actual = (0, import_node_fs5.realpathSync)((0, import_node_path4.join)(root, path));
|
|
610
|
+
const rel = (0, import_node_path4.relative)((0, import_node_fs5.realpathSync)(root), actual);
|
|
611
|
+
if (rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel)) throw new Error("acquired entry escapes package");
|
|
275
612
|
return actual;
|
|
276
613
|
}
|
|
277
614
|
function argv(node, root, file, args, prefix, version) {
|
|
@@ -281,54 +618,59 @@ function acquisitionRepairCommand(dir, version) {
|
|
|
281
618
|
const state = readState(dir);
|
|
282
619
|
if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
|
|
283
620
|
const root = candidateRoot(dir, state.acquired.candidate);
|
|
284
|
-
const acquisition = readAcquisition((0,
|
|
621
|
+
const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
|
|
285
622
|
if (!acquisition) throw new Error("installed acquisition declaration is missing");
|
|
286
|
-
const prefix = (0,
|
|
623
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
287
624
|
const productRoot = packageRoot(prefix, acquisition);
|
|
288
|
-
const pkg = JSON.parse((0,
|
|
625
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
|
|
289
626
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
|
|
290
|
-
return argv((0,
|
|
627
|
+
return argv((0, import_node_fs5.realpathSync)(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
|
|
291
628
|
}
|
|
292
629
|
async function recoverAcquisition(dir, run2, env) {
|
|
293
|
-
if (!(0,
|
|
294
|
-
const pending = JSON.parse((0,
|
|
630
|
+
if (!(0, import_node_fs5.existsSync)(pendingPath(dir))) return;
|
|
631
|
+
const pending = JSON.parse((0, import_node_fs5.readFileSync)(pendingPath(dir), "utf8"));
|
|
295
632
|
if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
|
|
296
633
|
const root = candidateRoot(dir, pending.candidate);
|
|
297
634
|
if (readState(dir)?.acquired?.candidate === pending.candidate) {
|
|
298
|
-
(0,
|
|
635
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
299
636
|
return;
|
|
300
637
|
}
|
|
301
|
-
const acquisition = readAcquisition((0,
|
|
638
|
+
const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
|
|
302
639
|
if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
|
|
303
|
-
const prefix = (0,
|
|
640
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
304
641
|
const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
|
|
305
642
|
if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
|
|
306
643
|
if (pending.previous) writeState(dir, pending.previous);
|
|
307
|
-
else if ((0,
|
|
308
|
-
(0,
|
|
644
|
+
else if ((0, import_node_fs5.existsSync)(statePath(dir))) (0, import_node_fs5.unlinkSync)(statePath(dir));
|
|
645
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
309
646
|
}
|
|
310
647
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
311
648
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
312
649
|
const root = candidateRoot(dir, candidate);
|
|
313
|
-
const payload = (0,
|
|
314
|
-
const prefix = (0,
|
|
315
|
-
(0,
|
|
316
|
-
const
|
|
650
|
+
const payload = (0, import_node_path4.join)(root, "payload");
|
|
651
|
+
const prefix = (0, import_node_path4.join)(root, "prefix");
|
|
652
|
+
const prepared = (0, import_node_path4.join)(root, "acquired.json");
|
|
653
|
+
const archiveHash = (0, import_node_crypto4.createHash)("sha256").update((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, acquisition.archive))).digest("hex");
|
|
654
|
+
const reused = (0, import_node_fs5.existsSync)(prepared);
|
|
655
|
+
if (reused && JSON.parse((0, import_node_fs5.readFileSync)(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
|
|
656
|
+
if (!reused) (0, import_node_fs5.mkdirSync)(prefix);
|
|
657
|
+
const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
|
|
317
658
|
const env = runtimeEnvironment(runtime.node, options.env);
|
|
318
659
|
const npmEnv = { ...env };
|
|
319
660
|
for (const key of Object.keys(npmEnv)) {
|
|
320
661
|
if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
|
|
321
662
|
}
|
|
322
|
-
const npmrc = (0,
|
|
323
|
-
const globalrc = (0,
|
|
324
|
-
(0,
|
|
325
|
-
(0,
|
|
663
|
+
const npmrc = (0, import_node_path4.join)(root, "public.npmrc");
|
|
664
|
+
const globalrc = (0, import_node_path4.join)(root, "global.npmrc");
|
|
665
|
+
(0, import_node_fs5.writeFileSync)(npmrc, "registry=https://registry.npmjs.org/\n");
|
|
666
|
+
(0, import_node_fs5.writeFileSync)(globalrc, "");
|
|
326
667
|
const install = [
|
|
327
668
|
runtime.node,
|
|
328
669
|
runtime.npm,
|
|
329
670
|
"install",
|
|
330
671
|
"--prefix",
|
|
331
672
|
prefix,
|
|
673
|
+
"--install-strategy=shallow",
|
|
332
674
|
"--ignore-scripts",
|
|
333
675
|
"--no-audit",
|
|
334
676
|
"--no-fund",
|
|
@@ -338,17 +680,18 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
338
680
|
globalrc,
|
|
339
681
|
"--registry",
|
|
340
682
|
"https://registry.npmjs.org/",
|
|
341
|
-
(0,
|
|
683
|
+
(0, import_node_path4.resolve)(payload, acquisition.archive)
|
|
342
684
|
];
|
|
343
|
-
if (!await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
685
|
+
if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
344
686
|
const productRoot = packageRoot(prefix, acquisition);
|
|
345
|
-
const pkg = JSON.parse((0,
|
|
687
|
+
const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
|
|
346
688
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
|
|
347
689
|
const runEntry = entry(productRoot, acquisition.runEntry);
|
|
348
690
|
const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
|
|
349
691
|
entry(productRoot, acquisition.rollbackEntry);
|
|
692
|
+
if (!reused) (0, import_node_fs5.writeFileSync)(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
|
|
350
693
|
const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
|
|
351
|
-
(0,
|
|
694
|
+
(0, import_node_fs5.writeFileSync)(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
|
|
352
695
|
try {
|
|
353
696
|
if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
|
|
354
697
|
(options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
|
|
@@ -356,7 +699,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
356
699
|
await recoverAcquisition(dir, options.run, env);
|
|
357
700
|
throw error;
|
|
358
701
|
}
|
|
359
|
-
(0,
|
|
702
|
+
(0, import_node_fs5.unlinkSync)(pendingPath(dir));
|
|
360
703
|
}
|
|
361
704
|
|
|
362
705
|
// src/index.ts
|
|
@@ -367,36 +710,36 @@ var import_node_path6 = require("node:path");
|
|
|
367
710
|
var import_node_url2 = require("node:url");
|
|
368
711
|
|
|
369
712
|
// ../face/src/face.ts
|
|
370
|
-
var
|
|
713
|
+
var import_node_fs6 = require("node:fs");
|
|
371
714
|
|
|
372
715
|
// ../face/src/products.ts
|
|
373
716
|
var PRODUCTS = Object.freeze({
|
|
374
717
|
"mm-strategy": Object.freeze({
|
|
375
718
|
name: "MM Strategy",
|
|
376
|
-
installWarm: "Welcome. Let's
|
|
719
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
377
720
|
accent: "38;2;249;115;22",
|
|
378
|
-
warm: "Welcome.
|
|
721
|
+
warm: "Welcome back. Checking for updates...",
|
|
379
722
|
doctor: "mm-strategy doctor"
|
|
380
723
|
}),
|
|
381
724
|
"mmi-hub": Object.freeze({
|
|
382
725
|
name: "mmi-hub",
|
|
383
|
-
installWarm: "Welcome.
|
|
726
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
384
727
|
accent: "38;2;125;211;252",
|
|
385
|
-
warm: "Welcome back. Checking
|
|
728
|
+
warm: "Welcome back. Checking for updates...",
|
|
386
729
|
doctor: "mmi doctor"
|
|
387
730
|
}),
|
|
388
731
|
"jerv-hub": Object.freeze({
|
|
389
732
|
name: "jerv-hub",
|
|
390
|
-
installWarm: "Welcome.
|
|
733
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
391
734
|
accent: "38;2;248;113;113",
|
|
392
|
-
warm: "Welcome back. Checking
|
|
735
|
+
warm: "Welcome back. Checking for updates...",
|
|
393
736
|
doctor: "jerv doctor"
|
|
394
737
|
}),
|
|
395
738
|
jervcode: Object.freeze({
|
|
396
739
|
name: "JervCode",
|
|
397
|
-
installWarm: "Welcome.
|
|
740
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
398
741
|
accent: "38;2;192;132;252",
|
|
399
|
-
warm: "Welcome back.
|
|
742
|
+
warm: "Welcome back. Checking for updates...",
|
|
400
743
|
doctor: "jervcode doctor"
|
|
401
744
|
})
|
|
402
745
|
});
|
|
@@ -410,6 +753,7 @@ function identityFor(product) {
|
|
|
410
753
|
|
|
411
754
|
// ../face/src/face.ts
|
|
412
755
|
var GLYPH = Object.freeze({
|
|
756
|
+
clock: "\u23F0",
|
|
413
757
|
diamond: "\u25C6",
|
|
414
758
|
hollow: "\u25C7",
|
|
415
759
|
bar: "\u2502",
|
|
@@ -481,12 +825,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
481
825
|
const continuesFace = continuedPhases.size > 0;
|
|
482
826
|
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
483
827
|
const progressFd = readProgressFd(env);
|
|
828
|
+
const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
|
|
484
829
|
const emitMilestone = (title, measure, kind) => {
|
|
485
|
-
if (progressFd === null) return false;
|
|
830
|
+
if (progressFd === null && !progressFile) return false;
|
|
486
831
|
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
487
832
|
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
833
|
+
if (kind === "running" && typeof measure === "string") record.measure = measure;
|
|
488
834
|
try {
|
|
489
|
-
(0,
|
|
835
|
+
if (progressFile) (0, import_node_fs6.appendFileSync)(progressFile, `${JSON.stringify(record)}
|
|
836
|
+
`);
|
|
837
|
+
else (0, import_node_fs6.writeSync)(progressFd, `${JSON.stringify(record)}
|
|
490
838
|
`);
|
|
491
839
|
return true;
|
|
492
840
|
} catch {
|
|
@@ -505,7 +853,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
505
853
|
};
|
|
506
854
|
const step = (title, measure = null, kind = "ok") => {
|
|
507
855
|
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);
|
|
856
|
+
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
857
|
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
510
858
|
const time = measured;
|
|
511
859
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
@@ -515,7 +863,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
515
863
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
516
864
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
517
865
|
};
|
|
518
|
-
const relay = (text) => String(text).
|
|
866
|
+
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
867
|
const receipt = (lines, { ready = true } = {}) => {
|
|
520
868
|
if (ready && nested) return [];
|
|
521
869
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
@@ -546,16 +894,35 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
546
894
|
};
|
|
547
895
|
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
548
896
|
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 };
|
|
897
|
+
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
898
|
}
|
|
551
899
|
|
|
552
900
|
// ../face/src/shell.ts
|
|
553
901
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
554
902
|
|
|
555
903
|
// ../face/src/spinner.ts
|
|
556
|
-
var
|
|
904
|
+
var import_node_fs7 = require("node:fs");
|
|
557
905
|
var import_node_worker_threads = require("node:worker_threads");
|
|
558
|
-
|
|
906
|
+
|
|
907
|
+
// ../face/src/encoding.ts
|
|
908
|
+
var UNICODE_SPINNER_FRAMES = Object.freeze(["\u25D2", "\u25D0", "\u25D3", "\u25D1"]);
|
|
909
|
+
var ASCII_SPINNER_FRAMES = Object.freeze(["-", "\\", "|", "/"]);
|
|
910
|
+
function consoleRendersUnicode(env = process.env, platform = process.platform) {
|
|
911
|
+
if (env.MM_FACE_ASCII === "1" || env.NO_UNICODE === "1") return false;
|
|
912
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
|
|
913
|
+
if (locale === "C" || locale === "POSIX" || /^C\./u.test(locale) || /^POSIX\./u.test(locale)) return false;
|
|
914
|
+
if (/cp437|ibm437|\b437\b/iu.test(locale)) return false;
|
|
915
|
+
if (/utf-?8/iu.test(locale)) return true;
|
|
916
|
+
if (platform === "win32") {
|
|
917
|
+
return Boolean(env.WT_SESSION) || env.TERM_PROGRAM === "vscode" || env.CHCP === "65001" || env.ConEmuANSI === "ON";
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
function spinnerFramesFor(env = process.env, platform = process.platform) {
|
|
922
|
+
return consoleRendersUnicode(env, platform) ? UNICODE_SPINNER_FRAMES : ASCII_SPINNER_FRAMES;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ../face/src/spinner.ts
|
|
559
926
|
var WORKER_SOURCE = `
|
|
560
927
|
const { parentPort, workerData } = require('node:worker_threads');
|
|
561
928
|
const { writeSync, appendFileSync } = require('node:fs');
|
|
@@ -565,7 +932,7 @@ function draw() {
|
|
|
565
932
|
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
566
933
|
try {
|
|
567
934
|
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
568
|
-
const text = frames[frame++ % frames.length];
|
|
935
|
+
const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
|
|
569
936
|
writeSync(2, text);
|
|
570
937
|
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
571
938
|
} finally {
|
|
@@ -577,31 +944,35 @@ parentPort.on('message', (next) => {
|
|
|
577
944
|
if (Atomics.load(control, 2)) return;
|
|
578
945
|
frames = next.frames;
|
|
579
946
|
frame = next.frame;
|
|
580
|
-
Atomics.store(control, 0, 0);
|
|
581
947
|
});
|
|
582
948
|
setInterval(draw, workerData.intervalMs);
|
|
583
949
|
`;
|
|
584
|
-
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
|
|
950
|
+
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath, env = process.env, platform = process.platform }) {
|
|
585
951
|
animate = animate && !face.nested && !face.emitsProgress;
|
|
952
|
+
const glyphs = spinnerFramesFor(env, platform);
|
|
953
|
+
const ascii = glyphs[0] === ASCII_SPINNER_FRAMES[0];
|
|
586
954
|
let timer = null;
|
|
587
955
|
let worker = null;
|
|
588
956
|
let control = null;
|
|
589
957
|
let frames = [];
|
|
590
958
|
let frame = 0;
|
|
959
|
+
let started = 0;
|
|
960
|
+
let suspended = false;
|
|
591
961
|
const write = (text) => {
|
|
592
962
|
if (stream) stream.write(text);
|
|
593
963
|
else {
|
|
594
|
-
(0,
|
|
595
|
-
if (transcriptPath) (0,
|
|
964
|
+
(0, import_node_fs7.writeSync)(2, text);
|
|
965
|
+
if (transcriptPath) (0, import_node_fs7.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
596
966
|
`);
|
|
597
967
|
}
|
|
598
968
|
};
|
|
599
969
|
const render = (text, measure) => {
|
|
600
|
-
const line = face.step(text, measure, "note").split("\n")[0];
|
|
601
|
-
|
|
970
|
+
const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
|
|
971
|
+
const prepared = line && ascii ? line.replaceAll(GLYPH.bar, "|") : line;
|
|
972
|
+
frames = prepared ? glyphs.map((glyph) => `\r\x1B[2K${prepared.replace(GLYPH.dot, glyph)}`) : [];
|
|
602
973
|
};
|
|
603
974
|
const draw = () => {
|
|
604
|
-
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
975
|
+
if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
|
|
605
976
|
};
|
|
606
977
|
const pause = () => {
|
|
607
978
|
if (!control) return;
|
|
@@ -621,6 +992,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
621
992
|
start(text, measure = null) {
|
|
622
993
|
if (!animate) return;
|
|
623
994
|
halt();
|
|
995
|
+
suspended = false;
|
|
996
|
+
started = Date.now();
|
|
624
997
|
render(text, measure);
|
|
625
998
|
frame = 0;
|
|
626
999
|
draw();
|
|
@@ -634,7 +1007,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
634
1007
|
frames,
|
|
635
1008
|
frame,
|
|
636
1009
|
intervalMs,
|
|
637
|
-
transcriptPath
|
|
1010
|
+
transcriptPath,
|
|
1011
|
+
started
|
|
638
1012
|
} });
|
|
639
1013
|
const active = worker;
|
|
640
1014
|
worker.on("error", () => {
|
|
@@ -652,23 +1026,35 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
652
1026
|
render(text, measure);
|
|
653
1027
|
draw();
|
|
654
1028
|
worker?.postMessage({ frames, frame });
|
|
1029
|
+
if (control && !suspended) Atomics.store(control, 0, 0);
|
|
1030
|
+
},
|
|
1031
|
+
pause() {
|
|
1032
|
+
if (suspended) return;
|
|
1033
|
+
suspended = true;
|
|
1034
|
+
pause();
|
|
1035
|
+
if (animate && frames.length) write("\r\x1B[2K");
|
|
1036
|
+
},
|
|
1037
|
+
resume() {
|
|
1038
|
+
suspended = false;
|
|
1039
|
+
if (control) Atomics.store(control, 0, 0);
|
|
1040
|
+
if (timer || worker) draw();
|
|
655
1041
|
},
|
|
656
1042
|
stop() {
|
|
657
1043
|
halt();
|
|
1044
|
+
frames = [];
|
|
658
1045
|
if (animate) write("\r\x1B[2K");
|
|
659
1046
|
}
|
|
660
1047
|
};
|
|
661
1048
|
}
|
|
662
|
-
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
663
1049
|
|
|
664
1050
|
// ../face/src/conformance.ts
|
|
665
1051
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
666
1052
|
|
|
667
1053
|
// ../face/src/run.ts
|
|
668
|
-
var
|
|
1054
|
+
var import_node_fs9 = require("node:fs");
|
|
669
1055
|
|
|
670
1056
|
// ../face/src/outcome.ts
|
|
671
|
-
var
|
|
1057
|
+
var import_node_fs8 = require("node:fs");
|
|
672
1058
|
var counts = ["total", "updated", "failed"];
|
|
673
1059
|
var strings = ["version", "retry", "detail", "logPath"];
|
|
674
1060
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
@@ -690,12 +1076,12 @@ function validateInstallerOutcome(value) {
|
|
|
690
1076
|
return { ...facts };
|
|
691
1077
|
}
|
|
692
1078
|
function writeInstallerOutcome(path, value) {
|
|
693
|
-
(0,
|
|
1079
|
+
(0, import_node_fs8.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
694
1080
|
}
|
|
695
1081
|
function readInstallerOutcome(path) {
|
|
696
1082
|
let text;
|
|
697
1083
|
try {
|
|
698
|
-
text = (0,
|
|
1084
|
+
text = (0, import_node_fs8.readFileSync)(path, "utf8");
|
|
699
1085
|
} catch (error) {
|
|
700
1086
|
if (error.code === "ENOENT") return void 0;
|
|
701
1087
|
throw error;
|
|
@@ -758,10 +1144,19 @@ var PHASES = {
|
|
|
758
1144
|
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
759
1145
|
verify: ["Verifying the payload", "Verified the payload"],
|
|
760
1146
|
install: ["Installing the product", "Installed the product"],
|
|
1147
|
+
configure: ["Configuring the product", "Configured the product"],
|
|
761
1148
|
activate: ["Activating surfaces", "Activated surfaces"],
|
|
762
1149
|
doctor: ["Checking health", "Checked health"],
|
|
763
1150
|
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
764
1151
|
};
|
|
1152
|
+
var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
|
|
1153
|
+
"preflight",
|
|
1154
|
+
"resolve",
|
|
1155
|
+
"verify-release",
|
|
1156
|
+
"download",
|
|
1157
|
+
"sign-in",
|
|
1158
|
+
"check"
|
|
1159
|
+
]);
|
|
765
1160
|
function createInstallerRun(value, options = {}) {
|
|
766
1161
|
const declaration = validateInstallerProduct(value);
|
|
767
1162
|
const env = options.env ?? process.env;
|
|
@@ -787,7 +1182,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
787
1182
|
if (!text) return;
|
|
788
1183
|
write(text, channel);
|
|
789
1184
|
if (env.MM_FACE_TRANSCRIPT) {
|
|
790
|
-
(0,
|
|
1185
|
+
(0, import_node_fs9.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
791
1186
|
`, "utf8");
|
|
792
1187
|
}
|
|
793
1188
|
};
|
|
@@ -797,13 +1192,26 @@ function createInstallerRun(value, options = {}) {
|
|
|
797
1192
|
};
|
|
798
1193
|
const spinner = createSpinner(face, {
|
|
799
1194
|
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
1195
|
+
env,
|
|
800
1196
|
...options.write ? { stream: { write: (text) => {
|
|
801
1197
|
emit(String(text), "spinner");
|
|
802
1198
|
return true;
|
|
803
1199
|
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
804
1200
|
});
|
|
1201
|
+
let activeTitle;
|
|
1202
|
+
let chunkOpen = false;
|
|
1203
|
+
let chunkChannel = "stdout";
|
|
1204
|
+
let chunkColumn = 0;
|
|
1205
|
+
const trailingCR = /* @__PURE__ */ new Set();
|
|
1206
|
+
let deferredActivity;
|
|
1207
|
+
const closeChunk = () => {
|
|
1208
|
+
if (chunkOpen) emit("\n", chunkChannel);
|
|
1209
|
+
chunkOpen = false;
|
|
1210
|
+
chunkColumn = 0;
|
|
1211
|
+
};
|
|
805
1212
|
let started = false;
|
|
806
1213
|
let finished = false;
|
|
1214
|
+
let operation = options.operation;
|
|
807
1215
|
const start = () => {
|
|
808
1216
|
if (started || finished) return;
|
|
809
1217
|
started = true;
|
|
@@ -811,11 +1219,14 @@ function createInstallerRun(value, options = {}) {
|
|
|
811
1219
|
const welcome = face.welcome();
|
|
812
1220
|
if (tty) lines(welcome);
|
|
813
1221
|
else if (welcome.length) {
|
|
814
|
-
const warm =
|
|
1222
|
+
const warm = operation === "install" ? face.identity.installWarm : face.identity.warm;
|
|
815
1223
|
lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
|
|
816
1224
|
}
|
|
817
1225
|
};
|
|
818
1226
|
const durable = (title, measure, kind) => {
|
|
1227
|
+
closeChunk();
|
|
1228
|
+
activeTitle = void 0;
|
|
1229
|
+
deferredActivity = void 0;
|
|
819
1230
|
spinner.stop();
|
|
820
1231
|
const rendered = face.step(title, measure, kind);
|
|
821
1232
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
@@ -824,15 +1235,31 @@ function createInstallerRun(value, options = {}) {
|
|
|
824
1235
|
get errors() {
|
|
825
1236
|
return [...errors];
|
|
826
1237
|
},
|
|
1238
|
+
get operation() {
|
|
1239
|
+
return operation;
|
|
1240
|
+
},
|
|
1241
|
+
set operation(value2) {
|
|
1242
|
+
operation = value2;
|
|
1243
|
+
},
|
|
827
1244
|
start,
|
|
828
1245
|
phase(id, facts = {}) {
|
|
829
1246
|
if (finished) throw new Error("installer run already finished");
|
|
830
1247
|
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
831
1248
|
start();
|
|
832
1249
|
const state = facts.state ?? "ok";
|
|
833
|
-
const
|
|
1250
|
+
const fold = operation === "update" && UPDATE_CHECK_PHASES.has(id) && state !== "fail";
|
|
1251
|
+
const title = PHASES[fold ? "check" : id][state === "ok" ? 1 : 0];
|
|
834
1252
|
if (state === "running") {
|
|
835
|
-
|
|
1253
|
+
if (face.progress(title, facts.measure ?? null)) return;
|
|
1254
|
+
if (activeTitle === title) spinner.say(title, facts.measure ?? null);
|
|
1255
|
+
else {
|
|
1256
|
+
activeTitle = title;
|
|
1257
|
+
spinner.start(title, facts.measure ?? null);
|
|
1258
|
+
}
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (fold && id !== "check") {
|
|
1262
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
836
1263
|
return;
|
|
837
1264
|
}
|
|
838
1265
|
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
@@ -852,8 +1279,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
852
1279
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
853
1280
|
if (facts.detail) run2.relay(facts.detail);
|
|
854
1281
|
},
|
|
855
|
-
milestone({ step, state, ms }) {
|
|
1282
|
+
milestone({ step, state, ms, measure }) {
|
|
856
1283
|
start();
|
|
1284
|
+
if (state === "running") {
|
|
1285
|
+
if (chunkOpen) {
|
|
1286
|
+
deferredActivity = { title: step, measure: measure ?? null };
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (activeTitle === step) spinner.say(step, measure ?? null);
|
|
1290
|
+
else {
|
|
1291
|
+
activeTitle = step;
|
|
1292
|
+
spinner.start(step, measure ?? null);
|
|
1293
|
+
}
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
857
1296
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
858
1297
|
},
|
|
859
1298
|
signIn({ url, code }) {
|
|
@@ -867,7 +1306,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
867
1306
|
},
|
|
868
1307
|
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
869
1308
|
relay(text, channel = "stdout", record = true) {
|
|
870
|
-
|
|
1309
|
+
closeChunk();
|
|
1310
|
+
spinner.pause();
|
|
871
1311
|
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
872
1312
|
for (const row of rendered.split("\n")) if (row) {
|
|
873
1313
|
emit(`${row}
|
|
@@ -875,6 +1315,37 @@ function createInstallerRun(value, options = {}) {
|
|
|
875
1315
|
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
876
1316
|
`);
|
|
877
1317
|
}
|
|
1318
|
+
spinner.resume();
|
|
1319
|
+
},
|
|
1320
|
+
relayChunk(text, channel = "stdout") {
|
|
1321
|
+
spinner.pause();
|
|
1322
|
+
if (chunkOpen && chunkChannel !== channel) closeChunk();
|
|
1323
|
+
chunkChannel = channel;
|
|
1324
|
+
if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
|
|
1325
|
+
if (text.endsWith("\r")) trailingCR.add(channel);
|
|
1326
|
+
const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
|
|
1327
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1328
|
+
const characters = [...stripColor(parts[i])];
|
|
1329
|
+
while (characters.length) {
|
|
1330
|
+
if (tty && chunkColumn >= face.width - 6) closeChunk();
|
|
1331
|
+
const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
|
|
1332
|
+
emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
|
|
1333
|
+
chunkColumn += [...part].length;
|
|
1334
|
+
chunkOpen = true;
|
|
1335
|
+
}
|
|
1336
|
+
if (i < parts.length - 1) {
|
|
1337
|
+
emit("\n", channel);
|
|
1338
|
+
chunkOpen = false;
|
|
1339
|
+
chunkColumn = 0;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (!chunkOpen) {
|
|
1343
|
+
if (deferredActivity) {
|
|
1344
|
+
activeTitle = deferredActivity.title;
|
|
1345
|
+
spinner.start(deferredActivity.title, deferredActivity.measure);
|
|
1346
|
+
deferredActivity = void 0;
|
|
1347
|
+
} else spinner.resume();
|
|
1348
|
+
}
|
|
878
1349
|
},
|
|
879
1350
|
cancel() {
|
|
880
1351
|
if (finished) return;
|
|
@@ -883,6 +1354,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
883
1354
|
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
884
1355
|
);
|
|
885
1356
|
start();
|
|
1357
|
+
closeChunk();
|
|
1358
|
+
activeTitle = void 0;
|
|
886
1359
|
spinner.stop();
|
|
887
1360
|
finished = true;
|
|
888
1361
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
@@ -895,6 +1368,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
895
1368
|
validateInstallerOutcome(facts);
|
|
896
1369
|
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
897
1370
|
start();
|
|
1371
|
+
closeChunk();
|
|
1372
|
+
activeTitle = void 0;
|
|
898
1373
|
spinner.stop();
|
|
899
1374
|
finished = true;
|
|
900
1375
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
@@ -915,6 +1390,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
915
1390
|
if (tty) lines([face.signOff()]);
|
|
916
1391
|
},
|
|
917
1392
|
stop() {
|
|
1393
|
+
closeChunk();
|
|
1394
|
+
activeTitle = void 0;
|
|
918
1395
|
spinner.stop();
|
|
919
1396
|
}
|
|
920
1397
|
};
|
|
@@ -923,9 +1400,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
923
1400
|
|
|
924
1401
|
// src/autoupdate.ts
|
|
925
1402
|
var import_node_child_process2 = require("node:child_process");
|
|
926
|
-
var
|
|
927
|
-
var
|
|
928
|
-
var
|
|
1403
|
+
var import_node_fs10 = require("node:fs");
|
|
1404
|
+
var import_node_os3 = require("node:os");
|
|
1405
|
+
var import_node_path5 = require("node:path");
|
|
929
1406
|
function schedulePlatform(override) {
|
|
930
1407
|
const platform = override ?? process.platform;
|
|
931
1408
|
if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
|
|
@@ -942,8 +1419,8 @@ function defaultExec(command2, args) {
|
|
|
942
1419
|
}
|
|
943
1420
|
function homeOf(options) {
|
|
944
1421
|
if (options.homeDir) return options.homeDir;
|
|
945
|
-
if (process.platform === "win32") return process.env.USERPROFILE ?? (0,
|
|
946
|
-
return process.env.HOME ?? (0,
|
|
1422
|
+
if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os3.tmpdir)();
|
|
1423
|
+
return process.env.HOME ?? (0, import_node_os3.tmpdir)();
|
|
947
1424
|
}
|
|
948
1425
|
function scheduleName(config) {
|
|
949
1426
|
return `${config.binName} autoupdate`;
|
|
@@ -969,10 +1446,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
969
1446
|
}
|
|
970
1447
|
if (platform === "darwin") {
|
|
971
1448
|
const label2 = scheduleLabel(config);
|
|
972
|
-
const dir2 = (0,
|
|
973
|
-
(0,
|
|
974
|
-
const plist = (0,
|
|
975
|
-
(0,
|
|
1449
|
+
const dir2 = (0, import_node_path5.join)(home, "Library", "LaunchAgents");
|
|
1450
|
+
(0, import_node_fs10.mkdirSync)(dir2, { recursive: true });
|
|
1451
|
+
const plist = (0, import_node_path5.join)(dir2, `${label2}.plist`);
|
|
1452
|
+
(0, import_node_fs10.writeFileSync)(plist, darwinPlist(label2, command2));
|
|
976
1453
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
977
1454
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
978
1455
|
if (result2.code !== 0) {
|
|
@@ -981,10 +1458,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
981
1458
|
return;
|
|
982
1459
|
}
|
|
983
1460
|
const label = scheduleLabel(config);
|
|
984
|
-
const dir = (0,
|
|
985
|
-
(0,
|
|
986
|
-
(0,
|
|
987
|
-
(0,
|
|
1461
|
+
const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
|
|
1462
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1463
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.service`), linuxService(command2));
|
|
1464
|
+
(0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
988
1465
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
989
1466
|
if (reload.code !== 0) {
|
|
990
1467
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -1009,14 +1486,14 @@ function disableSchedule(config, options = {}) {
|
|
|
1009
1486
|
if (platform === "darwin") {
|
|
1010
1487
|
const label2 = scheduleLabel(config);
|
|
1011
1488
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
1012
|
-
(0,
|
|
1489
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
1013
1490
|
return;
|
|
1014
1491
|
}
|
|
1015
1492
|
const label = scheduleLabel(config);
|
|
1016
|
-
const dir = (0,
|
|
1493
|
+
const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
|
|
1017
1494
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
1018
|
-
(0,
|
|
1019
|
-
(0,
|
|
1495
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.service`), { force: true });
|
|
1496
|
+
(0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.timer`), { force: true });
|
|
1020
1497
|
}
|
|
1021
1498
|
function querySchedule(config, options = {}) {
|
|
1022
1499
|
const platform = schedulePlatform(options.platform);
|
|
@@ -1034,12 +1511,12 @@ function querySchedule(config, options = {}) {
|
|
|
1034
1511
|
return state2;
|
|
1035
1512
|
}
|
|
1036
1513
|
if (platform === "darwin") {
|
|
1037
|
-
const plist = (0,
|
|
1038
|
-
if (!(0,
|
|
1514
|
+
const plist = (0, import_node_path5.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
1515
|
+
if (!(0, import_node_fs10.existsSync)(plist)) return { supported: true, enabled: false };
|
|
1039
1516
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
1040
1517
|
}
|
|
1041
|
-
const timer = (0,
|
|
1042
|
-
if (!(0,
|
|
1518
|
+
const timer = (0, import_node_path5.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
1519
|
+
if (!(0, import_node_fs10.existsSync)(timer)) return { supported: true, enabled: false };
|
|
1043
1520
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
1044
1521
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
1045
1522
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -1096,87 +1573,6 @@ function firstLine(text) {
|
|
|
1096
1573
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1097
1574
|
}
|
|
1098
1575
|
|
|
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
1576
|
// src/login-github.ts
|
|
1181
1577
|
var import_node_child_process3 = require("node:child_process");
|
|
1182
1578
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1299,7 +1695,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
1299
1695
|
}
|
|
1300
1696
|
|
|
1301
1697
|
// src/login-google.ts
|
|
1302
|
-
var
|
|
1698
|
+
var import_node_crypto5 = require("node:crypto");
|
|
1303
1699
|
var import_node_http = require("node:http");
|
|
1304
1700
|
var b64url = (bytes) => bytes.toString("base64url");
|
|
1305
1701
|
async function loginGoogle(options) {
|
|
@@ -1321,9 +1717,9 @@ async function loginGoogle(options) {
|
|
|
1321
1717
|
if (typeof clientId !== "string" || !clientId) {
|
|
1322
1718
|
throw new Error("the sign-in server returned a malformed registration");
|
|
1323
1719
|
}
|
|
1324
|
-
const verifier = b64url((0,
|
|
1325
|
-
const challenge = b64url((0,
|
|
1326
|
-
const state = b64url((0,
|
|
1720
|
+
const verifier = b64url((0, import_node_crypto5.randomBytes)(32));
|
|
1721
|
+
const challenge = b64url((0, import_node_crypto5.createHash)("sha256").update(verifier).digest());
|
|
1722
|
+
const state = b64url((0, import_node_crypto5.randomBytes)(16));
|
|
1327
1723
|
const authorize = new URL(`${server}/oauth/authorize`);
|
|
1328
1724
|
authorize.search = new URLSearchParams({
|
|
1329
1725
|
response_type: "code",
|
|
@@ -1411,175 +1807,8 @@ function page(title, body) {
|
|
|
1411
1807
|
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
1808
|
}
|
|
1413
1809
|
|
|
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
1810
|
// src/index.ts
|
|
1582
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1811
|
+
var LAUNCHER_VERSION = true ? "0.1.15" : readVersionFromPackage();
|
|
1583
1812
|
function defaultPrint(message) {
|
|
1584
1813
|
process.stdout.write(`${message}
|
|
1585
1814
|
`);
|
|
@@ -1815,12 +2044,13 @@ function parseProgress(raw) {
|
|
|
1815
2044
|
const message = JSON.parse(line);
|
|
1816
2045
|
if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
|
|
1817
2046
|
const state = message.state ?? "ok";
|
|
1818
|
-
if (state !== "ok" && state !== "fail" && state !== "note") continue;
|
|
2047
|
+
if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
|
|
1819
2048
|
if (message.ms !== void 0 && typeof message.ms !== "number") continue;
|
|
1820
2049
|
progress.push({
|
|
1821
2050
|
step: message.step,
|
|
1822
2051
|
state,
|
|
1823
|
-
...typeof message.ms === "number" ? { ms: message.ms } : {}
|
|
2052
|
+
...typeof message.ms === "number" && Number.isFinite(message.ms) && message.ms >= 0 ? { ms: message.ms } : {},
|
|
2053
|
+
...typeof message.measure === "string" ? { measure: message.measure } : {}
|
|
1824
2054
|
});
|
|
1825
2055
|
} catch {
|
|
1826
2056
|
}
|
|
@@ -1879,8 +2109,27 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1879
2109
|
let version = readState(dir)?.version ?? "unknown";
|
|
1880
2110
|
installer.start();
|
|
1881
2111
|
try {
|
|
2112
|
+
const current = readState(dir);
|
|
2113
|
+
const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
1882
2114
|
let started = Date.now();
|
|
2115
|
+
installer.phase("resolve", { state: "running" });
|
|
2116
|
+
const pointer = await fetchReleaseVersion(config, fetchImpl);
|
|
2117
|
+
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2118
|
+
version = pointer;
|
|
2119
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2120
|
+
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2121
|
+
return await finishLastMile(
|
|
2122
|
+
config,
|
|
2123
|
+
dir,
|
|
2124
|
+
options,
|
|
2125
|
+
installer,
|
|
2126
|
+
version,
|
|
2127
|
+
true,
|
|
2128
|
+
current.acquired ? acquisitionRepairCommand(dir, version) : void 0
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
1883
2131
|
installer.phase("sign-in", { state: "running" });
|
|
2132
|
+
started = Date.now();
|
|
1884
2133
|
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1885
2134
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1886
2135
|
installer.phase("resolve", { state: "running" });
|
|
@@ -1888,8 +2137,7 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1888
2137
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1889
2138
|
version = manifest.version;
|
|
1890
2139
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1891
|
-
const
|
|
1892
|
-
const unchanged = update && current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
2140
|
+
const unchanged = current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
|
|
1893
2141
|
if (unchanged && current.acquired) {
|
|
1894
2142
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
1895
2143
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -1897,11 +2145,17 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1897
2145
|
if (!unchanged) {
|
|
1898
2146
|
installer.phase("download", { state: "running" });
|
|
1899
2147
|
started = Date.now();
|
|
1900
|
-
const
|
|
2148
|
+
const cached = reusableCandidate(dir, manifest);
|
|
2149
|
+
const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
|
|
1901
2150
|
const candidateDir = (0, import_node_path6.join)(dir, "candidates", candidate);
|
|
1902
|
-
|
|
2151
|
+
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2152
|
+
state: "running",
|
|
2153
|
+
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
|
|
2154
|
+
});
|
|
2155
|
+
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
1903
2156
|
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
2157
|
if (acquisition) {
|
|
2158
|
+
rememberCandidate(dir, candidate, manifest);
|
|
1905
2159
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1906
2160
|
installer.phase("activate", { state: "running" });
|
|
1907
2161
|
let outcome;
|
|
@@ -1909,7 +2163,9 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1909
2163
|
await installAcquisition(dir, candidate, version, acquisition, {
|
|
1910
2164
|
fetchImpl,
|
|
1911
2165
|
env,
|
|
2166
|
+
onProgress,
|
|
1912
2167
|
run: async (command2, cwd, childEnv) => {
|
|
2168
|
+
installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
|
|
1913
2169
|
const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
|
|
1914
2170
|
if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
|
|
1915
2171
|
return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
|
|
@@ -2027,17 +2283,22 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
2027
2283
|
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
2028
2284
|
}
|
|
2029
2285
|
async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
2286
|
+
installer.start();
|
|
2030
2287
|
const [command2, ...args] = entry2;
|
|
2031
2288
|
const shell = needsShell(command2);
|
|
2032
2289
|
const progress = !(process.platform === "win32" && shell);
|
|
2033
2290
|
const outcomeDir = (0, import_node_fs11.mkdtempSync)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
|
|
2034
2291
|
const outcomeFile = (0, import_node_path6.join)(outcomeDir, "outcome.json");
|
|
2292
|
+
const progressFile = (0, import_node_path6.join)(outcomeDir, "progress.jsonl");
|
|
2035
2293
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2036
2294
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2295
|
+
delete childEnv.MM_PROGRESS_FILE;
|
|
2037
2296
|
delete childEnv.MM_PROGRESS_FD;
|
|
2038
2297
|
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
2039
2298
|
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
2299
|
+
else Object.assign(childEnv, { MM_PROGRESS_FILE: progressFile, MM_PROGRESS_PROTOCOL: "1" });
|
|
2040
2300
|
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
2301
|
+
let progressError;
|
|
2041
2302
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2042
2303
|
try {
|
|
2043
2304
|
const result = await new Promise((resolve3) => {
|
|
@@ -2058,24 +2319,44 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2058
2319
|
}
|
|
2059
2320
|
partial = held ? safe.slice(-held) : "";
|
|
2060
2321
|
const visible = held ? safe.slice(0, -held) : safe;
|
|
2061
|
-
if (visible) installer.
|
|
2322
|
+
if (visible) installer.relayChunk(visible, channel2);
|
|
2062
2323
|
}).on("end", () => {
|
|
2063
|
-
if (partial) installer.
|
|
2324
|
+
if (partial) installer.relayChunk("[redacted]", channel2);
|
|
2064
2325
|
});
|
|
2065
2326
|
}
|
|
2066
2327
|
let pending = "";
|
|
2328
|
+
let progressOffset = 0;
|
|
2329
|
+
const progressDecoder = new import_node_string_decoder.StringDecoder("utf8");
|
|
2330
|
+
const receive = (text) => {
|
|
2331
|
+
pending += text;
|
|
2332
|
+
const end = pending.lastIndexOf("\n");
|
|
2333
|
+
if (end >= 0) {
|
|
2334
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step), ...record.measure ? { measure: redact(record.measure) } : {} });
|
|
2335
|
+
pending = pending.slice(end + 1);
|
|
2336
|
+
}
|
|
2337
|
+
if (pending.length > 65536) pending = "";
|
|
2338
|
+
};
|
|
2339
|
+
const pollProgress = () => {
|
|
2340
|
+
if (progressError) return;
|
|
2341
|
+
try {
|
|
2342
|
+
if (!(0, import_node_fs11.existsSync)(progressFile)) return;
|
|
2343
|
+
const bytes = (0, import_node_fs11.readFileSync)(progressFile);
|
|
2344
|
+
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2345
|
+
progressOffset = bytes.length;
|
|
2346
|
+
} catch {
|
|
2347
|
+
progressError = "installer progress: could not read child progress";
|
|
2348
|
+
if (timer) clearInterval(timer);
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
const timer = progress ? void 0 : setInterval(pollProgress, 90);
|
|
2352
|
+
child.once("close", () => {
|
|
2353
|
+
if (timer) clearInterval(timer);
|
|
2354
|
+
pollProgress();
|
|
2355
|
+
});
|
|
2067
2356
|
const channel = child.stdio[3];
|
|
2068
2357
|
if (channel && "setEncoding" in channel) {
|
|
2069
2358
|
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
|
-
});
|
|
2359
|
+
channel.on("data", receive);
|
|
2079
2360
|
}
|
|
2080
2361
|
child.on("error", (error) => resolve3({ ok: false, error: error.message }));
|
|
2081
2362
|
child.on("close", (code) => resolve3({
|
|
@@ -2084,6 +2365,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2084
2365
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
2085
2366
|
}));
|
|
2086
2367
|
});
|
|
2368
|
+
if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
|
|
2087
2369
|
try {
|
|
2088
2370
|
const outcome = readInstallerOutcome(outcomeFile);
|
|
2089
2371
|
return { ...result, ...outcome ? { outcome } : {} };
|
|
@@ -2091,6 +2373,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2091
2373
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2092
2374
|
}
|
|
2093
2375
|
} finally {
|
|
2376
|
+
if ((0, import_node_fs11.existsSync)(progressFile)) (0, import_node_fs11.unlinkSync)(progressFile);
|
|
2094
2377
|
if ((0, import_node_fs11.existsSync)(outcomeFile)) (0, import_node_fs11.unlinkSync)(outcomeFile);
|
|
2095
2378
|
(0, import_node_fs11.rmdirSync)(outcomeDir);
|
|
2096
2379
|
}
|