@mutmutco/installer-launcher 0.1.11 → 0.1.13
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 +548 -220
- package/dist/launcher.sea.cjs +563 -235
- package/package.json +4 -3
package/dist/launcher.js
CHANGED
|
@@ -1,10 +1,338 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// src/acquisition.ts
|
|
7
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
|
|
9
|
+
import { join as join3, resolve, relative as relative2, isAbsolute as isAbsolute2, dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/runtime.ts
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync, renameSync, rmSync, realpathSync } from "node:fs";
|
|
15
|
+
import { join, relative, isAbsolute } from "node:path";
|
|
16
|
+
var NODE_VERSION = "24.20.0";
|
|
17
|
+
var NPM_VERSION = "12.0.2";
|
|
18
|
+
var ARTIFACTS = {
|
|
19
|
+
"win32-x64": ["node-v24.20.0-win-x64.zip", "6cac9ffbca8f6a47091e4b5c772e0606049c3871cb67d900c0cedde630e545ba"],
|
|
20
|
+
"win32-arm64": ["node-v24.20.0-win-arm64.zip", "31c6799744de8a54601643098040c68c3697e56c94e407d61d0e5fa5f34191d7"],
|
|
21
|
+
"darwin-arm64": ["node-v24.20.0-darwin-arm64.tar.gz", "40e5607e5ecb3db9192723776da2d75d966260fc74a7a9e731c1bd67dda96bc8"],
|
|
22
|
+
"linux-x64": ["node-v24.20.0-linux-x64.tar.gz", "855d581f8a4eb1a8117e3426de25fe02770592febcfb31369aee1ffbfee9e8ec"],
|
|
23
|
+
"linux-arm64": ["node-v24.20.0-linux-arm64.tar.gz", "3515603e2487879a39bc75716f1a2affd027500c64ba50e845cf72cb33219013"]
|
|
24
|
+
};
|
|
25
|
+
var NPM_INTEGRITY = "uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==";
|
|
26
|
+
async function verifiedDownload(url, path, algorithm, digest, fetchImpl) {
|
|
27
|
+
const response = await fetchImpl(url);
|
|
28
|
+
if (!response.ok) throw new Error(`runtime download failed (${response.status})`);
|
|
29
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
30
|
+
if (createHash(algorithm).update(bytes).digest(algorithm === "sha512" ? "base64" : "hex") !== digest) throw new Error("runtime archive checksum mismatch");
|
|
31
|
+
writeFileSync(path, bytes);
|
|
32
|
+
}
|
|
33
|
+
function command(executable, args) {
|
|
34
|
+
const result = spawnSync(executable, args, { encoding: "utf8", windowsHide: true });
|
|
35
|
+
if (result.error || result.status !== 0) throw new Error(`runtime preparation failed: ${result.error?.message ?? result.stderr.trim()}`);
|
|
36
|
+
return result.stdout.trim();
|
|
37
|
+
}
|
|
38
|
+
async function acquireRuntime(dir, fetchImpl = fetch) {
|
|
39
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
40
|
+
const artifact = ARTIFACTS[platform];
|
|
41
|
+
if (!artifact) throw new Error(`managed runtime does not support ${platform}`);
|
|
42
|
+
const runtimeDir = join(dir, "runtimes");
|
|
43
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
44
|
+
const receipt = join(runtimeDir, `node-${NODE_VERSION}-npm-${NPM_VERSION}-${platform}.json`);
|
|
45
|
+
if (existsSync(receipt)) {
|
|
46
|
+
const runtime2 = JSON.parse(readFileSync(receipt, "utf8"));
|
|
47
|
+
for (const executable of [runtime2.node, runtime2.npm]) {
|
|
48
|
+
const rel = relative(realpathSync(runtimeDir), realpathSync(executable));
|
|
49
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("cached runtime escapes owned directory");
|
|
50
|
+
}
|
|
51
|
+
if (command(runtime2.node, ["--version"]) === `v${NODE_VERSION}` && command(runtime2.node, [runtime2.npm, "--version"]) === NPM_VERSION) return runtime2;
|
|
52
|
+
throw new Error("cached runtime validation failed");
|
|
53
|
+
}
|
|
54
|
+
const root = mkdtempSync(join(runtimeDir, "runtime-"));
|
|
55
|
+
const archive = join(root, artifact[0]);
|
|
56
|
+
await verifiedDownload(`https://nodejs.org/dist/v${NODE_VERSION}/${artifact[0]}`, archive, "sha256", artifact[1], fetchImpl);
|
|
57
|
+
const tar = process.platform === "win32" ? join(process.env.SystemRoot ?? "C:/Windows", "System32", "tar.exe") : "/usr/bin/tar";
|
|
58
|
+
command(tar, ["-xf", archive, "-C", root]);
|
|
59
|
+
const unpacked = join(root, artifact[0].replace(/\.(zip|tar\.gz)$/, ""));
|
|
60
|
+
const node = join(unpacked, process.platform === "win32" ? "node.exe" : "bin/node");
|
|
61
|
+
const npmArchive = join(root, "npm.tgz");
|
|
62
|
+
await verifiedDownload(`https://registry.npmjs.org/npm/-/npm-${NPM_VERSION}.tgz`, npmArchive, "sha512", NPM_INTEGRITY, fetchImpl);
|
|
63
|
+
const npmRoot = join(root, "npm");
|
|
64
|
+
mkdirSync(npmRoot);
|
|
65
|
+
command(tar, ["-xf", npmArchive, "-C", npmRoot]);
|
|
66
|
+
const installedNpm = join(unpacked, process.platform === "win32" ? "node_modules/npm" : "lib/node_modules/npm");
|
|
67
|
+
if (existsSync(installedNpm)) renameSync(installedNpm, join(root, "npm-bundled"));
|
|
68
|
+
renameSync(join(npmRoot, "package"), installedNpm);
|
|
69
|
+
const npm = join(installedNpm, "bin/npm-cli.js");
|
|
70
|
+
if (command(node, ["--version"]) !== `v${NODE_VERSION}` || command(node, [npm, "--version"]) !== NPM_VERSION) throw new Error("downloaded runtime validation failed");
|
|
71
|
+
const runtime = { node, npm };
|
|
72
|
+
const temporary = `${receipt}.${randomUUID()}.tmp`;
|
|
73
|
+
try {
|
|
74
|
+
writeFileSync(temporary, JSON.stringify(runtime), { flag: "wx", mode: 384, flush: true });
|
|
75
|
+
renameSync(temporary, receipt);
|
|
76
|
+
} finally {
|
|
77
|
+
rmSync(temporary, { force: true });
|
|
78
|
+
}
|
|
79
|
+
return runtime;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/store.ts
|
|
83
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "node:fs";
|
|
84
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
85
|
+
import { tmpdir } from "node:os";
|
|
86
|
+
import { join as join2 } from "node:path";
|
|
87
|
+
function defaultProductDir(product) {
|
|
88
|
+
if (process.platform === "win32") {
|
|
89
|
+
const base = process.env.LOCALAPPDATA ?? join2(tmpdir(), "launcher-fallback");
|
|
90
|
+
return join2(base, product);
|
|
91
|
+
}
|
|
92
|
+
const home = process.env.HOME ?? tmpdir();
|
|
93
|
+
return join2(home, `.${product}`);
|
|
94
|
+
}
|
|
95
|
+
function resolveProductDir(product, explicit) {
|
|
96
|
+
return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
|
|
97
|
+
}
|
|
98
|
+
function tokensPath(dir) {
|
|
99
|
+
return join2(dir, "tokens.json");
|
|
100
|
+
}
|
|
101
|
+
function statePath(dir) {
|
|
102
|
+
return join2(dir, "state.json");
|
|
103
|
+
}
|
|
104
|
+
function payloadDir(dir) {
|
|
105
|
+
const acquired = readState(dir)?.acquired;
|
|
106
|
+
if (acquired) return join2(dir, "candidates", acquired.candidate, "payload");
|
|
107
|
+
return join2(dir, "payload");
|
|
108
|
+
}
|
|
109
|
+
function readTokens(dir) {
|
|
110
|
+
try {
|
|
111
|
+
const data = JSON.parse(readFileSync2(tokensPath(dir), "utf8"));
|
|
112
|
+
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
113
|
+
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
114
|
+
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
115
|
+
if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
|
|
116
|
+
if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
|
|
117
|
+
return tokens;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function writeTokens(dir, tokens) {
|
|
123
|
+
mkdirSync2(dir, { recursive: true });
|
|
124
|
+
try {
|
|
125
|
+
writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
126
|
+
`, { mode: 384 });
|
|
127
|
+
} catch {
|
|
128
|
+
writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function clearTokens(dir) {
|
|
133
|
+
rmSync2(tokensPath(dir), { force: true });
|
|
134
|
+
}
|
|
135
|
+
function readState(dir) {
|
|
136
|
+
try {
|
|
137
|
+
const data = JSON.parse(readFileSync2(statePath(dir), "utf8"));
|
|
138
|
+
if (typeof data.version !== "string" || !data.version) return null;
|
|
139
|
+
const state = { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
140
|
+
if (data.acquired) {
|
|
141
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(data.acquired.candidate) || typeof data.acquired.node !== "string" || typeof data.acquired.entry !== "string") return null;
|
|
142
|
+
state.acquired = data.acquired;
|
|
143
|
+
}
|
|
144
|
+
return state;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function writeState(dir, state) {
|
|
150
|
+
mkdirSync2(dir, { recursive: true });
|
|
151
|
+
const temporary = `${statePath(dir)}.${randomUUID2()}.tmp`;
|
|
152
|
+
try {
|
|
153
|
+
writeFileSync2(temporary, `${JSON.stringify(state, null, 2)}
|
|
154
|
+
`, { flag: "wx", mode: 384, flush: true });
|
|
155
|
+
renameSync2(temporary, statePath(dir));
|
|
156
|
+
} finally {
|
|
157
|
+
rmSync2(temporary, { force: true });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function wipeProductDir(dir) {
|
|
161
|
+
const payload = payloadDir(dir);
|
|
162
|
+
rmSync2(tokensPath(dir), { force: true });
|
|
163
|
+
rmSync2(statePath(dir), { force: true });
|
|
164
|
+
rmSync2(payload, { force: true, recursive: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/acquisition.ts
|
|
168
|
+
function safePath(value) {
|
|
169
|
+
return typeof value === "string" && value.length > 0 && !/[\\:\0]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
170
|
+
}
|
|
171
|
+
function argumentsValid(value) {
|
|
172
|
+
return Array.isArray(value) && value.every((arg) => typeof arg === "string" && !arg.includes("\0") && (!arg.includes("$") || arg === "$prefix" || arg === "$version"));
|
|
173
|
+
}
|
|
174
|
+
function readAcquisition(payload) {
|
|
175
|
+
const metadata = JSON.parse(readFileSync3(join3(payload, "payload.json"), "utf8"));
|
|
176
|
+
if (metadata.acquisition === void 0) return null;
|
|
177
|
+
const a = metadata.acquisition;
|
|
178
|
+
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))) {
|
|
179
|
+
throw new Error("invalid signed acquisition declaration");
|
|
180
|
+
}
|
|
181
|
+
return a;
|
|
182
|
+
}
|
|
183
|
+
function runtimeEnvironment(node, inherited, platform = process.platform) {
|
|
184
|
+
const env = { ...inherited };
|
|
185
|
+
let current = env.PATH ?? "";
|
|
186
|
+
if (platform === "win32") {
|
|
187
|
+
const keys = Object.keys(env).filter((key) => key.toLowerCase() === "path").sort();
|
|
188
|
+
current = (keys.length ? env[keys[0]] : "") ?? "";
|
|
189
|
+
for (const key of keys) delete env[key];
|
|
190
|
+
}
|
|
191
|
+
env.PATH = `${dirname(node)}${platform === "win32" ? ";" : ":"}${current}`;
|
|
192
|
+
return env;
|
|
193
|
+
}
|
|
194
|
+
function lockInstallation(dir) {
|
|
195
|
+
mkdirSync3(dir, { recursive: true });
|
|
196
|
+
const path = join3(dir, "installation.lock");
|
|
197
|
+
const owner = `${process.pid}:${randomUUID3()}`;
|
|
198
|
+
try {
|
|
199
|
+
writeFileSync3(path, owner, { flag: "wx", mode: 384 });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error.code !== "EEXIST") throw error;
|
|
202
|
+
const recovery = `${path}.recovery`;
|
|
203
|
+
try {
|
|
204
|
+
writeFileSync3(recovery, owner, { flag: "wx", mode: 384, flush: true });
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error("installation lock recovery is already pending");
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const previous = readFileSync3(path, "utf8");
|
|
210
|
+
const pid = Number(previous.split(":")[0]);
|
|
211
|
+
if (!Number.isInteger(pid) || pid <= 0) throw new Error("installation lock is malformed; recovery required");
|
|
212
|
+
try {
|
|
213
|
+
process.kill(pid, 0);
|
|
214
|
+
throw new Error("another installer is running");
|
|
215
|
+
} catch (probe) {
|
|
216
|
+
if (probe.code !== "ESRCH") throw probe;
|
|
217
|
+
}
|
|
218
|
+
unlinkSync(path);
|
|
219
|
+
writeFileSync3(path, owner, { flag: "wx", mode: 384, flush: true });
|
|
220
|
+
} finally {
|
|
221
|
+
unlinkSync(recovery);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
let released = false;
|
|
225
|
+
return () => {
|
|
226
|
+
if (!released && readFileSync3(path, "utf8") === owner) unlinkSync(path);
|
|
227
|
+
released = true;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function pendingPath(dir) {
|
|
231
|
+
return join3(dir, "acquisition-pending.json");
|
|
232
|
+
}
|
|
233
|
+
function candidateRoot(dir, candidate) {
|
|
234
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(candidate)) throw new Error("invalid acquisition candidate");
|
|
235
|
+
return join3(dir, "candidates", candidate);
|
|
236
|
+
}
|
|
237
|
+
function packageRoot(prefix, acquisition) {
|
|
238
|
+
return join3(prefix, "node_modules", acquisition.package);
|
|
239
|
+
}
|
|
240
|
+
function entry(root, path) {
|
|
241
|
+
const actual = realpathSync2(join3(root, path));
|
|
242
|
+
const rel = relative2(realpathSync2(root), actual);
|
|
243
|
+
if (rel.startsWith("..") || isAbsolute2(rel)) throw new Error("acquired entry escapes package");
|
|
244
|
+
return actual;
|
|
245
|
+
}
|
|
246
|
+
function argv(node, root, file, args, prefix, version) {
|
|
247
|
+
return [node, entry(root, file), ...args.map((arg) => arg === "$prefix" ? prefix : arg === "$version" ? version : arg)];
|
|
248
|
+
}
|
|
249
|
+
function acquisitionRepairCommand(dir, version) {
|
|
250
|
+
const state = readState(dir);
|
|
251
|
+
if (!state?.acquired || state.version !== version) throw new Error("installed acquisition selection is missing");
|
|
252
|
+
const root = candidateRoot(dir, state.acquired.candidate);
|
|
253
|
+
const acquisition = readAcquisition(join3(root, "payload"));
|
|
254
|
+
if (!acquisition) throw new Error("installed acquisition declaration is missing");
|
|
255
|
+
const prefix = join3(root, "prefix");
|
|
256
|
+
const productRoot = packageRoot(prefix, acquisition);
|
|
257
|
+
const pkg = JSON.parse(readFileSync3(join3(productRoot, "package.json"), "utf8"));
|
|
258
|
+
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("installed package identity does not match signed release");
|
|
259
|
+
return argv(realpathSync2(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
|
|
260
|
+
}
|
|
261
|
+
async function recoverAcquisition(dir, run2, env) {
|
|
262
|
+
if (!existsSync3(pendingPath(dir))) return;
|
|
263
|
+
const pending = JSON.parse(readFileSync3(pendingPath(dir), "utf8"));
|
|
264
|
+
if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
|
|
265
|
+
const root = candidateRoot(dir, pending.candidate);
|
|
266
|
+
if (readState(dir)?.acquired?.candidate === pending.candidate) {
|
|
267
|
+
unlinkSync(pendingPath(dir));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const acquisition = readAcquisition(join3(root, "payload"));
|
|
271
|
+
if (!acquisition) throw new Error("pending acquisition lost its signed declaration");
|
|
272
|
+
const prefix = join3(root, "prefix");
|
|
273
|
+
const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
|
|
274
|
+
if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
|
|
275
|
+
if (pending.previous) writeState(dir, pending.previous);
|
|
276
|
+
else if (existsSync3(statePath(dir))) unlinkSync(statePath(dir));
|
|
277
|
+
unlinkSync(pendingPath(dir));
|
|
278
|
+
}
|
|
279
|
+
async function installAcquisition(dir, candidate, version, acquisition, options) {
|
|
280
|
+
if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
|
|
281
|
+
const root = candidateRoot(dir, candidate);
|
|
282
|
+
const payload = join3(root, "payload");
|
|
283
|
+
const prefix = join3(root, "prefix");
|
|
284
|
+
mkdirSync3(prefix);
|
|
285
|
+
const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl);
|
|
286
|
+
const env = runtimeEnvironment(runtime.node, options.env);
|
|
287
|
+
const npmEnv = { ...env };
|
|
288
|
+
for (const key of Object.keys(npmEnv)) {
|
|
289
|
+
if (/^npm_config_/i.test(key) || /^(NPM_TOKEN|NODE_AUTH_TOKEN|MM_INSTALLER_TOKEN)$/i.test(key)) delete npmEnv[key];
|
|
290
|
+
}
|
|
291
|
+
const npmrc = join3(root, "public.npmrc");
|
|
292
|
+
const globalrc = join3(root, "global.npmrc");
|
|
293
|
+
writeFileSync3(npmrc, "registry=https://registry.npmjs.org/\n");
|
|
294
|
+
writeFileSync3(globalrc, "");
|
|
295
|
+
const install = [
|
|
296
|
+
runtime.node,
|
|
297
|
+
runtime.npm,
|
|
298
|
+
"install",
|
|
299
|
+
"--prefix",
|
|
300
|
+
prefix,
|
|
301
|
+
"--ignore-scripts",
|
|
302
|
+
"--no-audit",
|
|
303
|
+
"--no-fund",
|
|
304
|
+
"--userconfig",
|
|
305
|
+
npmrc,
|
|
306
|
+
"--globalconfig",
|
|
307
|
+
globalrc,
|
|
308
|
+
"--registry",
|
|
309
|
+
"https://registry.npmjs.org/",
|
|
310
|
+
resolve(payload, acquisition.archive)
|
|
311
|
+
];
|
|
312
|
+
if (!await options.run(install, prefix, npmEnv)) throw new Error("local product archive installation failed");
|
|
313
|
+
const productRoot = packageRoot(prefix, acquisition);
|
|
314
|
+
const pkg = JSON.parse(readFileSync3(join3(productRoot, "package.json"), "utf8"));
|
|
315
|
+
if (pkg.name !== acquisition.package || pkg.version !== version) throw new Error("acquired package identity does not match signed release");
|
|
316
|
+
const runEntry = entry(productRoot, acquisition.runEntry);
|
|
317
|
+
const convergence = argv(runtime.node, productRoot, acquisition.convergeEntry, acquisition.convergeArgs, prefix, version);
|
|
318
|
+
entry(productRoot, acquisition.rollbackEntry);
|
|
319
|
+
const pending = { schema: 1, candidate, version, node: runtime.node, previous: readState(dir) };
|
|
320
|
+
writeFileSync3(pendingPath(dir), JSON.stringify(pending), { flag: "wx", mode: 384, flush: true });
|
|
321
|
+
try {
|
|
322
|
+
if (!await options.run(convergence, root, env)) throw new Error("product convergence did not finish");
|
|
323
|
+
(options.commit ?? writeState)(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), acquired: { candidate, node: runtime.node, entry: runEntry } });
|
|
324
|
+
} catch (error) {
|
|
325
|
+
await recoverAcquisition(dir, options.run, env);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
unlinkSync(pendingPath(dir));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/index.ts
|
|
332
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
333
|
+
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
334
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
7
|
-
import { dirname as
|
|
335
|
+
import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
8
336
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
9
337
|
|
|
10
338
|
// ../face/src/face.ts
|
|
@@ -309,7 +637,7 @@ var ALLOWED = new Set(Object.values(GLYPH));
|
|
|
309
637
|
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
310
638
|
|
|
311
639
|
// ../face/src/outcome.ts
|
|
312
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
640
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
313
641
|
var counts = ["total", "updated", "failed"];
|
|
314
642
|
var strings = ["version", "retry", "detail", "logPath"];
|
|
315
643
|
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
@@ -331,12 +659,12 @@ function validateInstallerOutcome(value) {
|
|
|
331
659
|
return { ...facts };
|
|
332
660
|
}
|
|
333
661
|
function writeInstallerOutcome(path, value) {
|
|
334
|
-
|
|
662
|
+
writeFileSync4(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
335
663
|
}
|
|
336
664
|
function readInstallerOutcome(path) {
|
|
337
665
|
let text;
|
|
338
666
|
try {
|
|
339
|
-
text =
|
|
667
|
+
text = readFileSync4(path, "utf8");
|
|
340
668
|
} catch (error) {
|
|
341
669
|
if (error.code === "ENOENT") return void 0;
|
|
342
670
|
throw error;
|
|
@@ -412,7 +740,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
412
740
|
product: declaration.product,
|
|
413
741
|
columns: options.columns,
|
|
414
742
|
env,
|
|
415
|
-
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
743
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0 && env.TERM !== "dumb"
|
|
416
744
|
});
|
|
417
745
|
const errors = [];
|
|
418
746
|
const write = options.write ? (text, channel) => {
|
|
@@ -448,9 +776,13 @@ function createInstallerRun(value, options = {}) {
|
|
|
448
776
|
const start = () => {
|
|
449
777
|
if (started || finished) return;
|
|
450
778
|
started = true;
|
|
779
|
+
if (options.quiet) return;
|
|
451
780
|
const welcome = face.welcome();
|
|
452
781
|
if (tty) lines(welcome);
|
|
453
|
-
else if (welcome.length)
|
|
782
|
+
else if (welcome.length) {
|
|
783
|
+
const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
|
|
784
|
+
lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
|
|
785
|
+
}
|
|
454
786
|
};
|
|
455
787
|
const durable = (title, measure, kind) => {
|
|
456
788
|
spinner.stop();
|
|
@@ -480,12 +812,13 @@ function createInstallerRun(value, options = {}) {
|
|
|
480
812
|
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
481
813
|
if (!surface) throw new Error("installer run: undeclared surface");
|
|
482
814
|
start();
|
|
483
|
-
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
815
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} ${tty ? "\u2192" : "->"} ${facts.to}` : ` ${facts.to}` : "";
|
|
484
816
|
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
485
817
|
if (!status) throw new Error("installer run: unknown surface state");
|
|
486
|
-
const
|
|
818
|
+
const separator = tty ? "\xB7" : "-";
|
|
819
|
+
const activation = facts.state === "updated" && surface.activation ? ` ${separator} ${surface.activation}` : "";
|
|
487
820
|
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
488
|
-
durable(`${facts.id}${versions}
|
|
821
|
+
durable(`${facts.id}${versions} ${separator} ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
489
822
|
if (facts.detail) run2.relay(facts.detail);
|
|
490
823
|
},
|
|
491
824
|
milestone({ step, state, ms }) {
|
|
@@ -512,6 +845,20 @@ function createInstallerRun(value, options = {}) {
|
|
|
512
845
|
`);
|
|
513
846
|
}
|
|
514
847
|
},
|
|
848
|
+
cancel() {
|
|
849
|
+
if (finished) return;
|
|
850
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(
|
|
851
|
+
env.MM_INSTALLER_OUTCOME_FILE,
|
|
852
|
+
{ total: 0, updated: 0, failed: 0, deferred: true, detail: "Operation cancelled." }
|
|
853
|
+
);
|
|
854
|
+
start();
|
|
855
|
+
spinner.stop();
|
|
856
|
+
finished = true;
|
|
857
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE) {
|
|
858
|
+
lines(tty ? face.receipt(["Operation cancelled."], { ready: false }) : ["Operation cancelled."]);
|
|
859
|
+
}
|
|
860
|
+
if (tty) lines([face.signOff()]);
|
|
861
|
+
},
|
|
515
862
|
finish(facts) {
|
|
516
863
|
if (finished) return;
|
|
517
864
|
validateInstallerOutcome(facts);
|
|
@@ -519,6 +866,7 @@ function createInstallerRun(value, options = {}) {
|
|
|
519
866
|
start();
|
|
520
867
|
spinner.stop();
|
|
521
868
|
finished = true;
|
|
869
|
+
if (options.quiet && facts.updated === 0 && facts.failed === 0 && !facts.operationFailed) return;
|
|
522
870
|
const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
|
|
523
871
|
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
524
872
|
const body = [
|
|
@@ -543,17 +891,17 @@ function createInstallerRun(value, options = {}) {
|
|
|
543
891
|
}
|
|
544
892
|
|
|
545
893
|
// src/autoupdate.ts
|
|
546
|
-
import { spawnSync } from "node:child_process";
|
|
547
|
-
import { existsSync, mkdirSync, readFileSync as
|
|
548
|
-
import { tmpdir } from "node:os";
|
|
549
|
-
import { join } from "node:path";
|
|
894
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
895
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
896
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
897
|
+
import { join as join4 } from "node:path";
|
|
550
898
|
function schedulePlatform(override) {
|
|
551
899
|
const platform = override ?? process.platform;
|
|
552
900
|
if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
|
|
553
901
|
return null;
|
|
554
902
|
}
|
|
555
|
-
function defaultExec(
|
|
556
|
-
const result =
|
|
903
|
+
function defaultExec(command2, args) {
|
|
904
|
+
const result = spawnSync2(command2, args, { encoding: "utf8", windowsHide: true });
|
|
557
905
|
if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
|
|
558
906
|
return {
|
|
559
907
|
code: typeof result.status === "number" ? result.status : 1,
|
|
@@ -563,8 +911,8 @@ function defaultExec(command, args) {
|
|
|
563
911
|
}
|
|
564
912
|
function homeOf(options) {
|
|
565
913
|
if (options.homeDir) return options.homeDir;
|
|
566
|
-
if (process.platform === "win32") return process.env.USERPROFILE ??
|
|
567
|
-
return process.env.HOME ??
|
|
914
|
+
if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir2();
|
|
915
|
+
return process.env.HOME ?? tmpdir2();
|
|
568
916
|
}
|
|
569
917
|
function scheduleName(config) {
|
|
570
918
|
return `${config.binName} autoupdate`;
|
|
@@ -575,13 +923,13 @@ function scheduleLabel(config) {
|
|
|
575
923
|
function quoteWindows(arg) {
|
|
576
924
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
577
925
|
}
|
|
578
|
-
function enableSchedule(config,
|
|
926
|
+
function enableSchedule(config, command2, options = {}) {
|
|
579
927
|
const platform = schedulePlatform(options.platform);
|
|
580
928
|
if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
|
|
581
929
|
const exec = options.exec ?? defaultExec;
|
|
582
930
|
const home = homeOf(options);
|
|
583
931
|
if (platform === "win32") {
|
|
584
|
-
const taskLine =
|
|
932
|
+
const taskLine = command2.map(quoteWindows).join(" ");
|
|
585
933
|
const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
|
|
586
934
|
if (result2.code !== 0) {
|
|
587
935
|
throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
|
|
@@ -590,10 +938,10 @@ function enableSchedule(config, command, options = {}) {
|
|
|
590
938
|
}
|
|
591
939
|
if (platform === "darwin") {
|
|
592
940
|
const label2 = scheduleLabel(config);
|
|
593
|
-
const dir2 =
|
|
594
|
-
|
|
595
|
-
const plist =
|
|
596
|
-
|
|
941
|
+
const dir2 = join4(home, "Library", "LaunchAgents");
|
|
942
|
+
mkdirSync4(dir2, { recursive: true });
|
|
943
|
+
const plist = join4(dir2, `${label2}.plist`);
|
|
944
|
+
writeFileSync5(plist, darwinPlist(label2, command2));
|
|
597
945
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
598
946
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
599
947
|
if (result2.code !== 0) {
|
|
@@ -602,10 +950,10 @@ function enableSchedule(config, command, options = {}) {
|
|
|
602
950
|
return;
|
|
603
951
|
}
|
|
604
952
|
const label = scheduleLabel(config);
|
|
605
|
-
const dir =
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
953
|
+
const dir = join4(home, ".config", "systemd", "user");
|
|
954
|
+
mkdirSync4(dir, { recursive: true });
|
|
955
|
+
writeFileSync5(join4(dir, `${label}.service`), linuxService(command2));
|
|
956
|
+
writeFileSync5(join4(dir, `${label}.timer`), linuxTimer(label));
|
|
609
957
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
610
958
|
if (reload.code !== 0) {
|
|
611
959
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -630,14 +978,14 @@ function disableSchedule(config, options = {}) {
|
|
|
630
978
|
if (platform === "darwin") {
|
|
631
979
|
const label2 = scheduleLabel(config);
|
|
632
980
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
633
|
-
|
|
981
|
+
rmSync3(join4(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
634
982
|
return;
|
|
635
983
|
}
|
|
636
984
|
const label = scheduleLabel(config);
|
|
637
|
-
const dir =
|
|
985
|
+
const dir = join4(home, ".config", "systemd", "user");
|
|
638
986
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
639
|
-
|
|
640
|
-
|
|
987
|
+
rmSync3(join4(dir, `${label}.service`), { force: true });
|
|
988
|
+
rmSync3(join4(dir, `${label}.timer`), { force: true });
|
|
641
989
|
}
|
|
642
990
|
function querySchedule(config, options = {}) {
|
|
643
991
|
const platform = schedulePlatform(options.platform);
|
|
@@ -655,20 +1003,20 @@ function querySchedule(config, options = {}) {
|
|
|
655
1003
|
return state2;
|
|
656
1004
|
}
|
|
657
1005
|
if (platform === "darwin") {
|
|
658
|
-
const plist =
|
|
659
|
-
if (!
|
|
1006
|
+
const plist = join4(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
1007
|
+
if (!existsSync4(plist)) return { supported: true, enabled: false };
|
|
660
1008
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
661
1009
|
}
|
|
662
|
-
const timer =
|
|
663
|
-
if (!
|
|
1010
|
+
const timer = join4(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
1011
|
+
if (!existsSync4(timer)) return { supported: true, enabled: false };
|
|
664
1012
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
665
1013
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
666
1014
|
const stamp = (shown.stdout ?? "").trim();
|
|
667
1015
|
if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
|
|
668
1016
|
return state;
|
|
669
1017
|
}
|
|
670
|
-
function darwinPlist(label,
|
|
671
|
-
const args =
|
|
1018
|
+
function darwinPlist(label, command2) {
|
|
1019
|
+
const args = command2.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
672
1020
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
673
1021
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
674
1022
|
<plist version="1.0">
|
|
@@ -685,8 +1033,8 @@ ${args}
|
|
|
685
1033
|
</plist>
|
|
686
1034
|
`;
|
|
687
1035
|
}
|
|
688
|
-
function linuxService(
|
|
689
|
-
const line =
|
|
1036
|
+
function linuxService(command2) {
|
|
1037
|
+
const line = command2.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
|
|
690
1038
|
return `[Unit]
|
|
691
1039
|
Description=${"Hourly update check"}
|
|
692
1040
|
[Service]
|
|
@@ -718,7 +1066,7 @@ function firstLine(text) {
|
|
|
718
1066
|
}
|
|
719
1067
|
|
|
720
1068
|
// src/config.ts
|
|
721
|
-
import { readFileSync as
|
|
1069
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
722
1070
|
import { getAsset } from "node:sea";
|
|
723
1071
|
|
|
724
1072
|
// src/module-url.ts
|
|
@@ -740,10 +1088,10 @@ function loadProductConfig(options = {}) {
|
|
|
740
1088
|
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
741
1089
|
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
742
1090
|
if (explicit) {
|
|
743
|
-
return parseProductConfig(
|
|
1091
|
+
return parseProductConfig(readFileSync6(explicit, "utf8"));
|
|
744
1092
|
}
|
|
745
1093
|
try {
|
|
746
|
-
return parseProductConfig(
|
|
1094
|
+
return parseProductConfig(readFileSync6(devFallback, "utf8"));
|
|
747
1095
|
} catch {
|
|
748
1096
|
}
|
|
749
1097
|
try {
|
|
@@ -799,7 +1147,7 @@ function field(record, key) {
|
|
|
799
1147
|
|
|
800
1148
|
// src/login-github.ts
|
|
801
1149
|
import { spawn } from "node:child_process";
|
|
802
|
-
var realSleep = (ms) => new Promise((
|
|
1150
|
+
var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
803
1151
|
function openBrowser(url) {
|
|
804
1152
|
if (process.env.LAUNCHER_NO_OPEN === "1") return;
|
|
805
1153
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
@@ -919,7 +1267,7 @@ async function refreshAccessToken(config, refreshToken, fetchImpl = fetch) {
|
|
|
919
1267
|
}
|
|
920
1268
|
|
|
921
1269
|
// src/login-google.ts
|
|
922
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
1270
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
923
1271
|
import { createServer } from "node:http";
|
|
924
1272
|
var b64url = (bytes) => bytes.toString("base64url");
|
|
925
1273
|
async function loginGoogle(options) {
|
|
@@ -927,7 +1275,7 @@ async function loginGoogle(options) {
|
|
|
927
1275
|
const server = options.host.replace(/\/+$/, "");
|
|
928
1276
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
929
1277
|
const listener = createServer();
|
|
930
|
-
await new Promise((
|
|
1278
|
+
await new Promise((resolve3) => listener.listen(0, "127.0.0.1", resolve3));
|
|
931
1279
|
const port = listener.address().port;
|
|
932
1280
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
933
1281
|
try {
|
|
@@ -942,7 +1290,7 @@ async function loginGoogle(options) {
|
|
|
942
1290
|
throw new Error("the sign-in server returned a malformed registration");
|
|
943
1291
|
}
|
|
944
1292
|
const verifier = b64url(randomBytes(32));
|
|
945
|
-
const challenge = b64url(
|
|
1293
|
+
const challenge = b64url(createHash2("sha256").update(verifier).digest());
|
|
946
1294
|
const state = b64url(randomBytes(16));
|
|
947
1295
|
const authorize = new URL(`${server}/oauth/authorize`);
|
|
948
1296
|
authorize.search = new URLSearchParams({
|
|
@@ -954,7 +1302,7 @@ async function loginGoogle(options) {
|
|
|
954
1302
|
code_challenge: challenge,
|
|
955
1303
|
code_challenge_method: "S256"
|
|
956
1304
|
}).toString();
|
|
957
|
-
const code = await new Promise((
|
|
1305
|
+
const code = await new Promise((resolve3, reject) => {
|
|
958
1306
|
const timer = setTimeout(() => reject(new Error("sign-in timed out \u2014 run login again.")), timeoutMs);
|
|
959
1307
|
listener.on("request", (req, res) => {
|
|
960
1308
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
@@ -972,7 +1320,7 @@ async function loginGoogle(options) {
|
|
|
972
1320
|
}
|
|
973
1321
|
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."));
|
|
974
1322
|
clearTimeout(timer);
|
|
975
|
-
|
|
1323
|
+
resolve3(received);
|
|
976
1324
|
});
|
|
977
1325
|
const print = options.print ?? ((line) => process.stderr.write(`${line}
|
|
978
1326
|
`));
|
|
@@ -1032,10 +1380,10 @@ function page(title, body) {
|
|
|
1032
1380
|
}
|
|
1033
1381
|
|
|
1034
1382
|
// src/payload.ts
|
|
1035
|
-
import { createHash as
|
|
1036
|
-
import { mkdirSync as
|
|
1037
|
-
import { tmpdir as
|
|
1038
|
-
import { dirname, join as
|
|
1383
|
+
import { createHash as createHash3, createPublicKey, verify } from "node:crypto";
|
|
1384
|
+
import { mkdirSync as mkdirSync5, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1385
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
1386
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1039
1387
|
|
|
1040
1388
|
// src/canonical.ts
|
|
1041
1389
|
function canonicalJson(value) {
|
|
@@ -1052,7 +1400,7 @@ function encode(value) {
|
|
|
1052
1400
|
return JSON.stringify(value);
|
|
1053
1401
|
}
|
|
1054
1402
|
if (Array.isArray(value)) {
|
|
1055
|
-
return `[${value.map((
|
|
1403
|
+
return `[${value.map((entry2) => encode(entry2)).join(",")}]`;
|
|
1056
1404
|
}
|
|
1057
1405
|
if (typeof value === "object") {
|
|
1058
1406
|
const record = value;
|
|
@@ -1101,9 +1449,9 @@ function verifyManifest(manifest, signature, config) {
|
|
|
1101
1449
|
return false;
|
|
1102
1450
|
}
|
|
1103
1451
|
}
|
|
1104
|
-
function verifyFileBytes(
|
|
1105
|
-
if (
|
|
1106
|
-
return
|
|
1452
|
+
function verifyFileBytes(entry2, bytes) {
|
|
1453
|
+
if (entry2.size !== bytes.length) return false;
|
|
1454
|
+
return createHash3("sha256").update(bytes).digest("hex") === entry2.sha256.toLowerCase();
|
|
1107
1455
|
}
|
|
1108
1456
|
async function readErrorCode(response) {
|
|
1109
1457
|
try {
|
|
@@ -1134,8 +1482,8 @@ function parseManifest(json) {
|
|
|
1134
1482
|
if (typeof record.signature !== "string" || !record.signature) {
|
|
1135
1483
|
throw new Error("the release manifest is unsigned");
|
|
1136
1484
|
}
|
|
1137
|
-
const files = record.files.map((
|
|
1138
|
-
const file =
|
|
1485
|
+
const files = record.files.map((entry2) => {
|
|
1486
|
+
const file = entry2;
|
|
1139
1487
|
if (typeof file.path !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number") {
|
|
1140
1488
|
throw new Error("the release manifest lists a malformed file");
|
|
1141
1489
|
}
|
|
@@ -1175,101 +1523,31 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
|
1175
1523
|
}
|
|
1176
1524
|
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
1177
1525
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1178
|
-
const staging =
|
|
1179
|
-
|
|
1526
|
+
const staging = join5(tmpdir3(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
1527
|
+
mkdirSync5(staging, { recursive: true });
|
|
1180
1528
|
try {
|
|
1181
|
-
for (const
|
|
1182
|
-
const bytes = await fetchFileBytes(config, accessToken,
|
|
1183
|
-
if (!verifyFileBytes(
|
|
1184
|
-
throw new Error(`file ${
|
|
1529
|
+
for (const entry2 of manifest.files) {
|
|
1530
|
+
const bytes = await fetchFileBytes(config, accessToken, entry2.path, fetchImpl);
|
|
1531
|
+
if (!verifyFileBytes(entry2, bytes)) {
|
|
1532
|
+
throw new Error(`file ${entry2.path} failed its checksum \u2014 refusing to install anything.`);
|
|
1185
1533
|
}
|
|
1186
|
-
const dest =
|
|
1187
|
-
|
|
1188
|
-
|
|
1534
|
+
const dest = join5(staging, entry2.path);
|
|
1535
|
+
mkdirSync5(dirname2(dest), { recursive: true });
|
|
1536
|
+
writeFileSync6(dest, bytes);
|
|
1189
1537
|
}
|
|
1190
|
-
const target =
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1538
|
+
const target = join5(dir, "payload");
|
|
1539
|
+
mkdirSync5(dir, { recursive: true });
|
|
1540
|
+
rmSync4(target, { force: true, recursive: true });
|
|
1541
|
+
renameSync3(staging, target);
|
|
1194
1542
|
} catch (error) {
|
|
1195
|
-
|
|
1543
|
+
rmSync4(staging, { force: true, recursive: true });
|
|
1196
1544
|
throw error;
|
|
1197
1545
|
}
|
|
1198
1546
|
return manifest.version;
|
|
1199
1547
|
}
|
|
1200
1548
|
|
|
1201
|
-
// src/store.ts
|
|
1202
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1203
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
1204
|
-
import { join as join3 } from "node:path";
|
|
1205
|
-
function defaultProductDir(product) {
|
|
1206
|
-
if (process.platform === "win32") {
|
|
1207
|
-
const base = process.env.LOCALAPPDATA ?? join3(tmpdir3(), "launcher-fallback");
|
|
1208
|
-
return join3(base, product);
|
|
1209
|
-
}
|
|
1210
|
-
const home = process.env.HOME ?? tmpdir3();
|
|
1211
|
-
return join3(home, `.${product}`);
|
|
1212
|
-
}
|
|
1213
|
-
function resolveProductDir(product, explicit) {
|
|
1214
|
-
return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
|
|
1215
|
-
}
|
|
1216
|
-
function tokensPath(dir) {
|
|
1217
|
-
return join3(dir, "tokens.json");
|
|
1218
|
-
}
|
|
1219
|
-
function statePath(dir) {
|
|
1220
|
-
return join3(dir, "state.json");
|
|
1221
|
-
}
|
|
1222
|
-
function payloadDir(dir) {
|
|
1223
|
-
return join3(dir, "payload");
|
|
1224
|
-
}
|
|
1225
|
-
function readTokens(dir) {
|
|
1226
|
-
try {
|
|
1227
|
-
const data = JSON.parse(readFileSync4(tokensPath(dir), "utf8"));
|
|
1228
|
-
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
1229
|
-
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
1230
|
-
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
1231
|
-
if (typeof data.refreshToken === "string" && data.refreshToken) tokens.refreshToken = data.refreshToken;
|
|
1232
|
-
if (typeof data.clientId === "string" && data.clientId) tokens.clientId = data.clientId;
|
|
1233
|
-
return tokens;
|
|
1234
|
-
} catch {
|
|
1235
|
-
return null;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
function writeTokens(dir, tokens) {
|
|
1239
|
-
mkdirSync3(dir, { recursive: true });
|
|
1240
|
-
try {
|
|
1241
|
-
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
1242
|
-
`, { mode: 384 });
|
|
1243
|
-
} catch {
|
|
1244
|
-
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
1245
|
-
`);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
function clearTokens(dir) {
|
|
1249
|
-
rmSync3(tokensPath(dir), { force: true });
|
|
1250
|
-
}
|
|
1251
|
-
function readState(dir) {
|
|
1252
|
-
try {
|
|
1253
|
-
const data = JSON.parse(readFileSync4(statePath(dir), "utf8"));
|
|
1254
|
-
if (typeof data.version !== "string" || !data.version) return null;
|
|
1255
|
-
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
1256
|
-
} catch {
|
|
1257
|
-
return null;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
function writeState(dir, state) {
|
|
1261
|
-
mkdirSync3(dir, { recursive: true });
|
|
1262
|
-
writeFileSync4(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
1263
|
-
`);
|
|
1264
|
-
}
|
|
1265
|
-
function wipeProductDir(dir) {
|
|
1266
|
-
rmSync3(tokensPath(dir), { force: true });
|
|
1267
|
-
rmSync3(statePath(dir), { force: true });
|
|
1268
|
-
rmSync3(payloadDir(dir), { force: true, recursive: true });
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1549
|
// src/index.ts
|
|
1272
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1550
|
+
var LAUNCHER_VERSION = true ? "0.1.13" : readVersionFromPackage();
|
|
1273
1551
|
function defaultPrint(message) {
|
|
1274
1552
|
process.stdout.write(`${message}
|
|
1275
1553
|
`);
|
|
@@ -1281,30 +1559,37 @@ function defaultPrintErr(message) {
|
|
|
1281
1559
|
async function run(rawOptions = {}) {
|
|
1282
1560
|
const print = rawOptions.print ?? defaultPrint;
|
|
1283
1561
|
const printErr = rawOptions.printErr ?? defaultPrintErr;
|
|
1284
|
-
const
|
|
1285
|
-
const runAt =
|
|
1286
|
-
if (runAt >= 0) return runFile(
|
|
1562
|
+
const argv2 = rawOptions.argv ?? process.argv.slice(2);
|
|
1563
|
+
const runAt = argv2.indexOf("--run");
|
|
1564
|
+
if (runAt >= 0) return runFile(argv2[runAt + 1], argv2.slice(runAt + 2), printErr);
|
|
1287
1565
|
let config;
|
|
1288
1566
|
try {
|
|
1289
|
-
config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(
|
|
1567
|
+
config = loadProductConfig({ configPath: rawOptions.configPath ?? flagValue(argv2, "--config") });
|
|
1290
1568
|
} catch (error) {
|
|
1291
1569
|
print(`cannot start: ${error.message}`);
|
|
1292
1570
|
return 2;
|
|
1293
1571
|
}
|
|
1294
|
-
const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(
|
|
1295
|
-
const positional =
|
|
1296
|
-
if (
|
|
1572
|
+
const dir = resolveProductDir(config.product, rawOptions.dir ?? flagValue(argv2, "--dir"));
|
|
1573
|
+
const positional = argv2.filter((arg) => !arg.startsWith("-"));
|
|
1574
|
+
if (argv2.includes("--version") || argv2.includes("-v")) {
|
|
1297
1575
|
print(`${config.binName} launcher ${LAUNCHER_VERSION}`);
|
|
1298
1576
|
return 0;
|
|
1299
1577
|
}
|
|
1300
|
-
if (
|
|
1578
|
+
if (argv2.includes("--help") || argv2.includes("-h") || positional.length === 0) {
|
|
1301
1579
|
printUsage(config, print);
|
|
1302
1580
|
return positional.length === 0 ? 2 : 0;
|
|
1303
1581
|
}
|
|
1304
|
-
const
|
|
1582
|
+
const command2 = positional[0];
|
|
1583
|
+
let unlock;
|
|
1305
1584
|
try {
|
|
1306
|
-
|
|
1585
|
+
unlock = lockInstallation(dir);
|
|
1586
|
+
await recoverAcquisition(dir, async (command3, cwd, env) => {
|
|
1587
|
+
const result = (rawOptions.runEntry ?? defaultRunEntry)(command3, cwd, env);
|
|
1588
|
+
return result.ok && (result.code === void 0 || result.code === 0);
|
|
1589
|
+
}, payloadEnv(dir) ?? process.env);
|
|
1590
|
+
switch (command2) {
|
|
1307
1591
|
case "login":
|
|
1592
|
+
unlock();
|
|
1308
1593
|
await doLogin(config, dir, rawOptions, print);
|
|
1309
1594
|
return 0;
|
|
1310
1595
|
case "logout":
|
|
@@ -1316,11 +1601,11 @@ async function run(rawOptions = {}) {
|
|
|
1316
1601
|
case "update":
|
|
1317
1602
|
return await doUpdate(config, dir, rawOptions, print);
|
|
1318
1603
|
case "doctor":
|
|
1319
|
-
return doDoctor(config, dir, rawOptions, print);
|
|
1604
|
+
return doDoctor(config, dir, rawOptions, print, unlock);
|
|
1320
1605
|
case "autoupdate":
|
|
1321
1606
|
return doAutoupdate(config, rawOptions, positional.slice(1), print);
|
|
1322
1607
|
default:
|
|
1323
|
-
return doForward(config, dir, rawOptions,
|
|
1608
|
+
return doForward(config, dir, rawOptions, command2, argv2, print, unlock);
|
|
1324
1609
|
}
|
|
1325
1610
|
} catch (error) {
|
|
1326
1611
|
if (error instanceof NeedsLoginError) {
|
|
@@ -1329,12 +1614,14 @@ async function run(rawOptions = {}) {
|
|
|
1329
1614
|
}
|
|
1330
1615
|
print(`failed: ${error.message}`);
|
|
1331
1616
|
return 1;
|
|
1617
|
+
} finally {
|
|
1618
|
+
unlock?.();
|
|
1332
1619
|
}
|
|
1333
1620
|
}
|
|
1334
|
-
function flagValue(
|
|
1335
|
-
const index =
|
|
1621
|
+
function flagValue(argv2, flag) {
|
|
1622
|
+
const index = argv2.indexOf(flag);
|
|
1336
1623
|
if (index < 0) return void 0;
|
|
1337
|
-
const value =
|
|
1624
|
+
const value = argv2[index + 1];
|
|
1338
1625
|
return value && !value.startsWith("-") ? value : void 0;
|
|
1339
1626
|
}
|
|
1340
1627
|
function printUsage(config, print) {
|
|
@@ -1348,8 +1635,8 @@ async function runFile(file, args, printErr) {
|
|
|
1348
1635
|
printErr("launcher --run needs a file to run.");
|
|
1349
1636
|
return 1;
|
|
1350
1637
|
}
|
|
1351
|
-
const abs =
|
|
1352
|
-
if (!
|
|
1638
|
+
const abs = resolve2(process.cwd(), file);
|
|
1639
|
+
if (!existsSync6(abs)) {
|
|
1353
1640
|
printErr(`cannot run ${file}: no such file.`);
|
|
1354
1641
|
return 1;
|
|
1355
1642
|
}
|
|
@@ -1442,14 +1729,14 @@ async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
|
|
|
1442
1729
|
}
|
|
1443
1730
|
function readPayloadArgv(dir, key) {
|
|
1444
1731
|
try {
|
|
1445
|
-
const parsed = JSON.parse(
|
|
1446
|
-
const
|
|
1447
|
-
if (typeof
|
|
1448
|
-
const parts =
|
|
1732
|
+
const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
|
|
1733
|
+
const entry2 = parsed[key];
|
|
1734
|
+
if (typeof entry2 === "string") {
|
|
1735
|
+
const parts = entry2.trim().split(/\s+/).filter(Boolean);
|
|
1449
1736
|
return parts.length > 0 ? parts : null;
|
|
1450
1737
|
}
|
|
1451
|
-
if (Array.isArray(
|
|
1452
|
-
return
|
|
1738
|
+
if (Array.isArray(entry2) && entry2.every((part) => typeof part === "string" && part.length > 0)) {
|
|
1739
|
+
return entry2;
|
|
1453
1740
|
}
|
|
1454
1741
|
return null;
|
|
1455
1742
|
} catch {
|
|
@@ -1464,7 +1751,7 @@ function readPayloadRun(dir) {
|
|
|
1464
1751
|
}
|
|
1465
1752
|
function readPayloadVerbs(dir) {
|
|
1466
1753
|
try {
|
|
1467
|
-
const parsed = JSON.parse(
|
|
1754
|
+
const parsed = JSON.parse(readFileSync7(join6(payloadDir(dir), "payload.json"), "utf8"));
|
|
1468
1755
|
const verbs = parsed.verbs;
|
|
1469
1756
|
if (verbs === "*") return "*";
|
|
1470
1757
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -1475,13 +1762,13 @@ function readPayloadVerbs(dir) {
|
|
|
1475
1762
|
return null;
|
|
1476
1763
|
}
|
|
1477
1764
|
}
|
|
1478
|
-
function resolveEntry(
|
|
1479
|
-
return
|
|
1765
|
+
function resolveEntry(entry2) {
|
|
1766
|
+
return entry2[0] === "$self" ? [process.execPath, ...entry2.slice(1)] : entry2;
|
|
1480
1767
|
}
|
|
1481
|
-
function needsShell(
|
|
1768
|
+
function needsShell(command2) {
|
|
1482
1769
|
if (process.platform !== "win32") return false;
|
|
1483
|
-
if (/\.(cmd|bat)$/i.test(
|
|
1484
|
-
return !
|
|
1770
|
+
if (/\.(cmd|bat)$/i.test(command2)) return true;
|
|
1771
|
+
return !existsSync6(command2);
|
|
1485
1772
|
}
|
|
1486
1773
|
function quoteForShell(arg) {
|
|
1487
1774
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -1508,11 +1795,11 @@ function parseProgress(raw) {
|
|
|
1508
1795
|
}
|
|
1509
1796
|
return progress;
|
|
1510
1797
|
}
|
|
1511
|
-
function defaultRunEntry(
|
|
1512
|
-
const [
|
|
1513
|
-
const shell = needsShell(
|
|
1514
|
-
const commandLine = shell ? [
|
|
1515
|
-
const spawnEntry = (progress2) =>
|
|
1798
|
+
function defaultRunEntry(entry2, cwd, env) {
|
|
1799
|
+
const [command2, ...args] = entry2;
|
|
1800
|
+
const shell = needsShell(command2);
|
|
1801
|
+
const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
|
|
1802
|
+
const spawnEntry = (progress2) => spawnSync3(commandLine, shell ? [] : args, {
|
|
1516
1803
|
cwd,
|
|
1517
1804
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
1518
1805
|
shell,
|
|
@@ -1570,11 +1857,41 @@ async function installOrUpdate(config, dir, options, print, update) {
|
|
|
1570
1857
|
version = manifest.version;
|
|
1571
1858
|
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1572
1859
|
const current = readState(dir);
|
|
1573
|
-
const unchanged = update && current?.version === version &&
|
|
1860
|
+
const unchanged = update && current?.version === version && existsSync6(payloadDir(dir));
|
|
1861
|
+
if (unchanged && current.acquired) {
|
|
1862
|
+
installer.surface({ id: config.product, from: version, to: version, state: "current" });
|
|
1863
|
+
return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
|
|
1864
|
+
}
|
|
1574
1865
|
if (!unchanged) {
|
|
1575
1866
|
installer.phase("download", { state: "running" });
|
|
1576
1867
|
started = Date.now();
|
|
1577
|
-
|
|
1868
|
+
const candidate = randomUUID4();
|
|
1869
|
+
const candidateDir = join6(dir, "candidates", candidate);
|
|
1870
|
+
await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl });
|
|
1871
|
+
const acquisition = existsSync6(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
|
|
1872
|
+
if (acquisition) {
|
|
1873
|
+
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1874
|
+
installer.phase("activate", { state: "running" });
|
|
1875
|
+
let outcome;
|
|
1876
|
+
const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1877
|
+
await installAcquisition(dir, candidate, version, acquisition, {
|
|
1878
|
+
fetchImpl,
|
|
1879
|
+
env,
|
|
1880
|
+
run: async (command2, cwd, childEnv) => {
|
|
1881
|
+
const result = options.runEntry ? options.runEntry(command2, cwd, childEnv) : await runInstallEntry(command2, cwd, childEnv, installer, readTokens(dir));
|
|
1882
|
+
if (result.outcome) outcome = validateInstallerOutcome(result.outcome);
|
|
1883
|
+
return result.ok && (result.code === void 0 || result.code === 0) && !result.outcome?.operationFailed && !result.outcome?.failed;
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
installer.phase("activate");
|
|
1887
|
+
installer.surface({ id: config.product, from: current?.version, to: version, state: "updated" });
|
|
1888
|
+
installer.finish(outcome ?? { version, total: 1, updated: 1, failed: 0, installed: true });
|
|
1889
|
+
return 0;
|
|
1890
|
+
}
|
|
1891
|
+
const target = join6(dir, "payload");
|
|
1892
|
+
mkdirSync6(dir, { recursive: true });
|
|
1893
|
+
rmSync5(target, { recursive: true, force: true });
|
|
1894
|
+
renameSync4(join6(candidateDir, "payload"), target);
|
|
1578
1895
|
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1579
1896
|
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1580
1897
|
}
|
|
@@ -1612,12 +1929,12 @@ function payloadEnv(dir) {
|
|
|
1612
1929
|
return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
|
|
1613
1930
|
}
|
|
1614
1931
|
var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
|
|
1615
|
-
function payloadFileAsCommand(
|
|
1616
|
-
const first =
|
|
1932
|
+
function payloadFileAsCommand(entry2, payload) {
|
|
1933
|
+
const first = entry2[0];
|
|
1617
1934
|
if (!first || first === "$self") return null;
|
|
1618
1935
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
1619
|
-
const candidate =
|
|
1620
|
-
if (!
|
|
1936
|
+
const candidate = join6(payload, first);
|
|
1937
|
+
if (!existsSync6(candidate)) return null;
|
|
1621
1938
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
1622
1939
|
if (process.platform === "win32") return null;
|
|
1623
1940
|
try {
|
|
@@ -1626,22 +1943,22 @@ function payloadFileAsCommand(entry, payload) {
|
|
|
1626
1943
|
return null;
|
|
1627
1944
|
}
|
|
1628
1945
|
}
|
|
1629
|
-
async function finishLastMile(config, dir, options, installer, version, unchanged) {
|
|
1630
|
-
const
|
|
1946
|
+
async function finishLastMile(config, dir, options, installer, version, unchanged, repairCommand) {
|
|
1947
|
+
const entry2 = repairCommand ?? readPayloadEntry(dir);
|
|
1631
1948
|
const payload = payloadDir(dir);
|
|
1632
|
-
if (!
|
|
1949
|
+
if (!entry2) {
|
|
1633
1950
|
installer.finish({
|
|
1634
1951
|
version,
|
|
1635
1952
|
total: 1,
|
|
1636
1953
|
updated: unchanged ? 0 : 1,
|
|
1637
1954
|
failed: 0,
|
|
1638
1955
|
installed: true,
|
|
1639
|
-
detail: `next step: run ${
|
|
1956
|
+
detail: `next step: run ${join6(payload, config.binName)} to start ${config.product}.`
|
|
1640
1957
|
});
|
|
1641
1958
|
return 0;
|
|
1642
1959
|
}
|
|
1643
|
-
const
|
|
1644
|
-
const dataFile = payloadFileAsCommand(
|
|
1960
|
+
const command2 = resolveEntry(entry2);
|
|
1961
|
+
const dataFile = payloadFileAsCommand(entry2, payload);
|
|
1645
1962
|
if (dataFile) {
|
|
1646
1963
|
installer.finish({
|
|
1647
1964
|
version,
|
|
@@ -1654,8 +1971,9 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
1654
1971
|
}
|
|
1655
1972
|
installer.phase("activate", { state: "running" });
|
|
1656
1973
|
const started = Date.now();
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1974
|
+
const inherited = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1975
|
+
const env = repairCommand ? runtimeEnvironment(repairCommand[0], inherited) : inherited;
|
|
1976
|
+
const result = options.runEntry ? options.runEntry(command2, payload, env) : await runInstallEntry(command2, payload, env, installer, readTokens(dir));
|
|
1659
1977
|
for (const progress of result.progress ?? []) installer.milestone(progress);
|
|
1660
1978
|
const succeeded = result.ok && (result.code === void 0 || result.code === 0);
|
|
1661
1979
|
const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
|
|
@@ -1670,18 +1988,18 @@ async function finishLastMile(config, dir, options, installer, version, unchange
|
|
|
1670
1988
|
},
|
|
1671
1989
|
...!succeeded ? {
|
|
1672
1990
|
operationFailed: true,
|
|
1673
|
-
detail: result.code === void 0 ? `Arming this machine could not start: ${result.error ??
|
|
1674
|
-
retry: `(cd ${payload} && ${
|
|
1991
|
+
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}` : ""}`,
|
|
1992
|
+
retry: `(cd ${payload} && ${command2.join(" ")})`
|
|
1675
1993
|
} : {}
|
|
1676
1994
|
});
|
|
1677
1995
|
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1678
1996
|
}
|
|
1679
|
-
async function runInstallEntry(
|
|
1680
|
-
const [
|
|
1681
|
-
const shell = needsShell(
|
|
1997
|
+
async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
|
|
1998
|
+
const [command2, ...args] = entry2;
|
|
1999
|
+
const shell = needsShell(command2);
|
|
1682
2000
|
const progress = !(process.platform === "win32" && shell);
|
|
1683
|
-
const outcomeDir =
|
|
1684
|
-
const outcomeFile =
|
|
2001
|
+
const outcomeDir = mkdtempSync2(join6(tmpdir4(), "mm-installer-outcome-"));
|
|
2002
|
+
const outcomeFile = join6(outcomeDir, "outcome.json");
|
|
1685
2003
|
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
1686
2004
|
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
1687
2005
|
delete childEnv.MM_PROGRESS_FD;
|
|
@@ -1690,8 +2008,8 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
|
1690
2008
|
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
1691
2009
|
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
1692
2010
|
try {
|
|
1693
|
-
const result = await new Promise((
|
|
1694
|
-
const child = spawn2(shell ? [
|
|
2011
|
+
const result = await new Promise((resolve3) => {
|
|
2012
|
+
const child = spawn2(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
|
|
1695
2013
|
cwd,
|
|
1696
2014
|
shell,
|
|
1697
2015
|
windowsHide: true,
|
|
@@ -1727,8 +2045,8 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
|
1727
2045
|
if (pending.length > 65536) pending = "";
|
|
1728
2046
|
});
|
|
1729
2047
|
}
|
|
1730
|
-
child.on("error", (error) =>
|
|
1731
|
-
child.on("close", (code) =>
|
|
2048
|
+
child.on("error", (error) => resolve3({ ok: false, error: error.message }));
|
|
2049
|
+
child.on("close", (code) => resolve3({
|
|
1732
2050
|
ok: code === 0,
|
|
1733
2051
|
...code !== null ? { code } : {},
|
|
1734
2052
|
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
@@ -1741,25 +2059,30 @@ async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
|
1741
2059
|
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
1742
2060
|
}
|
|
1743
2061
|
} finally {
|
|
1744
|
-
if (
|
|
2062
|
+
if (existsSync6(outcomeFile)) unlinkSync2(outcomeFile);
|
|
1745
2063
|
rmdirSync(outcomeDir);
|
|
1746
2064
|
}
|
|
1747
2065
|
}
|
|
1748
|
-
function doForward(config, dir, options,
|
|
1749
|
-
if (
|
|
1750
|
-
print(`${
|
|
2066
|
+
function doForward(config, dir, options, command2, argv2, print, unlock) {
|
|
2067
|
+
if (command2 && existsSync6(command2)) {
|
|
2068
|
+
print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
|
|
1751
2069
|
return 2;
|
|
1752
2070
|
}
|
|
1753
|
-
const
|
|
2071
|
+
const acquired = readState(dir)?.acquired;
|
|
2072
|
+
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
1754
2073
|
const verbs = readPayloadVerbs(dir);
|
|
1755
|
-
const declared = verbs === "*" || Array.isArray(verbs) &&
|
|
2074
|
+
const declared = verbs === "*" || Array.isArray(verbs) && command2 !== void 0 && verbs.includes(command2);
|
|
1756
2075
|
if (!target || !declared) {
|
|
1757
|
-
print(`unknown command: ${
|
|
2076
|
+
print(`unknown command: ${command2}`);
|
|
1758
2077
|
printUsage(config, print);
|
|
1759
2078
|
return 2;
|
|
1760
2079
|
}
|
|
1761
|
-
const forwarded = [...resolveEntry(target), ...
|
|
1762
|
-
const
|
|
2080
|
+
const forwarded = [...resolveEntry(target), ...argv2];
|
|
2081
|
+
const cwd = payloadDir(dir);
|
|
2082
|
+
const inherited = payloadEnv(dir);
|
|
2083
|
+
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2084
|
+
unlock();
|
|
2085
|
+
const result = (options.runEntry ?? defaultRunEntry)(forwarded, cwd, env);
|
|
1763
2086
|
if (!result.ok && result.code === void 0) {
|
|
1764
2087
|
print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
|
|
1765
2088
|
return 1;
|
|
@@ -1769,10 +2092,10 @@ function doForward(config, dir, options, command, argv, print) {
|
|
|
1769
2092
|
function doAutoupdate(config, options, args, print) {
|
|
1770
2093
|
const mode = args[0] ?? "status";
|
|
1771
2094
|
const scheduleOptions = options.autoupdate ?? {};
|
|
1772
|
-
const
|
|
2095
|
+
const command2 = options.autoupdate?.command ?? [process.execPath, "update"];
|
|
1773
2096
|
if (mode === "on") {
|
|
1774
2097
|
try {
|
|
1775
|
-
enableSchedule(config,
|
|
2098
|
+
enableSchedule(config, command2, scheduleOptions);
|
|
1776
2099
|
} catch (error) {
|
|
1777
2100
|
print(error.message);
|
|
1778
2101
|
return 1;
|
|
@@ -1804,19 +2127,24 @@ function doAutoupdate(config, options, args, print) {
|
|
|
1804
2127
|
if (state.lastRun) print(`last run: ${state.lastRun}`);
|
|
1805
2128
|
return 0;
|
|
1806
2129
|
}
|
|
1807
|
-
function doDoctor(config, dir, options, print) {
|
|
2130
|
+
function doDoctor(config, dir, options, print, unlock) {
|
|
1808
2131
|
doLauncherDoctor(config, dir, options, print);
|
|
1809
|
-
const
|
|
2132
|
+
const acquired = readState(dir)?.acquired;
|
|
2133
|
+
const target = acquired ? [acquired.node, acquired.entry] : readPayloadRun(dir);
|
|
1810
2134
|
const verbs = readPayloadVerbs(dir);
|
|
1811
2135
|
const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
|
|
1812
2136
|
if (!chains) return 0;
|
|
1813
|
-
const
|
|
2137
|
+
const cwd = payloadDir(dir);
|
|
2138
|
+
const inherited = payloadEnv(dir);
|
|
2139
|
+
const env = acquired ? runtimeEnvironment(acquired.node, inherited ?? process.env) : inherited;
|
|
2140
|
+
unlock();
|
|
2141
|
+
const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], cwd, env);
|
|
1814
2142
|
return result.code ?? (result.ok ? 0 : 1);
|
|
1815
2143
|
}
|
|
1816
2144
|
function doLauncherDoctor(config, dir, options, print) {
|
|
1817
2145
|
const tokens = readTokens(dir);
|
|
1818
2146
|
const state = readState(dir);
|
|
1819
|
-
const payloadPresent =
|
|
2147
|
+
const payloadPresent = existsSync6(payloadDir(dir));
|
|
1820
2148
|
print(`product: ${config.product}`);
|
|
1821
2149
|
print(`host: ${config.host}`);
|
|
1822
2150
|
print(`login: ${config.loginKind}`);
|
|
@@ -1828,7 +2156,7 @@ function doLauncherDoctor(config, dir, options, print) {
|
|
|
1828
2156
|
print("token: none \u2014 run login first.");
|
|
1829
2157
|
}
|
|
1830
2158
|
print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
|
|
1831
|
-
print(`paths: tokens ${
|
|
2159
|
+
print(`paths: tokens ${join6(dir, "tokens.json")}, state ${join6(dir, "state.json")}, payload ${payloadDir(dir)}`);
|
|
1832
2160
|
const schedule = querySchedule(config, options.autoupdate ?? {});
|
|
1833
2161
|
print(
|
|
1834
2162
|
`auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
|