@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.
@@ -24,18 +24,323 @@ __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
+ return [
155
+ frame(GLYPH.boxTop, GLYPH.boxTopEnd),
156
+ ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${line}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
157
+ frame(GLYPH.boxBottom, GLYPH.boxBottomEnd)
158
+ ];
159
+ };
160
+ const signOff = () => `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, `${identity.name} \xB7 Mutatis Mutandis`)}`;
161
+ const refusal = (message) => `${bar()} ${paint(identity.accent, GLYPH.cross)} ${message}`;
162
+ return { identity, width, welcome, step, relay, receipt, signOff, refusal, paint };
163
+ }
164
+ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
165
+ var ALLOWED = new Set(Object.values(GLYPH));
166
+
167
+ // src/autoupdate.ts
168
+ var import_node_child_process = require("node:child_process");
38
169
  var import_node_fs = require("node:fs");
170
+ var import_node_os = require("node:os");
171
+ var import_node_path = require("node:path");
172
+ function schedulePlatform(override) {
173
+ const platform = override ?? process.platform;
174
+ if (platform === "win32" || platform === "darwin" || platform === "linux") return platform;
175
+ return null;
176
+ }
177
+ function defaultExec(command, args) {
178
+ const result = (0, import_node_child_process.spawnSync)(command, args, { encoding: "utf8", windowsHide: true });
179
+ if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
180
+ return {
181
+ code: typeof result.status === "number" ? result.status : 1,
182
+ stdout: String(result.stdout ?? ""),
183
+ stderr: String(result.stderr ?? "")
184
+ };
185
+ }
186
+ function homeOf(options) {
187
+ if (options.homeDir) return options.homeDir;
188
+ if (process.platform === "win32") return process.env.USERPROFILE ?? (0, import_node_os.tmpdir)();
189
+ return process.env.HOME ?? (0, import_node_os.tmpdir)();
190
+ }
191
+ function scheduleName(config) {
192
+ return `${config.binName} autoupdate`;
193
+ }
194
+ function scheduleLabel(config) {
195
+ return `${config.binName}-autoupdate`.replace(/[^A-Za-z0-9_-]+/g, "-");
196
+ }
197
+ function quoteWindows(arg) {
198
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
199
+ }
200
+ function enableSchedule(config, command, options = {}) {
201
+ const platform = schedulePlatform(options.platform);
202
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
203
+ const exec = options.exec ?? defaultExec;
204
+ const home = homeOf(options);
205
+ if (platform === "win32") {
206
+ const taskLine = command.map(quoteWindows).join(" ");
207
+ const result2 = exec("schtasks", ["/Create", "/TN", scheduleName(config), "/TR", taskLine, "/SC", "HOURLY", "/IT", "/F"]);
208
+ if (result2.code !== 0) {
209
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stdout || result2.stderr) || `exit ${result2.code}`}`);
210
+ }
211
+ return;
212
+ }
213
+ if (platform === "darwin") {
214
+ const label2 = scheduleLabel(config);
215
+ const dir2 = (0, import_node_path.join)(home, "Library", "LaunchAgents");
216
+ (0, import_node_fs.mkdirSync)(dir2, { recursive: true });
217
+ const plist = (0, import_node_path.join)(dir2, `${label2}.plist`);
218
+ (0, import_node_fs.writeFileSync)(plist, darwinPlist(label2, command));
219
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
220
+ const result2 = exec("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 501}`, plist]);
221
+ if (result2.code !== 0) {
222
+ throw new Error(`could not turn autoupdate on: ${firstLine(result2.stderr || result2.stdout) || `exit ${result2.code}`}`);
223
+ }
224
+ return;
225
+ }
226
+ const label = scheduleLabel(config);
227
+ const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
228
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
229
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.service`), linuxService(command));
230
+ (0, import_node_fs.writeFileSync)((0, import_node_path.join)(dir, `${label}.timer`), linuxTimer(label));
231
+ const reload = exec("systemctl", ["--user", "daemon-reload"]);
232
+ if (reload.code !== 0) {
233
+ throw new Error(`could not turn autoupdate on: ${firstLine(reload.stderr || reload.stdout) || `exit ${reload.code}`}`);
234
+ }
235
+ const result = exec("systemctl", ["--user", "enable", "--now", `${label}.timer`]);
236
+ if (result.code !== 0) {
237
+ throw new Error(`could not turn autoupdate on: ${firstLine(result.stderr || result.stdout) || `exit ${result.code}`}`);
238
+ }
239
+ }
240
+ function disableSchedule(config, options = {}) {
241
+ const platform = schedulePlatform(options.platform);
242
+ if (!platform) throw new Error(`autoupdate is not supported on ${options.platform ?? process.platform}.`);
243
+ const exec = options.exec ?? defaultExec;
244
+ const home = homeOf(options);
245
+ if (platform === "win32") {
246
+ const result = exec("schtasks", ["/Delete", "/TN", scheduleName(config), "/F"]);
247
+ if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
248
+ throw new Error(`could not turn autoupdate off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
249
+ }
250
+ return;
251
+ }
252
+ if (platform === "darwin") {
253
+ const label2 = scheduleLabel(config);
254
+ exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
255
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
256
+ return;
257
+ }
258
+ const label = scheduleLabel(config);
259
+ const dir = (0, import_node_path.join)(home, ".config", "systemd", "user");
260
+ exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
261
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.service`), { force: true });
262
+ (0, import_node_fs.rmSync)((0, import_node_path.join)(dir, `${label}.timer`), { force: true });
263
+ }
264
+ function querySchedule(config, options = {}) {
265
+ const platform = schedulePlatform(options.platform);
266
+ if (!platform) return { supported: false, enabled: false };
267
+ const exec = options.exec ?? defaultExec;
268
+ const home = homeOf(options);
269
+ if (platform === "win32") {
270
+ const result = exec("schtasks", ["/Query", "/TN", scheduleName(config), "/FO", "LIST", "/V"]);
271
+ if (result.code !== 0) return { supported: true, enabled: false };
272
+ const lastRun = valueOf(result.stdout, "Last Run Time:");
273
+ const nextRun = valueOf(result.stdout, "Next Run Time:");
274
+ const state2 = { supported: true, enabled: true, cadence: "hourly" };
275
+ if (lastRun && !/disabled|never|not run/i.test(lastRun)) state2.lastRun = lastRun;
276
+ else if (nextRun && !/disabled|never/i.test(nextRun)) state2.lastRun = `next run ${nextRun}`;
277
+ return state2;
278
+ }
279
+ if (platform === "darwin") {
280
+ const plist = (0, import_node_path.join)(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
281
+ if (!(0, import_node_fs.existsSync)(plist)) return { supported: true, enabled: false };
282
+ return { supported: true, enabled: true, cadence: "hourly" };
283
+ }
284
+ const timer = (0, import_node_path.join)(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
285
+ if (!(0, import_node_fs.existsSync)(timer)) return { supported: true, enabled: false };
286
+ const state = { supported: true, enabled: true, cadence: "hourly" };
287
+ const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
288
+ const stamp = (shown.stdout ?? "").trim();
289
+ if (shown.code === 0 && stamp && stamp !== "n/a") state.lastRun = stamp;
290
+ return state;
291
+ }
292
+ function darwinPlist(label, command) {
293
+ const args = command.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
294
+ return `<?xml version="1.0" encoding="UTF-8"?>
295
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
296
+ <plist version="1.0">
297
+ <dict>
298
+ <key>Label</key>
299
+ <string>${xmlEscape(label)}</string>
300
+ <key>ProgramArguments</key>
301
+ <array>
302
+ ${args}
303
+ </array>
304
+ <key>StartInterval</key>
305
+ <integer>3600</integer>
306
+ </dict>
307
+ </plist>
308
+ `;
309
+ }
310
+ function linuxService(command) {
311
+ const line = command.map((arg) => /[\s"\\]/.test(arg) ? `"${arg.replace(/(["\\])/g, "\\$1")}"` : arg).join(" ");
312
+ return `[Unit]
313
+ Description=${"Hourly update check"}
314
+ [Service]
315
+ Type=oneshot
316
+ ExecStart=${line}
317
+ `;
318
+ }
319
+ function linuxTimer(label) {
320
+ return `[Unit]
321
+ Description=Hourly update check for ${label}
322
+ [Timer]
323
+ OnCalendar=hourly
324
+ Persistent=true
325
+ [Install]
326
+ WantedBy=timers.target
327
+ `;
328
+ }
329
+ function xmlEscape(text) {
330
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
331
+ }
332
+ function valueOf(output, key) {
333
+ const line = output.split(/\r?\n/).find((candidate) => candidate.trimStart().startsWith(key));
334
+ if (!line) return null;
335
+ const value = line.slice(line.indexOf(key) + key.length).trim();
336
+ return value ? value : null;
337
+ }
338
+ function firstLine(text) {
339
+ return String(text).split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
340
+ }
341
+
342
+ // src/config.ts
343
+ var import_node_fs2 = require("node:fs");
39
344
  var import_node_sea = require("node:sea");
