@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.
@@ -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 import_node_crypto3 = require("node:crypto");
39
- var import_node_fs3 = require("node:fs");
40
- var import_node_path3 = require("node:path");
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 = Buffer.from(await response.arrayBuffer());
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
- await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl);
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, import_node_fs3.readFileSync)((0, import_node_path3.join)(payload, "payload.json"), "utf8"));
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, import_node_path3.dirname)(node)}${platform === "win32" ? ";" : ":"}${current}`;
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, import_node_fs3.mkdirSync)(dir, { recursive: true });
227
- const path = (0, import_node_path3.join)(dir, "installation.lock");
228
- const owner = `${process.pid}:${(0, import_node_crypto3.randomUUID)()}`;
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, import_node_fs3.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
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, import_node_fs3.writeFileSync)(recovery, owner, { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.readFileSync)(path, "utf8");
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, import_node_fs3.unlinkSync)(path);
250
- (0, import_node_fs3.writeFileSync)(path, owner, { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.unlinkSync)(recovery);
549
+ (0, import_node_fs5.unlinkSync)(recovery);
253
550
  }
254
551
  }
255
552
  let released = false;
256
553
  return () => {
257
- if (!released && (0, import_node_fs3.readFileSync)(path, "utf8") === owner) (0, import_node_fs3.unlinkSync)(path);
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, import_node_path3.join)(dir, "acquisition-pending.json");
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, import_node_path3.join)(dir, "candidates", candidate);
563
+ return (0, import_node_path4.join)(dir, "candidates", candidate);
267
564
  }
268
565
  function packageRoot(prefix, acquisition) {
269
- return (0, import_node_path3.join)(prefix, "node_modules", acquisition.package);
566
+ return (0, import_node_path4.join)(prefix, "node_modules", acquisition.package);
270
567
  }
271
568
  function entry(root, path) {
272
- const actual = (0, import_node_fs3.realpathSync)((0, import_node_path3.join)(root, path));
273
- const rel = (0, import_node_path3.relative)((0, import_node_fs3.realpathSync)(root), actual);
274
- if (rel.startsWith("..") || (0, import_node_path3.isAbsolute)(rel)) throw new Error("acquired entry escapes package");
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, import_node_path3.join)(root, "payload"));
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, import_node_path3.join)(root, "prefix");
583
+ const prefix = (0, import_node_path4.join)(root, "prefix");
287
584
  const productRoot = packageRoot(prefix, acquisition);
288
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(productRoot, "package.json"), "utf8"));
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, import_node_fs3.realpathSync)(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
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, import_node_fs3.existsSync)(pendingPath(dir))) return;
294
- const pending = JSON.parse((0, import_node_fs3.readFileSync)(pendingPath(dir), "utf8"));
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, import_node_fs3.unlinkSync)(pendingPath(dir));
595
+ (0, import_node_fs5.unlinkSync)(pendingPath(dir));
299
596
  return;
300
597
  }
301
- const acquisition = readAcquisition((0, import_node_path3.join)(root, "payload"));
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, import_node_path3.join)(root, "prefix");
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, import_node_fs3.existsSync)(statePath(dir))) (0, import_node_fs3.unlinkSync)(statePath(dir));
308
- (0, import_node_fs3.unlinkSync)(pendingPath(dir));
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, import_node_path3.join)(root, "payload");
314
- const prefix = (0, import_node_path3.join)(root, "prefix");
315
- (0, import_node_fs3.mkdirSync)(prefix);
316
- const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl);
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, import_node_path3.join)(root, "public.npmrc");
323
- const globalrc = (0, import_node_path3.join)(root, "global.npmrc");
324
- (0, import_node_fs3.writeFileSync)(npmrc, "registry=https://registry.npmjs.org/\n");
325
- (0, import_node_fs3.writeFileSync)(globalrc, "");
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, import_node_path3.resolve)(payload, acquisition.archive)
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, import_node_fs3.readFileSync)((0, import_node_path3.join)(productRoot, "package.json"), "utf8"));
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, import_node_fs3.writeFileSync)(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.unlinkSync)(pendingPath(dir));
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 import_node_fs4 = require("node:fs");
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 set up MM Strategy \u2014 about a minute.",
679
+ installWarm: "Welcome. Let's get you ready.",
377
680
  accent: "38;2;249;115;22",
378
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
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. Setting up mmi-hub \u2014 about a minute.",
686
+ installWarm: "Welcome. Let's get you ready.",
384
687
  accent: "38;2;125;211;252",
385
- warm: "Welcome back. Checking your surfaces\u2026",
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. Setting up jerv-hub \u2014 about a minute.",
693
+ installWarm: "Welcome. Let's get you ready.",
391
694
  accent: "38;2;248;113;113",
392
- warm: "Welcome back. Checking your surfaces\u2026",
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. Setting up JervCode \u2014 about a minute.",
700
+ installWarm: "Welcome. Let's get you ready.",
398
701
  accent: "38;2;192;132;252",
399
- warm: "Welcome back. Keeping JervCode current\u2026",
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, import_node_fs4.writeSync)(progressFd, `${JSON.stringify(record)}
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).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
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 import_node_fs5 = require("node:fs");
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, import_node_fs5.writeSync)(2, text);
595
- if (transcriptPath) (0, import_node_fs5.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
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 import_node_fs7 = require("node:fs");
993
+ var import_node_fs9 = require("node:fs");
669
994
 
670
995
  // ../face/src/outcome.ts
671
- var import_node_fs6 = require("node:fs");
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, import_node_fs6.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
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, import_node_fs6.readFileSync)(path, "utf8");
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, import_node_fs7.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
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
- spinner.start(title, facts.measure ?? null);
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
- spinner.stop();
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 import_node_fs8 = require("node:fs");
927
- var import_node_os2 = require("node:os");
928
- var import_node_path4 = require("node:path");
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, import_node_os2.tmpdir)();
946
- return process.env.HOME ?? (0, import_node_os2.tmpdir)();
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, import_node_path4.join)(home, "Library", "LaunchAgents");
973
- (0, import_node_fs8.mkdirSync)(dir2, { recursive: true });
974
- const plist = (0, import_node_path4.join)(dir2, `${label2}.plist`);
975
- (0, import_node_fs8.writeFileSync)(plist, darwinPlist(label2, command2));
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, import_node_path4.join)(home, ".config", "systemd", "user");
985
- (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
986
- (0, import_node_fs8.writeFileSync)((0, import_node_path4.join)(dir, `${label}.service`), linuxService(command2));
987
- (0, import_node_fs8.writeFileSync)((0, import_node_path4.join)(dir, `${label}.timer`), linuxTimer(label));
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, import_node_fs8.rmSync)((0, import_node_path4.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
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, import_node_path4.join)(home, ".config", "systemd", "user");
1411
+ const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
1017
1412
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
1018
- (0, import_node_fs8.rmSync)((0, import_node_path4.join)(dir, `${label}.service`), { force: true });
1019
- (0, import_node_fs8.rmSync)((0, import_node_path4.join)(dir, `${label}.timer`), { force: true });
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, import_node_path4.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
1038
- if (!(0, import_node_fs8.existsSync)(plist)) return { supported: true, enabled: false };
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, import_node_path4.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
1042
- if (!(0, import_node_fs8.existsSync)(timer)) return { supported: true, enabled: false };
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 import_node_crypto4 = require("node:crypto");
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, import_node_crypto4.randomBytes)(32));
1325
- const challenge = b64url((0, import_node_crypto4.createHash)("sha256").update(verifier).digest());
1326
- const state = b64url((0, import_node_crypto4.randomBytes)(16));
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.13" : readVersionFromPackage();
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 = update && current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
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 candidate = (0, import_node_crypto6.randomUUID)();
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
- await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl });
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.relay(visible, channel2, false);
2222
+ if (visible) installer.relayChunk(visible, channel2);
2062
2223
  }).on("end", () => {
2063
- if (partial) installer.relay("[redacted]", channel2, false);
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", (text) => {
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
  }