@gajae-gateway/cli 0.1.0 → 0.1.2
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/main.d.ts +38 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +617 -66
- package/dist/restart-stack.d.ts +101 -0
- package/dist/restart-stack.d.ts.map +1 -0
- package/dist/services.d.ts +32 -1
- package/dist/services.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/main.ts +358 -51
- package/src/restart-stack.ts +361 -0
- package/src/services.ts +138 -15
package/dist/main.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/main.ts
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { copyFile, lstat, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
7
|
+
import { isAbsolute, join as join3 } from "path";
|
|
8
|
+
import { LOOPBACK_ORIGIN, originKey as originKey2 } from "@gajae-gateway/protocol";
|
|
8
9
|
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
9
10
|
|
|
10
11
|
// src/list.ts
|
|
@@ -126,6 +127,11 @@ function renderList(columns, rows, options, envelope) {
|
|
|
126
127
|
return [JSON.stringify({ ...envelope.result, [envelope.key]: projected })];
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
// src/restart-stack.ts
|
|
131
|
+
import { randomUUID } from "crypto";
|
|
132
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, stat, writeFile as writeFile2 } from "fs/promises";
|
|
133
|
+
import { join as join2 } from "path";
|
|
134
|
+
|
|
129
135
|
// src/services.ts
|
|
130
136
|
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
131
137
|
import { homedir } from "os";
|
|
@@ -134,22 +140,34 @@ import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol"
|
|
|
134
140
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
135
141
|
var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
136
142
|
var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
|
|
143
|
+
var GATEWAY_UNIT = "gajaeway-gateway.service";
|
|
137
144
|
var SERVICE_SPECS = [
|
|
138
|
-
{
|
|
145
|
+
{
|
|
146
|
+
id: "gateway",
|
|
147
|
+
label: "dev.gajaeway.gateway",
|
|
148
|
+
binary: "gajaeway-gateway",
|
|
149
|
+
args: ["daemon"],
|
|
150
|
+
dependsOnGateway: false
|
|
151
|
+
},
|
|
139
152
|
{
|
|
140
153
|
id: "adapter-discord",
|
|
141
154
|
label: "dev.gajaeway.adapter-discord",
|
|
142
155
|
binary: "gajaeway-discord",
|
|
143
|
-
args: []
|
|
156
|
+
args: [],
|
|
157
|
+
dependsOnGateway: true
|
|
144
158
|
},
|
|
145
159
|
{
|
|
146
160
|
id: "adapter-slack",
|
|
147
161
|
label: "dev.gajaeway.adapter-slack",
|
|
148
162
|
binary: "gajaeway-slack",
|
|
149
|
-
args: []
|
|
163
|
+
args: [],
|
|
164
|
+
dependsOnGateway: true
|
|
150
165
|
},
|
|
151
|
-
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"] }
|
|
166
|
+
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true }
|
|
152
167
|
];
|
|
168
|
+
function serviceSpecs() {
|
|
169
|
+
return SERVICE_SPECS;
|
|
170
|
+
}
|
|
153
171
|
function homeForEnvironment(env) {
|
|
154
172
|
return env.GAJAEWAY_HOME || join(env.HOME || homedir(), ".gajaeway");
|
|
155
173
|
}
|
|
@@ -237,6 +255,64 @@ function renderLaunchAgent(spec, binDir, home, path) {
|
|
|
237
255
|
</dict></plist>
|
|
238
256
|
`;
|
|
239
257
|
}
|
|
258
|
+
function unitEnvironment(name, value) {
|
|
259
|
+
return `Environment="${name}=${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
260
|
+
}
|
|
261
|
+
function systemdUnitName(spec) {
|
|
262
|
+
return `gajaeway-${spec.id}.service`;
|
|
263
|
+
}
|
|
264
|
+
function renderSystemdUnit(spec, binDir, home, path) {
|
|
265
|
+
const execStart = [join(binDir, spec.binary), ...spec.args].join(" ");
|
|
266
|
+
const unit = [
|
|
267
|
+
"[Unit]",
|
|
268
|
+
`Description=gajaeway ${spec.id}`,
|
|
269
|
+
...spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
|
|
270
|
+
"",
|
|
271
|
+
"[Service]",
|
|
272
|
+
`ExecStart=${execStart}`,
|
|
273
|
+
`WorkingDirectory=${binDir}`,
|
|
274
|
+
unitEnvironment("GAJAEWAY_HOME", home),
|
|
275
|
+
unitEnvironment("PATH", path),
|
|
276
|
+
"Restart=always",
|
|
277
|
+
"RestartSec=2",
|
|
278
|
+
...spec.dependsOnGateway ? [] : ["KillMode=process", "TimeoutStopSec=30s"],
|
|
279
|
+
"",
|
|
280
|
+
"[Install]",
|
|
281
|
+
`WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
|
|
282
|
+
""
|
|
283
|
+
];
|
|
284
|
+
return unit.join(`
|
|
285
|
+
`);
|
|
286
|
+
}
|
|
287
|
+
function systemdUnitDir(env, userHome) {
|
|
288
|
+
const base = env.XDG_CONFIG_HOME || join(userHome, ".config");
|
|
289
|
+
return join(base, "systemd", "user");
|
|
290
|
+
}
|
|
291
|
+
function restartStackCommands(platform, uid) {
|
|
292
|
+
if (platform === "linux")
|
|
293
|
+
return [["systemctl", "--user", "restart", GATEWAY_UNIT]];
|
|
294
|
+
if (platform === "darwin") {
|
|
295
|
+
const target = uid ?? process.getuid?.() ?? 0;
|
|
296
|
+
return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`unsupported service platform: ${platform}`);
|
|
299
|
+
}
|
|
300
|
+
function restartOrder() {
|
|
301
|
+
return [
|
|
302
|
+
...SERVICE_SPECS.filter((spec) => !spec.dependsOnGateway),
|
|
303
|
+
...SERVICE_SPECS.filter((spec) => spec.dependsOnGateway)
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
function requirePlatform(platform) {
|
|
307
|
+
if (platform !== "darwin" && platform !== "linux")
|
|
308
|
+
throw new Error(`unsupported service platform: ${platform}`);
|
|
309
|
+
return platform;
|
|
310
|
+
}
|
|
311
|
+
function currentPlatform() {
|
|
312
|
+
if (process.platform === "darwin" || process.platform === "linux")
|
|
313
|
+
return process.platform;
|
|
314
|
+
throw new Error(`unsupported service platform: ${process.platform}`);
|
|
315
|
+
}
|
|
240
316
|
async function readRuntime(home) {
|
|
241
317
|
const configPath = join(home, "config.json");
|
|
242
318
|
let raw;
|
|
@@ -267,10 +343,11 @@ async function installServices(options) {
|
|
|
267
343
|
if (options.binDir.length === 0)
|
|
268
344
|
throw new Error("services requires a non-empty --bin-dir DIR");
|
|
269
345
|
const env = options.env ?? process.env;
|
|
346
|
+
const platform = options.platform === undefined ? currentPlatform() : requirePlatform(options.platform);
|
|
270
347
|
const userHome = env.HOME || homedir();
|
|
271
348
|
const home = homeForEnvironment(env);
|
|
272
349
|
const binDir = expandHome(options.binDir, userHome);
|
|
273
|
-
const
|
|
350
|
+
const targetDir = platform === "darwin" ? expandHome(options.launchAgentsDir ?? join(userHome, "Library", "LaunchAgents"), userHome) : expandHome(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
|
|
274
351
|
const runtime = await readRuntime(home);
|
|
275
352
|
const path = await resolvePath({
|
|
276
353
|
binDir,
|
|
@@ -279,24 +356,257 @@ async function installServices(options) {
|
|
|
279
356
|
env,
|
|
280
357
|
loginPathRunner: options.loginPathRunner
|
|
281
358
|
});
|
|
282
|
-
await mkdir(
|
|
283
|
-
const
|
|
284
|
-
path: join(
|
|
359
|
+
await mkdir(targetDir, { recursive: true, mode: 448 });
|
|
360
|
+
const definitions = SERVICE_SPECS.map((spec) => platform === "darwin" ? {
|
|
361
|
+
path: join(targetDir, `dev.gajaeway.${spec.id}.plist`),
|
|
285
362
|
contents: renderLaunchAgent(spec, binDir, home, path)
|
|
286
|
-
}));
|
|
363
|
+
} : { path: join(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
|
|
287
364
|
const write = options.writeFile;
|
|
288
|
-
for (const
|
|
365
|
+
for (const definition of definitions) {
|
|
289
366
|
if (write)
|
|
290
|
-
await write(
|
|
367
|
+
await write(definition.path, definition.contents);
|
|
291
368
|
else {
|
|
292
|
-
await writeFile(
|
|
293
|
-
await chmod(
|
|
369
|
+
await writeFile(definition.path, definition.contents, { encoding: "utf8", mode: 384 });
|
|
370
|
+
await chmod(definition.path, 384);
|
|
294
371
|
}
|
|
295
372
|
}
|
|
296
|
-
return
|
|
373
|
+
return definitions.map((definition) => definition.path);
|
|
297
374
|
}
|
|
298
375
|
function serviceUsage() {
|
|
299
|
-
return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR]";
|
|
376
|
+
return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/restart-stack.ts
|
|
380
|
+
var RECEIPT_FILE = "restart-stack.json";
|
|
381
|
+
var VERIFY_TIMEOUT_MS = 90000;
|
|
382
|
+
var POLL_MS = 1000;
|
|
383
|
+
function restartReceiptPath(home) {
|
|
384
|
+
return join2(home, RECEIPT_FILE);
|
|
385
|
+
}
|
|
386
|
+
async function readRestartReceipt(home) {
|
|
387
|
+
let raw;
|
|
388
|
+
try {
|
|
389
|
+
raw = await readFile2(restartReceiptPath(home), "utf8");
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error.code === "ENOENT")
|
|
392
|
+
return;
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
return JSON.parse(raw);
|
|
396
|
+
}
|
|
397
|
+
async function writeReceipt(home, receipt) {
|
|
398
|
+
await mkdir2(home, { recursive: true });
|
|
399
|
+
const path = restartReceiptPath(home);
|
|
400
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
401
|
+
await writeFile2(temporary, `${JSON.stringify(receipt, null, 2)}
|
|
402
|
+
`, { mode: 384 });
|
|
403
|
+
await rename(temporary, path);
|
|
404
|
+
}
|
|
405
|
+
function newReceipt(id, platform, uid, now) {
|
|
406
|
+
const commands = restartStackCommands(platform, uid);
|
|
407
|
+
return {
|
|
408
|
+
id,
|
|
409
|
+
state: "queued",
|
|
410
|
+
platform,
|
|
411
|
+
requestedAt: new Date(now).toISOString(),
|
|
412
|
+
steps: restartOrder().map((spec, index) => ({
|
|
413
|
+
label: spec.label,
|
|
414
|
+
...platform === "darwin" ? { command: commands[index] } : index === 0 ? { command: commands[0] } : {},
|
|
415
|
+
result: "pending"
|
|
416
|
+
}))
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
async function defaultCommandRunner(command) {
|
|
420
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
421
|
+
return await child.exited;
|
|
422
|
+
}
|
|
423
|
+
async function capture(command) {
|
|
424
|
+
const child = Bun.spawn([...command], {
|
|
425
|
+
stdin: "ignore",
|
|
426
|
+
stdout: "pipe",
|
|
427
|
+
stderr: "ignore",
|
|
428
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
429
|
+
});
|
|
430
|
+
const output = await new Response(child.stdout).text();
|
|
431
|
+
return await child.exited === 0 ? output : undefined;
|
|
432
|
+
}
|
|
433
|
+
function defaultServiceProbe(platform) {
|
|
434
|
+
return async (label) => {
|
|
435
|
+
const spec = serviceSpecs().find((candidate) => candidate.label === label);
|
|
436
|
+
if (spec === undefined)
|
|
437
|
+
throw new Error(`unknown service label: ${label}`);
|
|
438
|
+
let pid;
|
|
439
|
+
if (platform === "darwin") {
|
|
440
|
+
const listed = await capture(["launchctl", "list", label]);
|
|
441
|
+
const match = listed?.match(/"PID"\s*=\s*(\d+);/);
|
|
442
|
+
pid = match ? Number(match[1]) : undefined;
|
|
443
|
+
} else {
|
|
444
|
+
const shown = await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
|
|
445
|
+
pid = shown === undefined ? undefined : Number(shown.trim());
|
|
446
|
+
}
|
|
447
|
+
if (pid === undefined || !Number.isInteger(pid) || pid <= 0)
|
|
448
|
+
return;
|
|
449
|
+
const lstart = await capture(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
450
|
+
const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
|
|
451
|
+
if (lstart === undefined || args === undefined)
|
|
452
|
+
return;
|
|
453
|
+
const startedAt = Date.parse(lstart.trim());
|
|
454
|
+
if (!Number.isFinite(startedAt))
|
|
455
|
+
return;
|
|
456
|
+
const suffix = spec.args.length > 0 ? ` ${spec.args.join(" ")}` : "";
|
|
457
|
+
const line = args.trim();
|
|
458
|
+
const binary = suffix && line.endsWith(suffix) ? line.slice(0, -suffix.length) : line;
|
|
459
|
+
return { pid, startedAt, binary };
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async function defaultBinaryModifiedAt(path) {
|
|
463
|
+
return (await stat(path)).mtimeMs;
|
|
464
|
+
}
|
|
465
|
+
function floorSecond(ms) {
|
|
466
|
+
return Math.floor(ms / 1000) * 1000;
|
|
467
|
+
}
|
|
468
|
+
async function runRestartStack(options) {
|
|
469
|
+
const platform = options.platform ?? currentPlatform();
|
|
470
|
+
const runner = options.runner ?? defaultCommandRunner;
|
|
471
|
+
const probe = options.probe ?? defaultServiceProbe(platform);
|
|
472
|
+
const modifiedAt = options.binaryModifiedAt ?? defaultBinaryModifiedAt;
|
|
473
|
+
const now = options.now ?? Date.now;
|
|
474
|
+
const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
|
|
475
|
+
const timeoutMs = options.verifyTimeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
476
|
+
const pollMs = options.pollMs ?? POLL_MS;
|
|
477
|
+
const existing = await readRestartReceipt(options.home);
|
|
478
|
+
const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now());
|
|
479
|
+
receipt.state = "running";
|
|
480
|
+
receipt.supervisorPid = process.pid;
|
|
481
|
+
await writeReceipt(options.home, receipt);
|
|
482
|
+
let issuedAt = now();
|
|
483
|
+
for (const step of receipt.steps) {
|
|
484
|
+
if (receipt.state !== "running") {
|
|
485
|
+
step.result = "skipped";
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
if (step.command !== undefined) {
|
|
490
|
+
issuedAt = now();
|
|
491
|
+
const status = await runner(step.command);
|
|
492
|
+
step.exitStatus = status;
|
|
493
|
+
if (status !== 0) {
|
|
494
|
+
step.result = "failed";
|
|
495
|
+
step.detail = `${step.command.join(" ")} exited with status ${status}`;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (step.result === "pending")
|
|
499
|
+
await verifyStep(step, issuedAt);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
step.result = "failed";
|
|
502
|
+
step.detail = error instanceof Error ? error.message : String(error);
|
|
503
|
+
}
|
|
504
|
+
if (step.result !== "ok")
|
|
505
|
+
receipt.state = step.result === "stale" ? "stale" : "failed";
|
|
506
|
+
await writeReceipt(options.home, receipt);
|
|
507
|
+
}
|
|
508
|
+
if (receipt.state === "running")
|
|
509
|
+
receipt.state = "ok";
|
|
510
|
+
receipt.finishedAt = new Date(now()).toISOString();
|
|
511
|
+
await writeReceipt(options.home, receipt);
|
|
512
|
+
return receipt;
|
|
513
|
+
async function verifyStep(step, since) {
|
|
514
|
+
const deadline = now() + timeoutMs;
|
|
515
|
+
for (;; ) {
|
|
516
|
+
const running = await probe(step.label);
|
|
517
|
+
let binaryModifiedAt = 0;
|
|
518
|
+
if (running !== undefined) {
|
|
519
|
+
binaryModifiedAt = await modifiedAt(running.binary);
|
|
520
|
+
step.pid = running.pid;
|
|
521
|
+
step.processStartedAt = new Date(running.startedAt).toISOString();
|
|
522
|
+
step.binary = running.binary;
|
|
523
|
+
step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
|
|
524
|
+
if (running.startedAt >= floorSecond(Math.max(binaryModifiedAt, since))) {
|
|
525
|
+
step.result = "ok";
|
|
526
|
+
delete step.detail;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (now() >= deadline) {
|
|
531
|
+
if (running === undefined) {
|
|
532
|
+
step.result = "failed";
|
|
533
|
+
step.detail = "no running process after restart";
|
|
534
|
+
} else {
|
|
535
|
+
step.result = "stale";
|
|
536
|
+
step.detail = running.startedAt < binaryModifiedAt ? "process started before the deployed binary was modified" : "process was not restarted by this sequence";
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
await sleep(pollMs);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function cliCommand() {
|
|
545
|
+
return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
|
|
546
|
+
}
|
|
547
|
+
function alive(pid) {
|
|
548
|
+
if (pid === undefined)
|
|
549
|
+
return false;
|
|
550
|
+
try {
|
|
551
|
+
process.kill(pid, 0);
|
|
552
|
+
return true;
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function launchRestartStack(options) {
|
|
558
|
+
const platform = options.platform ?? currentPlatform();
|
|
559
|
+
const existing = await readRestartReceipt(options.home);
|
|
560
|
+
if (existing?.state === "running" && alive(existing.supervisorPid))
|
|
561
|
+
throw new Error(`restart-stack ${existing.id} is still running (supervisor pid ${existing.supervisorPid})`);
|
|
562
|
+
const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
|
|
563
|
+
await writeReceipt(options.home, receipt);
|
|
564
|
+
const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
|
|
565
|
+
const child = Bun.spawn(supervisorArgv(platform, worker, options.home, receipt.id), {
|
|
566
|
+
detached: true,
|
|
567
|
+
stdin: "ignore",
|
|
568
|
+
stdout: "ignore",
|
|
569
|
+
stderr: "ignore",
|
|
570
|
+
env: { ...options.env ?? process.env, GAJAEWAY_HOME: options.home }
|
|
571
|
+
});
|
|
572
|
+
child.unref();
|
|
573
|
+
return { receipt, supervisorPid: child.pid };
|
|
574
|
+
}
|
|
575
|
+
function supervisorArgv(platform, worker, home, id) {
|
|
576
|
+
if (platform === "darwin")
|
|
577
|
+
return [...worker];
|
|
578
|
+
return [
|
|
579
|
+
"systemd-run",
|
|
580
|
+
"--user",
|
|
581
|
+
"--collect",
|
|
582
|
+
"--quiet",
|
|
583
|
+
`--unit=gajaeway-restart-stack-${id}`,
|
|
584
|
+
`--setenv=GAJAEWAY_HOME=${home}`,
|
|
585
|
+
"--",
|
|
586
|
+
...worker
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
function effectiveRestartState(receipt) {
|
|
590
|
+
if (receipt.state === "running" && !alive(receipt.supervisorPid))
|
|
591
|
+
return "interrupted";
|
|
592
|
+
return receipt.state;
|
|
593
|
+
}
|
|
594
|
+
function renderRestartReceipt(receipt) {
|
|
595
|
+
const lines = [
|
|
596
|
+
`restart-stack ${receipt.id}: ${effectiveRestartState(receipt)}`,
|
|
597
|
+
`requestedAt: ${receipt.requestedAt}`,
|
|
598
|
+
`finishedAt: ${receipt.finishedAt ?? "-"}`
|
|
599
|
+
];
|
|
600
|
+
for (const step of receipt.steps) {
|
|
601
|
+
const facts = [
|
|
602
|
+
step.pid === undefined ? undefined : `pid=${step.pid}`,
|
|
603
|
+
step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
|
|
604
|
+
step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
|
|
605
|
+
step.detail
|
|
606
|
+
].filter((fact) => fact !== undefined);
|
|
607
|
+
lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
|
|
608
|
+
}
|
|
609
|
+
return lines;
|
|
300
610
|
}
|
|
301
611
|
|
|
302
612
|
// src/main.ts
|
|
@@ -315,7 +625,7 @@ var COMMANDS = [
|
|
|
315
625
|
"work",
|
|
316
626
|
"services"
|
|
317
627
|
];
|
|
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
|
|
628
|
+
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 ... (test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] <text>|work status <name>|work steer <name> <text>|work retire <name>|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restore <backupPath>|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux] (work run waits for a response; caller timeout does not end the attempt)";
|
|
319
629
|
var USAGE_EXIT_CODE = 2;
|
|
320
630
|
function usageFor(command) {
|
|
321
631
|
return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
|
|
@@ -340,22 +650,171 @@ function parseServicesArgs(args) {
|
|
|
340
650
|
throw new Error(serviceUsage());
|
|
341
651
|
let binDir;
|
|
342
652
|
let launchAgentsDir;
|
|
653
|
+
let unitDir;
|
|
654
|
+
let platform;
|
|
343
655
|
for (let i = 1;i < args.length; i++) {
|
|
344
656
|
const flag = args[i];
|
|
345
|
-
if (flag === "--bin-dir" || flag === "--launch-agents-dir") {
|
|
657
|
+
if (flag === "--bin-dir" || flag === "--launch-agents-dir" || flag === "--unit-dir") {
|
|
346
658
|
const value = args[++i];
|
|
347
659
|
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
348
660
|
throw new Error(`${flag} expects a non-empty DIR`);
|
|
349
661
|
if (flag === "--bin-dir")
|
|
350
662
|
binDir = value;
|
|
663
|
+
else if (flag === "--unit-dir")
|
|
664
|
+
unitDir = value;
|
|
351
665
|
else
|
|
352
666
|
launchAgentsDir = value;
|
|
667
|
+
} else if (flag === "--platform") {
|
|
668
|
+
const value = args[++i];
|
|
669
|
+
if (value !== "darwin" && value !== "linux")
|
|
670
|
+
throw new Error("--platform expects darwin or linux");
|
|
671
|
+
platform = value;
|
|
353
672
|
} else
|
|
354
673
|
throw new Error(`unknown option: ${flag}`);
|
|
355
674
|
}
|
|
356
675
|
if (binDir === undefined)
|
|
357
676
|
throw new Error("services requires --bin-dir DIR");
|
|
358
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
action,
|
|
679
|
+
binDir,
|
|
680
|
+
...launchAgentsDir === undefined ? {} : { launchAgentsDir },
|
|
681
|
+
...unitDir === undefined ? {} : { unitDir },
|
|
682
|
+
...platform === undefined ? {} : { platform }
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
var MONITOR_UPDATE_USAGE = "usage: gajaeway monitors update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])";
|
|
686
|
+
function parseMonitorUpdateArgs(args) {
|
|
687
|
+
const [monitorId, ...options] = args;
|
|
688
|
+
if (!monitorId || monitorId.startsWith("--") || options.length === 0)
|
|
689
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
690
|
+
let jsonPatch;
|
|
691
|
+
let schedule;
|
|
692
|
+
let enabled;
|
|
693
|
+
for (let i = 0;i < options.length; i++) {
|
|
694
|
+
const option = options[i];
|
|
695
|
+
if (option === "--json") {
|
|
696
|
+
const value = options[++i];
|
|
697
|
+
if (jsonPatch !== undefined || value === undefined || value.startsWith("--"))
|
|
698
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
699
|
+
let parsed;
|
|
700
|
+
try {
|
|
701
|
+
parsed = JSON.parse(value);
|
|
702
|
+
} catch {
|
|
703
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
704
|
+
}
|
|
705
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
706
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
707
|
+
jsonPatch = parsed;
|
|
708
|
+
} else if (option === "--schedule") {
|
|
709
|
+
const value = options[++i];
|
|
710
|
+
if (schedule !== undefined || !value || value.startsWith("--"))
|
|
711
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
712
|
+
schedule = value;
|
|
713
|
+
} else if (option === "--enabled") {
|
|
714
|
+
const value = options[++i];
|
|
715
|
+
if (enabled !== undefined || value !== "true" && value !== "false")
|
|
716
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
717
|
+
enabled = value === "true";
|
|
718
|
+
} else
|
|
719
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
720
|
+
}
|
|
721
|
+
if (jsonPatch !== undefined && (schedule !== undefined || enabled !== undefined))
|
|
722
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
723
|
+
if (jsonPatch !== undefined)
|
|
724
|
+
return { monitorId, params: { ...jsonPatch, monitorId } };
|
|
725
|
+
return {
|
|
726
|
+
monitorId,
|
|
727
|
+
params: {
|
|
728
|
+
monitorId,
|
|
729
|
+
...schedule === undefined ? {} : { schedule },
|
|
730
|
+
...enabled === undefined ? {} : { enabled }
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
var MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
|
|
735
|
+
var MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}
|
|
736
|
+
--wait expects a finite non-negative integer up to 86400`;
|
|
737
|
+
function parseMonitorTestArgs(args) {
|
|
738
|
+
const [monitorId, ...flags] = args;
|
|
739
|
+
if (!monitorId || monitorId.startsWith("--"))
|
|
740
|
+
throw new Error(MONITOR_TEST_USAGE);
|
|
741
|
+
let eventType;
|
|
742
|
+
let payload = {};
|
|
743
|
+
let waitSeconds;
|
|
744
|
+
const seen = new Set;
|
|
745
|
+
for (let i = 0;i < flags.length; i++) {
|
|
746
|
+
const flag = flags[i];
|
|
747
|
+
if (flag === "--type" || flag === "--payload") {
|
|
748
|
+
if (seen.has(flag))
|
|
749
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
750
|
+
duplicate ${flag}`);
|
|
751
|
+
seen.add(flag);
|
|
752
|
+
const value = flags[++i];
|
|
753
|
+
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
754
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
755
|
+
${flag} requires a value`);
|
|
756
|
+
if (flag === "--type")
|
|
757
|
+
eventType = value;
|
|
758
|
+
else
|
|
759
|
+
payload = JSON.parse(value);
|
|
760
|
+
} else if (flag === "--wait" || flag.startsWith("--wait=")) {
|
|
761
|
+
if (seen.has("--wait"))
|
|
762
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
763
|
+
duplicate --wait`);
|
|
764
|
+
seen.add("--wait");
|
|
765
|
+
const value = flag === "--wait" ? undefined : flag.slice("--wait=".length);
|
|
766
|
+
if (value === undefined)
|
|
767
|
+
waitSeconds = 30;
|
|
768
|
+
else {
|
|
769
|
+
if (!/^\d+$/.test(value))
|
|
770
|
+
throw new Error(MONITOR_WAIT_ERROR);
|
|
771
|
+
const seconds = Number(value);
|
|
772
|
+
if (!Number.isFinite(seconds) || !Number.isInteger(seconds) || seconds > 86400)
|
|
773
|
+
throw new Error(MONITOR_WAIT_ERROR);
|
|
774
|
+
waitSeconds = seconds;
|
|
775
|
+
}
|
|
776
|
+
} else
|
|
777
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
778
|
+
unknown argument: ${flag}`);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
monitorId,
|
|
782
|
+
...eventType === undefined ? {} : { eventType },
|
|
783
|
+
payload,
|
|
784
|
+
...waitSeconds === undefined ? {} : { waitSeconds }
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
|
|
788
|
+
function waitForMonitorStage(client, eventId, timeoutSeconds) {
|
|
789
|
+
return new Promise((resolve) => {
|
|
790
|
+
let stage = "admitted";
|
|
791
|
+
let settled = false;
|
|
792
|
+
let timer;
|
|
793
|
+
let unsubscribe;
|
|
794
|
+
const finish = () => {
|
|
795
|
+
if (settled)
|
|
796
|
+
return;
|
|
797
|
+
settled = true;
|
|
798
|
+
if (timer !== undefined)
|
|
799
|
+
clearTimeout(timer);
|
|
800
|
+
unsubscribe?.();
|
|
801
|
+
resolve(stage);
|
|
802
|
+
};
|
|
803
|
+
unsubscribe = client.on("monitor.event", (payload) => {
|
|
804
|
+
if (typeof payload !== "object" || payload === null)
|
|
805
|
+
return;
|
|
806
|
+
const event = payload;
|
|
807
|
+
if (event.eventId !== eventId || typeof event.stage !== "string")
|
|
808
|
+
return;
|
|
809
|
+
stage = event.stage;
|
|
810
|
+
if (TERMINAL_MONITOR_STAGES.has(stage))
|
|
811
|
+
finish();
|
|
812
|
+
});
|
|
813
|
+
if (settled)
|
|
814
|
+
unsubscribe();
|
|
815
|
+
else
|
|
816
|
+
timer = setTimeout(finish, timeoutSeconds * 1000);
|
|
817
|
+
});
|
|
359
818
|
}
|
|
360
819
|
function renderCycle(cycle) {
|
|
361
820
|
const lines = [];
|
|
@@ -370,7 +829,14 @@ function renderCycle(cycle) {
|
|
|
370
829
|
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
830
|
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
831
|
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)" : ""}`);
|
|
832
|
+
if (cycle.agentDisk) {
|
|
833
|
+
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
834
|
+
const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
835
|
+
lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
|
|
836
|
+
}
|
|
373
837
|
lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
|
|
838
|
+
if (cycle.monitorAuthoringLost.length > 0)
|
|
839
|
+
lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
|
|
374
840
|
if (cycle.sessions.length > 0) {
|
|
375
841
|
lines.push("sessions:");
|
|
376
842
|
lines.push("INDEX ORIGIN EPOCH SESSION PENDING UNSETTLED OLDEST");
|
|
@@ -385,11 +851,61 @@ function renderCycle(cycle) {
|
|
|
385
851
|
function cycleExitCode(cycle) {
|
|
386
852
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
387
853
|
}
|
|
854
|
+
function verifyBackupIntegrity(backupPath) {
|
|
855
|
+
let database;
|
|
856
|
+
try {
|
|
857
|
+
database = new Database(backupPath, { readonly: true });
|
|
858
|
+
} catch (error) {
|
|
859
|
+
throw new Error(`Backup is not readable: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
860
|
+
}
|
|
861
|
+
let detail;
|
|
862
|
+
let pageCount;
|
|
863
|
+
try {
|
|
864
|
+
pageCount = database.query("PRAGMA page_count").get()?.page_count ?? 0;
|
|
865
|
+
detail = database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
|
|
866
|
+
} catch (error) {
|
|
867
|
+
throw new Error(`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
868
|
+
} finally {
|
|
869
|
+
database.close();
|
|
870
|
+
}
|
|
871
|
+
if (pageCount === 0)
|
|
872
|
+
throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
|
|
873
|
+
if (detail !== "ok")
|
|
874
|
+
throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
|
|
875
|
+
}
|
|
876
|
+
async function restoreTargetPath(home = gatewayHome()) {
|
|
877
|
+
const configPath = join3(home, "config.json");
|
|
878
|
+
let raw;
|
|
879
|
+
try {
|
|
880
|
+
raw = await readFile3(configPath, "utf8");
|
|
881
|
+
} catch (error) {
|
|
882
|
+
const code = error.code;
|
|
883
|
+
const entryExists = await lstat(configPath).then(() => true, () => false);
|
|
884
|
+
if (code !== "ENOENT" || entryExists)
|
|
885
|
+
throw new Error(`Cannot read ${configPath}; refusing restore.`);
|
|
886
|
+
}
|
|
887
|
+
if (raw === undefined)
|
|
888
|
+
return join3(home, "gateway.db");
|
|
889
|
+
let parsed;
|
|
890
|
+
try {
|
|
891
|
+
parsed = JSON.parse(raw);
|
|
892
|
+
} catch {
|
|
893
|
+
throw new Error(`Cannot parse ${configPath}; refusing restore.`);
|
|
894
|
+
}
|
|
895
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
896
|
+
throw new Error(`Invalid configuration ${configPath}: config must be an object; refusing restore.`);
|
|
897
|
+
const dbPath = parsed.dbPath;
|
|
898
|
+
if (dbPath === undefined)
|
|
899
|
+
return join3(home, "gateway.db");
|
|
900
|
+
if (typeof dbPath !== "string" || dbPath.length === 0)
|
|
901
|
+
throw new Error(`Invalid configuration ${configPath}: dbPath must be a non-empty string; refusing restore.`);
|
|
902
|
+
return dbPath;
|
|
903
|
+
}
|
|
388
904
|
async function restoreDatabase(socket, backupPath) {
|
|
389
905
|
if (!isAbsolute(backupPath))
|
|
390
906
|
throw new Error("ops restore requires an absolute backup path");
|
|
391
907
|
try {
|
|
392
|
-
await
|
|
908
|
+
await stat2(socket);
|
|
393
909
|
throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
|
|
394
910
|
} catch (error) {
|
|
395
911
|
if (error instanceof Error && error.message.startsWith("Refusing restore:"))
|
|
@@ -397,15 +913,8 @@ async function restoreDatabase(socket, backupPath) {
|
|
|
397
913
|
if (error.code !== "ENOENT")
|
|
398
914
|
throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
|
|
399
915
|
}
|
|
400
|
-
|
|
401
|
-
|
|
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");
|
|
916
|
+
verifyBackupIntegrity(backupPath);
|
|
917
|
+
const databasePath = await restoreTargetPath();
|
|
409
918
|
const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
410
919
|
await copyFile(databasePath, preservedPath);
|
|
411
920
|
await copyFile(backupPath, databasePath);
|
|
@@ -413,6 +922,7 @@ async function restoreDatabase(socket, backupPath) {
|
|
|
413
922
|
console.log(`Copied current database to: ${preservedPath}`);
|
|
414
923
|
console.log(`Restored backup to: ${databasePath}`);
|
|
415
924
|
}
|
|
925
|
+
var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
|
|
416
926
|
async function chat(socket) {
|
|
417
927
|
let client;
|
|
418
928
|
try {
|
|
@@ -437,9 +947,9 @@ async function chat(socket) {
|
|
|
437
947
|
await new Promise((resolve) => {
|
|
438
948
|
const timer = setTimeout(() => {
|
|
439
949
|
turnWaiters.delete(turnId);
|
|
440
|
-
console.error(
|
|
950
|
+
console.error(`(turn timed out after ${CHAT_TURN_TIMEOUT_MS / 60000}m)`);
|
|
441
951
|
resolve();
|
|
442
|
-
},
|
|
952
|
+
}, CHAT_TURN_TIMEOUT_MS);
|
|
443
953
|
turnWaiters.set(turnId, () => {
|
|
444
954
|
clearTimeout(timer);
|
|
445
955
|
turnWaiters.delete(turnId);
|
|
@@ -549,10 +1059,51 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
549
1059
|
await restoreDatabase(parsed.socket, path);
|
|
550
1060
|
break;
|
|
551
1061
|
}
|
|
1062
|
+
if (command === "restart-stack") {
|
|
1063
|
+
const usage2 = "usage: gajaeway ops restart-stack [--status]";
|
|
1064
|
+
const home = gatewayHome();
|
|
1065
|
+
const flag = parsed.rest[1];
|
|
1066
|
+
if (flag === "--run" && parsed.rest.length === 3 && parsed.rest[2]) {
|
|
1067
|
+
const receipt = await runRestartStack({
|
|
1068
|
+
...options.restartStack?.run,
|
|
1069
|
+
home,
|
|
1070
|
+
id: parsed.rest[2]
|
|
1071
|
+
});
|
|
1072
|
+
for (const line of renderRestartReceipt(receipt))
|
|
1073
|
+
console.log(line);
|
|
1074
|
+
if (receipt.state !== "ok")
|
|
1075
|
+
process.exitCode = 1;
|
|
1076
|
+
} else if (flag === "--status" && parsed.rest.length === 2) {
|
|
1077
|
+
const receipt = await readRestartReceipt(home);
|
|
1078
|
+
if (receipt === undefined)
|
|
1079
|
+
throw new Error("no restart-stack receipt");
|
|
1080
|
+
for (const line of renderRestartReceipt(receipt))
|
|
1081
|
+
console.log(line);
|
|
1082
|
+
if (effectiveRestartState(receipt) !== "ok")
|
|
1083
|
+
process.exitCode = 1;
|
|
1084
|
+
} else if (parsed.rest.length === 1) {
|
|
1085
|
+
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
1086
|
+
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
1087
|
+
console.log("read the outcome with: gajaeway ops restart-stack --status");
|
|
1088
|
+
} else
|
|
1089
|
+
throw new Error(usage2);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
let redeliverParams;
|
|
1093
|
+
if (command === "redeliver") {
|
|
1094
|
+
if (parsed.rest.length === 2 && path && path !== "--since")
|
|
1095
|
+
redeliverParams = { deliveryId: path };
|
|
1096
|
+
else if (parsed.rest.length === 3 && path === "--since" && parsed.rest[2] && Number.isFinite(Date.parse(parsed.rest[2])))
|
|
1097
|
+
redeliverParams = { since: parsed.rest[2] };
|
|
1098
|
+
else
|
|
1099
|
+
throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
1100
|
+
}
|
|
552
1101
|
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
553
1102
|
try {
|
|
554
1103
|
if (command === "backup" && path)
|
|
555
1104
|
console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
1105
|
+
else if (redeliverParams)
|
|
1106
|
+
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
556
1107
|
else if (command === "integrity")
|
|
557
1108
|
console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
558
1109
|
else if (command === "cycle") {
|
|
@@ -566,7 +1117,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
566
1117
|
console.log(line);
|
|
567
1118
|
process.exitCode = cycleExitCode(cycle);
|
|
568
1119
|
} else
|
|
569
|
-
throw new Error("usage: gajaeway ops backup <path>|cycle [--json]|integrity|restore <backupPath
|
|
1120
|
+
throw new Error("usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|restart-stack [--status]");
|
|
570
1121
|
} finally {
|
|
571
1122
|
await client.close();
|
|
572
1123
|
}
|
|
@@ -595,12 +1146,16 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
595
1146
|
break;
|
|
596
1147
|
}
|
|
597
1148
|
case "monitors": {
|
|
598
|
-
const
|
|
1149
|
+
const [command, ...args2] = parsed.rest;
|
|
1150
|
+
const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
|
|
1151
|
+
const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
|
|
1152
|
+
const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
|
|
599
1153
|
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
600
1154
|
try {
|
|
601
|
-
const [command, ...args2] = parsed.rest;
|
|
602
1155
|
if (command === "add" && args2[0] === "--json" && args2[1])
|
|
603
1156
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
|
|
1157
|
+
else if (command === "update" && update)
|
|
1158
|
+
console.log(JSON.stringify(await client.request("monitor.update", update.params)));
|
|
604
1159
|
else if (command === "list") {
|
|
605
1160
|
const options2 = listOptions;
|
|
606
1161
|
const result = await client.request("monitor.list");
|
|
@@ -613,22 +1168,20 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
613
1168
|
console.log(JSON.stringify(await client.request("monitor.inspect", { monitorId: args2[0] })));
|
|
614
1169
|
else if (command === "remove" && args2[0])
|
|
615
1170
|
console.log(JSON.stringify(await client.request("monitor.remove", { monitorId: args2[0] })));
|
|
616
|
-
else if (command === "test" &&
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1171
|
+
else if (command === "test" && monitorTest) {
|
|
1172
|
+
const result = await client.request("monitor.test", {
|
|
1173
|
+
monitorId: monitorTest.monitorId,
|
|
1174
|
+
...monitorTest.eventType === undefined ? {} : { eventType: monitorTest.eventType },
|
|
1175
|
+
payload: monitorTest.payload
|
|
1176
|
+
});
|
|
1177
|
+
if (monitorTest.waitSeconds === undefined)
|
|
1178
|
+
console.log(JSON.stringify(result));
|
|
1179
|
+
else {
|
|
1180
|
+
const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
|
|
1181
|
+
console.log(JSON.stringify({ eventId: result.eventId, stage }));
|
|
624
1182
|
}
|
|
625
|
-
console.log(JSON.stringify(await client.request("monitor.test", {
|
|
626
|
-
monitorId: args2[0],
|
|
627
|
-
...eventType ? { eventType } : {},
|
|
628
|
-
payload
|
|
629
|
-
})));
|
|
630
1183
|
} 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]`);
|
|
1184
|
+
throw new Error(`usage: gajaeway monitors add --json '<MonitorSpec json>'|update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])|list [--json] [--fields ${columnNames(MONITOR_COLUMNS).join(",")}] [--limit N] [--offset N]|inspect <id>|remove <id>|test <id> [--type T] [--payload J] [--wait[=SECONDS]]`);
|
|
632
1185
|
} finally {
|
|
633
1186
|
await client.close();
|
|
634
1187
|
}
|
|
@@ -636,7 +1189,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
636
1189
|
}
|
|
637
1190
|
case "work": {
|
|
638
1191
|
const [command, ...args2] = parsed.rest;
|
|
639
|
-
const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME]
|
|
1192
|
+
const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] "<task text>"|status <name>|steer <name> <text>|retire <name>|jobs';
|
|
640
1193
|
if (command === "status" || command === "steer") {
|
|
641
1194
|
const name2 = args2[0];
|
|
642
1195
|
const text2 = args2.slice(1).join(" ").trim();
|
|
@@ -670,7 +1223,8 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
670
1223
|
for (const job of result.jobs) {
|
|
671
1224
|
const name2 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
672
1225
|
const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
|
|
673
|
-
|
|
1226
|
+
const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
|
|
1227
|
+
console.log(`${name2} ${state} session=${job.session_id || "-"} accepted=${job.accepted_at || "-"} op=${job.attempt?.op_ref || "-"} last=${job.last_activity_at || "-"} head=${head} ${job.worktree_path}${job.reports && (job.reports.pending !== 0 || job.reports.claimed !== 0 || job.reports.held !== 0 || job.reports.undeliverable !== 0) ? ` reports=p:${job.reports.pending} c:${job.reports.claimed} h:${job.reports.held} u:${job.reports.undeliverable}` : ""}`);
|
|
674
1228
|
}
|
|
675
1229
|
}
|
|
676
1230
|
} finally {
|
|
@@ -684,11 +1238,10 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
684
1238
|
let cwd;
|
|
685
1239
|
let resume = false;
|
|
686
1240
|
let model;
|
|
687
|
-
let notify;
|
|
688
1241
|
const textParts = [];
|
|
689
1242
|
for (let i = 1;i < args2.length; i++) {
|
|
690
1243
|
const arg = args2[i];
|
|
691
|
-
if (arg === "--cwd" || arg === "--model" || arg === "--preset"
|
|
1244
|
+
if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
|
|
692
1245
|
const value = args2[++i];
|
|
693
1246
|
if (!value?.trim() || value.startsWith("--"))
|
|
694
1247
|
throw new Error(usage2);
|
|
@@ -696,15 +1249,6 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
696
1249
|
if (cwd !== undefined || !isAbsolute(value))
|
|
697
1250
|
throw new Error(usage2);
|
|
698
1251
|
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
1252
|
} else {
|
|
709
1253
|
if (model !== undefined)
|
|
710
1254
|
throw new Error(`${usage2}
|
|
@@ -725,13 +1269,14 @@ invalid --notify originKey`);
|
|
|
725
1269
|
requestTimeoutMs: command === "run" ? 3600000 : 120000
|
|
726
1270
|
});
|
|
727
1271
|
try {
|
|
1272
|
+
const callerSessionId = process.env.GJC_SESSION_ID?.trim();
|
|
728
1273
|
const params = {
|
|
729
1274
|
name,
|
|
730
1275
|
text,
|
|
731
1276
|
...cwd ? { cwd } : {},
|
|
732
1277
|
...resume ? { resume: true } : {},
|
|
733
1278
|
...model === undefined ? {} : { model },
|
|
734
|
-
...
|
|
1279
|
+
...callerSessionId ? { callerSessionId } : {}
|
|
735
1280
|
};
|
|
736
1281
|
if (command === "start") {
|
|
737
1282
|
const result = await client.request("work.start", params);
|
|
@@ -761,10 +1306,14 @@ job: ${result.jobId} state: ${result.state}`);
|
|
|
761
1306
|
const written = await installServices({
|
|
762
1307
|
binDir: service.binDir,
|
|
763
1308
|
...service.launchAgentsDir === undefined ? {} : { launchAgentsDir: service.launchAgentsDir },
|
|
1309
|
+
...service.unitDir === undefined ? {} : { unitDir: service.unitDir },
|
|
1310
|
+
...service.platform === undefined ? {} : { platform: service.platform },
|
|
764
1311
|
env: process.env,
|
|
765
1312
|
...options.services
|
|
766
1313
|
});
|
|
767
|
-
console.log(`services ${service.action}: wrote ${written.length}
|
|
1314
|
+
console.log(`services ${service.action}: wrote ${written.length} service definitions`);
|
|
1315
|
+
for (const definition of written)
|
|
1316
|
+
console.log(definition);
|
|
768
1317
|
break;
|
|
769
1318
|
}
|
|
770
1319
|
default:
|
|
@@ -787,6 +1336,8 @@ export {
|
|
|
787
1336
|
parseServicesArgs,
|
|
788
1337
|
renderCycle,
|
|
789
1338
|
restoreDatabase,
|
|
1339
|
+
restoreTargetPath,
|
|
790
1340
|
socketPath,
|
|
791
|
-
usageFor
|
|
1341
|
+
usageFor,
|
|
1342
|
+
verifyBackupIntegrity
|
|
792
1343
|
};
|