@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.
@@ -32,43 +32,674 @@ __export(index_exports, {
32
32
  runInstallEntry: () => runInstallEntry
33
33
  });
34
34
  module.exports = __toCommonJS(index_exports);
35
- var import_node_child_process3 = require("node:child_process");
36
- var import_node_fs9 = require("node:fs");
37
- var import_node_os4 = require("node:os");
35
+ var import_node_string_decoder = require("node:string_decoder");
36
+ var import_node_crypto6 = require("node:crypto");
37
+
38
+ // src/acquisition.ts
39
+ var import_node_crypto4 = require("node:crypto");
40
+ var import_node_fs5 = require("node:fs");
38
41
  var import_node_path4 = require("node:path");
42
+
43
+ // src/runtime.ts
44
+ var import_node_crypto = require("node:crypto");
45
+ var import_node_child_process = require("node:child_process");
46
+ var import_node_fs = require("node:fs");
47
+ var import_node_path = require("node:path");
48
+
49
+ // src/download.ts
50
+ async function readDownload(response, onProgress, expectedSize) {
51
+ const length = expectedSize ?? Number(response.headers.get("content-length"));
52
+ const total = Number.isSafeInteger(length) && length > 0 ? length : void 0;
53
+ let done = 0;
54
+ const report = () => onProgress?.({ done, ...total === void 0 ? {} : { total } });
55
+ report();
56
+ const chunks = [];
57
+ if (response.body) {
58
+ for await (const chunk of response.body) {
59
+ chunks.push(chunk);
60
+ done += chunk.length;
61
+ report();
62
+ }
63
+ }
64
+ return Buffer.concat(chunks);
65
+ }
66
+
67
+ // src/runtime.ts
68
+ var NODE_VERSION = "24.20.0";
69
+ var NPM_VERSION = "12.0.2";
70
+ var ARTIFACTS = {
71
+ "win32-x64": ["node-v24.20.0-win-x64.zip", "6cac9ffbca8f6a47091e4b5c772e0606049c3871cb67d900c0cedde630e545ba"],
72
+ "win32-arm64": ["node-v24.20.0-win-arm64.zip", "31c6799744de8a54601643098040c68c3697e56c94e407d61d0e5fa5f34191d7"],
73
+ "darwin-arm64": ["node-v24.20.0-darwin-arm64.tar.gz", "40e5607e5ecb3db9192723776da2d75d966260fc74a7a9e731c1bd67dda96bc8"],
74
+ "linux-x64": ["node-v24.20.0-linux-x64.tar.gz", "855d581f8a4eb1a8117e3426de25fe02770592febcfb31369aee1ffbfee9e8ec"],
75
+ "linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
76
+ };
77
+ var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
78
+ async function verifiedDownload(url, path, algorithm, digest, fetchImpl, onProgress) {
79
+ const response = await fetchImpl(url);
80
+ if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
81
+ const bytes = await readDownload(response, onProgress);
82
+ if ((0, import_node_crypto.createHash)(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
83
+ (0, import_node_fs.writeFileSync)(path, bytes);
84
+ }
85
+ function command(executable, args) {
86
+ const result = (0, import_node_child_process.spawnSync)(executable, args, { encoding: "utf8", windowsHide: true });
87
+ if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
88
+ return result.stdout.trim();
89
+ }
90
+ async function acquireRuntime(dir, fetchImpl = fetch, onProgress) {
91
+ const platform = `${process.platform}-${process.arch}`;
92
+ const artifact = ARTIFACTS[platform];
93
+ if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
94
+ const runtimeDir = (0, import_node_path.join)(dir, "runtimes");
95
+ (0, import_node_fs.mkdirSync)(runtimeDir, { recursive: true });
96
+ const receipt = (0, import_node_path.join)(runtimeDir, `node-${NODE_VERSION}-npm-${NPM_VERSION}-${platform}.json`);
97
+ if ((0, import_node_fs.existsSync)(receipt)) {
98
+ const runtime2 = JSON.parse((0, import_node_fs.readFileSync)(receipt, "utf8"));
99
+ for (const executable of [runtime2.node, runtime2.npm]) {
100
+ const rel = (0, import_node_path.relative)((0, import_node_fs.realpathSync)(runtimeDir), (0, import_node_fs.realpathSync)(executable));
101
+ if (rel.startsWith("..") || (0, import_node_path.isAbsolute)(rel)) throw new Error("cached runtime escapes owned directory");
102
+ }
103
+ if (command(runtime2.node, ["--version"]) === `v${NODE_VERSION}` && command(runtime2.node, [runtime2.npm, "--version"]) === NPM_VERSION) return runtime2;
104
+ throw new Error("cached runtime validation failed");
105
+ }
106
+ const root = (0, import_node_fs.mkdtempSync)((0, import_node_path.join)(runtimeDir, "runtime-"));
107
+ const archive = (0, import_node_path.join)(root, artifact[0]);
108
+ let downloaded = 0;
109
+ let currentBytes = 0;
110
+ const progress = ({ done }) => {
111
+ currentBytes = done;
112
+ onProgress?.({ done: downloaded + done });
113
+ };
114
+ await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl, progress);
115
+ downloaded += currentBytes;
116
+ const tar = process.platform === "win32" ? (0, import_node_path.join)(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
117
+ command(tar, ["-xf", archive, "-C", root]);
118
+ const unpacked = (0, import_node_path.join)(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
119
+ const node = (0, import_node_path.join)(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
120
+ const npmArchive = (0, import_node_path.join)(root, "npm.tgz");
121
+ await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl, progress);
122
+ const npmRoot = (0, import_node_path.join)(root, "npm");
123
+ (0, import_node_fs.mkdirSync)(npmRoot);
124
+ command(tar, ["-xf", npmArchive, "-C", npmRoot]);
125
+ const installedNpm = (0, import_node_path.join)(unpacked, process.platform === "win32" ? "node_modules/npm" : "lib/node_modules/npm");
126
+ if ((0, import_node_fs.existsSync)(installedNpm)) (0, import_node_fs.renameSync)(installedNpm, (0, import_node_path.join)(root, "npm-bundled"));
127
+ (0, import_node_fs.renameSync)((0, import_node_path.join)(npmRoot, "package"), installedNpm);
128
+ const npm = (0, import_node_path.join)(installedNpm, "bin/npm-cli.js");
129
+ if (command(node, ["--version"]) !== `v${NODE_VERSION}` || command(node, [npm, "--version"]) !== NPM_VERSION) throw new Error("downloaded runtime validation failed");
130
+ const runtime = { node, npm };
131
+ const temporary = `${receipt}.${(0, import_node_crypto.randomUUID)()}.tmp`;
132
+ try {
133
+ (0, import_node_fs.writeFileSync)(temporary, JSON.stringify(runtime), { flag: "wx", mode: 384, flush: true });
134
+ (0, import_node_fs.renameSync)(temporary, receipt);
135
+ } finally {
136
+ (0, import_node_fs.rmSync)(temporary, { force: true });
137
+ }
138
+ return runtime;
139
+ }
140
+
141
+ // src/store.ts
142
+ var import_node_fs2 = require("node:fs");
143
+ var import_node_crypto2 = require("node:crypto");
144
+ var import_node_os = require("node:os");
145
+ var import_node_path2 = require("node:path");
146
+ function defaultProductDir(product) {
147
+ if (process.platform === "win32") {
148
+ const base = process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os.tmpdir)(), "launcher-fallback");
149
+ return (0, import_node_path2.join)(base, product);
150
+ }
151
+ const home = process.env.HOME ?? (0, import_node_os.tmpdir)();
152
+ return (0, import_node_path2.join)(home, `.${product}`);
153
+ }
154
+ function resolveProductDir(product, explicit) {
155
+ return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
156
+ }
157
+ function tokensPath(dir) {
158
+ return (0, import_node_path2.join)(dir, "tokens.json");
159
+ }
160
+ function statePath(dir) {
161
+ return (0, import_node_path2.join)(dir, "state.json");
162
+ }
163
+ function payloadDir(dir) {
164
+ const acquired = readState(dir)?.acquired;
165
+ if (acquired) return (0, import_node_path2.join)(dir, "candidates", acquired.candidate, "payload");
166
+ return (0, import_node_path2.join)(dir, "payload");
167
+ }
168
+ function readTokens(dir) {
169
+ try {
170
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(tokensPath(dir), "utf8"));
171
+ if (typeof data.accessToken !== "string" || !data.accessToken) return null;
172
+ if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
173
+ const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
174
+ if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
175
+ if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
176
+ return tokens;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function writeTokens(dir, tokens) {
182
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
183
+ try {
184
+ (0, import_node_fs2.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
185
+ `, { mode: 384 });
186
+ } catch {
187
+ (0, import_node_fs2.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
188
+ `);
189
+ }
190
+ }
191
+ function clearTokens(dir) {
192
+ (0, import_node_fs2.rmSync)(tokensPath(dir), { force: true });
193
+ }
194
+ function readState(dir) {
195
+ try {
196
+ const data = JSON.parse((0, import_node_fs2.readFileSync)(statePath(dir), "utf8"));
197
+ if (typeof data.version !== "string" || !data.version) return null;
198
+ const state = { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
199
+ if (data.acquired) {
200
+ if (!/^[a-zA-Z0-9_-]+$/.test(data.acquired.candidate) || typeof data.acquired.node !== "string" || typeof data.acquired.entry !== "string") return null;
201
+ state.acquired = data.acquired;
202
+ }
203
+ return state;
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ function writeState(dir, state) {
209
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
210
+ const temporary = `${statePath(dir)}.${(0, import_node_crypto2.randomUUID)()}.tmp`;
211
+ try {
212
+ (0, import_node_fs2.writeFileSync)(temporary, `${JSON.stringify(state, null, 2)}
213
+ `, { flag: "wx", mode: 384, flush: true });
214
+ (0, import_node_fs2.renameSync)(temporary, statePath(dir));
215
+ } finally {
216
+ (0, import_node_fs2.rmSync)(temporary, { force: true });
217
+ }
218
+ }
219
+ function wipeProductDir(dir) {
220
+ const payload = payloadDir(dir);
221
+ (0, import_node_fs2.rmSync)(tokensPath(dir), { force: true });
222
+ (0, import_node_fs2.rmSync)(statePath(dir), { force: true });
223
+ (0, import_node_fs2.rmSync)(payload, { force: true, recursive: true });
224
+ }
225
+
226
+ // src/payload.ts
227
+ var import_node_crypto3 = require("node:crypto");
228
+ var import_node_fs4 = require("node:fs");
229
+ var import_node_os2 = require("node:os");
230
+ var import_node_path3 = require("node:path");
231
+
232
+ // src/canonical.ts
233
+ function canonicalJson(value) {
234
+ return encode(value);
235
+ }
236
+ function encode(value) {
237
+ if (value === null) return "null";
238
+ if (typeof value === "string") return JSON.stringify(value);
239
+ if (typeof value === "boolean") return value ? "true" : "false";
240
+ if (typeof value === "number") {
241
+ if (!Number.isFinite(value)) {
242
+ throw new TypeError("canonicalJson: cannot encode a non-finite number");
243
+ }
244
+ return JSON.stringify(value);
245
+ }
246
+ if (Array.isArray(value)) {
247
+ return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
248
+ }
249
+ if (typeof value === "object") {
250
+ const record = value;
251
+ const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
252
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
253
+ }
254
+ throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
255
+ }
256
+
257
+ // src/config.ts
258
+ var import_node_fs3 = require("node:fs");
259
+ var import_node_sea = require("node:sea");
260
+
261
+ // src/module-url.ts
262
+ var import_node_url = require("node:url");
263
+ var import_meta = {};
264
+ function moduleUrl() {
265
+ if (typeof __filename === "string") return (0, import_node_url.pathToFileURL)(__filename).href;
266
+ return import_meta.url;
267
+ }
268
+
269
+ // src/config.ts
270
+ function publicKeyBytes(config) {
271
+ const raw = Buffer.from(config.publicKey, "base64");
272
+ if (raw.length !== 32) {
273
+ throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
274
+ }
275
+ return raw;
276
+ }
277
+ function loadProductConfig(options = {}) {
278
+ const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
279
+ const devFallback = new URL("../config/product.template.json", moduleUrl());
280
+ if (explicit) {
281
+ return parseProductConfig((0, import_node_fs3.readFileSync)(explicit, "utf8"));
282
+ }
283
+ try {
284
+ return parseProductConfig((0, import_node_fs3.readFileSync)(devFallback, "utf8"));
285
+ } catch {
286
+ }
287
+ try {
288
+ const asset = (0, import_node_sea.getAsset)("product.json", "utf8");
289
+ if (typeof asset === "string") return parseProductConfig(asset);
290
+ } catch {
291
+ }
292
+ throw new Error(
293
+ "no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
294
+ );
295
+ }
296
+ function parseProductConfig(text) {
297
+ let data;
298
+ try {
299
+ data = JSON.parse(text);
300
+ } catch {
301
+ throw new Error("product config is not valid JSON");
302
+ }
303
+ if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
304
+ const record = data;
305
+ const product = field(record, "product");
306
+ const host = field(record, "host").replace(/\/+$/, "");
307
+ const loginKind = field(record, "loginKind");
308
+ const binName = field(record, "binName");
309
+ if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
310
+ if (loginKind !== "github" && loginKind !== "google") {
311
+ throw new Error('product config loginKind must be "github" or "google"');
312
+ }
313
+ const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
314
+ if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
315
+ publicKeyBytes(config);
316
+ if (loginKind === "github") {
317
+ const clientId = record.githubClientId;
318
+ if (typeof clientId !== "string" || clientId.length === 0) {
319
+ throw new Error("product config needs githubClientId for the github loginKind");
320
+ }
321
+ config.githubClientId = clientId;
322
+ } else if (typeof record.githubClientId === "string") {
323
+ config.githubClientId = record.githubClientId;
324
+ }
325
+ try {
326
+ const url = new URL(host);
327
+ if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
328
+ } catch {
329
+ throw new Error("product config host must be an http(s) URL");
330
+ }
331
+ return config;
332
+ }
333
+ function field(record, key) {
334
+ const value = record[key];
335
+ return typeof value === "string" ? value : "";
336
+ }
337
+
338
+ // src/payload.ts
339
+ var NeedsLoginError = class extends Error {
340
+ constructor() {
341
+ super("signed out \u2014 run login first");
342
+ this.name = "NeedsLoginError";
343
+ }
344
+ };
345
+ var ForbiddenError = class extends Error {
346
+ constructor() {
347
+ super("this install is not allowed for your account \u2014 access was revoked or never granted.");
348
+ this.name = "ForbiddenError";
349
+ }
350
+ };
351
+ function canonicalManifestBytes(manifest) {
352
+ return Buffer.from(
353
+ canonicalJson({
354
+ created: manifest.created,
355
+ files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
356
+ version: manifest.version
357
+ }),
358
+ "utf8"
359
+ );
360
+ }
361
+ function ed25519PublicKey(config) {
362
+ const raw = publicKeyBytes(config);
363
+ return (0, import_node_crypto3.createPublicKey)({
364
+ key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
365
+ format: "jwk"
366
+ });
367
+ }
368
+ function verifyManifest(manifest, signature, config) {
369
+ try {
370
+ const signatureBytes = Buffer.from(signature, "base64");
371
+ if (signatureBytes.length === 0) return false;
372
+ return (0, import_node_crypto3.verify)(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+ function verifyFileBytes(entry2, bytes) {
378
+ if (entry2.size !== bytes.length) return false;
379
+ return (0, import_node_crypto3.createHash)("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
380
+ }
381
+ async function readErrorCode(response) {
382
+ try {
383
+ const data = await response.json();
384
+ return typeof data.error === "string" ? data.error : "";
385
+ } catch {
386
+ return "";
387
+ }
388
+ }
389
+ function throwForStatus(status, errorCode) {
390
+ if (status === 401) throw new NeedsLoginError();
391
+ if (status === 403) throw new ForbiddenError();
392
+ throw new Error(
393
+ errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
394
+ );
395
+ }
396
+ async function getJson(url, accessToken, fetchImpl) {
397
+ const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
398
+ if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
399
+ return { status: response.status, json: await response.json() };
400
+ }
401
+ function parseManifest(json) {
402
+ if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
403
+ const record = json;
404
+ if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
405
+ if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
406
+ if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
407
+ if (typeof record.signature !== "string" || !record.signature) {
408
+ throw new Error("the release manifest is unsigned");
409
+ }
410
+ const files = record.files.map((entry2) => {
411
+ const file = entry2;
412
+ if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
413
+ throw new Error("the release manifest lists a malformed file");
414
+ }
415
+ const safe = safeManifestPath(file.path);
416
+ if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
417
+ return { path: safe, sha256: file.sha256, size: file.size };
418
+ });
419
+ return { version: record.version, created: record.created, files, signature: record.signature };
420
+ }
421
+ function safeManifestPath(rawPath) {
422
+ if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
423
+ if (rawPath.startsWith("/")) return null;
424
+ const segments = rawPath.split("/");
425
+ for (const segment of segments) {
426
+ if (segment === "" || segment === "." || segment === "..") return null;
427
+ }
428
+ return segments.join("/");
429
+ }
430
+ async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
431
+ const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
432
+ const manifest = parseManifest(json);
433
+ if (!verifyManifest(manifest, manifest.signature, config)) {
434
+ throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
435
+ }
436
+ return manifest;
437
+ }
438
+ async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
439
+ const encoded = path.split("/").map(encodeURIComponent).join("/");
440
+ const response = await fetchImpl(`${config.host}/release/${encoded}`, {
441
+ headers: { authorization: `Bearer ${accessToken}` }
442
+ });
443
+ if (!response.ok) {
444
+ if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
445
+ throwForStatus(response.status, await readErrorCode(response));
446
+ }
447
+ return readDownload(response, onProgress, expectedSize);
448
+ }
449
+ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
450
+ const fetchImpl = options.fetchImpl ?? fetch;
451
+ const staging = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
452
+ (0, import_node_fs4.mkdirSync)(staging, { recursive: true });
453
+ try {
454
+ const total = manifest.files.reduce((sum, entry2) => sum + entry2.size, 0);
455
+ let downloaded = 0;
456
+ for (const entry2 of manifest.files) {
457
+ const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl, (progress) => options.onProgress?.({ done: downloaded + progress.done, total }), entry2.size);
458
+ downloaded += bytes.length;
459
+ if (!verifyFileBytes(entry2, bytes)) {
460
+ throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
461
+ }
462
+ const dest = (0, import_node_path3.join)(staging, entry2.path);
463
+ (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(dest), { recursive: true });
464
+ (0, import_node_fs4.writeFileSync)(dest, bytes);
465
+ }
466
+ const target = (0, import_node_path3.join)(dir, "payload");
467
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
468
+ (0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
469
+ (0, import_node_fs4.renameSync)(staging, target);
470
+ } catch (error) {
471
+ (0, import_node_fs4.rmSync)(staging, { force: true, recursive: true });
472
+ throw error;
473
+ }
474
+ return manifest.version;
475
+ }
476
+
477
+ // src/acquisition.ts
478
+ function reusableCandidate(dir, manifest) {
479
+ try {
480
+ const saved = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), "utf8"));
481
+ if (saved.signature !== manifest.signature) return null;
482
+ const root = candidateRoot(dir, saved.candidate);
483
+ if (!(0, import_node_fs5.existsSync)((0, import_node_path4.join)(root, "acquired.json"))) return null;
484
+ for (const file of manifest.files) {
485
+ if (!safePath(file.path) || !verifyFileBytes(file, (0, import_node_fs5.readFileSync)((0, import_node_path4.join)(root, "payload", file.path)))) return null;
486
+ }
487
+ return saved.candidate;
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+ function rememberCandidate(dir, candidate, manifest) {
493
+ candidateRoot(dir, candidate);
494
+ (0, import_node_fs5.writeFileSync)((0, import_node_path4.join)(dir, "acquisition-cache.json"), JSON.stringify({ candidate, signature: manifest.signature }), { mode: 384 });
495
+ }
496
+ function safePath(value) {
497
+ return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
498
+ }
499
+ function argumentsValid(value) {
500
+ return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
501
+ }
502
+ function readAcquisition(payload) {
503
+ const metadata = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, "payload.json"), "utf8"));
504
+ if (metadata.acquisition === void 0) return null;
505
+ const a = metadata.acquisition;
506
+ if (!a || a.schema !== 1 || !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(a.package) || !safePath(a.archive) || !a.archive.endsWith(".tgz") || !safePath(a.convergeEntry) || !safePath(a.rollbackEntry) || !safePath(a.runEntry) || !argumentsValid(a.convergeArgs) || !argumentsValid(a.rollbackArgs) || !argumentsValid(a.repairArgs) || !Array.isArray(a.platforms) || !a.platforms.length || a.platforms.some((p) => !["win32-x64", "win32-arm64", "darwin-arm64", "linux-x64", "linux-arm64"].includes(p))) {
507
+ throw new Error("invalid signed acquisition declaration");
508
+ }
509
+ return a;
510
+ }
511
+ function runtimeEnvironment(node, inherited, platform = process.platform) {
512
+ const env = { ...inherited };
513
+ let current = env.PATH ?? "";
514
+ if (platform === "win32") {
515
+ const keys = Object.keys(env).filter((key) => key.toLowerCase() === "path").sort();
516
+ current = (keys.length ? env[keys[0]] : "") ?? "";
517
+ for (const key of keys) delete env[key];
518
+ }
519
+ env.PATH = `${(0, import_node_path4.dirname)(node)}${platform === "win32" ? ";" : ":"}${current}`;
520
+ return env;
521
+ }
522
+ function lockInstallation(dir) {
523
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
524
+ const path = (0, import_node_path4.join)(dir, "installation.lock");
525
+ const owner = `${process.pid}:${(0, import_node_crypto4.randomUUID)()}`;
526
+ try {
527
+ (0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
528
+ } catch (error) {
529
+ if (error.code !== "EEXIST") throw error;
530
+ const recovery = `${path}.recovery`;
531
+ try {
532
+ (0, import_node_fs5.writeFileSync)(recovery, owner, { flag: "wx", mode: 384, flush: true });
533
+ } catch {
534
+ throw new Error("installation lock recovery is already pending");
535
+ }
536
+ try {
537
+ const previous = (0, import_node_fs5.readFileSync)(path, "utf8");
538
+ const pid = Number(previous.split(":")[0]);
539
+ if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
540
+ try {
541
+ process.kill(pid, 0);
542
+ throw new Error("another installer is running");
543
+ } catch (probe) {
544
+ if (probe.code !== "ESRCH") throw probe;
545
+ }
546
+ (0, import_node_fs5.unlinkSync)(path);
547
+ (0, import_node_fs5.writeFileSync)(path, owner, { flag: "wx", mode: 384, flush: true });
548
+ } finally {
549
+ (0, import_node_fs5.unlinkSync)(recovery);
550
+ }
551
+ }
552
+ let released = false;
553
+ return () => {
554
+ if (!released && (0, import_node_fs5.readFileSync)(path, "utf8") === owner) (0, import_node_fs5.unlinkSync)(path);
555
+ released = true;
556
+ };
557
+ }
558
+ function pendingPath(dir) {
559
+ return (0, import_node_path4.join)(dir, "acquisition-pending.json");
560
+ }
561
+ function candidateRoot(dir, candidate) {
562
+ if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
563
+ return (0, import_node_path4.join)(dir, "candidates", candidate);
564
+ }
565
+ function packageRoot(prefix, acquisition) {
566
+ return (0, import_node_path4.join)(prefix, "node_modules", acquisition.package);
567
+ }
568
+ function entry(root, path) {
569
+ const actual = (0, import_node_fs5.realpathSync)((0, import_node_path4.join)(root, path));
570
+ const rel = (0, import_node_path4.relative)((0, import_node_fs5.realpathSync)(root), actual);
571
+ if (rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel)) throw new Error("acquired entry escapes package");
572
+ return actual;
573
+ }
574
+ function argv(node, root, file, args, prefix, version) {
575
+ return [node, entry(root, file), ...args.map((arg) => arg === "$prefix" ? prefix : arg === "$version" ? version : arg)];
576
+ }
577
+ function acquisitionRepairCommand(dir, version) {
578
+ const state = readState(dir);
579
+ if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
580
+ const root = candidateRoot(dir, state.acquired.candidate);
581
+ const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
582
+ if (!acquisition) throw new Error("installed acquisition declaration is missing");
583
+ const prefix = (0, import_node_path4.join)(root, "prefix");
584
+ const productRoot = packageRoot(prefix, acquisition);
585
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
586
+ if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
587
+ return argv((0, import_node_fs5.realpathSync)(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
588
+ }
589
+ async function recoverAcquisition(dir, run2, env) {
590
+ if (!(0, import_node_fs5.existsSync)(pendingPath(dir))) return;
591
+ const pending = JSON.parse((0, import_node_fs5.readFileSync)(pendingPath(dir), "utf8"));
592
+ if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
593
+ const root = candidateRoot(dir, pending.candidate);
594
+ if (readState(dir)?.acquired?.candidate === pending.candidate) {
595
+ (0, import_node_fs5.unlinkSync)(pendingPath(dir));
596
+ return;
597
+ }
598
+ const acquisition = readAcquisition((0, import_node_path4.join)(root, "payload"));
599
+ if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
600
+ const prefix = (0, import_node_path4.join)(root, "prefix");
601
+ const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
602
+ if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
603
+ if (pending.previous) writeState(dir, pending.previous);
604
+ else if ((0, import_node_fs5.existsSync)(statePath(dir))) (0, import_node_fs5.unlinkSync)(statePath(dir));
605
+ (0, import_node_fs5.unlinkSync)(pendingPath(dir));
606
+ }
607
+ async function installAcquisition(dir, candidate, version, acquisition, options) {
608
+ if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
609
+ const root = candidateRoot(dir, candidate);
610
+ const payload = (0, import_node_path4.join)(root, "payload");
611
+ const prefix = (0, import_node_path4.join)(root, "prefix");
612
+ const prepared = (0, import_node_path4.join)(root, "acquired.json");
613
+ const archiveHash = (0, import_node_crypto4.createHash)("sha256").update((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payload, acquisition.archive))).digest("hex");
614
+ const reused = (0, import_node_fs5.existsSync)(prepared);
615
+ if (reused && JSON.parse((0, import_node_fs5.readFileSync)(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
616
+ if (!reused) (0, import_node_fs5.mkdirSync)(prefix);
617
+ const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
618
+ const env = runtimeEnvironment(runtime.node, options.env);
619
+ const npmEnv = { ...env };
620
+ for (const key of Object.keys(npmEnv)) {
621
+ if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
622
+ }
623
+ const npmrc = (0, import_node_path4.join)(root, "public.npmrc");
624
+ const globalrc = (0, import_node_path4.join)(root, "global.npmrc");
625
+ (0, import_node_fs5.writeFileSync)(npmrc, "registry=https://registry.npmjs.org/\n");
626
+ (0, import_node_fs5.writeFileSync)(globalrc, "");
627
+ const install = [
628
+ runtime.node,
629
+ runtime.npm,
630
+ "install",
631
+ "--prefix",
632
+ prefix,
633
+ "--install-strategy=shallow",
634
+ "--ignore-scripts",
635
+ "--no-audit",
636
+ "--no-fund",
637
+ "--userconfig",
638
+ npmrc,
639
+ "--globalconfig",
640
+ globalrc,
641
+ "--registry",
642
+ "https://registry.npmjs.org/",
643
+ (0, import_node_path4.resolve)(payload, acquisition.archive)
644
+ ];
645
+ if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
646
+ const productRoot = packageRoot(prefix, acquisition);
647
+ const pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(productRoot, "package.json"), "utf8"));
648
+ if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
649
+ const runEntry = entry(productRoot, acquisition.runEntry);
650
+ const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
651
+ entry(productRoot, acquisition.rollbackEntry);
652
+ if (!reused) (0, import_node_fs5.writeFileSync)(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
653
+ const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
654
+ (0, import_node_fs5.writeFileSync)(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
655
+ try {
656
+ if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
657
+ (options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
658
+ } catch (error) {
659
+ await recoverAcquisition(dir, options.run, env);
660
+ throw error;
661
+ }
662
+ (0, import_node_fs5.unlinkSync)(pendingPath(dir));
663
+ }
664
+
665
+ // src/index.ts
666
+ var import_node_child_process4 = require("node:child_process");
667
+ var import_node_fs11 = require("node:fs");
668
+ var import_node_os4 = require("node:os");
669
+ var import_node_path6 = require("node:path");
39
670
  var import_node_url2 = require("node:url");
40
671
 
41
672
  // ../face/src/face.ts
42
- var import_node_fs = require("node:fs");
673
+ var import_node_fs6 = require("node:fs");
43
674
 
44
675
  // ../face/src/products.ts
45
676
  var PRODUCTS = Object.freeze({
46
677
  "mm-strategy": Object.freeze({
47
678
  name: "MM Strategy",
48
- installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
679
+ installWarm: "Welcome. Let's get you ready.",
49
680
  accent: "38;2;249;115;22",
50
- warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
681
+ warm: "Welcome back. Checking for updates...",
51
682
  doctor: "mm-strategy doctor"
52
683
  }),
53
684
  "mmi-hub": Object.freeze({
54
685
  name: "mmi-hub",
55
- installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
686
+ installWarm: "Welcome. Let's get you ready.",
56
687
  accent: "38;2;125;211;252",
57
- warm: "Welcome back. Checking your surfaces\u2026",
688
+ warm: "Welcome back. Checking for updates...",
58
689
  doctor: "mmi doctor"
59
690
  }),
60
691
  "jerv-hub": Object.freeze({
61
692
  name: "jerv-hub",
62
- installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
693
+ installWarm: "Welcome. Let's get you ready.",
63
694
  accent: "38;2;248;113;113",
64
- warm: "Welcome back. Checking your surfaces\u2026",
695
+ warm: "Welcome back. Checking for updates...",
65
696
  doctor: "jerv doctor"
66
697
  }),
67
698
  jervcode: Object.freeze({
68
699
  name: "JervCode",
69
- installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
700
+ installWarm: "Welcome. Let's get you ready.",
70
701
  accent: "38;2;192;132;252",
71
- warm: "Welcome back. Keeping JervCode current\u2026",
702
+ warm: "Welcome back. Checking for updates...",
72
703
  doctor: "jervcode doctor"
73
704
  })
74
705
  });
@@ -82,6 +713,7 @@ function identityFor(product) {
82
713
 
83
714
  // ../face/src/face.ts
84
715
  var GLYPH = Object.freeze({
716
+ clock: "\u23F0",
85
717
  diamond: "\u25C6",
86
718
  hollow: "\u25C7",
87
719
  bar: "\u2502",
@@ -153,12 +785,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
153
785
  const continuesFace = continuedPhases.size > 0;
154
786
  const nested = env.MM_OUTER_CONSOLE === "1";
155
787
  const progressFd = readProgressFd(env);
788
+ const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
156
789
  const emitMilestone = (title, measure, kind) => {
157
- if (progressFd === null) return false;
790
+ if (progressFd === null && !progressFile) return false;
158
791
  const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
159
792
  if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
793
+ if (kind === "running" && typeof measure === "string") record.measure = measure;
160
794
  try {
161
- (0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
795
+ if (progressFile) (0, import_node_fs6.appendFileSync)(progressFile, `${JSON.stringify(record)}
796
+ `);
797
+ else (0, import_node_fs6.writeSync)(progressFd, `${JSON.stringify(record)}
162
798
  `);
163
799
  return true;
164
800
  } catch {
@@ -177,7 +813,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
177
813
  };
178
814
  const step = (title, measure = null, kind = "ok") => {
179
815
  if (emitMilestone(title, measure, kind)) return "";
180
- const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
816
+ const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
181
817
  const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
182
818
  const time = measured;
183
819
  const column = Math.min(44, Math.max(0, width - 8));
@@ -187,7 +823,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
187
823
  const pad = Math.max(1, column - visibleWidth(head));
188
824
  return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
189
825
  };
190
- const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
826
+ const relay = (text) => String(text).replace(/\r?\n$/, "").split(/\r?\n/).flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
191
827
  const receipt = (lines, { ready = true } = {}) => {
192
828
  if (ready && nested) return [];
193
829
  const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
@@ -218,14 +854,14 @@ function createFace({ product, color = false, columns, env = process.env, operat
218
854
  };
219
855
  const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
220
856
  const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
221
- return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
857
+ return { identity, width, nested, emitsProgress: progressFd !== null || Boolean(progressFile), welcome, continues, step, progress: (title, measure = null) => emitMilestone(title, measure, "running"), relay, receipt, outcome, signOff, refusal, paint };
222
858
  }
223
859
 
224
860
  // ../face/src/shell.ts
225
861
  var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
226
862
 
227
863
  // ../face/src/spinner.ts
228
- var import_node_fs2 = require("node:fs");
864
+ var import_node_fs7 = require("node:fs");
229
865
  var import_node_worker_threads = require("node:worker_threads");
230
866
  var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
231
867
  var WORKER_SOURCE = `
@@ -237,7 +873,7 @@ function draw() {
237
873
  if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
238
874
  try {
239
875
  if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
240
- const text = frames[frame++ % frames.length];
876
+ const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
241
877
  writeSync(2, text);
242
878
  if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
243
879
  } finally {
@@ -249,7 +885,6 @@ parentPort.on('message', (next) => {
249
885
  if (Atomics.load(control, 2)) return;
250
886
  frames = next.frames;
251
887
  frame = next.frame;
252
- Atomics.store(control, 0, 0);
253
888
  });
254
889
  setInterval(draw, workerData.intervalMs);
255
890
  `;
@@ -260,20 +895,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
260
895
  let control = null;
261
896
  let frames = [];
262
897
  let frame = 0;
898
+ let started = 0;
899
+ let suspended = false;
263
900
  const write = (text) => {
264
901
  if (stream) stream.write(text);
265
902
  else {
266
- (0, import_node_fs2.writeSync)(2, text);
267
- if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
903
+ (0, import_node_fs7.writeSync)(2, text);
904
+ if (transcriptPath) (0, import_node_fs7.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
268
905
  `);
269
906
  }
270
907
  };
271
908
  const render = (text, measure) => {
272
- const line = face.step(text, measure, "note").split("\n")[0];
909
+ const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
273
910
  frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
274
911
  };
275
912
  const draw = () => {
276
- if (animate && frames.length) write(frames[frame++ % frames.length]);
913
+ if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
277
914
  };
278
915
  const pause = () => {
279
916
  if (!control) return;
@@ -293,6 +930,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
293
930
  start(text, measure = null) {
294
931
  if (!animate) return;
295
932
  halt();
933
+ suspended = false;
934
+ started = Date.now();
296
935
  render(text, measure);
297
936
  frame = 0;
298
937
  draw();
@@ -306,7 +945,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
306
945
  frames,
307
946
  frame,
308
947
  intervalMs,
309
- transcriptPath
948
+ transcriptPath,
949
+ started
310
950
  } });
311
951
  const active = worker;
312
952
  worker.on("error", () => {
@@ -324,9 +964,22 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
324
964
  render(text, measure);
325
965
  draw();
326
966
  worker?.postMessage({ frames, frame });
967
+ if (control && !suspended) Atomics.store(control, 0, 0);
968
+ },
969
+ pause() {
970
+ if (suspended) return;
971
+ suspended = true;
972
+ pause();
973
+ if (animate && frames.length) write("\r\x1B[2K");
974
+ },
975
+ resume() {
976
+ suspended = false;
977
+ if (control) Atomics.store(control, 0, 0);
978
+ if (timer || worker) draw();
327
979
  },
328
980
  stop() {
329
981
  halt();
982
+ frames = [];
330
983
  if (animate) write("\r\x1B[2K");
331
984
  }
332
985
  };
@@ -337,10 +990,10 @@ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
337
990
  var ALLOWED = new Set(Object.values(GLYPH));
338
991
 
339
992
  // ../face/src/run.ts
340
- var import_node_fs4 = require("node:fs");
993
+ var import_node_fs9 = require("node:fs");
341
994
 
342
995
  // ../face/src/outcome.ts
343
- var import_node_fs3 = require("node:fs");
996
+ var import_node_fs8 = require("node:fs");
344
997
  var counts = ["total", "updated", "failed"];
345
998
  var strings = ["version", "retry", "detail", "logPath"];
346
999
  var flags = ["dryRun", "installed", "deferred", "operationFailed"];
@@ -362,12 +1015,12 @@ function validateInstallerOutcome(value) {
362
1015
  return { ...facts };
363
1016
  }
364
1017
  function writeInstallerOutcome(path, value) {
365
- (0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
1018
+ (0, import_node_fs8.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
366
1019
  }
367
1020
  function readInstallerOutcome(path) {
368
1021
  let text;
369
1022
  try {
370
- text = (0, import_node_fs3.readFileSync)(path, "utf8");
1023
+ text = (0, import_node_fs8.readFileSync)(path, "utf8");
371
1024
  } catch (error) {
372
1025
  if (error.code === "ENOENT") return void 0;
373
1026
  throw error;
@@ -430,6 +1083,7 @@ var PHASES = {
430
1083
  "verify-release": ["Checking the release version", "Verified the release version"],
431
1084
  verify: ["Verifying the payload", "Verified the payload"],
432
1085
  install: ["Installing the product", "Installed the product"],
1086
+ configure: ["Configuring the product", "Configured the product"],
433
1087
  activate: ["Activating surfaces", "Activated surfaces"],
434
1088
  doctor: ["Checking health", "Checked health"],
435
1089
  rollback: ["Restoring the previous version", "Restored the previous version"]
@@ -459,7 +1113,7 @@ function createInstallerRun(value, options = {}) {
459
1113
  if (!text) return;
460
1114
  write(text, channel);
461
1115
  if (env.MM_FACE_TRANSCRIPT) {
462
- (0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
1116
+ (0, import_node_fs9.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
463
1117
  `, "utf8");
464
1118
  }
465
1119
  };
@@ -474,6 +1128,17 @@ function createInstallerRun(value, options = {}) {
474
1128
  return true;
475
1129
  } } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
476
1130
  });
1131
+ let activeTitle;
1132
+ let chunkOpen = false;
1133
+ let chunkChannel = "stdout";
1134
+ let chunkColumn = 0;
1135
+ const trailingCR = /* @__PURE__ */ new Set();
1136
+ let deferredActivity;
1137
+ const closeChunk = () => {
1138
+ if (chunkOpen) emit("\n", chunkChannel);
1139
+ chunkOpen = false;
1140
+ chunkColumn = 0;
1141
+ };
477
1142
  let started = false;
478
1143
  let finished = false;
479
1144
  const start = () => {
@@ -488,6 +1153,9 @@ function createInstallerRun(value, options = {}) {
488
1153
  }
489
1154
  };
490
1155
  const durable = (title, measure, kind) => {
1156
+ closeChunk();
1157
+ activeTitle = void 0;
1158
+ deferredActivity = void 0;
491
1159
  spinner.stop();
492
1160
  const rendered = face.step(title, measure, kind);
493
1161
  if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
@@ -504,7 +1172,12 @@ function createInstallerRun(value, options = {}) {
504
1172
  const state = facts.state ?? "ok";
505
1173
  const title = PHASES[id][state === "ok" ? 1 : 0];
506
1174
  if (state === "running") {
507
- spinner.start(title, facts.measure ?? null);
1175
+ if (face.progress(title, facts.measure ?? null)) return;
1176
+ if (activeTitle === title) spinner.say(title, facts.measure ?? null);
1177
+ else {
1178
+ activeTitle = title;
1179
+ spinner.start(title, facts.measure ?? null);
1180
+ }
508
1181
  return;
509
1182
  }
510
1183
  if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
@@ -524,8 +1197,20 @@ function createInstallerRun(value, options = {}) {
524
1197
  durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
525
1198
  if (facts.detail) run2.relay(facts.detail);
526
1199
  },
527
- milestone({ step, state, ms }) {
1200
+ milestone({ step, state, ms, measure }) {
528
1201
  start();
1202
+ if (state === "running") {
1203
+ if (chunkOpen) {
1204
+ deferredActivity = { title: step, measure: measure ?? null };
1205
+ return;
1206
+ }
1207
+ if (activeTitle === step) spinner.say(step, measure ?? null);
1208
+ else {
1209
+ activeTitle = step;
1210
+ spinner.start(step, measure ?? null);
1211
+ }
1212
+ return;
1213
+ }
529
1214
  durable(step, ms === void 0 ? null : ms / 1e3, state);
530
1215
  },
531
1216
  signIn({ url, code }) {
@@ -539,7 +1224,8 @@ function createInstallerRun(value, options = {}) {
539
1224
  },
540
1225
  // Only pass safe diagnostic text, never authentication output or credentials.
541
1226
  relay(text, channel = "stdout", record = true) {
542
- spinner.stop();
1227
+ closeChunk();
1228
+ spinner.pause();
543
1229
  const rendered = tty ? face.relay(text) : stripColor(text);
544
1230
  for (const row of rendered.split("\n")) if (row) {
545
1231
  emit(`${row}
@@ -547,6 +1233,37 @@ function createInstallerRun(value, options = {}) {
547
1233
  ` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
548
1234
  `);
549
1235
  }
1236
+ spinner.resume();
1237
+ },
1238
+ relayChunk(text, channel = "stdout") {
1239
+ spinner.pause();
1240
+ if (chunkOpen && chunkChannel !== channel) closeChunk();
1241
+ chunkChannel = channel;
1242
+ if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
1243
+ if (text.endsWith("\r")) trailingCR.add(channel);
1244
+ const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
1245
+ for (let i = 0; i < parts.length; i++) {
1246
+ const characters = [...stripColor(parts[i])];
1247
+ while (characters.length) {
1248
+ if (tty && chunkColumn >= face.width - 6) closeChunk();
1249
+ const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
1250
+ emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
1251
+ chunkColumn += [...part].length;
1252
+ chunkOpen = true;
1253
+ }
1254
+ if (i < parts.length - 1) {
1255
+ emit("\n", channel);
1256
+ chunkOpen = false;
1257
+ chunkColumn = 0;
1258
+ }
1259
+ }
1260
+ if (!chunkOpen) {
1261
+ if (deferredActivity) {
1262
+ activeTitle = deferredActivity.title;
1263
+ spinner.start(deferredActivity.title, deferredActivity.measure);
1264
+ deferredActivity = void 0;
1265
+ } else spinner.resume();
1266
+ }
550
1267
  },
551
1268
  cancel() {
552
1269
  if (finished) return;
@@ -555,6 +1272,8 @@ function createInstallerRun(value, options = {}) {
555
1272
  { total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
556
1273
  );
557
1274
  start();
1275
+ closeChunk();
1276
+ activeTitle = void 0;
558
1277
  spinner.stop();
559
1278
  finished = true;
560
1279
  if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
@@ -567,6 +1286,8 @@ function createInstallerRun(value, options = {}) {
567
1286
  validateInstallerOutcome(facts);
568
1287
  if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
569
1288
  start();
1289
+ closeChunk();
1290
+ activeTitle = void 0;
570
1291
  spinner.stop();
571
1292
  finished = true;
572
1293
  if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
@@ -587,6 +1308,8 @@ function createInstallerRun(value, options = {}) {
587
1308
  if (tty) lines([face.signOff()]);
588
1309
  },
589
1310
  stop() {
1311
+ closeChunk();
1312
+ activeTitle = void 0;
590
1313
  spinner.stop();
591
1314
  }
592
1315
  };
@@ -594,17 +1317,17 @@ function createInstallerRun(value, options = {}) {
594
1317
  }
595
1318
 
596
1319
  // src/autoupdate.ts
597
- var import_node_child_process = require("node:child_process");
598
- var import_node_fs5 = require("node:fs");
599
- var import_node_os = require("node:os");
600
- var import_node_path = require("node:path");
1320
+ var import_node_child_process2 = require("node:child_process");
1321
+ var import_node_fs10 = require("node:fs");
1322
+ var import_node_os3 = require("node:os");
1323
+ var import_node_path5 = require("node:path");
601
1324
  function schedulePlatform(override) {
602
1325
  const platform = override ?? process.platform;
603
1326
  if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
604
1327
  return null;
605
1328
  }
606
- function defaultExec(command, args) {
607
- const result = (0, import_node_child_process.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
1329
+ function defaultExec(command2, args) {
1330
+ const result = (0, import_node_child_process2.spawnSync)(command2, args, { encoding: "utf8", windowsHide: true });
608
1331
  if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
609
1332
  return {
610
1333
  code: typeof result.status === "number" ? result.status : 1,
@@ -614,8 +1337,8 @@ function defaultExec(command, args) {
614
1337
  }
615
1338
  function homeOf(options) {
616
1339
  if (options.homeDir) return options.homeDir;
617
- if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os.tmpdir)();
618
- return process.env.HOME ?? (0, import_node_os.tmpdir)();
1340
+ if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os3.tmpdir)();
1341
+ return process.env.HOME ?? (0, import_node_os3.tmpdir)();
619
1342
  }
620
1343
  function scheduleName(config) {
621
1344
  return `${config.binName} autoupdate`;
@@ -626,13 +1349,13 @@ function scheduleLabel(config) {
626
1349
  function quoteWindows(arg) {
627
1350
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
628
1351
  }
629
- function enableSchedule(config, command, options = {}) {
1352
+ function enableSchedule(config, command2, options = {}) {
630
1353
  const platform = schedulePlatform(options.platform);
631
1354
  if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
632
1355
  const exec = options.exec ?? defaultExec;
633
1356
  const home = homeOf(options);
634
1357
  if (platform === "win32") {
635
- const taskLine = command.map(quoteWindows).join(" ");
1358
+ const taskLine = command2.map(quoteWindows).join(" ");
636
1359
  const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
637
1360
  if (result2.code !== 0) {
638
1361
  throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
@@ -641,10 +1364,10 @@ function enableSchedule(config, command, options = {}) {
641
1364
  }
642
1365
  if (platform === "darwin") {
643
1366
  const label2 = scheduleLabel(config);
644
- const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
645
- (0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
646
- const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
647
- (0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
1367
+ const dir2 = (0, import_node_path5.join)(home, "Library", "LaunchAgents");
1368
+ (0, import_node_fs10.mkdirSync)(dir2, { recursive: true });
1369
+ const plist = (0, import_node_path5.join)(dir2, `${label2}.plist`);
1370
+ (0, import_node_fs10.writeFileSync)(plist, darwinPlist(label2, command2));
648
1371
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
649
1372
  const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
650
1373
  if (result2.code !== 0) {
@@ -653,10 +1376,10 @@ function enableSchedule(config, command, options = {}) {
653
1376
  return;
654
1377
  }
655
1378
  const label = scheduleLabel(config);
656
- const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
657
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
658
- (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
659
- (0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
1379
+ const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
1380
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
1381
+ (0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.service`), linuxService(command2));
1382
+ (0, import_node_fs10.writeFileSync)((0, import_node_path5.join)(dir, `${label}.timer`), linuxTimer(label));
660
1383
  const reload = exec("systemctl", ["--user", "daemon-reload"]);
661
1384
  if (reload.code !== 0) {
662
1385
  throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
@@ -681,14 +1404,14 @@ function disableSchedule(config, options = {}) {
681
1404
  if (platform === "darwin") {
682
1405
  const label2 = scheduleLabel(config);
683
1406
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
684
- (0, import_node_fs5.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
1407
+ (0, import_node_fs10.rmSync)((0, import_node_path5.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
685
1408
  return;
686
1409
  }
687
1410
  const label = scheduleLabel(config);
688
- const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
1411
+ const dir = (0, import_node_path5.join)(home, ".config", "systemd", "user");
689
1412
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
690
- (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
691
- (0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
1413
+ (0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.service`), { force: true });
1414
+ (0, import_node_fs10.rmSync)((0, import_node_path5.join)(dir, `${label}.timer`), { force: true });
692
1415
  }
693
1416
  function querySchedule(config, options = {}) {
694
1417
  const platform = schedulePlatform(options.platform);
@@ -706,158 +1429,77 @@ function querySchedule(config, options = {}) {
706
1429
  return state2;
707
1430
  }
708
1431
  if (platform === "darwin") {
709
- const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
710
- if (!(0, import_node_fs5.existsSync)(plist)) return { supported: true, enabled: false };
1432
+ const plist = (0, import_node_path5.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
1433
+ if (!(0, import_node_fs10.existsSync)(plist)) return { supported: true, enabled: false };
711
1434
  return { supported: true, enabled: true, cadence: "hourly" };
712
1435
  }
713
- const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
714
- if (!(0, import_node_fs5.existsSync)(timer)) return { supported: true, enabled: false };
1436
+ const timer = (0, import_node_path5.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
1437
+ if (!(0, import_node_fs10.existsSync)(timer)) return { supported: true, enabled: false };
715
1438
  const state = { supported: true, enabled: true, cadence: "hourly" };
716
1439
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
717
1440
  const stamp = (shown.stdout ?? "").trim();
718
1441
  if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
719
1442
  return state;
720
1443
  }
721
- function darwinPlist(label, command) {
722
- const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
1444
+ function darwinPlist(label, command2) {
1445
+ const args = command2.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
723
1446
  return `<?xml version="1.0" encoding="UTF-8"?>
724
1447
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
725
- <plist version="1.0">
726
- <dict>
727
- <key>Label</key>
728
- <string>${xmlEscape(label)}</string>
729
- <key>ProgramArguments</key>
730
- <array>
731
- ${args}
732
- </array>
733
- <key>StartInterval</key>
734
- <integer>3600</integer>
735
- </dict>
736
- </plist>
737
- `;
738
- }
739
- function linuxService(command) {
740
- const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
741
- return `[Unit]
742
- Description=${"Hourly update check"}
743
- [Service]
744
- Type=oneshot
745
- ExecStart=${line}
746
- `;
747
- }
748
- function linuxTimer(label) {
749
- return `[Unit]
750
- Description=Hourly update check for ${label}
751
- [Timer]
752
- OnCalendar=hourly
753
- Persistent=true
754
- [Install]
755
- WantedBy=timers.target
756
- `;
757
- }
758
- function xmlEscape(text) {
759
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
760
- }
761
- function valueOf(output, key) {
762
- const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
763
- if (!line) return null;
764
- const value = line.slice(line.indexOf(key) + key.length).trim();
765
- return value ? value : null;
766
- }
767
- function firstLine(text) {
768
- return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
769
- }
770
-
771
- // src/config.ts
772
- var import_node_fs6 = require("node:fs");
773
- var import_node_sea = require("node:sea");
774
-
775
- // src/module-url.ts
776
- var import_node_url = require("node:url");
777
- var import_meta = {};
778
- function moduleUrl() {
779
- if (typeof __filename === "string") return (0, import_node_url.pathToFileURL)(__filename).href;
780
- return import_meta.url;
781
- }
782
-
783
- // src/config.ts
784
- function publicKeyBytes(config) {
785
- const raw = Buffer.from(config.publicKey, "base64");
786
- if (raw.length !== 32) {
787
- throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
788
- }
789
- return raw;
790
- }
791
- function loadProductConfig(options = {}) {
792
- const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
793
- const devFallback = new URL("../config/product.template.json", moduleUrl());
794
- if (explicit) {
795
- return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
796
- }
797
- try {
798
- return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
799
- } catch {
800
- }
801
- try {
802
- const asset = (0, import_node_sea.getAsset)("product.json", "utf8");
803
- if (typeof asset === "string") return parseProductConfig(asset);
804
- } catch {
805
- }
806
- throw new Error(
807
- "no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
808
- );
809
- }
810
- function parseProductConfig(text) {
811
- let data;
812
- try {
813
- data = JSON.parse(text);
814
- } catch {
815
- throw new Error("product config is not valid JSON");
816
- }
817
- if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
818
- const record = data;
819
- const product = field(record, "product");
820
- const host = field(record, "host").replace(/\/+$/, "");
821
- const loginKind = field(record, "loginKind");
822
- const binName = field(record, "binName");
823
- if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
824
- if (loginKind !== "github" && loginKind !== "google") {
825
- throw new Error('product config loginKind must be "github" or "google"');
826
- }
827
- const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
828
- if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
829
- publicKeyBytes(config);
830
- if (loginKind === "github") {
831
- const clientId = record.githubClientId;
832
- if (typeof clientId !== "string" || clientId.length === 0) {
833
- throw new Error("product config needs githubClientId for the github loginKind");
834
- }
835
- config.githubClientId = clientId;
836
- } else if (typeof record.githubClientId === "string") {
837
- config.githubClientId = record.githubClientId;
838
- }
839
- try {
840
- const url = new URL(host);
841
- if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
842
- } catch {
843
- throw new Error("product config host must be an http(s) URL");
844
- }
845
- return config;
1448
+ <plist version="1.0">
1449
+ <dict>
1450
+ <key>Label</key>
1451
+ <string>${xmlEscape(label)}</string>
1452
+ <key>ProgramArguments</key>
1453
+ <array>
1454
+ ${args}
1455
+ </array>
1456
+ <key>StartInterval</key>
1457
+ <integer>3600</integer>
1458
+ </dict>
1459
+ </plist>
1460
+ `;
846
1461
  }
847
- function field(record, key) {
848
- const value = record[key];
849
- return typeof value === "string" ? value : "";
1462
+ function linuxService(command2) {
1463
+ const line = command2.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
1464
+ return `[Unit]
1465
+ Description=${"Hourly update check"}
1466
+ [Service]
1467
+ Type=oneshot
1468
+ ExecStart=${line}
1469
+ `;
1470
+ }
1471
+ function linuxTimer(label) {
1472
+ return `[Unit]
1473
+ Description=Hourly update check for ${label}
1474
+ [Timer]
1475
+ OnCalendar=hourly
1476
+ Persistent=true
1477
+ [Install]
1478
+ WantedBy=timers.target
1479
+ `;
1480
+ }
1481
+ function xmlEscape(text) {
1482
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1483
+ }
1484
+ function valueOf(output, key) {
1485
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
1486
+ if (!line) return null;
1487
+ const value = line.slice(line.indexOf(key) + key.length).trim();
1488
+ return value ? value : null;
1489
+ }
1490
+ function firstLine(text) {
1491
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
850
1492
  }
851
1493
 
852
1494
  // src/login-github.ts
853
- var import_node_child_process2 = require("node:child_process");
854
- var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1495
+ var import_node_child_process3 = require("node:child_process");
1496
+ var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
855
1497
  function openBrowser(url) {
856
1498
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
857
1499
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
858
1500
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
859
1501
  try {
860
- const child = (0, import_node_child_process2.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
1502
+ const child = (0, import_node_child_process3.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
861
1503
  child.on("error", () => {
862
1504
  });
863
1505
  child.unref();
@@ -971,7 +1613,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
971
1613
  }
972
1614
 
973
1615
  // src/login-google.ts
974
- var import_node_crypto = require("node:crypto");
1616
+ var import_node_crypto5 = require("node:crypto");
975
1617
  var import_node_http = require("node:http");
976
1618
  var b64url = (bytes) => bytes.toString("base64url");
977
1619
  async function loginGoogle(options) {
@@ -979,7 +1621,7 @@ async function loginGoogle(options) {
979
1621
  const server = options.host.replace(/\/+$/, "");
980
1622
  const timeoutMs = options.timeoutMs ?? 5 * 6e4;
981
1623
  const listener = (0, import_node_http.createServer)();
982
- await new Promise((resolve2) => listener.listen(0, "127.0.0.1", resolve2));
1624
+ await new Promise((resolve3) => listener.listen(0, "127.0.0.1", resolve3));
983
1625
  const port = listener.address().port;
984
1626
  const redirectUri = `http://127.0.0.1:${port}/callback`;
985
1627
  try {
@@ -993,9 +1635,9 @@ async function loginGoogle(options) {
993
1635
  if (typeof clientId !== "string" || !clientId) {
994
1636
  throw new Error("the sign-in server returned a malformed registration");
995
1637
  }
996
- const verifier = b64url((0, import_node_crypto.randomBytes)(32));
997
- const challenge = b64url((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
998
- const state = b64url((0, import_node_crypto.randomBytes)(16));
1638
+ const verifier = b64url((0, import_node_crypto5.randomBytes)(32));
1639
+ const challenge = b64url((0, import_node_crypto5.createHash)("sha256").update(verifier).digest());
1640
+ const state = b64url((0, import_node_crypto5.randomBytes)(16));
999
1641
  const authorize = new URL(`${server}/oauth/authorize`);
1000
1642
  authorize.search = new URLSearchParams({
1001
1643
  response_type: "code",
@@ -1006,7 +1648,7 @@ async function loginGoogle(options) {
1006
1648
  code_challenge: challenge,
1007
1649
  code_challenge_method: "S256"
1008
1650
  }).toString();
1009
- const code = await new Promise((resolve2, reject) => {
1651
+ const code = await new Promise((resolve3, reject) => {
1010
1652
  const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
1011
1653
  listener.on("request", (req, res) => {
1012
1654
  const url = new URL(req.url ?? "/", redirectUri);
@@ -1024,7 +1666,7 @@ async function loginGoogle(options) {
1024
1666
  }
1025
1667
  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."));
1026
1668
  clearTimeout(timer);
1027
- resolve2(received);
1669
+ resolve3(received);
1028
1670
  });
1029
1671
  const print = options.print ?? ((line) => process.stderr.write(`${line}
1030
1672
  `));
@@ -1083,245 +1725,8 @@ function page(title, body) {
1083
1725
  return `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="dark"><title>${title}</title><style>${style}</style><main><h1>${title}</h1><p>${body}</p></main>`;
1084
1726
  }
1085
1727
 
1086
- // src/payload.ts
1087
- var import_node_crypto2 = require("node:crypto");
1088
- var import_node_fs7 = require("node:fs");
1089
- var import_node_os2 = require("node:os");
1090
- var import_node_path2 = require("node:path");
1091
-
1092
- // src/canonical.ts
1093
- function canonicalJson(value) {
1094
- return encode(value);
1095
- }
1096
- function encode(value) {
1097
- if (value === null) return "null";
1098
- if (typeof value === "string") return JSON.stringify(value);
1099
- if (typeof value === "boolean") return value ? "true" : "false";
1100
- if (typeof value === "number") {
1101
- if (!Number.isFinite(value)) {
1102
- throw new TypeError("canonicalJson: cannot encode a non-finite number");
1103
- }
1104
- return JSON.stringify(value);
1105
- }
1106
- if (Array.isArray(value)) {
1107
- return `[${value.map((entry) => encode(entry)).join(",")}]`;
1108
- }
1109
- if (typeof value === "object") {
1110
- const record = value;
1111
- const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
1112
- return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
1113
- }
1114
- throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
1115
- }
1116
-
1117
- // src/payload.ts
1118
- var NeedsLoginError = class extends Error {
1119
- constructor() {
1120
- super("signed out \u2014 run login first");
1121
- this.name = "NeedsLoginError";
1122
- }
1123
- };
1124
- var ForbiddenError = class extends Error {
1125
- constructor() {
1126
- super("this install is not allowed for your account \u2014 access was revoked or never granted.");
1127
- this.name = "ForbiddenError";
1128
- }
1129
- };
1130
- function canonicalManifestBytes(manifest) {
1131
- return Buffer.from(
1132
- canonicalJson({
1133
- created: manifest.created,
1134
- files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
1135
- version: manifest.version
1136
- }),
1137
- "utf8"
1138
- );
1139
- }
1140
- function ed25519PublicKey(config) {
1141
- const raw = publicKeyBytes(config);
1142
- return (0, import_node_crypto2.createPublicKey)({
1143
- key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
1144
- format: "jwk"
1145
- });
1146
- }
1147
- function verifyManifest(manifest, signature, config) {
1148
- try {
1149
- const signatureBytes = Buffer.from(signature, "base64");
1150
- if (signatureBytes.length === 0) return false;
1151
- return (0, import_node_crypto2.verify)(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
1152
- } catch {
1153
- return false;
1154
- }
1155
- }
1156
- function verifyFileBytes(entry, bytes) {
1157
- if (entry.size !== bytes.length) return false;
1158
- return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex") === entry.sha256.toLowerCase();
1159
- }
1160
- async function readErrorCode(response) {
1161
- try {
1162
- const data = await response.json();
1163
- return typeof data.error === "string" ? data.error : "";
1164
- } catch {
1165
- return "";
1166
- }
1167
- }
1168
- function throwForStatus(status, errorCode) {
1169
- if (status === 401) throw new NeedsLoginError();
1170
- if (status === 403) throw new ForbiddenError();
1171
- throw new Error(
1172
- errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
1173
- );
1174
- }
1175
- async function getJson(url, accessToken, fetchImpl) {
1176
- const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
1177
- if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
1178
- return { status: response.status, json: await response.json() };
1179
- }
1180
- function parseManifest(json) {
1181
- if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
1182
- const record = json;
1183
- if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
1184
- if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
1185
- if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
1186
- if (typeof record.signature !== "string" || !record.signature) {
1187
- throw new Error("the release manifest is unsigned");
1188
- }
1189
- const files = record.files.map((entry) => {
1190
- const file = entry;
1191
- if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
1192
- throw new Error("the release manifest lists a malformed file");
1193
- }
1194
- const safe = safeManifestPath(file.path);
1195
- if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
1196
- return { path: safe, sha256: file.sha256, size: file.size };
1197
- });
1198
- return { version: record.version, created: record.created, files, signature: record.signature };
1199
- }
1200
- function safeManifestPath(rawPath) {
1201
- if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
1202
- if (rawPath.startsWith("/")) return null;
1203
- const segments = rawPath.split("/");
1204
- for (const segment of segments) {
1205
- if (segment === "" || segment === "." || segment === "..") return null;
1206
- }
1207
- return segments.join("/");
1208
- }
1209
- async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
1210
- const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
1211
- const manifest = parseManifest(json);
1212
- if (!verifyManifest(manifest, manifest.signature, config)) {
1213
- throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
1214
- }
1215
- return manifest;
1216
- }
1217
- async function fetchFileBytes(config, accessToken, path, fetchImpl) {
1218
- const encoded = path.split("/").map(encodeURIComponent).join("/");
1219
- const response = await fetchImpl(`${config.host}/release/${encoded}`, {
1220
- headers: { authorization: `Bearer ${accessToken}` }
1221
- });
1222
- if (!response.ok) {
1223
- if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
1224
- throwForStatus(response.status, await readErrorCode(response));
1225
- }
1226
- return Buffer.from(await response.arrayBuffer());
1227
- }
1228
- async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
1229
- const fetchImpl = options.fetchImpl ?? fetch;
1230
- const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
1231
- (0, import_node_fs7.mkdirSync)(staging, { recursive: true });
1232
- try {
1233
- for (const entry of manifest.files) {
1234
- const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
1235
- if (!verifyFileBytes(entry, bytes)) {
1236
- throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
1237
- }
1238
- const dest = (0, import_node_path2.join)(staging, entry.path);
1239
- (0, import_node_fs7.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
1240
- (0, import_node_fs7.writeFileSync)(dest, bytes);
1241
- }
1242
- const target = (0, import_node_path2.join)(dir, "payload");
1243
- (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
1244
- (0, import_node_fs7.rmSync)(target, { force: true, recursive: true });
1245
- (0, import_node_fs7.renameSync)(staging, target);
1246
- } catch (error) {
1247
- (0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
1248
- throw error;
1249
- }
1250
- return manifest.version;
1251
- }
1252
-
1253
- // src/store.ts
1254
- var import_node_fs8 = require("node:fs");
1255
- var import_node_os3 = require("node:os");
1256
- var import_node_path3 = require("node:path");
1257
- function defaultProductDir(product) {
1258
- if (process.platform === "win32") {
1259
- const base = process.env.LOCALAPPDATA ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "launcher-fallback");
1260
- return (0, import_node_path3.join)(base, product);
1261
- }
1262
- const home = process.env.HOME ?? (0, import_node_os3.tmpdir)();
1263
- return (0, import_node_path3.join)(home, `.${product}`);
1264
- }
1265
- function resolveProductDir(product, explicit) {
1266
- return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
1267
- }
1268
- function tokensPath(dir) {
1269
- return (0, import_node_path3.join)(dir, "tokens.json");
1270
- }
1271
- function statePath(dir) {
1272
- return (0, import_node_path3.join)(dir, "state.json");
1273
- }
1274
- function payloadDir(dir) {
1275
- return (0, import_node_path3.join)(dir, "payload");
1276
- }
1277
- function readTokens(dir) {
1278
- try {
1279
- const data = JSON.parse((0, import_node_fs8.readFileSync)(tokensPath(dir), "utf8"));
1280
- if (typeof data.accessToken !== "string" || !data.accessToken) return null;
1281
- if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
1282
- const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
1283
- if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
1284
- if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
1285
- return tokens;
1286
- } catch {
1287
- return null;
1288
- }
1289
- }
1290
- function writeTokens(dir, tokens) {
1291
- (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
1292
- try {
1293
- (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1294
- `, { mode: 384 });
1295
- } catch {
1296
- (0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
1297
- `);
1298
- }
1299
- }
1300
- function clearTokens(dir) {
1301
- (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
1302
- }
1303
- function readState(dir) {
1304
- try {
1305
- const data = JSON.parse((0, import_node_fs8.readFileSync)(statePath(dir), "utf8"));
1306
- if (typeof data.version !== "string" || !data.version) return null;
1307
- return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
1308
- } catch {
1309
- return null;
1310
- }
1311
- }
1312
- function writeState(dir, state) {
1313
- (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
1314
- (0, import_node_fs8.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
1315
- `);
1316
- }
1317
- function wipeProductDir(dir) {
1318
- (0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
1319
- (0, import_node_fs8.rmSync)(statePath(dir), { force: true });
1320
- (0, import_node_fs8.rmSync)(payloadDir(dir), { force: true, recursive: true });
1321
- }
1322
-
1323
1728
  // src/index.ts
1324
- var LAUNCHER_VERSION = true ? "0.1.12" : readVersionFromPackage();
1729
+ var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
1325
1730
  function defaultPrint(message) {
1326
1731
  process.stdout.write(`${message}
1327
1732
  `);
@@ -1333,30 +1738,37 @@ function defaultPrintErr(message) {
1333
1738
  async function run(rawOptions = {}) {
1334
1739
  const print = rawOptions.print ?? defaultPrint;
1335
1740
  const printErr = rawOptions.printErr ?? defaultPrintErr;
1336
- const argv = rawOptions.argv ?? process.argv.slice(2);
1337
- const runAt = argv.indexOf("--run");
1338
- if (runAt >= 0) return runFile(argv[runAt + 1], argv.slice(runAt + 2), printErr);
1741
+ const argv2 = rawOptions.argv ?? process.argv.slice(2);
1742
+ const runAt = argv2.indexOf("--run");
1743
+ if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
1339
1744
  let config;
1340
1745
  try {
1341
- config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv, "--config") });
1746
+ config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
1342
1747
  } catch (error) {
1343
1748
  print(`cannot start: ${error.message}`);
1344
1749
  return 2;
1345
1750
  }
1346
- const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv, "--dir"));
1347
- const positional = argv.filter((arg) => !arg.startsWith("-"));
1348
- if (argv.includes("--version") || argv.includes("-v")) {
1751
+ const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
1752
+ const positional = argv2.filter((arg) => !arg.startsWith("-"));
1753
+ if (argv2.includes("--version") || argv2.includes("-v")) {
1349
1754
  print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
1350
1755
  return 0;
1351
1756
  }
1352
- if (argv.includes("--help") || argv.includes("-h") || positional.length === 0) {
1757
+ if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
1353
1758
  printUsage(config, print);
1354
1759
  return positional.length === 0 ? 2 : 0;
1355
1760
  }
1356
- const command = positional[0];
1761
+ const command2 = positional[0];
1762
+ let unlock;
1357
1763
  try {
1358
- switch (command) {
1764
+ unlock = lockInstallation(dir);
1765
+ await recoverAcquisition(dir, async (command3, cwd, env) => {
1766
+ const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
1767
+ return result.ok && (result.code === void 0 || result.code === 0);
1768
+ }, payloadEnv(dir) ?? process.env);
1769
+ switch (command2) {
1359
1770
  case "login":
1771
+ unlock();
1360
1772
  await doLogin(config, dir, rawOptions, print);
1361
1773
  return 0;
1362
1774
  case "logout":
@@ -1368,11 +1780,11 @@ async function run(rawOptions = {}) {
1368
1780
  case "update":
1369
1781
  return await doUpdate(config, dir, rawOptions, print);
1370
1782
  case "doctor":
1371
- return doDoctor(config, dir, rawOptions, print);
1783
+ return doDoctor(config, dir, rawOptions, print, unlock);
1372
1784
  case "autoupdate":
1373
1785
  return doAutoupdate(config, rawOptions, positional.slice(1), print);
1374
1786
  default:
1375
- return doForward(config, dir, rawOptions, command, argv, print);
1787
+ return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
1376
1788
  }
1377
1789
  } catch (error) {
1378
1790
  if (error instanceof NeedsLoginError) {
@@ -1381,12 +1793,14 @@ async function run(rawOptions = {}) {
1381
1793
  }
1382
1794
  print(`failed: ${error.message}`);
1383
1795
  return 1;
1796
+ } finally {
1797
+ unlock?.();
1384
1798
  }
1385
1799
  }
1386
- function flagValue(argv, flag) {
1387
- const index = argv.indexOf(flag);
1800
+ function flagValue(argv2, flag) {
1801
+ const index = argv2.indexOf(flag);
1388
1802
  if (index < 0) return void 0;
1389
- const value = argv[index + 1];
1803
+ const value = argv2[index + 1];
1390
1804
  return value && !value.startsWith("-") ? value : void 0;
1391
1805
  }
1392
1806
  function printUsage(config, print) {
@@ -1400,8 +1814,8 @@ async function runFile(file, args, printErr) {
1400
1814
  printErr("launcher --run needs a file to run.");
1401
1815
  return 1;
1402
1816
  }
1403
- const abs = (0, import_node_path4.resolve)(process.cwd(), file);
1404
- if (!(0, import_node_fs9.existsSync)(abs)) {
1817
+ const abs = (0, import_node_path6.resolve)(process.cwd(), file);
1818
+ if (!(0, import_node_fs11.existsSync)(abs)) {
1405
1819
  printErr(`cannot run ${file}: no such file.`);
1406
1820
  return 1;
1407
1821
  }
@@ -1494,14 +1908,14 @@ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
1494
1908
  }
1495
1909
  function readPayloadArgv(dir, key) {
1496
1910
  try {
1497
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1498
- const entry = parsed[key];
1499
- if (typeof entry === "string") {
1500
- const parts = entry.trim().split(/\s+/).filter(Boolean);
1911
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(payloadDir(dir), "payload.json"), "utf8"));
1912
+ const entry2 = parsed[key];
1913
+ if (typeof entry2 === "string") {
1914
+ const parts = entry2.trim().split(/\s+/).filter(Boolean);
1501
1915
  return parts.length > 0 ? parts : null;
1502
1916
  }
1503
- if (Array.isArray(entry) && entry.every((part) => typeof part === "string" && part.length > 0)) {
1504
- return entry;
1917
+ if (Array.isArray(entry2) && entry2.every((part) => typeof part === "string" && part.length > 0)) {
1918
+ return entry2;
1505
1919
  }
1506
1920
  return null;
1507
1921
  } catch {
@@ -1516,7 +1930,7 @@ function readPayloadRun(dir) {
1516
1930
  }
1517
1931
  function readPayloadVerbs(dir) {
1518
1932
  try {
1519
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1933
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(payloadDir(dir), "payload.json"), "utf8"));
1520
1934
  const verbs = parsed.verbs;
1521
1935
  if (verbs === "*") return "*";
1522
1936
  if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
@@ -1527,13 +1941,13 @@ function readPayloadVerbs(dir) {
1527
1941
  return null;
1528
1942
  }
1529
1943
  }
1530
- function resolveEntry(entry) {
1531
- return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
1944
+ function resolveEntry(entry2) {
1945
+ return entry2[0] === "$self" ? [process.execPath, ...entry2.slice(1)] : entry2;
1532
1946
  }
1533
- function needsShell(command) {
1947
+ function needsShell(command2) {
1534
1948
  if (process.platform !== "win32") return false;
1535
- if (/\.(cmd|bat)$/i.test(command)) return true;
1536
- return !(0, import_node_fs9.existsSync)(command);
1949
+ if (/\.(cmd|bat)$/i.test(command2)) return true;
1950
+ return !(0, import_node_fs11.existsSync)(command2);
1537
1951
  }
1538
1952
  function quoteForShell(arg) {
1539
1953
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1548,23 +1962,24 @@ function parseProgress(raw) {
1548
1962
  const message = JSON.parse(line);
1549
1963
  if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
1550
1964
  const state = message.state ?? "ok";
1551
- if (state !== "ok" && state !== "fail" && state !== "note") continue;
1965
+ if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
1552
1966
  if (message.ms !== void 0 && typeof message.ms !== "number") continue;
1553
1967
  progress.push({
1554
1968
  step: message.step,
1555
1969
  state,
1556
- ...typeof message.ms === "number" ? { ms: message.ms } : {}
1970
+ ...typeof message.ms === "number" && Number.isFinite(message.ms) && message.ms >= 0 ? { ms: message.ms } : {},
1971
+ ...typeof message.measure === "string" ? { measure: message.measure } : {}
1557
1972
  });
1558
1973
  } catch {
1559
1974
  }
1560
1975
  }
1561
1976
  return progress;
1562
1977
  }
1563
- function defaultRunEntry(entry, cwd, env) {
1564
- const [command, ...args] = entry;
1565
- const shell = needsShell(command);
1566
- const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1567
- const spawnEntry = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
1978
+ function defaultRunEntry(entry2, cwd, env) {
1979
+ const [command2, ...args] = entry2;
1980
+ const shell = needsShell(command2);
1981
+ const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
1982
+ const spawnEntry = (progress2) => (0, import_node_child_process4.spawnSync)(commandLine, shell ? [] : args, {
1568
1983
  cwd,
1569
1984
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1570
1985
  shell,
@@ -1622,11 +2037,49 @@ async function installOrUpdate(config, dir, options, print, update) {
1622
2037
  version = manifest.version;
1623
2038
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
1624
2039
  const current = readState(dir);
1625
- const unchanged = update && current?.version === version && (0, import_node_fs9.existsSync)(payloadDir(dir));
2040
+ const unchanged = current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
2041
+ if (unchanged && current.acquired) {
2042
+ installer.surface({ id: config.product, from: version, to: version, state: "current" });
2043
+ return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
2044
+ }
1626
2045
  if (!unchanged) {
1627
2046
  installer.phase("download", { state: "running" });
1628
2047
  started = Date.now();
1629
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
2048
+ const cached = reusableCandidate(dir, manifest);
2049
+ const candidate = cached ?? (0, import_node_crypto6.randomUUID)();
2050
+ const candidateDir = (0, import_node_path6.join)(dir, "candidates", candidate);
2051
+ const onProgress = ({ done, total }) => installer.phase("download", {
2052
+ state: "running",
2053
+ measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
2054
+ });
2055
+ if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress });
2056
+ const acquisition = (0, import_node_fs11.existsSync)((0, import_node_path6.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path6.join)(candidateDir, "payload")) : null;
2057
+ if (acquisition) {
2058
+ rememberCandidate(dir, candidate, manifest);
2059
+ installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
2060
+ installer.phase("activate", { state: "running" });
2061
+ let outcome;
2062
+ const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
2063
+ await installAcquisition(dir, candidate, version, acquisition, {
2064
+ fetchImpl,
2065
+ env,
2066
+ onProgress,
2067
+ run: async (command2, cwd, childEnv) => {
2068
+ installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
2069
+ const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
2070
+ if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
2071
+ return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
2072
+ }
2073
+ });
2074
+ installer.phase("activate");
2075
+ installer.surface({ id: config.product, from: current?.version, to: version, state: "updated" });
2076
+ installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
2077
+ return 0;
2078
+ }
2079
+ const target = (0, import_node_path6.join)(dir, "payload");
2080
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2081
+ (0, import_node_fs11.rmSync)(target, { recursive: true, force: true });
2082
+ (0, import_node_fs11.renameSync)((0, import_node_path6.join)(candidateDir, "payload"), target);
1630
2083
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1631
2084
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
1632
2085
  }
@@ -1664,36 +2117,36 @@ function payloadEnv(dir) {
1664
2117
  return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1665
2118
  }
1666
2119
  var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1667
- function payloadFileAsCommand(entry, payload) {
1668
- const first = entry[0];
2120
+ function payloadFileAsCommand(entry2, payload) {
2121
+ const first = entry2[0];
1669
2122
  if (!first || first === "$self") return null;
1670
2123
  if (first.includes("/") || first.includes("\\")) return null;
1671
- const candidate = (0, import_node_path4.join)(payload, first);
1672
- if (!(0, import_node_fs9.existsSync)(candidate)) return null;
2124
+ const candidate = (0, import_node_path6.join)(payload, first);
2125
+ if (!(0, import_node_fs11.existsSync)(candidate)) return null;
1673
2126
  if (NEVER_A_PROGRAM.test(first)) return first;
1674
2127
  if (process.platform === "win32") return null;
1675
2128
  try {
1676
- return ((0, import_node_fs9.statSync)(candidate).mode & 73) === 0 ? first : null;
2129
+ return ((0, import_node_fs11.statSync)(candidate).mode & 73) === 0 ? first : null;
1677
2130
  } catch {
1678
2131
  return null;
1679
2132
  }
1680
2133
  }
1681
- async function finishLastMile(config, dir, options, installer, version, unchanged) {
1682
- const entry = readPayloadEntry(dir);
2134
+ async function finishLastMile(config, dir, options, installer, version, unchanged, repairCommand) {
2135
+ const entry2 = repairCommand ?? readPayloadEntry(dir);
1683
2136
  const payload = payloadDir(dir);
1684
- if (!entry) {
2137
+ if (!entry2) {
1685
2138
  installer.finish({
1686
2139
  version,
1687
2140
  total: 1,
1688
2141
  updated: unchanged ? 0 : 1,
1689
2142
  failed: 0,
1690
2143
  installed: true,
1691
- detail: `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
2144
+ detail: `next step: run ${(0, import_node_path6.join)(payload, config.binName)} to start ${config.product}.`
1692
2145
  });
1693
2146
  return 0;
1694
2147
  }
1695
- const command = resolveEntry(entry);
1696
- const dataFile = payloadFileAsCommand(entry, payload);
2148
+ const command2 = resolveEntry(entry2);
2149
+ const dataFile = payloadFileAsCommand(entry2, payload);
1697
2150
  if (dataFile) {
1698
2151
  installer.finish({
1699
2152
  version,
@@ -1706,8 +2159,9 @@ async function finishLastMile(config, dir, options, installer, version, unchange
1706
2159
  }
1707
2160
  installer.phase("activate", { state: "running" });
1708
2161
  const started = Date.now();
1709
- const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
1710
- const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
2162
+ const inherited = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
2163
+ const env = repairCommand ? runtimeEnvironment(repairCommand[0], inherited) : inherited;
2164
+ const result = options.runEntry ? options.runEntry(command2, payload, env) : await runInstallEntry(command2, payload, env, installer, readTokens(dir));
1711
2165
  for (const progress of result.progress ?? []) installer.milestone(progress);
1712
2166
  const succeeded = result.ok && (result.code === void 0 || result.code === 0);
1713
2167
  const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
@@ -1722,28 +2176,33 @@ async function finishLastMile(config, dir, options, installer, version, unchange
1722
2176
  },
1723
2177
  ...!succeeded ? {
1724
2178
  operationFailed: true,
1725
- 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}` : ""}`,
1726
- retry: `(cd ${payload} && ${command.join(" ")})`
2179
+ 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}` : ""}`,
2180
+ retry: `(cd ${payload} && ${command2.join(" ")})`
1727
2181
  } : {}
1728
2182
  });
1729
2183
  return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
1730
2184
  }
1731
- async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1732
- const [command, ...args] = entry;
1733
- const shell = needsShell(command);
2185
+ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2186
+ installer.start();
2187
+ const [command2, ...args] = entry2;
2188
+ const shell = needsShell(command2);
1734
2189
  const progress = !(process.platform === "win32" && shell);
1735
- const outcomeDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
1736
- const outcomeFile = (0, import_node_path4.join)(outcomeDir, "outcome.json");
2190
+ const outcomeDir = (0, import_node_fs11.mkdtempSync)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
2191
+ const outcomeFile = (0, import_node_path6.join)(outcomeDir, "outcome.json");
2192
+ const progressFile = (0, import_node_path6.join)(outcomeDir, "progress.jsonl");
1737
2193
  const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
1738
2194
  delete childEnv.MM_FACE_TRANSCRIPT;
2195
+ delete childEnv.MM_PROGRESS_FILE;
1739
2196
  delete childEnv.MM_PROGRESS_FD;
1740
2197
  delete childEnv.MM_PROGRESS_PROTOCOL;
1741
2198
  if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
2199
+ else Object.assign(childEnv, { MM_PROGRESS_FILE: progressFile, MM_PROGRESS_PROTOCOL: "1" });
1742
2200
  const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
2201
+ let progressError;
1743
2202
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
1744
2203
  try {
1745
- const result = await new Promise((resolve2) => {
1746
- const child = (0, import_node_child_process3.spawn)(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
2204
+ const result = await new Promise((resolve3) => {
2205
+ const child = (0, import_node_child_process4.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
1747
2206
  cwd,
1748
2207
  shell,
1749
2208
  windowsHide: true,
@@ -1760,32 +2219,53 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1760
2219
  }
1761
2220
  partial = held ? safe.slice(-held) : "";
1762
2221
  const visible = held ? safe.slice(0, -held) : safe;
1763
- if (visible) installer.relay(visible, channel2, false);
2222
+ if (visible) installer.relayChunk(visible, channel2);
1764
2223
  }).on("end", () => {
1765
- if (partial) installer.relay("[redacted]", channel2, false);
2224
+ if (partial) installer.relayChunk("[redacted]", channel2);
1766
2225
  });
1767
2226
  }
1768
2227
  let pending = "";
2228
+ let progressOffset = 0;
2229
+ const progressDecoder = new import_node_string_decoder.StringDecoder("utf8");
2230
+ const receive = (text) => {
2231
+ pending += text;
2232
+ const end = pending.lastIndexOf("\n");
2233
+ if (end >= 0) {
2234
+ for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step), ...record.measure ? { measure: redact(record.measure) } : {} });
2235
+ pending = pending.slice(end + 1);
2236
+ }
2237
+ if (pending.length > 65536) pending = "";
2238
+ };
2239
+ const pollProgress = () => {
2240
+ if (progressError) return;
2241
+ try {
2242
+ if (!(0, import_node_fs11.existsSync)(progressFile)) return;
2243
+ const bytes = (0, import_node_fs11.readFileSync)(progressFile);
2244
+ receive(progressDecoder.write(bytes.subarray(progressOffset)));
2245
+ progressOffset = bytes.length;
2246
+ } catch {
2247
+ progressError = "installer progress: could not read child progress";
2248
+ if (timer) clearInterval(timer);
2249
+ }
2250
+ };
2251
+ const timer = progress ? void 0 : setInterval(pollProgress, 90);
2252
+ child.once("close", () => {
2253
+ if (timer) clearInterval(timer);
2254
+ pollProgress();
2255
+ });
1769
2256
  const channel = child.stdio[3];
1770
2257
  if (channel && "setEncoding" in channel) {
1771
2258
  channel.setEncoding("utf8");
1772
- channel.on("data", (text) => {
1773
- pending += text;
1774
- const end = pending.lastIndexOf("\n");
1775
- if (end >= 0) {
1776
- for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
1777
- pending = pending.slice(end + 1);
1778
- }
1779
- if (pending.length > 65536) pending = "";
1780
- });
2259
+ channel.on("data", receive);
1781
2260
  }
1782
- child.on("error", (error) => resolve2({ ok: false, error: error.message }));
1783
- child.on("close", (code) => resolve2({
2261
+ child.on("error", (error) => resolve3({ ok: false, error: error.message }));
2262
+ child.on("close", (code) => resolve3({
1784
2263
  ok: code === 0,
1785
2264
  ...code !== null ? { code } : {},
1786
2265
  ...code !== 0 ? { error: `exit code ${code}` } : {}
1787
2266
  }));
1788
2267
  });
2268
+ if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
1789
2269
  try {
1790
2270
  const outcome = readInstallerOutcome(outcomeFile);
1791
2271
  return { ...result, ...outcome ? { outcome } : {} };
@@ -1793,25 +2273,31 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
1793
2273
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
1794
2274
  }
1795
2275
  } finally {
1796
- if ((0, import_node_fs9.existsSync)(outcomeFile)) (0, import_node_fs9.unlinkSync)(outcomeFile);
1797
- (0, import_node_fs9.rmdirSync)(outcomeDir);
2276
+ if ((0, import_node_fs11.existsSync)(progressFile)) (0, import_node_fs11.unlinkSync)(progressFile);
2277
+ if ((0, import_node_fs11.existsSync)(outcomeFile)) (0, import_node_fs11.unlinkSync)(outcomeFile);
2278
+ (0, import_node_fs11.rmdirSync)(outcomeDir);
1798
2279
  }
1799
2280
  }
1800
- function doForward(config, dir, options, command, argv, print) {
1801
- if (command && (0, import_node_fs9.existsSync)(command)) {
1802
- print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
2281
+ function doForward(config, dir, options, command2, argv2, print, unlock) {
2282
+ if (command2 && (0, import_node_fs11.existsSync)(command2)) {
2283
+ print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
1803
2284
  return 2;
1804
2285
  }
1805
- const target = readPayloadRun(dir);
2286
+ const acquired = readState(dir)?.acquired;
2287
+ const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
1806
2288
  const verbs = readPayloadVerbs(dir);
1807
- const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
2289
+ const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
1808
2290
  if (!target || !declared) {
1809
- print(`unknown command: ${command}`);
2291
+ print(`unknown command: ${command2}`);
1810
2292
  printUsage(config, print);
1811
2293
  return 2;
1812
2294
  }
1813
- const forwarded = [...resolveEntry(target), ...argv];
1814
- const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
2295
+ const forwarded = [...resolveEntry(target), ...argv2];
2296
+ const cwd = payloadDir(dir);
2297
+ const inherited = payloadEnv(dir);
2298
+ const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2299
+ unlock();
2300
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
1815
2301
  if (!result.ok && result.code === void 0) {
1816
2302
  print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1817
2303
  return 1;
@@ -1821,10 +2307,10 @@ function doForward(config, dir, options, command, argv, print) {
1821
2307
  function doAutoupdate(config, options, args, print) {
1822
2308
  const mode = args[0] ?? "status";
1823
2309
  const scheduleOptions = options.autoupdate ?? {};
1824
- const command = options.autoupdate?.command ?? [process.execPath, "update"];
2310
+ const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
1825
2311
  if (mode === "on") {
1826
2312
  try {
1827
- enableSchedule(config, command, scheduleOptions);
2313
+ enableSchedule(config, command2, scheduleOptions);
1828
2314
  } catch (error) {
1829
2315
  print(error.message);
1830
2316
  return 1;
@@ -1856,19 +2342,24 @@ function doAutoupdate(config, options, args, print) {
1856
2342
  if (state.lastRun) print(`last run: ${state.lastRun}`);
1857
2343
  return 0;
1858
2344
  }
1859
- function doDoctor(config, dir, options, print) {
2345
+ function doDoctor(config, dir, options, print, unlock) {
1860
2346
  doLauncherDoctor(config, dir, options, print);
1861
- const target = readPayloadRun(dir);
2347
+ const acquired = readState(dir)?.acquired;
2348
+ const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
1862
2349
  const verbs = readPayloadVerbs(dir);
1863
2350
  const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1864
2351
  if (!chains) return 0;
1865
- const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
2352
+ const cwd = payloadDir(dir);
2353
+ const inherited = payloadEnv(dir);
2354
+ const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
2355
+ unlock();
2356
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
1866
2357
  return result.code ?? (result.ok ? 0 : 1);
1867
2358
  }
1868
2359
  function doLauncherDoctor(config, dir, options, print) {
1869
2360
  const tokens = readTokens(dir);
1870
2361
  const state = readState(dir);
1871
- const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
2362
+ const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
1872
2363
  print(`product: ${config.product}`);
1873
2364
  print(`host: ${config.host}`);
1874
2365
  print(`login: ${config.loginKind}`);
@@ -1880,7 +2371,7 @@ function doLauncherDoctor(config, dir, options, print) {
1880
2371
  print("token: none \u2014 run login first.");
1881
2372
  }
1882
2373
  print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
1883
- print(`paths: tokens ${(0, import_node_path4.join)(dir, "tokens.json")}, state ${(0, import_node_path4.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
2374
+ print(`paths: tokens ${(0, import_node_path6.join)(dir, "tokens.json")}, state ${(0, import_node_path6.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
1884
2375
  const schedule = querySchedule(config, options.autoupdate ?? {});
1885
2376
  print(
1886
2377
  `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`