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