@gajae-gateway/cli 0.1.0
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/list.d.ts +55 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/main.d.ts +50 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +792 -0
- package/dist/services.d.ts +40 -0
- package/dist/services.d.ts.map +1 -0
- package/package.json +50 -0
- package/src/list.ts +187 -0
- package/src/main.ts +576 -0
- package/src/services.ts +224 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/main.ts
|
|
5
|
+
import { copyFile, stat } from "fs/promises";
|
|
6
|
+
import { isAbsolute, join as join2 } from "path";
|
|
7
|
+
import { LOOPBACK_ORIGIN, originKey as originKey2, parseOriginKey } from "@gajae-gateway/protocol";
|
|
8
|
+
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
9
|
+
|
|
10
|
+
// src/list.ts
|
|
11
|
+
import { originKey } from "@gajae-gateway/protocol";
|
|
12
|
+
var MAX_CELL_WIDTH = 40;
|
|
13
|
+
function columnNames(columns) {
|
|
14
|
+
return columns.map((column) => column.name);
|
|
15
|
+
}
|
|
16
|
+
function positiveInteger(flag, raw) {
|
|
17
|
+
const value = Number(raw);
|
|
18
|
+
if (raw === undefined || raw === "" || !Number.isInteger(value) || value < 0)
|
|
19
|
+
throw new Error(`${flag} expects a non-negative integer, got: ${raw ?? "(missing)"}`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function parseListOptions(args, columns) {
|
|
23
|
+
const valid = columnNames(columns);
|
|
24
|
+
let json = false;
|
|
25
|
+
let fields;
|
|
26
|
+
let limit;
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (let i = 0;i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg === "--json")
|
|
31
|
+
json = true;
|
|
32
|
+
else if (arg === "--fields") {
|
|
33
|
+
const raw = args[++i];
|
|
34
|
+
if (!raw)
|
|
35
|
+
throw new Error(`--fields expects a comma-separated column list (valid: ${valid.join(", ")})`);
|
|
36
|
+
fields = raw.split(",").map((field) => field.trim()).filter((field) => field.length > 0);
|
|
37
|
+
if (fields.length === 0)
|
|
38
|
+
throw new Error(`--fields expects a comma-separated column list (valid: ${valid.join(", ")})`);
|
|
39
|
+
const unknown = fields.filter((field) => !valid.includes(field));
|
|
40
|
+
if (unknown.length > 0)
|
|
41
|
+
throw new Error(`unknown field(s): ${unknown.join(", ")} (valid: ${valid.join(", ")})`);
|
|
42
|
+
} else if (arg === "--limit")
|
|
43
|
+
limit = positiveInteger("--limit", args[++i]);
|
|
44
|
+
else if (arg === "--offset")
|
|
45
|
+
offset = positiveInteger("--offset", args[++i]);
|
|
46
|
+
else
|
|
47
|
+
throw new Error(`unknown option: ${arg}`);
|
|
48
|
+
}
|
|
49
|
+
return { json, ...fields ? { fields } : {}, ...limit === undefined ? {} : { limit }, offset };
|
|
50
|
+
}
|
|
51
|
+
function selectColumns(columns, fields) {
|
|
52
|
+
if (!fields)
|
|
53
|
+
return [...columns];
|
|
54
|
+
return fields.map((field) => columns.find((column) => column.name === field));
|
|
55
|
+
}
|
|
56
|
+
function pageRows(rows, options) {
|
|
57
|
+
const entries = rows.map((row, index) => ({ row, index }));
|
|
58
|
+
const start = Math.min(options.offset, entries.length);
|
|
59
|
+
return options.limit === undefined ? entries.slice(start) : entries.slice(start, start + options.limit);
|
|
60
|
+
}
|
|
61
|
+
function headerLabel(name) {
|
|
62
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
63
|
+
}
|
|
64
|
+
function formatCell(value) {
|
|
65
|
+
if (value === null || value === undefined)
|
|
66
|
+
return "-";
|
|
67
|
+
const text = Array.isArray(value) ? value.length === 0 ? "-" : value.join(",") : String(value);
|
|
68
|
+
if (text === "")
|
|
69
|
+
return "-";
|
|
70
|
+
return text.length > MAX_CELL_WIDTH ? `${text.slice(0, MAX_CELL_WIDTH - 1)}\u2026` : text;
|
|
71
|
+
}
|
|
72
|
+
function renderTable(columns, entries) {
|
|
73
|
+
if (columns.length === 0)
|
|
74
|
+
return ["(no columns)"];
|
|
75
|
+
const header = columns.map((column) => headerLabel(column.name));
|
|
76
|
+
const body = entries.map((entry) => columns.map((column) => formatCell(column.value(entry.row, entry.index))));
|
|
77
|
+
if (body.length === 0)
|
|
78
|
+
return [header.join(" "), "(none)"];
|
|
79
|
+
const widths = header.map((cell, index) => Math.max(cell.length, ...body.map((row) => row[index].length)));
|
|
80
|
+
const line = (cells) => cells.map((cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index])).join(" ").trimEnd();
|
|
81
|
+
return [line(header), ...body.map(line)];
|
|
82
|
+
}
|
|
83
|
+
function projectRows(columns, entries) {
|
|
84
|
+
return entries.map((entry) => Object.fromEntries(columns.map((column) => [column.name, column.value(entry.row, entry.index)])));
|
|
85
|
+
}
|
|
86
|
+
function triggerSummary(monitor) {
|
|
87
|
+
const trigger = monitor.trigger;
|
|
88
|
+
switch (trigger.kind) {
|
|
89
|
+
case "cron":
|
|
90
|
+
return trigger.schedule;
|
|
91
|
+
case "webhook":
|
|
92
|
+
return `webhook:${trigger.route}`;
|
|
93
|
+
case "watcher":
|
|
94
|
+
return `watch:${trigger.root}`;
|
|
95
|
+
case "script":
|
|
96
|
+
return `script:${trigger.intervalMs}ms`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
var MONITOR_COLUMNS = [
|
|
100
|
+
{ name: "id", value: (monitor) => monitor.monitorId.slice(0, 8) },
|
|
101
|
+
{ name: "name", value: (monitor) => monitor.name },
|
|
102
|
+
{ name: "schedule", value: triggerSummary },
|
|
103
|
+
{ name: "events", value: (monitor) => [...monitor.eventTypes] },
|
|
104
|
+
{ name: "target", value: (monitor) => monitor.channelTarget ? originKey(monitor.channelTarget.origin) : null },
|
|
105
|
+
{ name: "enabled", value: (monitor) => monitor.enabled }
|
|
106
|
+
];
|
|
107
|
+
var SESSION_COLUMNS = [
|
|
108
|
+
{ name: "index", value: (_session, index) => index },
|
|
109
|
+
{ name: "origin", value: (session) => originKey(session.origin) },
|
|
110
|
+
{ name: "epoch", value: (session) => session.epoch },
|
|
111
|
+
{
|
|
112
|
+
name: "bootstrap",
|
|
113
|
+
value: (session) => session.bootstrap.pending ? "pending" : `${session.bootstrap.byteCount}B${session.bootstrap.truncated ? "/trunc" : ""}`
|
|
114
|
+
},
|
|
115
|
+
{ name: "createdAt", value: (session) => session.createdAt },
|
|
116
|
+
{ name: "lastActivityAt", value: (session) => session.lastActivityAt }
|
|
117
|
+
];
|
|
118
|
+
function renderList(columns, rows, options, envelope) {
|
|
119
|
+
const entries = pageRows(rows, options);
|
|
120
|
+
const selected = selectColumns(columns, options.fields);
|
|
121
|
+
if (!options.json)
|
|
122
|
+
return renderTable(selected, entries);
|
|
123
|
+
if (!options.fields && options.limit === undefined && options.offset === 0)
|
|
124
|
+
return [JSON.stringify(envelope.result)];
|
|
125
|
+
const projected = options.fields ? projectRows(selected, entries) : entries.map((entry) => entry.row);
|
|
126
|
+
return [JSON.stringify({ ...envelope.result, [envelope.key]: projected })];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/services.ts
|
|
130
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
131
|
+
import { homedir } from "os";
|
|
132
|
+
import { join } from "path";
|
|
133
|
+
import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol";
|
|
134
|
+
var CONFIG_SCHEMA_VERSION = 1;
|
|
135
|
+
var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
136
|
+
var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
|
|
137
|
+
var SERVICE_SPECS = [
|
|
138
|
+
{ id: "gateway", label: "dev.gajaeway.gateway", binary: "gajaeway-gateway", args: ["daemon"] },
|
|
139
|
+
{
|
|
140
|
+
id: "adapter-discord",
|
|
141
|
+
label: "dev.gajaeway.adapter-discord",
|
|
142
|
+
binary: "gajaeway-discord",
|
|
143
|
+
args: []
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "adapter-slack",
|
|
147
|
+
label: "dev.gajaeway.adapter-slack",
|
|
148
|
+
binary: "gajaeway-slack",
|
|
149
|
+
args: []
|
|
150
|
+
},
|
|
151
|
+
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"] }
|
|
152
|
+
];
|
|
153
|
+
function homeForEnvironment(env) {
|
|
154
|
+
return env.GAJAEWAY_HOME || join(env.HOME || homedir(), ".gajaeway");
|
|
155
|
+
}
|
|
156
|
+
function expandHome(value, home) {
|
|
157
|
+
return value.startsWith("~/") ? join(home, value.slice(2)) : value;
|
|
158
|
+
}
|
|
159
|
+
function normalizedEntries(entries, home) {
|
|
160
|
+
const seen = new Set;
|
|
161
|
+
const result = [];
|
|
162
|
+
for (const raw of entries) {
|
|
163
|
+
if (raw.length === 0)
|
|
164
|
+
continue;
|
|
165
|
+
const entry = expandHome(raw, home);
|
|
166
|
+
if (seen.has(entry))
|
|
167
|
+
continue;
|
|
168
|
+
seen.add(entry);
|
|
169
|
+
result.push(entry);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
function mergePathEntries(binDir, userHome, loginPath) {
|
|
174
|
+
return normalizedEntries([
|
|
175
|
+
expandHome(binDir, userHome),
|
|
176
|
+
join(userHome, "bin"),
|
|
177
|
+
join(userHome, ".local", "bin"),
|
|
178
|
+
join(userHome, ".bun", "bin"),
|
|
179
|
+
...loginPath.replace(/(?:\r\n|\n)+$/, "").split(":"),
|
|
180
|
+
...FALLBACK_PATH_DIRS
|
|
181
|
+
], userHome);
|
|
182
|
+
}
|
|
183
|
+
async function defaultLoginPathRunner(command) {
|
|
184
|
+
let child;
|
|
185
|
+
try {
|
|
186
|
+
child = Bun.spawn([...command], { stdout: "pipe", stderr: "pipe" });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`could not run login shell: ${error instanceof Error ? error.message : String(error)}`);
|
|
189
|
+
}
|
|
190
|
+
if (!(child.stdout instanceof ReadableStream))
|
|
191
|
+
throw new Error("login shell stdout was not piped");
|
|
192
|
+
const stdout = await new Response(child.stdout).text();
|
|
193
|
+
const exitCode = await child.exited;
|
|
194
|
+
if (exitCode !== 0)
|
|
195
|
+
throw new Error(`login shell exited with status ${exitCode}`);
|
|
196
|
+
return stdout;
|
|
197
|
+
}
|
|
198
|
+
async function discoverLoginPath(env = process.env, runner = defaultLoginPathRunner) {
|
|
199
|
+
const shell = env.SHELL || "/bin/zsh";
|
|
200
|
+
return await runner([shell, "-lc", LOGIN_PATH_COMMAND]);
|
|
201
|
+
}
|
|
202
|
+
async function resolvePath(options) {
|
|
203
|
+
const runtime = options.runtime;
|
|
204
|
+
const env = options.env;
|
|
205
|
+
const shellEnv = env ?? process.env;
|
|
206
|
+
const userHome = options.home;
|
|
207
|
+
let loginPath = "";
|
|
208
|
+
if (runtime?.path !== undefined)
|
|
209
|
+
loginPath = runtime.path.join(":");
|
|
210
|
+
else if (runtime?.inheritLoginPath !== false)
|
|
211
|
+
loginPath = options.loginPath ?? await discoverLoginPath(shellEnv, options.loginPathRunner ?? defaultLoginPathRunner);
|
|
212
|
+
return mergePathEntries(options.binDir, userHome, loginPath).join(":");
|
|
213
|
+
}
|
|
214
|
+
function escapeXml(value) {
|
|
215
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
216
|
+
}
|
|
217
|
+
function plistString(value) {
|
|
218
|
+
return `<string>${escapeXml(value)}</string>`;
|
|
219
|
+
}
|
|
220
|
+
function renderLaunchAgent(spec, binDir, home, path) {
|
|
221
|
+
const program = join(binDir, spec.binary);
|
|
222
|
+
const argumentsXml = [program, ...spec.args].map(plistString).join("");
|
|
223
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
224
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
225
|
+
<plist version="1.0"><dict>
|
|
226
|
+
<key>Label</key>${plistString(spec.label)}
|
|
227
|
+
<key>ProgramArguments</key><array>${argumentsXml}</array>
|
|
228
|
+
<key>WorkingDirectory</key>${plistString(binDir)}
|
|
229
|
+
<key>EnvironmentVariables</key><dict>
|
|
230
|
+
<key>GAJAEWAY_HOME</key>${plistString(home)}
|
|
231
|
+
<key>PATH</key>${plistString(path)}
|
|
232
|
+
</dict>
|
|
233
|
+
<key>RunAtLoad</key><true/>
|
|
234
|
+
<key>KeepAlive</key><true/>
|
|
235
|
+
<key>StandardOutPath</key>${plistString(join(home, `${spec.id}.stdout.log`))}
|
|
236
|
+
<key>StandardErrorPath</key>${plistString(join(home, `${spec.id}.stderr.log`))}
|
|
237
|
+
</dict></plist>
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
async function readRuntime(home) {
|
|
241
|
+
const configPath = join(home, "config.json");
|
|
242
|
+
let raw;
|
|
243
|
+
try {
|
|
244
|
+
raw = await readFile(configPath, "utf8");
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new Error(`could not read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
|
+
}
|
|
248
|
+
let parsed;
|
|
249
|
+
try {
|
|
250
|
+
parsed = JSON.parse(raw);
|
|
251
|
+
} catch {
|
|
252
|
+
throw new Error(`could not parse ${configPath}: malformed JSON`);
|
|
253
|
+
}
|
|
254
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
255
|
+
throw new Error(`invalid configuration ${configPath}: config must be an object`);
|
|
256
|
+
const input = parsed;
|
|
257
|
+
if (input.schemaVersion !== CONFIG_SCHEMA_VERSION)
|
|
258
|
+
throw new Error(`invalid configuration ${configPath}: schemaVersion must be ${CONFIG_SCHEMA_VERSION}`);
|
|
259
|
+
try {
|
|
260
|
+
return parseRuntimeConfig(input.runtime);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const message = error instanceof RuntimeConfigError ? error.message : String(error);
|
|
263
|
+
throw new Error(`invalid runtime configuration ${configPath}: ${message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function installServices(options) {
|
|
267
|
+
if (options.binDir.length === 0)
|
|
268
|
+
throw new Error("services requires a non-empty --bin-dir DIR");
|
|
269
|
+
const env = options.env ?? process.env;
|
|
270
|
+
const userHome = env.HOME || homedir();
|
|
271
|
+
const home = homeForEnvironment(env);
|
|
272
|
+
const binDir = expandHome(options.binDir, userHome);
|
|
273
|
+
const launchAgentsDir = expandHome(options.launchAgentsDir ?? join(userHome, "Library", "LaunchAgents"), userHome);
|
|
274
|
+
const runtime = await readRuntime(home);
|
|
275
|
+
const path = await resolvePath({
|
|
276
|
+
binDir,
|
|
277
|
+
home: userHome,
|
|
278
|
+
runtime,
|
|
279
|
+
env,
|
|
280
|
+
loginPathRunner: options.loginPathRunner
|
|
281
|
+
});
|
|
282
|
+
await mkdir(launchAgentsDir, { recursive: true, mode: 448 });
|
|
283
|
+
const plists = SERVICE_SPECS.map((spec) => ({
|
|
284
|
+
path: join(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`),
|
|
285
|
+
contents: renderLaunchAgent(spec, binDir, home, path)
|
|
286
|
+
}));
|
|
287
|
+
const write = options.writeFile;
|
|
288
|
+
for (const plist of plists) {
|
|
289
|
+
if (write)
|
|
290
|
+
await write(plist.path, plist.contents);
|
|
291
|
+
else {
|
|
292
|
+
await writeFile(plist.path, plist.contents, { encoding: "utf8", mode: 384 });
|
|
293
|
+
await chmod(plist.path, 384);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return plists.map((plist) => plist.path);
|
|
297
|
+
}
|
|
298
|
+
function serviceUsage() {
|
|
299
|
+
return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR]";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/main.ts
|
|
303
|
+
function socketPath(home = process.env.GAJAEWAY_HOME) {
|
|
304
|
+
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
305
|
+
}
|
|
306
|
+
var COMMANDS = [
|
|
307
|
+
"status",
|
|
308
|
+
"shutdown",
|
|
309
|
+
"chat",
|
|
310
|
+
"daemon",
|
|
311
|
+
"sessions",
|
|
312
|
+
"ops",
|
|
313
|
+
"memory",
|
|
314
|
+
"monitors",
|
|
315
|
+
"work",
|
|
316
|
+
"services"
|
|
317
|
+
];
|
|
318
|
+
var CLI_USAGE = "usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ...|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire <name>|work jobs|ops backup <path>|ops cycle [--json]|ops integrity|ops restore <backupPath>|services install|repair --bin-dir DIR [--launch-agents-dir DIR] (work run waits for a response; caller timeout does not end the attempt)";
|
|
319
|
+
var USAGE_EXIT_CODE = 2;
|
|
320
|
+
function usageFor(command) {
|
|
321
|
+
return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
|
|
322
|
+
}
|
|
323
|
+
function parseArgs(args) {
|
|
324
|
+
let socket = socketPath();
|
|
325
|
+
const rest = [];
|
|
326
|
+
for (let i = 0;i < args.length; i++) {
|
|
327
|
+
if (args[i] === "--socket")
|
|
328
|
+
socket = args[++i] ?? socket;
|
|
329
|
+
else
|
|
330
|
+
rest.push(args[i]);
|
|
331
|
+
}
|
|
332
|
+
return { command: rest[0], rest: rest.slice(1), socket };
|
|
333
|
+
}
|
|
334
|
+
function gatewayHome() {
|
|
335
|
+
return process.env.GAJAEWAY_HOME ?? `${process.env.HOME ?? "~"}/.gajaeway`;
|
|
336
|
+
}
|
|
337
|
+
function parseServicesArgs(args) {
|
|
338
|
+
const action = args[0];
|
|
339
|
+
if (action !== "install" && action !== "repair")
|
|
340
|
+
throw new Error(serviceUsage());
|
|
341
|
+
let binDir;
|
|
342
|
+
let launchAgentsDir;
|
|
343
|
+
for (let i = 1;i < args.length; i++) {
|
|
344
|
+
const flag = args[i];
|
|
345
|
+
if (flag === "--bin-dir" || flag === "--launch-agents-dir") {
|
|
346
|
+
const value = args[++i];
|
|
347
|
+
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
348
|
+
throw new Error(`${flag} expects a non-empty DIR`);
|
|
349
|
+
if (flag === "--bin-dir")
|
|
350
|
+
binDir = value;
|
|
351
|
+
else
|
|
352
|
+
launchAgentsDir = value;
|
|
353
|
+
} else
|
|
354
|
+
throw new Error(`unknown option: ${flag}`);
|
|
355
|
+
}
|
|
356
|
+
if (binDir === undefined)
|
|
357
|
+
throw new Error("services requires --bin-dir DIR");
|
|
358
|
+
return { action, binDir, ...launchAgentsDir === undefined ? {} : { launchAgentsDir } };
|
|
359
|
+
}
|
|
360
|
+
function renderCycle(cycle) {
|
|
361
|
+
const lines = [];
|
|
362
|
+
lines.push(`phase: ${cycle.phase}`);
|
|
363
|
+
if (cycle.gates.length > 0)
|
|
364
|
+
lines.push(`gates: ${cycle.gates.join(", ")}`);
|
|
365
|
+
else
|
|
366
|
+
lines.push("gates: none");
|
|
367
|
+
lines.push(`generatedAt: ${cycle.generatedAt}`);
|
|
368
|
+
lines.push(`instance: ${cycle.instanceId}`);
|
|
369
|
+
lines.push(`inbound: pending=${cycle.pendingInbound} inflight=${cycle.inFlightInbound}`);
|
|
370
|
+
lines.push(`context: unread=${cycle.contextDiff.unread} expired=${cycle.contextDiff.expired} truncated=${cycle.contextDiff.truncated} omitted_oldest=${cycle.contextDiff.omittedOldestAt ?? "-"} omitted_newest=${cycle.contextDiff.omittedNewestAt ?? "-"}`);
|
|
371
|
+
lines.push(`deliveries: pending=${cycle.deliveries.pending} inflight=${cycle.deliveries.inflight} confirmed=${cycle.deliveries.confirmed} failed_ambiguous=${cycle.deliveries.failedAmbiguous} expired=${cycle.deliveries.expired}`);
|
|
372
|
+
lines.push(`memory: queued=${cycle.memoryIntents.queued} written=${cycle.memoryIntents.written} committed=${cycle.memoryIntents.committed} receipted=${cycle.memoryIntents.receipted} quarantined=${cycle.memoryIntents.quarantined}${cycle.memoryClosing ? " (closing)" : ""}`);
|
|
373
|
+
lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
|
|
374
|
+
if (cycle.sessions.length > 0) {
|
|
375
|
+
lines.push("sessions:");
|
|
376
|
+
lines.push("INDEX ORIGIN EPOCH SESSION PENDING UNSETTLED OLDEST");
|
|
377
|
+
for (const [index, session] of cycle.sessions.entries()) {
|
|
378
|
+
const sessionId = session.sessionId === "" ? "(rebinding)" : session.sessionId.slice(0, 11);
|
|
379
|
+
const oldest = session.oldestUnsettledAgeMs === null ? "-" : `${Math.round(session.oldestUnsettledAgeMs / 1000)}s`;
|
|
380
|
+
lines.push(`${String(index).padEnd(6)} ${session.originKey.padEnd(43)} ${String(session.epoch).padEnd(6)} ${sessionId.padEnd(12)} ${String(session.pendingInbound).padEnd(8)} ${String(session.unsettledDeliveries).padEnd(10)} ${oldest}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return lines;
|
|
384
|
+
}
|
|
385
|
+
function cycleExitCode(cycle) {
|
|
386
|
+
return cycle.gates.length > 0 ? 1 : 0;
|
|
387
|
+
}
|
|
388
|
+
async function restoreDatabase(socket, backupPath) {
|
|
389
|
+
if (!isAbsolute(backupPath))
|
|
390
|
+
throw new Error("ops restore requires an absolute backup path");
|
|
391
|
+
try {
|
|
392
|
+
await stat(socket);
|
|
393
|
+
throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof Error && error.message.startsWith("Refusing restore:"))
|
|
396
|
+
throw error;
|
|
397
|
+
if (error.code !== "ENOENT")
|
|
398
|
+
throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
|
|
399
|
+
}
|
|
400
|
+
let header;
|
|
401
|
+
try {
|
|
402
|
+
header = new Uint8Array(await Bun.file(backupPath).slice(0, 16).arrayBuffer());
|
|
403
|
+
} catch {
|
|
404
|
+
throw new Error(`Backup is not readable: ${backupPath}`);
|
|
405
|
+
}
|
|
406
|
+
if (new TextDecoder().decode(header) !== "SQLite format 3\x00")
|
|
407
|
+
throw new Error(`Backup is not a SQLite database: ${backupPath}`);
|
|
408
|
+
const databasePath = join2(gatewayHome(), "gateway.db");
|
|
409
|
+
const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
410
|
+
await copyFile(databasePath, preservedPath);
|
|
411
|
+
await copyFile(backupPath, databasePath);
|
|
412
|
+
console.log(`Validated SQLite backup: ${backupPath}`);
|
|
413
|
+
console.log(`Copied current database to: ${preservedPath}`);
|
|
414
|
+
console.log(`Restored backup to: ${databasePath}`);
|
|
415
|
+
}
|
|
416
|
+
async function chat(socket) {
|
|
417
|
+
let client;
|
|
418
|
+
try {
|
|
419
|
+
client = await GajaewayClient.connectSocket(socket);
|
|
420
|
+
} catch {
|
|
421
|
+
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const turnWaiters = new Map;
|
|
426
|
+
client.onChatMessage((message) => {
|
|
427
|
+
console.log(message.text);
|
|
428
|
+
if (message.final)
|
|
429
|
+
turnWaiters.get(message.turnId)?.();
|
|
430
|
+
});
|
|
431
|
+
const sendAndWait = async (text) => {
|
|
432
|
+
const { turnId } = await client.chatSend(LOOPBACK_ORIGIN, text);
|
|
433
|
+
if (turnId === null) {
|
|
434
|
+
console.error("(message was not engaged)");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
await new Promise((resolve) => {
|
|
438
|
+
const timer = setTimeout(() => {
|
|
439
|
+
turnWaiters.delete(turnId);
|
|
440
|
+
console.error("(turn timed out after 120s)");
|
|
441
|
+
resolve();
|
|
442
|
+
}, 120000);
|
|
443
|
+
turnWaiters.set(turnId, () => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
turnWaiters.delete(turnId);
|
|
446
|
+
resolve();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
const reader = Bun.stdin.stream().getReader();
|
|
451
|
+
const decoder = new TextDecoder;
|
|
452
|
+
let buffer = "";
|
|
453
|
+
process.stdout.write("> ");
|
|
454
|
+
try {
|
|
455
|
+
while (true) {
|
|
456
|
+
const { value, done } = await reader.read();
|
|
457
|
+
if (done)
|
|
458
|
+
break;
|
|
459
|
+
buffer += decoder.decode(value, { stream: true });
|
|
460
|
+
const lines = buffer.split(/\r?\n/);
|
|
461
|
+
buffer = lines.pop() ?? "";
|
|
462
|
+
for (const line of lines) {
|
|
463
|
+
if (line === "/quit")
|
|
464
|
+
return;
|
|
465
|
+
if (line.trim())
|
|
466
|
+
await sendAndWait(line);
|
|
467
|
+
process.stdout.write("> ");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} finally {
|
|
471
|
+
await client.close();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function main(args = process.argv.slice(2), options = {}) {
|
|
475
|
+
const parsed = parseArgs(args);
|
|
476
|
+
const usage = usageFor(parsed.command);
|
|
477
|
+
if (usage !== undefined) {
|
|
478
|
+
console.error(usage);
|
|
479
|
+
process.exit(USAGE_EXIT_CODE);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
switch (parsed.command) {
|
|
483
|
+
case "status": {
|
|
484
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
485
|
+
try {
|
|
486
|
+
console.log(JSON.stringify(await client.status()));
|
|
487
|
+
} finally {
|
|
488
|
+
await client.close();
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "shutdown": {
|
|
493
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
494
|
+
try {
|
|
495
|
+
await client.shutdown();
|
|
496
|
+
} finally {
|
|
497
|
+
await client.close();
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case "chat":
|
|
502
|
+
await chat(parsed.socket);
|
|
503
|
+
break;
|
|
504
|
+
case "daemon":
|
|
505
|
+
if (parsed.rest[0] === "run")
|
|
506
|
+
console.log("Launch the gateway out-of-band with: bun packages/gateway/src/main.ts daemon");
|
|
507
|
+
else
|
|
508
|
+
throw new Error("usage: gajaeway daemon run");
|
|
509
|
+
break;
|
|
510
|
+
case "sessions": {
|
|
511
|
+
const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
512
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
513
|
+
try {
|
|
514
|
+
const result = await client.request("session.list");
|
|
515
|
+
const [command, selector] = parsed.rest;
|
|
516
|
+
if (command === "list") {
|
|
517
|
+
const options2 = listOptions;
|
|
518
|
+
for (const line of renderList(SESSION_COLUMNS, result.sessions, options2, {
|
|
519
|
+
key: "sessions",
|
|
520
|
+
result
|
|
521
|
+
}))
|
|
522
|
+
console.log(line);
|
|
523
|
+
} else if (command === "inspect" && selector) {
|
|
524
|
+
const index = Number(selector);
|
|
525
|
+
const session = Number.isInteger(index) ? result.sessions[index] : result.sessions.find((candidate) => originKey2(candidate.origin) === selector);
|
|
526
|
+
if (!session)
|
|
527
|
+
throw new Error(`Unknown session: ${selector}`);
|
|
528
|
+
console.log(`origin: ${originKey2(session.origin)}`);
|
|
529
|
+
console.log(`epoch: ${session.epoch}`);
|
|
530
|
+
console.log(`createdAt: ${session.createdAt}`);
|
|
531
|
+
console.log(`lastActivityAt: ${session.lastActivityAt ?? "-"}`);
|
|
532
|
+
console.log(`bootstrapEpoch: ${session.bootstrap.epoch}`);
|
|
533
|
+
console.log(`bootstrapPending: ${session.bootstrap.pending}`);
|
|
534
|
+
console.log(`bootstrapAppliedAt: ${session.bootstrap.appliedAt ?? "-"}`);
|
|
535
|
+
console.log(`bootstrapSections: ${session.bootstrap.includedSections.join(", ") || "-"}`);
|
|
536
|
+
console.log(`bootstrapBytes: ${session.bootstrap.byteCount}`);
|
|
537
|
+
console.log(`bootstrapTruncated: ${session.bootstrap.truncated}`);
|
|
538
|
+
console.log(`bootstrapDiagnostics: ${session.bootstrap.diagnostics.join(", ") || "-"}`);
|
|
539
|
+
} else
|
|
540
|
+
throw new Error(`usage: gajaeway sessions list [--json] [--fields ${columnNames(SESSION_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <originKey-or-index>`);
|
|
541
|
+
} finally {
|
|
542
|
+
await client.close();
|
|
543
|
+
}
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case "ops": {
|
|
547
|
+
const [command, path] = parsed.rest;
|
|
548
|
+
if (command === "restore" && path) {
|
|
549
|
+
await restoreDatabase(parsed.socket, path);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
553
|
+
try {
|
|
554
|
+
if (command === "backup" && path)
|
|
555
|
+
console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
556
|
+
else if (command === "integrity")
|
|
557
|
+
console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
558
|
+
else if (command === "cycle") {
|
|
559
|
+
const cycle = await client.opsCycle();
|
|
560
|
+
if (parsed.rest[1] === "--json")
|
|
561
|
+
console.log(JSON.stringify(cycle));
|
|
562
|
+
else if (parsed.rest[1])
|
|
563
|
+
throw new Error("usage: gajaeway ops cycle [--json]");
|
|
564
|
+
else
|
|
565
|
+
for (const line of renderCycle(cycle))
|
|
566
|
+
console.log(line);
|
|
567
|
+
process.exitCode = cycleExitCode(cycle);
|
|
568
|
+
} else
|
|
569
|
+
throw new Error("usage: gajaeway ops backup <path>|cycle [--json]|integrity|restore <backupPath>");
|
|
570
|
+
} finally {
|
|
571
|
+
await client.close();
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case "memory": {
|
|
576
|
+
const usage2 = "usage: gajaeway memory audit|memory autolink|memory search <query>";
|
|
577
|
+
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
578
|
+
throw new Error(`${usage2} (unknown argument: ${parsed.rest[1]})`);
|
|
579
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
580
|
+
try {
|
|
581
|
+
if (parsed.rest[0] === "audit") {
|
|
582
|
+
const result = await client.request("memory.audit");
|
|
583
|
+
console.log(JSON.stringify(result.issues));
|
|
584
|
+
if (!result.ok)
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
} else if (parsed.rest[0] === "autolink") {
|
|
587
|
+
console.log(JSON.stringify(await client.request("memory.autolink")));
|
|
588
|
+
} else if (parsed.rest[0] === "search" && parsed.rest.slice(1).join(" ")) {
|
|
589
|
+
console.log(JSON.stringify(await client.request("memory.search", { query: parsed.rest.slice(1).join(" ") })));
|
|
590
|
+
} else
|
|
591
|
+
throw new Error(usage2);
|
|
592
|
+
} finally {
|
|
593
|
+
await client.close();
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
case "monitors": {
|
|
598
|
+
const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), MONITOR_COLUMNS) : undefined;
|
|
599
|
+
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
600
|
+
try {
|
|
601
|
+
const [command, ...args2] = parsed.rest;
|
|
602
|
+
if (command === "add" && args2[0] === "--json" && args2[1])
|
|
603
|
+
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
|
|
604
|
+
else if (command === "list") {
|
|
605
|
+
const options2 = listOptions;
|
|
606
|
+
const result = await client.request("monitor.list");
|
|
607
|
+
for (const line of renderList(MONITOR_COLUMNS, result.monitors, options2, {
|
|
608
|
+
key: "monitors",
|
|
609
|
+
result
|
|
610
|
+
}))
|
|
611
|
+
console.log(line);
|
|
612
|
+
} else if (command === "inspect" && args2[0])
|
|
613
|
+
console.log(JSON.stringify(await client.request("monitor.inspect", { monitorId: args2[0] })));
|
|
614
|
+
else if (command === "remove" && args2[0])
|
|
615
|
+
console.log(JSON.stringify(await client.request("monitor.remove", { monitorId: args2[0] })));
|
|
616
|
+
else if (command === "test" && args2[0]) {
|
|
617
|
+
let eventType;
|
|
618
|
+
let payload = {};
|
|
619
|
+
for (let i = 1;i < args2.length; i++) {
|
|
620
|
+
if (args2[i] === "--type")
|
|
621
|
+
eventType = args2[++i];
|
|
622
|
+
else if (args2[i] === "--payload")
|
|
623
|
+
payload = JSON.parse(args2[++i] ?? "");
|
|
624
|
+
}
|
|
625
|
+
console.log(JSON.stringify(await client.request("monitor.test", {
|
|
626
|
+
monitorId: args2[0],
|
|
627
|
+
...eventType ? { eventType } : {},
|
|
628
|
+
payload
|
|
629
|
+
})));
|
|
630
|
+
} else
|
|
631
|
+
throw new Error(`usage: gajaeway monitors add --json '<MonitorSpec json>'|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J]`);
|
|
632
|
+
} finally {
|
|
633
|
+
await client.close();
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case "work": {
|
|
638
|
+
const [command, ...args2] = parsed.rest;
|
|
639
|
+
const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] "<task text>"|status <name>|steer <name> <text>|retire <name>|jobs';
|
|
640
|
+
if (command === "status" || command === "steer") {
|
|
641
|
+
const name2 = args2[0];
|
|
642
|
+
const text2 = args2.slice(1).join(" ").trim();
|
|
643
|
+
if (!name2 || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name2) || (command === "status" ? args2.length !== 1 : !text2 || args2.slice(1).some((arg) => arg.startsWith("--"))))
|
|
644
|
+
throw new Error(usage2);
|
|
645
|
+
const client2 = await GajaewayClient.connectSocket(parsed.socket);
|
|
646
|
+
try {
|
|
647
|
+
if (command === "status") {
|
|
648
|
+
console.log(JSON.stringify(await client2.request("work.status", { name: name2 })));
|
|
649
|
+
} else {
|
|
650
|
+
const result = await client2.request("work.steer", { name: name2, text: text2 });
|
|
651
|
+
console.log(result.steered ? `steered: ${result.clientRef}` : `not steered: ${result.reason}`);
|
|
652
|
+
if (!result.steered)
|
|
653
|
+
process.exitCode = 1;
|
|
654
|
+
}
|
|
655
|
+
} finally {
|
|
656
|
+
await client2.close();
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
if (command === "retire" || command === "jobs") {
|
|
661
|
+
if (command === "retire" ? args2.length !== 1 || !args2[0] || args2[0].startsWith("--") : args2.length !== 0)
|
|
662
|
+
throw new Error(usage2);
|
|
663
|
+
const client2 = await GajaewayClient.connectSocket(parsed.socket);
|
|
664
|
+
try {
|
|
665
|
+
if (command === "retire") {
|
|
666
|
+
const result = await client2.request("work.retire", { name: args2[0] });
|
|
667
|
+
console.log(result.retired ? `retired: ${result.sessionKey} session=${result.sessionId} closed=${result.closed}` : `not retired: ${result.reason}`);
|
|
668
|
+
} else {
|
|
669
|
+
const result = await client2.request("work.jobs");
|
|
670
|
+
for (const job of result.jobs) {
|
|
671
|
+
const name2 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
672
|
+
const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
|
|
673
|
+
console.log(`${name2} ${state} session=${job.session_id || "-"} last=${job.last_activity_at || "-"} ${job.worktree_path}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
await client2.close();
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
if (command !== "run" && command !== "start")
|
|
682
|
+
throw new Error(usage2);
|
|
683
|
+
const name = args2[0];
|
|
684
|
+
let cwd;
|
|
685
|
+
let resume = false;
|
|
686
|
+
let model;
|
|
687
|
+
let notify;
|
|
688
|
+
const textParts = [];
|
|
689
|
+
for (let i = 1;i < args2.length; i++) {
|
|
690
|
+
const arg = args2[i];
|
|
691
|
+
if (arg === "--cwd" || arg === "--model" || arg === "--preset" || arg === "--notify") {
|
|
692
|
+
const value = args2[++i];
|
|
693
|
+
if (!value?.trim() || value.startsWith("--"))
|
|
694
|
+
throw new Error(usage2);
|
|
695
|
+
if (arg === "--cwd") {
|
|
696
|
+
if (cwd !== undefined || !isAbsolute(value))
|
|
697
|
+
throw new Error(usage2);
|
|
698
|
+
cwd = value;
|
|
699
|
+
} else if (arg === "--notify") {
|
|
700
|
+
if (command !== "start" || notify !== undefined)
|
|
701
|
+
throw new Error(usage2);
|
|
702
|
+
try {
|
|
703
|
+
notify = parseOriginKey(value);
|
|
704
|
+
} catch {
|
|
705
|
+
throw new Error(`${usage2}
|
|
706
|
+
invalid --notify originKey`);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
if (model !== undefined)
|
|
710
|
+
throw new Error(`${usage2}
|
|
711
|
+
--model and --preset are mutually exclusive`);
|
|
712
|
+
model = arg === "--preset" ? { preset: value } : value;
|
|
713
|
+
}
|
|
714
|
+
} else if (arg === "--resume")
|
|
715
|
+
resume = true;
|
|
716
|
+
else if (arg?.startsWith("--"))
|
|
717
|
+
throw new Error(usage2);
|
|
718
|
+
else
|
|
719
|
+
textParts.push(arg);
|
|
720
|
+
}
|
|
721
|
+
const text = textParts.join(" ").trim();
|
|
722
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text)
|
|
723
|
+
throw new Error(usage2);
|
|
724
|
+
const client = await GajaewayClient.connectSocket(parsed.socket, {
|
|
725
|
+
requestTimeoutMs: command === "run" ? 3600000 : 120000
|
|
726
|
+
});
|
|
727
|
+
try {
|
|
728
|
+
const params = {
|
|
729
|
+
name,
|
|
730
|
+
text,
|
|
731
|
+
...cwd ? { cwd } : {},
|
|
732
|
+
...resume ? { resume: true } : {},
|
|
733
|
+
...model === undefined ? {} : { model },
|
|
734
|
+
...notify === undefined ? {} : { notify }
|
|
735
|
+
};
|
|
736
|
+
if (command === "start") {
|
|
737
|
+
const result = await client.request("work.start", params);
|
|
738
|
+
if (result.started) {
|
|
739
|
+
console.log(`started: ${result.sessionKey} session=${result.sessionId} job=${result.jobId} op=${result.opRef}`);
|
|
740
|
+
} else {
|
|
741
|
+
console.log(`HELD: ${result.reason}
|
|
742
|
+
job: ${result.jobId} state: ${result.state}`);
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
}
|
|
745
|
+
} else {
|
|
746
|
+
const result = await client.request("work.run", params);
|
|
747
|
+
if (result.held) {
|
|
748
|
+
console.log(`HELD: ${result.reason}
|
|
749
|
+
job: ${result.jobId} state: ${result.state}`);
|
|
750
|
+
process.exitCode = 1;
|
|
751
|
+
} else
|
|
752
|
+
console.log(result.text);
|
|
753
|
+
}
|
|
754
|
+
} finally {
|
|
755
|
+
await client.close();
|
|
756
|
+
}
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case "services": {
|
|
760
|
+
const service = parseServicesArgs(parsed.rest);
|
|
761
|
+
const written = await installServices({
|
|
762
|
+
binDir: service.binDir,
|
|
763
|
+
...service.launchAgentsDir === undefined ? {} : { launchAgentsDir: service.launchAgentsDir },
|
|
764
|
+
env: process.env,
|
|
765
|
+
...options.services
|
|
766
|
+
});
|
|
767
|
+
console.log(`services ${service.action}: wrote ${written.length} LaunchAgent plists`);
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
default:
|
|
771
|
+
throw new Error(CLI_USAGE);
|
|
772
|
+
}
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
775
|
+
process.exitCode = 1;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (import.meta.main)
|
|
779
|
+
await main();
|
|
780
|
+
export {
|
|
781
|
+
CLI_USAGE,
|
|
782
|
+
COMMANDS,
|
|
783
|
+
USAGE_EXIT_CODE,
|
|
784
|
+
cycleExitCode,
|
|
785
|
+
main,
|
|
786
|
+
parseArgs,
|
|
787
|
+
parseServicesArgs,
|
|
788
|
+
renderCycle,
|
|
789
|
+
restoreDatabase,
|
|
790
|
+
socketPath,
|
|
791
|
+
usageFor
|
|
792
|
+
};
|