@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.
package/dist/launcher.js CHANGED
@@ -1,13 +1,319 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { spawnSync } from "node:child_process";
5
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
6
- import { dirname as dirname2, join as join3, resolve } from "node:path";
4
+ import { spawnSync as spawnSync2 } from "node:child_process";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "node:fs";
6
+ import { dirname as dirname2, join as join4, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
8
8
 
9
+ // node_modules/@mutmutco/installer-face/dist/index.js
10
+ var PRODUCTS = Object.freeze({
11
+ "mm-strategy": Object.freeze({
12
+ name: "MM Strategy",
13
+ accent: "38;2;249;115;22",
14
+ warm: "Welcome. Let's set up MM Strategy \u2014 about a minute."
15
+ }),
16
+ "mmi-hub": Object.freeze({
17
+ name: "mmi-hub",
18
+ accent: "38;2;125;211;252",
19
+ warm: "Welcome back. Checking your surfaces\u2026"
20
+ }),
21
+ "jerv-hub": Object.freeze({
22
+ name: "jerv-hub",
23
+ accent: "38;2;248;113;113",
24
+ warm: "Welcome back. Checking your surfaces\u2026"
25
+ }),
26
+ jervcode: Object.freeze({
27
+ name: "JervCode",
28
+ accent: "38;2;192;132;252",
29
+ warm: "Welcome back. Keeping JervCode current\u2026"
30
+ })
31
+ });
32
+ function identityFor(product) {
33
+ const identity = PRODUCTS[product];
34
+ if (!identity) {
35
+ 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(", ")})`);
36
+ }
37
+ return identity;
38
+ }
39
+ var GLYPH = Object.freeze({
40
+ diamond: "\u25C6",
41
+ hollow: "\u25C7",
42
+ bar: "\u2502",
43
+ check: "\u2714",
44
+ cross: "\u2716",
45
+ dot: "\u25CF",
46
+ boxTop: "\u256D",
47
+ boxTopEnd: "\u256E",
48
+ boxBottom: "\u2570",
49
+ boxBottomEnd: "\u256F",
50
+ rule: "\u2500"
51
+ });
52
+ var PALETTE = Object.freeze({
53
+ green: "38;2;31;209;138",
54
+ ink: "38;2;237;237;237",
55
+ muted: "38;2;150;150;150",
56
+ line: "38;2;42;42;42"
57
+ });
58
+ var TITLE_COLUMN = 6;
59
+ var ANSI = /\u001b\[[0-9;]*m/g;
60
+ function visibleWidth(text) {
61
+ return [...String(text).replace(ANSI, "")].length;
62
+ }
63
+ function faceWidth(columns) {
64
+ const raw = Number(columns);
65
+ return Math.max(40, Math.min(100, Number.isFinite(raw) && raw > 0 ? raw : 100));
66
+ }
67
+ function wrapWords(text, width) {
68
+ const limit = Math.max(8, Math.floor(width));
69
+ const lines = [];
70
+ let line = "";
71
+ for (const word of String(text).split(/\s+/u).filter(Boolean)) {
72
+ let token = word;
73
+ while (visibleWidth(token) > limit) {
74
+ if (line) {
75
+ lines.push(line);
76
+ line = "";
77
+ }
78
+ lines.push([...token].slice(0, limit).join(""));
79
+ token = [...token].slice(limit).join("");
80
+ }
81
+ if (!line) line = token;
82
+ else if (visibleWidth(line) + 1 + visibleWidth(token) <= limit) line += ` ${token}`;
83
+ else {
84
+ lines.push(line);
85
+ line = token;
86
+ }
87
+ }
88
+ if (line) lines.push(line);
89
+ return lines.length ? lines : [""];
90
+ }
91
+ function createFace({ product, color = false, columns }) {
92
+ const identity = identityFor(product);
93
+ const width = faceWidth(columns);
94
+ const paint = (sgr, text) => color ? `\x1B[${sgr}m${text}\x1B[0m` : String(text);
95
+ const bar = () => paint(PALETTE.muted, GLYPH.bar);
96
+ const indent = " ".repeat(TITLE_COLUMN - 1);
97
+ const welcome = () => [
98
+ `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
99
+ bar(),
100
+ `${bar()} ${identity.warm}`,
101
+ bar()
102
+ ];
103
+ const step = (title, seconds = null, kind = "ok") => {
104
+ const glyph = kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.hollow) : paint(PALETTE.green, GLYPH.check);
105
+ const time = seconds === null || seconds === void 0 ? "" : paint(PALETTE.muted, `${Math.max(0, Math.round(seconds))}s`);
106
+ const column = Math.min(44, Math.max(0, width - 8));
107
+ const reserved = time ? 6 : 0;
108
+ const [first, ...rest] = wrapWords(title, width - TITLE_COLUMN - reserved);
109
+ const head = `${bar()} ${glyph} ${first}`;
110
+ const pad = Math.max(1, column - visibleWidth(head));
111
+ return [time ? `${head}${" ".repeat(pad)}${time}` : head, ...rest.map((line) => `${bar()}${indent}${line}`)].join("\n");
112
+ };
113
+ const relay = (text) => String(text).split("\n").map((line) => line.trim() === "" ? bar() : `${bar()}${indent}${line}`).join("\n");
114
+ const receipt = (lines) => {
115
+ const body = (Array.isArray(lines) ? lines : String(lines).split("\n")).flatMap((raw) => {
116
+ const line = String(raw);
117
+ if (visibleWidth(line) <= width - 6) return [line];
118
+ const lead = /^\s*/u.exec(line)?.[0] ?? "";
119
+ return wrapWords(line, width - 6 - lead.length).map((part) => `${lead}${part}`);
120
+ });
121
+ const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
122
+ const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
123
+ const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
124
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
125
+ return [
126
+ frame(GLYPH.boxTop, GLYPH.boxTopEnd),
127
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
128
+ frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
129
+ ];
130
+ };
131
+ const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
132
+ const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
133
+ return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
134
+ }
135
+ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
136
+ var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
137
+ var SPINNER_FRAMES = Object.freeze([...FRAMES]);
138
+ var ALLOWED = new Set(Object.values(GLYPH));
139
+
140
+ // src/autoupdate.ts
141
+ import { spawnSync } from "node:child_process";
142
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
143
+ import { tmpdir } from "node:os";
144
+ import { join } from "node:path";
145
+ function schedulePlatform(override) {
146
+ const platform = override ?? process.platform;
147
+ if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
148
+ return null;
149
+ }
150
+ function defaultExec(command, args) {
151
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
152
+ if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
153
+ return {
154
+ code: typeof result.status === "number" ? result.status : 1,
155
+ stdout: String(result.stdout ?? ""),
156
+ stderr: String(result.stderr ?? "")
157
+ };
158
+ }
159
+ function homeOf(options) {
160
+ if (options.homeDir) return options.homeDir;
161
+ if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir();
162
+ return process.env.HOME ?? tmpdir();
163
+ }
164
+ function scheduleName(config) {
165
+ return `${config.binName} autoupdate`;
166
+ }
167
+ function scheduleLabel(config) {
168
+ return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
169
+ }
170
+ function quoteWindows(arg) {
171
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
172
+ }
173
+ function enableSchedule(config, command, options = {}) {
174
+ const platform = schedulePlatform(options.platform);
175
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
176
+ const exec = options.exec ?? defaultExec;
177
+ const home = homeOf(options);
178
+ if (platform === "win32") {
179
+ const taskLine = command.map(quoteWindows).join(" ");
180
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
181
+ if (result2.code !== 0) {
182
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
183
+ }
184
+ return;
185
+ }
186
+ if (platform === "darwin") {
187
+ const label2 = scheduleLabel(config);
188
+ const dir2 = join(home, "Library", "LaunchAgents");
189
+ mkdirSync(dir2, { recursive: true });
190
+ const plist = join(dir2, `${label2}.plist`);
191
+ writeFileSync(plist, darwinPlist(label2, command));
192
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
193
+ const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
194
+ if (result2.code !== 0) {
195
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
196
+ }
197
+ return;
198
+ }
199
+ const label = scheduleLabel(config);
200
+ const dir = join(home, ".config", "systemd", "user");
201
+ mkdirSync(dir, { recursive: true });
202
+ writeFileSync(join(dir, `${label}.service`), linuxService(command));
203
+ writeFileSync(join(dir, `${label}.timer`), linuxTimer(label));
204
+ const reload = exec("systemctl", ["--user", "daemon-reload"]);
205
+ if (reload.code !== 0) {
206
+ throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
207
+ }
208
+ const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
209
+ if (result.code !== 0) {
210
+ throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
211
+ }
212
+ }
213
+ function disableSchedule(config, options = {}) {
214
+ const platform = schedulePlatform(options.platform);
215
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
216
+ const exec = options.exec ?? defaultExec;
217
+ const home = homeOf(options);
218
+ if (platform === "win32") {
219
+ const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
220
+ if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
221
+ throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
222
+ }
223
+ return;
224
+ }
225
+ if (platform === "darwin") {
226
+ const label2 = scheduleLabel(config);
227
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
228
+ rmSync(join(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
229
+ return;
230
+ }
231
+ const label = scheduleLabel(config);
232
+ const dir = join(home, ".config", "systemd", "user");
233
+ exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
234
+ rmSync(join(dir, `${label}.service`), { force: true });
235
+ rmSync(join(dir, `${label}.timer`), { force: true });
236
+ }
237
+ function querySchedule(config, options = {}) {
238
+ const platform = schedulePlatform(options.platform);
239
+ if (!platform) return { supported: false, enabled: false };
240
+ const exec = options.exec ?? defaultExec;
241
+ const home = homeOf(options);
242
+ if (platform === "win32") {
243
+ const result = exec("schtasks", ["/Query", "/TN", scheduleName(config), "/FO", "LIST", "/V"]);
244
+ if (result.code !== 0) return { supported: true, enabled: false };
245
+ const lastRun = valueOf(result.stdout, "Last Run Time:");
246
+ const nextRun = valueOf(result.stdout, "Next Run Time:");
247
+ const state2 = { supported: true, enabled: true, cadence: "hourly" };
248
+ if (lastRun && !/disabled|never|not run/i.test(lastRun)) state2.lastRun = lastRun;
249
+ else if (nextRun && !/disabled|never/i.test(nextRun)) state2.lastRun = `next run ${nextRun}`;
250
+ return state2;
251
+ }
252
+ if (platform === "darwin") {
253
+ const plist = join(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
254
+ if (!existsSync(plist)) return { supported: true, enabled: false };
255
+ return { supported: true, enabled: true, cadence: "hourly" };
256
+ }
257
+ const timer = join(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
258
+ if (!existsSync(timer)) return { supported: true, enabled: false };
259
+ const state = { supported: true, enabled: true, cadence: "hourly" };
260
+ const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
261
+ const stamp = (shown.stdout ?? "").trim();
262
+ if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
263
+ return state;
264
+ }
265
+ function darwinPlist(label, command) {
266
+ const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
267
+ return `<?xml version="1.0" encoding="UTF-8"?>
268
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
269
+ <plist version="1.0">
270
+ <dict>
271
+ <key>Label</key>
272
+ <string>${xmlEscape(label)}</string>
273
+ <key>ProgramArguments</key>
274
+ <array>
275
+ ${args}
276
+ </array>
277
+ <key>StartInterval</key>
278
+ <integer>3600</integer>
279
+ </dict>
280
+ </plist>
281
+ `;
282
+ }
283
+ function linuxService(command) {
284
+ const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
285
+ return `[Unit]
286
+ Description=${"Hourly update check"}
287
+ [Service]
288
+ Type=oneshot
289
+ ExecStart=${line}
290
+ `;
291
+ }
292
+ function linuxTimer(label) {
293
+ return `[Unit]
294
+ Description=Hourly update check for ${label}
295
+ [Timer]
296
+ OnCalendar=hourly
297
+ Persistent=true
298
+ [Install]
299
+ WantedBy=timers.target
300
+ `;
301
+ }
302
+ function xmlEscape(text) {
303
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
304
+ }
305
+ function valueOf(output, key) {
306
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
307
+ if (!line) return null;
308
+ const value = line.slice(line.indexOf(key) + key.length).trim();
309
+ return value ? value : null;
310
+ }
311
+ function firstLine(text) {
312
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
313
+ }
314
+
9
315
  // src/config.ts
10
- import { readFileSync } from "node:fs";
316
+ import { readFileSync as readFileSync2 } from "node:fs";
11
317
  import { getAsset } from "node:sea";
12
318
 
13
319
  // src/module-url.ts
@@ -29,10 +335,10 @@ function loadProductConfig(options = {}) {
29
335
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
30
336
  const devFallback = new URL("../config/product.template.json", moduleUrl());
31
337
  if (explicit) {
32
- return parseProductConfig(readFileSync(explicit, "utf8"));
338
+ return parseProductConfig(readFileSync2(explicit, "utf8"));
33
339
  }
34
340
  try {
35
- return parseProductConfig(readFileSync(devFallback, "utf8"));
341
+ return parseProductConfig(readFileSync2(devFallback, "utf8"));
36
342
  } catch {
37
343
  }
38
344
  try {
@@ -321,9 +627,9 @@ function page(title, body) {
321
627
 
322
628
  // src/payload.ts
323
629
  import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
324
- import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
325
- import { tmpdir } from "node:os";
326
- import { dirname, join } from "node:path";
630
+ import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
631
+ import { tmpdir as tmpdir2 } from "node:os";
632
+ import { dirname, join as join2 } from "node:path";
327
633
 
328
634
  // src/canonical.ts
329
635
  function canonicalJson(value) {
@@ -463,56 +769,56 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
463
769
  }
464
770
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
465
771
  const fetchImpl = options.fetchImpl ?? fetch;
466
- const staging = join(tmpdir(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
467
- mkdirSync(staging, { recursive: true });
772
+ const staging = join2(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
773
+ mkdirSync2(staging, { recursive: true });
468
774
  try {
469
775
  for (const entry of manifest.files) {
470
776
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
471
777
  if (!verifyFileBytes(entry, bytes)) {
472
778
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
473
779
  }
474
- const dest = join(staging, entry.path);
475
- mkdirSync(dirname(dest), { recursive: true });
476
- writeFileSync(dest, bytes);
780
+ const dest = join2(staging, entry.path);
781
+ mkdirSync2(dirname(dest), { recursive: true });
782
+ writeFileSync2(dest, bytes);
477
783
  }
478
- const target = join(dir, "payload");
479
- mkdirSync(dir, { recursive: true });
480
- rmSync(target, { force: true, recursive: true });
784
+ const target = join2(dir, "payload");
785
+ mkdirSync2(dir, { recursive: true });
786
+ rmSync2(target, { force: true, recursive: true });
481
787
  renameSync(staging, target);
482
788
  } catch (error) {
483
- rmSync(staging, { force: true, recursive: true });
789
+ rmSync2(staging, { force: true, recursive: true });
484
790
  throw error;
485
791
  }
486
792
  return manifest.version;
487
793
  }
488
794
 
489
795
  // src/store.ts
490
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
491
- import { tmpdir as tmpdir2 } from "node:os";
492
- import { join as join2 } from "node:path";
796
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
797
+ import { tmpdir as tmpdir3 } from "node:os";
798
+ import { join as join3 } from "node:path";
493
799
  function defaultProductDir(product) {
494
800
  if (process.platform === "win32") {
495
- const base = process.env.LOCALAPPDATA ?? join2(tmpdir2(), "launcher-fallback");
496
- return join2(base, product);
801
+ const base = process.env.LOCALAPPDATA ?? join3(tmpdir3(), "launcher-fallback");
802
+ return join3(base, product);
497
803
  }
498
- const home = process.env.HOME ?? tmpdir2();
499
- return join2(home, `.${product}`);
804
+ const home = process.env.HOME ?? tmpdir3();
805
+ return join3(home, `.${product}`);
500
806
  }
501
807
  function resolveProductDir(product, explicit) {
502
808
  return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
503
809
  }
504
810
  function tokensPath(dir) {
505
- return join2(dir, "tokens.json");
811
+ return join3(dir, "tokens.json");
506
812
  }
507
813
  function statePath(dir) {
508
- return join2(dir, "state.json");
814
+ return join3(dir, "state.json");
509
815
  }
510
816
  function payloadDir(dir) {
511
- return join2(dir, "payload");
817
+ return join3(dir, "payload");
512
818
  }
513
819
  function readTokens(dir) {
514
820
  try {
515
- const data = JSON.parse(readFileSync2(tokensPath(dir), "utf8"));
821
+ const data = JSON.parse(readFileSync3(tokensPath(dir), "utf8"));
516
822
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
517
823
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
518
824
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -524,21 +830,21 @@ function readTokens(dir) {
524
830
  }
525
831
  }
526
832
  function writeTokens(dir, tokens) {
527
- mkdirSync2(dir, { recursive: true });
833
+ mkdirSync3(dir, { recursive: true });
528
834
  try {
529
- writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
835
+ writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
530
836
  `, { mode: 384 });
531
837
  } catch {
532
- writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
838
+ writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
533
839
  `);
534
840
  }
535
841
  }
536
842
  function clearTokens(dir) {
537
- rmSync2(tokensPath(dir), { force: true });
843
+ rmSync3(tokensPath(dir), { force: true });
538
844
  }
539
845
  function readState(dir) {
540
846
  try {
541
- const data = JSON.parse(readFileSync2(statePath(dir), "utf8"));
847
+ const data = JSON.parse(readFileSync3(statePath(dir), "utf8"));
542
848
  if (typeof data.version !== "string" || !data.version) return null;
543
849
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
544
850
  } catch {
@@ -546,18 +852,18 @@ function readState(dir) {
546
852
  }
547
853
  }
548
854
  function writeState(dir, state) {
549
- mkdirSync2(dir, { recursive: true });
550
- writeFileSync2(statePath(dir), `${JSON.stringify(state, null, 2)}
855
+ mkdirSync3(dir, { recursive: true });
856
+ writeFileSync3(statePath(dir), `${JSON.stringify(state, null, 2)}
551
857
  `);
552
858
  }
553
859
  function wipeProductDir(dir) {
554
- rmSync2(tokensPath(dir), { force: true });
555
- rmSync2(statePath(dir), { force: true });
556
- rmSync2(payloadDir(dir), { force: true, recursive: true });
860
+ rmSync3(tokensPath(dir), { force: true });
861
+ rmSync3(statePath(dir), { force: true });
862
+ rmSync3(payloadDir(dir), { force: true, recursive: true });
557
863
  }
558
864
 
559
865
  // src/index.ts
560
- var LAUNCHER_VERSION = true ? "0.1.2" : readVersionFromPackage();
866
+ var LAUNCHER_VERSION = true ? "0.1.4" : readVersionFromPackage();
561
867
  function defaultPrint(message) {
562
868
  process.stdout.write(`${message}
563
869
  `);
@@ -604,12 +910,11 @@ async function run(rawOptions = {}) {
604
910
  case "update":
605
911
  return await doUpdate(config, dir, rawOptions, print);
606
912
  case "doctor":
607
- doDoctor(config, dir, print);
608
- return 0;
913
+ return doDoctor(config, dir, rawOptions, print);
914
+ case "autoupdate":
915
+ return doAutoupdate(config, rawOptions, positional.slice(1), print);
609
916
  default:
610
- print(`unknown command: ${command}`);
611
- printUsage(config, print);
612
- return 2;
917
+ return doForward(config, dir, rawOptions, command, argv, print);
613
918
  }
614
919
  } catch (error) {
615
920
  if (error instanceof NeedsLoginError) {
@@ -628,8 +933,9 @@ function flagValue(argv, flag) {
628
933
  }
629
934
  function printUsage(config, print) {
630
935
  print(`${config.binName} launcher ${LAUNCHER_VERSION} \u2014 sign in and install ${config.product}.`);
631
- print("usage: launcher <login|logout|install|update|doctor> [--config <path>] [--dir <path>]");
936
+ print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
632
937
  print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
938
+ print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
633
939
  }
634
940
  async function runFile(file, args, printErr) {
635
941
  if (!file) {
@@ -637,7 +943,7 @@ async function runFile(file, args, printErr) {
637
943
  return 1;
638
944
  }
639
945
  const abs = resolve(process.cwd(), file);
640
- if (!existsSync3(abs)) {
946
+ if (!existsSync4(abs)) {
641
947
  printErr(`cannot run ${file}: no such file.`);
642
948
  return 1;
643
949
  }
@@ -723,10 +1029,10 @@ async function fetchAccessTokenOrLogin(config, dir, options, print) {
723
1029
  if (!after) throw new Error("sign-in did not produce a token");
724
1030
  return after.accessToken;
725
1031
  }
726
- function readPayloadEntry(dir) {
1032
+ function readPayloadArgv(dir, key) {
727
1033
  try {
728
- const parsed = JSON.parse(readFileSync3(join3(payloadDir(dir), "payload.json"), "utf8"));
729
- const entry = parsed.entry;
1034
+ const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1035
+ const entry = parsed[key];
730
1036
  if (typeof entry === "string") {
731
1037
  const parts = entry.trim().split(/\s+/).filter(Boolean);
732
1038
  return parts.length > 0 ? parts : null;
@@ -739,67 +1045,251 @@ function readPayloadEntry(dir) {
739
1045
  return null;
740
1046
  }
741
1047
  }
1048
+ function readPayloadEntry(dir) {
1049
+ return readPayloadArgv(dir, "entry");
1050
+ }
1051
+ function readPayloadRun(dir) {
1052
+ return readPayloadArgv(dir, "run");
1053
+ }
1054
+ function readPayloadVerbs(dir) {
1055
+ try {
1056
+ const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1057
+ const verbs = parsed.verbs;
1058
+ if (verbs === "*") return "*";
1059
+ if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
1060
+ return verbs;
1061
+ }
1062
+ return null;
1063
+ } catch {
1064
+ return null;
1065
+ }
1066
+ }
742
1067
  function resolveEntry(entry) {
743
1068
  return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
744
1069
  }
745
1070
  function needsShell(command) {
746
1071
  if (process.platform !== "win32") return false;
747
1072
  if (/\.(cmd|bat)$/i.test(command)) return true;
748
- return !existsSync3(command);
1073
+ return !existsSync4(command);
1074
+ }
1075
+ function quoteForShell(arg) {
1076
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
749
1077
  }
750
- function defaultRunEntry(entry, cwd) {
1078
+ function defaultRunEntry(entry, cwd, env) {
751
1079
  const [command, ...args] = entry;
752
- const result = spawnSync(command, args, {
1080
+ const shell = needsShell(command);
1081
+ const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1082
+ const result = spawnSync2(commandLine, shell ? [] : args, {
753
1083
  cwd,
754
1084
  stdio: "inherit",
755
- shell: needsShell(command),
1085
+ shell,
1086
+ env: env ?? process.env,
756
1087
  windowsHide: true
757
1088
  });
758
1089
  if (result.error) return { ok: false, error: result.error.message };
759
- return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}` };
1090
+ const code = typeof result.status === "number" ? result.status : void 0;
1091
+ return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1092
+ }
1093
+ function faceProduct(config) {
1094
+ const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1095
+ return known[config.product] ?? config.product;
1096
+ }
1097
+ function faceFor(config, options) {
1098
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
1099
+ if (!tty) return null;
1100
+ try {
1101
+ return createFace({
1102
+ product: faceProduct(config),
1103
+ color: !process.env.NO_COLOR && process.env.TERM !== "dumb",
1104
+ columns: process.stdout.columns
1105
+ });
1106
+ } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ function since(start) {
1111
+ return (Date.now() - start) / 1e3;
1112
+ }
1113
+ function printReceipt(face, config, print, ready, lines) {
1114
+ const name = face ? face.identity.name : config.product;
1115
+ const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1116
+ if (!face) {
1117
+ print(headline);
1118
+ for (const line of lines) print(line.trim());
1119
+ return;
1120
+ }
1121
+ const glyph = ready ? "\u2714" : "\u2716";
1122
+ for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1123
+ print(face.signOff());
1124
+ }
1125
+ function printStep(face, print, title, seconds, kind = "ok") {
1126
+ print(face ? face.step(title, seconds, kind) : title);
760
1127
  }
761
1128
  async function doInstall(config, dir, options, print) {
762
1129
  const fetchImpl = options.fetchImpl ?? fetch;
1130
+ const face = faceFor(config, options);
763
1131
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1132
+ const started = Date.now();
764
1133
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
765
1134
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
766
1135
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
767
- print(`installed ${config.product} ${manifest.version} into ${dir}.`);
1136
+ printStep(face, print, `installed ${config.product} ${manifest.version}`, since(started));
1137
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1138
+ }
1139
+ async function doUpdate(config, dir, options, print) {
1140
+ const fetchImpl = options.fetchImpl ?? fetch;
1141
+ const face = faceFor(config, options);
1142
+ if (face) for (const line of face.welcome()) print(line);
1143
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1144
+ const started = Date.now();
1145
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1146
+ const current = readState(dir);
1147
+ if (current && current.version === manifest.version && existsSync4(payloadDir(dir))) {
1148
+ printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1149
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1150
+ }
1151
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1152
+ writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1153
+ printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1154
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1155
+ }
1156
+ function payloadEnv(dir) {
1157
+ const stored = readTokens(dir);
1158
+ if (!stored?.accessToken) return void 0;
1159
+ return { ...process.env, MM_INSTALLER_TOKEN: stored.accessToken };
1160
+ }
1161
+ var NEVER_A_PROGRAM = /\.(tgz|tar|gz|bz2|xz|zip|json|txt|md|lock)$/i;
1162
+ function payloadFileAsCommand(entry, payload) {
1163
+ const first = entry[0];
1164
+ if (!first || first === "$self") return null;
1165
+ if (first.includes("/") || first.includes("\\")) return null;
1166
+ const candidate = join4(payload, first);
1167
+ if (!existsSync4(candidate)) return null;
1168
+ if (NEVER_A_PROGRAM.test(first)) return first;
1169
+ if (process.platform === "win32") return null;
1170
+ try {
1171
+ return (statSync(candidate).mode & 73) === 0 ? first : null;
1172
+ } catch {
1173
+ return null;
1174
+ }
1175
+ }
1176
+ function finishLastMile(config, dir, options, print, face, version) {
768
1177
  const entry = readPayloadEntry(dir);
1178
+ const payload = payloadDir(dir);
769
1179
  if (!entry) {
770
- print(`next step: run ${join3(payloadDir(dir), config.binName)} to start ${config.product}.`);
1180
+ printReceipt(face, config, print, true, [
1181
+ ` Installed ${version} into ${dir}`,
1182
+ ` next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1183
+ ]);
771
1184
  return 0;
772
1185
  }
773
- const payload = payloadDir(dir);
774
1186
  const command = resolveEntry(entry);
775
- const runEntry = options.runEntry ?? defaultRunEntry;
776
- const result = runEntry(command, payload);
1187
+ const dataFile = payloadFileAsCommand(entry, payload);
1188
+ if (dataFile) {
1189
+ printStep(face, print, `The payload names ${dataFile} as its command, but that is a file, not a program`, null, "fail");
1190
+ printReceipt(face, config, print, false, [
1191
+ ` Downloaded ${version} into ${dir}`,
1192
+ ` This payload was built wrong: its entry must be a command, not one of its own files.`,
1193
+ ` Nothing on this machine can finish it \u2014 report it to the ${config.product} maintainers.`
1194
+ ]);
1195
+ return 2;
1196
+ }
1197
+ const started = Date.now();
1198
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload, payloadEnv(dir));
777
1199
  if (result.ok) {
778
- print(`${config.product} is ready.`);
1200
+ printStep(face, print, "Armed this machine", since(started));
1201
+ printReceipt(face, config, print, true, [
1202
+ ` Installed ${version} into ${dir}`,
1203
+ ` Check health any time: ${config.binName} doctor`
1204
+ ]);
779
1205
  return 0;
780
1206
  }
781
- print(`could not finish the last mile automatically${result.error ? ` (${result.error})` : ""}. Run it yourself:`);
782
- print(` (cd ${payload} && ${command.join(" ")})`);
783
- return 0;
1207
+ const code = result.code ?? 1;
1208
+ printStep(
1209
+ face,
1210
+ print,
1211
+ 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})`,
1212
+ null,
1213
+ "fail"
1214
+ );
1215
+ printReceipt(face, config, print, false, [
1216
+ ` Downloaded ${version} into ${dir}`,
1217
+ ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1218
+ ]);
1219
+ return code;
784
1220
  }
785
- async function doUpdate(config, dir, options, print) {
786
- const fetchImpl = options.fetchImpl ?? fetch;
787
- const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
788
- const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
789
- const current = readState(dir);
790
- if (current && current.version === manifest.version && existsSync3(payloadDir(dir))) {
791
- print(`${config.product} is already up to date at ${manifest.version}.`);
1221
+ function doForward(config, dir, options, command, argv, print) {
1222
+ if (command && existsSync4(command)) {
1223
+ print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1224
+ return 2;
1225
+ }
1226
+ const target = readPayloadRun(dir);
1227
+ const verbs = readPayloadVerbs(dir);
1228
+ const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
1229
+ if (!target || !declared) {
1230
+ print(`unknown command: ${command}`);
1231
+ printUsage(config, print);
1232
+ return 2;
1233
+ }
1234
+ const forwarded = [...resolveEntry(target), ...argv];
1235
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir), payloadEnv(dir));
1236
+ if (!result.ok && result.code === void 0) {
1237
+ print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1238
+ return 1;
1239
+ }
1240
+ return result.code ?? 0;
1241
+ }
1242
+ function doAutoupdate(config, options, args, print) {
1243
+ const mode = args[0] ?? "status";
1244
+ const scheduleOptions = options.autoupdate ?? {};
1245
+ const command = options.autoupdate?.command ?? [process.execPath, "update"];
1246
+ if (mode === "on") {
1247
+ try {
1248
+ enableSchedule(config, command, scheduleOptions);
1249
+ } catch (error) {
1250
+ print(error.message);
1251
+ return 1;
1252
+ }
1253
+ const state2 = querySchedule(config, scheduleOptions);
1254
+ print(`auto-update: on (${state2.cadence ?? "hourly"})`);
792
1255
  return 0;
793
1256
  }
794
- await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
795
- writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
796
- print(`updated ${config.product} to ${manifest.version}.`);
1257
+ if (mode === "off") {
1258
+ try {
1259
+ disableSchedule(config, scheduleOptions);
1260
+ } catch (error) {
1261
+ print(error.message);
1262
+ return 1;
1263
+ }
1264
+ print("auto-update: off");
1265
+ return 0;
1266
+ }
1267
+ if (mode !== "status") {
1268
+ print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
1269
+ return 1;
1270
+ }
1271
+ const state = querySchedule(config, scheduleOptions);
1272
+ if (!state.supported) {
1273
+ print("auto-update: unsupported on this platform");
1274
+ return 1;
1275
+ }
1276
+ print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
1277
+ if (state.lastRun) print(`last run: ${state.lastRun}`);
797
1278
  return 0;
798
1279
  }
799
- function doDoctor(config, dir, print) {
1280
+ function doDoctor(config, dir, options, print) {
1281
+ doLauncherDoctor(config, dir, options, print);
1282
+ const target = readPayloadRun(dir);
1283
+ const verbs = readPayloadVerbs(dir);
1284
+ const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1285
+ if (!chains) return 0;
1286
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir), payloadEnv(dir));
1287
+ return result.code ?? (result.ok ? 0 : 1);
1288
+ }
1289
+ function doLauncherDoctor(config, dir, options, print) {
800
1290
  const tokens = readTokens(dir);
801
1291
  const state = readState(dir);
802
- const payloadPresent = existsSync3(payloadDir(dir));
1292
+ const payloadPresent = existsSync4(payloadDir(dir));
803
1293
  print(`product: ${config.product}`);
804
1294
  print(`host: ${config.host}`);
805
1295
  print(`login: ${config.loginKind}`);
@@ -811,7 +1301,11 @@ function doDoctor(config, dir, print) {
811
1301
  print("token: none \u2014 run login first.");
812
1302
  }
813
1303
  print(`payload: ${state ? state.version : "none"}${payloadPresent ? "" : " (not downloaded)"}`);
814
- print(`paths: tokens ${join3(dir, "tokens.json")}, state ${join3(dir, "state.json")}, payload ${payloadDir(dir)}`);
1304
+ print(`paths: tokens ${join4(dir, "tokens.json")}, state ${join4(dir, "state.json")}, payload ${payloadDir(dir)}`);
1305
+ const schedule = querySchedule(config, options.autoupdate ?? {});
1306
+ print(
1307
+ `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
1308
+ );
815
1309
  }
816
1310
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
817
1311
  try {
@@ -841,6 +1335,8 @@ export {
841
1335
  defaultRunEntry,
842
1336
  ensureFreshToken,
843
1337
  readPayloadEntry,
1338
+ readPayloadRun,
1339
+ readPayloadVerbs,
844
1340
  resolveEntry,
845
1341
  run,
846
1342
  runFile