@mutmutco/installer-launcher 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/launcher.js +611 -144
- package/dist/launcher.sea.cjs +637 -169
- 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,35 +105,64 @@ 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);
|
|
95
119
|
const bar = () => paint(PALETTE.muted, GLYPH.bar);
|
|
96
120
|
const indent = " ".repeat(TITLE_COLUMN - 1);
|
|
97
|
-
const
|
|
121
|
+
const continuedPhases = new Set((env.MM_FACE_CONTINUES ?? "").split(",").map((phase) => phase.trim()).filter(Boolean));
|
|
122
|
+
const continuesFace = continuedPhases.size > 0;
|
|
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 ? [] : [
|
|
98
138
|
`${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
|
|
99
139
|
bar(),
|
|
100
|
-
`${bar()} ${identity.warm}`,
|
|
140
|
+
`${bar()} ${operation === "install" ? identity.installWarm : identity.warm}`,
|
|
101
141
|
bar()
|
|
102
142
|
];
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
143
|
+
const continues = (phase, kind = "ok") => {
|
|
144
|
+
const inherited = continuedPhases.delete(String(phase).trim());
|
|
145
|
+
return kind === "fail" ? false : inherited;
|
|
146
|
+
};
|
|
147
|
+
const step = (title, measure = null, kind = "ok") => {
|
|
148
|
+
if (emitMilestone(title, measure, kind)) return "";
|
|
149
|
+
const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
|
|
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;
|
|
106
152
|
const column = Math.min(44, Math.max(0, width - 8));
|
|
107
|
-
const reserved = time ? 6 : 0;
|
|
153
|
+
const reserved = time ? Math.max(6, visibleWidth(time) + 2) : 0;
|
|
108
154
|
const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
|
|
109
155
|
const head = `${bar()} ${glyph} ${first}`;
|
|
110
156
|
const pad = Math.max(1, column - visibleWidth(head));
|
|
111
157
|
return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
|
|
112
158
|
};
|
|
113
|
-
const relay = (text) => String(text).split("\n").
|
|
114
|
-
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 [];
|
|
115
162
|
const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
|
|
116
163
|
const line = String(raw);
|
|
117
164
|
if (visibleWidth(line) <= width - 6) return [line];
|
|
118
|
-
|
|
119
|
-
return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
|
|
165
|
+
return wrapWords(line, width - 6);
|
|
120
166
|
});
|
|
121
167
|
const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
|
|
122
168
|
const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
|
|
@@ -128,18 +174,361 @@ function createFace({ product, color = false, columns }) {
|
|
|
128
174
|
frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
|
|
129
175
|
];
|
|
130
176
|
};
|
|
131
|
-
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`)}`;
|
|
132
189
|
const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
|
|
133
|
-
return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
|
|
190
|
+
return { identity, width, nested, emitsProgress: progressFd !== null, welcome, continues, step, relay, receipt, outcome, signOff, refusal, paint };
|
|
134
191
|
}
|
|
192
|
+
|
|
193
|
+
// ../face/src/shell.ts
|
|
135
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";
|
|
136
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
|
+
}
|
|
137
303
|
var SPINNER_FRAMES = Object.freeze([...FRAMES]);
|
|
304
|
+
|
|
305
|
+
// ../face/src/conformance.ts
|
|
138
306
|
var ALLOWED = new Set(Object.values(GLYPH));
|
|
139
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"];
|
|
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];
|
|
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
|
+
return { ...facts };
|
|
329
|
+
}
|
|
330
|
+
function writeInstallerOutcome(path, value) {
|
|
331
|
+
writeFileSync(path, JSON.stringify(validateInstallerOutcome(value)), { encoding: "utf8", mode: 384 });
|
|
332
|
+
}
|
|
333
|
+
function readInstallerOutcome(path) {
|
|
334
|
+
let text;
|
|
335
|
+
try {
|
|
336
|
+
text = readFileSync(path, "utf8");
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error.code === "ENOENT") return void 0;
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
if (text.length > 16384) throw new Error("installer outcome: child result is too large");
|
|
342
|
+
try {
|
|
343
|
+
return validateInstallerOutcome(JSON.parse(text));
|
|
344
|
+
} catch {
|
|
345
|
+
throw new Error("installer outcome: invalid child result");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ../face/src/run.ts
|
|
350
|
+
function validateInstallerProduct(value) {
|
|
351
|
+
const fail = (field2) => {
|
|
352
|
+
throw new Error(`installer product: invalid ${field2}`);
|
|
353
|
+
};
|
|
354
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return fail("declaration");
|
|
355
|
+
const input = value;
|
|
356
|
+
const text = (value2, field2) => typeof value2 === "string" && value2.trim() && !/[\r\n\x00-\x1f]/u.test(value2) ? value2 : fail(field2);
|
|
357
|
+
const key = text(input.product, "product");
|
|
358
|
+
const product = { mmi: "mmi-hub", jerv: "jerv-hub" }[key] ?? key;
|
|
359
|
+
const identity = identityFor(product);
|
|
360
|
+
const gate = text(input.gate, "gate");
|
|
361
|
+
let gateUrl;
|
|
362
|
+
try {
|
|
363
|
+
gateUrl = new URL(gate);
|
|
364
|
+
} catch {
|
|
365
|
+
return fail("gate");
|
|
366
|
+
}
|
|
367
|
+
if (!["https:", "http:"].includes(gateUrl.protocol) || gateUrl.username || gateUrl.password) return fail("gate");
|
|
368
|
+
const doctor = text(input.doctor, "doctor");
|
|
369
|
+
if (doctor !== identity.doctor) return fail("doctor");
|
|
370
|
+
if (!Array.isArray(input.surfaces) || input.surfaces.length === 0) return fail("surfaces");
|
|
371
|
+
const ids = /* @__PURE__ */ new Set();
|
|
372
|
+
const surfaces = input.surfaces.map((raw) => {
|
|
373
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("surface");
|
|
374
|
+
const source = raw;
|
|
375
|
+
const id = text(source.id, "surface.id");
|
|
376
|
+
if (ids.has(id)) return fail("duplicate surface.id");
|
|
377
|
+
ids.add(id);
|
|
378
|
+
const surface = { id };
|
|
379
|
+
for (const field2 of ["npm", "bin", "kind", "activation"]) {
|
|
380
|
+
if (source[field2] !== void 0) surface[field2] = text(source[field2], `surface.${field2}`);
|
|
381
|
+
}
|
|
382
|
+
if (Boolean(surface.npm) !== Boolean(surface.bin)) return fail("surface npm/bin pair");
|
|
383
|
+
if (surface.kind !== void 0 && !["agent-home", "payload"].includes(surface.kind)) return fail("surface.kind");
|
|
384
|
+
if (!surface.npm && !surface.kind) return fail("surface implementation");
|
|
385
|
+
return surface;
|
|
386
|
+
});
|
|
387
|
+
return { product, gate, doctor, surfaces };
|
|
388
|
+
}
|
|
389
|
+
var PHASES = {
|
|
390
|
+
preflight: ["Checking prerequisites", "Checked prerequisites"],
|
|
391
|
+
resolve: ["Resolving the release", "Resolved the release"],
|
|
392
|
+
download: ["Downloading the payload", "Downloaded the payload"],
|
|
393
|
+
"sign-in": ["Signing in", "Signed in"],
|
|
394
|
+
check: ["Checking surfaces", "Checked surfaces"],
|
|
395
|
+
arm: ["Scheduling updates", "Armed hourly updates"],
|
|
396
|
+
"verify-release": ["Checking the release version", "Verified the release version"],
|
|
397
|
+
verify: ["Verifying the payload", "Verified the payload"],
|
|
398
|
+
install: ["Installing the product", "Installed the product"],
|
|
399
|
+
activate: ["Activating surfaces", "Activated surfaces"],
|
|
400
|
+
doctor: ["Checking health", "Checked health"]
|
|
401
|
+
};
|
|
402
|
+
function createInstallerRun(value, options = {}) {
|
|
403
|
+
const declaration = validateInstallerProduct(value);
|
|
404
|
+
const env = options.env ?? process.env;
|
|
405
|
+
const tty = options.tty ?? Boolean(process.stdout.isTTY);
|
|
406
|
+
const face = createFace({
|
|
407
|
+
operation: options.operation,
|
|
408
|
+
product: declaration.product,
|
|
409
|
+
columns: options.columns,
|
|
410
|
+
env,
|
|
411
|
+
color: tty && options.color !== false && env.NO_COLOR === void 0
|
|
412
|
+
});
|
|
413
|
+
const write = options.write ?? ((text, channel) => {
|
|
414
|
+
(channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
415
|
+
});
|
|
416
|
+
const emit = (text, channel = "stdout", recorded = text) => {
|
|
417
|
+
if (!text) return;
|
|
418
|
+
write(text, channel);
|
|
419
|
+
if (env.MM_FACE_TRANSCRIPT) {
|
|
420
|
+
appendFileSync2(env.MM_FACE_TRANSCRIPT, `${JSON.stringify({ channel, text: recorded })}
|
|
421
|
+
`, "utf8");
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const lines = (rows, channel = "stdout") => {
|
|
425
|
+
for (const row of rows.flatMap((row2) => row2.split("\n"))) if (row) emit(`${row}
|
|
426
|
+
`, channel);
|
|
427
|
+
};
|
|
428
|
+
const spinner = createSpinner(face, {
|
|
429
|
+
animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
|
|
430
|
+
...options.write ? { stream: { write: (text) => {
|
|
431
|
+
emit(String(text), "spinner");
|
|
432
|
+
return true;
|
|
433
|
+
} } } : { transcriptPath: env.MM_FACE_TRANSCRIPT }
|
|
434
|
+
});
|
|
435
|
+
let started = false;
|
|
436
|
+
let finished = false;
|
|
437
|
+
const start = () => {
|
|
438
|
+
if (started || finished) return;
|
|
439
|
+
started = true;
|
|
440
|
+
const welcome = face.welcome();
|
|
441
|
+
if (tty) lines(welcome);
|
|
442
|
+
else if (welcome.length) lines([`${face.identity.name} \u2014 Mutatis Mutandis`, options.operation === "install" ? face.identity.installWarm : face.identity.warm]);
|
|
443
|
+
};
|
|
444
|
+
const durable = (title, measure, kind) => {
|
|
445
|
+
spinner.stop();
|
|
446
|
+
const rendered = face.step(title, measure, kind);
|
|
447
|
+
if (rendered) lines([tty ? rendered : `${kind === "fail" ? "Failed: " : ""}${title}${measure === null ? "" : ` (${typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : measure})`}`]);
|
|
448
|
+
};
|
|
449
|
+
const run2 = {
|
|
450
|
+
start,
|
|
451
|
+
phase(id, facts = {}) {
|
|
452
|
+
if (finished) throw new Error("installer run already finished");
|
|
453
|
+
if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
|
|
454
|
+
start();
|
|
455
|
+
const state = facts.state ?? "ok";
|
|
456
|
+
const title = PHASES[id][state === "ok" ? 1 : 0];
|
|
457
|
+
if (state === "running") {
|
|
458
|
+
spinner.start(title, facts.measure ?? null);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
|
|
462
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
463
|
+
},
|
|
464
|
+
surface(facts) {
|
|
465
|
+
if (finished) throw new Error("installer run already finished");
|
|
466
|
+
const surface = declaration.surfaces.find((surface2) => surface2.id === facts.id);
|
|
467
|
+
if (!surface) throw new Error("installer run: undeclared surface");
|
|
468
|
+
start();
|
|
469
|
+
const versions = facts.to ? facts.from && facts.from !== facts.to ? ` ${facts.from} \u2192 ${facts.to}` : ` ${facts.to}` : "";
|
|
470
|
+
const status = { updated: options.dryRun ? "would update" : "updated", current: "already current", failed: "failed", skipped: "skipped", retry: "retrying", pending: "pending", kept: "kept" }[facts.state];
|
|
471
|
+
if (!status) throw new Error("installer run: unknown surface state");
|
|
472
|
+
const activation = facts.state === "updated" && surface.activation ? ` \xB7 ${surface.activation}` : "";
|
|
473
|
+
const completed = ["updated", "current", "kept", "skipped"].includes(facts.state);
|
|
474
|
+
durable(`${facts.id}${versions} \xB7 ${status}${activation}`, completed ? facts.seconds ?? null : null, facts.state === "failed" ? "fail" : facts.state === "updated" ? "ok" : "note");
|
|
475
|
+
if (facts.detail) run2.relay(facts.detail);
|
|
476
|
+
},
|
|
477
|
+
milestone({ step, state, ms }) {
|
|
478
|
+
start();
|
|
479
|
+
durable(step, ms === void 0 ? null : ms / 1e3, state);
|
|
480
|
+
},
|
|
481
|
+
signIn({ url, code }) {
|
|
482
|
+
start();
|
|
483
|
+
spinner.stop();
|
|
484
|
+
for (const text of [`Open ${url}`, `Enter code: ${code}`]) {
|
|
485
|
+
const rendered = `${tty ? face.relay(text) : text}
|
|
486
|
+
`;
|
|
487
|
+
emit(rendered, "stdout", rendered.replace(code, "[redacted]"));
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
// Only pass safe diagnostic text, never authentication output or credentials.
|
|
491
|
+
relay(text, channel = "stdout", record = true) {
|
|
492
|
+
spinner.stop();
|
|
493
|
+
const rendered = tty ? face.relay(text) : stripColor(text);
|
|
494
|
+
for (const row of rendered.split("\n")) if (row) {
|
|
495
|
+
emit(`${row}
|
|
496
|
+
`, channel, record ? `${row}
|
|
497
|
+
` : `${tty ? face.relay("[external output omitted]") : "[external output omitted]"}
|
|
498
|
+
`);
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
finish(facts) {
|
|
502
|
+
if (finished) return;
|
|
503
|
+
validateInstallerOutcome(facts);
|
|
504
|
+
if (env.MM_INSTALLER_OUTCOME_FILE) writeInstallerOutcome(env.MM_INSTALLER_OUTCOME_FILE, facts);
|
|
505
|
+
start();
|
|
506
|
+
spinner.stop();
|
|
507
|
+
finished = true;
|
|
508
|
+
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}.`;
|
|
509
|
+
const ready = facts.failed === 0 && !facts.dryRun && !facts.deferred && !facts.operationFailed && Boolean(facts.version);
|
|
510
|
+
const body = [
|
|
511
|
+
`${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."}`,
|
|
512
|
+
changed,
|
|
513
|
+
...facts.failed ? [`Failed ${facts.failed} of ${facts.total} surfaces.`] : [],
|
|
514
|
+
...facts.detail ? [facts.detail] : [],
|
|
515
|
+
facts.retry && facts.failed ? `Retry: ${facts.retry}` : `Check health any time: ${declaration.doctor}`
|
|
516
|
+
];
|
|
517
|
+
if (!face.nested || !env.MM_INSTALLER_OUTCOME_FILE && (facts.failed > 0 || facts.operationFailed)) {
|
|
518
|
+
lines(tty ? face.receipt(body, { ready }) : body.map((row) => row.replace(/^[✔✖●] /u, "")));
|
|
519
|
+
}
|
|
520
|
+
if (tty) lines([face.signOff()]);
|
|
521
|
+
},
|
|
522
|
+
stop() {
|
|
523
|
+
spinner.stop();
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
return run2;
|
|
527
|
+
}
|
|
528
|
+
|
|
140
529
|
// src/autoupdate.ts
|
|
141
530
|
import { spawnSync } from "node:child_process";
|
|
142
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
531
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
143
532
|
import { tmpdir } from "node:os";
|
|
144
533
|
import { join } from "node:path";
|
|
145
534
|
function schedulePlatform(override) {
|
|
@@ -188,7 +577,7 @@ function enableSchedule(config, command, options = {}) {
|
|
|
188
577
|
const dir2 = join(home, "Library", "LaunchAgents");
|
|
189
578
|
mkdirSync(dir2, { recursive: true });
|
|
190
579
|
const plist = join(dir2, `${label2}.plist`);
|
|
191
|
-
|
|
580
|
+
writeFileSync2(plist, darwinPlist(label2, command));
|
|
192
581
|
exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
|
|
193
582
|
const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
|
|
194
583
|
if (result2.code !== 0) {
|
|
@@ -199,8 +588,8 @@ function enableSchedule(config, command, options = {}) {
|
|
|
199
588
|
const label = scheduleLabel(config);
|
|
200
589
|
const dir = join(home, ".config", "systemd", "user");
|
|
201
590
|
mkdirSync(dir, { recursive: true });
|
|
202
|
-
|
|
203
|
-
|
|
591
|
+
writeFileSync2(join(dir, `${label}.service`), linuxService(command));
|
|
592
|
+
writeFileSync2(join(dir, `${label}.timer`), linuxTimer(label));
|
|
204
593
|
const reload = exec("systemctl", ["--user", "daemon-reload"]);
|
|
205
594
|
if (reload.code !== 0) {
|
|
206
595
|
throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
|
|
@@ -313,7 +702,7 @@ function firstLine(text) {
|
|
|
313
702
|
}
|
|
314
703
|
|
|
315
704
|
// src/config.ts
|
|
316
|
-
import { readFileSync as
|
|
705
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
317
706
|
import { getAsset } from "node:sea";
|
|
318
707
|
|
|
319
708
|
// src/module-url.ts
|
|
@@ -335,10 +724,10 @@ function loadProductConfig(options = {}) {
|
|
|
335
724
|
const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
|
|
336
725
|
const devFallback = new URL("../config/product.template.json", moduleUrl());
|
|
337
726
|
if (explicit) {
|
|
338
|
-
return parseProductConfig(
|
|
727
|
+
return parseProductConfig(readFileSync3(explicit, "utf8"));
|
|
339
728
|
}
|
|
340
729
|
try {
|
|
341
|
-
return parseProductConfig(
|
|
730
|
+
return parseProductConfig(readFileSync3(devFallback, "utf8"));
|
|
342
731
|
} catch {
|
|
343
732
|
}
|
|
344
733
|
try {
|
|
@@ -457,7 +846,8 @@ async function loginGithub(config, options = {}) {
|
|
|
457
846
|
const sleep = options.sleep ?? realSleep;
|
|
458
847
|
const issued = await requestDeviceCode(config, fetchImpl);
|
|
459
848
|
const openUrl = issued.verification_uri_complete ?? issued.verification_uri;
|
|
460
|
-
|
|
849
|
+
if (options.onDeviceCode) options.onDeviceCode({ url: issued.verification_uri, code: issued.user_code });
|
|
850
|
+
else print(`To sign in, open ${issued.verification_uri} and enter the code ${issued.user_code}.`);
|
|
461
851
|
open(openUrl);
|
|
462
852
|
const deadline = now() + issued.expires_in * 1e3;
|
|
463
853
|
let intervalMs = Math.max(1, issued.interval) * 1e3;
|
|
@@ -627,7 +1017,7 @@ function page(title, body) {
|
|
|
627
1017
|
|
|
628
1018
|
// src/payload.ts
|
|
629
1019
|
import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
|
|
630
|
-
import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as
|
|
1020
|
+
import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
631
1021
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
632
1022
|
import { dirname, join as join2 } from "node:path";
|
|
633
1023
|
|
|
@@ -779,7 +1169,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
779
1169
|
}
|
|
780
1170
|
const dest = join2(staging, entry.path);
|
|
781
1171
|
mkdirSync2(dirname(dest), { recursive: true });
|
|
782
|
-
|
|
1172
|
+
writeFileSync3(dest, bytes);
|
|
783
1173
|
}
|
|
784
1174
|
const target = join2(dir, "payload");
|
|
785
1175
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -793,7 +1183,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
|
|
|
793
1183
|
}
|
|
794
1184
|
|
|
795
1185
|
// src/store.ts
|
|
796
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
1186
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
797
1187
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
798
1188
|
import { join as join3 } from "node:path";
|
|
799
1189
|
function defaultProductDir(product) {
|
|
@@ -818,7 +1208,7 @@ function payloadDir(dir) {
|
|
|
818
1208
|
}
|
|
819
1209
|
function readTokens(dir) {
|
|
820
1210
|
try {
|
|
821
|
-
const data = JSON.parse(
|
|
1211
|
+
const data = JSON.parse(readFileSync4(tokensPath(dir), "utf8"));
|
|
822
1212
|
if (typeof data.accessToken !== "string" || !data.accessToken) return null;
|
|
823
1213
|
if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
|
|
824
1214
|
const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
|
|
@@ -832,10 +1222,10 @@ function readTokens(dir) {
|
|
|
832
1222
|
function writeTokens(dir, tokens) {
|
|
833
1223
|
mkdirSync3(dir, { recursive: true });
|
|
834
1224
|
try {
|
|
835
|
-
|
|
1225
|
+
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
836
1226
|
`, { mode: 384 });
|
|
837
1227
|
} catch {
|
|
838
|
-
|
|
1228
|
+
writeFileSync4(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
839
1229
|
`);
|
|
840
1230
|
}
|
|
841
1231
|
}
|
|
@@ -844,7 +1234,7 @@ function clearTokens(dir) {
|
|
|
844
1234
|
}
|
|
845
1235
|
function readState(dir) {
|
|
846
1236
|
try {
|
|
847
|
-
const data = JSON.parse(
|
|
1237
|
+
const data = JSON.parse(readFileSync4(statePath(dir), "utf8"));
|
|
848
1238
|
if (typeof data.version !== "string" || !data.version) return null;
|
|
849
1239
|
return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
|
|
850
1240
|
} catch {
|
|
@@ -853,7 +1243,7 @@ function readState(dir) {
|
|
|
853
1243
|
}
|
|
854
1244
|
function writeState(dir, state) {
|
|
855
1245
|
mkdirSync3(dir, { recursive: true });
|
|
856
|
-
|
|
1246
|
+
writeFileSync4(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
857
1247
|
`);
|
|
858
1248
|
}
|
|
859
1249
|
function wipeProductDir(dir) {
|
|
@@ -863,7 +1253,7 @@ function wipeProductDir(dir) {
|
|
|
863
1253
|
}
|
|
864
1254
|
|
|
865
1255
|
// src/index.ts
|
|
866
|
-
var LAUNCHER_VERSION = true ? "0.1.
|
|
1256
|
+
var LAUNCHER_VERSION = true ? "0.1.10" : readVersionFromPackage();
|
|
867
1257
|
function defaultPrint(message) {
|
|
868
1258
|
process.stdout.write(`${message}
|
|
869
1259
|
`);
|
|
@@ -999,11 +1389,16 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
|
|
|
999
1389
|
});
|
|
1000
1390
|
return refreshed.accessToken;
|
|
1001
1391
|
}
|
|
1002
|
-
async function doLogin(config, dir, options, print) {
|
|
1392
|
+
async function doLogin(config, dir, options, print, installer) {
|
|
1003
1393
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1004
1394
|
const open = options.open ?? openBrowser;
|
|
1005
1395
|
if (config.loginKind === "github") {
|
|
1006
|
-
const tokens = await loginGithub(config, {
|
|
1396
|
+
const tokens = await loginGithub(config, {
|
|
1397
|
+
fetchImpl,
|
|
1398
|
+
open,
|
|
1399
|
+
print,
|
|
1400
|
+
...installer ? { onDeviceCode: (prompt) => installer.signIn(prompt) } : {}
|
|
1401
|
+
});
|
|
1007
1402
|
writeTokens(dir, {
|
|
1008
1403
|
accessToken: tokens.accessToken,
|
|
1009
1404
|
refreshToken: tokens.refreshToken,
|
|
@@ -1018,20 +1413,20 @@ async function doLogin(config, dir, options, print) {
|
|
|
1018
1413
|
clientId: tokens.clientId
|
|
1019
1414
|
});
|
|
1020
1415
|
}
|
|
1021
|
-
print(`signed in to ${config.product}.`);
|
|
1416
|
+
if (!installer) print(`signed in to ${config.product}.`);
|
|
1022
1417
|
}
|
|
1023
|
-
async function fetchAccessTokenOrLogin(config, dir, options, print) {
|
|
1418
|
+
async function fetchAccessTokenOrLogin(config, dir, options, print, installer) {
|
|
1024
1419
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1025
1420
|
const fresh = await ensureFreshToken(config, dir, fetchImpl);
|
|
1026
1421
|
if (fresh) return fresh;
|
|
1027
|
-
await doLogin(config, dir, options, print);
|
|
1422
|
+
await doLogin(config, dir, options, print, installer);
|
|
1028
1423
|
const after = readTokens(dir);
|
|
1029
1424
|
if (!after) throw new Error("sign-in did not produce a token");
|
|
1030
1425
|
return after.accessToken;
|
|
1031
1426
|
}
|
|
1032
1427
|
function readPayloadArgv(dir, key) {
|
|
1033
1428
|
try {
|
|
1034
|
-
const parsed = JSON.parse(
|
|
1429
|
+
const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
|
|
1035
1430
|
const entry = parsed[key];
|
|
1036
1431
|
if (typeof entry === "string") {
|
|
1037
1432
|
const parts = entry.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1053,7 +1448,7 @@ function readPayloadRun(dir) {
|
|
|
1053
1448
|
}
|
|
1054
1449
|
function readPayloadVerbs(dir) {
|
|
1055
1450
|
try {
|
|
1056
|
-
const parsed = JSON.parse(
|
|
1451
|
+
const parsed = JSON.parse(readFileSync5(join4(payloadDir(dir), "payload.json"), "utf8"));
|
|
1057
1452
|
const verbs = parsed.verbs;
|
|
1058
1453
|
if (verbs === "*") return "*";
|
|
1059
1454
|
if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
|
|
@@ -1101,7 +1496,7 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1101
1496
|
const [command, ...args] = entry;
|
|
1102
1497
|
const shell = needsShell(command);
|
|
1103
1498
|
const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
|
|
1104
|
-
const
|
|
1499
|
+
const spawnEntry = (progress2) => spawnSync2(commandLine, shell ? [] : args, {
|
|
1105
1500
|
cwd,
|
|
1106
1501
|
stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
|
|
1107
1502
|
shell,
|
|
@@ -1110,12 +1505,12 @@ function defaultRunEntry(entry, cwd, env) {
|
|
|
1110
1505
|
});
|
|
1111
1506
|
let result;
|
|
1112
1507
|
if (process.platform === "win32" && shell) {
|
|
1113
|
-
result =
|
|
1508
|
+
result = spawnEntry(false);
|
|
1114
1509
|
} else {
|
|
1115
1510
|
try {
|
|
1116
|
-
result =
|
|
1511
|
+
result = spawnEntry(true);
|
|
1117
1512
|
} catch {
|
|
1118
|
-
result =
|
|
1513
|
+
result = spawnEntry(false);
|
|
1119
1514
|
}
|
|
1120
1515
|
}
|
|
1121
1516
|
if (result.error) return { ok: false, error: result.error.message };
|
|
@@ -1132,73 +1527,71 @@ function faceProduct(config) {
|
|
|
1132
1527
|
const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
|
|
1133
1528
|
return known[config.product] ?? config.product;
|
|
1134
1529
|
}
|
|
1135
|
-
function
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1530
|
+
async function installOrUpdate(config, dir, options, print, update) {
|
|
1531
|
+
const product = faceProduct(config);
|
|
1532
|
+
const installer = createInstallerRun({
|
|
1533
|
+
product,
|
|
1534
|
+
gate: config.host,
|
|
1535
|
+
doctor: identityFor(product).doctor,
|
|
1536
|
+
surfaces: [{ id: config.product, kind: "payload" }]
|
|
1537
|
+
}, {
|
|
1538
|
+
operation: update ? "update" : "install",
|
|
1539
|
+
tty: options.tty,
|
|
1540
|
+
animate: !options.print && Boolean(process.stderr.isTTY),
|
|
1541
|
+
write: (text, channel) => {
|
|
1542
|
+
if (options.print) print(text.replace(/\n$/, ""));
|
|
1543
|
+
else (channel === "stdout" ? process.stdout : process.stderr).write(text);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1547
|
+
let version = readState(dir)?.version ?? "unknown";
|
|
1548
|
+
installer.start();
|
|
1138
1549
|
try {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1550
|
+
let started = Date.now();
|
|
1551
|
+
installer.phase("sign-in", { state: "running" });
|
|
1552
|
+
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
|
|
1553
|
+
installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
|
|
1554
|
+
installer.phase("resolve", { state: "running" });
|
|
1555
|
+
started = Date.now();
|
|
1556
|
+
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1557
|
+
version = manifest.version;
|
|
1558
|
+
installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
|
|
1559
|
+
const current = readState(dir);
|
|
1560
|
+
const unchanged = update && current?.version === version && existsSync4(payloadDir(dir));
|
|
1561
|
+
if (!unchanged) {
|
|
1562
|
+
installer.phase("download", { state: "running" });
|
|
1563
|
+
started = Date.now();
|
|
1564
|
+
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1565
|
+
writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1566
|
+
installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
|
|
1567
|
+
}
|
|
1568
|
+
installer.surface({
|
|
1569
|
+
id: config.product,
|
|
1570
|
+
from: current?.version,
|
|
1571
|
+
to: version,
|
|
1572
|
+
state: unchanged ? "current" : "updated"
|
|
1143
1573
|
});
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (!face) {
|
|
1159
|
-
print(headline);
|
|
1160
|
-
for (const line of lines) print(line.trim());
|
|
1161
|
-
return;
|
|
1574
|
+
return await finishLastMile(config, dir, options, installer, version, unchanged);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
const code = error instanceof NeedsLoginError ? 3 : 1;
|
|
1577
|
+
installer.finish({
|
|
1578
|
+
version,
|
|
1579
|
+
total: 1,
|
|
1580
|
+
updated: 0,
|
|
1581
|
+
failed: 1,
|
|
1582
|
+
detail: error.message,
|
|
1583
|
+
retry: `${config.binName} ${update ? "update" : "install"}`
|
|
1584
|
+
});
|
|
1585
|
+
return code;
|
|
1586
|
+
} finally {
|
|
1587
|
+
installer.stop();
|
|
1162
1588
|
}
|
|
1163
|
-
const glyph = ready ? "\u2714" : "\u2716";
|
|
1164
|
-
for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
|
|
1165
|
-
print(face.signOff());
|
|
1166
|
-
}
|
|
1167
|
-
function printStep(face, print, title, seconds, kind = "ok") {
|
|
1168
|
-
print(face ? face.step(title, seconds, kind) : title);
|
|
1169
1589
|
}
|
|
1170
|
-
function
|
|
1171
|
-
|
|
1172
|
-
printStep(face, print, message.step, message.ms === void 0 ? null : message.ms / 1e3, message.state);
|
|
1173
|
-
}
|
|
1590
|
+
function doInstall(config, dir, options, print) {
|
|
1591
|
+
return installOrUpdate(config, dir, options, print, false);
|
|
1174
1592
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const face = faceFor(config, options);
|
|
1178
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1179
|
-
const started = Date.now();
|
|
1180
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1181
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1182
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1183
|
-
printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
|
|
1184
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1185
|
-
}
|
|
1186
|
-
async function doUpdate(config, dir, options, print) {
|
|
1187
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1188
|
-
const face = faceFor(config, options);
|
|
1189
|
-
if (face && !outerConsoleOwnsOutcome()) for (const line of face.welcome()) print(line);
|
|
1190
|
-
const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
|
|
1191
|
-
const started = Date.now();
|
|
1192
|
-
const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
|
|
1193
|
-
const current = readState(dir);
|
|
1194
|
-
if (current && current.version === manifest.version && existsSync4(payloadDir(dir))) {
|
|
1195
|
-
printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
|
|
1196
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1197
|
-
}
|
|
1198
|
-
await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
|
|
1199
|
-
writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1200
|
-
printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
|
|
1201
|
-
return finishLastMile(config, dir, options, print, face, manifest.version);
|
|
1593
|
+
function doUpdate(config, dir, options, print) {
|
|
1594
|
+
return installOrUpdate(config, dir, options, print, true);
|
|
1202
1595
|
}
|
|
1203
1596
|
function payloadEnv(dir) {
|
|
1204
1597
|
const stored = readTokens(dir);
|
|
@@ -1220,51 +1613,124 @@ function payloadFileAsCommand(entry, payload) {
|
|
|
1220
1613
|
return null;
|
|
1221
1614
|
}
|
|
1222
1615
|
}
|
|
1223
|
-
function finishLastMile(config, dir, options,
|
|
1616
|
+
async function finishLastMile(config, dir, options, installer, version, unchanged) {
|
|
1224
1617
|
const entry = readPayloadEntry(dir);
|
|
1225
1618
|
const payload = payloadDir(dir);
|
|
1226
1619
|
if (!entry) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1620
|
+
installer.finish({
|
|
1621
|
+
version,
|
|
1622
|
+
total: 1,
|
|
1623
|
+
updated: unchanged ? 0 : 1,
|
|
1624
|
+
failed: 0,
|
|
1625
|
+
installed: true,
|
|
1626
|
+
detail: `next step: run ${join4(payload, config.binName)} to start ${config.product}.`
|
|
1627
|
+
});
|
|
1231
1628
|
return 0;
|
|
1232
1629
|
}
|
|
1233
1630
|
const command = resolveEntry(entry);
|
|
1234
1631
|
const dataFile = payloadFileAsCommand(entry, payload);
|
|
1235
1632
|
if (dataFile) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1633
|
+
installer.finish({
|
|
1634
|
+
version,
|
|
1635
|
+
total: 1,
|
|
1636
|
+
updated: 0,
|
|
1637
|
+
failed: 1,
|
|
1638
|
+
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.`
|
|
1639
|
+
});
|
|
1242
1640
|
return 2;
|
|
1243
1641
|
}
|
|
1642
|
+
installer.phase("activate", { state: "running" });
|
|
1244
1643
|
const started = Date.now();
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1644
|
+
const env = { ...payloadEnv(dir) ?? process.env, MM_OUTER_CONSOLE: "1" };
|
|
1645
|
+
const result = options.runEntry ? options.runEntry(command, payload, env) : await runInstallEntry(command, payload, env, installer, readTokens(dir));
|
|
1646
|
+
for (const progress of result.progress ?? []) installer.milestone(progress);
|
|
1647
|
+
const succeeded = result.ok && (result.code === void 0 || result.code === 0);
|
|
1648
|
+
const outcome = result.outcome ? validateInstallerOutcome(result.outcome) : void 0;
|
|
1649
|
+
installer.phase("activate", { state: succeeded ? "ok" : "fail", seconds: (Date.now() - started) / 1e3 });
|
|
1650
|
+
installer.finish({
|
|
1651
|
+
...outcome ?? {
|
|
1652
|
+
version,
|
|
1653
|
+
total: 1,
|
|
1654
|
+
updated: succeeded && !unchanged ? 1 : 0,
|
|
1655
|
+
failed: succeeded ? 0 : 1,
|
|
1656
|
+
installed: true
|
|
1657
|
+
},
|
|
1658
|
+
...!succeeded ? {
|
|
1659
|
+
operationFailed: true,
|
|
1660
|
+
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}` : ""}`,
|
|
1661
|
+
retry: `(cd ${payload} && ${command.join(" ")})`
|
|
1662
|
+
} : {}
|
|
1663
|
+
});
|
|
1664
|
+
return succeeded ? outcome?.operationFailed || outcome?.failed ? 1 : 0 : result.code || 1;
|
|
1665
|
+
}
|
|
1666
|
+
async function runInstallEntry(entry, cwd, env, installer, tokens = null) {
|
|
1667
|
+
const [command, ...args] = entry;
|
|
1668
|
+
const shell = needsShell(command);
|
|
1669
|
+
const progress = !(process.platform === "win32" && shell);
|
|
1670
|
+
const outcomeDir = mkdtempSync(join4(tmpdir4(), "mm-installer-outcome-"));
|
|
1671
|
+
const outcomeFile = join4(outcomeDir, "outcome.json");
|
|
1672
|
+
const childEnv = { ...env, MM_INSTALLER_OUTCOME_FILE: outcomeFile };
|
|
1673
|
+
delete childEnv.MM_FACE_TRANSCRIPT;
|
|
1674
|
+
delete childEnv.MM_PROGRESS_FD;
|
|
1675
|
+
delete childEnv.MM_PROGRESS_PROTOCOL;
|
|
1676
|
+
if (progress) Object.assign(childEnv, { MM_PROGRESS_FD: "3", MM_PROGRESS_PROTOCOL: "1" });
|
|
1677
|
+
const secrets = [tokens?.accessToken, tokens?.refreshToken].filter((value) => Boolean(value));
|
|
1678
|
+
const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
|
|
1679
|
+
try {
|
|
1680
|
+
const result = await new Promise((resolve2) => {
|
|
1681
|
+
const child = spawn2(shell ? [command, ...args].map(quoteForShell).join(" ") : command, shell ? [] : args, {
|
|
1682
|
+
cwd,
|
|
1683
|
+
shell,
|
|
1684
|
+
windowsHide: true,
|
|
1685
|
+
env: childEnv,
|
|
1686
|
+
stdio: progress ? ["inherit", "pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1687
|
+
});
|
|
1688
|
+
for (const [stream, channel2] of [[child.stdout, "stdout"], [child.stderr, "stderr"]]) {
|
|
1689
|
+
let partial = "";
|
|
1690
|
+
stream?.setEncoding("utf8").on("data", (text) => {
|
|
1691
|
+
const safe = redact(partial + text);
|
|
1692
|
+
let held = 0;
|
|
1693
|
+
for (const secret of secrets) for (let length = 1; length < secret.length; length++) {
|
|
1694
|
+
if (safe.endsWith(secret.slice(0, length))) held = Math.max(held, length);
|
|
1695
|
+
}
|
|
1696
|
+
partial = held ? safe.slice(-held) : "";
|
|
1697
|
+
const visible = held ? safe.slice(0, -held) : safe;
|
|
1698
|
+
if (visible) installer.relay(visible, channel2, false);
|
|
1699
|
+
}).on("end", () => {
|
|
1700
|
+
if (partial) installer.relay("[redacted]", channel2, false);
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
let pending = "";
|
|
1704
|
+
const channel = child.stdio[3];
|
|
1705
|
+
if (channel && "setEncoding" in channel) {
|
|
1706
|
+
channel.setEncoding("utf8");
|
|
1707
|
+
channel.on("data", (text) => {
|
|
1708
|
+
pending += text;
|
|
1709
|
+
const end = pending.lastIndexOf("\n");
|
|
1710
|
+
if (end >= 0) {
|
|
1711
|
+
for (const record of parseProgress(pending.slice(0, end))) installer.milestone({ ...record, step: redact(record.step) });
|
|
1712
|
+
pending = pending.slice(end + 1);
|
|
1713
|
+
}
|
|
1714
|
+
if (pending.length > 65536) pending = "";
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
child.on("error", (error) => resolve2({ ok: false, error: error.message }));
|
|
1718
|
+
child.on("close", (code) => resolve2({
|
|
1719
|
+
ok: code === 0,
|
|
1720
|
+
...code !== null ? { code } : {},
|
|
1721
|
+
...code !== 0 ? { error: `exit code ${code}` } : {}
|
|
1722
|
+
}));
|
|
1723
|
+
});
|
|
1724
|
+
try {
|
|
1725
|
+
const outcome = readInstallerOutcome(outcomeFile);
|
|
1726
|
+
return { ...result, ...outcome ? { outcome } : {} };
|
|
1727
|
+
} catch {
|
|
1728
|
+
return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
|
|
1729
|
+
}
|
|
1730
|
+
} finally {
|
|
1731
|
+
if (existsSync4(outcomeFile)) unlinkSync(outcomeFile);
|
|
1732
|
+
rmdirSync(outcomeDir);
|
|
1254
1733
|
}
|
|
1255
|
-
const code = result.code ?? 1;
|
|
1256
|
-
printStep(
|
|
1257
|
-
face,
|
|
1258
|
-
print,
|
|
1259
|
-
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})`,
|
|
1260
|
-
null,
|
|
1261
|
-
"fail"
|
|
1262
|
-
);
|
|
1263
|
-
printReceipt(face, config, print, false, [
|
|
1264
|
-
`Downloaded ${version} into ${dir}`,
|
|
1265
|
-
`Finish it with: (cd ${payload} && ${command.join(" ")})`
|
|
1266
|
-
]);
|
|
1267
|
-
return code;
|
|
1268
1734
|
}
|
|
1269
1735
|
function doForward(config, dir, options, command, argv, print) {
|
|
1270
1736
|
if (command && existsSync4(command)) {
|
|
@@ -1387,5 +1853,6 @@ export {
|
|
|
1387
1853
|
readPayloadVerbs,
|
|
1388
1854
|
resolveEntry,
|
|
1389
1855
|
run,
|
|
1390
|
-
runFile
|
|
1856
|
+
runFile,
|
|
1857
|
+
runInstallEntry
|
|
1391
1858
|
};
|