@mutmutco/installer-launcher 0.1.13 → 0.1.14

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