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