@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.
@@ -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,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, import_node_fs3.readFileSync)((0, import_node_path3.join)(payload, "payload.json"), "utf8"));
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, import_node_path3.dirname)(node)}${platform === "win32" ? ";" : ":"}${current}`;
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, 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)()}`;
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, import_node_fs3.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
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, import_node_fs3.writeFileSync)(recovery, owner, { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.readFileSync)(path, "utf8");
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, import_node_fs3.unlinkSync)(path);
250
- (0, import_node_fs3.writeFileSync)(path, owner, { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.unlinkSync)(recovery);
589
+ (0, import_node_fs5.unlinkSync)(recovery);
253
590
  }
254
591
  }
255
592
  let released = false;
256
593
  return () => {
257
- if (!released && (0, import_node_fs3.readFileSync)(path, "utf8") === owner) (0, import_node_fs3.unlinkSync)(path);
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, import_node_path3.join)(dir, "acquisition-pending.json");
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, import_node_path3.join)(dir, "candidates", candidate);
603
+ return (0, import_node_path4.join)(dir, "candidates", candidate);
267
604
  }
268
605
  function packageRoot(prefix, acquisition) {
269
- return (0, import_node_path3.join)(prefix, "node_modules", acquisition.package);
606
+ return (0, import_node_path4.join)(prefix, "node_modules", acquisition.package);
270
607
  }
271
608
  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");
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, import_node_path3.join)(root, "payload"));
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, import_node_path3.join)(root, "prefix");
623
+ const prefix = (0, import_node_path4.join)(root, "prefix");
287
624
  const productRoot = packageRoot(prefix, acquisition);
288
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.join)(productRoot, "package.json"), "utf8"));
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, import_node_fs3.realpathSync)(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
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, import_node_fs3.existsSync)(pendingPath(dir))) return;
294
- const pending = JSON.parse((0, import_node_fs3.readFileSync)(pendingPath(dir), "utf8"));
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, import_node_fs3.unlinkSync)(pendingPath(dir));
635
+ (0, import_node_fs5.unlinkSync)(pendingPath(dir));
299
636
  return;
300
637
  }
301
- const acquisition = readAcquisition((0, import_node_path3.join)(root, "payload"));
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, import_node_path3.join)(root, "prefix");
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, import_node_fs3.existsSync)(statePath(dir))) (0, import_node_fs3.unlinkSync)(statePath(dir));
308
- (0, import_node_fs3.unlinkSync)(pendingPath(dir));
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, 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);
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, 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, "");
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, import_node_path3.resolve)(payload, acquisition.archive)
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, import_node_fs3.readFileSync)((0, import_node_path3.join)(productRoot, "package.json"), "utf8"));
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, import_node_fs3.writeFileSync)(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
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, import_node_fs3.unlinkSync)(pendingPath(dir));
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 import_node_fs4 = require("node:fs");
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 set up MM Strategy \u2014 about a minute.",
719
+ installWarm: "Welcome. Let's get you ready.",
377
720
  accent: "38;2;249;115;22",
378
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
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. Setting up mmi-hub \u2014 about a minute.",
726
+ installWarm: "Welcome. Let's get you ready.",
384
727
  accent: "38;2;125;211;252",
385
- warm: "Welcome back. Checking your surfaces\u2026",
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. Setting up jerv-hub \u2014 about a minute.",
733
+ installWarm: "Welcome. Let's get you ready.",
391
734
  accent: "38;2;248;113;113",
392
- warm: "Welcome back. Checking your surfaces\u2026",
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. Setting up JervCode \u2014 about a minute.",
740
+ installWarm: "Welcome. Let's get you ready.",
398
741
  accent: "38;2;192;132;252",
399
- warm: "Welcome back. Keeping JervCode current\u2026",
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, import_node_fs4.writeSync)(progressFd, `${JSON.stringify(record)}
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).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");
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 import_node_fs5 = require("node:fs");
904
+ var import_node_fs7 = require("node:fs");
557
905
  var import_node_worker_threads = require("node:worker_threads");
558
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
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, import_node_fs5.writeSync)(2, text);
595
- if (transcriptPath) (0, import_node_fs5.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
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
- frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
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 import_node_fs7 = require("node:fs");
1054
+ var import_node_fs9 = require("node:fs");
669
1055
 
670
1056
  // ../face/src/outcome.ts
671
- var import_node_fs6 = require("node:fs");
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, import_node_fs6.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
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, import_node_fs6.readFileSync)(path, "utf8");
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, import_node_fs7.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
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 = options.operation === "install" ? face.identity.installWarm : face.identity.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 title = PHASES[id][state === "ok" ? 1 : 0];
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
- spinner.start(title, facts.measure ?? null);
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
- spinner.stop();
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 import_node_fs8 = require("node:fs");
927
- var import_node_os2 = require("node:os");
928
- var import_node_path4 = require("node:path");
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, import_node_os2.tmpdir)();
946
- return process.env.HOME ?? (0, import_node_os2.tmpdir)();
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, 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));
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, 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));
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, import_node_fs8.rmSync)((0, import_node_path4.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
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, import_node_path4.join)(home, ".config", "systemd", "user");
1493
+ const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
1017
1494
  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 });
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, import_node_path4.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
1038
- if (!(0, import_node_fs8.existsSync)(plist)) return { supported: true, enabled: false };
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, import_node_path4.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
1042
- if (!(0, import_node_fs8.existsSync)(timer)) return { supported: true, enabled: false };
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 import_node_crypto4 = require("node:crypto");
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, 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));
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.13" : readVersionFromPackage();
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 current = readState(dir);
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 candidate = (0, import_node_crypto6.randomUUID)();
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
- await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl });
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.relay(visible, channel2, false);
2322
+ if (visible) installer.relayChunk(visible, channel2);
2062
2323
  }).on("end", () => {
2063
- if (partial) installer.relay("[redacted]", channel2, false);
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", (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
- });
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
  }