@mutmutco/installer-launcher 0.1.2 → 0.1.3

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,316 @@
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 } 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
+ return [
125
+ frame(GLYPH.boxTop, GLYPH.boxTopEnd),
126
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
127
+ frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
128
+ ];
129
+ };
130
+ const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
131
+ const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
132
+ return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
133
+ }
134
+ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
135
+ var ALLOWED = new Set(Object.values(GLYPH));
136
+
137
+ // src/autoupdate.ts
138
+ import { spawnSync } from "node:child_process";
139
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
140
+ import { tmpdir } from "node:os";
141
+ import { join } from "node:path";
142
+ function schedulePlatform(override) {
143
+ const platform = override ?? process.platform;
144
+ if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
145
+ return null;
146
+ }
147
+ function defaultExec(command, args) {
148
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
149
+ if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
150
+ return {
151
+ code: typeof result.status === "number" ? result.status : 1,
152
+ stdout: String(result.stdout ?? ""),
153
+ stderr: String(result.stderr ?? "")
154
+ };
155
+ }
156
+ function homeOf(options) {
157
+ if (options.homeDir) return options.homeDir;
158
+ if (process.platform === "win32") return process.env.USERPROFILE ?? tmpdir();
159
+ return process.env.HOME ?? tmpdir();
160
+ }
161
+ function scheduleName(config) {
162
+ return `${config.binName} autoupdate`;
163
+ }
164
+ function scheduleLabel(config) {
165
+ return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
166
+ }
167
+ function quoteWindows(arg) {
168
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
169
+ }
170
+ function enableSchedule(config, command, options = {}) {
171
+ const platform = schedulePlatform(options.platform);
172
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
173
+ const exec = options.exec ?? defaultExec;
174
+ const home = homeOf(options);
175
+ if (platform === "win32") {
176
+ const taskLine = command.map(quoteWindows).join(" ");
177
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
178
+ if (result2.code !== 0) {
179
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
180
+ }
181
+ return;
182
+ }
183
+ if (platform === "darwin") {
184
+ const label2 = scheduleLabel(config);
185
+ const dir2 = join(home, "Library", "LaunchAgents");
186
+ mkdirSync(dir2, { recursive: true });
187
+ const plist = join(dir2, `${label2}.plist`);
188
+ writeFileSync(plist, darwinPlist(label2, command));
189
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
190
+ const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
191
+ if (result2.code !== 0) {
192
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
193
+ }
194
+ return;
195
+ }
196
+ const label = scheduleLabel(config);
197
+ const dir = join(home, ".config", "systemd", "user");
198
+ mkdirSync(dir, { recursive: true });
199
+ writeFileSync(join(dir, `${label}.service`), linuxService(command));
200
+ writeFileSync(join(dir, `${label}.timer`), linuxTimer(label));
201
+ const reload = exec("systemctl", ["--user", "daemon-reload"]);
202
+ if (reload.code !== 0) {
203
+ throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
204
+ }
205
+ const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
206
+ if (result.code !== 0) {
207
+ throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
208
+ }
209
+ }
210
+ function disableSchedule(config, options = {}) {
211
+ const platform = schedulePlatform(options.platform);
212
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
213
+ const exec = options.exec ?? defaultExec;
214
+ const home = homeOf(options);
215
+ if (platform === "win32") {
216
+ const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
217
+ if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
218
+ throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
219
+ }
220
+ return;
221
+ }
222
+ if (platform === "darwin") {
223
+ const label2 = scheduleLabel(config);
224
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
225
+ rmSync(join(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
226
+ return;
227
+ }
228
+ const label = scheduleLabel(config);
229
+ const dir = join(home, ".config", "systemd", "user");
230
+ exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
231
+ rmSync(join(dir, `${label}.service`), { force: true });
232
+ rmSync(join(dir, `${label}.timer`), { force: true });
233
+ }
234
+ function querySchedule(config, options = {}) {
235
+ const platform = schedulePlatform(options.platform);
236
+ if (!platform) return { supported: false, enabled: false };
237
+ const exec = options.exec ?? defaultExec;
238
+ const home = homeOf(options);
239
+ if (platform === "win32") {
240
+ const result = exec("schtasks", ["/Query", "/TN", scheduleName(config), "/FO", "LIST", "/V"]);
241
+ if (result.code !== 0) return { supported: true, enabled: false };
242
+ const lastRun = valueOf(result.stdout, "Last Run Time:");
243
+ const nextRun = valueOf(result.stdout, "Next Run Time:");
244
+ const state2 = { supported: true, enabled: true, cadence: "hourly" };
245
+ if (lastRun && !/disabled|never|not run/i.test(lastRun)) state2.lastRun = lastRun;
246
+ else if (nextRun && !/disabled|never/i.test(nextRun)) state2.lastRun = `next run ${nextRun}`;
247
+ return state2;
248
+ }
249
+ if (platform === "darwin") {
250
+ const plist = join(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
251
+ if (!existsSync(plist)) return { supported: true, enabled: false };
252
+ return { supported: true, enabled: true, cadence: "hourly" };
253
+ }
254
+ const timer = join(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
255
+ if (!existsSync(timer)) return { supported: true, enabled: false };
256
+ const state = { supported: true, enabled: true, cadence: "hourly" };
257
+ const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
258
+ const stamp = (shown.stdout ?? "").trim();
259
+ if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
260
+ return state;
261
+ }
262
+ function darwinPlist(label, command) {
263
+ const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
264
+ return `<?xml version="1.0" encoding="UTF-8"?>
265
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
266
+ <plist version="1.0">
267
+ <dict>
268
+ <key>Label</key>
269
+ <string>${xmlEscape(label)}</string>
270
+ <key>ProgramArguments</key>
271
+ <array>
272
+ ${args}
273
+ </array>
274
+ <key>StartInterval</key>
275
+ <integer>3600</integer>
276
+ </dict>
277
+ </plist>
278
+ `;
279
+ }
280
+ function linuxService(command) {
281
+ const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
282
+ return `[Unit]
283
+ Description=${"Hourly update check"}
284
+ [Service]
285
+ Type=oneshot
286
+ ExecStart=${line}
287
+ `;
288
+ }
289
+ function linuxTimer(label) {
290
+ return `[Unit]
291
+ Description=Hourly update check for ${label}
292
+ [Timer]
293
+ OnCalendar=hourly
294
+ Persistent=true
295
+ [Install]
296
+ WantedBy=timers.target
297
+ `;
298
+ }
299
+ function xmlEscape(text) {
300
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
301
+ }
302
+ function valueOf(output, key) {
303
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
304
+ if (!line) return null;
305
+ const value = line.slice(line.indexOf(key) + key.length).trim();
306
+ return value ? value : null;
307
+ }
308
+ function firstLine(text) {
309
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
310
+ }
311
+
9
312
  // src/config.ts
10
- import { readFileSync } from "node:fs";
313
+ import { readFileSync as readFileSync2 } from "node:fs";
11
314
  import { getAsset } from "node:sea";
12
315
 
13
316
  // src/module-url.ts
@@ -29,10 +332,10 @@ function loadProductConfig(options = {}) {
29
332
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
30
333
  const devFallback = new URL("../config/product.template.json", moduleUrl());
31
334
  if (explicit) {
32
- return parseProductConfig(readFileSync(explicit, "utf8"));
335
+ return parseProductConfig(readFileSync2(explicit, "utf8"));
33
336
  }
34
337
  try {
35
- return parseProductConfig(readFileSync(devFallback, "utf8"));
338
+ return parseProductConfig(readFileSync2(devFallback, "utf8"));
36
339
  } catch {
37
340
  }
38
341
  try {
@@ -321,9 +624,9 @@ function page(title, body) {
321
624
 
322
625
  // src/payload.ts
323
626
  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";
627
+ import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
628
+ import { tmpdir as tmpdir2 } from "node:os";
629
+ import { dirname, join as join2 } from "node:path";
327
630
 
328
631
  // src/canonical.ts
329
632
  function canonicalJson(value) {
@@ -463,56 +766,56 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
463
766
  }
464
767
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
465
768
  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 });
769
+ const staging = join2(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
770
+ mkdirSync2(staging, { recursive: true });
468
771
  try {
469
772
  for (const entry of manifest.files) {
470
773
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
471
774
  if (!verifyFileBytes(entry, bytes)) {
472
775
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
473
776
  }
474
- const dest = join(staging, entry.path);
475
- mkdirSync(dirname(dest), { recursive: true });
476
- writeFileSync(dest, bytes);
777
+ const dest = join2(staging, entry.path);
778
+ mkdirSync2(dirname(dest), { recursive: true });
779
+ writeFileSync2(dest, bytes);
477
780
  }
478
- const target = join(dir, "payload");
479
- mkdirSync(dir, { recursive: true });
480
- rmSync(target, { force: true, recursive: true });
781
+ const target = join2(dir, "payload");
782
+ mkdirSync2(dir, { recursive: true });
783
+ rmSync2(target, { force: true, recursive: true });
481
784
  renameSync(staging, target);
482
785
  } catch (error) {
483
- rmSync(staging, { force: true, recursive: true });
786
+ rmSync2(staging, { force: true, recursive: true });
484
787
  throw error;
485
788
  }
486
789
  return manifest.version;
487
790
  }
488
791
 
489
792
  // 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";
793
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
794
+ import { tmpdir as tmpdir3 } from "node:os";
795
+ import { join as join3 } from "node:path";
493
796
  function defaultProductDir(product) {
494
797
  if (process.platform === "win32") {
495
- const base = process.env.LOCALAPPDATA ?? join2(tmpdir2(), "launcher-fallback");
496
- return join2(base, product);
798
+ const base = process.env.LOCALAPPDATA ?? join3(tmpdir3(), "launcher-fallback");
799
+ return join3(base, product);
497
800
  }
498
- const home = process.env.HOME ?? tmpdir2();
499
- return join2(home, `.${product}`);
801
+ const home = process.env.HOME ?? tmpdir3();
802
+ return join3(home, `.${product}`);
500
803
  }
501
804
  function resolveProductDir(product, explicit) {
502
805
  return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
503
806
  }
504
807
  function tokensPath(dir) {
505
- return join2(dir, "tokens.json");
808
+ return join3(dir, "tokens.json");
506
809
  }
507
810
  function statePath(dir) {
508
- return join2(dir, "state.json");
811
+ return join3(dir, "state.json");
509
812
  }
510
813
  function payloadDir(dir) {
511
- return join2(dir, "payload");
814
+ return join3(dir, "payload");
512
815
  }
513
816
  function readTokens(dir) {
514
817
  try {
515
- const data = JSON.parse(readFileSync2(tokensPath(dir), "utf8"));
818
+ const data = JSON.parse(readFileSync3(tokensPath(dir), "utf8"));
516
819
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
517
820
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
518
821
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -524,21 +827,21 @@ function readTokens(dir) {
524
827
  }
525
828
  }
526
829
  function writeTokens(dir, tokens) {
527
- mkdirSync2(dir, { recursive: true });
830
+ mkdirSync3(dir, { recursive: true });
528
831
  try {
529
- writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
832
+ writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
530
833
  `, { mode: 384 });
531
834
  } catch {
532
- writeFileSync2(tokensPath(dir), `${JSON.stringify(tokens)}
835
+ writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
533
836
  `);
534
837
  }
535
838
  }
536
839
  function clearTokens(dir) {
537
- rmSync2(tokensPath(dir), { force: true });
840
+ rmSync3(tokensPath(dir), { force: true });
538
841
  }
539
842
  function readState(dir) {
540
843
  try {
541
- const data = JSON.parse(readFileSync2(statePath(dir), "utf8"));
844
+ const data = JSON.parse(readFileSync3(statePath(dir), "utf8"));
542
845
  if (typeof data.version !== "string" || !data.version) return null;
543
846
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
544
847
  } catch {
@@ -546,18 +849,18 @@ function readState(dir) {
546
849
  }
547
850
  }
548
851
  function writeState(dir, state) {
549
- mkdirSync2(dir, { recursive: true });
550
- writeFileSync2(statePath(dir), `${JSON.stringify(state, null, 2)}
852
+ mkdirSync3(dir, { recursive: true });
853
+ writeFileSync3(statePath(dir), `${JSON.stringify(state, null, 2)}
551
854
  `);
552
855
  }
553
856
  function wipeProductDir(dir) {
554
- rmSync2(tokensPath(dir), { force: true });
555
- rmSync2(statePath(dir), { force: true });
556
- rmSync2(payloadDir(dir), { force: true, recursive: true });
857
+ rmSync3(tokensPath(dir), { force: true });
858
+ rmSync3(statePath(dir), { force: true });
859
+ rmSync3(payloadDir(dir), { force: true, recursive: true });
557
860
  }
558
861
 
559
862
  // src/index.ts
560
- var LAUNCHER_VERSION = true ? "0.1.2" : readVersionFromPackage();
863
+ var LAUNCHER_VERSION = true ? "0.1.3" : readVersionFromPackage();
561
864
  function defaultPrint(message) {
562
865
  process.stdout.write(`${message}
563
866
  `);
@@ -604,12 +907,11 @@ async function run(rawOptions = {}) {
604
907
  case "update":
605
908
  return await doUpdate(config, dir, rawOptions, print);
606
909
  case "doctor":
607
- doDoctor(config, dir, print);
608
- return 0;
910
+ return doDoctor(config, dir, rawOptions, print);
911
+ case "autoupdate":
912
+ return doAutoupdate(config, rawOptions, positional.slice(1), print);
609
913
  default:
610
- print(`unknown command: ${command}`);
611
- printUsage(config, print);
612
- return 2;
914
+ return doForward(config, dir, rawOptions, command, argv, print);
613
915
  }
614
916
  } catch (error) {
615
917
  if (error instanceof NeedsLoginError) {
@@ -628,8 +930,9 @@ function flagValue(argv, flag) {
628
930
  }
629
931
  function printUsage(config, print) {
630
932
  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>]");
933
+ print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
632
934
  print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
935
+ print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
633
936
  }
634
937
  async function runFile(file, args, printErr) {
635
938
  if (!file) {
@@ -637,7 +940,7 @@ async function runFile(file, args, printErr) {
637
940
  return 1;
638
941
  }
639
942
  const abs = resolve(process.cwd(), file);
640
- if (!existsSync3(abs)) {
943
+ if (!existsSync4(abs)) {
641
944
  printErr(`cannot run ${file}: no such file.`);
642
945
  return 1;
643
946
  }
@@ -723,10 +1026,10 @@ async function fetchAccessTokenOrLogin(config, dir, options, print) {
723
1026
  if (!after) throw new Error("sign-in did not produce a token");
724
1027
  return after.accessToken;
725
1028
  }
726
- function readPayloadEntry(dir) {
1029
+ function readPayloadArgv(dir, key) {
727
1030
  try {
728
- const parsed = JSON.parse(readFileSync3(join3(payloadDir(dir), "payload.json"), "utf8"));
729
- const entry = parsed.entry;
1031
+ const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1032
+ const entry = parsed[key];
730
1033
  if (typeof entry === "string") {
731
1034
  const parts = entry.trim().split(/\s+/).filter(Boolean);
732
1035
  return parts.length > 0 ? parts : null;
@@ -739,67 +1042,213 @@ function readPayloadEntry(dir) {
739
1042
  return null;
740
1043
  }
741
1044
  }
1045
+ function readPayloadEntry(dir) {
1046
+ return readPayloadArgv(dir, "entry");
1047
+ }
1048
+ function readPayloadRun(dir) {
1049
+ return readPayloadArgv(dir, "run");
1050
+ }
1051
+ function readPayloadVerbs(dir) {
1052
+ try {
1053
+ const parsed = JSON.parse(readFileSync4(join4(payloadDir(dir), "payload.json"), "utf8"));
1054
+ const verbs = parsed.verbs;
1055
+ if (verbs === "*") return "*";
1056
+ if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
1057
+ return verbs;
1058
+ }
1059
+ return null;
1060
+ } catch {
1061
+ return null;
1062
+ }
1063
+ }
742
1064
  function resolveEntry(entry) {
743
1065
  return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
744
1066
  }
745
1067
  function needsShell(command) {
746
1068
  if (process.platform !== "win32") return false;
747
1069
  if (/\.(cmd|bat)$/i.test(command)) return true;
748
- return !existsSync3(command);
1070
+ return !existsSync4(command);
1071
+ }
1072
+ function quoteForShell(arg) {
1073
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
749
1074
  }
750
1075
  function defaultRunEntry(entry, cwd) {
751
1076
  const [command, ...args] = entry;
752
- const result = spawnSync(command, args, {
1077
+ const shell = needsShell(command);
1078
+ const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1079
+ const result = spawnSync2(commandLine, shell ? [] : args, {
753
1080
  cwd,
754
1081
  stdio: "inherit",
755
- shell: needsShell(command),
1082
+ shell,
756
1083
  windowsHide: true
757
1084
  });
758
1085
  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}` };
1086
+ const code = typeof result.status === "number" ? result.status : void 0;
1087
+ return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1088
+ }
1089
+ function faceProduct(config) {
1090
+ const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1091
+ return known[config.product] ?? config.product;
1092
+ }
1093
+ function faceFor(config, options) {
1094
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
1095
+ try {
1096
+ return createFace({
1097
+ product: faceProduct(config),
1098
+ color: tty && !process.env.NO_COLOR && process.env.TERM !== "dumb",
1099
+ columns: process.stdout.columns
1100
+ });
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ function since(start) {
1106
+ return (Date.now() - start) / 1e3;
1107
+ }
1108
+ function printReceipt(face, config, print, ready, lines) {
1109
+ const name = face ? face.identity.name : config.product;
1110
+ const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1111
+ if (!face) {
1112
+ print(headline);
1113
+ for (const line of lines) print(line.trim());
1114
+ return;
1115
+ }
1116
+ const glyph = ready ? "\u2714" : "\u2716";
1117
+ for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1118
+ print(face.signOff());
1119
+ }
1120
+ function printStep(face, print, title, seconds, kind = "ok") {
1121
+ print(face ? face.step(title, seconds, kind) : title);
760
1122
  }
761
1123
  async function doInstall(config, dir, options, print) {
762
1124
  const fetchImpl = options.fetchImpl ?? fetch;
1125
+ const face = faceFor(config, options);
1126
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1127
+ const started = Date.now();
1128
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1129
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1130
+ writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1131
+ printStep(face, print, `installed ${config.product} ${manifest.version} into ${dir}`, since(started));
1132
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1133
+ }
1134
+ async function doUpdate(config, dir, options, print) {
1135
+ const fetchImpl = options.fetchImpl ?? fetch;
1136
+ const face = faceFor(config, options);
1137
+ if (face) for (const line of face.welcome()) print(line);
763
1138
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1139
+ const started = Date.now();
764
1140
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1141
+ const current = readState(dir);
1142
+ if (current && current.version === manifest.version && existsSync4(payloadDir(dir))) {
1143
+ printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1144
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1145
+ }
765
1146
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
766
1147
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
767
- print(`installed ${config.product} ${manifest.version} into ${dir}.`);
1148
+ printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1149
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1150
+ }
1151
+ function finishLastMile(config, dir, options, print, face, version) {
768
1152
  const entry = readPayloadEntry(dir);
1153
+ const payload = payloadDir(dir);
769
1154
  if (!entry) {
770
- print(`next step: run ${join3(payloadDir(dir), config.binName)} to start ${config.product}.`);
1155
+ printReceipt(face, config, print, true, [
1156
+ ` Installed ${version} into ${dir}`,
1157
+ ` next step: run ${join4(payload, config.binName)} to start ${config.product}.`
1158
+ ]);
771
1159
  return 0;
772
1160
  }
773
- const payload = payloadDir(dir);
774
1161
  const command = resolveEntry(entry);
775
- const runEntry = options.runEntry ?? defaultRunEntry;
776
- const result = runEntry(command, payload);
1162
+ const started = Date.now();
1163
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload);
777
1164
  if (result.ok) {
778
- print(`${config.product} is ready.`);
1165
+ printStep(face, print, "Armed this machine", since(started));
1166
+ printReceipt(face, config, print, true, [
1167
+ ` Installed ${version} into ${dir}`,
1168
+ ` Check health any time: ${config.binName} doctor`
1169
+ ]);
779
1170
  return 0;
780
1171
  }
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;
1172
+ const code = result.code ?? 1;
1173
+ printStep(face, print, `Arming this machine did not finish (exit code ${code})`, null, "fail");
1174
+ printReceipt(face, config, print, false, [
1175
+ ` Downloaded ${version} into ${dir}`,
1176
+ ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1177
+ ]);
1178
+ return code;
784
1179
  }
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}.`);
1180
+ function doForward(config, dir, options, command, argv, print) {
1181
+ if (command && existsSync4(command)) {
1182
+ print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1183
+ return 2;
1184
+ }
1185
+ const target = readPayloadRun(dir);
1186
+ const verbs = readPayloadVerbs(dir);
1187
+ const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
1188
+ if (!target || !declared) {
1189
+ print(`unknown command: ${command}`);
1190
+ printUsage(config, print);
1191
+ return 2;
1192
+ }
1193
+ const forwarded = [...resolveEntry(target), ...argv];
1194
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir));
1195
+ if (!result.ok && result.code === void 0) {
1196
+ print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1197
+ return 1;
1198
+ }
1199
+ return result.code ?? 0;
1200
+ }
1201
+ function doAutoupdate(config, options, args, print) {
1202
+ const mode = args[0] ?? "status";
1203
+ const scheduleOptions = options.autoupdate ?? {};
1204
+ const command = options.autoupdate?.command ?? [process.execPath, "update"];
1205
+ if (mode === "on") {
1206
+ try {
1207
+ enableSchedule(config, command, scheduleOptions);
1208
+ } catch (error) {
1209
+ print(error.message);
1210
+ return 1;
1211
+ }
1212
+ const state2 = querySchedule(config, scheduleOptions);
1213
+ print(`auto-update: on (${state2.cadence ?? "hourly"})`);
792
1214
  return 0;
793
1215
  }
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}.`);
1216
+ if (mode === "off") {
1217
+ try {
1218
+ disableSchedule(config, scheduleOptions);
1219
+ } catch (error) {
1220
+ print(error.message);
1221
+ return 1;
1222
+ }
1223
+ print("auto-update: off");
1224
+ return 0;
1225
+ }
1226
+ if (mode !== "status") {
1227
+ print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
1228
+ return 1;
1229
+ }
1230
+ const state = querySchedule(config, scheduleOptions);
1231
+ if (!state.supported) {
1232
+ print("auto-update: unsupported on this platform");
1233
+ return 1;
1234
+ }
1235
+ print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
1236
+ if (state.lastRun) print(`last run: ${state.lastRun}`);
797
1237
  return 0;
798
1238
  }
799
- function doDoctor(config, dir, print) {
1239
+ function doDoctor(config, dir, options, print) {
1240
+ doLauncherDoctor(config, dir, options, print);
1241
+ const target = readPayloadRun(dir);
1242
+ const verbs = readPayloadVerbs(dir);
1243
+ const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1244
+ if (!chains) return 0;
1245
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir));
1246
+ return result.code ?? (result.ok ? 0 : 1);
1247
+ }
1248
+ function doLauncherDoctor(config, dir, options, print) {
800
1249
  const tokens = readTokens(dir);
801
1250
  const state = readState(dir);
802
- const payloadPresent = existsSync3(payloadDir(dir));
1251
+ const payloadPresent = existsSync4(payloadDir(dir));
803
1252
  print(`product: ${config.product}`);
804
1253
  print(`host: ${config.host}`);
805
1254
  print(`login: ${config.loginKind}`);
@@ -811,7 +1260,11 @@ function doDoctor(config, dir, print) {
811
1260
  print("token: none \u2014 run login first.");
812
1261
  }
813
1262
  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)}`);
1263
+ print(`paths: tokens ${join4(dir, "tokens.json")}, state ${join4(dir, "state.json")}, payload ${payloadDir(dir)}`);
1264
+ const schedule = querySchedule(config, options.autoupdate ?? {});
1265
+ print(
1266
+ `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
1267
+ );
815
1268
  }
816
1269
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
817
1270
  try {
@@ -841,6 +1294,8 @@ export {
841
1294
  defaultRunEntry,
842
1295
  ensureFreshToken,
843
1296
  readPayloadEntry,
1297
+ readPayloadRun,
1298
+ readPayloadVerbs,
844
1299
  resolveEntry,
845
1300
  run,
846
1301
  runFile