40
345
 
41
346
  // src/module-url.ts
@@ -58,10 +363,10 @@ function loadProductConfig(options = {}) {
58
363
  const explicit = options.configPath ?? process.env.LAUNCHER_CONFIG;
59
364
  const devFallback = new URL("../config/product.template.json", moduleUrl());
60
365
  if (explicit) {
61
- return parseProductConfig((0, import_node_fs.readFileSync)(explicit, "utf8"));
366
+ return parseProductConfig((0, import_node_fs2.readFileSync)(explicit, "utf8"));
62
367
  }
63
368
  try {
64
- return parseProductConfig((0, import_node_fs.readFileSync)(devFallback, "utf8"));
369
+ return parseProductConfig((0, import_node_fs2.readFileSync)(devFallback, "utf8"));
65
370
  } catch {
66
371
  }
67
372
  try {
@@ -116,14 +421,14 @@ function field(record, key) {
116
421
  }
117
422
 
118
423
  // src/login-github.ts
119
- var import_node_child_process = require("node:child_process");
424
+ var import_node_child_process2 = require("node:child_process");
120
425
  var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
121
426
  function openBrowser(url) {
122
427
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
123
428
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
124
429
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
125
430
  try {
126
- const child = (0, import_node_child_process.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
431
+ const child = (0, import_node_child_process2.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
127
432
  child.on("error", () => {
128
433
  });
129
434
  child.unref();
@@ -350,9 +655,9 @@ function page(title, body) {
350
655
 
351
656
  // src/payload.ts
352
657
  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");
658
+ var import_node_fs3 = require("node:fs");
659
+ var import_node_os2 = require("node:os");
660
+ var import_node_path2 = require("node:path");
356
661
 
357
662
  // src/canonical.ts
358
663
  function canonicalJson(value) {
@@ -492,56 +797,56 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl) {
492
797
  }
493
798
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
494
799
  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 });
800
+ const staging = (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
801
+ (0, import_node_fs3.mkdirSync)(staging, { recursive: true });
497
802
  try {
498
803
  for (const entry of manifest.files) {
499
804
  const bytes = await fetchFileBytes(config, accessToken, entry.path, fetchImpl);
500
805
  if (!verifyFileBytes(entry, bytes)) {
501
806
  throw new Error(`file ${entry.path} failed its checksum \u2014 refusing to install anything.`);
502
807
  }
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);
808
+ const dest = (0, import_node_path2.join)(staging, entry.path);
809
+ (0, import_node_fs3.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
810
+ (0, import_node_fs3.writeFileSync)(dest, bytes);
506
811
  }
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);
812
+ const target = (0, import_node_path2.join)(dir, "payload");
813
+ (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
814
+ (0, import_node_fs3.rmSync)(target, { force: true, recursive: true });
815
+ (0, import_node_fs3.renameSync)(staging, target);
511
816
  } catch (error) {
512
- (0, import_node_fs2.rmSync)(staging, { force: true, recursive: true });
817
+ (0, import_node_fs3.rmSync)(staging, { force: true, recursive: true });
513
818
  throw error;
514
819
  }
515
820
  return manifest.version;
516
821
  }
517
822
 
518
823
  // 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");
824
+ var import_node_fs4 = require("node:fs");
825
+ var import_node_os3 = require("node:os");
826
+ var import_node_path3 = require("node:path");
522
827
  function defaultProductDir(product) {
523
828
  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);
829
+ const base = process.env.LOCALAPPDATA ?? (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), "launcher-fallback");
830
+ return (0, import_node_path3.join)(base, product);
526
831
  }
527
- const home = process.env.HOME ?? (0, import_node_os2.tmpdir)();
528
- return (0, import_node_path2.join)(home, `.${product}`);
832
+ const home = process.env.HOME ?? (0, import_node_os3.tmpdir)();
833
+ return (0, import_node_path3.join)(home, `.${product}`);
529
834
  }
530
835
  function resolveProductDir(product, explicit) {
531
836
  return explicit ?? process.env.LAUNCHER_DIR ?? defaultProductDir(product);
532
837
  }
533
838
  function tokensPath(dir) {
534
- return (0, import_node_path2.join)(dir, "tokens.json");
839
+ return (0, import_node_path3.join)(dir, "tokens.json");
535
840
  }
536
841
  function statePath(dir) {
537
- return (0, import_node_path2.join)(dir, "state.json");
842
+ return (0, import_node_path3.join)(dir, "state.json");
538
843
  }
539
844
  function payloadDir(dir) {
540
- return (0, import_node_path2.join)(dir, "payload");
845
+ return (0, import_node_path3.join)(dir, "payload");
541
846
  }
542
847
  function readTokens(dir) {
543
848
  try {
544
- const data = JSON.parse((0, import_node_fs3.readFileSync)(tokensPath(dir), "utf8"));
849
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(tokensPath(dir), "utf8"));
545
850
  if (typeof data.accessToken !== "string" || !data.accessToken) return null;
546
851
  if (typeof data.expiresAt !== "number" || !Number.isFinite(data.expiresAt)) return null;
547
852
  const tokens = { accessToken: data.accessToken, expiresAt: data.expiresAt };
@@ -553,21 +858,21 @@ function readTokens(dir) {
553
858
  }
554
859
  }
555
860
  function writeTokens(dir, tokens) {
556
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
861
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
557
862
  try {
558
- (0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
863
+ (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
559
864
  `, { mode: 384 });
560
865
  } catch {
561
- (0, import_node_fs3.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
866
+ (0, import_node_fs4.writeFileSync)(tokensPath(dir), `${JSON.stringify(tokens)}
562
867
  `);
563
868
  }
564
869
  }
565
870
  function clearTokens(dir) {
566
- (0, import_node_fs3.rmSync)(tokensPath(dir), { force: true });
871
+ (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
567
872
  }
568
873
  function readState(dir) {
569
874
  try {
570
- const data = JSON.parse((0, import_node_fs3.readFileSync)(statePath(dir), "utf8"));
875
+ const data = JSON.parse((0, import_node_fs4.readFileSync)(statePath(dir), "utf8"));
571
876
  if (typeof data.version !== "string" || !data.version) return null;
572
877
  return { version: data.version, updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : "" };
573
878
  } catch {
@@ -575,18 +880,18 @@ function readState(dir) {
575
880
  }
576
881
  }
577
882
  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)}
883
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
884
+ (0, import_node_fs4.writeFileSync)(statePath(dir), `${JSON.stringify(state, null, 2)}
580
885
  `);
581
886
  }
582
887
  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 });
888
+ (0, import_node_fs4.rmSync)(tokensPath(dir), { force: true });
889
+ (0, import_node_fs4.rmSync)(statePath(dir), { force: true });
890
+ (0, import_node_fs4.rmSync)(payloadDir(dir), { force: true, recursive: true });
586
891
  }
587
892
 
588
893
  // src/index.ts
589
- var LAUNCHER_VERSION = true ? "0.1.2" : readVersionFromPackage();
894
+ var LAUNCHER_VERSION = true ? "0.1.3" : readVersionFromPackage();
590
895
  function defaultPrint(message) {
591
896
  process.stdout.write(`${message}
592
897
  `);
@@ -633,12 +938,11 @@ async function run(rawOptions = {}) {
633
938
  case "update":
634
939
  return await doUpdate(config, dir, rawOptions, print);
635
940
  case "doctor":
636
- doDoctor(config, dir, print);
637
- return 0;
941
+ return doDoctor(config, dir, rawOptions, print);
942
+ case "autoupdate":
943
+ return doAutoupdate(config, rawOptions, positional.slice(1), print);
638
944
  default:
639
- print(`unknown command: ${command}`);
640
- printUsage(config, print);
641
- return 2;
945
+ return doForward(config, dir, rawOptions, command, argv, print);
642
946
  }
643
947
  } catch (error) {
644
948
  if (error instanceof NeedsLoginError) {
@@ -657,16 +961,17 @@ function flagValue(argv, flag) {
657
961
  }
658
962
  function printUsage(config, print) {
659
963
  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>]");
964
+ print("usage: launcher <login|logout|install|update|doctor|autoupdate on|off|status> [--config <path>] [--dir <path>]");
661
965
  print(" launcher --run <file> [args\u2026] (run a payload file with the embedded runtime)");
966
+ print(" launcher <verb> [args\u2026] (forwarded to the payload when it declares run + verbs)");
662
967
  }
663
968
  async function runFile(file, args, printErr) {
664
969
  if (!file) {
665
970
  printErr("launcher --run needs a file to run.");
666
971
  return 1;
667
972
  }
668
- const abs = (0, import_node_path3.resolve)(process.cwd(), file);
669
- if (!(0, import_node_fs4.existsSync)(abs)) {
973
+ const abs = (0, import_node_path4.resolve)(process.cwd(), file);
974
+ if (!(0, import_node_fs5.existsSync)(abs)) {
670
975
  printErr(`cannot run ${file}: no such file.`);
671
976
  return 1;
672
977
  }
@@ -752,10 +1057,10 @@ async function fetchAccessTokenOrLogin(config, dir, options, print) {
752
1057
  if (!after) throw new Error("sign-in did not produce a token");
753
1058
  return after.accessToken;
754
1059
  }
755
- function readPayloadEntry(dir) {
1060
+ function readPayloadArgv(dir, key) {
756
1061
  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;
1062
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1063
+ const entry = parsed[key];
759
1064
  if (typeof entry === "string") {
760
1065
  const parts = entry.trim().split(/\s+/).filter(Boolean);
761
1066
  return parts.length > 0 ? parts : null;
@@ -768,67 +1073,213 @@ function readPayloadEntry(dir) {
768
1073
  return null;
769
1074
  }
770
1075
  }
1076
+ function readPayloadEntry(dir) {
1077
+ return readPayloadArgv(dir, "entry");
1078
+ }
1079
+ function readPayloadRun(dir) {
1080
+ return readPayloadArgv(dir, "run");
1081
+ }
1082
+ function readPayloadVerbs(dir) {
1083
+ try {
1084
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(payloadDir(dir), "payload.json"), "utf8"));
1085
+ const verbs = parsed.verbs;
1086
+ if (verbs === "*") return "*";
1087
+ if (Array.isArray(verbs) && verbs.every((v) => typeof v === "string" && v.trim().length > 0)) {
1088
+ return verbs;
1089
+ }
1090
+ return null;
1091
+ } catch {
1092
+ return null;
1093
+ }
1094
+ }
771
1095
  function resolveEntry(entry) {
772
1096
  return entry[0] === "$self" ? [process.execPath, ...entry.slice(1)] : entry;
773
1097
  }
774
1098
  function needsShell(command) {
775
1099
  if (process.platform !== "win32") return false;
776
1100
  if (/\.(cmd|bat)$/i.test(command)) return true;
777
- return !(0, import_node_fs4.existsSync)(command);
1101
+ return !(0, import_node_fs5.existsSync)(command);
1102
+ }
1103
+ function quoteForShell(arg) {
1104
+ return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
778
1105
  }
779
1106
  function defaultRunEntry(entry, cwd) {
780
1107
  const [command, ...args] = entry;
781
- const result = (0, import_node_child_process2.spawnSync)(command, args, {
1108
+ const shell = needsShell(command);
1109
+ const commandLine = shell ? [command, ...args].map(quoteForShell).join(" ") : command;
1110
+ const result = (0, import_node_child_process3.spawnSync)(commandLine, shell ? [] : args, {
782
1111
  cwd,
783
1112
  stdio: "inherit",
784
- shell: needsShell(command),
1113
+ shell,
785
1114
  windowsHide: true
786
1115
  });
787
1116
  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}` };
1117
+ const code = typeof result.status === "number" ? result.status : void 0;
1118
+ return { ok: result.status === 0, error: result.status === 0 ? void 0 : `exit code ${result.status}`, code };
1119
+ }
1120
+ function faceProduct(config) {
1121
+ const known = { mmi: "mmi-hub", jerv: "jerv-hub" };
1122
+ return known[config.product] ?? config.product;
1123
+ }
1124
+ function faceFor(config, options) {
1125
+ const tty = options.tty ?? Boolean(process.stdout.isTTY);
1126
+ try {
1127
+ return createFace({
1128
+ product: faceProduct(config),
1129
+ color: tty && !process.env.NO_COLOR && process.env.TERM !== "dumb",
1130
+ columns: process.stdout.columns
1131
+ });
1132
+ } catch {
1133
+ return null;
1134
+ }
1135
+ }
1136
+ function since(start) {
1137
+ return (Date.now() - start) / 1e3;
1138
+ }
1139
+ function printReceipt(face, config, print, ready, lines) {
1140
+ const name = face ? face.identity.name : config.product;
1141
+ const headline = ready ? `${name} is ready.` : `${name} is installed, but not ready yet.`;
1142
+ if (!face) {
1143
+ print(headline);
1144
+ for (const line of lines) print(line.trim());
1145
+ return;
1146
+ }
1147
+ const glyph = ready ? "\u2714" : "\u2716";
1148
+ for (const line of face.receipt([`${glyph} ${headline}`, ...lines])) print(line);
1149
+ print(face.signOff());
1150
+ }
1151
+ function printStep(face, print, title, seconds, kind = "ok") {
1152
+ print(face ? face.step(title, seconds, kind) : title);
789
1153
  }
790
1154
  async function doInstall(config, dir, options, print) {
791
1155
  const fetchImpl = options.fetchImpl ?? fetch;
1156
+ const face = faceFor(config, options);
792
1157
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1158
+ const started = Date.now();
793
1159
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
794
1160
  await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
795
1161
  writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
796
- print(`installed ${config.product} ${manifest.version} into ${dir}.`);
1162
+ printStep(face, print, `installed ${config.product} ${manifest.version} into ${dir}`, since(started));
1163
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1164
+ }
1165
+ async function doUpdate(config, dir, options, print) {
1166
+ const fetchImpl = options.fetchImpl ?? fetch;
1167
+ const face = faceFor(config, options);
1168
+ if (face) for (const line of face.welcome()) print(line);
1169
+ const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print);
1170
+ const started = Date.now();
1171
+ const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
1172
+ const current = readState(dir);
1173
+ if (current && current.version === manifest.version && (0, import_node_fs5.existsSync)(payloadDir(dir))) {
1174
+ printStep(face, print, `${config.product} is already up to date at ${manifest.version}`, since(started));
1175
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1176
+ }
1177
+ await downloadAndUnpack(config, dir, manifest, accessToken, { fetchImpl });
1178
+ writeState(dir, { version: manifest.version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1179
+ printStep(face, print, `updated ${config.product} to ${manifest.version}`, since(started));
1180
+ return finishLastMile(config, dir, options, print, face, manifest.version);
1181
+ }
1182
+ function finishLastMile(config, dir, options, print, face, version) {
797
1183
  const entry = readPayloadEntry(dir);
1184
+ const payload = payloadDir(dir);
798
1185
  if (!entry) {
799
- print(`next step: run ${(0, import_node_path3.join)(payloadDir(dir), config.binName)} to start ${config.product}.`);
1186
+ printReceipt(face, config, print, true, [
1187
+ ` Installed ${version} into ${dir}`,
1188
+ ` next step: run ${(0, import_node_path4.join)(payload, config.binName)} to start ${config.product}.`
1189
+ ]);
800
1190
  return 0;
801
1191
  }
802
- const payload = payloadDir(dir);
803
1192
  const command = resolveEntry(entry);
804
- const runEntry = options.runEntry ?? defaultRunEntry;
805
- const result = runEntry(command, payload);
1193
+ const started = Date.now();
1194
+ const result = (options.runEntry ?? defaultRunEntry)(command, payload);
806
1195
  if (result.ok) {
807
- print(`${config.product} is ready.`);
1196
+ printStep(face, print, "Armed this machine", since(started));
1197
+ printReceipt(face, config, print, true, [
1198
+ ` Installed ${version} into ${dir}`,
1199
+ ` Check health any time: ${config.binName} doctor`
1200
+ ]);
808
1201
  return 0;
809
1202
  }
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;
1203
+ const code = result.code ?? 1;
1204
+ printStep(face, print, `Arming this machine did not finish (exit code ${code})`, null, "fail");
1205
+ printReceipt(face, config, print, false, [
1206
+ ` Downloaded ${version} into ${dir}`,
1207
+ ` Finish it with: (cd ${payload} && ${command.join(" ")})`
1208
+ ]);
1209
+ return code;
813
1210
  }
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}.`);
1211
+ function doForward(config, dir, options, command, argv, print) {
1212
+ if (command && (0, import_node_fs5.existsSync)(command)) {
1213
+ print(`${command} is a file, not a command \u2014 did you mean \`--run ${command}\`?`);
1214
+ return 2;
1215
+ }
1216
+ const target = readPayloadRun(dir);
1217
+ const verbs = readPayloadVerbs(dir);
1218
+ const declared = verbs === "*" || Array.isArray(verbs) && command !== void 0 && verbs.includes(command);
1219
+ if (!target || !declared) {
1220
+ print(`unknown command: ${command}`);
1221
+ printUsage(config, print);
1222
+ return 2;
1223
+ }
1224
+ const forwarded = [...resolveEntry(target), ...argv];
1225
+ const result = (options.runEntry ?? defaultRunEntry)(forwarded, payloadDir(dir));
1226
+ if (!result.ok && result.code === void 0) {
1227
+ print(`failed: ${result.error ?? `could not run ${forwarded[0]}`}`);
1228
+ return 1;
1229
+ }
1230
+ return result.code ?? 0;
1231
+ }
1232
+ function doAutoupdate(config, options, args, print) {
1233
+ const mode = args[0] ?? "status";
1234
+ const scheduleOptions = options.autoupdate ?? {};
1235
+ const command = options.autoupdate?.command ?? [process.execPath, "update"];
1236
+ if (mode === "on") {
1237
+ try {
1238
+ enableSchedule(config, command, scheduleOptions);
1239
+ } catch (error) {
1240
+ print(error.message);
1241
+ return 1;
1242
+ }
1243
+ const state2 = querySchedule(config, scheduleOptions);
1244
+ print(`auto-update: on (${state2.cadence ?? "hourly"})`);
821
1245
  return 0;
822
1246
  }
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}.`);
1247
+ if (mode === "off") {
1248
+ try {
1249
+ disableSchedule(config, scheduleOptions);
1250
+ } catch (error) {
1251
+ print(error.message);
1252
+ return 1;
1253
+ }
1254
+ print("auto-update: off");
1255
+ return 0;
1256
+ }
1257
+ if (mode !== "status") {
1258
+ print(`unknown autoupdate mode: ${mode} \u2014 use on, off or status.`);
1259
+ return 1;
1260
+ }
1261
+ const state = querySchedule(config, scheduleOptions);
1262
+ if (!state.supported) {
1263
+ print("auto-update: unsupported on this platform");
1264
+ return 1;
1265
+ }
1266
+ print(`auto-update: ${state.enabled ? `on (${state.cadence ?? "hourly"})` : "off"}`);
1267
+ if (state.lastRun) print(`last run: ${state.lastRun}`);
826
1268
  return 0;
827
1269
  }
828
- function doDoctor(config, dir, print) {
1270
+ function doDoctor(config, dir, options, print) {
1271
+ doLauncherDoctor(config, dir, options, print);
1272
+ const target = readPayloadRun(dir);
1273
+ const verbs = readPayloadVerbs(dir);
1274
+ const chains = target !== null && (verbs === "*" || Array.isArray(verbs) && verbs.includes("doctor"));
1275
+ if (!chains) return 0;
1276
+ const result = (options.runEntry ?? defaultRunEntry)([...resolveEntry(target), "doctor"], payloadDir(dir));
1277
+ return result.code ?? (result.ok ? 0 : 1);
1278
+ }
1279
+ function doLauncherDoctor(config, dir, options, print) {
829
1280
  const tokens = readTokens(dir);
830
1281
  const state = readState(dir);
831
- const payloadPresent = (0, import_node_fs4.existsSync)(payloadDir(dir));
1282
+ const payloadPresent = (0, import_node_fs5.existsSync)(payloadDir(dir));
832
1283
  print(`product: ${config.product}`);
833
1284
  print(`host: ${config.host}`);
834
1285
  print(`login: ${config.loginKind}`);
@@ -840,7 +1291,11 @@ function doDoctor(config, dir, print) {
840
1291
  print("token: none \u2014 run login first.");
841
1292
  }
842
1293
  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)}`);
1294
+ print(`paths: tokens ${(0, import_node_path4.join)(dir, "tokens.json")}, state ${(0, import_node_path4.join)(dir, "state.json")}, payload ${payloadDir(dir)}`);
1295
+ const schedule = querySchedule(config, options.autoupdate ?? {});
1296
+ print(
1297
+ `auto-update: ${!schedule.supported ? "unsupported on this platform" : schedule.enabled ? `on (${schedule.cadence ?? "hourly"})` : "off"}`
1298
+ );
844
1299
  }
845
1300
  var invokedAsMain = typeof process.argv[1] === "string" && (() => {
846
1301
  try {
@@ -871,6 +1326,8 @@ if (invokedAsMain) {
871
1326
  defaultRunEntry,
872
1327
  ensureFreshToken,
873
1328
  readPayloadEntry,
1329
+ readPayloadRun,
1330
+ readPayloadVerbs,
874
1331
  resolveEntry,
875
1332
  run,
876
1333
  runFile