@mutmutco/installer-launcher 0.1.8 → 0.1.11
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 +616 -141
- package/dist/launcher.sea.cjs +642 -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,377 @@ 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", "logPath"];
|
|
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, "rollback", "logState"];
|
|
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
|
+
if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
|
|
360
|
+
if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
|
|
361
|
+
if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
|
|
362
|
+
return { ...facts };
|
|
363
|
+
}
|
|
364
|
+
function writeInstallerOutcome(path, value) {
|
|
365
|
+
(0, import_node_fs3.writeFileSync)(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
366
|
+
}
|
|
367
|
+
function readInstallerOutcome(path) {
|
|
368
|
+
let text;
|
|
369
|
+
try {
|
|
370
|
+
text = (0, import_node_fs3.readFileSync)(path, "utf8");
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (error.code === "ENOENT") return void 0;
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
if (text.length > 16384) throw new Error("installer outcome: child result is too large");
|
|
376
|
+
try {
|
|
377
|
+
return validateInstallerOutcome(JSON.parse(text));
|
|
378
|
+
} catch {
|
|
379
|
+
throw new Error("installer outcome: invalid child result");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ../face/src/run.ts
|
|
384
|
+
function validateInstallerProduct(value) {
|
|
385
|
+
const fail = (field2) => {
|
|
386
|
+
throw new Error(`installer product: invalid ${field2}`);
|
|
387
|
+
};
|
|
388
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
|
|
389
|
+
const input = value;
|
|
390
|
+
const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
|
|
391
|
+
const key = text(input.product, "product");
|
|
392
|
+
const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
|
|
393
|
+
const identity = identityFor(product);
|
|
394
|
+
const gate = text(input.gate, "gate");
|
|
395
|
+
let gateUrl;
|
|
396
|
+
try {
|
|
397
|
+
gateUrl = new URL(gate);
|
|
398
|
+
} catch {
|
|
399
|
+
return fail("gate");
|
|
400
|
+
}
|
|
401
|
+
if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
|
|
402
|
+
const doctor = text(input.doctor, "doctor");
|
|
403
|
+
if (doctor !== identity.doctor) return fail("doctor");
|
|
404
|
+
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
405
|
+
const ids = /* @__PURE__ */ new Set();
|
|
406
|
+
const surfaces = input.surfaces.map((raw) => {
|
|
407
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
|
|
408
|
+
const source = raw;
|
|
409
|
+
const id = text(source.id, "surface.id");
|
|
410
|
+
if (ids.has(id)) return fail("duplicate surface.id");
|
|
411
|
+
ids.add(id);
|
|
412
|
+
const surface = { id };
|
|
413
|
+
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
414
|
+
if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
|
|
415
|
+
}
|
|
416
|
+
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
417
|
+
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
418
|
+
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
419
|
+
return surface;
|
|
420
|
+
});
|
|
421
|
+
return { product, gate, doctor, surfaces };
|
|
422
|
+
}
|
|
423
|
+
var PHASES = {
|
|
424
|
+
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
425
|
+
resolve: ["Resolving the release", "Resolved the release"],
|
|
426
|
+
download: ["Downloading the payload", "Downloaded the payload"],
|
|
427
|
+
"sign-in": ["Signing in", "Signed in"],
|
|
428
|
+
check: ["Checking surfaces", "Checked surfaces"],
|
|
429
|
+
arm: ["Scheduling updates", "Armed hourly updates"],
|
|
430
|
+
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
431
|
+
verify: ["Verifying the payload", "Verified the payload"],
|
|
432
|
+
install: ["Installing the product", "Installed the product"],
|
|
433
|
+
activate: ["Activating surfaces", "Activated surfaces"],
|
|
434
|
+
doctor: ["Checking health", "Checked health"],
|
|
435
|
+
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
436
|
+
};
|
|
437
|
+
function createInstallerRun(value, options = {}) {
|
|
438
|
+
const declaration = validateInstallerProduct(value);
|
|
439
|
+
const env = options.env ?? process.env;
|
|
440
|
+
const tty = options.tty ?? Boolean(process.stdout.isTTY);
|
|
441
|
+
const face = createFace({
|
|
442
|
+
operation: options.operation,
|
|
443
|
+
product: declaration.product,
|
|
444
|
+
columns: options.columns,
|
|
445
|
+
env,
|
|
446
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
447
|
+
});
|
|
448
|
+
const errors = [];
|
|
449
|
+
const write = options.write ? (text, channel) => {
|
|
450
|
+
try {
|
|
451
|
+
options.write(text, channel);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
}
|
|
455
|
+
} : (text, channel) => {
|
|
456
|
+
(channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
457
|
+
};
|
|
458
|
+
const emit = (text, channel = "stdout", recorded = text) => {
|
|
459
|
+
if (!text) return;
|
|
460
|
+
write(text, channel);
|
|
461
|
+
if (env.MM_FACE_TRANSCRIPT) {
|
|
462
|
+
(0, import_node_fs4.appendFileSync)(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
463
|
+
`, "utf8");
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const lines = (rows, channel = "stdout") => {
|
|
467
|
+
for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
|
|
468
|
+
`, channel);
|
|
469
|
+
};
|
|
470
|
+
const spinner = createSpinner(face, {
|
|
471
|
+
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
472
|
+
...options.write ? { stream: { write: (text) => {
|
|
473
|
+
emit(String(text), "spinner");
|
|
474
|
+
return true;
|
|
475
|
+
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
476
|
+
});
|
|
477
|
+
let started = false;
|
|
478
|
+
let finished = false;
|
|
479
|
+
const start = () => {
|
|
480
|
+
if (started || finished) return;
|
|
481
|
+
started = true;
|
|
482
|
+
const welcome = face.welcome();
|
|
483
|
+
if (tty) lines(welcome);
|
|
484
|
+
else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
|
|
485
|
+
};
|
|
486
|
+
const durable = (title, measure, kind) => {
|
|
487
|
+
spinner.stop();
|
|
488
|
+
const rendered = face.step(title, measure, kind);
|
|
489
|
+
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
490
|
+
};
|
|
491
|
+
const run2 = {
|
|
492
|
+
get errors() {
|
|
493
|
+
return [...errors];
|
|
494
|
+
},
|
|
495
|
+
start,
|
|
496
|
+
phase(id, facts = {}) {
|
|
497
|
+
if (finished) throw new Error("installer run already finished");
|
|
498
|
+
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
499
|
+
start();
|
|
500
|
+
const state = facts.state ?? "ok";
|
|
501
|
+
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
502
|
+
if (state === "running") {
|
|
503
|
+
spinner.start(title, facts.measure ?? null);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
507
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
508
|
+
},
|
|
509
|
+
surface(facts) {
|
|
510
|
+
if (finished) throw new Error("installer run already finished");
|
|
511
|
+
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
512
|
+
if (!surface) throw new Error("installer run: undeclared surface");
|
|
513
|
+
start();
|
|
514
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
515
|
+
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
516
|
+
if (!status) throw new Error("installer run: unknown surface state");
|
|
517
|
+
const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
|
|
518
|
+
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
519
|
+
durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
520
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
521
|
+
},
|
|
522
|
+
milestone({ step, state, ms }) {
|
|
523
|
+
start();
|
|
524
|
+
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
525
|
+
},
|
|
526
|
+
signIn({ url, code }) {
|
|
527
|
+
start();
|
|
528
|
+
spinner.stop();
|
|
529
|
+
for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
|
|
530
|
+
const rendered = `${tty ? face.relay(text) : text}
|
|
531
|
+
`;
|
|
532
|
+
emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
536
|
+
relay(text, channel = "stdout", record = true) {
|
|
537
|
+
spinner.stop();
|
|
538
|
+
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
539
|
+
for (const row of rendered.split("\n")) if (row) {
|
|
540
|
+
emit(`${row}
|
|
541
|
+
`, channel, record ? `${row}
|
|
542
|
+
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
543
|
+
`);
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
finish(facts) {
|
|
547
|
+
if (finished) return;
|
|
548
|
+
validateInstallerOutcome(facts);
|
|
549
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
550
|
+
start();
|
|
551
|
+
spinner.stop();
|
|
552
|
+
finished = true;
|
|
553
|
+
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}.`;
|
|
554
|
+
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
555
|
+
const body = [
|
|
556
|
+
`${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."}`,
|
|
557
|
+
changed,
|
|
558
|
+
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
559
|
+
...facts.detail ? [facts.detail] : [],
|
|
560
|
+
...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
|
|
561
|
+
...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
|
|
562
|
+
facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
563
|
+
];
|
|
564
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
565
|
+
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
566
|
+
}
|
|
567
|
+
if (tty) lines([face.signOff()]);
|
|
568
|
+
},
|
|
569
|
+
stop() {
|
|
570
|
+
spinner.stop();
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
return run2;
|
|
574
|
+
}
|
|
575
|
+
|
|
175
576
|
// src/autoupdate.ts
|
|
176
577
|
var import_node_child_process = require("node:child_process");
|
|
177
|
-
var
|
|
578
|
+
var import_node_fs5 = require("node:fs");
|
|
178
579
|
var import_node_os = require("node:os");
|
|
179
580
|
var import_node_path = require("node:path");
|
|
180
581
|
function schedulePlatform(override) {
|
|
@@ -221,9 +622,9 @@ function enableSchedule(config, command, options = {}) {
|
|
|
221
622
|
if (platform === "darwin") {
|
|
222
623
|
const label2 = scheduleLabel(config);
|
|
223
624
|
const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
|
|
224
|
-
(0,
|
|
625
|
+
(0, import_node_fs5.mkdirSync)(dir2, { recursive: true });
|
|
225
626
|
const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
|
|
226
|
-
(0,
|
|
627
|
+
(0, import_node_fs5.writeFileSync)(plist, darwinPlist(label2, command));
|
|
227
628
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
228
629
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
229
630
|
if (result2.code !== 0) {
|
|
@@ -233,9 +634,9 @@ function enableSchedule(config, command, options = {}) {
|
|
|
233
634
|
}
|
|
234
635
|
const label = scheduleLabel(config);
|
|
235
636
|
const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
|
|
236
|
-
(0,
|
|
237
|
-
(0,
|
|
238
|
-
(0,
|
|
637
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
638
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
|
|
639
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
|
|
239
640
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
240
641
|
if (reload.code !== 0) {
|
|
241
642
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -260,14 +661,14 @@ function disableSchedule(config, options = {}) {
|
|
|
260
661
|
if (platform === "darwin") {
|
|
261
662
|
const label2 = scheduleLabel(config);
|
|
262
663
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
263
|
-
(0,
|
|
664
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
|
|
264
665
|
return;
|
|
265
666
|
}
|
|
266
667
|
const label = scheduleLabel(config);
|
|
267
668
|
const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
|
|
268
669
|
exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
|
|
269
|
-
(0,
|
|
270
|
-
(0,
|
|
670
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
|
|
671
|
+
(0, import_node_fs5.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
|
|
271
672
|
}
|
|
272
673
|
function querySchedule(config, options = {}) {
|
|
273
674
|
const platform = schedulePlatform(options.platform);
|
|
@@ -286,11 +687,11 @@ function querySchedule(config, options = {}) {
|
|
|
286
687
|
}
|
|
287
688
|
if (platform === "darwin") {
|
|
288
689
|
const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
|
|
289
|
-
if (!(0,
|
|
690
|
+
if (!(0, import_node_fs5.existsSync)(plist)) return { supported: true, enabled: false };
|
|
290
691
|
return { supported: true, enabled: true, cadence: "hourly" };
|
|
291
692
|
}
|
|
292
693
|
const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
|
|
293
|
-
if (!(0,
|
|
694
|
+
if (!(0, import_node_fs5.existsSync)(timer)) return { supported: true, enabled: false };
|
|
294
695
|
const state = { supported: true, enabled: true, cadence: "hourly" };
|
|
295
696
|
const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
|
|
296
697
|
const stamp = (shown.stdout ?? "").trim();
|
|
@@ -348,7 +749,7 @@ function firstLine(text) {
|
|
|
348
749
|
}
|
|
349
750
|
|
|
350
751
|
// src/config.ts
|
|
351
|
-
var
|
|
752
|
+
var import_node_fs6 = require("node:fs");
|
|
352
753
|
var import_node_sea = require("node:sea");
|
|
353
754
|
|
|
354
755
|
// src/module-url.ts
|
|
@@ -371,10 +772,10 @@ function loadProductConfig(options = {}) {
|
|
|
371
772
|
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
372
773
|
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
373
774
|
if (explicit) {
|
|
374
|
-
return parseProductConfig((0,
|
|
775
|
+
return parseProductConfig((0, import_node_fs6.readFileSync)(explicit, "utf8"));
|
|
375
776
|
}
|
|
376
777
|
try {
|
|
377
|
-
return parseProductConfig((0,
|
|
778
|
+
return parseProductConfig((0, import_node_fs6.readFileSync)(devFallback, "utf8"));
|
|
378
779
|
} catch {
|
|
379
780
|
}
|
|
380
781
|
try {
|
|
@@ -493,7 +894,8 @@ async function loginGithub(config, options = {}) {
|
|
|
493
894
|
const sleep = options.sleep ?? realSleep;
|
|
494
895
|
const issued = await requestDeviceCode(config, fetchImpl);
|
|
495
896
|
const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
|
|
496
|
-
|
|
897
|
+
if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
|
|
898
|
+
else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
|
|
497
899
|
open(openUrl);
|
|
498
900
|
const deadline = now() + issued.expires_in * 1e3;
|
|
499
901
|
let intervalMs = Math.max(1, issued.interval) * 1e3;
|
|
@@ -663,7 +1065,7 @@ function page(title, body) {
|
|
|
663
1065
|
|
|
664
1066
|
// src/payload.ts
|
|
665
1067
|
var import_node_crypto2 = require("node:crypto");
|
|
666
|
-
var
|
|
1068
|
+
var import_node_fs7 = require("node:fs");
|
|
667
1069
|
var import_node_os2 = require("node:os");
|
|
668
1070
|
var import_node_path2 = require("node:path");
|
|
669
1071
|
|
|
@@ -806,7 +1208,7 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
|
|
|
806
1208
|
async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
|
|
807
1209
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
808
1210
|
const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
|
|
809
|
-
(0,
|
|
1211
|
+
(0, import_node_fs7.mkdirSync)(staging, { recursive: true });
|
|
810
1212
|
try {
|
|
811
1213
|
for (const entry of manifest.files) {
|
|
812
1214
|
const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
|
|
@@ -814,22 +1216,22 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
814
1216
|
throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
|
|
815
1217
|
}
|
|
816
1218
|
const dest = (0, import_node_path2.join)(staging, entry.path);
|
|
817
|
-
(0,
|
|
818
|
-
(0,
|
|
1219
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
|
|
1220
|
+
(0, import_node_fs7.writeFileSync)(dest, bytes);
|
|
819
1221
|
}
|
|
820
1222
|
const target = (0, import_node_path2.join)(dir, "payload");
|
|
821
|
-
(0,
|
|
822
|
-
(0,
|
|
823
|
-
(0,
|
|
1223
|
+
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
1224
|
+
(0, import_node_fs7.rmSync)(target, { force: true, recursive: true });
|
|
1225
|
+
(0, import_node_fs7.renameSync)(staging, target);
|
|
824
1226
|
} catch (error) {
|
|
825
|
-
(0,
|
|
1227
|
+
(0, import_node_fs7.rmSync)(staging, { force: true, recursive: true });
|
|
826
1228
|
throw error;
|
|
827
1229
|
}
|
|
828
1230
|
return manifest.version;
|
|
829
1231
|
}
|
|
830
1232
|
|
|
831
1233
|
// src/store.ts
|
|
832
|
-
var
|
|
1234
|
+
var import_node_fs8 = require("node:fs");
|
|
833
1235
|
var import_node_os3 = require("node:os");
|
|
834
1236
|
var import_node_path3 = require("node:path");
|
|
835
1237
|
function defaultProductDir(product) {
|
|
@@ -854,7 +1256,7 @@ function payloadDir(dir) {
|
|
|
854
1256
|
}
|
|
855
1257
|
function readTokens(dir) {
|
|
856
1258
|
try {
|
|
857
|
-
const data = JSON.parse((0,
|
|
1259
|
+
const data = JSON.parse((0, import_node_fs8.readFileSync)(tokensPath(dir), "utf8"));
|
|
858
1260
|
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
859
1261
|
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
860
1262
|
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
@@ -866,21 +1268,21 @@ function readTokens(dir) {
|
|
|
866
1268
|
}
|
|
867
1269
|
}
|
|
868
1270
|
function writeTokens(dir, tokens) {
|
|
869
|
-
(0,
|
|
1271
|
+
(0, import_node_fs8.mkdirSync)(dir, { recursive: true });
|
|
870
1272
|
try {
|
|
871
|
-
(0,
|
|
1273
|
+
(0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
872
1274
|
`, { mode: 384 });
|
|
873
1275
|
} catch {
|
|
874
|
-
(0,
|
|
1276
|
+
(0, import_node_fs8.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
875
1277
|
`);
|
|
876
1278
|
}
|
|
877
1279
|
}
|
|
878
1280
|
function clearTokens(dir) {
|
|
879
|
-
(0,
|
|
1281
|
+
(0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
|
|
880
1282
|
}
|
|
881
1283
|
function readState(dir) {
|
|
882
1284
|
try {
|
|
883
|
-
const data = JSON.parse((0,
|
|
1285
|
+
const data = JSON.parse((0, import_node_fs8.readFileSync)(statePath(dir), "utf8"));
|
|
884
1286
|
if (typeof data.version !== "string" || !data.version) return null;
|
|
885
1287
|
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
886
1288
|
} catch {
|
|
@@ -888,18 +1290,18 @@ function readState(dir) {
|
|
|
888
1290
|
}
|
|
889
1291
|
}
|
|
890
1292
|
function writeState(dir, state) {
|
|
891
|
-
(0,
|
|
892
|
-
(0,
|
|
1293
|
+
(0, import_node_fs8.mkdirSync)(dir, { recursive: true });
|
|
1294
|
+
(0, import_node_fs8.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
893
1295
|
`);
|
|
894
1296
|
}
|
|
895
1297
|
function wipeProductDir(dir) {
|
|
896
|
-
(0,
|
|
897
|
-
(0,
|
|
898
|
-
(0,
|
|
1298
|
+
(0, import_node_fs8.rmSync)(tokensPath(dir), { force: true });
|
|
1299
|
+
(0, import_node_fs8.rmSync)(statePath(dir), { force: true });
|
|
1300
|
+
(0, import_node_fs8.rmSync)(payloadDir(dir), { force: true, recursive: true });
|
|
899
1301
|
}
|
|
900
1302
|
|
|
901
1303
|
// src/index.ts
|
|
902
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1304
|
+
var LAUNCHER_VERSION = true ? "0.1.11" : readVersionFromPackage();
|
|
903
1305
|
function defaultPrint(message) {
|
|
904
1306
|
process.stdout.write(`${message}
|
|
905
1307
|
`);
|
|
@@ -979,7 +1381,7 @@ async function runFile(file, args, printErr) {
|
|
|
979
1381
|
return 1;
|
|
980
1382
|
}
|
|
981
1383
|
const abs = (0, import_node_path4.resolve)(process.cwd(), file);
|
|
982
|
-
if (!(0,
|
|
1384
|
+
if (!(0, import_node_fs9.existsSync)(abs)) {
|
|
983
1385
|
printErr(`cannot run ${file}: no such file.`);
|
|
984
1386
|
return 1;
|
|
985
1387
|
}
|
|
@@ -1035,11 +1437,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1035
1437
|
});
|
|
1036
1438
|
return refreshed.accessToken;
|
|
1037
1439
|
}
|
|
1038
|
-
async function doLogin(config, dir, options, print) {
|
|
1440
|
+
async function doLogin(config, dir, options, print, installer) {
|
|
1039
1441
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1040
1442
|
const open = options.open ?? openBrowser;
|
|
1041
1443
|
if (config.loginKind === "github") {
|
|
1042
|
-
const tokens = await loginGithub(config, {
|
|
1444
|
+
const tokens = await loginGithub(config, {
|
|
1445
|
+
fetchImpl,
|
|
1446
|
+
open,
|
|
1447
|
+
print,
|
|
1448
|
+
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
1449
|
+
});
|
|
1043
1450
|
writeTokens(dir, {
|
|
1044
1451
|
accessToken: tokens.accessToken,
|
|
1045
1452
|
refreshToken: tokens.refreshToken,
|
|
@@ -1054,20 +1461,20 @@ async function doLogin(config, dir, options, print) {
|
|
|
1054
1461
|
clientId: tokens.clientId
|
|
1055
1462
|
});
|
|
1056
1463
|
}
|
|
1057
|
-
print(`signed in to ${config.product}.`);
|
|
1464
|
+
if (!installer) print(`signed in to ${config.product}.`);
|
|
1058
1465
|
}
|
|
1059
|
-
async function fetchAccessTokenOrLogin(config, dir, options, print) {
|
|
1466
|
+
async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
|
|
1060
1467
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1061
1468
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1062
1469
|
if (fresh) return fresh;
|
|
1063
|
-
await doLogin(config, dir, options, print);
|
|
1470
|
+
await doLogin(config, dir, options, print, installer);
|
|
1064
1471
|
const after = readTokens(dir);
|
|
1065
1472
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
1066
1473
|
return after.accessToken;
|
|
1067
1474
|
}
|
|
1068
1475
|
function readPayloadArgv(dir, key) {
|
|
1069
1476
|
try {
|
|
1070
|
-
const parsed = JSON.parse((0,
|
|
1477
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
1071
1478
|
const entry = parsed[key];
|
|
1072
1479
|
if (typeof entry === "string") {
|
|
1073
1480
|
const parts = entry.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1089,7 +1496,7 @@ function readPayloadRun(dir) {
|
|
|
1089
1496
|
}
|
|
1090
1497
|
function readPayloadVerbs(dir) {
|
|
1091
1498
|
try {
|
|
1092
|
-
const parsed = JSON.parse((0,
|
|
1499
|
+
const parsed = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
|
|
1093
1500
|
const verbs = parsed.verbs;
|
|
1094
1501
|
if (verbs === "*") return "*";
|
|
1095
1502
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -1106,7 +1513,7 @@ function resolveEntry(entry) {
|
|
|
1106
1513
|
function needsShell(command) {
|
|
1107
1514
|
if (process.platform !== "win32") return false;
|
|
1108
1515
|
if (/\.(cmd|bat)$/i.test(command)) return true;
|
|
1109
|
-
return !(0,
|
|
1516
|
+
return !(0, import_node_fs9.existsSync)(command);
|
|
1110
1517
|
}
|
|
1111
1518
|
function quoteForShell(arg) {
|
|
1112
1519
|
return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
@@ -1137,7 +1544,7 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1137
1544
|
const [command, ...args] = entry;
|
|
1138
1545
|
const shell = needsShell(command);
|
|
1139
1546
|
const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
|
|
1140
|
-
const
|
|
1547
|
+
const spawnEntry = (progress2) => (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
|
|
1141
1548
|
cwd,
|
|
1142
1549
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
1143
1550
|
shell,
|
|
@@ -1146,12 +1553,12 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1146
1553
|
});
|
|
1147
1554
|
let result;
|
|
1148
1555
|
if (process.platform === "win32" && shell) {
|
|
1149
|
-
result =
|
|
1556
|
+
result = spawnEntry(false);
|
|
1150
1557
|
} else {
|
|
1151
1558
|
try {
|
|
1152
|
-
result =
|
|
1559
|
+
result = spawnEntry(true);
|
|
1153
1560
|
} catch {
|
|
1154
|
-
result =
|
|
1561
|
+
result = spawnEntry(false);
|
|
1155
1562
|
}
|
|
1156
1563
|
}
|
|
1157
1564
|
if (result.error) return { ok: false, error: result.error.message };
|
|
@@ -1168,73 +1575,68 @@ function faceProduct(config) {
|
|
|
1168
1575
|
const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
|
|
1169
1576
|
return known[config.product] ?? config.product;
|
|
1170
1577
|
}
|
|
1171
|
-
function
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1578
|
+
async function installOrUpdate(config, dir, options, print, update) {
|
|
1579
|
+
const product = faceProduct(config);
|
|
1580
|
+
const installer = createInstallerRun({
|
|
1581
|
+
product,
|
|
1582
|
+
gate: config.host,
|
|
1583
|
+
doctor: identityFor(product).doctor,
|
|
1584
|
+
surfaces: [{ id: config.product, kind: "payload" }]
|
|
1585
|
+
}, {
|
|
1586
|
+
operation: update ? "update" : "install",
|
|
1587
|
+
tty: options.tty,
|
|
1588
|
+
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
1589
|
+
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
1590
|
+
});
|
|
1591
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1592
|
+
let version = readState(dir)?.version ?? "unknown";
|
|
1593
|
+
installer.start();
|
|
1174
1594
|
try {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1595
|
+
let started = Date.now();
|
|
1596
|
+
installer.phase("sign-in", { state: "running" });
|
|
1597
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1598
|
+
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1599
|
+
installer.phase("resolve", { state: "running" });
|
|
1600
|
+
started = Date.now();
|
|
1601
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1602
|
+
version = manifest.version;
|
|
1603
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1604
|
+
const current = readState(dir);
|
|
1605
|
+
const unchanged = update && current?.version === version && (0, import_node_fs9.existsSync)(payloadDir(dir));
|
|
1606
|
+
if (!unchanged) {
|
|
1607
|
+
installer.phase("download", { state: "running" });
|
|
1608
|
+
started = Date.now();
|
|
1609
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1610
|
+
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1611
|
+
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1612
|
+
}
|
|
1613
|
+
installer.surface({
|
|
1614
|
+
id: config.product,
|
|
1615
|
+
from: current?.version,
|
|
1616
|
+
to: version,
|
|
1617
|
+
state: unchanged ? "current" : "updated"
|
|
1179
1618
|
});
|
|
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;
|
|
1619
|
+
return await finishLastMile(config, dir, options, installer, version, unchanged);
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
const code = error instanceof NeedsLoginError ? 3 : 1;
|
|
1622
|
+
installer.finish({
|
|
1623
|
+
version,
|
|
1624
|
+
total: 1,
|
|
1625
|
+
updated: 0,
|
|
1626
|
+
failed: 1,
|
|
1627
|
+
detail: error.message,
|
|
1628
|
+
retry: `${config.binName} ${update ? "update" : "install"}`
|
|
1629
|
+
});
|
|
1630
|
+
return code;
|
|
1631
|
+
} finally {
|
|
1632
|
+
installer.stop();
|
|
1198
1633
|
}
|
|
1199
|
-
const glyph = ready ? "\u2714" : "\u2716";
|
|
1200
|
-
for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
|
|
1201
|
-
print(face.signOff());
|
|
1202
1634
|
}
|
|
1203
|
-
function
|
|
1204
|
-
|
|
1635
|
+
function doInstall(config, dir, options, print) {
|
|
1636
|
+
return installOrUpdate(config, dir, options, print, false);
|
|
1205
1637
|
}
|
|
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);
|
|
1638
|
+
function doUpdate(config, dir, options, print) {
|
|
1639
|
+
return installOrUpdate(config, dir, options, print, true);
|
|
1238
1640
|
}
|
|
1239
1641
|
function payloadEnv(dir) {
|
|
1240
1642
|
const stored = readTokens(dir);
|
|
@@ -1247,63 +1649,136 @@ function payloadFileAsCommand(entry, payload) {
|
|
|
1247
1649
|
if (!first || first === "$self") return null;
|
|
1248
1650
|
if (first.includes("/") || first.includes("\\")) return null;
|
|
1249
1651
|
const candidate = (0, import_node_path4.join)(payload, first);
|
|
1250
|
-
if (!(0,
|
|
1652
|
+
if (!(0, import_node_fs9.existsSync)(candidate)) return null;
|
|
1251
1653
|
if (NEVER_A_PROGRAM.test(first)) return first;
|
|
1252
1654
|
if (process.platform === "win32") return null;
|
|
1253
1655
|
try {
|
|
1254
|
-
return ((0,
|
|
1656
|
+
return ((0, import_node_fs9.statSync)(candidate).mode & 73) === 0 ? first : null;
|
|
1255
1657
|
} catch {
|
|
1256
1658
|
return null;
|
|
1257
1659
|
}
|
|
1258
1660
|
}
|
|
1259
|
-
function finishLastMile(config, dir, options,
|
|
1661
|
+
async function finishLastMile(config, dir, options, installer, version, unchanged) {
|
|
1260
1662
|
const entry = readPayloadEntry(dir);
|
|
1261
1663
|
const payload = payloadDir(dir);
|
|
1262
1664
|
if (!entry) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1665
|
+
installer.finish({
|
|
1666
|
+
version,
|
|
1667
|
+
total: 1,
|
|
1668
|
+
updated: unchanged ? 0 : 1,
|
|
1669
|
+
failed: 0,
|
|
1670
|
+
installed: true,
|
|
1671
|
+
detail: `next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
|
|
1672
|
+
});
|
|
1267
1673
|
return 0;
|
|
1268
1674
|
}
|
|
1269
1675
|
const command = resolveEntry(entry);
|
|
1270
1676
|
const dataFile = payloadFileAsCommand(entry, payload);
|
|
1271
1677
|
if (dataFile) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1678
|
+
installer.finish({
|
|
1679
|
+
version,
|
|
1680
|
+
total: 1,
|
|
1681
|
+
updated: 0,
|
|
1682
|
+
failed: 1,
|
|
1683
|
+
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.`
|
|
1684
|
+
});
|
|
1278
1685
|
return 2;
|
|
1279
1686
|
}
|
|
1687
|
+
installer.phase("activate", { state: "running" });
|
|
1280
1688
|
const started = Date.now();
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1689
|
+
const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1690
|
+
const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
|
|
1691
|
+
for (const progress of result.progress ?? []) installer.milestone(progress);
|
|
1692
|
+
const succeeded = result.ok && (result.code === void 0 || result.code === 0);
|
|
1693
|
+
const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
|
|
1694
|
+
installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
|
|
1695
|
+
installer.finish({
|
|
1696
|
+
...outcome ?? {
|
|
1697
|
+
version,
|
|
1698
|
+
total: 1,
|
|
1699
|
+
updated: succeeded && !unchanged ? 1 : 0,
|
|
1700
|
+
failed: succeeded ? 0 : 1,
|
|
1701
|
+
installed: true
|
|
1702
|
+
},
|
|
1703
|
+
...!succeeded ? {
|
|
1704
|
+
operationFailed: true,
|
|
1705
|
+
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}` : ""}`,
|
|
1706
|
+
retry: `(cd ${payload} && ${command.join(" ")})`
|
|
1707
|
+
} : {}
|
|
1708
|
+
});
|
|
1709
|
+
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1710
|
+
}
|
|
1711
|
+
async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
1712
|
+
const [command, ...args] = entry;
|
|
1713
|
+
const shell = needsShell(command);
|
|
1714
|
+
const progress = !(process.platform === "win32" && shell);
|
|
1715
|
+
const outcomeDir = (0, import_node_fs9.mkdtempSync)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "mm-installer-outcome-"));
|
|
1716
|
+
const outcomeFile = (0, import_node_path4.join)(outcomeDir, "outcome.json");
|
|
1717
|
+
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
1718
|
+
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
1719
|
+
delete childEnv.MM_PROGRESS_FD;
|
|
1720
|
+
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
1721
|
+
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
1722
|
+
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
1723
|
+
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
1724
|
+
try {
|
|
1725
|
+
const result = await new Promise((resolve2) => {
|
|
1726
|
+
const child = (0, import_node_child_process3.spawn)(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
|
|
1727
|
+
cwd,
|
|
1728
|
+
shell,
|
|
1729
|
+
windowsHide: true,
|
|
1730
|
+
env: childEnv,
|
|
1731
|
+
stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1732
|
+
});
|
|
1733
|
+
for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
|
|
1734
|
+
let partial = "";
|
|
1735
|
+
stream?.setEncoding("utf8").on("data", (text) => {
|
|
1736
|
+
const safe = redact(partial + text);
|
|
1737
|
+
let held = 0;
|
|
1738
|
+
for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
|
|
1739
|
+
if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
|
|
1740
|
+
}
|
|
1741
|
+
partial = held ? safe.slice(-held) : "";
|
|
1742
|
+
const visible = held ? safe.slice(0, -held) : safe;
|
|
1743
|
+
if (visible) installer.relay(visible, channel2, false);
|
|
1744
|
+
}).on("end", () => {
|
|
1745
|
+
if (partial) installer.relay("[redacted]", channel2, false);
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
let pending = "";
|
|
1749
|
+
const channel = child.stdio[3];
|
|
1750
|
+
if (channel && "setEncoding" in channel) {
|
|
1751
|
+
channel.setEncoding("utf8");
|
|
1752
|
+
channel.on("data", (text) => {
|
|
1753
|
+
pending += text;
|
|
1754
|
+
const end = pending.lastIndexOf("\n");
|
|
1755
|
+
if (end >= 0) {
|
|
1756
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
1757
|
+
pending = pending.slice(end + 1);
|
|
1758
|
+
}
|
|
1759
|
+
if (pending.length > 65536) pending = "";
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
child.on("error", (error) => resolve2({ ok: false, error: error.message }));
|
|
1763
|
+
child.on("close", (code) => resolve2({
|
|
1764
|
+
ok: code === 0,
|
|
1765
|
+
...code !== null ? { code } : {},
|
|
1766
|
+
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
1767
|
+
}));
|
|
1768
|
+
});
|
|
1769
|
+
try {
|
|
1770
|
+
const outcome = readInstallerOutcome(outcomeFile);
|
|
1771
|
+
return { ...result, ...outcome ? { outcome } : {} };
|
|
1772
|
+
} catch {
|
|
1773
|
+
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
1774
|
+
}
|
|
1775
|
+
} finally {
|
|
1776
|
+
if ((0, import_node_fs9.existsSync)(outcomeFile)) (0, import_node_fs9.unlinkSync)(outcomeFile);
|
|
1777
|
+
(0, import_node_fs9.rmdirSync)(outcomeDir);
|
|
1290
1778
|
}
|
|
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
1779
|
}
|
|
1305
1780
|
function doForward(config, dir, options, command, argv, print) {
|
|
1306
|
-
if (command && (0,
|
|
1781
|
+
if (command && (0, import_node_fs9.existsSync)(command)) {
|
|
1307
1782
|
print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
|
|
1308
1783
|
return 2;
|
|
1309
1784
|
}
|
|
@@ -1373,7 +1848,7 @@ function doDoctor(config, dir, options, print) {
|
|
|
1373
1848
|
function doLauncherDoctor(config, dir, options, print) {
|
|
1374
1849
|
const tokens = readTokens(dir);
|
|
1375
1850
|
const state = readState(dir);
|
|
1376
|
-
const payloadPresent = (0,
|
|
1851
|
+
const payloadPresent = (0, import_node_fs9.existsSync)(payloadDir(dir));
|
|
1377
1852
|
print(`product: ${config.product}`);
|
|
1378
1853
|
print(`host: ${config.host}`);
|
|
1379
1854
|
print(`login: ${config.loginKind}`);
|
|
@@ -1424,5 +1899,6 @@ if (invokedAsMain) {
|
|
|
1424
1899
|
readPayloadVerbs,
|
|
1425
1900
|
resolveEntry,
|
|
1426
1901
|
run,
|
|
1427
|
-
runFile
|
|
1902
|
+
runFile,
|
|
1903
|
+
runInstallEntry
|
|
1428
1904
|
});
|