@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.js
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5
|
-
import { existsSync as existsSync4, readFileSync as
|
|
4
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, statSync, mkdtempSync, unlinkSync, rmdirSync } from "node:fs";
|
|
6
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
6
7
|
import { dirname as dirname2, join as join4, resolve } from "node:path";
|
|
7
8
|
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// ../face/src/face.ts
|
|
11
|
+
import { writeSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// ../face/src/products.ts
|
|
10
14
|
var PRODUCTS = Object.freeze({
|
|
11
15
|
"mm-strategy": Object.freeze({
|
|
12
16
|
name: "MM Strategy",
|
|
17
|
+
installWarm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
|
|
13
18
|
accent: "38;2;249;115;22",
|
|
14
|
-
warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
|
|
19
|
+
warm: "Welcome. Let's set up MM Strategy \u2014 about a minute.",
|
|
20
|
+
doctor: "mm-strategy doctor"
|
|
15
21
|
}),
|
|
16
22
|
"mmi-hub": Object.freeze({
|
|
17
23
|
name: "mmi-hub",
|
|
24
|
+
installWarm: "Welcome. Setting up mmi-hub \u2014 about a minute.",
|
|
18
25
|
accent: "38;2;125;211;252",
|
|
19
|
-
warm: "Welcome back. Checking your surfaces\u2026"
|
|
26
|
+
warm: "Welcome back. Checking your surfaces\u2026",
|
|
27
|
+
doctor: "mmi doctor"
|
|
20
28
|
}),
|
|
21
29
|
"jerv-hub": Object.freeze({
|
|
22
30
|
name: "jerv-hub",
|
|
31
|
+
installWarm: "Welcome. Setting up jerv-hub \u2014 about a minute.",
|
|
23
32
|
accent: "38;2;248;113;113",
|
|
24
|
-
warm: "Welcome back. Checking your surfaces\u2026"
|
|
33
|
+
warm: "Welcome back. Checking your surfaces\u2026",
|
|
34
|
+
doctor: "jerv doctor"
|
|
25
35
|
}),
|
|
26
36
|
jervcode: Object.freeze({
|
|
27
37
|
name: "JervCode",
|
|
38
|
+
installWarm: "Welcome. Setting up JervCode \u2014 about a minute.",
|
|
28
39
|
accent: "38;2;192;132;252",
|
|
29
|
-
warm: "Welcome back. Keeping JervCode current\u2026"
|
|
40
|
+
warm: "Welcome back. Keeping JervCode current\u2026",
|
|
41
|
+
doctor: "jervcode doctor"
|
|
30
42
|
})
|
|
31
43
|
});
|
|
32
44
|
function identityFor(product) {
|
|
@@ -36,6 +48,8 @@ function identityFor(product) {
|
|
|
36
48
|
}
|
|
37
49
|
return identity;
|
|
38
50
|
}
|
|
51
|
+
|
|
52
|
+
// ../face/src/face.ts
|
|
39
53
|
var GLYPH = Object.freeze({
|
|
40
54
|
diamond: "\u25C6",
|
|
41
55
|
hollow: "\u25C7",
|
|
@@ -60,6 +74,9 @@ var ANSI = /\u001b\[[0-9;]*m/g;
|
|
|
60
74
|
function visibleWidth(text) {
|
|
61
75
|
return [...String(text).replace(ANSI, "")].length;
|
|
62
76
|
}
|
|
77
|
+
function stripColor(text) {
|
|
78
|
+
return String(text).replace(ANSI, "");
|
|
79
|
+
}
|
|
63
80
|
function faceWidth(columns) {
|
|
64
81
|
const raw = Number(columns);
|
|
65
82
|
return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
|
|
@@ -88,7 +105,14 @@ function wrapWords(text, width) {
|
|
|
88
105
|
if (line) lines.push(line);
|
|
89
106
|
return lines.length ? lines : [""];
|
|
90
107
|
}
|
|
91
|
-
|
|
108
|
+
var PROGRESS_PROTOCOL = 1;
|
|
109
|
+
function readProgressFd(env) {
|
|
110
|
+
const fd = Number.parseInt(String(env.MM_PROGRESS_FD ?? ""), 10);
|
|
111
|
+
if (!Number.isInteger(fd) || fd <= 0) return null;
|
|
112
|
+
const protocol = Number.parseInt(String(env.MM_PROGRESS_PROTOCOL ?? PROGRESS_PROTOCOL), 10);
|
|
113
|
+
return protocol === PROGRESS_PROTOCOL ? fd : null;
|
|
114
|
+
}
|
|
115
|
+
function createFace({ product, color = false, columns, env = process.env, operation }) {
|
|
92
116
|
const identity = identityFor(product);
|
|
93
117
|
const width = faceWidth(columns);
|
|
94
118
|
const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
|
|
@@ -96,28 +120,45 @@ function createFace({ product, color = false, columns, env = process.env }) {
|
|
|
96
120
|
const indent = " ".repeat(TITLE_COLUMN - 1);
|
|
97
121
|
const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
|
|
98
122
|
const continuesFace = continuedPhases.size > 0;
|
|
99
|
-
const
|
|
123
|
+
const nested = env.MM_OUTER_CONSOLE === "1";
|
|
124
|
+
const progressFd = readProgressFd(env);
|
|
125
|
+
const emitMilestone = (title, measure, kind) => {
|
|
126
|
+
if (progressFd === null) return false;
|
|
127
|
+
const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
|
|
128
|
+
if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
|
|
129
|
+
try {
|
|
130
|
+
writeSync(progressFd, `${JSON.stringify(record)}
|
|
131
|
+
`);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const welcome = () => continuesFace || nested ? [] : [
|
|
100
138
|
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
101
139
|
bar(),
|
|
102
|
-
`${bar()} ${identity.warm}`,
|
|
140
|
+
`${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
|
|
103
141
|
bar()
|
|
104
142
|
];
|
|
105
143
|
const continues = (phase, kind = "ok") => {
|
|
106
144
|
const inherited = continuedPhases.delete(String(phase).trim());
|
|
107
145
|
return kind === "fail" ? false : inherited;
|
|
108
146
|
};
|
|
109
|
-
const step = (title,
|
|
147
|
+
const step = (title, measure = null, kind = "ok") => {
|
|
148
|
+
if (emitMilestone(title, measure, kind)) return "";
|
|
110
149
|
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
111
|
-
const
|
|
150
|
+
const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
|
|
151
|
+
const time = measured;
|
|
112
152
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
113
|
-
const reserved = time ? 6 : 0;
|
|
153
|
+
const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
|
|
114
154
|
const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
|
|
115
155
|
const head = `${bar()} ${glyph} ${first}`;
|
|
116
156
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
117
157
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
118
158
|
};
|
|
119
|
-
const relay = (text) => String(text).split("\n").
|
|
120
|
-
const receipt = (lines) => {
|
|
159
|
+
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");
|
|
160
|
+
const receipt = (lines, { ready = true } = {}) => {
|
|
161
|
+
if (ready && nested) return [];
|
|
121
162
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
122
163
|
const line = String(raw);
|
|
123
164
|
if (visibleWidth(line) <= width - 6) return [line];
|
|
@@ -133,18 +174,377 @@ function createFace({ product, color = false, columns, env = process.env }) {
|
|
|
133
174
|
frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
|
|
134
175
|
];
|
|
135
176
|
};
|
|
136
|
-
const
|
|
177
|
+
const outcome = (changed, options = {}) => {
|
|
178
|
+
const fact = String(changed ?? "").trim();
|
|
179
|
+
if (!fact) {
|
|
180
|
+
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})`);
|
|
181
|
+
}
|
|
182
|
+
return receipt([
|
|
183
|
+
`${GLYPH.check} ${identity.name} is ready.`,
|
|
184
|
+
fact,
|
|
185
|
+
`Check health any time: ${identity.doctor}`
|
|
186
|
+
], options);
|
|
187
|
+
};
|
|
188
|
+
const signOff = () => nested ? "" : `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
|
|
137
189
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
138
|
-
return { identity, width, welcome, continues, step, relay, receipt, signOff, refusal, paint };
|
|
190
|
+
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
139
191
|
}
|
|
192
|
+
|
|
193
|
+
// ../face/src/shell.ts
|
|
140
194
|
var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
|
|
195
|
+
|
|
196
|
+
// ../face/src/spinner.ts
|
|
197
|
+
import { appendFileSync, writeSync as writeSync2 } from "node:fs";
|
|
198
|
+
import { Worker } from "node:worker_threads";
|
|
141
199
|
var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
200
|
+
var WORKER_SOURCE = `
|
|
201
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
202
|
+
const { writeSync, appendFileSync } = require('node:fs');
|
|
203
|
+
const control = new Int32Array(workerData.control);
|
|
204
|
+
let frames = workerData.frames, frame = workerData.frame;
|
|
205
|
+
function draw() {
|
|
206
|
+
if (Atomics.compareExchange(control, 1, 0, 1) !== 0) return;
|
|
207
|
+
try {
|
|
208
|
+
if (Atomics.load(control, 0) || Atomics.load(control, 2) || !frames.length) return;
|
|
209
|
+
const text = frames[frame++ % frames.length];
|
|
210
|
+
writeSync(2, text);
|
|
211
|
+
if (workerData.transcriptPath) appendFileSync(workerData.transcriptPath, JSON.stringify({ channel: 'spinner', text }) + '\\n');
|
|
212
|
+
} finally {
|
|
213
|
+
Atomics.store(control, 1, 0);
|
|
214
|
+
Atomics.notify(control, 1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
parentPort.on('message', (next) => {
|
|
218
|
+
if (Atomics.load(control, 2)) return;
|
|
219
|
+
frames = next.frames;
|
|
220
|
+
frame = next.frame;
|
|
221
|
+
Atomics.store(control, 0, 0);
|
|
222
|
+
});
|
|
223
|
+
setInterval(draw, workerData.intervalMs);
|
|
224
|
+
`;
|
|
225
|
+
function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
|
|
226
|
+
animate = animate && !face.nested && !face.emitsProgress;
|
|
227
|
+
let timer = null;
|
|
228
|
+
let worker = null;
|
|
229
|
+
let control = null;
|
|
230
|
+
let frames = [];
|
|
231
|
+
let frame = 0;
|
|
232
|
+
const write = (text) => {
|
|
233
|
+
if (stream) stream.write(text);
|
|
234
|
+
else {
|
|
235
|
+
writeSync2(2, text);
|
|
236
|
+
if (transcriptPath) appendFileSync(transcriptPath, `${JSON.stringify({ channel: "spinner", text })}
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const render = (text, measure) => {
|
|
241
|
+
const line = face.step(text, measure, "note").split("\n")[0];
|
|
242
|
+
frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
|
|
243
|
+
};
|
|
244
|
+
const draw = () => {
|
|
245
|
+
if (animate && frames.length) write(frames[frame++ % frames.length]);
|
|
246
|
+
};
|
|
247
|
+
const pause = () => {
|
|
248
|
+
if (!control) return;
|
|
249
|
+
Atomics.store(control, 0, 1);
|
|
250
|
+
while (Atomics.load(control, 1)) Atomics.wait(control, 1, 1);
|
|
251
|
+
};
|
|
252
|
+
const halt = () => {
|
|
253
|
+
if (timer) clearInterval(timer);
|
|
254
|
+
timer = null;
|
|
255
|
+
if (control) Atomics.store(control, 2, 1);
|
|
256
|
+
pause();
|
|
257
|
+
if (worker) void worker.terminate();
|
|
258
|
+
worker = null;
|
|
259
|
+
control = null;
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
start(text, measure = null) {
|
|
263
|
+
if (!animate) return;
|
|
264
|
+
halt();
|
|
265
|
+
render(text, measure);
|
|
266
|
+
frame = 0;
|
|
267
|
+
draw();
|
|
268
|
+
if (!frames.length) return;
|
|
269
|
+
if (stream) timer = setInterval(draw, intervalMs).unref();
|
|
270
|
+
else {
|
|
271
|
+
control = new Int32Array(new SharedArrayBuffer(12));
|
|
272
|
+
try {
|
|
273
|
+
worker = new Worker(WORKER_SOURCE, { eval: true, workerData: {
|
|
274
|
+
control: control.buffer,
|
|
275
|
+
frames,
|
|
276
|
+
frame,
|
|
277
|
+
intervalMs,
|
|
278
|
+
transcriptPath
|
|
279
|
+
} });
|
|
280
|
+
const active = worker;
|
|
281
|
+
worker.on("error", () => {
|
|
282
|
+
if (worker === active) halt();
|
|
283
|
+
});
|
|
284
|
+
worker.unref();
|
|
285
|
+
} catch {
|
|
286
|
+
halt();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
say(text, measure = null) {
|
|
291
|
+
if (!animate) return;
|
|
292
|
+
pause();
|
|
293
|
+
render(text, measure);
|
|
294
|
+
draw();
|
|
295
|
+
worker?.postMessage({ frames, frame });
|
|
296
|
+
},
|
|
297
|
+
stop() {
|
|
298
|
+
halt();
|
|
299
|
+
if (animate) write("\r\x1B[2K");
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
142
303
|
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
304
|
+
|
|
305
|
+
// ../face/src/conformance.ts
|
|
143
306
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
144
307
|
|
|
308
|
+
// ../face/src/run.ts
|
|
309
|
+
import { appendFileSync as appendFileSync2 } from "node:fs";
|
|
310
|
+
|
|
311
|
+
// ../face/src/outcome.ts
|
|
312
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
313
|
+
var counts = ["total", "updated", "failed"];
|
|
314
|
+
var strings = ["version", "retry", "detail", "logPath"];
|
|
315
|
+
var flags = ["dryRun", "installed", "deferred", "operationFailed"];
|
|
316
|
+
function validateInstallerOutcome(value) {
|
|
317
|
+
const invalid = () => {
|
|
318
|
+
throw new Error("installer outcome: invalid child result");
|
|
319
|
+
};
|
|
320
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalid();
|
|
321
|
+
const facts = value;
|
|
322
|
+
const allowed = [...counts, ...strings, ...flags, "rollback", "logState"];
|
|
323
|
+
if (Object.keys(facts).some((key) => !allowed.includes(key))) return invalid();
|
|
324
|
+
for (const key of counts) if (!Number.isSafeInteger(facts[key]) || facts[key] < 0) return invalid();
|
|
325
|
+
if (facts.updated + facts.failed > facts.total) return invalid();
|
|
326
|
+
for (const key of strings) if (facts[key] !== void 0 && (typeof facts[key] !== "string" || facts[key].length > 4096)) return invalid();
|
|
327
|
+
for (const key of flags) if (facts[key] !== void 0 && typeof facts[key] !== "boolean") return invalid();
|
|
328
|
+
if (facts.rollback !== void 0 && !["completed", "partial", "not-needed", "unknown"].includes(facts.rollback)) return invalid();
|
|
329
|
+
if (facts.logState !== void 0 && !["unavailable", "omitted"].includes(facts.logState)) return invalid();
|
|
330
|
+
if (facts.logPath !== void 0 && (!facts.logPath.trim() || /[\x00-\x1f\x7f]/u.test(facts.logPath))) return invalid();
|
|
331
|
+
return { ...facts };
|
|
332
|
+
}
|
|
333
|
+
function writeInstallerOutcome(path, value) {
|
|
334
|
+
writeFileSync(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
335
|
+
}
|
|
336
|
+
function readInstallerOutcome(path) {
|
|
337
|
+
let text;
|
|
338
|
+
try {
|
|
339
|
+
text = readFileSync(path, "utf8");
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error.code === "ENOENT") return void 0;
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
if (text.length > 16384) throw new Error("installer outcome: child result is too large");
|
|
345
|
+
try {
|
|
346
|
+
return validateInstallerOutcome(JSON.parse(text));
|
|
347
|
+
} catch {
|
|
348
|
+
throw new Error("installer outcome: invalid child result");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ../face/src/run.ts
|
|
353
|
+
function validateInstallerProduct(value) {
|
|
354
|
+
const fail = (field2) => {
|
|
355
|
+
throw new Error(`installer product: invalid ${field2}`);
|
|
356
|
+
};
|
|
357
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
|
|
358
|
+
const input = value;
|
|
359
|
+
const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
|
|
360
|
+
const key = text(input.product, "product");
|
|
361
|
+
const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
|
|
362
|
+
const identity = identityFor(product);
|
|
363
|
+
const gate = text(input.gate, "gate");
|
|
364
|
+
let gateUrl;
|
|
365
|
+
try {
|
|
366
|
+
gateUrl = new URL(gate);
|
|
367
|
+
} catch {
|
|
368
|
+
return fail("gate");
|
|
369
|
+
}
|
|
370
|
+
if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
|
|
371
|
+
const doctor = text(input.doctor, "doctor");
|
|
372
|
+
if (doctor !== identity.doctor) return fail("doctor");
|
|
373
|
+
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
374
|
+
const ids = /* @__PURE__ */ new Set();
|
|
375
|
+
const surfaces = input.surfaces.map((raw) => {
|
|
376
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
|
|
377
|
+
const source = raw;
|
|
378
|
+
const id = text(source.id, "surface.id");
|
|
379
|
+
if (ids.has(id)) return fail("duplicate surface.id");
|
|
380
|
+
ids.add(id);
|
|
381
|
+
const surface = { id };
|
|
382
|
+
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
383
|
+
if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
|
|
384
|
+
}
|
|
385
|
+
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
386
|
+
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
387
|
+
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
388
|
+
return surface;
|
|
389
|
+
});
|
|
390
|
+
return { product, gate, doctor, surfaces };
|
|
391
|
+
}
|
|
392
|
+
var PHASES = {
|
|
393
|
+
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
394
|
+
resolve: ["Resolving the release", "Resolved the release"],
|
|
395
|
+
download: ["Downloading the payload", "Downloaded the payload"],
|
|
396
|
+
"sign-in": ["Signing in", "Signed in"],
|
|
397
|
+
check: ["Checking surfaces", "Checked surfaces"],
|
|
398
|
+
arm: ["Scheduling updates", "Armed hourly updates"],
|
|
399
|
+
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
400
|
+
verify: ["Verifying the payload", "Verified the payload"],
|
|
401
|
+
install: ["Installing the product", "Installed the product"],
|
|
402
|
+
activate: ["Activating surfaces", "Activated surfaces"],
|
|
403
|
+
doctor: ["Checking health", "Checked health"],
|
|
404
|
+
rollback: ["Restoring the previous version", "Restored the previous version"]
|
|
405
|
+
};
|
|
406
|
+
function createInstallerRun(value, options = {}) {
|
|
407
|
+
const declaration = validateInstallerProduct(value);
|
|
408
|
+
const env = options.env ?? process.env;
|
|
409
|
+
const tty = options.tty ?? Boolean(process.stdout.isTTY);
|
|
410
|
+
const face = createFace({
|
|
411
|
+
operation: options.operation,
|
|
412
|
+
product: declaration.product,
|
|
413
|
+
columns: options.columns,
|
|
414
|
+
env,
|
|
415
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
416
|
+
});
|
|
417
|
+
const errors = [];
|
|
418
|
+
const write = options.write ? (text, channel) => {
|
|
419
|
+
try {
|
|
420
|
+
options.write(text, channel);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
errors.push(`installer output observer: ${error instanceof Error ? error.message : String(error)}`);
|
|
423
|
+
}
|
|
424
|
+
} : (text, channel) => {
|
|
425
|
+
(channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
426
|
+
};
|
|
427
|
+
const emit = (text, channel = "stdout", recorded = text) => {
|
|
428
|
+
if (!text) return;
|
|
429
|
+
write(text, channel);
|
|
430
|
+
if (env.MM_FACE_TRANSCRIPT) {
|
|
431
|
+
appendFileSync2(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
432
|
+
`, "utf8");
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const lines = (rows, channel = "stdout") => {
|
|
436
|
+
for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
|
|
437
|
+
`, channel);
|
|
438
|
+
};
|
|
439
|
+
const spinner = createSpinner(face, {
|
|
440
|
+
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
441
|
+
...options.write ? { stream: { write: (text) => {
|
|
442
|
+
emit(String(text), "spinner");
|
|
443
|
+
return true;
|
|
444
|
+
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
445
|
+
});
|
|
446
|
+
let started = false;
|
|
447
|
+
let finished = false;
|
|
448
|
+
const start = () => {
|
|
449
|
+
if (started || finished) return;
|
|
450
|
+
started = true;
|
|
451
|
+
const welcome = face.welcome();
|
|
452
|
+
if (tty) lines(welcome);
|
|
453
|
+
else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
|
|
454
|
+
};
|
|
455
|
+
const durable = (title, measure, kind) => {
|
|
456
|
+
spinner.stop();
|
|
457
|
+
const rendered = face.step(title, measure, kind);
|
|
458
|
+
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
459
|
+
};
|
|
460
|
+
const run2 = {
|
|
461
|
+
get errors() {
|
|
462
|
+
return [...errors];
|
|
463
|
+
},
|
|
464
|
+
start,
|
|
465
|
+
phase(id, facts = {}) {
|
|
466
|
+
if (finished) throw new Error("installer run already finished");
|
|
467
|
+
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
468
|
+
start();
|
|
469
|
+
const state = facts.state ?? "ok";
|
|
470
|
+
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
471
|
+
if (state === "running") {
|
|
472
|
+
spinner.start(title, facts.measure ?? null);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
476
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
477
|
+
},
|
|
478
|
+
surface(facts) {
|
|
479
|
+
if (finished) throw new Error("installer run already finished");
|
|
480
|
+
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
481
|
+
if (!surface) throw new Error("installer run: undeclared surface");
|
|
482
|
+
start();
|
|
483
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
484
|
+
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
485
|
+
if (!status) throw new Error("installer run: unknown surface state");
|
|
486
|
+
const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
|
|
487
|
+
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
488
|
+
durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
489
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
490
|
+
},
|
|
491
|
+
milestone({ step, state, ms }) {
|
|
492
|
+
start();
|
|
493
|
+
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
494
|
+
},
|
|
495
|
+
signIn({ url, code }) {
|
|
496
|
+
start();
|
|
497
|
+
spinner.stop();
|
|
498
|
+
for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
|
|
499
|
+
const rendered = `${tty ? face.relay(text) : text}
|
|
500
|
+
`;
|
|
501
|
+
emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
505
|
+
relay(text, channel = "stdout", record = true) {
|
|
506
|
+
spinner.stop();
|
|
507
|
+
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
508
|
+
for (const row of rendered.split("\n")) if (row) {
|
|
509
|
+
emit(`${row}
|
|
510
|
+
`, channel, record ? `${row}
|
|
511
|
+
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
finish(facts) {
|
|
516
|
+
if (finished) return;
|
|
517
|
+
validateInstallerOutcome(facts);
|
|
518
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
519
|
+
start();
|
|
520
|
+
spinner.stop();
|
|
521
|
+
finished = true;
|
|
522
|
+
const changed = !facts.version ? "No release target is available." : facts.dryRun ? `Would update ${facts.updated} of ${facts.total} surfaces to ${facts.version}.` : facts.installed ? `Installed ${facts.version} across ${facts.total} surfaces.` : `Updated ${facts.updated} of ${facts.total} surfaces to ${facts.version}.`;
|
|
523
|
+
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
524
|
+
const body = [
|
|
525
|
+
`${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."}`,
|
|
526
|
+
changed,
|
|
527
|
+
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
528
|
+
...facts.detail ? [facts.detail] : [],
|
|
529
|
+
...facts.rollback ? [`Rollback: ${facts.rollback}`] : [],
|
|
530
|
+
...facts.logPath ? [`Log: ${facts.logPath}`] : facts.logState ? [`Log: ${facts.logState}`] : [],
|
|
531
|
+
facts.retry && (facts.failed > 0 || facts.operationFailed) ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
532
|
+
];
|
|
533
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
534
|
+
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
535
|
+
}
|
|
536
|
+
if (tty) lines([face.signOff()]);
|
|
537
|
+
},
|
|
538
|
+
stop() {
|
|
539
|
+
spinner.stop();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
return run2;
|
|
543
|
+
}
|
|
544
|
+
|
|
145
545
|
// src/autoupdate.ts
|
|
146
546
|
import { spawnSync } from "node:child_process";
|
|
147
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
547
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
148
548
|
import { tmpdir } from "node:os";
|
|
149
549
|
import { join } from "node:path";
|
|
150
550
|
function schedulePlatform(override) {
|
|
@@ -193,7 +593,7 @@ function enableSchedule(config, command, options = {}) {
|
|
|
193
593
|
const dir2 = join(home, "Library", "LaunchAgents");
|
|
194
594
|
mkdirSync(dir2, { recursive: true });
|
|
195
595
|
const plist = join(dir2, `${label2}.plist`);
|
|
196
|
-
|
|
596
|
+
writeFileSync2(plist, darwinPlist(label2, command));
|
|
197
597
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
198
598
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
199
599
|
if (result2.code !== 0) {
|
|
@@ -204,8 +604,8 @@ function enableSchedule(config, command, options = {}) {
|
|
|
204
604
|
const label = scheduleLabel(config);
|
|
205
605
|
const dir = join(home, ".config", "systemd", "user");
|
|
206
606
|
mkdirSync(dir, { recursive: true });
|
|
207
|
-
|
|
208
|
-
|
|
607
|
+
writeFileSync2(join(dir, `${label}.service`), linuxService(command));
|
|
608
|
+
writeFileSync2(join(dir, `${label}.timer`), linuxTimer(label));
|
|
209
609
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
210
610
|
if (reload.code !== 0) {
|
|
211
611
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -318,7 +718,7 @@ function firstLine(text) {
|
|
|
318
718
|
}
|
|
319
719
|
|
|
320
720
|
// src/config.ts
|
|
321
|
-
import { readFileSync as
|
|
721
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
322
722
|
import { getAsset } from "node:sea";
|
|
323
723
|
|
|
324
724
|
// src/module-url.ts
|
|
@@ -340,10 +740,10 @@ function loadProductConfig(options = {}) {
|
|
|
340
740
|
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
341
741
|
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
342
742
|
if (explicit) {
|
|
343
|
-
return parseProductConfig(
|
|
743
|
+
return parseProductConfig(readFileSync3(explicit, "utf8"));
|
|
344
744
|
}
|
|
345
745
|
try {
|
|
346
|
-
return parseProductConfig(
|
|
746
|
+
return parseProductConfig(readFileSync3(devFallback, "utf8"));
|
|
347
747
|
} catch {
|
|
348
748
|
}
|
|
349
749
|
try {
|
|
@@ -462,7 +862,8 @@ async function loginGithub(config, options = {}) {
|
|
|
462
862
|
const sleep = options.sleep ?? realSleep;
|
|
463
863
|
const issued = await requestDeviceCode(config, fetchImpl);
|
|
464
864
|
const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
|
|
465
|
-
|
|
865
|
+
if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
|
|
866
|
+
else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
|
|
466
867
|
open(openUrl);
|
|
467
868
|
const deadline = now() + issued.expires_in * 1e3;
|
|
468
869
|
let intervalMs = Math.max(1, issued.interval) * 1e3;
|
|
@@ -632,7 +1033,7 @@ function page(title, body) {
|
|
|
632
1033
|
|
|
633
1034
|
// src/payload.ts
|
|
634
1035
|
import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
|
|
635
|
-
import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as
|
|
1036
|
+
import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
636
1037
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
637
1038
|
import { dirname, join as join2 } from "node:path";
|
|
638
1039
|
|
|
@@ -784,7 +1185,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
784
1185
|
}
|
|
785
1186
|
const dest = join2(staging, entry.path);
|
|
786
1187
|
mkdirSync2(dirname(dest), { recursive: true });
|
|
787
|
-
|
|
1188
|
+
writeFileSync3(dest, bytes);
|
|
788
1189
|
}
|
|
789
1190
|
const target = join2(dir, "payload");
|
|
790
1191
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -798,7 +1199,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
798
1199
|
}
|
|
799
1200
|
|
|
800
1201
|
// src/store.ts
|
|
801
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
1202
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
802
1203
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
803
1204
|
import { join as join3 } from "node:path";
|
|
804
1205
|
function defaultProductDir(product) {
|
|
@@ -823,7 +1224,7 @@ function payloadDir(dir) {
|
|
|
823
1224
|
}
|
|
824
1225
|
function readTokens(dir) {
|
|
825
1226
|
try {
|
|
826
|
-
const data = JSON.parse(
|
|
1227
|
+
const data = JSON.parse(readFileSync4(tokensPath(dir), "utf8"));
|
|
827
1228
|
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
828
1229
|
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
829
1230
|
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
@@ -837,10 +1238,10 @@ function readTokens(dir) {
|
|
|
837
1238
|
function writeTokens(dir, tokens) {
|
|
838
1239
|
mkdirSync3(dir, { recursive: true });
|
|
839
1240
|
try {
|
|
840
|
-
|
|
1241
|
+
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
841
1242
|
`, { mode: 384 });
|
|
842
1243
|
} catch {
|
|
843
|
-
|
|
1244
|
+
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
844
1245
|
`);
|
|
845
1246
|
}
|
|
846
1247
|
}
|
|
@@ -849,7 +1250,7 @@ function clearTokens(dir) {
|
|
|
849
1250
|
}
|
|
850
1251
|
function readState(dir) {
|
|
851
1252
|
try {
|
|
852
|
-
const data = JSON.parse(
|
|
1253
|
+
const data = JSON.parse(readFileSync4(statePath(dir), "utf8"));
|
|
853
1254
|
if (typeof data.version !== "string" || !data.version) return null;
|
|
854
1255
|
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
855
1256
|
} catch {
|
|
@@ -858,7 +1259,7 @@ function readState(dir) {
|
|
|
858
1259
|
}
|
|
859
1260
|
function writeState(dir, state) {
|
|
860
1261
|
mkdirSync3(dir, { recursive: true });
|
|
861
|
-
|
|
1262
|
+
writeFileSync4(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
862
1263
|
`);
|
|
863
1264
|
}
|
|
864
1265
|
function wipeProductDir(dir) {
|
|
@@ -868,7 +1269,7 @@ function wipeProductDir(dir) {
|
|
|
868
1269
|
}
|
|
869
1270
|
|
|
870
1271
|
// src/index.ts
|
|
871
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1272
|
+
var LAUNCHER_VERSION = true ? "0.1.11" : readVersionFromPackage();
|
|
872
1273
|
function defaultPrint(message) {
|
|
873
1274
|
process.stdout.write(`${message}
|
|
874
1275
|
`);
|
|
@@ -1004,11 +1405,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
1004
1405
|
});
|
|
1005
1406
|
return refreshed.accessToken;
|
|
1006
1407
|
}
|
|
1007
|
-
async function doLogin(config, dir, options, print) {
|
|
1408
|
+
async function doLogin(config, dir, options, print, installer) {
|
|
1008
1409
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1009
1410
|
const open = options.open ?? openBrowser;
|
|
1010
1411
|
if (config.loginKind === "github") {
|
|
1011
|
-
const tokens = await loginGithub(config, {
|
|
1412
|
+
const tokens = await loginGithub(config, {
|
|
1413
|
+
fetchImpl,
|
|
1414
|
+
open,
|
|
1415
|
+
print,
|
|
1416
|
+
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
1417
|
+
});
|
|
1012
1418
|
writeTokens(dir, {
|
|
1013
1419
|
accessToken: tokens.accessToken,
|
|
1014
1420
|
refreshToken: tokens.refreshToken,
|
|
@@ -1023,20 +1429,20 @@ async function doLogin(config, dir, options, print) {
|
|
|
1023
1429
|
clientId: tokens.clientId
|
|
1024
1430
|
});
|
|
1025
1431
|
}
|
|
1026
|
-
print(`signed in to ${config.product}.`);
|
|
1432
|
+
if (!installer) print(`signed in to ${config.product}.`);
|
|
1027
1433
|
}
|
|
1028
|
-
async function fetchAccessTokenOrLogin(config, dir, options, print) {
|
|
1434
|
+
async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
|
|
1029
1435
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1030
1436
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1031
1437
|
if (fresh) return fresh;
|
|
1032
|
-
await doLogin(config, dir, options, print);
|
|
1438
|
+
await doLogin(config, dir, options, print, installer);
|
|
1033
1439
|
const after = readTokens(dir);
|
|
1034
1440
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
1035
1441
|
return after.accessToken;
|
|
1036
1442
|
}
|
|
1037
1443
|
function readPayloadArgv(dir, key) {
|
|
1038
1444
|
try {
|
|
1039
|
-
const parsed = JSON.parse(
|
|
1445
|
+
const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
|
|
1040
1446
|
const entry = parsed[key];
|
|
1041
1447
|
if (typeof entry === "string") {
|
|
1042
1448
|
const parts = entry.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1058,7 +1464,7 @@ function readPayloadRun(dir) {
|
|
|
1058
1464
|
}
|
|
1059
1465
|
function readPayloadVerbs(dir) {
|
|
1060
1466
|
try {
|
|
1061
|
-
const parsed = JSON.parse(
|
|
1467
|
+
const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
|
|
1062
1468
|
const verbs = parsed.verbs;
|
|
1063
1469
|
if (verbs === "*") return "*";
|
|
1064
1470
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -1106,7 +1512,7 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1106
1512
|
const [command, ...args] = entry;
|
|
1107
1513
|
const shell = needsShell(command);
|
|
1108
1514
|
const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
|
|
1109
|
-
const
|
|
1515
|
+
const spawnEntry = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
|
|
1110
1516
|
cwd,
|
|
1111
1517
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
1112
1518
|
shell,
|
|
@@ -1115,12 +1521,12 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1115
1521
|
});
|
|
1116
1522
|
let result;
|
|
1117
1523
|
if (process.platform === "win32" && shell) {
|
|
1118
|
-
result =
|
|
1524
|
+
result = spawnEntry(false);
|
|
1119
1525
|
} else {
|
|
1120
1526
|
try {
|
|
1121
|
-
result =
|
|
1527
|
+
result = spawnEntry(true);
|
|
1122
1528
|
} catch {
|
|
1123
|
-
result =
|
|
1529
|
+
result = spawnEntry(false);
|
|
1124
1530
|
}
|
|
1125
1531
|
}
|
|
1126
1532
|
if (result.error) return { ok: false, error: result.error.message };
|
|
@@ -1137,73 +1543,68 @@ function faceProduct(config) {
|
|
|
1137
1543
|
const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
|
|
1138
1544
|
return known[config.product] ?? config.product;
|
|
1139
1545
|
}
|
|
1140
|
-
function
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1546
|
+
async function installOrUpdate(config, dir, options, print, update) {
|
|
1547
|
+
const product = faceProduct(config);
|
|
1548
|
+
const installer = createInstallerRun({
|
|
1549
|
+
product,
|
|
1550
|
+
gate: config.host,
|
|
1551
|
+
doctor: identityFor(product).doctor,
|
|
1552
|
+
surfaces: [{ id: config.product, kind: "payload" }]
|
|
1553
|
+
}, {
|
|
1554
|
+
operation: update ? "update" : "install",
|
|
1555
|
+
tty: options.tty,
|
|
1556
|
+
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
1557
|
+
...options.print ? { write: (text) => print(text.replace(/\n$/, "")) } : {}
|
|
1558
|
+
});
|
|
1559
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1560
|
+
let version = readState(dir)?.version ?? "unknown";
|
|
1561
|
+
installer.start();
|
|
1143
1562
|
try {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1563
|
+
let started = Date.now();
|
|
1564
|
+
installer.phase("sign-in", { state: "running" });
|
|
1565
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1566
|
+
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1567
|
+
installer.phase("resolve", { state: "running" });
|
|
1568
|
+
started = Date.now();
|
|
1569
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1570
|
+
version = manifest.version;
|
|
1571
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1572
|
+
const current = readState(dir);
|
|
1573
|
+
const unchanged = update && current?.version === version && existsSync4(payloadDir(dir));
|
|
1574
|
+
if (!unchanged) {
|
|
1575
|
+
installer.phase("download", { state: "running" });
|
|
1576
|
+
started = Date.now();
|
|
1577
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1578
|
+
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1579
|
+
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1580
|
+
}
|
|
1581
|
+
installer.surface({
|
|
1582
|
+
id: config.product,
|
|
1583
|
+
from: current?.version,
|
|
1584
|
+
to: version,
|
|
1585
|
+
state: unchanged ? "current" : "updated"
|
|
1148
1586
|
});
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if (!face) {
|
|
1164
|
-
print(headline);
|
|
1165
|
-
for (const line of lines) print(line.trim());
|
|
1166
|
-
return;
|
|
1167
|
-
}
|
|
1168
|
-
const glyph = ready ? "\u2714" : "\u2716";
|
|
1169
|
-
for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
|
|
1170
|
-
print(face.signOff());
|
|
1171
|
-
}
|
|
1172
|
-
function printStep(face, print, title, seconds, kind = "ok") {
|
|
1173
|
-
print(face ? face.step(title, seconds, kind) : title);
|
|
1174
|
-
}
|
|
1175
|
-
function printProgress(face, print, progress) {
|
|
1176
|
-
for (const message of progress ?? []) {
|
|
1177
|
-
printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
|
|
1587
|
+
return await finishLastMile(config, dir, options, installer, version, unchanged);
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
const code = error instanceof NeedsLoginError ? 3 : 1;
|
|
1590
|
+
installer.finish({
|
|
1591
|
+
version,
|
|
1592
|
+
total: 1,
|
|
1593
|
+
updated: 0,
|
|
1594
|
+
failed: 1,
|
|
1595
|
+
detail: error.message,
|
|
1596
|
+
retry: `${config.binName} ${update ? "update" : "install"}`
|
|
1597
|
+
});
|
|
1598
|
+
return code;
|
|
1599
|
+
} finally {
|
|
1600
|
+
installer.stop();
|
|
1178
1601
|
}
|
|
1179
1602
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const face = faceFor(config, options);
|
|
1183
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1184
|
-
const started = Date.now();
|
|
1185
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1186
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1187
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1188
|
-
printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
|
|
1189
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1603
|
+
function doInstall(config, dir, options, print) {
|
|
1604
|
+
return installOrUpdate(config, dir, options, print, false);
|
|
1190
1605
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
const face = faceFor(config, options);
|
|
1194
|
-
if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
|
|
1195
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1196
|
-
const started = Date.now();
|
|
1197
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1198
|
-
const current = readState(dir);
|
|
1199
|
-
if (current && current.version === manifest.version && existsSync4(payloadDir(dir))) {
|
|
1200
|
-
printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
|
|
1201
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1202
|
-
}
|
|
1203
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1204
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1205
|
-
printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
|
|
1206
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1606
|
+
function doUpdate(config, dir, options, print) {
|
|
1607
|
+
return installOrUpdate(config, dir, options, print, true);
|
|
1207
1608
|
}
|
|
1208
1609
|
function payloadEnv(dir) {
|
|
1209
1610
|
const stored = readTokens(dir);
|
|
@@ -1225,51 +1626,124 @@ function payloadFileAsCommand(entry, payload) {
|
|
|
1225
1626
|
return null;
|
|
1226
1627
|
}
|
|
1227
1628
|
}
|
|
1228
|
-
function finishLastMile(config, dir, options,
|
|
1629
|
+
async function finishLastMile(config, dir, options, installer, version, unchanged) {
|
|
1229
1630
|
const entry = readPayloadEntry(dir);
|
|
1230
1631
|
const payload = payloadDir(dir);
|
|
1231
1632
|
if (!entry) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1633
|
+
installer.finish({
|
|
1634
|
+
version,
|
|
1635
|
+
total: 1,
|
|
1636
|
+
updated: unchanged ? 0 : 1,
|
|
1637
|
+
failed: 0,
|
|
1638
|
+
installed: true,
|
|
1639
|
+
detail: `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
|
|
1640
|
+
});
|
|
1236
1641
|
return 0;
|
|
1237
1642
|
}
|
|
1238
1643
|
const command = resolveEntry(entry);
|
|
1239
1644
|
const dataFile = payloadFileAsCommand(entry, payload);
|
|
1240
1645
|
if (dataFile) {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1646
|
+
installer.finish({
|
|
1647
|
+
version,
|
|
1648
|
+
total: 1,
|
|
1649
|
+
updated: 0,
|
|
1650
|
+
failed: 1,
|
|
1651
|
+
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.`
|
|
1652
|
+
});
|
|
1247
1653
|
return 2;
|
|
1248
1654
|
}
|
|
1655
|
+
installer.phase("activate", { state: "running" });
|
|
1249
1656
|
const started = Date.now();
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1657
|
+
const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1658
|
+
const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
|
|
1659
|
+
for (const progress of result.progress ?? []) installer.milestone(progress);
|
|
1660
|
+
const succeeded = result.ok && (result.code === void 0 || result.code === 0);
|
|
1661
|
+
const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
|
|
1662
|
+
installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
|
|
1663
|
+
installer.finish({
|
|
1664
|
+
...outcome ?? {
|
|
1665
|
+
version,
|
|
1666
|
+
total: 1,
|
|
1667
|
+
updated: succeeded && !unchanged ? 1 : 0,
|
|
1668
|
+
failed: succeeded ? 0 : 1,
|
|
1669
|
+
installed: true
|
|
1670
|
+
},
|
|
1671
|
+
...!succeeded ? {
|
|
1672
|
+
operationFailed: true,
|
|
1673
|
+
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}` : ""}`,
|
|
1674
|
+
retry: `(cd ${payload} && ${command.join(" ")})`
|
|
1675
|
+
} : {}
|
|
1676
|
+
});
|
|
1677
|
+
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1678
|
+
}
|
|
1679
|
+
async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
1680
|
+
const [command, ...args] = entry;
|
|
1681
|
+
const shell = needsShell(command);
|
|
1682
|
+
const progress = !(process.platform === "win32" && shell);
|
|
1683
|
+
const outcomeDir = mkdtempSync(join4(tmpdir4(), "mm-installer-outcome-"));
|
|
1684
|
+
const outcomeFile = join4(outcomeDir, "outcome.json");
|
|
1685
|
+
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
1686
|
+
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
1687
|
+
delete childEnv.MM_PROGRESS_FD;
|
|
1688
|
+
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
1689
|
+
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
1690
|
+
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
1691
|
+
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
1692
|
+
try {
|
|
1693
|
+
const result = await new Promise((resolve2) => {
|
|
1694
|
+
const child = spawn2(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
|
|
1695
|
+
cwd,
|
|
1696
|
+
shell,
|
|
1697
|
+
windowsHide: true,
|
|
1698
|
+
env: childEnv,
|
|
1699
|
+
stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1700
|
+
});
|
|
1701
|
+
for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
|
|
1702
|
+
let partial = "";
|
|
1703
|
+
stream?.setEncoding("utf8").on("data", (text) => {
|
|
1704
|
+
const safe = redact(partial + text);
|
|
1705
|
+
let held = 0;
|
|
1706
|
+
for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
|
|
1707
|
+
if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
|
|
1708
|
+
}
|
|
1709
|
+
partial = held ? safe.slice(-held) : "";
|
|
1710
|
+
const visible = held ? safe.slice(0, -held) : safe;
|
|
1711
|
+
if (visible) installer.relay(visible, channel2, false);
|
|
1712
|
+
}).on("end", () => {
|
|
1713
|
+
if (partial) installer.relay("[redacted]", channel2, false);
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
let pending = "";
|
|
1717
|
+
const channel = child.stdio[3];
|
|
1718
|
+
if (channel && "setEncoding" in channel) {
|
|
1719
|
+
channel.setEncoding("utf8");
|
|
1720
|
+
channel.on("data", (text) => {
|
|
1721
|
+
pending += text;
|
|
1722
|
+
const end = pending.lastIndexOf("\n");
|
|
1723
|
+
if (end >= 0) {
|
|
1724
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
1725
|
+
pending = pending.slice(end + 1);
|
|
1726
|
+
}
|
|
1727
|
+
if (pending.length > 65536) pending = "";
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
child.on("error", (error) => resolve2({ ok: false, error: error.message }));
|
|
1731
|
+
child.on("close", (code) => resolve2({
|
|
1732
|
+
ok: code === 0,
|
|
1733
|
+
...code !== null ? { code } : {},
|
|
1734
|
+
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
1735
|
+
}));
|
|
1736
|
+
});
|
|
1737
|
+
try {
|
|
1738
|
+
const outcome = readInstallerOutcome(outcomeFile);
|
|
1739
|
+
return { ...result, ...outcome ? { outcome } : {} };
|
|
1740
|
+
} catch {
|
|
1741
|
+
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
1742
|
+
}
|
|
1743
|
+
} finally {
|
|
1744
|
+
if (existsSync4(outcomeFile)) unlinkSync(outcomeFile);
|
|
1745
|
+
rmdirSync(outcomeDir);
|
|
1259
1746
|
}
|
|
1260
|
-
const code = result.code ?? 1;
|
|
1261
|
-
printStep(
|
|
1262
|
-
face,
|
|
1263
|
-
print,
|
|
1264
|
-
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})`,
|
|
1265
|
-
null,
|
|
1266
|
-
"fail"
|
|
1267
|
-
);
|
|
1268
|
-
printReceipt(face, config, print, false, [
|
|
1269
|
-
`Downloaded ${version} into ${dir}`,
|
|
1270
|
-
`Finish it with: (cd ${payload} && ${command.join(" ")})`
|
|
1271
|
-
]);
|
|
1272
|
-
return code;
|
|
1273
1747
|
}
|
|
1274
1748
|
function doForward(config, dir, options, command, argv, print) {
|
|
1275
1749
|
if (command && existsSync4(command)) {
|
|
@@ -1392,5 +1866,6 @@ export {
|
|
|
1392
1866
|
readPayloadVerbs,
|
|
1393
1867
|
resolveEntry,
|
|
1394
1868
|
run,
|
|
1395
|
-
runFile
|
|
1869
|
+
runFile,
|
|
1870
|
+
runInstallEntry
|
|
1396
1871
|
};
|