@mutmutco/installer-launcher 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/launcher.js +654 -371
- package/dist/launcher.sea.cjs +660 -377
- package/package.json +1 -1
package/dist/launcher.js
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
5
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5
6
|
|
|
6
7
|
// src/acquisition.ts
|
|
7
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { join as
|
|
8
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
9
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
10
|
+
import { join as join4, resolve, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "node:path";
|
|
10
11
|
|
|
11
12
|
// src/runtime.ts
|
|
12
13
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
14
|
import { spawnSync } from "node:child_process";
|
|
14
15
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync, renameSync, rmSync, realpathSync } from "node:fs";
|
|
15
16
|
import { join, relative, isAbsolute } from "node:path";
|
|
17
|
+
|
|
18
|
+
// src/download.ts
|
|
19
|
+
async function readDownload(response, onProgress, expectedSize) {
|
|
20
|
+
const length = expectedSize ?? Number(response.headers.get("content-length"));
|
|
21
|
+
const total = Number.isSafeInteger(length) && length > 0 ? length : void 0;
|
|
22
|
+
let done = 0;
|
|
23
|
+
const report = () => onProgress?.({ done, ...total === void 0 ? {} : { total } });
|
|
24
|
+
report();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
if (response.body) {
|
|
27
|
+
for await (const chunk of response.body) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
done += chunk.length;
|
|
30
|
+
report();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return Buffer.concat(chunks);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/runtime.ts
|
|
16
37
|
var NODE_VERSION = "24.20.0";
|
|
17
38
|
var NPM_VERSION = "12.0.2";
|
|
18
39
|
var ARTIFACTS = {
|
|
@@ -23,10 +44,10 @@ var ARTIFACTS = {
|
|
|
23
44
|
"linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
|
|
24
45
|
};
|
|
25
46
|
var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
|
|
26
|
-
async function verifiedDownload(url, path, algorithm, digest, fetchImpl) {
|
|
47
|
+
async function verifiedDownload(url, path, algorithm, digest, fetchImpl, onProgress) {
|
|
27
48
|
const response = await fetchImpl(url);
|
|
28
49
|
if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
|
|
29
|
-
const bytes =
|
|
50
|
+
const bytes = await readDownload(response, onProgress);
|
|
30
51
|
if (createHash(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
|
|
31
52
|
writeFileSync(path, bytes);
|
|
32
53
|
}
|
|
@@ -35,7 +56,7 @@ function command(executable, args) {
|
|
|
35
56
|
if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
|
|
36
57
|
return result.stdout.trim();
|
|
37
58
|
}
|
|
38
|
-
async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
59
|
+
async function acquireRuntime(dir, fetchImpl = fetch, onProgress) {
|
|
39
60
|
const platform = `${process.platform}-${process.arch}`;
|
|
40
61
|
const artifact = ARTIFACTS[platform];
|
|
41
62
|
if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
|
|
@@ -53,13 +74,20 @@ async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
|
53
74
|
}
|
|
54
75
|
const root = mkdtempSync(join(runtimeDir, "runtime-"));
|
|
55
76
|
const archive = join(root, artifact[0]);
|
|
56
|
-
|
|
77
|
+
let downloaded = 0;
|
|
78
|
+
let currentBytes = 0;
|
|
79
|
+
const progress = ({ done }) => {
|
|
80
|
+
currentBytes = done;
|
|
81
|
+
onProgress?.({ done: downloaded + done });
|
|
82
|
+
};
|
|
83
|
+
await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl, progress);
|
|
84
|
+
downloaded += currentBytes;
|
|
57
85
|
const tar = process.platform === "win32" ? join(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
|
|
58
86
|
command(tar, ["-xf", archive, "-C", root]);
|
|
59
87
|
const unpacked = join(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
|
|
60
88
|
const node = join(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
|
|
61
89
|
const npmArchive = join(root, "npm.tgz");
|
|
62
|
-
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl);
|
|
90
|
+
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl, progress);
|
|
63
91
|
const npmRoot = join(root, "npm");
|
|
64
92
|
mkdirSync(npmRoot);
|
|
65
93
|
command(tar, ["-xf", npmArchive, "-C", npmRoot]);
|
|
@@ -164,7 +192,315 @@ function wipeProductDir(dir) {
|
|
|
164
192
|
rmSync2(payload, { force: true, recursive: true });
|
|
165
193
|
}
|
|
166
194
|
|
|
195
|
+
// src/payload.ts
|
|
196
|
+
import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
|
|
197
|
+
import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
198
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
199
|
+
import { dirname, join as join3 } from "node:path";
|
|
200
|
+
|
|
201
|
+
// src/canonical.ts
|
|
202
|
+
function canonicalJson(value) {
|
|
203
|
+
return encode(value);
|
|
204
|
+
}
|
|
205
|
+
function encode(value) {
|
|
206
|
+
if (value === null) return "null";
|
|
207
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
208
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
209
|
+
if (typeof value === "number") {
|
|
210
|
+
if (!Number.isFinite(value)) {
|
|
211
|
+
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(value);
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
217
|
+
}
|
|
218
|
+
if (typeof value === "object") {
|
|
219
|
+
const record = value;
|
|
220
|
+
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
221
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
222
|
+
}
|
|
223
|
+
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/config.ts
|
|
227
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
228
|
+
import { getAsset } from "node:sea";
|
|
229
|
+
|
|
230
|
+
// src/module-url.ts
|
|
231
|
+
import { pathToFileURL } from "node:url";
|
|
232
|
+
function moduleUrl() {
|
|
233
|
+
if (typeof __filename === "string") return pathToFileURL(__filename).href;
|
|
234
|
+
return import.meta.url;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/config.ts
|
|
238
|
+
function publicKeyBytes(config) {
|
|
239
|
+
const raw = Buffer.from(config.publicKey, "base64");
|
|
240
|
+
if (raw.length !== 32) {
|
|
241
|
+
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
242
|
+
}
|
|
243
|
+
return raw;
|
|
244
|
+
}
|
|
245
|
+
function loadProductConfig(options = {}) {
|
|
246
|
+
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
247
|
+
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
248
|
+
if (explicit) {
|
|
249
|
+
return parseProductConfig(readFileSync3(explicit, "utf8"));
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
return parseProductConfig(readFileSync3(devFallback, "utf8"));
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const asset = getAsset("product.json", "utf8");
|
|
257
|
+
if (typeof asset === "string") return parseProductConfig(asset);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function parseProductConfig(text) {
|
|
265
|
+
let data;
|
|
266
|
+
try {
|
|
267
|
+
data = JSON.parse(text);
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error("product config is not valid JSON");
|
|
270
|
+
}
|
|
271
|
+
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
272
|
+
const record = data;
|
|
273
|
+
const product = field(record, "product");
|
|
274
|
+
const host = field(record, "host").replace(/\/+$/, "");
|
|
275
|
+
const loginKind = field(record, "loginKind");
|
|
276
|
+
const binName = field(record, "binName");
|
|
277
|
+
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
278
|
+
if (loginKind !== "github" && loginKind !== "google") {
|
|
279
|
+
throw new Error('product config loginKind must be "github" or "google"');
|
|
280
|
+
}
|
|
281
|
+
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
282
|
+
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
283
|
+
publicKeyBytes(config);
|
|
284
|
+
if (loginKind === "github") {
|
|
285
|
+
const clientId = record.githubClientId;
|
|
286
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
287
|
+
throw new Error("product config needs githubClientId for the github loginKind");
|
|
288
|
+
}
|
|
289
|
+
config.githubClientId = clientId;
|
|
290
|
+
} else if (typeof record.githubClientId === "string") {
|
|
291
|
+
config.githubClientId = record.githubClientId;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const url = new URL(host);
|
|
295
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error("product config host must be an http(s) URL");
|
|
298
|
+
}
|
|
299
|
+
return config;
|
|
300
|
+
}
|
|
301
|
+
function field(record, key) {
|
|
302
|
+
const value = record[key];
|
|
303
|
+
return typeof value === "string" ? value : "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/payload.ts
|
|
307
|
+
var NeedsLoginError = class extends Error {
|
|
308
|
+
constructor() {
|
|
309
|
+
super("signed out \u2014 run login first");
|
|
310
|
+
this.name = "NeedsLoginError";
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var ForbiddenError = class extends Error {
|
|
314
|
+
constructor() {
|
|
315
|
+
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
316
|
+
this.name = "ForbiddenError";
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function canonicalManifestBytes(manifest) {
|
|
320
|
+
return Buffer.from(
|
|
321
|
+
canonicalJson({
|
|
322
|
+
created: manifest.created,
|
|
323
|
+
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
324
|
+
version: manifest.version
|
|
325
|
+
}),
|
|
326
|
+
"utf8"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
function ed25519PublicKey(config) {
|
|
330
|
+
const raw = publicKeyBytes(config);
|
|
331
|
+
return createPublicKey({
|
|
332
|
+
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
333
|
+
format: "jwk"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function verifyManifest(manifest, signature, config) {
|
|
337
|
+
try {
|
|
338
|
+
const signatureBytes = Buffer.from(signature, "base64");
|
|
339
|
+
if (signatureBytes.length === 0) return false;
|
|
340
|
+
return verify(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function verifyFileBytes(entry2, bytes) {
|
|
346
|
+
if (entry2.size !== bytes.length) return false;
|
|
347
|
+
return createHash2("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
348
|
+
}
|
|
349
|
+
async function readErrorCode(response) {
|
|
350
|
+
try {
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
return typeof data.error === "string" ? data.error : "";
|
|
353
|
+
} catch {
|
|
354
|
+
return "";
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function throwForStatus(status, errorCode) {
|
|
358
|
+
if (status === 401) throw new NeedsLoginError();
|
|
359
|
+
if (status === 403) throw new ForbiddenError();
|
|
360
|
+
throw new Error(
|
|
361
|
+
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
async function getJson(url, accessToken, fetchImpl) {
|
|
365
|
+
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
366
|
+
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
367
|
+
return { status: response.status, json: await response.json() };
|
|
368
|
+
}
|
|
369
|
+
function parseManifest(json) {
|
|
370
|
+
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
371
|
+
const record = json;
|
|
372
|
+
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
373
|
+
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
374
|
+
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
375
|
+
if (typeof record.signature !== "string" || !record.signature) {
|
|
376
|
+
throw new Error("the release manifest is unsigned");
|
|
377
|
+
}
|
|
378
|
+
const files = record.files.map((entry2) => {
|
|
379
|
+
const file = entry2;
|
|
380
|
+
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
381
|
+
throw new Error("the release manifest lists a malformed file");
|
|
382
|
+
}
|
|
383
|
+
const safe = safeManifestPath(file.path);
|
|
384
|
+
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
385
|
+
return { path: safe, sha256: file.sha256, size: file.size };
|
|
386
|
+
});
|
|
387
|
+
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
388
|
+
}
|
|
389
|
+
function safeManifestPath(rawPath) {
|
|
390
|
+
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
391
|
+
if (rawPath.startsWith("/")) return null;
|
|
392
|
+
const segments = rawPath.split("/");
|
|
393
|
+
for (const segment of segments) {
|
|
394
|
+
if (segment === "" || segment === "." || segment === "..") return null;
|
|
395
|
+
}
|
|
396
|
+
return segments.join("/");
|
|
397
|
+
}
|
|
398
|
+
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
399
|
+
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
400
|
+
const manifest = parseManifest(json);
|
|
401
|
+
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
402
|
+
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
403
|
+
}
|
|
404
|
+
return manifest;
|
|
405
|
+
}
|
|
406
|
+
async function fetchReleaseVersion(config, fetchImpl = fetch) {
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetchImpl(`${config.host}/release/version`);
|
|
409
|
+
if (!response.ok) return null;
|
|
410
|
+
const json = await response.json();
|
|
411
|
+
const version = typeof json?.version === "string" ? json.version.trim() : "";
|
|
412
|
+
return version || null;
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
|
|
418
|
+
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
419
|
+
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
420
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
424
|
+
throwForStatus(response.status, await readErrorCode(response));
|
|
425
|
+
}
|
|
426
|
+
return readDownload(response, onProgress, expectedSize);
|
|
427
|
+
}
|
|
428
|
+
var VERSION_CACHE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
429
|
+
function versionCacheRoot(dir, version) {
|
|
430
|
+
return VERSION_CACHE_RE.test(version) ? join3(dir, "payload-cache", version) : "";
|
|
431
|
+
}
|
|
432
|
+
function cachedPayloadForVersion(dir, version) {
|
|
433
|
+
const root = versionCacheRoot(dir, version);
|
|
434
|
+
if (!root) return null;
|
|
435
|
+
const payload = join3(root, "payload");
|
|
436
|
+
return existsSync4(payload) ? payload : null;
|
|
437
|
+
}
|
|
438
|
+
function rememberPayloadVersion(dir, version, payload) {
|
|
439
|
+
try {
|
|
440
|
+
const root = versionCacheRoot(dir, version);
|
|
441
|
+
if (!root) return;
|
|
442
|
+
const dest = join3(root, "payload");
|
|
443
|
+
if (existsSync4(dest)) return;
|
|
444
|
+
mkdirSync3(root, { recursive: true });
|
|
445
|
+
cpSync(payload, dest, { recursive: true });
|
|
446
|
+
} catch {
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
450
|
+
const target = join3(dir, "payload");
|
|
451
|
+
const cached = options.cacheDir ? cachedPayloadForVersion(options.cacheDir, manifest.version) : null;
|
|
452
|
+
if (cached) {
|
|
453
|
+
mkdirSync3(dir, { recursive: true });
|
|
454
|
+
rmSync3(target, { force: true, recursive: true });
|
|
455
|
+
cpSync(cached, target, { recursive: true });
|
|
456
|
+
return manifest.version;
|
|
457
|
+
}
|
|
458
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
459
|
+
const staging = join3(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
460
|
+
mkdirSync3(staging, { recursive: true });
|
|
461
|
+
try {
|
|
462
|
+
const total = manifest.files.reduce((sum, entry2) => sum + entry2.size, 0);
|
|
463
|
+
let downloaded = 0;
|
|
464
|
+
for (const entry2 of manifest.files) {
|
|
465
|
+
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl, (progress) => options.onProgress?.({ done: downloaded + progress.done, total }), entry2.size);
|
|
466
|
+
downloaded += bytes.length;
|
|
467
|
+
if (!verifyFileBytes(entry2, bytes)) {
|
|
468
|
+
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
469
|
+
}
|
|
470
|
+
const dest = join3(staging, entry2.path);
|
|
471
|
+
mkdirSync3(dirname(dest), { recursive: true });
|
|
472
|
+
writeFileSync3(dest, bytes);
|
|
473
|
+
}
|
|
474
|
+
mkdirSync3(dir, { recursive: true });
|
|
475
|
+
rmSync3(target, { force: true, recursive: true });
|
|
476
|
+
renameSync3(staging, target);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
rmSync3(staging, { force: true, recursive: true });
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
if (options.cacheDir) rememberPayloadVersion(options.cacheDir, manifest.version, target);
|
|
482
|
+
return manifest.version;
|
|
483
|
+
}
|
|
484
|
+
|
|
167
485
|
// src/acquisition.ts
|
|
486
|
+
function reusableCandidate(dir, manifest) {
|
|
487
|
+
try {
|
|
488
|
+
const saved = JSON.parse(readFileSync4(join4(dir, "acquisition-cache.json"), "utf8"));
|
|
489
|
+
if (saved.signature !== manifest.signature) return null;
|
|
490
|
+
const root = candidateRoot(dir, saved.candidate);
|
|
491
|
+
if (!existsSync5(join4(root, "acquired.json"))) return null;
|
|
492
|
+
for (const file of manifest.files) {
|
|
493
|
+
if (!safePath(file.path) || !verifyFileBytes(file, readFileSync4(join4(root, "payload", file.path)))) return null;
|
|
494
|
+
}
|
|
495
|
+
return saved.candidate;
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function rememberCandidate(dir, candidate, manifest) {
|
|
501
|
+
candidateRoot(dir, candidate);
|
|
502
|
+
writeFileSync4(join4(dir, "acquisition-cache.json"), JSON.stringify({ candidate, signature: manifest.signature }), { mode: 384 });
|
|
503
|
+
}
|
|
168
504
|
function safePath(value) {
|
|
169
505
|
return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
170
506
|
}
|
|
@@ -172,7 +508,7 @@ function argumentsValid(value) {
|
|
|
172
508
|
return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
|
|
173
509
|
}
|
|
174
510
|
function readAcquisition(payload) {
|
|
175
|
-
const metadata = JSON.parse(
|
|
511
|
+
const metadata = JSON.parse(readFileSync4(join4(payload, "payload.json"), "utf8"));
|
|
176
512
|
if (metadata.acquisition === void 0) return null;
|
|
177
513
|
const a = metadata.acquisition;
|
|
178
514
|
if (!a || a.schema !== 1 || !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(a.package) || !safePath(a.archive) || !a.archive.endsWith(".tgz") || !safePath(a.convergeEntry) || !safePath(a.rollbackEntry) || !safePath(a.runEntry) || !argumentsValid(a.convergeArgs) || !argumentsValid(a.rollbackArgs) || !argumentsValid(a.repairArgs) || !Array.isArray(a.platforms) || !a.platforms.length || a.platforms.some((p) => !["win32-x64", "win32-arm64", "darwin-arm64", "linux-x64", "linux-arm64"].includes(p))) {
|
|
@@ -188,25 +524,25 @@ function runtimeEnvironment(node, inherited, platform = process.platform) {
|
|
|
188
524
|
current = (keys.length ? env[keys[0]] : "") ?? "";
|
|
189
525
|
for (const key of keys) delete env[key];
|
|
190
526
|
}
|
|
191
|
-
env.PATH = `${
|
|
527
|
+
env.PATH = `${dirname2(node)}${platform === "win32" ? ";" : ":"}${current}`;
|
|
192
528
|
return env;
|
|
193
529
|
}
|
|
194
530
|
function lockInstallation(dir) {
|
|
195
|
-
|
|
196
|
-
const path =
|
|
531
|
+
mkdirSync4(dir, { recursive: true });
|
|
532
|
+
const path = join4(dir, "installation.lock");
|
|
197
533
|
const owner = `${process.pid}:${randomUUID3()}`;
|
|
198
534
|
try {
|
|
199
|
-
|
|
535
|
+
writeFileSync4(path, owner, { flag: "wx", mode: 384 });
|
|
200
536
|
} catch (error) {
|
|
201
537
|
if (error.code !== "EEXIST") throw error;
|
|
202
538
|
const recovery = `${path}.recovery`;
|
|
203
539
|
try {
|
|
204
|
-
|
|
540
|
+
writeFileSync4(recovery, owner, { flag: "wx", mode: 384, flush: true });
|
|
205
541
|
} catch {
|
|
206
542
|
throw new Error("installation lock recovery is already pending");
|
|
207
543
|
}
|
|
208
544
|
try {
|
|
209
|
-
const previous =
|
|
545
|
+
const previous = readFileSync4(path, "utf8");
|
|
210
546
|
const pid = Number(previous.split(":")[0]);
|
|
211
547
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
|
|
212
548
|
try {
|
|
@@ -216,29 +552,29 @@ function lockInstallation(dir) {
|
|
|
216
552
|
if (probe.code !== "ESRCH") throw probe;
|
|
217
553
|
}
|
|
218
554
|
unlinkSync(path);
|
|
219
|
-
|
|
555
|
+
writeFileSync4(path, owner, { flag: "wx", mode: 384, flush: true });
|
|
220
556
|
} finally {
|
|
221
557
|
unlinkSync(recovery);
|
|
222
558
|
}
|
|
223
559
|
}
|
|
224
560
|
let released = false;
|
|
225
561
|
return () => {
|
|
226
|
-
if (!released &&
|
|
562
|
+
if (!released && readFileSync4(path, "utf8") === owner) unlinkSync(path);
|
|
227
563
|
released = true;
|
|
228
564
|
};
|
|
229
565
|
}
|
|
230
566
|
function pendingPath(dir) {
|
|
231
|
-
return
|
|
567
|
+
return join4(dir, "acquisition-pending.json");
|
|
232
568
|
}
|
|
233
569
|
function candidateRoot(dir, candidate) {
|
|
234
570
|
if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
|
|
235
|
-
return
|
|
571
|
+
return join4(dir, "candidates", candidate);
|
|
236
572
|
}
|
|
237
573
|
function packageRoot(prefix, acquisition) {
|
|
238
|
-
return
|
|
574
|
+
return join4(prefix, "node_modules", acquisition.package);
|
|
239
575
|
}
|
|
240
576
|
function entry(root, path) {
|
|
241
|
-
const actual = realpathSync2(
|
|
577
|
+
const actual = realpathSync2(join4(root, path));
|
|
242
578
|
const rel = relative2(realpathSync2(root), actual);
|
|
243
579
|
if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("acquired entry escapes package");
|
|
244
580
|
return actual;
|
|
@@ -250,54 +586,59 @@ function acquisitionRepairCommand(dir, version) {
|
|
|
250
586
|
const state = readState(dir);
|
|
251
587
|
if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
|
|
252
588
|
const root = candidateRoot(dir, state.acquired.candidate);
|
|
253
|
-
const acquisition = readAcquisition(
|
|
589
|
+
const acquisition = readAcquisition(join4(root, "payload"));
|
|
254
590
|
if (!acquisition) throw new Error("installed acquisition declaration is missing");
|
|
255
|
-
const prefix =
|
|
591
|
+
const prefix = join4(root, "prefix");
|
|
256
592
|
const productRoot = packageRoot(prefix, acquisition);
|
|
257
|
-
const pkg = JSON.parse(
|
|
593
|
+
const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
|
|
258
594
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
|
|
259
595
|
return argv(realpathSync2(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
|
|
260
596
|
}
|
|
261
597
|
async function recoverAcquisition(dir, run2, env) {
|
|
262
|
-
if (!
|
|
263
|
-
const pending = JSON.parse(
|
|
598
|
+
if (!existsSync5(pendingPath(dir))) return;
|
|
599
|
+
const pending = JSON.parse(readFileSync4(pendingPath(dir), "utf8"));
|
|
264
600
|
if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
|
|
265
601
|
const root = candidateRoot(dir, pending.candidate);
|
|
266
602
|
if (readState(dir)?.acquired?.candidate === pending.candidate) {
|
|
267
603
|
unlinkSync(pendingPath(dir));
|
|
268
604
|
return;
|
|
269
605
|
}
|
|
270
|
-
const acquisition = readAcquisition(
|
|
606
|
+
const acquisition = readAcquisition(join4(root, "payload"));
|
|
271
607
|
if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
|
|
272
|
-
const prefix =
|
|
608
|
+
const prefix = join4(root, "prefix");
|
|
273
609
|
const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
|
|
274
610
|
if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
|
|
275
611
|
if (pending.previous) writeState(dir, pending.previous);
|
|
276
|
-
else if (
|
|
612
|
+
else if (existsSync5(statePath(dir))) unlinkSync(statePath(dir));
|
|
277
613
|
unlinkSync(pendingPath(dir));
|
|
278
614
|
}
|
|
279
615
|
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
280
616
|
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
281
617
|
const root = candidateRoot(dir, candidate);
|
|
282
|
-
const payload =
|
|
283
|
-
const prefix =
|
|
284
|
-
|
|
285
|
-
const
|
|
618
|
+
const payload = join4(root, "payload");
|
|
619
|
+
const prefix = join4(root, "prefix");
|
|
620
|
+
const prepared = join4(root, "acquired.json");
|
|
621
|
+
const archiveHash = createHash3("sha256").update(readFileSync4(join4(payload, acquisition.archive))).digest("hex");
|
|
622
|
+
const reused = existsSync5(prepared);
|
|
623
|
+
if (reused && JSON.parse(readFileSync4(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
|
|
624
|
+
if (!reused) mkdirSync4(prefix);
|
|
625
|
+
const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
|
|
286
626
|
const env = runtimeEnvironment(runtime.node, options.env);
|
|
287
627
|
const npmEnv = { ...env };
|
|
288
628
|
for (const key of Object.keys(npmEnv)) {
|
|
289
629
|
if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
|
|
290
630
|
}
|
|
291
|
-
const npmrc =
|
|
292
|
-
const globalrc =
|
|
293
|
-
|
|
294
|
-
|
|
631
|
+
const npmrc = join4(root, "public.npmrc");
|
|
632
|
+
const globalrc = join4(root, "global.npmrc");
|
|
633
|
+
writeFileSync4(npmrc, "registry=https://registry.npmjs.org/\n");
|
|
634
|
+
writeFileSync4(globalrc, "");
|
|
295
635
|
const install = [
|
|
296
636
|
runtime.node,
|
|
297
637
|
runtime.npm,
|
|
298
638
|
"install",
|
|
299
639
|
"--prefix",
|
|
300
640
|
prefix,
|
|
641
|
+
"--install-strategy=shallow",
|
|
301
642
|
"--ignore-scripts",
|
|
302
643
|
"--no-audit",
|
|
303
644
|
"--no-fund",
|
|
@@ -309,15 +650,16 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
309
650
|
"https://registry.npmjs.org/",
|
|
310
651
|
resolve(payload, acquisition.archive)
|
|
311
652
|
];
|
|
312
|
-
if (!await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
653
|
+
if (!reused && !await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
313
654
|
const productRoot = packageRoot(prefix, acquisition);
|
|
314
|
-
const pkg = JSON.parse(
|
|
655
|
+
const pkg = JSON.parse(readFileSync4(join4(productRoot, "package.json"), "utf8"));
|
|
315
656
|
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
|
|
316
657
|
const runEntry = entry(productRoot, acquisition.runEntry);
|
|
317
658
|
const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
|
|
318
659
|
entry(productRoot, acquisition.rollbackEntry);
|
|
660
|
+
if (!reused) writeFileSync4(prepared, JSON.stringify({ archiveHash }), { flag: "wx", mode: 384, flush: true });
|
|
319
661
|
const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
|
|
320
|
-
|
|
662
|
+
writeFileSync4(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
|
|
321
663
|
try {
|
|
322
664
|
if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
|
|
323
665
|
(options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
|
|
@@ -330,42 +672,42 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
|
|
|
330
672
|
|
|
331
673
|
// src/index.ts
|
|
332
674
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
333
|
-
import { existsSync as
|
|
675
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
|
|
334
676
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
335
677
|
import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
336
678
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
337
679
|
|
|
338
680
|
// ../face/src/face.ts
|
|
339
|
-
import { writeSync } from "node:fs";
|
|
681
|
+
import { appendFileSync, writeSync } from "node:fs";
|
|
340
682
|
|
|
341
683
|
// ../face/src/products.ts
|
|
342
684
|
var PRODUCTS = Object.freeze({
|
|
343
685
|
"mm-strategy": Object.freeze({
|
|
344
686
|
name: "MM Strategy",
|
|
345
|
-
installWarm: "Welcome. Let's
|
|
687
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
346
688
|
accent: "38;2;249;115;22",
|
|
347
|
-
warm: "Welcome.
|
|
689
|
+
warm: "Welcome back. Checking for updates...",
|
|
348
690
|
doctor: "mm-strategy doctor"
|
|
349
691
|
}),
|
|
350
692
|
"mmi-hub": Object.freeze({
|
|
351
693
|
name: "mmi-hub",
|
|
352
|
-
installWarm: "Welcome.
|
|
694
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
353
695
|
accent: "38;2;125;211;252",
|
|
354
|
-
warm: "Welcome back. Checking
|
|
696
|
+
warm: "Welcome back. Checking for updates...",
|
|
355
697
|
doctor: "mmi doctor"
|
|
356
698
|
}),
|
|
357
699
|
"jerv-hub": Object.freeze({
|
|
358
700
|
name: "jerv-hub",
|
|
359
|
-
installWarm: "Welcome.
|
|
701
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
360
702
|
accent: "38;2;248;113;113",
|
|
361
|
-
warm: "Welcome back. Checking
|
|
703
|
+
warm: "Welcome back. Checking for updates...",
|
|
362
704
|
doctor: "jerv doctor"
|
|
363
705
|
}),
|
|
364
706
|
jervcode: Object.freeze({
|
|
365
707
|
name: "JervCode",
|
|
366
|
-
installWarm: "Welcome.
|
|
708
|
+
installWarm: "Welcome. Let's get you ready.",
|
|
367
709
|
accent: "38;2;192;132;252",
|
|
368
|
-
warm: "Welcome back.
|
|
710
|
+
warm: "Welcome back. Checking for updates...",
|
|
369
711
|
doctor: "jervcode doctor"
|
|
370
712
|
})
|
|
371
713
|
});
|
|
@@ -379,6 +721,7 @@ function identityFor(product) {
|
|
|
379
721
|
|
|
380
722
|
// ../face/src/face.ts
|
|
381
723
|
var GLYPH = Object.freeze({
|
|
724
|
+
clock: "\u23F0",
|
|
382
725
|
diamond: "\u25C6",
|
|
383
726
|
hollow: "\u25C7",
|
|
384
727
|
bar: "\u2502",
|
|
@@ -450,12 +793,16 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
450
793
|
const continuesFace = continuedPhases.size > 0;
|
|
451
794
|
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
452
795
|
const progressFd = readProgressFd(env);
|
|
796
|
+
const progressFile = env.MM_PROGRESS_PROTOCOL === "1" ? env.MM_PROGRESS_FILE : void 0;
|
|
453
797
|
const emitMilestone = (title, measure, kind) => {
|
|
454
|
-
if (progressFd === null) return false;
|
|
798
|
+
if (progressFd === null && !progressFile) return false;
|
|
455
799
|
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
456
800
|
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
801
|
+
if (kind === "running" && typeof measure === "string") record.measure = measure;
|
|
457
802
|
try {
|
|
458
|
-
|
|
803
|
+
if (progressFile) appendFileSync(progressFile, `${JSON.stringify(record)}
|
|
804
|
+
`);
|
|
805
|
+
else writeSync(progressFd, `${JSON.stringify(record)}
|
|
459
806
|
`);
|
|
460
807
|
return true;
|
|
461
808
|
} catch {
|
|
@@ -474,7 +821,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
474
821
|
};
|
|
475
822
|
const step = (title, measure = null, kind = "ok") => {
|
|
476
823
|
if (emitMilestone(title, measure, kind)) return "";
|
|
477
|
-
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
824
|
+
const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
478
825
|
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
479
826
|
const time = measured;
|
|
480
827
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
@@ -484,7 +831,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
484
831
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
485
832
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
486
833
|
};
|
|
487
|
-
const relay = (text) => String(text).
|
|
834
|
+
const relay = (text) => String(text).replace(/\r?\n$/, "").split(/\r?\n/).flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
|
|
488
835
|
const receipt = (lines, { ready = true } = {}) => {
|
|
489
836
|
if (ready && nested) return [];
|
|
490
837
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
@@ -515,16 +862,35 @@ function createFace({ product, color = false, columns, env = process.env, operat
|
|
|
515
862
|
};
|
|
516
863
|
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
517
864
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
518
|
-
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
865
|
+
return { identity, width, nested, emitsProgress: progressFd !== null || Boolean(progressFile), welcome, continues, step, progress: (title, measure = null) => emitMilestone(title, measure, "running"), relay, receipt, outcome, signOff, refusal, paint };
|
|
519
866
|
}
|
|
520
867
|
|
|
521
868
|
// ../face/src/shell.ts
|
|
522
869
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
523
870
|
|
|
524
871
|
// ../face/src/spinner.ts
|
|
525
|
-
import { appendFileSync, writeSync as writeSync2 } from "node:fs";
|
|
872
|
+
import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
|
|
526
873
|
import { Worker } from "node:worker_threads";
|
|
527
|
-
|
|
874
|
+
|
|
875
|
+
// ../face/src/encoding.ts
|
|
876
|
+
var UNICODE_SPINNER_FRAMES = Object.freeze(["\u25D2", "\u25D0", "\u25D3", "\u25D1"]);
|
|
877
|
+
var ASCII_SPINNER_FRAMES = Object.freeze(["-", "\\", "|", "/"]);
|
|
878
|
+
function consoleRendersUnicode(env = process.env, platform = process.platform) {
|
|
879
|
+
if (env.MM_FACE_ASCII === "1" || env.NO_UNICODE === "1") return false;
|
|
880
|
+
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
|
|
881
|
+
if (locale === "C" || locale === "POSIX" || /^C\./u.test(locale) || /^POSIX\./u.test(locale)) return false;
|
|
882
|
+
if (/cp437|ibm437|\b437\b/iu.test(locale)) return false;
|
|
883
|
+
if (/utf-?8/iu.test(locale)) return true;
|
|
884
|
+
if (platform === "win32") {
|
|
885
|
+
return Boolean(env.WT_SESSION) || env.TERM_PROGRAM === "vscode" || env.CHCP === "65001" || env.ConEmuANSI === "ON";
|
|
886
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
function spinnerFramesFor(env = process.env, platform = process.platform) {
|
|
890
|
+
return consoleRendersUnicode(env, platform) ? UNICODE_SPINNER_FRAMES : ASCII_SPINNER_FRAMES;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ../face/src/spinner.ts
|
|
528
894
|
var WORKER_SOURCE = `
|
|
529
895
|
const { parentPort, workerData } = require('node:worker_threads');
|
|
530
896
|
const { writeSync, appendFileSync } = require('node:fs');
|
|
@@ -534,7 +900,7 @@ function draw() {
|
|
|
534
900
|
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
535
901
|
try {
|
|
536
902
|
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
537
|
-
const text = frames[frame++ % frames.length];
|
|
903
|
+
const text = frames[frame++ % frames.length].replace('__elapsed__', Math.floor((Date.now() - workerData.started) / 1000) + 's');
|
|
538
904
|
writeSync(2, text);
|
|
539
905
|
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
540
906
|
} finally {
|
|
@@ -546,31 +912,35 @@ parentPort.on('message', (next) => {
|
|
|
546
912
|
if (Atomics.load(control, 2)) return;
|
|
547
913
|
frames = next.frames;
|
|
548
914
|
frame = next.frame;
|
|
549
|
-
Atomics.store(control, 0, 0);
|
|
550
915
|
});
|
|
551
916
|
setInterval(draw, workerData.intervalMs);
|
|
552
917
|
`;
|
|
553
|
-
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
|
|
918
|
+
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath, env = process.env, platform = process.platform }) {
|
|
554
919
|
animate = animate && !face.nested && !face.emitsProgress;
|
|
920
|
+
const glyphs = spinnerFramesFor(env, platform);
|
|
921
|
+
const ascii = glyphs[0] === ASCII_SPINNER_FRAMES[0];
|
|
555
922
|
let timer = null;
|
|
556
923
|
let worker = null;
|
|
557
924
|
let control = null;
|
|
558
925
|
let frames = [];
|
|
559
926
|
let frame = 0;
|
|
927
|
+
let started = 0;
|
|
928
|
+
let suspended = false;
|
|
560
929
|
const write = (text) => {
|
|
561
930
|
if (stream) stream.write(text);
|
|
562
931
|
else {
|
|
563
932
|
writeSync2(2, text);
|
|
564
|
-
if (transcriptPath)
|
|
933
|
+
if (transcriptPath) appendFileSync2(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
565
934
|
`);
|
|
566
935
|
}
|
|
567
936
|
};
|
|
568
937
|
const render = (text, measure) => {
|
|
569
|
-
const line = face.step(text, measure, "note").split("\n")[0];
|
|
570
|
-
|
|
938
|
+
const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
|
|
939
|
+
const prepared = line && ascii ? line.replaceAll(GLYPH.bar, "|") : line;
|
|
940
|
+
frames = prepared ? glyphs.map((glyph) => `\r\x1B[2K${prepared.replace(GLYPH.dot, glyph)}`) : [];
|
|
571
941
|
};
|
|
572
942
|
const draw = () => {
|
|
573
|
-
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
943
|
+
if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
|
|
574
944
|
};
|
|
575
945
|
const pause = () => {
|
|
576
946
|
if (!control) return;
|
|
@@ -590,6 +960,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
590
960
|
start(text, measure = null) {
|
|
591
961
|
if (!animate) return;
|
|
592
962
|
halt();
|
|
963
|
+
suspended = false;
|
|
964
|
+
started = Date.now();
|
|
593
965
|
render(text, measure);
|
|
594
966
|
frame = 0;
|
|
595
967
|
draw();
|
|
@@ -603,7 +975,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
603
975
|
frames,
|
|
604
976
|
frame,
|
|
605
977
|
intervalMs,
|
|
606
|
-
transcriptPath
|
|
978
|
+
transcriptPath,
|
|
979
|
+
started
|
|
607
980
|
} });
|
|
608
981
|
const active = worker;
|
|
609
982
|
worker.on("error", () => {
|
|
@@ -621,23 +994,35 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
|
|
|
621
994
|
render(text, measure);
|
|
622
995
|
draw();
|
|
623
996
|
worker?.postMessage({ frames, frame });
|
|
997
|
+
if (control && !suspended) Atomics.store(control, 0, 0);
|
|
998
|
+
},
|
|
999
|
+
pause() {
|
|
1000
|
+
if (suspended) return;
|
|
1001
|
+
suspended = true;
|
|
1002
|
+
pause();
|
|
1003
|
+
if (animate && frames.length) write("\r\x1B[2K");
|
|
1004
|
+
},
|
|
1005
|
+
resume() {
|
|
1006
|
+
suspended = false;
|
|
1007
|
+
if (control) Atomics.store(control, 0, 0);
|
|
1008
|
+
if (timer || worker) draw();
|
|
624
1009
|
},
|
|
625
1010
|
stop() {
|
|
626
1011
|
halt();
|
|
1012
|
+
frames = [];
|
|
627
1013
|
if (animate) write("\r\x1B[2K");
|
|
628
1014
|
}
|
|
629
1015
|
};
|
|
630
1016
|
}
|
|
631
|
-
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
632
1017
|
|
|
633
1018
|
// ../face/src/conformance.ts
|
|
634
1019
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
635
1020
|
|
|
636
1021
|
// ../face/src/run.ts
|
|
637
|
-
import { appendFileSync as
|
|
1022
|
+
import { appendFileSync as appendFileSync3 } from "node:fs";
|
|
638
1023
|
|
|
639
1024
|
// ../face/src/outcome.ts
|
|
640
|
-
import { readFileSync as
|
|
1025
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
641
1026
|
var counts = ["total", "updated", "failed"];
|
|
642
1027
|
var strings = ["version", "retry", "detail", "logPath"];
|
|
643
1028
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
@@ -659,12 +1044,12 @@ function validateInstallerOutcome(value) {
|
|
|
659
1044
|
return { ...facts };
|
|
660
1045
|
}
|
|
661
1046
|
function writeInstallerOutcome(path, value) {
|
|
662
|
-
|
|
1047
|
+
writeFileSync5(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
663
1048
|
}
|
|
664
1049
|
function readInstallerOutcome(path) {
|
|
665
1050
|
let text;
|
|
666
1051
|
try {
|
|
667
|
-
text =
|
|
1052
|
+
text = readFileSync5(path, "utf8");
|
|
668
1053
|
} catch (error) {
|
|
669
1054
|
if (error.code === "ENOENT") return void 0;
|
|
670
1055
|
throw error;
|
|
@@ -727,10 +1112,19 @@ var PHASES = {
|
|
|
727
1112
|
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
728
1113
|
verify: ["Verifying the payload", "Verified the payload"],
|
|
729
1114
|
install: ["Installing the product", "Installed the product"],
|
|
1115
|
+
configure: ["Configuring the product", "Configured the product"],
|
|
730
1116
|
activate: ["Activating surfaces", "Activated surfaces"],
|
|
731
1117
|
doctor: ["Checking health", "Checked health"],
|
|
732
1118
|
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
733
1119
|
};
|
|
1120
|
+
var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
|
|
1121
|
+
"preflight",
|
|
1122
|
+
"resolve",
|
|
1123
|
+
"verify-release",
|
|
1124
|
+
"download",
|
|
1125
|
+
"sign-in",
|
|
1126
|
+
"check"
|
|
1127
|
+
]);
|
|
734
1128
|
function createInstallerRun(value, options = {}) {
|
|
735
1129
|
const declaration = validateInstallerProduct(value);
|
|
736
1130
|
const env = options.env ?? process.env;
|
|
@@ -756,7 +1150,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
756
1150
|
if (!text) return;
|
|
757
1151
|
write(text, channel);
|
|
758
1152
|
if (env.MM_FACE_TRANSCRIPT) {
|
|
759
|
-
|
|
1153
|
+
appendFileSync3(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
760
1154
|
`, "utf8");
|
|
761
1155
|
}
|
|
762
1156
|
};
|
|
@@ -766,13 +1160,26 @@ function createInstallerRun(value, options = {}) {
|
|
|
766
1160
|
};
|
|
767
1161
|
const spinner = createSpinner(face, {
|
|
768
1162
|
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
1163
|
+
env,
|
|
769
1164
|
...options.write ? { stream: { write: (text) => {
|
|
770
1165
|
emit(String(text), "spinner");
|
|
771
1166
|
return true;
|
|
772
1167
|
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
773
1168
|
});
|
|
1169
|
+
let activeTitle;
|
|
1170
|
+
let chunkOpen = false;
|
|
1171
|
+
let chunkChannel = "stdout";
|
|
1172
|
+
let chunkColumn = 0;
|
|
1173
|
+
const trailingCR = /* @__PURE__ */ new Set();
|
|
1174
|
+
let deferredActivity;
|
|
1175
|
+
const closeChunk = () => {
|
|
1176
|
+
if (chunkOpen) emit("\n", chunkChannel);
|
|
1177
|
+
chunkOpen = false;
|
|
1178
|
+
chunkColumn = 0;
|
|
1179
|
+
};
|
|
774
1180
|
let started = false;
|
|
775
1181
|
let finished = false;
|
|
1182
|
+
let operation = options.operation;
|
|
776
1183
|
const start = () => {
|
|
777
1184
|
if (started || finished) return;
|
|
778
1185
|
started = true;
|
|
@@ -780,11 +1187,14 @@ function createInstallerRun(value, options = {}) {
|
|
|
780
1187
|
const welcome = face.welcome();
|
|
781
1188
|
if (tty) lines(welcome);
|
|
782
1189
|
else if (welcome.length) {
|
|
783
|
-
const warm =
|
|
1190
|
+
const warm = operation === "install" ? face.identity.installWarm : face.identity.warm;
|
|
784
1191
|
lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
|
|
785
1192
|
}
|
|
786
1193
|
};
|
|
787
1194
|
const durable = (title, measure, kind) => {
|
|
1195
|
+
closeChunk();
|
|
1196
|
+
activeTitle = void 0;
|
|
1197
|
+
deferredActivity = void 0;
|
|
788
1198
|
spinner.stop();
|
|
789
1199
|
const rendered = face.step(title, measure, kind);
|
|
790
1200
|
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
@@ -793,15 +1203,31 @@ function createInstallerRun(value, options = {}) {
|
|
|
793
1203
|
get errors() {
|
|
794
1204
|
return [...errors];
|
|
795
1205
|
},
|
|
1206
|
+
get operation() {
|
|
1207
|
+
return operation;
|
|
1208
|
+
},
|
|
1209
|
+
set operation(value2) {
|
|
1210
|
+
operation = value2;
|
|
1211
|
+
},
|
|
796
1212
|
start,
|
|
797
1213
|
phase(id, facts = {}) {
|
|
798
1214
|
if (finished) throw new Error("installer run already finished");
|
|
799
1215
|
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
800
1216
|
start();
|
|
801
1217
|
const state = facts.state ?? "ok";
|
|
802
|
-
const
|
|
1218
|
+
const fold = operation === "update" && UPDATE_CHECK_PHASES.has(id) && state !== "fail";
|
|
1219
|
+
const title = PHASES[fold ? "check" : id][state === "ok" ? 1 : 0];
|
|
803
1220
|
if (state === "running") {
|
|
804
|
-
|
|
1221
|
+
if (face.progress(title, facts.measure ?? null)) return;
|
|
1222
|
+
if (activeTitle === title) spinner.say(title, facts.measure ?? null);
|
|
1223
|
+
else {
|
|
1224
|
+
activeTitle = title;
|
|
1225
|
+
spinner.start(title, facts.measure ?? null);
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (fold && id !== "check") {
|
|
1230
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
805
1231
|
return;
|
|
806
1232
|
}
|
|
807
1233
|
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
@@ -821,8 +1247,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
821
1247
|
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
822
1248
|
if (facts.detail) run2.relay(facts.detail);
|
|
823
1249
|
},
|
|
824
|
-
milestone({ step, state, ms }) {
|
|
1250
|
+
milestone({ step, state, ms, measure }) {
|
|
825
1251
|
start();
|
|
1252
|
+
if (state === "running") {
|
|
1253
|
+
if (chunkOpen) {
|
|
1254
|
+
deferredActivity = { title: step, measure: measure ?? null };
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (activeTitle === step) spinner.say(step, measure ?? null);
|
|
1258
|
+
else {
|
|
1259
|
+
activeTitle = step;
|
|
1260
|
+
spinner.start(step, measure ?? null);
|
|
1261
|
+
}
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
826
1264
|
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
827
1265
|
},
|
|
828
1266
|
signIn({ url, code }) {
|
|
@@ -836,7 +1274,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
836
1274
|
},
|
|
837
1275
|
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
838
1276
|
relay(text, channel = "stdout", record = true) {
|
|
839
|
-
|
|
1277
|
+
closeChunk();
|
|
1278
|
+
spinner.pause();
|
|
840
1279
|
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
841
1280
|
for (const row of rendered.split("\n")) if (row) {
|
|
842
1281
|
emit(`${row}
|
|
@@ -844,6 +1283,37 @@ function createInstallerRun(value, options = {}) {
|
|
|
844
1283
|
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
845
1284
|
`);
|
|
846
1285
|
}
|
|
1286
|
+
spinner.resume();
|
|
1287
|
+
},
|
|
1288
|
+
relayChunk(text, channel = "stdout") {
|
|
1289
|
+
spinner.pause();
|
|
1290
|
+
if (chunkOpen && chunkChannel !== channel) closeChunk();
|
|
1291
|
+
chunkChannel = channel;
|
|
1292
|
+
if (trailingCR.delete(channel) && text.startsWith("\n")) text = text.slice(1);
|
|
1293
|
+
if (text.endsWith("\r")) trailingCR.add(channel);
|
|
1294
|
+
const parts = text.replace(/\r\n|\r/g, "\n").split("\n");
|
|
1295
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1296
|
+
const characters = [...stripColor(parts[i])];
|
|
1297
|
+
while (characters.length) {
|
|
1298
|
+
if (tty && chunkColumn >= face.width - 6) closeChunk();
|
|
1299
|
+
const part = characters.splice(0, tty ? face.width - 6 - chunkColumn : characters.length).join("");
|
|
1300
|
+
emit(tty && !chunkOpen ? face.relay(part) : part, channel, chunkOpen ? "" : tty ? face.relay("[external output omitted]") : "[external output omitted]");
|
|
1301
|
+
chunkColumn += [...part].length;
|
|
1302
|
+
chunkOpen = true;
|
|
1303
|
+
}
|
|
1304
|
+
if (i < parts.length - 1) {
|
|
1305
|
+
emit("\n", channel);
|
|
1306
|
+
chunkOpen = false;
|
|
1307
|
+
chunkColumn = 0;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (!chunkOpen) {
|
|
1311
|
+
if (deferredActivity) {
|
|
1312
|
+
activeTitle = deferredActivity.title;
|
|
1313
|
+
spinner.start(deferredActivity.title, deferredActivity.measure);
|
|
1314
|
+
deferredActivity = void 0;
|
|
1315
|
+
} else spinner.resume();
|
|
1316
|
+
}
|
|
847
1317
|
},
|
|
848
1318
|
cancel() {
|
|
849
1319
|
if (finished) return;
|
|
@@ -852,6 +1322,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
852
1322
|
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
853
1323
|
);
|
|
854
1324
|
start();
|
|
1325
|
+
closeChunk();
|
|
1326
|
+
activeTitle = void 0;
|
|
855
1327
|
spinner.stop();
|
|
856
1328
|
finished = true;
|
|
857
1329
|
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
@@ -864,6 +1336,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
864
1336
|
validateInstallerOutcome(facts);
|
|
865
1337
|
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
866
1338
|
start();
|
|
1339
|
+
closeChunk();
|
|
1340
|
+
activeTitle = void 0;
|
|
867
1341
|
spinner.stop();
|
|
868
1342
|
finished = true;
|
|
869
1343
|
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
@@ -884,6 +1358,8 @@ function createInstallerRun(value, options = {}) {
|
|
|
884
1358
|
if (tty) lines([face.signOff()]);
|
|
885
1359
|
},
|
|
886
1360
|
stop() {
|
|
1361
|
+
closeChunk();
|
|
1362
|
+
activeTitle = void 0;
|
|
887
1363
|
spinner.stop();
|
|
888
1364
|
}
|
|
889
1365
|
};
|
|
@@ -892,9 +1368,9 @@ function createInstallerRun(value, options = {}) {
|
|
|
892
1368
|
|
|
893
1369
|
// src/autoupdate.ts
|
|
894
1370
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
895
|
-
import { existsSync as
|
|
896
|
-
import { tmpdir as
|
|
897
|
-
import { join as
|
|
1371
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1372
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1373
|
+
import { join as join5 } from "node:path";
|
|
898
1374
|
function schedulePlatform(override) {
|
|
899
1375
|
const platform = override ?? process.platform;
|
|
900
1376
|
if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
|
|
@@ -911,8 +1387,8 @@ function defaultExec(command2, args) {
|
|
|
911
1387
|
}
|
|
912
1388
|
function homeOf(options) {
|
|
913
1389
|
if (options.homeDir) return options.homeDir;
|
|
914
|
-
if (process.platform === "win32") return process.env.USERPROFILE ??
|
|
915
|
-
return process.env.HOME ??
|
|
1390
|
+
if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir3();
|
|
1391
|
+
return process.env.HOME ?? tmpdir3();
|
|
916
1392
|
}
|
|
917
1393
|
function scheduleName(config) {
|
|
918
1394
|
return `${config.binName} autoupdate`;
|
|
@@ -938,10 +1414,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
938
1414
|
}
|
|
939
1415
|
if (platform === "darwin") {
|
|
940
1416
|
const label2 = scheduleLabel(config);
|
|
941
|
-
const dir2 =
|
|
942
|
-
|
|
943
|
-
const plist =
|
|
944
|
-
|
|
1417
|
+
const dir2 = join5(home, "Library", "LaunchAgents");
|
|
1418
|
+
mkdirSync5(dir2, { recursive: true });
|
|
1419
|
+
const plist = join5(dir2, `${label2}.plist`);
|
|
1420
|
+
writeFileSync6(plist, darwinPlist(label2, command2));
|
|
945
1421
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
946
1422
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
947
1423
|
if (result2.code !== 0) {
|
|
@@ -950,10 +1426,10 @@ function enableSchedule(config, command2, options = {}) {
|
|
|
950
1426
|
return;
|
|
951
1427
|
}
|
|
952
1428
|
const label = scheduleLabel(config);
|
|
953
|
-
const dir =
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1429
|
+
const dir = join5(home, ".config", "systemd", "user");
|
|
1430
|
+
mkdirSync5(dir, { recursive: true });
|
|
1431
|
+
writeFileSync6(join5(dir, `${label}.service`), linuxService(command2));
|
|
1432
|
+
writeFileSync6(join5(dir, `${label}.timer`), linuxTimer(label));
|
|
957
1433
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
958
1434
|
if (reload.code !== 0) {
|
|
959
1435
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -978,14 +1454,14 @@ function disableSchedule(config, options = {}) {
|
|
|
978
1454
|
if (platform === "darwin") {
|
|
979
1455
|
const label2 = scheduleLabel(config);
|
|
980
1456
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
981
|
-
|
|
1457
|
+
rmSync4(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
982
1458
|
return;
|
|
983
1459
|
}
|
|
984
1460
|
const label = scheduleLabel(config);
|
|
985
|
-
const dir =
|
|
1461
|
+
const dir = join5(home, ".config", "systemd", "user");
|
|
986
1462
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
987
|
-
|
|
988
|
-
|
|
1463
|
+
rmSync4(join5(dir, `${label}.service`), { force: true });
|
|
1464
|
+
rmSync4(join5(dir, `${label}.timer`), { force: true });
|
|
989
1465
|
}
|
|
990
1466
|
function querySchedule(config, options = {}) {
|
|
991
1467
|
const platform = schedulePlatform(options.platform);
|
|
@@ -1003,12 +1479,12 @@ function querySchedule(config, options = {}) {
|
|
|
1003
1479
|
return state2;
|
|
1004
1480
|
}
|
|
1005
1481
|
if (platform === "darwin") {
|
|
1006
|
-
const plist =
|
|
1007
|
-
if (!
|
|
1482
|
+
const plist = join5(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
1483
|
+
if (!existsSync6(plist)) return { supported: true, enabled: false };
|
|
1008
1484
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
1009
1485
|
}
|
|
1010
|
-
const timer =
|
|
1011
|
-
if (!
|
|
1486
|
+
const timer = join5(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
1487
|
+
if (!existsSync6(timer)) return { supported: true, enabled: false };
|
|
1012
1488
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
1013
1489
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
1014
1490
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -1065,86 +1541,6 @@ function firstLine(text) {
|
|
|
1065
1541
|
return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
|
|
1066
1542
|
}
|
|
1067
1543
|
|
|
1068
|
-
// src/config.ts
|
|
1069
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1070
|
-
import { getAsset } from "node:sea";
|
|
1071
|
-
|
|
1072
|
-
// src/module-url.ts
|
|
1073
|
-
import { pathToFileURL } from "node:url";
|
|
1074
|
-
function moduleUrl() {
|
|
1075
|
-
if (typeof __filename === "string") return pathToFileURL(__filename).href;
|
|
1076
|
-
return import.meta.url;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// src/config.ts
|
|
1080
|
-
function publicKeyBytes(config) {
|
|
1081
|
-
const raw = Buffer.from(config.publicKey, "base64");
|
|
1082
|
-
if (raw.length !== 32) {
|
|
1083
|
-
throw new Error(`product config for "${config.product}" has a bad publicKey (want 32 raw bytes)`);
|
|
1084
|
-
}
|
|
1085
|
-
return raw;
|
|
1086
|
-
}
|
|
1087
|
-
function loadProductConfig(options = {}) {
|
|
1088
|
-
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
1089
|
-
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
1090
|
-
if (explicit) {
|
|
1091
|
-
return parseProductConfig(readFileSync6(explicit, "utf8"));
|
|
1092
|
-
}
|
|
1093
|
-
try {
|
|
1094
|
-
return parseProductConfig(readFileSync6(devFallback, "utf8"));
|
|
1095
|
-
} catch {
|
|
1096
|
-
}
|
|
1097
|
-
try {
|
|
1098
|
-
const asset = getAsset("product.json", "utf8");
|
|
1099
|
-
if (typeof asset === "string") return parseProductConfig(asset);
|
|
1100
|
-
} catch {
|
|
1101
|
-
}
|
|
1102
|
-
throw new Error(
|
|
1103
|
-
"no product config found (pass --config <path>, set LAUNCHER_CONFIG, or bake product.json into the SEA binary)"
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
function parseProductConfig(text) {
|
|
1107
|
-
let data;
|
|
1108
|
-
try {
|
|
1109
|
-
data = JSON.parse(text);
|
|
1110
|
-
} catch {
|
|
1111
|
-
throw new Error("product config is not valid JSON");
|
|
1112
|
-
}
|
|
1113
|
-
if (typeof data !== "object" || data === null) throw new Error("product config must be an object");
|
|
1114
|
-
const record = data;
|
|
1115
|
-
const product = field(record, "product");
|
|
1116
|
-
const host = field(record, "host").replace(/\/+$/, "");
|
|
1117
|
-
const loginKind = field(record, "loginKind");
|
|
1118
|
-
const binName = field(record, "binName");
|
|
1119
|
-
if (!product || !host || !binName) throw new Error("product config needs product, host and binName");
|
|
1120
|
-
if (loginKind !== "github" && loginKind !== "google") {
|
|
1121
|
-
throw new Error('product config loginKind must be "github" or "google"');
|
|
1122
|
-
}
|
|
1123
|
-
const config = { product, host, loginKind, publicKey: field(record, "publicKey"), binName };
|
|
1124
|
-
if (!config.publicKey) throw new Error("product config needs publicKey (base64 raw Ed25519)");
|
|
1125
|
-
publicKeyBytes(config);
|
|
1126
|
-
if (loginKind === "github") {
|
|
1127
|
-
const clientId = record.githubClientId;
|
|
1128
|
-
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
1129
|
-
throw new Error("product config needs githubClientId for the github loginKind");
|
|
1130
|
-
}
|
|
1131
|
-
config.githubClientId = clientId;
|
|
1132
|
-
} else if (typeof record.githubClientId === "string") {
|
|
1133
|
-
config.githubClientId = record.githubClientId;
|
|
1134
|
-
}
|
|
1135
|
-
try {
|
|
1136
|
-
const url = new URL(host);
|
|
1137
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error();
|
|
1138
|
-
} catch {
|
|
1139
|
-
throw new Error("product config host must be an http(s) URL");
|
|
1140
|
-
}
|
|
1141
|
-
return config;
|
|
1142
|
-
}
|
|
1143
|
-
function field(record, key) {
|
|
1144
|
-
const value = record[key];
|
|
1145
|
-
return typeof value === "string" ? value : "";
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
1544
|
// src/login-github.ts
|
|
1149
1545
|
import { spawn } from "node:child_process";
|
|
1150
1546
|
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
@@ -1267,7 +1663,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
1267
1663
|
}
|
|
1268
1664
|
|
|
1269
1665
|
// src/login-google.ts
|
|
1270
|
-
import { createHash as
|
|
1666
|
+
import { createHash as createHash4, randomBytes } from "node:crypto";
|
|
1271
1667
|
import { createServer } from "node:http";
|
|
1272
1668
|
var b64url = (bytes) => bytes.toString("base64url");
|
|
1273
1669
|
async function loginGoogle(options) {
|
|
@@ -1290,7 +1686,7 @@ async function loginGoogle(options) {
|
|
|
1290
1686
|
throw new Error("the sign-in server returned a malformed registration");
|
|
1291
1687
|
}
|
|
1292
1688
|
const verifier = b64url(randomBytes(32));
|
|
1293
|
-
const challenge = b64url(
|
|
1689
|
+
const challenge = b64url(createHash4("sha256").update(verifier).digest());
|
|
1294
1690
|
const state = b64url(randomBytes(16));
|
|
1295
1691
|
const authorize = new URL(`${server}/oauth/authorize`);
|
|
1296
1692
|
authorize.search = new URLSearchParams({
|
|
@@ -1379,175 +1775,8 @@ function page(title, body) {
|
|
|
1379
1775
|
return `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="dark"><title>${title}</title><style>${style}</style><main><h1>${title}</h1><p>${body}</p></main>`;
|
|
1380
1776
|
}
|
|
1381
1777
|
|
|
1382
|
-
// src/payload.ts
|
|
1383
|
-
import { createHash as createHash3, createPublicKey, verify } from "node:crypto";
|
|
1384
|
-
import { mkdirSync as mkdirSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1385
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
1386
|
-
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1387
|
-
|
|
1388
|
-
// src/canonical.ts
|
|
1389
|
-
function canonicalJson(value) {
|
|
1390
|
-
return encode(value);
|
|
1391
|
-
}
|
|
1392
|
-
function encode(value) {
|
|
1393
|
-
if (value === null) return "null";
|
|
1394
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1395
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1396
|
-
if (typeof value === "number") {
|
|
1397
|
-
if (!Number.isFinite(value)) {
|
|
1398
|
-
throw new TypeError("canonicalJson: cannot encode a non-finite number");
|
|
1399
|
-
}
|
|
1400
|
-
return JSON.stringify(value);
|
|
1401
|
-
}
|
|
1402
|
-
if (Array.isArray(value)) {
|
|
1403
|
-
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
1404
|
-
}
|
|
1405
|
-
if (typeof value === "object") {
|
|
1406
|
-
const record = value;
|
|
1407
|
-
const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
|
|
1408
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`).join(",")}}`;
|
|
1409
|
-
}
|
|
1410
|
-
throw new TypeError(`canonicalJson: cannot encode a value of type ${typeof value}`);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// src/payload.ts
|
|
1414
|
-
var NeedsLoginError = class extends Error {
|
|
1415
|
-
constructor() {
|
|
1416
|
-
super("signed out \u2014 run login first");
|
|
1417
|
-
this.name = "NeedsLoginError";
|
|
1418
|
-
}
|
|
1419
|
-
};
|
|
1420
|
-
var ForbiddenError = class extends Error {
|
|
1421
|
-
constructor() {
|
|
1422
|
-
super("this install is not allowed for your account \u2014 access was revoked or never granted.");
|
|
1423
|
-
this.name = "ForbiddenError";
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
function canonicalManifestBytes(manifest) {
|
|
1427
|
-
return Buffer.from(
|
|
1428
|
-
canonicalJson({
|
|
1429
|
-
created: manifest.created,
|
|
1430
|
-
files: manifest.files.map((file) => ({ path: file.path, sha256: file.sha256, size: file.size })),
|
|
1431
|
-
version: manifest.version
|
|
1432
|
-
}),
|
|
1433
|
-
"utf8"
|
|
1434
|
-
);
|
|
1435
|
-
}
|
|
1436
|
-
function ed25519PublicKey(config) {
|
|
1437
|
-
const raw = publicKeyBytes(config);
|
|
1438
|
-
return createPublicKey({
|
|
1439
|
-
key: { kty: "OKP", crv: "Ed25519", x: raw.toString("base64url") },
|
|
1440
|
-
format: "jwk"
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1443
|
-
function verifyManifest(manifest, signature, config) {
|
|
1444
|
-
try {
|
|
1445
|
-
const signatureBytes = Buffer.from(signature, "base64");
|
|
1446
|
-
if (signatureBytes.length === 0) return false;
|
|
1447
|
-
return verify(null, canonicalManifestBytes(manifest), ed25519PublicKey(config), signatureBytes);
|
|
1448
|
-
} catch {
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function verifyFileBytes(entry2, bytes) {
|
|
1453
|
-
if (entry2.size !== bytes.length) return false;
|
|
1454
|
-
return createHash3("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
1455
|
-
}
|
|
1456
|
-
async function readErrorCode(response) {
|
|
1457
|
-
try {
|
|
1458
|
-
const data = await response.json();
|
|
1459
|
-
return typeof data.error === "string" ? data.error : "";
|
|
1460
|
-
} catch {
|
|
1461
|
-
return "";
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
function throwForStatus(status, errorCode) {
|
|
1465
|
-
if (status === 401) throw new NeedsLoginError();
|
|
1466
|
-
if (status === 403) throw new ForbiddenError();
|
|
1467
|
-
throw new Error(
|
|
1468
|
-
errorCode ? `the release server refused the request (${status}: ${errorCode})` : `the release server refused the request (${status})`
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
async function getJson(url, accessToken, fetchImpl) {
|
|
1472
|
-
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
1473
|
-
if (!response.ok) throwForStatus(response.status, await readErrorCode(response));
|
|
1474
|
-
return { status: response.status, json: await response.json() };
|
|
1475
|
-
}
|
|
1476
|
-
function parseManifest(json) {
|
|
1477
|
-
if (typeof json !== "object" || json === null) throw new Error("the release manifest is not an object");
|
|
1478
|
-
const record = json;
|
|
1479
|
-
if (typeof record.version !== "string" || !record.version) throw new Error("the release manifest has no version");
|
|
1480
|
-
if (typeof record.created !== "string" || !record.created) throw new Error("the release manifest has no created stamp");
|
|
1481
|
-
if (!Array.isArray(record.files)) throw new Error("the release manifest has no files list");
|
|
1482
|
-
if (typeof record.signature !== "string" || !record.signature) {
|
|
1483
|
-
throw new Error("the release manifest is unsigned");
|
|
1484
|
-
}
|
|
1485
|
-
const files = record.files.map((entry2) => {
|
|
1486
|
-
const file = entry2;
|
|
1487
|
-
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
1488
|
-
throw new Error("the release manifest lists a malformed file");
|
|
1489
|
-
}
|
|
1490
|
-
const safe = safeManifestPath(file.path);
|
|
1491
|
-
if (safe === null) throw new Error(`the release manifest lists an unsafe path: ${file.path}`);
|
|
1492
|
-
return { path: safe, sha256: file.sha256, size: file.size };
|
|
1493
|
-
});
|
|
1494
|
-
return { version: record.version, created: record.created, files, signature: record.signature };
|
|
1495
|
-
}
|
|
1496
|
-
function safeManifestPath(rawPath) {
|
|
1497
|
-
if (rawPath.length === 0 || rawPath.includes("\0") || rawPath.includes("\\")) return null;
|
|
1498
|
-
if (rawPath.startsWith("/")) return null;
|
|
1499
|
-
const segments = rawPath.split("/");
|
|
1500
|
-
for (const segment of segments) {
|
|
1501
|
-
if (segment === "" || segment === "." || segment === "..") return null;
|
|
1502
|
-
}
|
|
1503
|
-
return segments.join("/");
|
|
1504
|
-
}
|
|
1505
|
-
async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
|
|
1506
|
-
const { json } = await getJson(`${config.host}/release/manifest`, accessToken, fetchImpl);
|
|
1507
|
-
const manifest = parseManifest(json);
|
|
1508
|
-
if (!verifyManifest(manifest, manifest.signature, config)) {
|
|
1509
|
-
throw new Error("the release manifest signature is invalid \u2014 refusing to install anything.");
|
|
1510
|
-
}
|
|
1511
|
-
return manifest;
|
|
1512
|
-
}
|
|
1513
|
-
async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
1514
|
-
const encoded = path.split("/").map(encodeURIComponent).join("/");
|
|
1515
|
-
const response = await fetchImpl(`${config.host}/release/${encoded}`, {
|
|
1516
|
-
headers: { authorization: `Bearer ${accessToken}` }
|
|
1517
|
-
});
|
|
1518
|
-
if (!response.ok) {
|
|
1519
|
-
if (response.status === 404) throw new Error(`the release server has no file at ${path}`);
|
|
1520
|
-
throwForStatus(response.status, await readErrorCode(response));
|
|
1521
|
-
}
|
|
1522
|
-
return Buffer.from(await response.arrayBuffer());
|
|
1523
|
-
}
|
|
1524
|
-
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
1525
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1526
|
-
const staging = join5(tmpdir3(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
1527
|
-
mkdirSync5(staging, { recursive: true });
|
|
1528
|
-
try {
|
|
1529
|
-
for (const entry2 of manifest.files) {
|
|
1530
|
-
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl);
|
|
1531
|
-
if (!verifyFileBytes(entry2, bytes)) {
|
|
1532
|
-
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
1533
|
-
}
|
|
1534
|
-
const dest = join5(staging, entry2.path);
|
|
1535
|
-
mkdirSync5(dirname2(dest), { recursive: true });
|
|
1536
|
-
writeFileSync6(dest, bytes);
|
|
1537
|
-
}
|
|
1538
|
-
const target = join5(dir, "payload");
|
|
1539
|
-
mkdirSync5(dir, { recursive: true });
|
|
1540
|
-
rmSync4(target, { force: true, recursive: true });
|
|
1541
|
-
renameSync3(staging, target);
|
|
1542
|
-
} catch (error) {
|
|
1543
|
-
rmSync4(staging, { force: true, recursive: true });
|
|
1544
|
-
throw error;
|
|
1545
|
-
}
|
|
1546
|
-
return manifest.version;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
1778
|
// src/index.ts
|
|
1550
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1779
|
+
var LAUNCHER_VERSION = true ? "0.1.15" : readVersionFromPackage();
|
|
1551
1780
|
function defaultPrint(message) {
|
|
1552
1781
|
process.stdout.write(`${message}
|
|
1553
1782
|
`);
|
|
@@ -1636,7 +1865,7 @@ async function runFile(file, args, printErr) {
|
|
|
1636
1865
|
return 1;
|
|
1637
1866
|
}
|
|
1638
1867
|
const abs = resolve2(process.cwd(), file);
|
|
1639
|
-
if (!
|
|
1868
|
+
if (!existsSync7(abs)) {
|
|
1640
1869
|
printErr(`cannot run ${file}: no such file.`);
|
|
1641
1870
|
return 1;
|
|
1642
1871
|
}
|
|
@@ -1768,7 +1997,7 @@ function resolveEntry(entry2) {
|
|
|
1768
1997
|
function needsShell(command2) {
|
|
1769
1998
|
if (process.platform !== "win32") return false;
|
|
1770
1999
|
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
1771
|
-
return !
|
|
2000
|
+
return !existsSync7(command2);
|
|
1772
2001
|
}
|
|
1773
2002
|
function quoteForShell(arg) {
|
|
1774
2003
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -1783,12 +2012,13 @@ function parseProgress(raw) {
|
|
|
1783
2012
|
const message = JSON.parse(line);
|
|
1784
2013
|
if (!SUPPORTED_PROGRESS_PROTOCOLS.has(message.v) || typeof message.step !== "string") continue;
|
|
1785
2014
|
const state = message.state ?? "ok";
|
|
1786
|
-
if (state !== "ok" && state !== "fail" && state !== "note") continue;
|
|
2015
|
+
if (state !== "ok" && state !== "fail" && state !== "note" && state !== "running") continue;
|
|
1787
2016
|
if (message.ms !== void 0 && typeof message.ms !== "number") continue;
|
|
1788
2017
|
progress.push({
|
|
1789
2018
|
step: message.step,
|
|
1790
2019
|
state,
|
|
1791
|
-
...typeof message.ms === "number" ? { ms: message.ms } : {}
|
|
2020
|
+
...typeof message.ms === "number" && Number.isFinite(message.ms) && message.ms >= 0 ? { ms: message.ms } : {},
|
|
2021
|
+
...typeof message.measure === "string" ? { measure: message.measure } : {}
|
|
1792
2022
|
});
|
|
1793
2023
|
} catch {
|
|
1794
2024
|
}
|
|
@@ -1847,8 +2077,27 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1847
2077
|
let version = readState(dir)?.version ?? "unknown";
|
|
1848
2078
|
installer.start();
|
|
1849
2079
|
try {
|
|
2080
|
+
const current = readState(dir);
|
|
2081
|
+
const payloadPresent = existsSync7(payloadDir(dir));
|
|
1850
2082
|
let started = Date.now();
|
|
2083
|
+
installer.phase("resolve", { state: "running" });
|
|
2084
|
+
const pointer = await fetchReleaseVersion(config, fetchImpl);
|
|
2085
|
+
if (pointer && current?.version === pointer && payloadPresent) {
|
|
2086
|
+
version = pointer;
|
|
2087
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
2088
|
+
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
2089
|
+
return await finishLastMile(
|
|
2090
|
+
config,
|
|
2091
|
+
dir,
|
|
2092
|
+
options,
|
|
2093
|
+
installer,
|
|
2094
|
+
version,
|
|
2095
|
+
true,
|
|
2096
|
+
current.acquired ? acquisitionRepairCommand(dir, version) : void 0
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
1851
2099
|
installer.phase("sign-in", { state: "running" });
|
|
2100
|
+
started = Date.now();
|
|
1852
2101
|
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1853
2102
|
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1854
2103
|
installer.phase("resolve", { state: "running" });
|
|
@@ -1856,8 +2105,7 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1856
2105
|
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1857
2106
|
version = manifest.version;
|
|
1858
2107
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1859
|
-
const
|
|
1860
|
-
const unchanged = update && current?.version === version && existsSync6(payloadDir(dir));
|
|
2108
|
+
const unchanged = current?.version === version && existsSync7(payloadDir(dir));
|
|
1861
2109
|
if (unchanged && current.acquired) {
|
|
1862
2110
|
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
1863
2111
|
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
@@ -1865,11 +2113,17 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1865
2113
|
if (!unchanged) {
|
|
1866
2114
|
installer.phase("download", { state: "running" });
|
|
1867
2115
|
started = Date.now();
|
|
1868
|
-
const
|
|
2116
|
+
const cached = reusableCandidate(dir, manifest);
|
|
2117
|
+
const candidate = cached ?? randomUUID4();
|
|
1869
2118
|
const candidateDir = join6(dir, "candidates", candidate);
|
|
1870
|
-
|
|
1871
|
-
|
|
2119
|
+
const onProgress = ({ done, total }) => installer.phase("download", {
|
|
2120
|
+
state: "running",
|
|
2121
|
+
measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
|
|
2122
|
+
});
|
|
2123
|
+
if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
|
|
2124
|
+
const acquisition = existsSync7(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
|
|
1872
2125
|
if (acquisition) {
|
|
2126
|
+
rememberCandidate(dir, candidate, manifest);
|
|
1873
2127
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1874
2128
|
installer.phase("activate", { state: "running" });
|
|
1875
2129
|
let outcome;
|
|
@@ -1877,7 +2131,9 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1877
2131
|
await installAcquisition(dir, candidate, version, acquisition, {
|
|
1878
2132
|
fetchImpl,
|
|
1879
2133
|
env,
|
|
2134
|
+
onProgress,
|
|
1880
2135
|
run: async (command2, cwd, childEnv) => {
|
|
2136
|
+
installer.phase(command2.includes("--prefix") ? "install" : "activate", { state: "running" });
|
|
1881
2137
|
const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
|
|
1882
2138
|
if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
|
|
1883
2139
|
return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
|
|
@@ -1934,7 +2190,7 @@ function payloadFileAsCommand(entry2, payload) {
|
|
|
1934
2190
|
if (!first || first === "$self") return null;
|
|
1935
2191
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
1936
2192
|
const candidate = join6(payload, first);
|
|
1937
|
-
if (!
|
|
2193
|
+
if (!existsSync7(candidate)) return null;
|
|
1938
2194
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
1939
2195
|
if (process.platform === "win32") return null;
|
|
1940
2196
|
try {
|
|
@@ -1995,17 +2251,22 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
1995
2251
|
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1996
2252
|
}
|
|
1997
2253
|
async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
2254
|
+
installer.start();
|
|
1998
2255
|
const [command2, ...args] = entry2;
|
|
1999
2256
|
const shell = needsShell(command2);
|
|
2000
2257
|
const progress = !(process.platform === "win32" && shell);
|
|
2001
2258
|
const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
|
|
2002
2259
|
const outcomeFile = join6(outcomeDir, "outcome.json");
|
|
2260
|
+
const progressFile = join6(outcomeDir, "progress.jsonl");
|
|
2003
2261
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
2004
2262
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
2263
|
+
delete childEnv.MM_PROGRESS_FILE;
|
|
2005
2264
|
delete childEnv.MM_PROGRESS_FD;
|
|
2006
2265
|
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
2007
2266
|
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
2267
|
+
else Object.assign(childEnv, { MM_PROGRESS_FILE: progressFile, MM_PROGRESS_PROTOCOL: "1" });
|
|
2008
2268
|
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
2269
|
+
let progressError;
|
|
2009
2270
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
2010
2271
|
try {
|
|
2011
2272
|
const result = await new Promise((resolve3) => {
|
|
@@ -2026,24 +2287,44 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2026
2287
|
}
|
|
2027
2288
|
partial = held ? safe.slice(-held) : "";
|
|
2028
2289
|
const visible = held ? safe.slice(0, -held) : safe;
|
|
2029
|
-
if (visible) installer.
|
|
2290
|
+
if (visible) installer.relayChunk(visible, channel2);
|
|
2030
2291
|
}).on("end", () => {
|
|
2031
|
-
if (partial) installer.
|
|
2292
|
+
if (partial) installer.relayChunk("[redacted]", channel2);
|
|
2032
2293
|
});
|
|
2033
2294
|
}
|
|
2034
2295
|
let pending = "";
|
|
2296
|
+
let progressOffset = 0;
|
|
2297
|
+
const progressDecoder = new StringDecoder("utf8");
|
|
2298
|
+
const receive = (text) => {
|
|
2299
|
+
pending += text;
|
|
2300
|
+
const end = pending.lastIndexOf("\n");
|
|
2301
|
+
if (end >= 0) {
|
|
2302
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step), ...record.measure ? { measure: redact(record.measure) } : {} });
|
|
2303
|
+
pending = pending.slice(end + 1);
|
|
2304
|
+
}
|
|
2305
|
+
if (pending.length > 65536) pending = "";
|
|
2306
|
+
};
|
|
2307
|
+
const pollProgress = () => {
|
|
2308
|
+
if (progressError) return;
|
|
2309
|
+
try {
|
|
2310
|
+
if (!existsSync7(progressFile)) return;
|
|
2311
|
+
const bytes = readFileSync7(progressFile);
|
|
2312
|
+
receive(progressDecoder.write(bytes.subarray(progressOffset)));
|
|
2313
|
+
progressOffset = bytes.length;
|
|
2314
|
+
} catch {
|
|
2315
|
+
progressError = "installer progress: could not read child progress";
|
|
2316
|
+
if (timer) clearInterval(timer);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
const timer = progress ? void 0 : setInterval(pollProgress, 90);
|
|
2320
|
+
child.once("close", () => {
|
|
2321
|
+
if (timer) clearInterval(timer);
|
|
2322
|
+
pollProgress();
|
|
2323
|
+
});
|
|
2035
2324
|
const channel = child.stdio[3];
|
|
2036
2325
|
if (channel && "setEncoding" in channel) {
|
|
2037
2326
|
channel.setEncoding("utf8");
|
|
2038
|
-
channel.on("data",
|
|
2039
|
-
pending += text;
|
|
2040
|
-
const end = pending.lastIndexOf("\n");
|
|
2041
|
-
if (end >= 0) {
|
|
2042
|
-
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
2043
|
-
pending = pending.slice(end + 1);
|
|
2044
|
-
}
|
|
2045
|
-
if (pending.length > 65536) pending = "";
|
|
2046
|
-
});
|
|
2327
|
+
channel.on("data", receive);
|
|
2047
2328
|
}
|
|
2048
2329
|
child.on("error", (error) => resolve3({ ok: false, error: error.message }));
|
|
2049
2330
|
child.on("close", (code) => resolve3({
|
|
@@ -2052,6 +2333,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2052
2333
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
2053
2334
|
}));
|
|
2054
2335
|
});
|
|
2336
|
+
if (progressError) return { ...result, ok: false, code: result.code || 1, error: progressError };
|
|
2055
2337
|
try {
|
|
2056
2338
|
const outcome = readInstallerOutcome(outcomeFile);
|
|
2057
2339
|
return { ...result, ...outcome ? { outcome } : {} };
|
|
@@ -2059,12 +2341,13 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
|
2059
2341
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
2060
2342
|
}
|
|
2061
2343
|
} finally {
|
|
2062
|
-
if (
|
|
2344
|
+
if (existsSync7(progressFile)) unlinkSync2(progressFile);
|
|
2345
|
+
if (existsSync7(outcomeFile)) unlinkSync2(outcomeFile);
|
|
2063
2346
|
rmdirSync(outcomeDir);
|
|
2064
2347
|
}
|
|
2065
2348
|
}
|
|
2066
2349
|
function doForward(config, dir, options, command2, argv2, print, unlock) {
|
|
2067
|
-
if (command2 &&
|
|
2350
|
+
if (command2 && existsSync7(command2)) {
|
|
2068
2351
|
print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
|
|
2069
2352
|
return 2;
|
|
2070
2353
|
}
|
|
@@ -2144,7 +2427,7 @@ function doDoctor(config, dir, options, print, unlock) {
|
|
|
2144
2427
|
function doLauncherDoctor(config, dir, options, print) {
|
|
2145
2428
|
const tokens = readTokens(dir);
|
|
2146
2429
|
const state = readState(dir);
|
|
2147
|
-
const payloadPresent =
|
|
2430
|
+
const payloadPresent = existsSync7(payloadDir(dir));
|
|
2148
2431
|
print(`product: ${config.product}`);
|
|
2149
2432
|
print(`host: ${config.host}`);
|
|
2150
2433
|
print(`login: ${config.loginKind}`);
|