@mutmutco/installer-launcher 0.1.12 → 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,43 +1,673 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
5
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync, mkdtempSync, unlinkSync, rmdirSync } from "node:fs";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ import { randomUUID as randomUUID4 } from "node:crypto";
6
+
7
+ // src/acquisition.ts
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";
11
+
12
+ // src/runtime.ts
13
+ import { createHash, randomUUID } from "node:crypto";
14
+ import { spawnSync } from "node:child_process";
15
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync, renameSync, rmSync, realpathSync } from "node:fs";
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
37
+ var NODE_VERSION = "24.20.0";
38
+ var NPM_VERSION = "12.0.2";
39
+ var ARTIFACTS = {
40
+ "win32-x64": ["node-v24.20.0-win-x64.zip", "6cac9ffbca8f6a47091e4b5c772e0606049c3871cb67d900c0cedde630e545ba"],
41
+ "win32-arm64": ["node-v24.20.0-win-arm64.zip", "31c6799744de8a54601643098040c68c3697e56c94e407d61d0e5fa5f34191d7"],
42
+ "darwin-arm64": ["node-v24.20.0-darwin-arm64.tar.gz", "40e5607e5ecb3db9192723776da2d75d966260fc74a7a9e731c1bd67dda96bc8"],
43
+ "linux-x64": ["node-v24.20.0-linux-x64.tar.gz", "855d581f8a4eb1a8117e3426de25fe02770592febcfb31369aee1ffbfee9e8ec"],
44
+ "linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
45
+ };
46
+ var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
47
+ async function verifiedDownload(url, path, algorithm, digest, fetchImpl, onProgress) {
48
+ const response = await fetchImpl(url);
49
+ if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
50
+ const bytes = await readDownload(response, onProgress);
51
+ if (createHash(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
52
+ writeFileSync(path, bytes);
53
+ }
54
+ function command(executable, args) {
55
+ const result = spawnSync(executable, args, { encoding: "utf8", windowsHide: true });
56
+ if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
57
+ return result.stdout.trim();
58
+ }
59
+ async function acquireRuntime(dir, fetchImpl = fetch, onProgress) {
60
+ const platform = `${process.platform}-${process.arch}`;
61
+ const artifact = ARTIFACTS[platform];
62
+ if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
63
+ const runtimeDir = join(dir, "runtimes");
64
+ mkdirSync(runtimeDir, { recursive: true });
65
+ const receipt = join(runtimeDir, `node-${NODE_VERSION}-npm-${NPM_VERSION}-${platform}.json`);
66
+ if (existsSync(receipt)) {
67
+ const runtime2 = JSON.parse(readFileSync(receipt, "utf8"));
68
+ for (const executable of [runtime2.node, runtime2.npm]) {
69
+ const rel = relative(realpathSync(runtimeDir), realpathSync(executable));
70
+ if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("cached runtime escapes owned directory");
71
+ }
72
+ if (command(runtime2.node, ["--version"]) === `v${NODE_VERSION}` && command(runtime2.node, [runtime2.npm, "--version"]) === NPM_VERSION) return runtime2;
73
+ throw new Error("cached runtime validation failed");
74
+ }
75
+ const root = mkdtempSync(join(runtimeDir, "runtime-"));
76
+ const archive = join(root, artifact[0]);
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;
85
+ const tar = process.platform === "win32" ? join(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
86
+ command(tar, ["-xf", archive, "-C", root]);
87
+ const unpacked = join(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
88
+ const node = join(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
89
+ const npmArchive = join(root, "npm.tgz");
90
+ await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl, progress);
91
+ const npmRoot = join(root, "npm");
92
+ mkdirSync(npmRoot);
93
+ command(tar, ["-xf", npmArchive, "-C", npmRoot]);
94
+ const installedNpm = join(unpacked, process.platform === "win32" ? "node_modules/npm" : "lib/node_modules/npm");
95
+ if (existsSync(installedNpm)) renameSync(installedNpm, join(root, "npm-bundled"));
96
+ renameSync(join(npmRoot, "package"), installedNpm);
97
+ const npm = join(installedNpm, "bin/npm-cli.js");
98
+ if (command(node, ["--version"]) !== `v${NODE_VERSION}` || command(node, [npm, "--version"]) !== NPM_VERSION) throw new Error("downloaded runtime validation failed");
99
+ const runtime = { node, npm };
100
+ const temporary = `${receipt}.${randomUUID()}.tmp`;
101
+ try {
102
+ writeFileSync(temporary, JSON.stringify(runtime), { flag: "wx", mode: 384, flush: true });
103
+ renameSync(temporary, receipt);
104
+ } finally {
105
+ rmSync(temporary, { force: true });
106
+ }
107
+ return runtime;
108
+ }
109
+
110
+ // src/store.ts
111
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "node:fs";
112
+ import { randomUUID as randomUUID2 } from "node:crypto";
113
+ import { tmpdir } from "node:os";
114
+ import { join as join2 } from "node:path";
115
+ function defaultProductDir(product) {
116
+ if (process.platform === "win32") {
117
+ const base = process.env.LOCALAPPDATA ?? join2(tmpdir(), "launcher-fallback");
118
+ return join2(base, product);
119
+ }
120
+ const home = process.env.HOME ?? tmpdir();
121
+ return join2(home, `.${product}`);
122
+ }
123
+ function resolveProductDir(product, explicit) {
124
+ return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
125
+ }
126
+ function tokensPath(dir) {
127
+ return join2(dir, "tokens.json");
128
+ }
129
+ function statePath(dir) {
130
+ return join2(dir, "state.json");
131
+ }
132
+ function payloadDir(dir) {
133
+ const acquired = readState(dir)?.acquired;
134
+ if (acquired) return join2(dir, "candidates", acquired.candidate, "payload");
135
+ return join2(dir, "payload");
136
+ }
137
+ function readTokens(dir) {
138
+ try {
139
+ const data = JSON.parse(readFileSync2(tokensPath(dir), "utf8"));
140
+ if (typeof data.accessToken !== "string" || !data.accessToken) return null;
141
+ if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
142
+ const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
143
+ if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
144
+ if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
145
+ return tokens;
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+ function writeTokens(dir, tokens) {
151
+ mkdirSync2(dir, { recursive: true });
152
+ try {
153
+ writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
154
+ `, { mode: 384 });
155
+ } catch {
156
+ writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
157
+ `);
158
+ }
159
+ }
160
+ function clearTokens(dir) {
161
+ rmSync2(tokensPath(dir), { force: true });
162
+ }
163
+ function readState(dir) {
164
+ try {
165
+ const data = JSON.parse(readFileSync2(statePath(dir), "utf8"));
166
+ if (typeof data.version !== "string" || !data.version) return null;
167
+ const state = { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
168
+ if (data.acquired) {
169
+ if (!/^[a-zA-Z0-9_-]+$/.test(data.acquired.candidate) || typeof data.acquired.node !== "string" || typeof data.acquired.entry !== "string") return null;
170
+ state.acquired = data.acquired;
171
+ }
172
+ return state;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ function writeState(dir, state) {
178
+ mkdirSync2(dir, { recursive: true });
179
+ const temporary = `${statePath(dir)}.${randomUUID2()}.tmp`;
180
+ try {
181
+ writeFileSync2(temporary, `${JSON.stringify(state, null, 2)}
182
+ `, { flag: "wx", mode: 384, flush: true });
183
+ renameSync2(temporary, statePath(dir));
184
+ } finally {
185
+ rmSync2(temporary, { force: true });
186
+ }
187
+ }
188
+ function wipeProductDir(dir) {
189
+ const payload = payloadDir(dir);
190
+ rmSync2(tokensPath(dir), { force: true });
191
+ rmSync2(statePath(dir), { force: true });
192
+ rmSync2(payload, { force: true, recursive: true });
193
+ }
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
+
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
+ }
464
+ function safePath(value) {
465
+ return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
466
+ }
467
+ function argumentsValid(value) {
468
+ return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
469
+ }
470
+ function readAcquisition(payload) {
471
+ const metadata = JSON.parse(readFileSync4(join4(payload, "payload.json"), "utf8"));
472
+ if (metadata.acquisition === void 0) return null;
473
+ const a = metadata.acquisition;
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))) {
475
+ throw new Error("invalid signed acquisition declaration");
476
+ }
477
+ return a;
478
+ }
479
+ function runtimeEnvironment(node, inherited, platform = process.platform) {
480
+ const env = { ...inherited };
481
+ let current = env.PATH ?? "";
482
+ if (platform === "win32") {
483
+ const keys = Object.keys(env).filter((key) => key.toLowerCase() === "path").sort();
484
+ current = (keys.length ? env[keys[0]] : "") ?? "";
485
+ for (const key of keys) delete env[key];
486
+ }
487
+ env.PATH = `${dirname2(node)}${platform === "win32" ? ";" : ":"}${current}`;
488
+ return env;
489
+ }
490
+ function lockInstallation(dir) {
491
+ mkdirSync4(dir, { recursive: true });
492
+ const path = join4(dir, "installation.lock");
493
+ const owner = `${process.pid}:${randomUUID3()}`;
494
+ try {
495
+ writeFileSync4(path, owner, { flag: "wx", mode: 384 });
496
+ } catch (error) {
497
+ if (error.code !== "EEXIST") throw error;
498
+ const recovery = `${path}.recovery`;
499
+ try {
500
+ writeFileSync4(recovery, owner, { flag: "wx", mode: 384, flush: true });
501
+ } catch {
502
+ throw new Error("installation lock recovery is already pending");
503
+ }
504
+ try {
505
+ const previous = readFileSync4(path, "utf8");
506
+ const pid = Number(previous.split(":")[0]);
507
+ if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
508
+ try {
509
+ process.kill(pid, 0);
510
+ throw new Error("another installer is running");
511
+ } catch (probe) {
512
+ if (probe.code !== "ESRCH") throw probe;
513
+ }
514
+ unlinkSync(path);
515
+ writeFileSync4(path, owner, { flag: "wx", mode: 384, flush: true });
516
+ } finally {
517
+ unlinkSync(recovery);
518
+ }
519
+ }
520
+ let released = false;
521
+ return () => {
522
+ if (!released && readFileSync4(path, "utf8") === owner) unlinkSync(path);
523
+ released = true;
524
+ };
525
+ }
526
+ function pendingPath(dir) {
527
+ return join4(dir, "acquisition-pending.json");
528
+ }
529
+ function candidateRoot(dir, candidate) {
530
+ if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
531
+ return join4(dir, "candidates", candidate);
532
+ }
533
+ function packageRoot(prefix, acquisition) {
534
+ return join4(prefix, "node_modules", acquisition.package);
535
+ }
536
+ function entry(root, path) {
537
+ const actual = realpathSync2(join4(root, path));
538
+ const rel = relative2(realpathSync2(root), actual);
539
+ if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("acquired entry escapes package");
540
+ return actual;
541
+ }
542
+ function argv(node, root, file, args, prefix, version) {
543
+ return [node, entry(root, file), ...args.map((arg) => arg === "$prefix" ? prefix : arg === "$version" ? version : arg)];
544
+ }
545
+ function acquisitionRepairCommand(dir, version) {
546
+ const state = readState(dir);
547
+ if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
548
+ const root = candidateRoot(dir, state.acquired.candidate);
549
+ const acquisition = readAcquisition(join4(root, "payload"));
550
+ if (!acquisition) throw new Error("installed acquisition declaration is missing");
551
+ const prefix = join4(root, "prefix");
552
+ const productRoot = packageRoot(prefix, acquisition);
553
+ const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
554
+ if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
555
+ return argv(realpathSync2(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
556
+ }
557
+ async function recoverAcquisition(dir, run2, env) {
558
+ if (!existsSync4(pendingPath(dir))) return;
559
+ const pending = JSON.parse(readFileSync4(pendingPath(dir), "utf8"));
560
+ if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
561
+ const root = candidateRoot(dir, pending.candidate);
562
+ if (readState(dir)?.acquired?.candidate === pending.candidate) {
563
+ unlinkSync(pendingPath(dir));
564
+ return;
565
+ }
566
+ const acquisition = readAcquisition(join4(root, "payload"));
567
+ if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
568
+ const prefix = join4(root, "prefix");
569
+ const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
570
+ if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
571
+ if (pending.previous) writeState(dir, pending.previous);
572
+ else if (existsSync4(statePath(dir))) unlinkSync(statePath(dir));
573
+ unlinkSync(pendingPath(dir));
574
+ }
575
+ async function installAcquisition(dir, candidate, version, acquisition, options) {
576
+ if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
577
+ const root = candidateRoot(dir, candidate);
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);
586
+ const env = runtimeEnvironment(runtime.node, options.env);
587
+ const npmEnv = { ...env };
588
+ for (const key of Object.keys(npmEnv)) {
589
+ if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
590
+ }
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, "");
595
+ const install = [
596
+ runtime.node,
597
+ runtime.npm,
598
+ "install",
599
+ "--prefix",
600
+ prefix,
601
+ "--install-strategy=shallow",
602
+ "--ignore-scripts",
603
+ "--no-audit",
604
+ "--no-fund",
605
+ "--userconfig",
606
+ npmrc,
607
+ "--globalconfig",
608
+ globalrc,
609
+ "--registry",
610
+ "https://registry.npmjs.org/",
611
+ resolve(payload, acquisition.archive)
612
+ ];
613
+ if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
614
+ const productRoot = packageRoot(prefix, acquisition);
615
+ const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
616
+ if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
617
+ const runEntry = entry(productRoot, acquisition.runEntry);
618
+ const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
619
+ entry(productRoot, acquisition.rollbackEntry);
620
+ if (!reused) writeFileSync4(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
621
+ const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
622
+ writeFileSync4(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
623
+ try {
624
+ if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
625
+ (options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
626
+ } catch (error) {
627
+ await recoverAcquisition(dir, options.run, env);
628
+ throw error;
629
+ }
630
+ unlinkSync(pendingPath(dir));
631
+ }
632
+
633
+ // src/index.ts
634
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
635
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
6
636
  import { tmpdir as tmpdir4 } from "node:os";
7
- import { dirname as dirname2, join as join4, resolve } from "node:path";
637
+ import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
8
638
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
9
639
 
10
640
  // ../face/src/face.ts
11
- import { writeSync } from "node:fs";
641
+ import { appendFileSync, writeSync } from "node:fs";
12
642
 
13
643
  // ../face/src/products.ts
14
644
  var PRODUCTS = Object.freeze({
15
645
  "mm-strategy": Object.freeze({
16
646
  name: "MM Strategy",
17
- installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
647
+ installWarm: "Welcome. Let's get you ready.",
18
648
  accent: "38;2;249;115;22",
19
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
649
+ warm: "Welcome back. Checking for updates...",
20
650
  doctor: "mm-strategy doctor"
21
651
  }),
22
652
  "mmi-hub": Object.freeze({
23
653
  name: "mmi-hub",
24
- installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
654
+ installWarm: "Welcome. Let's get you ready.",
25
655
  accent: "38;2;125;211;252",
26
- warm: "Welcome back. Checking your surfaces\u2026",
656
+ warm: "Welcome back. Checking for updates...",
27
657
  doctor: "mmi doctor"
28
658
  }),
29
659
  "jerv-hub": Object.freeze({
30
660
  name: "jerv-hub",
31
- installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
661
+ installWarm: "Welcome. Let's get you ready.",
32
662
  accent: "38;2;248;113;113",
33
- warm: "Welcome back. Checking your surfaces\u2026",
663
+ warm: "Welcome back. Checking for updates...",
34
664
  doctor: "jerv doctor"
35
665
  }),
36
666
  jervcode: Object.freeze({
37
667
  name: "JervCode",
38
- installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
668
+ installWarm: "Welcome. Let's get you ready.",
39
669
  accent: "38;2;192;132;252",
40
- warm: "Welcome back. Keeping JervCode current\u2026",
670
+ warm: "Welcome back. Checking for updates...",
41
671
  doctor: "jervcode doctor"
42
672
  })
43
673
  });
@@ -51,6 +681,7 @@ function identityFor(product) {
51
681
 
52
682
  // ../face/src/face.ts
53
683
  var GLYPH = Object.freeze({
684
+ clock: "\u23F0",
54
685
  diamond: "\u25C6",
55
686
  hollow: "\u25C7",
56
687
  bar: "\u2502",
@@ -122,12 +753,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
122
753
  const continuesFace = continuedPhases.size > 0;
123
754
  const nested = env.MM_OUTER_CONSOLE === "1";
124
755
  const progressFd = readProgressFd(env);
756
+ const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
125
757
  const emitMilestone = (title, measure, kind) => {
126
- if (progressFd === null) return false;
758
+ if (progressFd === null && !progressFile) return false;
127
759
  const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
128
760
  if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
761
+ if (kind === "running" && typeof measure === "string") record.measure = measure;
129
762
  try {
130
- writeSync(progressFd, `${JSON.stringify(record)}
763
+ if (progressFile) appendFileSync(progressFile, `${JSON.stringify(record)}
764
+ `);
765
+ else writeSync(progressFd, `${JSON.stringify(record)}
131
766
  `);
132
767
  return true;
133
768
  } catch {
@@ -146,7 +781,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
146
781
  };
147
782
  const step = (title, measure = null, kind = "ok") => {
148
783
  if (emitMilestone(title, measure, kind)) return "";
149
- 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);
150
785
  const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
151
786
  const time = measured;
152
787
  const column = Math.min(44, Math.max(0, width - 8));
@@ -156,7 +791,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
156
791
  const pad = Math.max(1, column - visibleWidth(head));
157
792
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
158
793
  };
159
- 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");
160
795
  const receipt = (lines, { ready = true } = {}) => {
161
796
  if (ready && nested) return [];
162
797
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
@@ -187,14 +822,14 @@ function createFace({ product, color = false, columns, env = process.env, operat
187
822
  };
188
823
  const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
189
824
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
190
- 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 };
191
826
  }
192
827
 
193
828
  // ../face/src/shell.ts
194
829
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
195
830
 
196
831
  // ../face/src/spinner.ts
197
- import { appendFileSync, writeSync as writeSync2 } from "node:fs";
832
+ import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
198
833
  import { Worker } from "node:worker_threads";
199
834
  var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
200
835
  var WORKER_SOURCE = `
@@ -206,7 +841,7 @@ function draw() {
206
841
  if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
207
842
  try {
208
843
  if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
209
- const text = frames[frame++ % frames.length];
844
+ const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
210
845
  writeSync(2, text);
211
846
  if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
212
847
  } finally {
@@ -218,7 +853,6 @@ parentPort.on('message', (next) => {
218
853
  if (Atomics.load(control, 2)) return;
219
854
  frames = next.frames;
220
855
  frame = next.frame;
221
- Atomics.store(control, 0, 0);
222
856
  });
223
857
  setInterval(draw, workerData.intervalMs);
224
858
  `;
@@ -229,20 +863,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
229
863
  let control = null;
230
864
  let frames = [];
231
865
  let frame = 0;
866
+ let started = 0;
867
+ let suspended = false;
232
868
  const write = (text) => {
233
869
  if (stream) stream.write(text);
234
870
  else {
235
871
  writeSync2(2, text);
236
- if (transcriptPath) appendFileSync(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
872
+ if (transcriptPath) appendFileSync2(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
237
873
  `);
238
874
  }
239
875
  };
240
876
  const render = (text, measure) => {
241
- const line = face.step(text, measure, "note").split("\n")[0];
877
+ const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
242
878
  frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
243
879
  };
244
880
  const draw = () => {
245
- 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`));
246
882
  };
247
883
  const pause = () => {
248
884
  if (!control) return;
@@ -262,6 +898,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
262
898
  start(text, measure = null) {
263
899
  if (!animate) return;
264
900
  halt();
901
+ suspended = false;
902
+ started = Date.now();
265
903
  render(text, measure);
266
904
  frame = 0;
267
905
  draw();
@@ -275,7 +913,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
275
913
  frames,
276
914
  frame,
277
915
  intervalMs,
278
- transcriptPath
916
+ transcriptPath,
917
+ started
279
918
  } });
280
919
  const active = worker;
281
920
  worker.on("error", () => {
@@ -293,9 +932,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
293
932
  render(text, measure);
294
933
  draw();
295
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();
296
947
  },
297
948
  stop() {
298
949
  halt();
950
+ frames = [];
299
951
  if (animate) write("\r\x1B[2K");
300
952
  }
301
953
  };
@@ -306,10 +958,10 @@ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
306
958
  var ALLOWED = new Set(Object.values(GLYPH));
307
959
 
308
960
  // ../face/src/run.ts
309
- import { appendFileSync as appendFileSync2 } from "node:fs";
961
+ import { appendFileSync as appendFileSync3 } from "node:fs";
310
962
 
311
963
  // ../face/src/outcome.ts
312
- import { readFileSync, writeFileSync } from "node:fs";
964
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
313
965
  var counts = ["total", "updated", "failed"];
314
966
  var strings = ["version", "retry", "detail", "logPath"];
315
967
  var flags = ["dryRun", "installed", "deferred", "operationFailed"];
@@ -331,12 +983,12 @@ function validateInstallerOutcome(value) {
331
983
  return { ...facts };
332
984
  }
333
985
  function writeInstallerOutcome(path, value) {
334
- writeFileSync(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
986
+ writeFileSync5(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
335
987
  }
336
988
  function readInstallerOutcome(path) {
337
989
  let text;
338
990
  try {
339
- text = readFileSync(path, "utf8");
991
+ text = readFileSync5(path, "utf8");
340
992
  } catch (error) {
341
993
  if (error.code === "ENOENT") return void 0;
342
994
  throw error;
@@ -399,6 +1051,7 @@ var PHASES = {
399
1051
  "verify-release": ["Checking the release version", "Verified the release version"],
400
1052
  verify: ["Verifying the payload", "Verified the payload"],
401
1053
  install: ["Installing the product", "Installed the product"],
1054
+ configure: ["Configuring the product", "Configured the product"],
402
1055
  activate: ["Activating surfaces", "Activated surfaces"],
403
1056
  doctor: ["Checking health", "Checked health"],
404
1057
  rollback: ["Restoring the previous version", "Restored the previous version"]
@@ -428,7 +1081,7 @@ function createInstallerRun(value, options = {}) {
428
1081
  if (!text) return;
429
1082
  write(text, channel);
430
1083
  if (env.MM_FACE_TRANSCRIPT) {
431
- appendFileSync2(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
1084
+ appendFileSync3(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
432
1085
  `, "utf8");
433
1086
  }
434
1087
  };
@@ -443,6 +1096,17 @@ function createInstallerRun(value, options = {}) {
443
1096
  return true;
444
1097
  } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
445
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
+ };
446
1110
  let started = false;
447
1111
  let finished = false;
448
1112
  const start = () => {
@@ -457,6 +1121,9 @@ function createInstallerRun(value, options = {}) {
457
1121
  }
458
1122
  };
459
1123
  const durable = (title, measure, kind) => {
1124
+ closeChunk();
1125
+ activeTitle = void 0;
1126
+ deferredActivity = void 0;
460
1127
  spinner.stop();
461
1128
  const rendered = face.step(title, measure, kind);
462
1129
  if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
@@ -473,7 +1140,12 @@ function createInstallerRun(value, options = {}) {
473
1140
  const state = facts.state ?? "ok";
474
1141
  const title = PHASES[id][state === "ok" ? 1 : 0];
475
1142
  if (state === "running") {
476
- 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
+ }
477
1149
  return;
478
1150
  }
479
1151
  if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
@@ -493,8 +1165,20 @@ function createInstallerRun(value, options = {}) {
493
1165
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
494
1166
  if (facts.detail) run2.relay(facts.detail);
495
1167
  },
496
- milestone({ step, state, ms }) {
1168
+ milestone({ step, state, ms, measure }) {
497
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
+ }
498
1182
  durable(step, ms === void 0 ? null : ms / 1e3, state);
499
1183
  },
500
1184
  signIn({ url, code }) {
@@ -508,7 +1192,8 @@ function createInstallerRun(value, options = {}) {
508
1192
  },
509
1193
  // Only pass safe diagnostic text, never authentication output or credentials.
510
1194
  relay(text, channel = "stdout", record = true) {
511
- spinner.stop();
1195
+ closeChunk();
1196
+ spinner.pause();
512
1197
  const rendered = tty ? face.relay(text) : stripColor(text);
513
1198
  for (const row of rendered.split("\n")) if (row) {
514
1199
  emit(`${row}
@@ -516,6 +1201,37 @@ function createInstallerRun(value, options = {}) {
516
1201
  ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
517
1202
  `);
518
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
+ }
519
1235
  },
520
1236
  cancel() {
521
1237
  if (finished) return;
@@ -524,6 +1240,8 @@ function createInstallerRun(value, options = {}) {
524
1240
  { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
525
1241
  );
526
1242
  start();
1243
+ closeChunk();
1244
+ activeTitle = void 0;
527
1245
  spinner.stop();
528
1246
  finished = true;
529
1247
  if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
@@ -536,6 +1254,8 @@ function createInstallerRun(value, options = {}) {
536
1254
  validateInstallerOutcome(facts);
537
1255
  if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
538
1256
  start();
1257
+ closeChunk();
1258
+ activeTitle = void 0;
539
1259
  spinner.stop();
540
1260
  finished = true;
541
1261
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
@@ -556,6 +1276,8 @@ function createInstallerRun(value, options = {}) {
556
1276
  if (tty) lines([face.signOff()]);
557
1277
  },
558
1278
  stop() {
1279
+ closeChunk();
1280
+ activeTitle = void 0;
559
1281
  spinner.stop();
560
1282
  }
561
1283
  };
@@ -563,17 +1285,17 @@ function createInstallerRun(value, options = {}) {
563
1285
  }
564
1286
 
565
1287
  // src/autoupdate.ts
566
- import { spawnSync } from "node:child_process";
567
- import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
568
- import { tmpdir } from "node:os";
569
- import { join } from "node:path";
1288
+ import { spawnSync as spawnSync2 } from "node:child_process";
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";
570
1292
  function schedulePlatform(override) {
571
1293
  const platform = override ?? process.platform;
572
1294
  if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
573
1295
  return null;
574
1296
  }
575
- function defaultExec(command, args) {
576
- const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
1297
+ function defaultExec(command2, args) {
1298
+ const result = spawnSync2(command2, args, { encoding: "utf8", windowsHide: true });
577
1299
  if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
578
1300
  return {
579
1301
  code: typeof result.status === "number" ? result.status : 1,
@@ -583,8 +1305,8 @@ function defaultExec(command, args) {
583
1305
  }
584
1306
  function homeOf(options) {
585
1307
  if (options.homeDir) return options.homeDir;
586
- if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir();
587
- return process.env.HOME ?? tmpdir();
1308
+ if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir3();
1309
+ return process.env.HOME ?? tmpdir3();
588
1310
  }
589
1311
  function scheduleName(config) {
590
1312
  return `${config.binName} autoupdate`;
@@ -595,13 +1317,13 @@ function scheduleLabel(config) {
595
1317
  function quoteWindows(arg) {
596
1318
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
597
1319
  }
598
- function enableSchedule(config, command, options = {}) {
1320
+ function enableSchedule(config, command2, options = {}) {
599
1321
  const platform = schedulePlatform(options.platform);
600
1322
  if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
601
1323
  const exec = options.exec ?? defaultExec;
602
1324
  const home = homeOf(options);
603
1325
  if (platform === "win32") {
604
- const taskLine = command.map(quoteWindows).join(" ");
1326
+ const taskLine = command2.map(quoteWindows).join(" ");
605
1327
  const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
606
1328
  if (result2.code !== 0) {
607
1329
  throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
@@ -610,10 +1332,10 @@ function enableSchedule(config, command, options = {}) {
610
1332
  }
611
1333
  if (platform === "darwin") {
612
1334
  const label2 = scheduleLabel(config);
613
- const dir2 = join(home, "Library", "LaunchAgents");
614
- mkdirSync(dir2, { recursive: true });
615
- const plist = join(dir2, `${label2}.plist`);
616
- writeFileSync2(plist, darwinPlist(label2, command));
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));
617
1339
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
618
1340
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
619
1341
  if (result2.code !== 0) {
@@ -622,10 +1344,10 @@ function enableSchedule(config, command, options = {}) {
622
1344
  return;
623
1345
  }
624
1346
  const label = scheduleLabel(config);
625
- const dir = join(home, ".config", "systemd", "user");
626
- mkdirSync(dir, { recursive: true });
627
- writeFileSync2(join(dir, `${label}.service`), linuxService(command));
628
- writeFileSync2(join(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));
629
1351
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
630
1352
  if (reload.code !== 0) {
631
1353
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -650,14 +1372,14 @@ function disableSchedule(config, options = {}) {
650
1372
  if (platform === "darwin") {
651
1373
  const label2 = scheduleLabel(config);
652
1374
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
653
- rmSync(join(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
1375
+ rmSync4(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
654
1376
  return;
655
1377
  }
656
1378
  const label = scheduleLabel(config);
657
- const dir = join(home, ".config", "systemd", "user");
1379
+ const dir = join5(home, ".config", "systemd", "user");
658
1380
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
659
- rmSync(join(dir, `${label}.service`), { force: true });
660
- rmSync(join(dir, `${label}.timer`), { force: true });
1381
+ rmSync4(join5(dir, `${label}.service`), { force: true });
1382
+ rmSync4(join5(dir, `${label}.timer`), { force: true });
661
1383
  }
662
1384
  function querySchedule(config, options = {}) {
663
1385
  const platform = schedulePlatform(options.platform);
@@ -675,20 +1397,20 @@ function querySchedule(config, options = {}) {
675
1397
  return state2;
676
1398
  }
677
1399
  if (platform === "darwin") {
678
- const plist = join(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
679
- if (!existsSync(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 };
680
1402
  return { supported: true, enabled: true, cadence: "hourly" };
681
1403
  }
682
- const timer = join(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
683
- if (!existsSync(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 };
684
1406
  const state = { supported: true, enabled: true, cadence: "hourly" };
685
1407
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
686
1408
  const stamp = (shown.stdout ?? "").trim();
687
1409
  if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
688
1410
  return state;
689
1411
  }
690
- function darwinPlist(label, command) {
691
- const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
1412
+ function darwinPlist(label, command2) {
1413
+ const args = command2.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
692
1414
  return `<?xml version="1.0" encoding="UTF-8"?>
693
1415
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
694
1416
  <plist version="1.0">
@@ -705,8 +1427,8 @@ ${args}
705
1427
  </plist>
706
1428
  `;
707
1429
  }
708
- function linuxService(command) {
709
- const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
1430
+ function linuxService(command2) {
1431
+ const line = command2.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
710
1432
  return `[Unit]
711
1433
  Description=${"Hourly update check"}
712
1434
  [Service]
@@ -724,102 +1446,22 @@ Persistent=true
724
1446
  WantedBy=timers.target
725
1447
  `;
726
1448
  }
727
- function xmlEscape(text) {
728
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
729
- }
730
- function valueOf(output, key) {
731
- const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
732
- if (!line) return null;
733
- const value = line.slice(line.indexOf(key) + key.length).trim();
734
- return value ? value : null;
735
- }
736
- function firstLine(text) {
737
- return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
738
- }
739
-
740
- // src/config.ts
741
- import { readFileSync as readFileSync3 } from "node:fs";
742
- import { getAsset } from "node:sea";
743
-
744
- // src/module-url.ts
745
- import { pathToFileURL } from "node:url";
746
- function moduleUrl() {
747
- if (typeof __filename === "string") return pathToFileURL(__filename).href;
748
- return import.meta.url;
749
- }
750
-
751
- // src/config.ts
752
- function publicKeyBytes(config) {
753
- const raw = Buffer.from(config.publicKey, "base64");
754
- if (raw.length !== 32) {
755
- throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
756
- }
757
- return raw;
758
- }
759
- function loadProductConfig(options = {}) {
760
- const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
761
- const devFallback = new URL("../config/product.template.json", moduleUrl());
762
- if (explicit) {
763
- return parseProductConfig(readFileSync3(explicit, "utf8"));
764
- }
765
- try {
766
- return parseProductConfig(readFileSync3(devFallback, "utf8"));
767
- } catch {
768
- }
769
- try {
770
- const asset = getAsset("product.json", "utf8");
771
- if (typeof asset === "string") return parseProductConfig(asset);
772
- } catch {
773
- }
774
- throw new Error(
775
- "no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
776
- );
1449
+ function xmlEscape(text) {
1450
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
777
1451
  }
778
- function parseProductConfig(text) {
779
- let data;
780
- try {
781
- data = JSON.parse(text);
782
- } catch {
783
- throw new Error("product config is not valid JSON");
784
- }
785
- if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
786
- const record = data;
787
- const product = field(record, "product");
788
- const host = field(record, "host").replace(/\/+$/, "");
789
- const loginKind = field(record, "loginKind");
790
- const binName = field(record, "binName");
791
- if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
792
- if (loginKind !== "github" && loginKind !== "google") {
793
- throw new Error('product config loginKind must be "github" or "google"');
794
- }
795
- const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
796
- if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
797
- publicKeyBytes(config);
798
- if (loginKind === "github") {
799
- const clientId = record.githubClientId;
800
- if (typeof clientId !== "string" || clientId.length === 0) {
801
- throw new Error("product config needs githubClientId for the github loginKind");
802
- }
803
- config.githubClientId = clientId;
804
- } else if (typeof record.githubClientId === "string") {
805
- config.githubClientId = record.githubClientId;
806
- }
807
- try {
808
- const url = new URL(host);
809
- if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
810
- } catch {
811
- throw new Error("product config host must be an http(s) URL");
812
- }
813
- return config;
1452
+ function valueOf(output, key) {
1453
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
1454
+ if (!line) return null;
1455
+ const value = line.slice(line.indexOf(key) + key.length).trim();
1456
+ return value ? value : null;
814
1457
  }
815
- function field(record, key) {
816
- const value = record[key];
817
- return typeof value === "string" ? value : "";
1458
+ function firstLine(text) {
1459
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
818
1460
  }
819
1461
 
820
1462
  // src/login-github.ts
821
1463
  import { spawn } from "node:child_process";
822
- var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1464
+ var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
823
1465
  function openBrowser(url) {
824
1466
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
825
1467
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
@@ -939,7 +1581,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
939
1581
  }
940
1582
 
941
1583
  // src/login-google.ts
942
- import { createHash, randomBytes } from "node:crypto";
1584
+ import { createHash as createHash4, randomBytes } from "node:crypto";
943
1585
  import { createServer } from "node:http";
944
1586
  var b64url = (bytes) => bytes.toString("base64url");
945
1587
  async function loginGoogle(options) {
@@ -947,7 +1589,7 @@ async function loginGoogle(options) {
947
1589
  const server = options.host.replace(/\/+$/, "");
948
1590
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
949
1591
  const listener = createServer();
950
- await new Promise((resolve2) => listener.listen(0, "127.0.0.1", resolve2));
1592
+ await new Promise((resolve3) => listener.listen(0, "127.0.0.1", resolve3));
951
1593
  const port = listener.address().port;
952
1594
  const redirectUri = `http://127.0.0.1:${port}/callback`;
953
1595
  try {
@@ -962,7 +1604,7 @@ async function loginGoogle(options) {
962
1604
  throw new Error("the sign-in server returned a malformed registration");
963
1605
  }
964
1606
  const verifier = b64url(randomBytes(32));
965
- const challenge = b64url(createHash("sha256").update(verifier).digest());
1607
+ const challenge = b64url(createHash4("sha256").update(verifier).digest());
966
1608
  const state = b64url(randomBytes(16));
967
1609
  const authorize = new URL(`${server}/oauth/authorize`);
968
1610
  authorize.search = new URLSearchParams({
@@ -974,7 +1616,7 @@ async function loginGoogle(options) {
974
1616
  code_challenge: challenge,
975
1617
  code_challenge_method: "S256"
976
1618
  }).toString();
977
- const code = await new Promise((resolve2, reject) => {
1619
+ const code = await new Promise((resolve3, reject) => {
978
1620
  const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
979
1621
  listener.on("request", (req, res) => {
980
1622
  const url = new URL(req.url ?? "/", redirectUri);
@@ -992,7 +1634,7 @@ async function loginGoogle(options) {
992
1634
  }
993
1635
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Signed in.", "You can close this tab and go back to the terminal."));
994
1636
  clearTimeout(timer);
995
- resolve2(received);
1637
+ resolve3(received);
996
1638
  });
997
1639
  const print = options.print ?? ((line) => process.stderr.write(`${line}
998
1640
  `));
@@ -1051,245 +1693,8 @@ function page(title, body) {
1051
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>`;
1052
1694
  }
1053
1695
 
1054
- // src/payload.ts
1055
- import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
1056
- import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
1057
- import { tmpdir as tmpdir2 } from "node:os";
1058
- import { dirname, join as join2 } from "node:path";
1059
-
1060
- // src/canonical.ts
1061
- function canonicalJson(value) {
1062
- return encode(value);
1063
- }
1064
- function encode(value) {
1065
- if (value === null) return "null";
1066
- if (typeof value === "string") return JSON.stringify(value);
1067
- if (typeof value === "boolean") return value ? "true" : "false";
1068
- if (typeof value === "number") {
1069
- if (!Number.isFinite(value)) {
1070
- throw new TypeError("canonicalJson: cannot encode a non-finite number");
1071
- }
1072
- return JSON.stringify(value);
1073
- }
1074
- if (Array.isArray(value)) {
1075
- return `[${value.map((entry) => encode(entry)).join(",")}]`;
1076
- }
1077
- if (typeof value === "object") {
1078
- const record = value;
1079
- const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
1080
- return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
1081
- }
1082
- throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
1083
- }
1084
-
1085
- // src/payload.ts
1086
- var NeedsLoginError = class extends Error {
1087
- constructor() {
1088
- super("signed out \u2014 run login first");
1089
- this.name = "NeedsLoginError";
1090
- }
1091
- };
1092
- var ForbiddenError = class extends Error {
1093
- constructor() {
1094
- super("this install is not allowed for your account \u2014 access was revoked or never granted.");
1095
- this.name = "ForbiddenError";
1096
- }
1097
- };
1098
- function canonicalManifestBytes(manifest) {
1099
- return Buffer.from(
1100
- canonicalJson({
1101
- created: manifest.created,
1102
- files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
1103
- version: manifest.version
1104
- }),
1105
- "utf8"
1106
- );
1107
- }
1108
- function ed25519PublicKey(config) {
1109
- const raw = publicKeyBytes(config);
1110
- return createPublicKey({
1111
- key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
1112
- format: "jwk"
1113
- });
1114
- }
1115
- function verifyManifest(manifest, signature, config) {
1116
- try {
1117
- const signatureBytes = Buffer.from(signature, "base64");
1118
- if (signatureBytes.length === 0) return false;
1119
- return verify(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
1120
- } catch {
1121
- return false;
1122
- }
1123
- }
1124
- function verifyFileBytes(entry, bytes) {
1125
- if (entry.size !== bytes.length) return false;
1126
- return createHash2("sha256").update(bytes).digest("hex") === entry.sha256.toLowerCase();
1127
- }
1128
- async function readErrorCode(response) {
1129
- try {
1130
- const data = await response.json();
1131
- return typeof data.error === "string" ? data.error : "";
1132
- } catch {
1133
- return "";
1134
- }
1135
- }
1136
- function throwForStatus(status, errorCode) {
1137
- if (status === 401) throw new NeedsLoginError();
1138
- if (status === 403) throw new ForbiddenError();
1139
- throw new Error(
1140
- errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
1141
- );
1142
- }
1143
- async function getJson(url, accessToken, fetchImpl) {
1144
- const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
1145
- if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
1146
- return { status: response.status, json: await response.json() };
1147
- }
1148
- function parseManifest(json) {
1149
- if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
1150
- const record = json;
1151
- if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
1152
- if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
1153
- if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
1154
- if (typeof record.signature !== "string" || !record.signature) {
1155
- throw new Error("the release manifest is unsigned");
1156
- }
1157
- const files = record.files.map((entry) => {
1158
- const file = entry;
1159
- if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
1160
- throw new Error("the release manifest lists a malformed file");
1161
- }
1162
- const safe = safeManifestPath(file.path);
1163
- if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
1164
- return { path: safe, sha256: file.sha256, size: file.size };
1165
- });
1166
- return { version: record.version, created: record.created, files, signature: record.signature };
1167
- }
1168
- function safeManifestPath(rawPath) {
1169
- if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
1170
- if (rawPath.startsWith("/")) return null;
1171
- const segments = rawPath.split("/");
1172
- for (const segment of segments) {
1173
- if (segment === "" || segment === "." || segment === "..") return null;
1174
- }
1175
- return segments.join("/");
1176
- }
1177
- async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
1178
- const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
1179
- const manifest = parseManifest(json);
1180
- if (!verifyManifest(manifest, manifest.signature, config)) {
1181
- throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
1182
- }
1183
- return manifest;
1184
- }
1185
- async function fetchFileBytes(config, accessToken, path, fetchImpl) {
1186
- const encoded = path.split("/").map(encodeURIComponent).join("/");
1187
- const response = await fetchImpl(`${config.host}/release/${encoded}`, {
1188
- headers: { authorization: `Bearer ${accessToken}` }
1189
- });
1190
- if (!response.ok) {
1191
- if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
1192
- throwForStatus(response.status, await readErrorCode(response));
1193
- }
1194
- return Buffer.from(await response.arrayBuffer());
1195
- }
1196
- async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
1197
- const fetchImpl = options.fetchImpl ?? fetch;
1198
- const staging = join2(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
1199
- mkdirSync2(staging, { recursive: true });
1200
- try {
1201
- for (const entry of manifest.files) {
1202
- const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
1203
- if (!verifyFileBytes(entry, bytes)) {
1204
- throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
1205
- }
1206
- const dest = join2(staging, entry.path);
1207
- mkdirSync2(dirname(dest), { recursive: true });
1208
- writeFileSync3(dest, bytes);
1209
- }
1210
- const target = join2(dir, "payload");
1211
- mkdirSync2(dir, { recursive: true });
1212
- rmSync2(target, { force: true, recursive: true });
1213
- renameSync(staging, target);
1214
- } catch (error) {
1215
- rmSync2(staging, { force: true, recursive: true });
1216
- throw error;
1217
- }
1218
- return manifest.version;
1219
- }
1220
-
1221
- // src/store.ts
1222
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
1223
- import { tmpdir as tmpdir3 } from "node:os";
1224
- import { join as join3 } from "node:path";
1225
- function defaultProductDir(product) {
1226
- if (process.platform === "win32") {
1227
- const base = process.env.LOCALAPPDATA ?? join3(tmpdir3(), "launcher-fallback");
1228
- return join3(base, product);
1229
- }
1230
- const home = process.env.HOME ?? tmpdir3();
1231
- return join3(home, `.${product}`);
1232
- }
1233
- function resolveProductDir(product, explicit) {
1234
- return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
1235
- }
1236
- function tokensPath(dir) {
1237
- return join3(dir, "tokens.json");
1238
- }
1239
- function statePath(dir) {
1240
- return join3(dir, "state.json");
1241
- }
1242
- function payloadDir(dir) {
1243
- return join3(dir, "payload");
1244
- }
1245
- function readTokens(dir) {
1246
- try {
1247
- const data = JSON.parse(readFileSync4(tokensPath(dir), "utf8"));
1248
- if (typeof data.accessToken !== "string" || !data.accessToken) return null;
1249
- if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
1250
- const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
1251
- if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
1252
- if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
1253
- return tokens;
1254
- } catch {
1255
- return null;
1256
- }
1257
- }
1258
- function writeTokens(dir, tokens) {
1259
- mkdirSync3(dir, { recursive: true });
1260
- try {
1261
- writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
1262
- `, { mode: 384 });
1263
- } catch {
1264
- writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
1265
- `);
1266
- }
1267
- }
1268
- function clearTokens(dir) {
1269
- rmSync3(tokensPath(dir), { force: true });
1270
- }
1271
- function readState(dir) {
1272
- try {
1273
- const data = JSON.parse(readFileSync4(statePath(dir), "utf8"));
1274
- if (typeof data.version !== "string" || !data.version) return null;
1275
- return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
1276
- } catch {
1277
- return null;
1278
- }
1279
- }
1280
- function writeState(dir, state) {
1281
- mkdirSync3(dir, { recursive: true });
1282
- writeFileSync4(statePath(dir), `${JSON.stringify(state, null, 2)}
1283
- `);
1284
- }
1285
- function wipeProductDir(dir) {
1286
- rmSync3(tokensPath(dir), { force: true });
1287
- rmSync3(statePath(dir), { force: true });
1288
- rmSync3(payloadDir(dir), { force: true, recursive: true });
1289
- }
1290
-
1291
1696
  // src/index.ts
1292
- var LAUNCHER_VERSION = true ? "0.1.12" : readVersionFromPackage();
1697
+ var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
1293
1698
  function defaultPrint(message) {
1294
1699
  process.stdout.write(`${message}
1295
1700
  `);
@@ -1301,30 +1706,37 @@ function defaultPrintErr(message) {
1301
1706
  async function run(rawOptions = {}) {
1302
1707
  const print = rawOptions.print ?? defaultPrint;
1303
1708
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1304
- const argv = rawOptions.argv ?? process.argv.slice(2);
1305
- const runAt = argv.indexOf("--run");
1306
- if (runAt >= 0) return runFile(argv[runAt + 1], argv.slice(runAt + 2), printErr);
1709
+ const argv2 = rawOptions.argv ?? process.argv.slice(2);
1710
+ const runAt = argv2.indexOf("--run");
1711
+ if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
1307
1712
  let config;
1308
1713
  try {
1309
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv, "--config") });
1714
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
1310
1715
  } catch (error) {
1311
1716
  print(`cannot start: ${error.message}`);
1312
1717
  return 2;
1313
1718
  }
1314
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv, "--dir"));
1315
- const positional = argv.filter((arg) => !arg.startsWith("-"));
1316
- if (argv.includes("--version") || argv.includes("-v")) {
1719
+ const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1720
+ const positional = argv2.filter((arg) => !arg.startsWith("-"));
1721
+ if (argv2.includes("--version") || argv2.includes("-v")) {
1317
1722
  print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
1318
1723
  return 0;
1319
1724
  }
1320
- if (argv.includes("--help") || argv.includes("-h") || positional.length === 0) {
1725
+ if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1321
1726
  printUsage(config, print);
1322
1727
  return positional.length === 0 ? 2 : 0;
1323
1728
  }
1324
- const command = positional[0];
1729
+ const command2 = positional[0];
1730
+ let unlock;
1325
1731
  try {
1326
- switch (command) {
1732
+ unlock = lockInstallation(dir);
1733
+ await recoverAcquisition(dir, async (command3, cwd, env) => {
1734
+ const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1735
+ return result.ok && (result.code === void 0 || result.code === 0);
1736
+ }, payloadEnv(dir) ?? process.env);
1737
+ switch (command2) {
1327
1738
  case "login":
1739
+ unlock();
1328
1740
  await doLogin(config, dir, rawOptions, print);
1329
1741
  return 0;
1330
1742
  case "logout":
@@ -1336,11 +1748,11 @@ async function run(rawOptions = {}) {
1336
1748
  case "update":
1337
1749
  return await doUpdate(config, dir, rawOptions, print);
1338
1750
  case "doctor":
1339
- return doDoctor(config, dir, rawOptions, print);
1751
+ return doDoctor(config, dir, rawOptions, print, unlock);
1340
1752
  case "autoupdate":
1341
1753
  return doAutoupdate(config, rawOptions, positional.slice(1), print);
1342
1754
  default:
1343
- return doForward(config, dir, rawOptions, command, argv, print);
1755
+ return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
1344
1756
  }
1345
1757
  } catch (error) {
1346
1758
  if (error instanceof NeedsLoginError) {
@@ -1349,12 +1761,14 @@ async function run(rawOptions = {}) {
1349
1761
  }
1350
1762
  print(`failed: ${error.message}`);
1351
1763
  return 1;
1764
+ } finally {
1765
+ unlock?.();
1352
1766
  }
1353
1767
  }
1354
- function flagValue(argv, flag) {
1355
- const index = argv.indexOf(flag);
1768
+ function flagValue(argv2, flag) {
1769
+ const index = argv2.indexOf(flag);
1356
1770
  if (index < 0) return void 0;
1357
- const value = argv[index + 1];
1771
+ const value = argv2[index + 1];
1358
1772
  return value && !value.startsWith("-") ? value : void 0;
1359
1773
  }
1360
1774
  function printUsage(config, print) {
@@ -1368,8 +1782,8 @@ async function runFile(file, args, printErr) {
1368
1782
  printErr("launcher --run needs a file to run.");
1369
1783
  return 1;
1370
1784
  }
1371
- const abs = resolve(process.cwd(), file);
1372
- if (!existsSync4(abs)) {
1785
+ const abs = resolve2(process.cwd(), file);
1786
+ if (!existsSync6(abs)) {
1373
1787
  printErr(`cannot run ${file}: no such file.`);
1374
1788
  return 1;
1375
1789
  }
@@ -1462,14 +1876,14 @@ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1462
1876
  }
1463
1877
  function readPayloadArgv(dir, key) {
1464
1878
  try {
1465
- const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
1466
- const entry = parsed[key];
1467
- if (typeof entry === "string") {
1468
- const parts = entry.trim().split(/\s+/).filter(Boolean);
1879
+ const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
1880
+ const entry2 = parsed[key];
1881
+ if (typeof entry2 === "string") {
1882
+ const parts = entry2.trim().split(/\s+/).filter(Boolean);
1469
1883
  return parts.length > 0 ? parts : null;
1470
1884
  }
1471
- if (Array.isArray(entry) && entry.every((part) => typeof part === "string" && part.length > 0)) {
1472
- return entry;
1885
+ if (Array.isArray(entry2) && entry2.every((part) => typeof part === "string" && part.length > 0)) {
1886
+ return entry2;
1473
1887
  }
1474
1888
  return null;
1475
1889
  } catch {
@@ -1484,7 +1898,7 @@ function readPayloadRun(dir) {
1484
1898
  }
1485
1899
  function readPayloadVerbs(dir) {
1486
1900
  try {
1487
- const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
1901
+ const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
1488
1902
  const verbs = parsed.verbs;
1489
1903
  if (verbs === "*") return "*";
1490
1904
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1495,13 +1909,13 @@ function readPayloadVerbs(dir) {
1495
1909
  return null;
1496
1910
  }
1497
1911
  }
1498
- function resolveEntry(entry) {
1499
- return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
1912
+ function resolveEntry(entry2) {
1913
+ return entry2[0] === "$self" ? [process.execPath, ...entry2.slice(1)] : entry2;
1500
1914
  }
1501
- function needsShell(command) {
1915
+ function needsShell(command2) {
1502
1916
  if (process.platform !== "win32") return false;
1503
- if (/\.(cmd|bat)$/i.test(command)) return true;
1504
- return !existsSync4(command);
1917
+ if (/\.(cmd|bat)$/i.test(command2)) return true;
1918
+ return !existsSync6(command2);
1505
1919
  }
1506
1920
  function quoteForShell(arg) {
1507
1921
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1516,23 +1930,24 @@ function parseProgress(raw) {
1516
1930
  const message = JSON.parse(line);
1517
1931
  if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
1518
1932
  const state = message.state ?? "ok";
1519
- if (state !== "ok" && state !== "fail" && state !== "note") continue;
1933
+ if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
1520
1934
  if (message.ms !== void 0 && typeof message.ms !== "number") continue;
1521
1935
  progress.push({
1522
1936
  step: message.step,
1523
1937
  state,
1524
- ...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 } : {}
1525
1940
  });
1526
1941
  } catch {
1527
1942
  }
1528
1943
  }
1529
1944
  return progress;
1530
1945
  }
1531
- function defaultRunEntry(entry, cwd, env) {
1532
- const [command, ...args] = entry;
1533
- const shell = needsShell(command);
1534
- const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1535
- const spawnEntry = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
1946
+ function defaultRunEntry(entry2, cwd, env) {
1947
+ const [command2, ...args] = entry2;
1948
+ const shell = needsShell(command2);
1949
+ const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
1950
+ const spawnEntry = (progress2) => spawnSync3(commandLine, shell ? [] : args, {
1536
1951
  cwd,
1537
1952
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1538
1953
  shell,
@@ -1590,11 +2005,49 @@ async function installOrUpdate(config, dir, options, print, update) {
1590
2005
  version = manifest.version;
1591
2006
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
1592
2007
  const current = readState(dir);
1593
- const unchanged = update && current?.version === version && existsSync4(payloadDir(dir));
2008
+ const unchanged = current?.version === version && existsSync6(payloadDir(dir));
2009
+ if (unchanged && current.acquired) {
2010
+ installer.surface({ id: config.product, from: version, to: version, state: "current" });
2011
+ return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
2012
+ }
1594
2013
  if (!unchanged) {
1595
2014
  installer.phase("download", { state: "running" });
1596
2015
  started = Date.now();
1597
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
2016
+ const cached = reusableCandidate(dir, manifest);
2017
+ const candidate = cached ?? randomUUID4();
2018
+ const candidateDir = join6(dir, "candidates", candidate);
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 });
2024
+ const acquisition = existsSync6(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
2025
+ if (acquisition) {
2026
+ rememberCandidate(dir, candidate, manifest);
2027
+ installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2028
+ installer.phase("activate", { state: "running" });
2029
+ let outcome;
2030
+ const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
2031
+ await installAcquisition(dir, candidate, version, acquisition, {
2032
+ fetchImpl,
2033
+ env,
2034
+ onProgress,
2035
+ run: async (command2, cwd, childEnv) => {
2036
+ installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
2037
+ const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
2038
+ if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
2039
+ return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
2040
+ }
2041
+ });
2042
+ installer.phase("activate");
2043
+ installer.surface({ id: config.product, from: current?.version, to: version, state: "updated" });
2044
+ installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2045
+ return 0;
2046
+ }
2047
+ const target = join6(dir, "payload");
2048
+ mkdirSync6(dir, { recursive: true });
2049
+ rmSync5(target, { recursive: true, force: true });
2050
+ renameSync4(join6(candidateDir, "payload"), target);
1598
2051
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1599
2052
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
1600
2053
  }
@@ -1632,12 +2085,12 @@ function payloadEnv(dir) {
1632
2085
  return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1633
2086
  }
1634
2087
  var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1635
- function payloadFileAsCommand(entry, payload) {
1636
- const first = entry[0];
2088
+ function payloadFileAsCommand(entry2, payload) {
2089
+ const first = entry2[0];
1637
2090
  if (!first || first === "$self") return null;
1638
2091
  if (first.includes("/") || first.includes("\\")) return null;
1639
- const candidate = join4(payload, first);
1640
- if (!existsSync4(candidate)) return null;
2092
+ const candidate = join6(payload, first);
2093
+ if (!existsSync6(candidate)) return null;
1641
2094
  if (NEVER_A_PROGRAM.test(first)) return first;
1642
2095
  if (process.platform === "win32") return null;
1643
2096
  try {
@@ -1646,22 +2099,22 @@ function payloadFileAsCommand(entry, payload) {
1646
2099
  return null;
1647
2100
  }
1648
2101
  }
1649
- async function finishLastMile(config, dir, options, installer, version, unchanged) {
1650
- const entry = readPayloadEntry(dir);
2102
+ async function finishLastMile(config, dir, options, installer, version, unchanged, repairCommand) {
2103
+ const entry2 = repairCommand ?? readPayloadEntry(dir);
1651
2104
  const payload = payloadDir(dir);
1652
- if (!entry) {
2105
+ if (!entry2) {
1653
2106
  installer.finish({
1654
2107
  version,
1655
2108
  total: 1,
1656
2109
  updated: unchanged ? 0 : 1,
1657
2110
  failed: 0,
1658
2111
  installed: true,
1659
- detail: `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
2112
+ detail: `next step: run ${join6(payload, config.binName)} to start ${config.product}.`
1660
2113
  });
1661
2114
  return 0;
1662
2115
  }
1663
- const command = resolveEntry(entry);
1664
- const dataFile = payloadFileAsCommand(entry, payload);
2116
+ const command2 = resolveEntry(entry2);
2117
+ const dataFile = payloadFileAsCommand(entry2, payload);
1665
2118
  if (dataFile) {
1666
2119
  installer.finish({
1667
2120
  version,
@@ -1674,8 +2127,9 @@ async function finishLastMile(config, dir, options, installer, version, unchange
1674
2127
  }
1675
2128
  installer.phase("activate", { state: "running" });
1676
2129
  const started = Date.now();
1677
- const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
1678
- const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
2130
+ const inherited = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
2131
+ const env = repairCommand ? runtimeEnvironment(repairCommand[0], inherited) : inherited;
2132
+ const result = options.runEntry ? options.runEntry(command2, payload, env) : await runInstallEntry(command2, payload, env, installer, readTokens(dir));
1679
2133
  for (const progress of result.progress ?? []) installer.milestone(progress);
1680
2134
  const succeeded = result.ok && (result.code === void 0 || result.code === 0);
1681
2135
  const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
@@ -1690,28 +2144,33 @@ async function finishLastMile(config, dir, options, installer, version, unchange
1690
2144
  },
1691
2145
  ...!succeeded ? {
1692
2146
  operationFailed: true,
1693
- detail: result.code === void 0 ? `Arming this machine could not start: ${result.error ?? command[0]}` : `Arming this machine did not finish (exit code ${result.code})${result.error?.startsWith("installer outcome:") ? `: ${result.error}` : ""}`,
1694
- retry: `(cd ${payload} && ${command.join(" ")})`
2147
+ detail: result.code === void 0 ? `Arming this machine could not start: ${result.error ?? command2[0]}` : `Arming this machine did not finish (exit code ${result.code})${result.error?.startsWith("installer outcome:") ? `: ${result.error}` : ""}`,
2148
+ retry: `(cd ${payload} && ${command2.join(" ")})`
1695
2149
  } : {}
1696
2150
  });
1697
2151
  return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
1698
2152
  }
1699
- async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1700
- const [command, ...args] = entry;
1701
- const shell = needsShell(command);
2153
+ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2154
+ installer.start();
2155
+ const [command2, ...args] = entry2;
2156
+ const shell = needsShell(command2);
1702
2157
  const progress = !(process.platform === "win32" && shell);
1703
- const outcomeDir = mkdtempSync(join4(tmpdir4(), "mm-installer-outcome-"));
1704
- const outcomeFile = join4(outcomeDir, "outcome.json");
2158
+ const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
2159
+ const outcomeFile = join6(outcomeDir, "outcome.json");
2160
+ const progressFile = join6(outcomeDir, "progress.jsonl");
1705
2161
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
1706
2162
  delete childEnv.MM_FACE_TRANSCRIPT;
2163
+ delete childEnv.MM_PROGRESS_FILE;
1707
2164
  delete childEnv.MM_PROGRESS_FD;
1708
2165
  delete childEnv.MM_PROGRESS_PROTOCOL;
1709
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" });
1710
2168
  const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
2169
+ let progressError;
1711
2170
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
1712
2171
  try {
1713
- const result = await new Promise((resolve2) => {
1714
- const child = spawn2(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
2172
+ const result = await new Promise((resolve3) => {
2173
+ const child = spawn2(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
1715
2174
  cwd,
1716
2175
  shell,
1717
2176
  windowsHide: true,
@@ -1728,32 +2187,53 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1728
2187
  }
1729
2188
  partial = held ? safe.slice(-held) : "";
1730
2189
  const visible = held ? safe.slice(0, -held) : safe;
1731
- if (visible) installer.relay(visible, channel2, false);
2190
+ if (visible) installer.relayChunk(visible, channel2);
1732
2191
  }).on("end", () => {
1733
- if (partial) installer.relay("[redacted]", channel2, false);
2192
+ if (partial) installer.relayChunk("[redacted]", channel2);
1734
2193
  });
1735
2194
  }
1736
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
+ });
1737
2224
  const channel = child.stdio[3];
1738
2225
  if (channel && "setEncoding" in channel) {
1739
2226
  channel.setEncoding("utf8");
1740
- channel.on("data", (text) => {
1741
- pending += text;
1742
- const end = pending.lastIndexOf("\n");
1743
- if (end >= 0) {
1744
- for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
1745
- pending = pending.slice(end + 1);
1746
- }
1747
- if (pending.length > 65536) pending = "";
1748
- });
2227
+ channel.on("data", receive);
1749
2228
  }
1750
- child.on("error", (error) => resolve2({ ok: false, error: error.message }));
1751
- child.on("close", (code) => resolve2({
2229
+ child.on("error", (error) => resolve3({ ok: false, error: error.message }));
2230
+ child.on("close", (code) => resolve3({
1752
2231
  ok: code === 0,
1753
2232
  ...code !== null ? { code } : {},
1754
2233
  ...code !== 0 ? { error: `exit code ${code}` } : {}
1755
2234
  }));
1756
2235
  });
2236
+ if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
1757
2237
  try {
1758
2238
  const outcome = readInstallerOutcome(outcomeFile);
1759
2239
  return { ...result, ...outcome ? { outcome } : {} };
@@ -1761,25 +2241,31 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1761
2241
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
1762
2242
  }
1763
2243
  } finally {
1764
- if (existsSync4(outcomeFile)) unlinkSync(outcomeFile);
2244
+ if (existsSync6(progressFile)) unlinkSync2(progressFile);
2245
+ if (existsSync6(outcomeFile)) unlinkSync2(outcomeFile);
1765
2246
  rmdirSync(outcomeDir);
1766
2247
  }
1767
2248
  }
1768
- function doForward(config, dir, options, command, argv, print) {
1769
- if (command && existsSync4(command)) {
1770
- print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
2249
+ function doForward(config, dir, options, command2, argv2, print, unlock) {
2250
+ if (command2 && existsSync6(command2)) {
2251
+ print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
1771
2252
  return 2;
1772
2253
  }
1773
- const target = readPayloadRun(dir);
2254
+ const acquired = readState(dir)?.acquired;
2255
+ const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
1774
2256
  const verbs = readPayloadVerbs(dir);
1775
- const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
2257
+ const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
1776
2258
  if (!target || !declared) {
1777
- print(`unknown command: ${command}`);
2259
+ print(`unknown command: ${command2}`);
1778
2260
  printUsage(config, print);
1779
2261
  return 2;
1780
2262
  }
1781
- const forwarded = [...resolveEntry(target), ...argv];
1782
- const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
2263
+ const forwarded = [...resolveEntry(target), ...argv2];
2264
+ const cwd = payloadDir(dir);
2265
+ const inherited = payloadEnv(dir);
2266
+ const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2267
+ unlock();
2268
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
1783
2269
  if (!result.ok && result.code === void 0) {
1784
2270
  print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1785
2271
  return 1;
@@ -1789,10 +2275,10 @@ function doForward(config, dir, options, command, argv, print) {
1789
2275
  function doAutoupdate(config, options, args, print) {
1790
2276
  const mode = args[0] ?? "status";
1791
2277
  const scheduleOptions = options.autoupdate ?? {};
1792
- const command = options.autoupdate?.command ?? [process.execPath, "update"];
2278
+ const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
1793
2279
  if (mode === "on") {
1794
2280
  try {
1795
- enableSchedule(config, command, scheduleOptions);
2281
+ enableSchedule(config, command2, scheduleOptions);
1796
2282
  } catch (error) {
1797
2283
  print(error.message);
1798
2284
  return 1;
@@ -1824,19 +2310,24 @@ function doAutoupdate(config, options, args, print) {
1824
2310
  if (state.lastRun) print(`last run: ${state.lastRun}`);
1825
2311
  return 0;
1826
2312
  }
1827
- function doDoctor(config, dir, options, print) {
2313
+ function doDoctor(config, dir, options, print, unlock) {
1828
2314
  doLauncherDoctor(config, dir, options, print);
1829
- const target = readPayloadRun(dir);
2315
+ const acquired = readState(dir)?.acquired;
2316
+ const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
1830
2317
  const verbs = readPayloadVerbs(dir);
1831
2318
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1832
2319
  if (!chains) return 0;
1833
- const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
2320
+ const cwd = payloadDir(dir);
2321
+ const inherited = payloadEnv(dir);
2322
+ const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2323
+ unlock();
2324
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
1834
2325
  return result.code ?? (result.ok ? 0 : 1);
1835
2326
  }
1836
2327
  function doLauncherDoctor(config, dir, options, print) {
1837
2328
  const tokens = readTokens(dir);
1838
2329
  const state = readState(dir);
1839
- const payloadPresent = existsSync4(payloadDir(dir));
2330
+ const payloadPresent = existsSync6(payloadDir(dir));
1840
2331
  print(`product: ${config.product}`);
1841
2332
  print(`host: ${config.host}`);
1842
2333
  print(`login: ${config.loginKind}`);
@@ -1848,7 +2339,7 @@ function doLauncherDoctor(config, dir, options, print) {
1848
2339
  print("token: none \u2014 run login first.");
1849
2340
  }
1850
2341
  print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
1851
- print(`paths: tokens ${join4(dir, "tokens.json")}, state ${join4(dir, "state.json")}, payload ${payloadDir(dir)}`);
2342
+ print(`paths: tokens ${join6(dir, "tokens.json")}, state ${join6(dir, "state.json")}, payload ${payloadDir(dir)}`);
1852
2343
  const schedule = querySchedule(config, options.autoupdate ?? {});
1853
2344
  print(
1854
2345
  `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`