@mutmutco/installer-launcher 0.1.1 → 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 +531 -76
- package/dist/launcher.sea.cjs +537 -80
- package/package.json +4 -1
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
|
|
6
|
-
import { join as
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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(
|
|
335
|
+
return parseProductConfig(readFileSync2(explicit, "utf8"));
|
|
33
336
|
}
|
|
34
337
|
try {
|
|
35
|
-
return parseProductConfig(
|
|
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 =
|
|
467
|
-
|
|
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 =
|
|
475
|
-
|
|
476
|
-
|
|
777
|
+
const dest = join2(staging, entry.path);
|
|
778
|
+
mkdirSync2(dirname(dest), { recursive: true });
|
|
779
|
+
writeFileSync2(dest, bytes);
|
|
477
780
|
}
|
|
478
|
-
const target =
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
|
491
|
-
import { tmpdir as
|
|
492
|
-
import { join as
|
|
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 ??
|
|
496
|
-
return
|
|
798
|
+
const base = process.env.LOCALAPPDATA ?? join3(tmpdir3(), "launcher-fallback");
|
|
799
|
+
return join3(base, product);
|
|
497
800
|
}
|
|
498
|
-
const home = process.env.HOME ??
|
|
499
|
-
return
|
|
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
|
|
808
|
+
return join3(dir, "tokens.json");
|
|
506
809
|
}
|
|
507
810
|
function statePath(dir) {
|
|
508
|
-
return
|
|
811
|
+
return join3(dir, "state.json");
|
|
509
812
|
}
|
|
510
813
|
function payloadDir(dir) {
|
|
511
|
-
return
|
|
814
|
+
return join3(dir, "payload");
|
|
512
815
|
}
|
|
513
816
|
function readTokens(dir) {
|
|
514
817
|
try {
|
|
515
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
830
|
+
mkdirSync3(dir, { recursive: true });
|
|
528
831
|
try {
|
|
529
|
-
|
|
832
|
+
writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
530
833
|
`, { mode: 384 });
|
|
531
834
|
} catch {
|
|
532
|
-
|
|
835
|
+
writeFileSync3(tokensPath(dir), `${JSON.stringify(tokens)}
|
|
533
836
|
`);
|
|
534
837
|
}
|
|
535
838
|
}
|
|
536
839
|
function clearTokens(dir) {
|
|
537
|
-
|
|
840
|
+
rmSync3(tokensPath(dir), { force: true });
|
|
538
841
|
}
|
|
539
842
|
function readState(dir) {
|
|
540
843
|
try {
|
|
541
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
550
|
-
|
|
852
|
+
mkdirSync3(dir, { recursive: true });
|
|
853
|
+
writeFileSync3(statePath(dir), `${JSON.stringify(state, null, 2)}
|
|
551
854
|
`);
|
|
552
855
|
}
|
|
553
856
|
function wipeProductDir(dir) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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 = "0.1.
|
|
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
|
-
|
|
910
|
+
return doDoctor(config, dir, rawOptions, print);
|
|
911
|
+
case "autoupdate":
|
|
912
|
+
return doAutoupdate(config, rawOptions, positional.slice(1), print);
|
|
609
913
|
default:
|
|
610
|
-
|
|
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 (!
|
|
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
|
|
1029
|
+
function readPayloadArgv(dir, key) {
|
|
727
1030
|
try {
|
|
728
|
-
const parsed = JSON.parse(
|
|
729
|
-
const entry = parsed
|
|
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 !
|
|
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
|
|
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
|
|
1082
|
+
shell,
|
|
756
1083
|
windowsHide: true
|
|
757
1084
|
});
|
|
758
1085
|
if (result.error) return { ok: false, error: result.error.message };
|
|
759
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
print
|
|
783
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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 =
|
|
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 ${
|
|
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
|