@mutmutco/installer-launcher 0.1.2 → 0.1.4

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.
@@ -24,18 +24,326 @@ __export(index_exports, {
24
24
  defaultRunEntry: () => defaultRunEntry,
25
25
  ensureFreshToken: () => ensureFreshToken,
26
26
  readPayloadEntry: () => readPayloadEntry,
27
+ readPayloadRun: () => readPayloadRun,
28
+ readPayloadVerbs: () => readPayloadVerbs,
27
29
  resolveEntry: () => resolveEntry,
28
30
  run: () => run,
29
31
  runFile: () => runFile
30
32
  });
31
33
  module.exports = __toCommonJS(index_exports);
32
- var import_node_child_process2 = require("node:child_process");
33
- var import_node_fs4 = require("node:fs");
34
- var import_node_path3 = require("node:path");
34
+ var import_node_child_process3 = require("node:child_process");
35
+ var import_node_fs5 = require("node:fs");
36
+ var import_node_path4 = require("node:path");
35
37
  var import_node_url2 = require("node:url");
36
38
 
37
- // src/config.ts
39
+ // node_modules/@mutmutco/installer-face/dist/index.js
40
+ var PRODUCTS = Object.freeze({
41
+ "mm-strategy": Object.freeze({
42
+ name: "MM Strategy",
43
+ accent: "38;2;249;115;22",
44
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
45
+ }),
46
+ "mmi-hub": Object.freeze({
47
+ name: "mmi-hub",
48
+ accent: "38;2;125;211;252",
49
+ warm: "Welcome back. Checking your surfaces\u2026"
50
+ }),
51
+ "jerv-hub": Object.freeze({
52
+ name: "jerv-hub",
53
+ accent: "38;2;248;113;113",
54
+ warm: "Welcome back. Checking your surfaces\u2026"
55
+ }),
56
+ jervcode: Object.freeze({
57
+ name: "JervCode",
58
+ accent: "38;2;192;132;252",
59
+ warm: "Welcome back. Keeping JervCode current\u2026"
60
+ })
61
+ });
62
+ function identityFor(product) {
63
+ const identity = PRODUCTS[product];
64
+ if (!identity) {
65
+ throw new Error(`unknown product ${JSON.stringify(product)} \u2014 add it to installer/face/src/products.ts rather than composing a greeting locally (known: ${Object.keys(PRODUCTS).join(", ")})`);
66
+ }
67
+ return identity;
68
+ }
69
+ var GLYPH = Object.freeze({
70
+ diamond: "\u25C6",
71
+ hollow: "\u25C7",
72
+ bar: "\u2502",
73
+ check: "\u2714",
74
+ cross: "\u2716",
75
+ dot: "\u25CF",
76
+ boxTop: "\u256D",
77
+ boxTopEnd: "\u256E",
78
+ boxBottom: "\u2570",
79
+ boxBottomEnd: "\u256F",
80
+ rule: "\u2500"
81
+ });
82
+ var PALETTE = Object.freeze({
83
+ green: "38;2;31;209;138",
84
+ ink: "38;2;237;237;237",
85
+ muted: "38;2;150;150;150",
86
+ line: "38;2;42;42;42"
87
+ });
88
+ var TITLE_COLUMN = 6;
89
+ var ANSI = /\u001b\[[0-9;]*m/g;
90
+ function visibleWidth(text) {
91
+ return [...String(text).replace(ANSI, "")].length;
92
+ }
93
+ function faceWidth(columns) {
94
+ const raw = Number(columns);
95
+ return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
96
+ }
97
+ function wrapWords(text, width) {
98
+ const limit = Math.max(8, Math.floor(width));
99
+ const lines = [];
100
+ let line = "";
101
+ for (const word of String(text).split(/\s+/u).filter(Boolean)) {
102
+ let token = word;
103
+ while (visibleWidth(token) > limit) {
104
+ if (line) {
105
+ lines.push(line);
106
+ line = "";
107
+ }
108
+ lines.push([...token].slice(0, limit).join(""));
109
+ token = [...token].slice(limit).join("");
110
+ }
111
+ if (!line) line = token;
112
+ else if (visibleWidth(line) + 1 + visibleWidth(token) <= limit) line += ` ${token}`;
113
+ else {
114
+ lines.push(line);
115
+ line = token;
116
+ }
117
+ }
118
+ if (line) lines.push(line);
119
+ return lines.length ? lines : [""];
120
+ }
121
+ function createFace({ product, color = false, columns }) {
122
+ const identity = identityFor(product);
123
+ const width = faceWidth(columns);
124
+ const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
125
+ const bar = () => paint(PALETTE.muted, GLYPH.bar);
126
+ const indent = " ".repeat(TITLE_COLUMN - 1);
127
+ const welcome = () => [
128
+ `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
129
+ bar(),
130
+ `${bar()} ${identity.warm}`,
131
+ bar()
132
+ ];
133
+ const step = (title, seconds = null, kind = "ok") => {
134
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
135
+ const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
136
+ const column = Math.min(44, Math.max(0, width - 8));
137
+ const reserved = time ? 6 : 0;
138
+ const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
139
+ const head = `${bar()} ${glyph} ${first}`;
140
+ const pad = Math.max(1, column - visibleWidth(head));
141
+ return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
142
+ };
143
+ const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
144
+ const receipt = (lines) => {
145
+ const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
146
+ const line = String(raw);
147
+ if (visibleWidth(line) <= width - 6) return [line];
148
+ const lead = /^\s*/u.exec(line)?.[0] ?? "";
149
+ return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
150
+ });
151
+ const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
152
+ const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
153
+ const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
154
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
155
+ return [
156
+ frame(GLYPH.boxTop, GLYPH.boxTopEnd),
157
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
158
+ frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
159
+ ];
160
+ };
161
+ const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
162
+ const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
163
+ return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
164
+ }
165
+ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
166
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
167
+ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
168
+ var ALLOWED = new Set(Object.values(GLYPH));
169
+
170
+ // src/autoupdate.ts
171
+ var import_node_child_process = require("node:child_process");
38
172
  var import_node_fs = require("node:fs");
173
+ var import_node_os = require("node:os");
174
+ var import_node_path = require("node:path");
175
+ function schedulePlatform(override) {
176
+ const platform = override ?? process.platform;
177
+ if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
178
+ return null;
179
+ }
180
+ function defaultExec(command, args) {
181
+ const result = (0, import_node_child_process.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
182
+ if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
183
+ return {
184
+ code: typeof result.status === "number" ? result.status : 1,
185
+ stdout: String(result.stdout ?? ""),
186
+ stderr: String(result.stderr ?? "")
187
+ };
188
+ }
189
+ function homeOf(options) {
190
+ if (options.homeDir) return options.homeDir;
191
+ if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os.tmpdir)();
192
+ return process.env.HOME ?? (0, import_node_os.tmpdir)();
193
+ }
194
+ function scheduleName(config) {
195
+ return `${config.binName} autoupdate`;
196
+ }
197
+ function scheduleLabel(config) {
198
+ return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
199
+ }
200
+ function quoteWindows(arg) {
201
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
202
+ }
203
+ function enableSchedule(config, command, options = {}) {
204
+ const platform = schedulePlatform(options.platform);
205
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
206
+ const exec = options.exec ?? defaultExec;
207
+ const home = homeOf(options);
208
+ if (platform === "win32") {
209
+ const taskLine = command.map(quoteWindows).join(" ");
210
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
211
+ if (result2.code !== 0) {
212
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
213
+ }
214
+ return;
215
+ }
216
+ if (platform === "darwin") {
217
+ const label2 = scheduleLabel(config);
218
+ const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
219
+ (0, import_node_fs.mkdirSync)(dir2, { recursive: true });
220
+ const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
221
+ (0, import_node_fs.writeFileSync)(plist, darwinPlist(label2, command));
222
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
223
+ const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
224
+ if (result2.code !== 0) {
225
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
226
+ }
227
+ return;
228
+ }
229
+ const label = scheduleLabel(config);
230
+ const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
231
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
232
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
233
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
234
+ const reload = exec("systemctl", ["--user", "daemon-reload"]);
235
+ if (reload.code !== 0) {
236
+ throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
237
+ }
238
+ const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
239
+ if (result.code !== 0) {
240
+ throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
241
+ }
242
+ }
243
+ function disableSchedule(config, options = {}) {
244
+ const platform = schedulePlatform(options.platform);
245
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
246
+ const exec = options.exec ?? defaultExec;
247
+ const home = homeOf(options);
248
+ if (platform === "win32") {
249
+ const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
250
+ if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
251
+ throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
252
+ }
253
+ return;
254
+ }
255
+ if (platform === "darwin") {
256
+ const label2 = scheduleLabel(config);
257
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
258
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
259
+ return;
260
+ }
261
+ const label = scheduleLabel(config);
262
+ const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
263
+ exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
264
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
265
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
266
+ }
267
+ function querySchedule(config, options = {}) {
268
+ const platform = schedulePlatform(options.platform);
269
+ if (!platform) return { supported: false, enabled: false };
270
+ const exec = options.exec ?? defaultExec;
271
+ const home = homeOf(options);
272
+ if (platform === "win32") {
273
+ const result = exec("schtasks", ["/Query", "/TN", scheduleName(config), "/FO", "LIST", "/V"]);
274
+ if (result.code !== 0) return { supported: true, enabled: false };
275
+ const lastRun = valueOf(result.stdout, "Last Run Time:");
276
+ const nextRun = valueOf(result.stdout, "Next Run Time:");
277
+ const state2 = { supported: true, enabled: true, cadence: "hourly" };
278
+ if (lastRun && !/disabled|never|not run/i.test(lastRun)) state2.lastRun = lastRun;
279
+ else if (nextRun && !/disabled|never/i.test(nextRun)) state2.lastRun = `next run ${nextRun}`;
280
+ return state2;
281
+ }
282
+ if (platform === "darwin") {
283
+ const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
284
+ if (!(0, import_node_fs.existsSync)(plist)) return { supported: true, enabled: false };
285
+ return { supported: true, enabled: true, cadence: "hourly" };
286
+ }
287
+ const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
288
+ if (!(0, import_node_fs.existsSync)(timer)) return { supported: true, enabled: false };
289
+ const state = { supported: true, enabled: true, cadence: "hourly" };
290
+ const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
291
+ const stamp = (shown.stdout ?? "").trim();
292
+ if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
293
+ return state;
294
+ }
295
+ function darwinPlist(label, command) {
296
+ const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
297
+ return `<?xml version="1.0" encoding="UTF-8"?>
298
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
299
+ <plist version="1.0">
300
+ <dict>
301
+ <key>Label</key>
302
+ <string>${xmlEscape(label)}</string>
303
+ <key>ProgramArguments</key>
304
+ <array>
305
+ ${args}
306
+ </array>
307
+ <key>StartInterval</key>
308
+ <integer>3600</integer>
309
+ </dict>
310
+ </plist>
311
+ `;
312
+ }
313
+ function linuxService(command) {
314
+ const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
315
+ return `[Unit]
316
+ Description=${"Hourly update check"}
317
+ [Service]
318
+ Type=oneshot
319
+ ExecStart=${line}
320
+ `;
321
+ }
322
+ function linuxTimer(label) {
323
+ return `[Unit]
324
+ Description=Hourly update check for ${label}
325
+ [Timer]
326
+ OnCalendar=hourly
327
+ Persistent=true
328
+ [Install]
329
+ WantedBy=timers.target
330
+ `;
331
+ }
332
+ function xmlEscape(text) {
333
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
334
+ }
335
+ function valueOf(output, key) {
336
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
337
+ if (!line) return null;
338
+ const value = line.slice(line.indexOf(key) + key.length).trim();
339
+ return value ? value : null;
340
+ }
341
+ function firstLine(text) {
342
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
343
+ }
344
+
345
+ // src/config.ts
346
+ var import_node_fs2 = require("node:fs");
39
347
  var import_node_sea = require("node:sea");
40
348
 
41
349
  // src/module-url.ts
@@ -58,10 +366,10 @@ function loadProductConfig(options = {}) {
58
366
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
59
367
  const devFallback = new URL("../config/product.template.json", moduleUrl());
60
368
  if (explicit) {
61
- return parseProductConfig((0, import_node_fs.readFileSync)(explicit, "utf8"));
369
+ return parseProductConfig((0, import_node_fs2.readFileSync)(explicit, "utf8"));
62
370
  }
63
371
  try {
64
- return parseProductConfig((0, import_node_fs.readFileSync)(devFallback, "utf8"));
372
+ return parseProductConfig((0, import_node_fs2.readFileSync)(devFallback, "utf8"));
65
373
  } catch {
66
374
  }
67
375
  try {
@@ -116,14 +424,14 @@ function field(record, key) {
116
424
  }
117
425
 
118
426
  // src/login-github.ts
119
- var import_node_child_process = require("node:child_process");
427
+ var import_node_child_process2 = require("node:child_process");
120
428
  var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
121
429
  function openBrowser(url) {
122
430
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
123
431
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
124
432
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
125
433
  try {
126
- const child = (0, import_node_child_process.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
434
+ const child = (0, import_node_child_process2.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
127
435
  child.on("error", () => {
128
436
  });
129
437
  child.unref();
@@ -350,9 +658,9 @@ function page(title, body) {
350
658
 
351
659
  // src/payload.ts
352
660
  var import_node_crypto2 = require("node:crypto");
353
- var import_node_fs2 = require("node:fs");
354
- var import_node_os = require("node:os");
355
- var import_node_path = require("node:path");
661
+ var import_node_fs3 = require("node:fs");
662
+ var import_node_os2 = require("node:os");
663
+ var import_node_path2 = require("node:path");
356
664
 
357
665
  // src/canonical.ts
358
666
  function canonicalJson(value) {
@@ -492,56 +800,56 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
492
800
  }
493
801
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
494
802
  const fetchImpl = options.fetchImpl ?? fetch;
495
- const staging = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
496
- (0, import_node_fs2.mkdirSync)(staging, { recursive: true });
803
+ const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
804
+ (0, import_node_fs3.mkdirSync)(staging, { recursive: true });
497
805
  try {
498
806
  for (const entry of manifest.files) {
499
807
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
500
808
  if (!verifyFileBytes(entry, bytes)) {
501
809
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
502
810
  }
503
- const dest = (0, import_node_path.join)(staging, entry.path);
504
- (0, import_node_fs2.mkdirSync)((0, import_node_path.dirname)(dest), { recursive: true });
505
- (0, import_node_fs2.writeFileSync)(dest, bytes);
811
+ const dest = (0, import_node_path2.join)(staging, entry.path);
812
+ (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
813
+ (0, import_node_fs3.writeFileSync)(dest, bytes);
506
814
  }
507
- const target = (0, import_node_path.join)(dir, "payload");
508
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
509
- (0, import_node_fs2.rmSync)(target, { force: true, recursive: true });
510
- (0, import_node_fs2.renameSync)(staging, target);
815
+ const target = (0, import_node_path2.join)(dir, "payload");
816
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
817
+ (0, import_node_fs3.rmSync)(target, { force: true, recursive: true });
818
+ (0, import_node_fs3.renameSync)(staging, target);
511
819
  } catch (error) {
512
- (0, import_node_fs2.rmSync)(staging, { force: true, recursive: true });
820
+ (0, import_node_fs3.rmSync)(staging, { force: true, recursive: true });
513
821
  throw error;
514
822
  }
515
823
  return manifest.version;
516
824
  }
517
825
 
518
826
  // src/store.ts
519
- var import_node_fs3 = require("node:fs");
520
- var import_node_os2 = require("node:os");
521
- var import_node_path2 = require("node:path");
827
+ var import_node_fs4 = require("node:fs");
828
+ var import_node_os3 = require("node:os");
829
+ var import_node_path3 = require("node:path");
522
830
  function defaultProductDir(product) {
523
831
  if (process.platform === "win32") {
524
- const base = process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "launcher-fallback");
525
- return (0, import_node_path2.join)(base, product);
832
+ const base = process.env.LOCALAPPDATA ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "launcher-fallback");
833
+ return (0, import_node_path3.join)(base, product);
526
834
  }
527
- const home = process.env.HOME ?? (0, import_node_os2.tmpdir)();
528
- return (0, import_node_path2.join)(home, `.${product}`);
835
+ const home = process.env.HOME ?? (0, import_node_os3.tmpdir)();
836
+ return (0, import_node_path3.join)(home, `.${product}`);
529
837
  }
530
838
  function resolveProductDir(product, explicit) {
531
839
  return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
532
840
  }
533
841
  function tokensPath(dir) {
534
- return (0, import_node_path2.join)(dir, "tokens.json");
842
+ return (0, import_node_path3.join)(dir, "tokens.json");
535
843
  }
536
844
  function statePath(dir) {
537
- return (0, import_node_path2.join)(dir, "state.json");
845
+ return (0, import_node_path3.join)(dir, "state.json");
538
846
  }
539
847
  function payloadDir(dir) {
540
- return (0, import_node_path2.join)(dir, "payload");
848
+ return (0, import_node_path3.join)(dir, "payload");
541
849
  }
542
850
  function readTokens(dir) {
543
851
  try {
544
- const data = JSON.parse((0, import_node_fs3.readFileSync)(tokensPath(dir), "utf8"));
852
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(tokensPath(dir), "utf8"));
545
853
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
546
854
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
547
855
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -553,21 +861,21 @@ function readTokens(dir) {
553
861
  }
554
862
  }
555
863
  function writeTokens(dir, tokens) {
556
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
864
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
557
865
  try {
558
- (0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
866
+ (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
559
867
  `, { mode: 384 });
560
868
  } catch {
561
- (0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
869
+ (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
562
870
  `);
563
871
  }
564
872
  }
565
873
  function clearTokens(dir) {
566
- (0, import_node_fs3.rmSync)(tokensPath(dir), { force: true });
874
+ (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
567
875
  }
568
876
  function readState(dir) {
569
877
  try {
570
- const data = JSON.parse((0, import_node_fs3.readFileSync)(statePath(dir), "utf8"));
878
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(statePath(dir), "utf8"));
571
879
  if (typeof data.version !== "string" || !data.version) return null;
572
880
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
573
881
  } catch {
@@ -575,18 +883,18 @@ function readState(dir) {
575
883
  }
576
884
  }
577
885
  function writeState(dir, state) {
578
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
579
- (0, import_node_fs3.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
886
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
887
+ (0, import_node_fs4.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
580
888
  `);
581
889
  }
582
890
  function wipeProductDir(dir) {
583
- (0, import_node_fs3.rmSync)(tokensPath(dir), { force: true });
584
- (0, import_node_fs3.rmSync)(statePath(dir), { force: true });
585
- (0, import_node_fs3.rmSync)(payloadDir(dir), { force: true, recursive: true });
891
+ (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
892
+ (0, import_node_fs4.rmSync)(statePath(dir), { force: true });
893
+ (0, import_node_fs4.rmSync)(payloadDir(dir), { force: true, recursive: true });
586
894
  }
587
895
 
588
896
  // src/index.ts
589
- var LAUNCHER_VERSION = true ? "0.1.2" : readVersionFromPackage();
897
+ var LAUNCHER_VERSION = true ? "0.1.4" : readVersionFromPackage();
590
898
  function defaultPrint(message) {
591
899
  process.stdout.write(`${message}
592
900
  `);
@@ -633,12 +941,11 @@ async function run(rawOptions = {}) {
633
941
  case "update":
634
942
  return await doUpdate(config, dir, rawOptions, print);
635
943
  case "doctor":
636
- doDoctor(config, dir, print);
637
- return 0;
944
+ return doDoctor(config, dir, rawOptions, print);
945
+ case "autoupdate":
946
+ return doAutoupdate(config, rawOptions, positional.slice(1), print);
638
947
  default:
639
- print(`unknown command: ${command}`);
640
- printUsage(config, print);
641
- return 2;
948
+ return doForward(config, dir, rawOptions, command, argv, print);
642
949
  }
643
950
  } catch (error) {
644
951
  if (error instanceof NeedsLoginError) {
@@ -657,16 +964,17 @@ function flagValue(argv, flag) {
657
964
  }
658
965
  function printUsage(config, print) {
659
966
  print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
660
- print("usage: launcher <login|logout|install|update|doctor> [--config <path>] [--dir <path>]");
967
+ print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
661
968
  print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
969
+ print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
662
970
  }
663
971
  async function runFile(file, args, printErr) {
664
972
  if (!file) {
665
973
  printErr("launcher --run needs a file to run.");
666
974
  return 1;
667
975
  }
668
- const abs = (0, import_node_path3.resolve)(process.cwd(), file);
669
- if (!(0, import_node_fs4.existsSync)(abs)) {
976
+ const abs = (0, import_node_path4.resolve)(process.cwd(), file);
977
+ if (!(0, import_node_fs5.existsSync)(abs)) {
670
978
  printErr(`cannot run ${file}: no such file.`);
671
979
  return 1;
672
980
  }
@@ -752,10 +1060,10 @@ async function fetchAccessTokenOrLogin(config, dir, options, print) {
752
1060
  if (!after) throw new Error("sign-in did not produce a token");
753
1061
  return after.accessToken;
754
1062
  }
755
- function readPayloadEntry(dir) {
1063
+ function readPayloadArgv(dir, key) {
756
1064
  try {
757
- const parsed = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path3.join)(payloadDir(dir), "payload.json"), "utf8"));
758
- const entry = parsed.entry;
1065
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1066
+ const entry = parsed[key];
759
1067
  if (typeof entry === "string") {
760
1068
  const parts = entry.trim().split(/\s+/).filter(Boolean);
761
1069
  return parts.length > 0 ? parts : null;
@@ -768,67 +1076,251 @@ function readPayloadEntry(dir) {
768
1076
  return null;
769
1077
  }
770
1078
  }
1079
+ function readPayloadEntry(dir) {
1080
+ return readPayloadArgv(dir, "entry");
1081
+ }
1082
+ function readPayloadRun(dir) {
1083
+ return readPayloadArgv(dir, "run");
1084
+ }
1085
+ function readPayloadVerbs(dir) {
1086
+ try {
1087
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1088
+ const verbs = parsed.verbs;
1089
+ if (verbs === "*") return "*";
1090
+ if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
1091
+ return verbs;
1092
+ }
1093
+ return null;
1094
+ } catch {
1095
+ return null;
1096
+ }
1097
+ }
771
1098
  function resolveEntry(entry) {
772
1099
  return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
773
1100
  }
774
1101
  function needsShell(command) {
775
1102
  if (process.platform !== "win32") return false;
776
1103
  if (/\.(cmd|bat)$/i.test(command)) return true;
777
- return !(0, import_node_fs4.existsSync)(command);
1104
+ return !(0, import_node_fs5.existsSync)(command);
1105
+ }
1106
+ function quoteForShell(arg) {
1107
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
778
1108
  }
779
- function defaultRunEntry(entry, cwd) {
1109
+ function defaultRunEntry(entry, cwd, env) {
780
1110
  const [command, ...args] = entry;
781
- const result = (0, import_node_child_process2.spawnSync)(command, args, {
1111
+ const shell = needsShell(command);
1112
+ const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1113
+ const result = (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
782
1114
  cwd,
783
1115
  stdio: "inherit",
784
- shell: needsShell(command),
1116
+ shell,
1117
+ env: env ?? process.env,
785
1118
  windowsHide: true
786
1119
  });
787
1120
  if (result.error) return { ok: false, error: result.error.message };
788
- return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}` };
1121
+ const code = typeof result.status === "number" ? result.status : void 0;
1122
+ return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1123
+ }
1124
+ function faceProduct(config) {
1125
+ const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1126
+ return known[config.product] ?? config.product;
1127
+ }
1128
+ function faceFor(config, options) {
1129
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
1130
+ if (!tty) return null;
1131
+ try {
1132
+ return createFace({
1133
+ product: faceProduct(config),
1134
+ color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1135
+ columns: process.stdout.columns
1136
+ });
1137
+ } catch {
1138
+ return null;
1139
+ }
1140
+ }
1141
+ function since(start) {
1142
+ return (Date.now() - start) / 1e3;
1143
+ }
1144
+ function printReceipt(face, config, print, ready, lines) {
1145
+ const name = face ? face.identity.name : config.product;
1146
+ const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1147
+ if (!face) {
1148
+ print(headline);
1149
+ for (const line of lines) print(line.trim());
1150
+ return;
1151
+ }
1152
+ const glyph = ready ? "\u2714" : "\u2716";
1153
+ for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1154
+ print(face.signOff());
1155
+ }
1156
+ function printStep(face, print, title, seconds, kind = "ok") {
1157
+ print(face ? face.step(title, seconds, kind) : title);
789
1158
  }
790
1159
  async function doInstall(config, dir, options, print) {
791
1160
  const fetchImpl = options.fetchImpl ?? fetch;
1161
+ const face = faceFor(config, options);
1162
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1163
+ const started = Date.now();
1164
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1165
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1166
+ writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1167
+ printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1168
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1169
+ }
1170
+ async function doUpdate(config, dir, options, print) {
1171
+ const fetchImpl = options.fetchImpl ?? fetch;
1172
+ const face = faceFor(config, options);
1173
+ if (face) for (const line of face.welcome()) print(line);
792
1174
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1175
+ const started = Date.now();
793
1176
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1177
+ const current = readState(dir);
1178
+ if (current && current.version === manifest.version && (0, import_node_fs5.existsSync)(payloadDir(dir))) {
1179
+ printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1180
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1181
+ }
794
1182
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
795
1183
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
796
- print(`installed ${config.product} ${manifest.version} into ${dir}.`);
1184
+ printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1185
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1186
+ }
1187
+ function payloadEnv(dir) {
1188
+ const stored = readTokens(dir);
1189
+ if (!stored?.accessToken) return void 0;
1190
+ return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1191
+ }
1192
+ var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1193
+ function payloadFileAsCommand(entry, payload) {
1194
+ const first = entry[0];
1195
+ if (!first || first === "$self") return null;
1196
+ if (first.includes("/") || first.includes("\\")) return null;
1197
+ const candidate = (0, import_node_path4.join)(payload, first);
1198
+ if (!(0, import_node_fs5.existsSync)(candidate)) return null;
1199
+ if (NEVER_A_PROGRAM.test(first)) return first;
1200
+ if (process.platform === "win32") return null;
1201
+ try {
1202
+ return ((0, import_node_fs5.statSync)(candidate).mode & 73) === 0 ? first : null;
1203
+ } catch {
1204
+ return null;
1205
+ }
1206
+ }
1207
+ function finishLastMile(config, dir, options, print, face, version) {
797
1208
  const entry = readPayloadEntry(dir);
1209
+ const payload = payloadDir(dir);
798
1210
  if (!entry) {
799
- print(`next step: run ${(0, import_node_path3.join)(payloadDir(dir), config.binName)} to start ${config.product}.`);
1211
+ printReceipt(face, config, print, true, [
1212
+ ` Installed ${version} into ${dir}`,
1213
+ ` next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1214
+ ]);
800
1215
  return 0;
801
1216
  }
802
- const payload = payloadDir(dir);
803
1217
  const command = resolveEntry(entry);
804
- const runEntry = options.runEntry ?? defaultRunEntry;
805
- const result = runEntry(command, payload);
1218
+ const dataFile = payloadFileAsCommand(entry, payload);
1219
+ if (dataFile) {
1220
+ printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1221
+ printReceipt(face, config, print, false, [
1222
+ ` Downloaded ${version} into ${dir}`,
1223
+ ` This payload was built wrong: its entry must be a command, not one of its own files.`,
1224
+ ` Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1225
+ ]);
1226
+ return 2;
1227
+ }
1228
+ const started = Date.now();
1229
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
806
1230
  if (result.ok) {
807
- print(`${config.product} is ready.`);
1231
+ printStep(face, print, "Armed this machine", since(started));
1232
+ printReceipt(face, config, print, true, [
1233
+ ` Installed ${version} into ${dir}`,
1234
+ ` Check health any time: ${config.binName} doctor`
1235
+ ]);
808
1236
  return 0;
809
1237
  }
810
- print(`could not finish the last mile automatically${result.error ? ` (${result.error})` : ""}. Run it yourself:`);
811
- print(` (cd ${payload} && ${command.join(" ")})`);
812
- return 0;
1238
+ const code = result.code ?? 1;
1239
+ printStep(
1240
+ face,
1241
+ print,
1242
+ 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})`,
1243
+ null,
1244
+ "fail"
1245
+ );
1246
+ printReceipt(face, config, print, false, [
1247
+ ` Downloaded ${version} into ${dir}`,
1248
+ ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1249
+ ]);
1250
+ return code;
813
1251
  }
814
- async function doUpdate(config, dir, options, print) {
815
- const fetchImpl = options.fetchImpl ?? fetch;
816
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
817
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
818
- const current = readState(dir);
819
- if (current && current.version === manifest.version && (0, import_node_fs4.existsSync)(payloadDir(dir))) {
820
- print(`${config.product} is already up to date at ${manifest.version}.`);
1252
+ function doForward(config, dir, options, command, argv, print) {
1253
+ if (command && (0, import_node_fs5.existsSync)(command)) {
1254
+ print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1255
+ return 2;
1256
+ }
1257
+ const target = readPayloadRun(dir);
1258
+ const verbs = readPayloadVerbs(dir);
1259
+ const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
1260
+ if (!target || !declared) {
1261
+ print(`unknown command: ${command}`);
1262
+ printUsage(config, print);
1263
+ return 2;
1264
+ }
1265
+ const forwarded = [...resolveEntry(target), ...argv];
1266
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
1267
+ if (!result.ok && result.code === void 0) {
1268
+ print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1269
+ return 1;
1270
+ }
1271
+ return result.code ?? 0;
1272
+ }
1273
+ function doAutoupdate(config, options, args, print) {
1274
+ const mode = args[0] ?? "status";
1275
+ const scheduleOptions = options.autoupdate ?? {};
1276
+ const command = options.autoupdate?.command ?? [process.execPath, "update"];
1277
+ if (mode === "on") {
1278
+ try {
1279
+ enableSchedule(config, command, scheduleOptions);
1280
+ } catch (error) {
1281
+ print(error.message);
1282
+ return 1;
1283
+ }
1284
+ const state2 = querySchedule(config, scheduleOptions);
1285
+ print(`auto-update: on (${state2.cadence ?? "hourly"})`);
821
1286
  return 0;
822
1287
  }
823
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
824
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
825
- print(`updated ${config.product} to ${manifest.version}.`);
1288
+ if (mode === "off") {
1289
+ try {
1290
+ disableSchedule(config, scheduleOptions);
1291
+ } catch (error) {
1292
+ print(error.message);
1293
+ return 1;
1294
+ }
1295
+ print("auto-update: off");
1296
+ return 0;
1297
+ }
1298
+ if (mode !== "status") {
1299
+ print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
1300
+ return 1;
1301
+ }
1302
+ const state = querySchedule(config, scheduleOptions);
1303
+ if (!state.supported) {
1304
+ print("auto-update: unsupported on this platform");
1305
+ return 1;
1306
+ }
1307
+ print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
1308
+ if (state.lastRun) print(`last run: ${state.lastRun}`);
826
1309
  return 0;
827
1310
  }
828
- function doDoctor(config, dir, print) {
1311
+ function doDoctor(config, dir, options, print) {
1312
+ doLauncherDoctor(config, dir, options, print);
1313
+ const target = readPayloadRun(dir);
1314
+ const verbs = readPayloadVerbs(dir);
1315
+ const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1316
+ if (!chains) return 0;
1317
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
1318
+ return result.code ?? (result.ok ? 0 : 1);
1319
+ }
1320
+ function doLauncherDoctor(config, dir, options, print) {
829
1321
  const tokens = readTokens(dir);
830
1322
  const state = readState(dir);
831
- const payloadPresent = (0, import_node_fs4.existsSync)(payloadDir(dir));
1323
+ const payloadPresent = (0, import_node_fs5.existsSync)(payloadDir(dir));
832
1324
  print(`product: ${config.product}`);
833
1325
  print(`host: ${config.host}`);
834
1326
  print(`login: ${config.loginKind}`);
@@ -840,7 +1332,11 @@ function doDoctor(config, dir, print) {
840
1332
  print("token: none \u2014 run login first.");
841
1333
  }
842
1334
  print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
843
- print(`paths: tokens ${(0, import_node_path3.join)(dir, "tokens.json")}, state ${(0, import_node_path3.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
1335
+ print(`paths: tokens ${(0, import_node_path4.join)(dir, "tokens.json")}, state ${(0, import_node_path4.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
1336
+ const schedule = querySchedule(config, options.autoupdate ?? {});
1337
+ print(
1338
+ `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
1339
+ );
844
1340
  }
845
1341
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
846
1342
  try {
@@ -871,6 +1367,8 @@ if (invokedAsMain) {
871
1367
  defaultRunEntry,
872
1368
  ensureFreshToken,
873
1369
  readPayloadEntry,
1370
+ readPayloadRun,
1371
+ readPayloadVerbs,
874
1372
  resolveEntry,
875
1373
  run,
876
1374
  runFile