@mutmutco/installer-launcher 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/launcher.js +603 -141
- package/dist/launcher.sea.cjs +629 -166
- package/package.json +1 -4
package/dist/launcher.sea.cjs
CHANGED
|
@@ -28,35 +28,48 @@ __export(index_exports, {
|
|
|
28
28
|
readPayloadVerbs: () => readPayloadVerbs,
|
|
29
29
|
resolveEntry: () => resolveEntry,
|
|
30
30
|
run: () => run,
|
|
31
|
-
runFile: () => runFile
|
|
31
|
+
runFile: () => runFile,
|
|
32
|
+
runInstallEntry: () => runInstallEntry
|
|
32
33
|
});
|
|
33
34
|
module.exports = __toCommonJS(index_exports);
|
|
34
35
|
var import_node_child_process3 = require("node:child_process");
|
|
35
|
-
var
|
|
36
|
+
var import_node_fs9 = require("node:fs");
|
|
37
|
+
var import_node_os4 = require("node:os");
|
|
36
38
|
var import_node_path4 = require("node:path");
|
|
37
39
|
var import_node_url2 = require("node:url");
|
|
38
40
|
|
|
39
|
-
//
|
|
41
|
+
// ../face/src/face.ts
|
|
42
|
+
var import_node_fs = require("node:fs");
|
|
43
|
+
|
|
44
|
+
// ../face/src/products.ts
|
|
40
45
|
var PRODUCTS = Object.freeze({
|
|
41
46
|
"mm-strategy": Object.freeze({
|
|
42
47
|
name: "MM Strategy",
|
|
48
|
+
installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
|
|
43
49
|
accent: "38;2;249;115;22",
|
|
44
|
-
warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
|
|
50
|
+
warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
|
|
51
|
+
doctor: "mm-strategy doctor"
|
|
45
52
|
}),
|
|
46
53
|
"mmi-hub": Object.freeze({
|
|
47
54
|
name: "mmi-hub",
|
|
55
|
+
installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
|
|
48
56
|
accent: "38;2;125;211;252",
|
|
49
|
-
warm: "Welcome back. Checking your surfaces\u2026"
|
|
57
|
+
warm: "Welcome back. Checking your surfaces\u2026",
|
|
58
|
+
doctor: "mmi doctor"
|
|
50
59
|
}),
|
|
51
60
|
"jerv-hub": Object.freeze({
|
|
52
61
|
name: "jerv-hub",
|
|
62
|
+
installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
|
|
53
63
|
accent: "38;2;248;113;113",
|
|
54
|
-
warm: "Welcome back. Checking your surfaces\u2026"
|
|
64
|
+
warm: "Welcome back. Checking your surfaces\u2026",
|
|
65
|
+
doctor: "jerv doctor"
|
|
55
66
|
}),
|
|
56
67
|
jervcode: Object.freeze({
|
|
57
68
|
name: "JervCode",
|
|
69
|
+
installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
|
|
58
70
|
accent: "38;2;192;132;252",
|
|
59
|
-
warm: "Welcome back. Keeping JervCode current\u2026"
|
|
71
|
+
warm: "Welcome back. Keeping JervCode current\u2026",
|
|
72
|
+
doctor: "jervcode doctor"
|
|
60
73
|
})
|
|
61
74
|
});
|
|
62
75
|
function identityFor(product) {
|
|
@@ -66,6 +79,8 @@ function identityFor(product) {
|
|
|
66
79
|
}
|
|
67
80
|
return identity;
|
|
68
81
|
}
|
|
82
|
+
|
|
83
|
+
// ../face/src/face.ts
|
|
69
84
|
var GLYPH = Object.freeze({
|
|
70
85
|
diamond: "\u25C6",
|
|
71
86
|
hollow: "\u25C7",
|
|
@@ -90,6 +105,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
|
|
|
90
105
|
function visibleWidth(text) {
|
|
91
106
|
return [...String(text).replace(ANSI, "")].length;
|
|
92
107
|
}
|
|
108
|
+
function stripColor(text) {
|
|
109
|
+
return String(text).replace(ANSI, "");
|
|
110
|
+
}
|
|
93
111
|
function faceWidth(columns) {
|
|
94
112
|
const raw = Number(columns);
|
|
95
113
|
return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
|
|
@@ -118,7 +136,14 @@ function wrapWords(text, width) {
|
|
|
118
136
|
if (line) lines.push(line);
|
|
119
137
|
return lines.length ? lines : [""];
|
|
120
138
|
}
|
|
121
|
-
|
|
139
|
+
var PROGRESS_PROTOCOL = 1;
|
|
140
|
+
function readProgressFd(env) {
|
|
141
|
+
const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
|
|
142
|
+
if (!Number.isInteger(fd) || fd <= 0) return null;
|
|
143
|
+
const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
|
|
144
|
+
return protocol === PROGRESS_PROTOCOL ? fd : null;
|
|
145
|
+
}
|
|
146
|
+
function createFace({ product, color = false, columns, env = process.env, operation }) {
|
|
122
147
|
const identity = identityFor(product);
|
|
123
148
|
const width = faceWidth(columns);
|
|
124
149
|
const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
|
|
@@ -126,28 +151,45 @@ function createFace({ product, color = false, columns, env = process.env }) {
|
|
|
126
151
|
const indent = " ".repeat(TITLE_COLUMN - 1);
|
|
127
152
|
const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
|
|
128
153
|
const continuesFace = continuedPhases.size > 0;
|
|
129
|
-
const
|
|
154
|
+
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
155
|
+
const progressFd = readProgressFd(env);
|
|
156
|
+
const emitMilestone = (title, measure, kind) => {
|
|
157
|
+
if (progressFd === null) return false;
|
|
158
|
+
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
159
|
+
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
160
|
+
try {
|
|
161
|
+
(0, import_node_fs.writeSync)(progressFd, `${JSON.stringify(record)}
|
|
162
|
+
`);
|
|
163
|
+
return true;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const welcome = () => continuesFace || nested ? [] : [
|
|
130
169
|
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
131
170
|
bar(),
|
|
132
|
-
`${bar()} ${identity.warm}`,
|
|
171
|
+
`${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
|
|
133
172
|
bar()
|
|
134
173
|
];
|
|
135
174
|
const continues = (phase, kind = "ok") => {
|
|
136
175
|
const inherited = continuedPhases.delete(String(phase).trim());
|
|
137
176
|
return kind === "fail" ? false : inherited;
|
|
138
177
|
};
|
|
139
|
-
const step = (title,
|
|
178
|
+
const step = (title, measure = null, kind = "ok") => {
|
|
179
|
+
if (emitMilestone(title, measure, kind)) return "";
|
|
140
180
|
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
141
|
-
const
|
|
181
|
+
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
182
|
+
const time = measured;
|
|
142
183
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
143
|
-
const reserved = time ? 6 : 0;
|
|
184
|
+
const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
|
|
144
185
|
const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
|
|
145
186
|
const head = `${bar()} ${glyph} ${first}`;
|
|
146
187
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
147
188
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
148
189
|
};
|
|
149
|
-
const relay = (text) => String(text).split("\n").
|
|
150
|
-
const receipt = (lines) => {
|
|
190
|
+
const relay = (text) => String(text).split("\n").flatMap((line) => line.trim() === "" ? [bar()] : (visibleWidth(line) <= width - TITLE_COLUMN ? [line] : wrapWords(line, width - TITLE_COLUMN)).map((part) => `${bar()}${indent}${part}`)).join("\n");
|
|
191
|
+
const receipt = (lines, { ready = true } = {}) => {
|
|
192
|
+
if (ready && nested) return [];
|
|
151
193
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
152
194
|
const line = String(raw);
|
|
153
195
|
if (visibleWidth(line) <= width - 6) return [line];
|
|
@@ -163,18 +205,361 @@ function createFace({ product, color = false, columns, env = process.env }) {
|
|
|
163
205
|
frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
|
|
164
206
|
];
|
|
165
207
|
};
|
|
166
|
-
const
|
|
208
|
+
const outcome = (changed, options = {}) => {
|
|
209
|
+
const fact = String(changed ?? "").trim();
|
|
210
|
+
if (!fact) {
|
|
211
|
+
throw new Error(`installer face: the receipt must say what changed \u2014 pass the one measured fact this run produced, such as "Updated 2 of 7 surfaces to 4.4.10." (${identity.name})`);
|
|
212
|
+
}
|
|
213
|
+
return receipt([
|
|
214
|
+
`${GLYPH.check} ${identity.name} is ready.`,
|
|
215
|
+
fact,
|
|
216
|
+
`Check health any time: ${identity.doctor}`
|
|
217
|
+
], options);
|
|
218
|
+
};
|
|
219
|
+
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
167
220
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
168
|
-
return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
|
|
221
|
+
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
169
222
|
}
|
|
223
|
+
|
|
224
|
+
// ../face/src/shell.ts
|
|
170
225
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
226
|
+
|
|
227
|
+
// ../face/src/spinner.ts
|
|
228
|
+
var import_node_fs2 = require("node:fs");
|
|
229
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
171
230
|
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
231
|
+
var WORKER_SOURCE = `
|
|
232
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
233
|
+
const { writeSync, appendFileSync } = require('node:fs');
|
|
234
|
+
const control = new Int32Array(workerData.control);
|
|
235
|
+
let frames = workerData.frames, frame = workerData.frame;
|
|
236
|
+
function draw() {
|
|
237
|
+
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
238
|
+
try {
|
|
239
|
+
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
240
|
+
const text = frames[frame++ % frames.length];
|
|
241
|
+
writeSync(2, text);
|
|
242
|
+
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
243
|
+
} finally {
|
|
244
|
+
Atomics.store(control, 1, 0);
|
|
245
|
+
Atomics.notify(control, 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
parentPort.on('message', (next) => {
|
|
249
|
+
if (Atomics.load(control, 2)) return;
|
|
250
|
+
frames = next.frames;
|
|
251
|
+
frame = next.frame;
|
|
252
|
+
Atomics.store(control, 0, 0);
|
|
253
|
+
});
|
|
254
|
+
setInterval(draw, workerData.intervalMs);
|
|
255
|
+
`;
|
|
256
|
+
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
|
|
257
|
+
animate = animate && !face.nested && !face.emitsProgress;
|
|
258
|
+
let timer = null;
|
|
259
|
+
let worker = null;
|
|
260
|
+
let control = null;
|
|
261
|
+
let frames = [];
|
|
262
|
+
let frame = 0;
|
|
263
|
+
const write = (text) => {
|
|
264
|
+
if (stream) stream.write(text);
|
|
265
|
+
else {
|
|
266
|
+
(0, import_node_fs2.writeSync)(2, text);
|
|
267
|
+
if (transcriptPath) (0, import_node_fs2.appendFileSync)(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
268
|
+
`);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const render = (text, measure) => {
|
|
272
|
+
const line = face.step(text, measure, "note").split("\n")[0];
|
|
273
|
+
frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
|
|
274
|
+
};
|
|
275
|
+
const draw = () => {
|
|
276
|
+
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
277
|
+
};
|
|
278
|
+
const pause = () => {
|
|
279
|
+
if (!control) return;
|
|
280
|
+
Atomics.store(control, 0, 1);
|
|
281
|
+
while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
|
|
282
|
+
};
|
|
283
|
+
const halt = () => {
|
|
284
|
+
if (timer) clearInterval(timer);
|
|
285
|
+
timer = null;
|
|
286
|
+
if (control) Atomics.store(control, 2, 1);
|
|
287
|
+
pause();
|
|
288
|
+
if (worker) void worker.terminate();
|
|
289
|
+
worker = null;
|
|
290
|
+
control = null;
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
start(text, measure = null) {
|
|
294
|
+
if (!animate) return;
|
|
295
|
+
halt();
|
|
296
|
+
render(text, measure);
|
|
297
|
+
frame = 0;
|
|
298
|
+
draw();
|
|
299
|
+
if (!frames.length) return;
|
|
300
|
+
if (stream) timer = setInterval(draw, intervalMs).unref();
|
|
301
|
+
else {
|
|
302
|
+
control = new Int32Array(new SharedArrayBuffer(12));
|
|
303
|
+
try {
|
|
304
|
+
worker = new import_node_worker_threads.Worker(WORKER_SOURCE, { eval: true, workerData: {
|
|
305
|
+
control: control.buffer,
|
|
306
|
+
frames,
|
|
307
|
+
frame,
|
|
308
|
+
intervalMs,
|
|
309
|
+
transcriptPath
|
|
310
|
+
} });
|
|
311
|
+
const active = worker;
|
|
312
|
+
worker.on("error", () => {
|
|
313
|
+
if (worker === active) halt();
|
|
314
|
+
});
|
|
315
|
+
worker.unref();
|
|
316
|
+
} catch {
|
|
317
|
+
halt();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
say(text, measure = null) {
|
|
322
|
+
if (!animate) return;
|
|
323
|
+
pause();
|
|
324
|
+
render(text, measure);
|
|
325
|
+
draw();
|
|
326
|
+
worker?.postMessage({ frames, frame });
|
|
327
|
+
},
|
|
328
|
+
stop() {
|
|
329
|
+
halt();
|
|
330
|
+
if (animate) write("\r\x1B[2K");
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
172
334
|
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
335
|
+
|
|
336
|
+
// ../face/src/conformance.ts
|
|
173
337
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
174
338
|
|
|
339
|
+
// ../face/src/run.ts
|
|
340
|
+
var import_node_fs4 = require("node:fs");
|
|
341
|
+
|
|
342
|
+
// ../face/src/outcome.ts
|
|
343
|
+
var import_node_fs3 = require("node:fs");
|
|
344
|
+
var counts = ["total", "updated", "failed"];
|
|
345
|
+
var strings = ["version", "retry", "detail"];
|
|
346
|
+
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
347
|
+
function validateInstallerOutcome(value) {
|
|
348
|
+
const invalid = () => {
|
|
349
|
+
throw new Error("installer outcome: invalid child result");
|
|
350
|
+
};
|
|
351
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
|
|
352
|
+
const facts = value;
|
|
353
|
+
const allowed = [...counts, ...strings, ...flags];
|
|
354
|
+
if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
|
|
355
|
+
for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
|
|
356
|
+
if (facts.updated + facts.failed > facts.total) return invalid();
|
|
357
|
+
for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
|
|
358
|
+
for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
|
|
359
|
+
return { ...facts };
|
|
360
|
+
}
|
|
361
|
+
function writeInstallerOutcome(path, value) {
|
|
362
|
+
(0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
363
|
+
}
|
|
364
|
+
function readInstallerOutcome(path) {
|
|
365
|
+
let text;
|
|
366
|
+
try {
|
|
367
|
+
text = (0, import_node_fs3.readFileSync)(path, "utf8");
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error.code === "ENOENT") return void 0;
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
if (text.length > 16384) throw new Error("installer outcome: child result is too large");
|
|
373
|
+
try {
|
|
374
|
+
return validateInstallerOutcome(JSON.parse(text));
|
|
375
|
+
} catch {
|
|
376
|
+
throw new Error("installer outcome: invalid child result");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ../face/src/run.ts
|
|
381
|
+
function validateInstallerProduct(value) {
|
|
382
|
+
const fail = (field2) => {
|
|
383
|
+
throw new Error(`installer product: invalid ${field2}`);
|
|
384
|
+
};
|
|
385
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
|
|
386
|
+
const input = value;
|
|
387
|
+
const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
|
|
388
|
+
const key = text(input.product, "product");
|
|
389
|
+
const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
|
|
390
|
+
const identity = identityFor(product);
|
|
391
|
+
const gate = text(input.gate, "gate");
|
|
392
|
+
let gateUrl;
|
|
393
|
+
try {
|
|
394
|
+
gateUrl = new URL(gate);
|
|
395
|
+
} catch {
|
|
396
|
+
return fail("gate");
|
|
397
|
+
}
|
|
398
|
+
if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
|
|
399
|
+
const doctor = text(input.doctor, "doctor");
|
|
400
|
+
if (doctor !== identity.doctor) return fail("doctor");
|
|
401
|
+
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
402
|
+
const ids = /* @__PURE__ */ new Set();
|
|
403
|
+
const surfaces = input.surfaces.map((raw) => {
|
|
404
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
|
|
405
|
+
const source = raw;
|
|
406
|
+
const id = text(source.id, "surface.id");
|
|
407
|
+
if (ids.has(id)) return fail("duplicate surface.id");
|
|
408
|
+
ids.add(id);
|
|
409
|
+
const surface = { id };
|
|
410
|
+
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
411
|
+
if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
|
|
412
|
+
}
|
|
413
|
+
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
414
|
+
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
415
|
+
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
416
|
+
return surface;
|
|
417
|
+
});
|
|
418
|
+
return { product, gate, doctor, surfaces };
|
|
419
|
+
}
|
|
420
|
+
var PHASES = {
|
|
421
|
+
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
422
|
+
resolve: ["Resolving the release", "Resolved the release"],
|
|
423
|
+
download: ["Downloading the payload", "Downloaded the payload"],
|
|
424
|
+
"sign-in": ["Signing in", "Signed in"],
|
|
425
|
+
check: ["Checking surfaces", "Checked surfaces"],
|
|
426
|
+
arm: ["Scheduling updates", "Armed hourly updates"],
|
|
427
|
+
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
428
|
+
verify: ["Verifying the payload", "Verified the payload"],
|
|
429
|
+
install: ["Installing the product", "Installed the product"],
|
|
430
|
+
activate: ["Activating surfaces", "Activated surfaces"],
|
|
431
|
+
doctor: ["Checking health", "Checked health"]
|
|
432
|
+
};
|
|
433
|
+
function createInstallerRun(value, options = {}) {
|
|
434
|
+
const declaration = validateInstallerProduct(value);
|
|
435
|
+
const env = options.env ?? process.env;
|
|
436
|
+
const tty = options.tty ?? Boolean(process.stdout.isTTY);
|
|
437
|
+
const face = createFace({
|
|
438
|
+
operation: options.operation,
|
|
439
|
+
product: declaration.product,
|
|
440
|
+
columns: options.columns,
|
|
441
|
+
env,
|
|
442
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
443
|
+
});
|
|
444
|
+
const write = options.write ?? ((text, channel) => {
|
|
445
|
+
(channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
446
|
+
});
|
|
447
|
+
const emit = (text, channel = "stdout", recorded = text) => {
|
|
448
|
+
if (!text) return;
|
|
449
|
+
write(text, channel);
|
|
450
|
+
if (env.MM_FACE_TRANSCRIPT) {
|
|
451
|
+
(0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
452
|
+
`, "utf8");
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const lines = (rows, channel = "stdout") => {
|
|
456
|
+
for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
|
|
457
|
+
`, channel);
|
|
458
|
+
};
|
|
459
|
+
const spinner = createSpinner(face, {
|
|
460
|
+
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
461
|
+
...options.write ? { stream: { write: (text) => {
|
|
462
|
+
emit(String(text), "spinner");
|
|
463
|
+
return true;
|
|
464
|
+
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
465
|
+
});
|
|
466
|
+
let started = false;
|
|
467
|
+
let finished = false;
|
|
468
|
+
const start = () => {
|
|
469
|
+
if (started || finished) return;
|
|
470
|
+
started = true;
|
|
471
|
+
const welcome = face.welcome();
|
|
472
|
+
if (tty) lines(welcome);
|
|
473
|
+
else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
|
|
474
|
+
};
|
|
475
|
+
const durable = (title, measure, kind) => {
|
|
476
|
+
spinner.stop();
|
|
477
|
+
const rendered = face.step(title, measure, kind);
|
|
478
|
+
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
479
|
+
};
|
|
480
|
+
const run2 = {
|
|
481
|
+
start,
|
|
482
|
+
phase(id, facts = {}) {
|
|
483
|
+
if (finished) throw new Error("installer run already finished");
|
|
484
|
+
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
485
|
+
start();
|
|
486
|
+
const state = facts.state ?? "ok";
|
|
487
|
+
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
488
|
+
if (state === "running") {
|
|
489
|
+
spinner.start(title, facts.measure ?? null);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
493
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
494
|
+
},
|
|
495
|
+
surface(facts) {
|
|
496
|
+
if (finished) throw new Error("installer run already finished");
|
|
497
|
+
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
498
|
+
if (!surface) throw new Error("installer run: undeclared surface");
|
|
499
|
+
start();
|
|
500
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
501
|
+
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
502
|
+
if (!status) throw new Error("installer run: unknown surface state");
|
|
503
|
+
const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
|
|
504
|
+
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
505
|
+
durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
506
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
507
|
+
},
|
|
508
|
+
milestone({ step, state, ms }) {
|
|
509
|
+
start();
|
|
510
|
+
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
511
|
+
},
|
|
512
|
+
signIn({ url, code }) {
|
|
513
|
+
start();
|
|
514
|
+
spinner.stop();
|
|
515
|
+
for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
|
|
516
|
+
const rendered = `${tty ? face.relay(text) : text}
|
|
517
|
+
`;
|
|
518
|
+
emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
522
|
+
relay(text, channel = "stdout", record = true) {
|
|
523
|
+
spinner.stop();
|
|
524
|
+
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
525
|
+
for (const row of rendered.split("\n")) if (row) {
|
|
526
|
+
emit(`${row}
|
|
527
|
+
`, channel, record ? `${row}
|
|
528
|
+
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
529
|
+
`);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
finish(facts) {
|
|
533
|
+
if (finished) return;
|
|
534
|
+
validateInstallerOutcome(facts);
|
|
535
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
536
|
+
start();
|
|
537
|
+
spinner.stop();
|
|
538
|
+
finished = true;
|
|
539
|
+
const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
|
|
540
|
+
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
541
|
+
const body = [
|
|
542
|
+
`${ready ? GLYPH.check : facts.dryRun ? GLYPH.dot : GLYPH.cross} ${face.identity.name}${ready ? " is ready." : facts.dryRun ? " preview complete." : facts.deferred ? " update deferred." : " is not ready yet."}`,
|
|
543
|
+
changed,
|
|
544
|
+
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
545
|
+
...facts.detail ? [facts.detail] : [],
|
|
546
|
+
facts.retry && facts.failed ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
547
|
+
];
|
|
548
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
549
|
+
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
550
|
+
}
|
|
551
|
+
if (tty) lines([face.signOff()]);
|
|
552
|
+
},
|
|
553
|
+
stop() {
|
|
554
|
+
spinner.stop();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
return run2;
|
|
558
|
+
}
|
|
559
|
+
|
|
175
560
|
// src/autoupdate.ts
|
|
176
561
|
var import_node_child_process = require("node:child_process");
|
|
177
|
-
var
|
|
562
|
+
var import_node_fs5 = require("node:fs");
|
|
178
563
|
var import_node_os = require("node:os");
|
|
179
564
|
var import_node_path = require("node:path");
|
|
180
565
|
function schedulePlatform(override) {
|
|
@@ -221,9 +606,9 @@ function enableSchedule(config, command, options = {}) {
|
|
|
221
606
|
if (platform === "darwin") {
|
|
222
607
|
const label2 = scheduleLabel(config);
|
|
223
608
|
const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
|
|
224
|
-
(0,
|
|
609
|
+
(0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
|
|
225
610
|
const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
|
|
226
|
-
(0,
|
|
611
|
+
(0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
|
|
227
612
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
228
613
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
229
614
|
if (result2.code !== 0) {
|
|
@@ -233,9 +618,9 @@ function enableSchedule(config, command, options = {}) {
|
|
|
233
618
|
}
|
|
234
619
|
const label = scheduleLabel(config);
|
|
235
620
|
const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
|
|
236
|
-
(0,
|
|
237
|
-
(0,
|
|
238
|
-
(0,
|
|
621
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
622
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
|
|
623
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
239
624
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
240
625
|
if (reload.code !== 0) {
|
|
241
626
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -260,14 +645,14 @@ function disableSchedule(config, options = {}) {
|
|
|
260
645
|
if (platform === "darwin") {
|
|
261
646
|
const label2 = scheduleLabel(config);
|
|
262
647
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
263
|
-
(0,
|
|
648
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
264
649
|
return;
|
|
265
650
|
}
|
|
266
651
|
const label = scheduleLabel(config);
|
|
267
652
|
const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
|
|
268
653
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
269
|
-
(0,
|
|
270
|
-
(0,
|
|
654
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
|
|
655
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
|
|
271
656
|
}
|
|
272
657
|
function querySchedule(config, options = {}) {
|
|
273
658
|
const platform = schedulePlatform(options.platform);
|
|
@@ -286,11 +671,11 @@ function querySchedule(config, options = {}) {
|
|
|
286
671
|
}
|
|
287
672
|
if (platform === "darwin") {
|
|
288
673
|
const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
289
|
-
if (!(0,
|
|
674
|
+
if (!(0, import_node_fs5.existsSync)(plist)) return { supported: true, enabled: false };
|
|
290
675
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
291
676
|
}
|
|
292
677
|
const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
293
|
-
if (!(0,
|
|
678
|
+
if (!(0, import_node_fs5.existsSync)(timer)) return { supported: true, enabled: false };
|
|
294
679
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
295
680
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
296
681
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -348,7 +733,7 @@ function firstLine(text) {
|
|
|
348
733
|
}
|
|
349
734
|
|
|
350
735
|
// src/config.ts
|
|
351
|
-
var
|
|
736
|
+
var import_node_fs6 = require("node:fs");
|
|
352
737
|
var import_node_sea = require("node:sea");
|
|
353
738
|
|
|
354
739
|
// src/module-url.ts
|
|
@@ -371,10 +756,10 @@ function loadProductConfig(options = {}) {
|
|
|
371
756
|
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
372
757
|
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
373
758
|
if (explicit) {
|
|
374
|
-
return parseProductConfig((0,
|
|
759
|
+
return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
|
|
375
760
|
}
|
|
376
761
|
try {
|
|
377
|
-
return parseProductConfig((0,
|
|
762
|
+
return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
|
|
378
763
|
} catch {
|
|
379
764
|
}
|
|
380
765
|
try {
|
|
@@ -493,7 +878,8 @@ async function loginGithub(config, options = {}) {
|
|
|
493
878
|
const sleep = options.sleep ?? realSleep;
|
|
494
879
|
const issued = await requestDeviceCode(config, fetchImpl);
|
|
495
880
|
const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
|
|
496
|
-
|
|
881
|
+
if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
|
|
882
|
+
else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
|
|
497
883
|
open(openUrl);
|
|
498
884
|
const deadline = now() + issued.expires_in * 1e3;
|
|
499
885
|
let intervalMs = Math.max(1, issued.interval) * 1e3;
|
|
@@ -663,7 +1049,7 @@ function page(title, body) {
|
|
|
663
1049
|
|
|
664
1050
|
// src/payload.ts
|
|
665
1051
|
var import_node_crypto2 = require("node:crypto");
|
|
666
|
-
var
|
|
1052
|
+
var import_node_fs7 = require("node:fs");
|
|
667
1053
|
var import_node_os2 = require("node:os");
|
|
668
1054
|
var import_node_path2 = require("node:path");
|
|
669
1055
|
|
|
@@ -806,7 +1192,7 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
|
806
1192
|
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
807
1193
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
808
1194
|
const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
809
|
-
(0,
|
|
1195
|
+
(0, import_node_fs7.mkdirSync)(staging, { recursive: true });
|
|
810
1196
|
try {
|
|
811
1197
|
for (const entry of manifest.files) {
|
|
812
1198
|
const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
|
|
@@ -814,22 +1200,22 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
814
1200
|
throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
|
|
815
1201
|
}
|
|
816
1202
|
const dest = (0, import_node_path2.join)(staging, entry.path);
|
|
817
|
-
(0,
|
|
818
|
-
(0,
|
|
1203
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
|
|
1204
|
+
(0, import_node_fs7.writeFileSync)(dest, bytes);
|
|
819
1205
|
}
|
|
820
1206
|
const target = (0, import_node_path2.join)(dir, "payload");
|
|
821
|
-
(0,
|
|
822
|
-
(0,
|
|
823
|
-
(0,
|
|
1207
|
+
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
1208
|
+
(0, import_node_fs7.rmSync)(target, { force: true, recursive: true });
|
|
1209
|
+
(0, import_node_fs7.renameSync)(staging, target);
|
|
824
1210
|
} catch (error) {
|
|
825
|
-
(0,
|
|
1211
|
+
(0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
|
|
826
1212
|
throw error;
|
|
827
1213
|
}
|
|
828
1214
|
return manifest.version;
|
|
829
1215
|
}
|
|
830
1216
|
|
|
831
1217
|
// src/store.ts
|
|
832
|
-
var
|
|
1218
|
+
var import_node_fs8 = require("node:fs");
|
|
833
1219
|
var import_node_os3 = require("node:os");
|
|
834
1220
|
var import_node_path3 = require("node:path");
|
|
835
1221
|
function defaultProductDir(product) {
|
|
@@ -854,7 +1240,7 @@ function payloadDir(dir) {
|
|
|
854
1240
|
}
|
|
855
1241
|
function readTokens(dir) {
|
|
856
1242
|
try {
|
|
857
|
-
const data = JSON.parse((0,
|
|
1243
|
+
const data = JSON.parse((0, import_node_fs8.readFileSync)(tokensPath(dir), "utf8"));
|
|
858
1244
|
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
859
1245
|
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
860
1246
|
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
@@ -866,21 +1252,21 @@ function readTokens(dir) {
|
|
|
866
1252
|
}
|
|
867
1253
|
}
|
|
868
1254
|
function writeTokens(dir, tokens) {
|
|
869
|
-
(0,
|
|
1255
|
+
(0, import_node_fs8.mkdirSync)(dir, { recursive: true });
|
|
870
1256
|
try {
|
|
871
|
-
(0,
|
|
1257
|
+
(0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
872
1258
|
`, { mode: 384 });
|
|
873
1259
|
} catch {
|
|
874
|
-
(0,
|
|
1260
|
+
(0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
875
1261
|
`);
|
|
876
1262
|
}
|
|
877
1263
|
}
|
|
878
1264
|
function clearTokens(dir) {
|
|
879
|
-
(0,
|
|
1265
|
+
(0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
|
|
880
1266
|
}
|
|
881
1267
|
function readState(dir) {
|
|
882
1268
|
try {
|
|
883
|
-
const data = JSON.parse((0,
|
|
1269
|
+
const data = JSON.parse((0, import_node_fs8.readFileSync)(statePath(dir), "utf8"));
|
|
884
1270
|
if (typeof data.version !== "string" || !data.version) return null;
|
|
885
1271
|
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
886
1272
|
} catch {
|
|
@@ -888,18 +1274,18 @@ function readState(dir) {
|
|
|
888
1274
|
}
|
|
889
1275
|
}
|
|
890
1276
|
function writeState(dir, state) {
|
|
891
|
-
(0,
|
|
892
|
-
(0,
|
|
1277
|
+
(0, import_node_fs8.mkdirSync)(dir, { recursive: true });
|
|
1278
|
+
(0, import_node_fs8.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
893
1279
|
`);
|
|
894
1280
|
}
|
|
895
1281
|
function wipeProductDir(dir) {
|
|
896
|
-
(0,
|
|
897
|
-
(0,
|
|
898
|
-
(0,
|
|
1282
|
+
(0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
|
|
1283
|
+
(0, import_node_fs8.rmSync)(statePath(dir), { force: true });
|
|
1284
|
+
(0, import_node_fs8.rmSync)(payloadDir(dir), { force: true, recursive: true });
|
|
899
1285
|
}
|
|
900
1286
|
|
|
901
1287
|
// src/index.ts
|
|
902
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1288
|
+
var LAUNCHER_VERSION = true ? "0.1.10" : readVersionFromPackage();
|
|
903
1289
|
function defaultPrint(message) {
|
|
904
1290
|
process.stdout.write(`${message}
|
|
905
1291
|
`);
|
|
@@ -979,7 +1365,7 @@ async function runFile(file, args, printErr) {
|
|
|
979
1365
|
return 1;
|
|
980
1366
|
}
|
|
981
1367
|
const abs = (0, import_node_path4.resolve)(process.cwd(), file);
|
|
982
|
-
if (!(0,
|
|
1368
|
+
if (!(0, import_node_fs9.existsSync)(abs)) {
|
|
983
1369
|
printErr(`cannot run ${file}: no such file.`);
|
|
984
1370
|
return 1;
|
|
985
1371
|
}
|
|
@@ -1035,11 +1421,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1035
1421
|
});
|
|
1036
1422
|
return refreshed.accessToken;
|
|
1037
1423
|
}
|
|
1038
|
-
async function doLogin(config, dir, options, print) {
|
|
1424
|
+
async function doLogin(config, dir, options, print, installer) {
|
|
1039
1425
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1040
1426
|
const open = options.open ?? openBrowser;
|
|
1041
1427
|
if (config.loginKind === "github") {
|
|
1042
|
-
const tokens = await loginGithub(config, {
|
|
1428
|
+
const tokens = await loginGithub(config, {
|
|
1429
|
+
fetchImpl,
|
|
1430
|
+
open,
|
|
1431
|
+
print,
|
|
1432
|
+
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
1433
|
+
});
|
|
1043
1434
|
writeTokens(dir, {
|
|
1044
1435
|
accessToken: tokens.accessToken,
|
|
1045
1436
|
refreshToken: tokens.refreshToken,
|
|
@@ -1054,20 +1445,20 @@ async function doLogin(config, dir, options, print) {
|
|
|
1054
1445
|
clientId: tokens.clientId
|
|
1055
1446
|
});
|
|
1056
1447
|
}
|
|
1057
|
-
print(`signed in to ${config.product}.`);
|
|
1448
|
+
if (!installer) print(`signed in to ${config.product}.`);
|
|
1058
1449
|
}
|
|
1059
|
-
async function fetchAccessTokenOrLogin(config, dir, options, print) {
|
|
1450
|
+
async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
|
|
1060
1451
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1061
1452
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1062
1453
|
if (fresh) return fresh;
|
|
1063
|
-
await doLogin(config, dir, options, print);
|
|
1454
|
+
await doLogin(config, dir, options, print, installer);
|
|
1064
1455
|
const after = readTokens(dir);
|
|
1065
1456
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
1066
1457
|
return after.accessToken;
|
|
1067
1458
|
}
|
|
1068
1459
|
function readPayloadArgv(dir, key) {
|
|
1069
1460
|
try {
|
|
1070
|
-
const parsed = JSON.parse((0,
|
|
1461
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
1071
1462
|
const entry = parsed[key];
|
|
1072
1463
|
if (typeof entry === "string") {
|
|
1073
1464
|
const parts = entry.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1089,7 +1480,7 @@ function readPayloadRun(dir) {
|
|
|
1089
1480
|
}
|
|
1090
1481
|
function readPayloadVerbs(dir) {
|
|
1091
1482
|
try {
|
|
1092
|
-
const parsed = JSON.parse((0,
|
|
1483
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
1093
1484
|
const verbs = parsed.verbs;
|
|
1094
1485
|
if (verbs === "*") return "*";
|
|
1095
1486
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -1106,7 +1497,7 @@ function resolveEntry(entry) {
|
|
|
1106
1497
|
function needsShell(command) {
|
|
1107
1498
|
if (process.platform !== "win32") return false;
|
|
1108
1499
|
if (/\.(cmd|bat)$/i.test(command)) return true;
|
|
1109
|
-
return !(0,
|
|
1500
|
+
return !(0, import_node_fs9.existsSync)(command);
|
|
1110
1501
|
}
|
|
1111
1502
|
function quoteForShell(arg) {
|
|
1112
1503
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -1137,7 +1528,7 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1137
1528
|
const [command, ...args] = entry;
|
|
1138
1529
|
const shell = needsShell(command);
|
|
1139
1530
|
const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
|
|
1140
|
-
const
|
|
1531
|
+
const spawnEntry = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
|
|
1141
1532
|
cwd,
|
|
1142
1533
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
1143
1534
|
shell,
|
|
@@ -1146,12 +1537,12 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1146
1537
|
});
|
|
1147
1538
|
let result;
|
|
1148
1539
|
if (process.platform === "win32" && shell) {
|
|
1149
|
-
result =
|
|
1540
|
+
result = spawnEntry(false);
|
|
1150
1541
|
} else {
|
|
1151
1542
|
try {
|
|
1152
|
-
result =
|
|
1543
|
+
result = spawnEntry(true);
|
|
1153
1544
|
} catch {
|
|
1154
|
-
result =
|
|
1545
|
+
result = spawnEntry(false);
|
|
1155
1546
|
}
|
|
1156
1547
|
}
|
|
1157
1548
|
if (result.error) return { ok: false, error: result.error.message };
|
|
@@ -1168,73 +1559,71 @@ function faceProduct(config) {
|
|
|
1168
1559
|
const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
|
|
1169
1560
|
return known[config.product] ?? config.product;
|
|
1170
1561
|
}
|
|
1171
|
-
function
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1562
|
+
async function installOrUpdate(config, dir, options, print, update) {
|
|
1563
|
+
const product = faceProduct(config);
|
|
1564
|
+
const installer = createInstallerRun({
|
|
1565
|
+
product,
|
|
1566
|
+
gate: config.host,
|
|
1567
|
+
doctor: identityFor(product).doctor,
|
|
1568
|
+
surfaces: [{ id: config.product, kind: "payload" }]
|
|
1569
|
+
}, {
|
|
1570
|
+
operation: update ? "update" : "install",
|
|
1571
|
+
tty: options.tty,
|
|
1572
|
+
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
1573
|
+
write: (text, channel) => {
|
|
1574
|
+
if (options.print) print(text.replace(/\n$/, ""));
|
|
1575
|
+
else (channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1579
|
+
let version = readState(dir)?.version ?? "unknown";
|
|
1580
|
+
installer.start();
|
|
1174
1581
|
try {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1582
|
+
let started = Date.now();
|
|
1583
|
+
installer.phase("sign-in", { state: "running" });
|
|
1584
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1585
|
+
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1586
|
+
installer.phase("resolve", { state: "running" });
|
|
1587
|
+
started = Date.now();
|
|
1588
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1589
|
+
version = manifest.version;
|
|
1590
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1591
|
+
const current = readState(dir);
|
|
1592
|
+
const unchanged = update && current?.version === version && (0, import_node_fs9.existsSync)(payloadDir(dir));
|
|
1593
|
+
if (!unchanged) {
|
|
1594
|
+
installer.phase("download", { state: "running" });
|
|
1595
|
+
started = Date.now();
|
|
1596
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1597
|
+
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1598
|
+
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1599
|
+
}
|
|
1600
|
+
installer.surface({
|
|
1601
|
+
id: config.product,
|
|
1602
|
+
from: current?.version,
|
|
1603
|
+
to: version,
|
|
1604
|
+
state: unchanged ? "current" : "updated"
|
|
1179
1605
|
});
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if (!face) {
|
|
1195
|
-
print(headline);
|
|
1196
|
-
for (const line of lines) print(line.trim());
|
|
1197
|
-
return;
|
|
1606
|
+
return await finishLastMile(config, dir, options, installer, version, unchanged);
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
const code = error instanceof NeedsLoginError ? 3 : 1;
|
|
1609
|
+
installer.finish({
|
|
1610
|
+
version,
|
|
1611
|
+
total: 1,
|
|
1612
|
+
updated: 0,
|
|
1613
|
+
failed: 1,
|
|
1614
|
+
detail: error.message,
|
|
1615
|
+
retry: `${config.binName} ${update ? "update" : "install"}`
|
|
1616
|
+
});
|
|
1617
|
+
return code;
|
|
1618
|
+
} finally {
|
|
1619
|
+
installer.stop();
|
|
1198
1620
|
}
|
|
1199
|
-
const glyph = ready ? "\u2714" : "\u2716";
|
|
1200
|
-
for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
|
|
1201
|
-
print(face.signOff());
|
|
1202
1621
|
}
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1622
|
+
function doInstall(config, dir, options, print) {
|
|
1623
|
+
return installOrUpdate(config, dir, options, print, false);
|
|
1205
1624
|
}
|
|
1206
|
-
function
|
|
1207
|
-
|
|
1208
|
-
printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
async function doInstall(config, dir, options, print) {
|
|
1212
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1213
|
-
const face = faceFor(config, options);
|
|
1214
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1215
|
-
const started = Date.now();
|
|
1216
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1217
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1218
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1219
|
-
printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
|
|
1220
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1221
|
-
}
|
|
1222
|
-
async function doUpdate(config, dir, options, print) {
|
|
1223
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1224
|
-
const face = faceFor(config, options);
|
|
1225
|
-
if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
|
|
1226
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1227
|
-
const started = Date.now();
|
|
1228
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1229
|
-
const current = readState(dir);
|
|
1230
|
-
if (current && current.version === manifest.version && (0, import_node_fs5.existsSync)(payloadDir(dir))) {
|
|
1231
|
-
printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
|
|
1232
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1233
|
-
}
|
|
1234
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1235
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1236
|
-
printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
|
|
1237
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1625
|
+
function doUpdate(config, dir, options, print) {
|
|
1626
|
+
return installOrUpdate(config, dir, options, print, true);
|
|
1238
1627
|
}
|
|
1239
1628
|
function payloadEnv(dir) {
|
|
1240
1629
|
const stored = readTokens(dir);
|
|
@@ -1247,63 +1636,136 @@ function payloadFileAsCommand(entry, payload) {
|
|
|
1247
1636
|
if (!first || first === "$self") return null;
|
|
1248
1637
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
1249
1638
|
const candidate = (0, import_node_path4.join)(payload, first);
|
|
1250
|
-
if (!(0,
|
|
1639
|
+
if (!(0, import_node_fs9.existsSync)(candidate)) return null;
|
|
1251
1640
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
1252
1641
|
if (process.platform === "win32") return null;
|
|
1253
1642
|
try {
|
|
1254
|
-
return ((0,
|
|
1643
|
+
return ((0, import_node_fs9.statSync)(candidate).mode & 73) === 0 ? first : null;
|
|
1255
1644
|
} catch {
|
|
1256
1645
|
return null;
|
|
1257
1646
|
}
|
|
1258
1647
|
}
|
|
1259
|
-
function finishLastMile(config, dir, options,
|
|
1648
|
+
async function finishLastMile(config, dir, options, installer, version, unchanged) {
|
|
1260
1649
|
const entry = readPayloadEntry(dir);
|
|
1261
1650
|
const payload = payloadDir(dir);
|
|
1262
1651
|
if (!entry) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1652
|
+
installer.finish({
|
|
1653
|
+
version,
|
|
1654
|
+
total: 1,
|
|
1655
|
+
updated: unchanged ? 0 : 1,
|
|
1656
|
+
failed: 0,
|
|
1657
|
+
installed: true,
|
|
1658
|
+
detail: `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
|
|
1659
|
+
});
|
|
1267
1660
|
return 0;
|
|
1268
1661
|
}
|
|
1269
1662
|
const command = resolveEntry(entry);
|
|
1270
1663
|
const dataFile = payloadFileAsCommand(entry, payload);
|
|
1271
1664
|
if (dataFile) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1665
|
+
installer.finish({
|
|
1666
|
+
version,
|
|
1667
|
+
total: 1,
|
|
1668
|
+
updated: 0,
|
|
1669
|
+
failed: 1,
|
|
1670
|
+
detail: `The payload names ${dataFile} as its command, but that is a file, not a program. This payload was built wrong: its entry must be a command.`
|
|
1671
|
+
});
|
|
1278
1672
|
return 2;
|
|
1279
1673
|
}
|
|
1674
|
+
installer.phase("activate", { state: "running" });
|
|
1280
1675
|
const started = Date.now();
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1676
|
+
const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1677
|
+
const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
|
|
1678
|
+
for (const progress of result.progress ?? []) installer.milestone(progress);
|
|
1679
|
+
const succeeded = result.ok && (result.code === void 0 || result.code === 0);
|
|
1680
|
+
const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
|
|
1681
|
+
installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
|
|
1682
|
+
installer.finish({
|
|
1683
|
+
...outcome ?? {
|
|
1684
|
+
version,
|
|
1685
|
+
total: 1,
|
|
1686
|
+
updated: succeeded && !unchanged ? 1 : 0,
|
|
1687
|
+
failed: succeeded ? 0 : 1,
|
|
1688
|
+
installed: true
|
|
1689
|
+
},
|
|
1690
|
+
...!succeeded ? {
|
|
1691
|
+
operationFailed: true,
|
|
1692
|
+
detail: result.code === void 0 ? `Arming this machine could not start: ${result.error ?? command[0]}` : `Arming this machine did not finish (exit code ${result.code})${result.error?.startsWith("installer outcome:") ? `: ${result.error}` : ""}`,
|
|
1693
|
+
retry: `(cd ${payload} && ${command.join(" ")})`
|
|
1694
|
+
} : {}
|
|
1695
|
+
});
|
|
1696
|
+
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1697
|
+
}
|
|
1698
|
+
async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
1699
|
+
const [command, ...args] = entry;
|
|
1700
|
+
const shell = needsShell(command);
|
|
1701
|
+
const progress = !(process.platform === "win32" && shell);
|
|
1702
|
+
const outcomeDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
|
|
1703
|
+
const outcomeFile = (0, import_node_path4.join)(outcomeDir, "outcome.json");
|
|
1704
|
+
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
1705
|
+
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
1706
|
+
delete childEnv.MM_PROGRESS_FD;
|
|
1707
|
+
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
1708
|
+
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
1709
|
+
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
1710
|
+
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
1711
|
+
try {
|
|
1712
|
+
const result = await new Promise((resolve2) => {
|
|
1713
|
+
const child = (0, import_node_child_process3.spawn)(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
|
|
1714
|
+
cwd,
|
|
1715
|
+
shell,
|
|
1716
|
+
windowsHide: true,
|
|
1717
|
+
env: childEnv,
|
|
1718
|
+
stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1719
|
+
});
|
|
1720
|
+
for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
|
|
1721
|
+
let partial = "";
|
|
1722
|
+
stream?.setEncoding("utf8").on("data", (text) => {
|
|
1723
|
+
const safe = redact(partial + text);
|
|
1724
|
+
let held = 0;
|
|
1725
|
+
for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
|
|
1726
|
+
if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
|
|
1727
|
+
}
|
|
1728
|
+
partial = held ? safe.slice(-held) : "";
|
|
1729
|
+
const visible = held ? safe.slice(0, -held) : safe;
|
|
1730
|
+
if (visible) installer.relay(visible, channel2, false);
|
|
1731
|
+
}).on("end", () => {
|
|
1732
|
+
if (partial) installer.relay("[redacted]", channel2, false);
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
let pending = "";
|
|
1736
|
+
const channel = child.stdio[3];
|
|
1737
|
+
if (channel && "setEncoding" in channel) {
|
|
1738
|
+
channel.setEncoding("utf8");
|
|
1739
|
+
channel.on("data", (text) => {
|
|
1740
|
+
pending += text;
|
|
1741
|
+
const end = pending.lastIndexOf("\n");
|
|
1742
|
+
if (end >= 0) {
|
|
1743
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
1744
|
+
pending = pending.slice(end + 1);
|
|
1745
|
+
}
|
|
1746
|
+
if (pending.length > 65536) pending = "";
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
child.on("error", (error) => resolve2({ ok: false, error: error.message }));
|
|
1750
|
+
child.on("close", (code) => resolve2({
|
|
1751
|
+
ok: code === 0,
|
|
1752
|
+
...code !== null ? { code } : {},
|
|
1753
|
+
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
1754
|
+
}));
|
|
1755
|
+
});
|
|
1756
|
+
try {
|
|
1757
|
+
const outcome = readInstallerOutcome(outcomeFile);
|
|
1758
|
+
return { ...result, ...outcome ? { outcome } : {} };
|
|
1759
|
+
} catch {
|
|
1760
|
+
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
1761
|
+
}
|
|
1762
|
+
} finally {
|
|
1763
|
+
if ((0, import_node_fs9.existsSync)(outcomeFile)) (0, import_node_fs9.unlinkSync)(outcomeFile);
|
|
1764
|
+
(0, import_node_fs9.rmdirSync)(outcomeDir);
|
|
1290
1765
|
}
|
|
1291
|
-
const code = result.code ?? 1;
|
|
1292
|
-
printStep(
|
|
1293
|
-
face,
|
|
1294
|
-
print,
|
|
1295
|
-
result.code === void 0 ? `Arming this machine could not start: ${result.error ?? `could not run ${command[0]}`}` : `Arming this machine did not finish (exit code ${code})`,
|
|
1296
|
-
null,
|
|
1297
|
-
"fail"
|
|
1298
|
-
);
|
|
1299
|
-
printReceipt(face, config, print, false, [
|
|
1300
|
-
`Downloaded ${version} into ${dir}`,
|
|
1301
|
-
`Finish it with: (cd ${payload} && ${command.join(" ")})`
|
|
1302
|
-
]);
|
|
1303
|
-
return code;
|
|
1304
1766
|
}
|
|
1305
1767
|
function doForward(config, dir, options, command, argv, print) {
|
|
1306
|
-
if (command && (0,
|
|
1768
|
+
if (command && (0, import_node_fs9.existsSync)(command)) {
|
|
1307
1769
|
print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
|
|
1308
1770
|
return 2;
|
|
1309
1771
|
}
|
|
@@ -1373,7 +1835,7 @@ function doDoctor(config, dir, options, print) {
|
|
|
1373
1835
|
function doLauncherDoctor(config, dir, options, print) {
|
|
1374
1836
|
const tokens = readTokens(dir);
|
|
1375
1837
|
const state = readState(dir);
|
|
1376
|
-
const payloadPresent = (0,
|
|
1838
|
+
const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
|
|
1377
1839
|
print(`product: ${config.product}`);
|
|
1378
1840
|
print(`host: ${config.host}`);
|
|
1379
1841
|
print(`login: ${config.loginKind}`);
|
|
@@ -1424,5 +1886,6 @@ if (invokedAsMain) {
|
|
|
1424
1886
|
readPayloadVerbs,
|
|
1425
1887
|
resolveEntry,
|
|
1426
1888
|
run,
|
|
1427
|
-
runFile
|
|
1889
|
+
runFile,
|
|
1890
|
+
runInstallEntry
|
|
1428
1891
|
});
|