@gajae-gateway/cli 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/main.d.ts +38 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +665 -66
- package/dist/restart-stack.d.ts +110 -0
- package/dist/restart-stack.d.ts.map +1 -0
- package/dist/services.d.ts +37 -1
- package/dist/services.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/main.ts +358 -51
- package/src/restart-stack.ts +429 -0
- package/src/services.ts +148 -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,12 @@ 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 { access, mkdir as mkdir2, readFile as readFile2, rename, stat, writeFile as writeFile2 } from "fs/promises";
|
|
133
|
+
import { homedir as homedir2 } from "os";
|
|
134
|
+
import { join as join2 } from "path";
|
|
135
|
+
|
|
129
136
|
// src/services.ts
|
|
130
137
|
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
131
138
|
import { homedir } from "os";
|
|
@@ -134,22 +141,34 @@ import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol"
|
|
|
134
141
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
135
142
|
var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
136
143
|
var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
|
|
144
|
+
var GATEWAY_UNIT = "gajaeway-gateway.service";
|
|
137
145
|
var SERVICE_SPECS = [
|
|
138
|
-
{
|
|
146
|
+
{
|
|
147
|
+
id: "gateway",
|
|
148
|
+
label: "dev.gajaeway.gateway",
|
|
149
|
+
binary: "gajaeway-gateway",
|
|
150
|
+
args: ["daemon"],
|
|
151
|
+
dependsOnGateway: false
|
|
152
|
+
},
|
|
139
153
|
{
|
|
140
154
|
id: "adapter-discord",
|
|
141
155
|
label: "dev.gajaeway.adapter-discord",
|
|
142
156
|
binary: "gajaeway-discord",
|
|
143
|
-
args: []
|
|
157
|
+
args: [],
|
|
158
|
+
dependsOnGateway: true
|
|
144
159
|
},
|
|
145
160
|
{
|
|
146
161
|
id: "adapter-slack",
|
|
147
162
|
label: "dev.gajaeway.adapter-slack",
|
|
148
163
|
binary: "gajaeway-slack",
|
|
149
|
-
args: []
|
|
164
|
+
args: [],
|
|
165
|
+
dependsOnGateway: true
|
|
150
166
|
},
|
|
151
|
-
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"] }
|
|
167
|
+
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true }
|
|
152
168
|
];
|
|
169
|
+
function serviceSpecs() {
|
|
170
|
+
return SERVICE_SPECS;
|
|
171
|
+
}
|
|
153
172
|
function homeForEnvironment(env) {
|
|
154
173
|
return env.GAJAEWAY_HOME || join(env.HOME || homedir(), ".gajaeway");
|
|
155
174
|
}
|
|
@@ -237,6 +256,70 @@ function renderLaunchAgent(spec, binDir, home, path) {
|
|
|
237
256
|
</dict></plist>
|
|
238
257
|
`;
|
|
239
258
|
}
|
|
259
|
+
function unitEnvironment(name, value) {
|
|
260
|
+
return `Environment="${name}=${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
261
|
+
}
|
|
262
|
+
function systemdUnitName(spec) {
|
|
263
|
+
return `gajaeway-${spec.id}.service`;
|
|
264
|
+
}
|
|
265
|
+
function renderSystemdUnit(spec, binDir, home, path) {
|
|
266
|
+
const execStart = [join(binDir, spec.binary), ...spec.args].join(" ");
|
|
267
|
+
const unit = [
|
|
268
|
+
"[Unit]",
|
|
269
|
+
`Description=gajaeway ${spec.id}`,
|
|
270
|
+
...spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
|
|
271
|
+
"",
|
|
272
|
+
"[Service]",
|
|
273
|
+
`ExecStart=${execStart}`,
|
|
274
|
+
`WorkingDirectory=${binDir}`,
|
|
275
|
+
unitEnvironment("GAJAEWAY_HOME", home),
|
|
276
|
+
unitEnvironment("PATH", path),
|
|
277
|
+
"Restart=always",
|
|
278
|
+
"RestartSec=2",
|
|
279
|
+
...spec.dependsOnGateway ? [] : ["KillMode=process", "TimeoutStopSec=30s"],
|
|
280
|
+
"",
|
|
281
|
+
"[Install]",
|
|
282
|
+
`WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
|
|
283
|
+
""
|
|
284
|
+
];
|
|
285
|
+
return unit.join(`
|
|
286
|
+
`);
|
|
287
|
+
}
|
|
288
|
+
function systemdUnitDir(env, userHome) {
|
|
289
|
+
const base = env.XDG_CONFIG_HOME || join(userHome, ".config");
|
|
290
|
+
return join(base, "systemd", "user");
|
|
291
|
+
}
|
|
292
|
+
function launchAgentPlistPath(spec, launchAgentsDir) {
|
|
293
|
+
return join(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`);
|
|
294
|
+
}
|
|
295
|
+
function systemdUnitPath(spec, unitDir) {
|
|
296
|
+
return join(unitDir, systemdUnitName(spec));
|
|
297
|
+
}
|
|
298
|
+
function restartStackCommands(platform, uid) {
|
|
299
|
+
if (platform === "linux")
|
|
300
|
+
return [["systemctl", "--user", "restart", GATEWAY_UNIT]];
|
|
301
|
+
if (platform === "darwin") {
|
|
302
|
+
const target = uid ?? process.getuid?.() ?? 0;
|
|
303
|
+
return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`unsupported service platform: ${platform}`);
|
|
306
|
+
}
|
|
307
|
+
function restartOrder() {
|
|
308
|
+
return [
|
|
309
|
+
...SERVICE_SPECS.filter((spec) => !spec.dependsOnGateway),
|
|
310
|
+
...SERVICE_SPECS.filter((spec) => spec.dependsOnGateway)
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
function requirePlatform(platform) {
|
|
314
|
+
if (platform !== "darwin" && platform !== "linux")
|
|
315
|
+
throw new Error(`unsupported service platform: ${platform}`);
|
|
316
|
+
return platform;
|
|
317
|
+
}
|
|
318
|
+
function currentPlatform() {
|
|
319
|
+
if (process.platform === "darwin" || process.platform === "linux")
|
|
320
|
+
return process.platform;
|
|
321
|
+
throw new Error(`unsupported service platform: ${process.platform}`);
|
|
322
|
+
}
|
|
240
323
|
async function readRuntime(home) {
|
|
241
324
|
const configPath = join(home, "config.json");
|
|
242
325
|
let raw;
|
|
@@ -267,10 +350,11 @@ async function installServices(options) {
|
|
|
267
350
|
if (options.binDir.length === 0)
|
|
268
351
|
throw new Error("services requires a non-empty --bin-dir DIR");
|
|
269
352
|
const env = options.env ?? process.env;
|
|
353
|
+
const platform = options.platform === undefined ? currentPlatform() : requirePlatform(options.platform);
|
|
270
354
|
const userHome = env.HOME || homedir();
|
|
271
355
|
const home = homeForEnvironment(env);
|
|
272
356
|
const binDir = expandHome(options.binDir, userHome);
|
|
273
|
-
const
|
|
357
|
+
const targetDir = platform === "darwin" ? expandHome(options.launchAgentsDir ?? join(userHome, "Library", "LaunchAgents"), userHome) : expandHome(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
|
|
274
358
|
const runtime = await readRuntime(home);
|
|
275
359
|
const path = await resolvePath({
|
|
276
360
|
binDir,
|
|
@@ -279,24 +363,298 @@ async function installServices(options) {
|
|
|
279
363
|
env,
|
|
280
364
|
loginPathRunner: options.loginPathRunner
|
|
281
365
|
});
|
|
282
|
-
await mkdir(
|
|
283
|
-
const
|
|
284
|
-
path: join(
|
|
366
|
+
await mkdir(targetDir, { recursive: true, mode: 448 });
|
|
367
|
+
const definitions = SERVICE_SPECS.map((spec) => platform === "darwin" ? {
|
|
368
|
+
path: join(targetDir, `dev.gajaeway.${spec.id}.plist`),
|
|
285
369
|
contents: renderLaunchAgent(spec, binDir, home, path)
|
|
286
|
-
}));
|
|
370
|
+
} : { path: join(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
|
|
287
371
|
const write = options.writeFile;
|
|
288
|
-
for (const
|
|
372
|
+
for (const definition of definitions) {
|
|
289
373
|
if (write)
|
|
290
|
-
await write(
|
|
374
|
+
await write(definition.path, definition.contents);
|
|
291
375
|
else {
|
|
292
|
-
await writeFile(
|
|
293
|
-
await chmod(
|
|
376
|
+
await writeFile(definition.path, definition.contents, { encoding: "utf8", mode: 384 });
|
|
377
|
+
await chmod(definition.path, 384);
|
|
294
378
|
}
|
|
295
379
|
}
|
|
296
|
-
return
|
|
380
|
+
return definitions.map((definition) => definition.path);
|
|
297
381
|
}
|
|
298
382
|
function serviceUsage() {
|
|
299
|
-
return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR]";
|
|
383
|
+
return "usage: gajaeway services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/restart-stack.ts
|
|
387
|
+
var RECEIPT_FILE = "restart-stack.json";
|
|
388
|
+
var VERIFY_TIMEOUT_MS = 90000;
|
|
389
|
+
var POLL_MS = 1000;
|
|
390
|
+
function restartReceiptPath(home) {
|
|
391
|
+
return join2(home, RECEIPT_FILE);
|
|
392
|
+
}
|
|
393
|
+
async function readRestartReceipt(home) {
|
|
394
|
+
let raw;
|
|
395
|
+
try {
|
|
396
|
+
raw = await readFile2(restartReceiptPath(home), "utf8");
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error.code === "ENOENT")
|
|
399
|
+
return;
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
return JSON.parse(raw);
|
|
403
|
+
}
|
|
404
|
+
async function writeReceipt(home, receipt) {
|
|
405
|
+
await mkdir2(home, { recursive: true });
|
|
406
|
+
const path = restartReceiptPath(home);
|
|
407
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
408
|
+
await writeFile2(temporary, `${JSON.stringify(receipt, null, 2)}
|
|
409
|
+
`, { mode: 384 });
|
|
410
|
+
await rename(temporary, path);
|
|
411
|
+
}
|
|
412
|
+
function newReceipt(id, platform, uid, now) {
|
|
413
|
+
const commands = restartStackCommands(platform, uid);
|
|
414
|
+
return {
|
|
415
|
+
id,
|
|
416
|
+
state: "queued",
|
|
417
|
+
platform,
|
|
418
|
+
requestedAt: new Date(now).toISOString(),
|
|
419
|
+
steps: restartOrder().map((spec, index) => ({
|
|
420
|
+
label: spec.label,
|
|
421
|
+
...platform === "darwin" ? { command: commands[index] } : index === 0 ? { command: commands[0] } : {},
|
|
422
|
+
result: "pending"
|
|
423
|
+
}))
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async function defaultCommandRunner(command) {
|
|
427
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
428
|
+
return await child.exited;
|
|
429
|
+
}
|
|
430
|
+
async function capture(command) {
|
|
431
|
+
const child = Bun.spawn([...command], {
|
|
432
|
+
stdin: "ignore",
|
|
433
|
+
stdout: "pipe",
|
|
434
|
+
stderr: "ignore",
|
|
435
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
436
|
+
});
|
|
437
|
+
const output = await new Response(child.stdout).text();
|
|
438
|
+
return await child.exited === 0 ? output : undefined;
|
|
439
|
+
}
|
|
440
|
+
function defaultServiceProbe(platform) {
|
|
441
|
+
return async (label) => {
|
|
442
|
+
const spec = serviceSpecs().find((candidate) => candidate.label === label);
|
|
443
|
+
if (spec === undefined)
|
|
444
|
+
throw new Error(`unknown service label: ${label}`);
|
|
445
|
+
let pid;
|
|
446
|
+
if (platform === "darwin") {
|
|
447
|
+
const listed = await capture(["launchctl", "list", label]);
|
|
448
|
+
const match = listed?.match(/"PID"\s*=\s*(\d+);/);
|
|
449
|
+
pid = match ? Number(match[1]) : undefined;
|
|
450
|
+
} else {
|
|
451
|
+
const shown = await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
|
|
452
|
+
pid = shown === undefined ? undefined : Number(shown.trim());
|
|
453
|
+
}
|
|
454
|
+
if (pid === undefined || !Number.isInteger(pid) || pid <= 0)
|
|
455
|
+
return;
|
|
456
|
+
const lstart = await capture(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
457
|
+
const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
|
|
458
|
+
if (lstart === undefined || args === undefined)
|
|
459
|
+
return;
|
|
460
|
+
const startedAt = Date.parse(lstart.trim());
|
|
461
|
+
if (!Number.isFinite(startedAt))
|
|
462
|
+
return;
|
|
463
|
+
const suffix = spec.args.length > 0 ? ` ${spec.args.join(" ")}` : "";
|
|
464
|
+
const line = args.trim();
|
|
465
|
+
const binary = suffix && line.endsWith(suffix) ? line.slice(0, -suffix.length) : line;
|
|
466
|
+
return { pid, startedAt, binary };
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async function defaultBinaryModifiedAt(path) {
|
|
470
|
+
return (await stat(path)).mtimeMs;
|
|
471
|
+
}
|
|
472
|
+
function defaultIsServiceInstalled(options) {
|
|
473
|
+
return async (label) => {
|
|
474
|
+
const spec = serviceSpecs().find((s) => s.label === label);
|
|
475
|
+
if (spec === undefined)
|
|
476
|
+
return false;
|
|
477
|
+
if (options.platform === "darwin") {
|
|
478
|
+
const userHome2 = options.env.HOME || homedir2();
|
|
479
|
+
const agentsDir = options.launchAgentsDir || join2(userHome2, "Library", "LaunchAgents");
|
|
480
|
+
const plistPath = launchAgentPlistPath(spec, agentsDir);
|
|
481
|
+
try {
|
|
482
|
+
await access(plistPath);
|
|
483
|
+
return true;
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const userHome = options.env.HOME || homedir2();
|
|
489
|
+
const unitDirPath = options.unitDir || systemdUnitDir(options.env, userHome);
|
|
490
|
+
const unitPath = systemdUnitPath(spec, unitDirPath);
|
|
491
|
+
try {
|
|
492
|
+
await access(unitPath);
|
|
493
|
+
return true;
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function floorSecond(ms) {
|
|
500
|
+
return Math.floor(ms / 1000) * 1000;
|
|
501
|
+
}
|
|
502
|
+
async function runRestartStack(options) {
|
|
503
|
+
const platform = options.platform ?? currentPlatform();
|
|
504
|
+
const runner = options.runner ?? defaultCommandRunner;
|
|
505
|
+
const probe = options.probe ?? defaultServiceProbe(platform);
|
|
506
|
+
const modifiedAt = options.binaryModifiedAt ?? defaultBinaryModifiedAt;
|
|
507
|
+
const isInstalled = options.isServiceInstalled ?? defaultIsServiceInstalled({
|
|
508
|
+
platform,
|
|
509
|
+
env: options.env ?? process.env,
|
|
510
|
+
launchAgentsDir: options.launchAgentsDir,
|
|
511
|
+
unitDir: options.unitDir
|
|
512
|
+
});
|
|
513
|
+
const now = options.now ?? Date.now;
|
|
514
|
+
const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
|
|
515
|
+
const timeoutMs = options.verifyTimeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
516
|
+
const pollMs = options.pollMs ?? POLL_MS;
|
|
517
|
+
const existing = await readRestartReceipt(options.home);
|
|
518
|
+
const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now());
|
|
519
|
+
receipt.state = "running";
|
|
520
|
+
receipt.supervisorPid = process.pid;
|
|
521
|
+
await writeReceipt(options.home, receipt);
|
|
522
|
+
let issuedAt = now();
|
|
523
|
+
for (const step of receipt.steps) {
|
|
524
|
+
if (receipt.state !== "running") {
|
|
525
|
+
step.result = "skipped";
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const installed = await isInstalled(step.label);
|
|
530
|
+
if (!installed) {
|
|
531
|
+
step.result = "skipped";
|
|
532
|
+
step.detail = "service is not installed";
|
|
533
|
+
await writeReceipt(options.home, receipt);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (step.command !== undefined) {
|
|
537
|
+
issuedAt = now();
|
|
538
|
+
const status = await runner(step.command);
|
|
539
|
+
step.exitStatus = status;
|
|
540
|
+
if (status !== 0) {
|
|
541
|
+
step.result = "failed";
|
|
542
|
+
step.detail = `${step.command.join(" ")} exited with status ${status}`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (step.result === "pending")
|
|
546
|
+
await verifyStep(step, issuedAt);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
step.result = "failed";
|
|
549
|
+
step.detail = error instanceof Error ? error.message : String(error);
|
|
550
|
+
}
|
|
551
|
+
if (step.result !== "ok" && step.result !== "skipped") {
|
|
552
|
+
receipt.state = step.result === "stale" ? "stale" : "failed";
|
|
553
|
+
}
|
|
554
|
+
await writeReceipt(options.home, receipt);
|
|
555
|
+
}
|
|
556
|
+
if (receipt.state === "running")
|
|
557
|
+
receipt.state = "ok";
|
|
558
|
+
receipt.finishedAt = new Date(now()).toISOString();
|
|
559
|
+
await writeReceipt(options.home, receipt);
|
|
560
|
+
return receipt;
|
|
561
|
+
async function verifyStep(step, since) {
|
|
562
|
+
const deadline = now() + timeoutMs;
|
|
563
|
+
for (;; ) {
|
|
564
|
+
const running = await probe(step.label);
|
|
565
|
+
let binaryModifiedAt = 0;
|
|
566
|
+
if (running !== undefined) {
|
|
567
|
+
binaryModifiedAt = await modifiedAt(running.binary);
|
|
568
|
+
step.pid = running.pid;
|
|
569
|
+
step.processStartedAt = new Date(running.startedAt).toISOString();
|
|
570
|
+
step.binary = running.binary;
|
|
571
|
+
step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
|
|
572
|
+
if (running.startedAt >= floorSecond(Math.max(binaryModifiedAt, since))) {
|
|
573
|
+
step.result = "ok";
|
|
574
|
+
delete step.detail;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (now() >= deadline) {
|
|
579
|
+
if (running === undefined) {
|
|
580
|
+
step.result = "failed";
|
|
581
|
+
step.detail = "no running process after restart";
|
|
582
|
+
} else {
|
|
583
|
+
step.result = "stale";
|
|
584
|
+
step.detail = running.startedAt < binaryModifiedAt ? "process started before the deployed binary was modified" : "process was not restarted by this sequence";
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
await sleep(pollMs);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function cliCommand() {
|
|
593
|
+
return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
|
|
594
|
+
}
|
|
595
|
+
function alive(pid) {
|
|
596
|
+
if (pid === undefined)
|
|
597
|
+
return false;
|
|
598
|
+
try {
|
|
599
|
+
process.kill(pid, 0);
|
|
600
|
+
return true;
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function launchRestartStack(options) {
|
|
606
|
+
const platform = options.platform ?? currentPlatform();
|
|
607
|
+
const existing = await readRestartReceipt(options.home);
|
|
608
|
+
if (existing?.state === "running" && alive(existing.supervisorPid))
|
|
609
|
+
throw new Error(`restart-stack ${existing.id} is still running (supervisor pid ${existing.supervisorPid})`);
|
|
610
|
+
const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
|
|
611
|
+
await writeReceipt(options.home, receipt);
|
|
612
|
+
const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
|
|
613
|
+
const child = Bun.spawn(supervisorArgv(platform, worker, options.home, receipt.id), {
|
|
614
|
+
detached: true,
|
|
615
|
+
stdin: "ignore",
|
|
616
|
+
stdout: "ignore",
|
|
617
|
+
stderr: "ignore",
|
|
618
|
+
env: { ...options.env ?? process.env, GAJAEWAY_HOME: options.home }
|
|
619
|
+
});
|
|
620
|
+
child.unref();
|
|
621
|
+
return { receipt, supervisorPid: child.pid };
|
|
622
|
+
}
|
|
623
|
+
function supervisorArgv(platform, worker, home, id) {
|
|
624
|
+
if (platform === "darwin")
|
|
625
|
+
return [...worker];
|
|
626
|
+
return [
|
|
627
|
+
"systemd-run",
|
|
628
|
+
"--user",
|
|
629
|
+
"--collect",
|
|
630
|
+
"--quiet",
|
|
631
|
+
`--unit=gajaeway-restart-stack-${id}`,
|
|
632
|
+
`--setenv=GAJAEWAY_HOME=${home}`,
|
|
633
|
+
"--",
|
|
634
|
+
...worker
|
|
635
|
+
];
|
|
636
|
+
}
|
|
637
|
+
function effectiveRestartState(receipt) {
|
|
638
|
+
if (receipt.state === "running" && !alive(receipt.supervisorPid))
|
|
639
|
+
return "interrupted";
|
|
640
|
+
return receipt.state;
|
|
641
|
+
}
|
|
642
|
+
function renderRestartReceipt(receipt) {
|
|
643
|
+
const lines = [
|
|
644
|
+
`restart-stack ${receipt.id}: ${effectiveRestartState(receipt)}`,
|
|
645
|
+
`requestedAt: ${receipt.requestedAt}`,
|
|
646
|
+
`finishedAt: ${receipt.finishedAt ?? "-"}`
|
|
647
|
+
];
|
|
648
|
+
for (const step of receipt.steps) {
|
|
649
|
+
const facts = [
|
|
650
|
+
step.pid === undefined ? undefined : `pid=${step.pid}`,
|
|
651
|
+
step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
|
|
652
|
+
step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
|
|
653
|
+
step.detail
|
|
654
|
+
].filter((fact) => fact !== undefined);
|
|
655
|
+
lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
|
|
656
|
+
}
|
|
657
|
+
return lines;
|
|
300
658
|
}
|
|
301
659
|
|
|
302
660
|
// src/main.ts
|
|
@@ -315,7 +673,7 @@ var COMMANDS = [
|
|
|
315
673
|
"work",
|
|
316
674
|
"services"
|
|
317
675
|
];
|
|
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
|
|
676
|
+
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
677
|
var USAGE_EXIT_CODE = 2;
|
|
320
678
|
function usageFor(command) {
|
|
321
679
|
return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
|
|
@@ -340,22 +698,171 @@ function parseServicesArgs(args) {
|
|
|
340
698
|
throw new Error(serviceUsage());
|
|
341
699
|
let binDir;
|
|
342
700
|
let launchAgentsDir;
|
|
701
|
+
let unitDir;
|
|
702
|
+
let platform;
|
|
343
703
|
for (let i = 1;i < args.length; i++) {
|
|
344
704
|
const flag = args[i];
|
|
345
|
-
if (flag === "--bin-dir" || flag === "--launch-agents-dir") {
|
|
705
|
+
if (flag === "--bin-dir" || flag === "--launch-agents-dir" || flag === "--unit-dir") {
|
|
346
706
|
const value = args[++i];
|
|
347
707
|
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
348
708
|
throw new Error(`${flag} expects a non-empty DIR`);
|
|
349
709
|
if (flag === "--bin-dir")
|
|
350
710
|
binDir = value;
|
|
711
|
+
else if (flag === "--unit-dir")
|
|
712
|
+
unitDir = value;
|
|
351
713
|
else
|
|
352
714
|
launchAgentsDir = value;
|
|
715
|
+
} else if (flag === "--platform") {
|
|
716
|
+
const value = args[++i];
|
|
717
|
+
if (value !== "darwin" && value !== "linux")
|
|
718
|
+
throw new Error("--platform expects darwin or linux");
|
|
719
|
+
platform = value;
|
|
353
720
|
} else
|
|
354
721
|
throw new Error(`unknown option: ${flag}`);
|
|
355
722
|
}
|
|
356
723
|
if (binDir === undefined)
|
|
357
724
|
throw new Error("services requires --bin-dir DIR");
|
|
358
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
action,
|
|
727
|
+
binDir,
|
|
728
|
+
...launchAgentsDir === undefined ? {} : { launchAgentsDir },
|
|
729
|
+
...unitDir === undefined ? {} : { unitDir },
|
|
730
|
+
...platform === undefined ? {} : { platform }
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
var MONITOR_UPDATE_USAGE = "usage: gajaeway monitors update <id> (--json '<partial MonitorSpec JSON>'|--schedule '<cron>' [--enabled true|false]|--enabled true|false [--schedule '<cron>'])";
|
|
734
|
+
function parseMonitorUpdateArgs(args) {
|
|
735
|
+
const [monitorId, ...options] = args;
|
|
736
|
+
if (!monitorId || monitorId.startsWith("--") || options.length === 0)
|
|
737
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
738
|
+
let jsonPatch;
|
|
739
|
+
let schedule;
|
|
740
|
+
let enabled;
|
|
741
|
+
for (let i = 0;i < options.length; i++) {
|
|
742
|
+
const option = options[i];
|
|
743
|
+
if (option === "--json") {
|
|
744
|
+
const value = options[++i];
|
|
745
|
+
if (jsonPatch !== undefined || value === undefined || value.startsWith("--"))
|
|
746
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
747
|
+
let parsed;
|
|
748
|
+
try {
|
|
749
|
+
parsed = JSON.parse(value);
|
|
750
|
+
} catch {
|
|
751
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
752
|
+
}
|
|
753
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
754
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
755
|
+
jsonPatch = parsed;
|
|
756
|
+
} else if (option === "--schedule") {
|
|
757
|
+
const value = options[++i];
|
|
758
|
+
if (schedule !== undefined || !value || value.startsWith("--"))
|
|
759
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
760
|
+
schedule = value;
|
|
761
|
+
} else if (option === "--enabled") {
|
|
762
|
+
const value = options[++i];
|
|
763
|
+
if (enabled !== undefined || value !== "true" && value !== "false")
|
|
764
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
765
|
+
enabled = value === "true";
|
|
766
|
+
} else
|
|
767
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
768
|
+
}
|
|
769
|
+
if (jsonPatch !== undefined && (schedule !== undefined || enabled !== undefined))
|
|
770
|
+
throw new Error(MONITOR_UPDATE_USAGE);
|
|
771
|
+
if (jsonPatch !== undefined)
|
|
772
|
+
return { monitorId, params: { ...jsonPatch, monitorId } };
|
|
773
|
+
return {
|
|
774
|
+
monitorId,
|
|
775
|
+
params: {
|
|
776
|
+
monitorId,
|
|
777
|
+
...schedule === undefined ? {} : { schedule },
|
|
778
|
+
...enabled === undefined ? {} : { enabled }
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
var MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
|
|
783
|
+
var MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}
|
|
784
|
+
--wait expects a finite non-negative integer up to 86400`;
|
|
785
|
+
function parseMonitorTestArgs(args) {
|
|
786
|
+
const [monitorId, ...flags] = args;
|
|
787
|
+
if (!monitorId || monitorId.startsWith("--"))
|
|
788
|
+
throw new Error(MONITOR_TEST_USAGE);
|
|
789
|
+
let eventType;
|
|
790
|
+
let payload = {};
|
|
791
|
+
let waitSeconds;
|
|
792
|
+
const seen = new Set;
|
|
793
|
+
for (let i = 0;i < flags.length; i++) {
|
|
794
|
+
const flag = flags[i];
|
|
795
|
+
if (flag === "--type" || flag === "--payload") {
|
|
796
|
+
if (seen.has(flag))
|
|
797
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
798
|
+
duplicate ${flag}`);
|
|
799
|
+
seen.add(flag);
|
|
800
|
+
const value = flags[++i];
|
|
801
|
+
if (value === undefined || value.length === 0 || value.startsWith("--"))
|
|
802
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
803
|
+
${flag} requires a value`);
|
|
804
|
+
if (flag === "--type")
|
|
805
|
+
eventType = value;
|
|
806
|
+
else
|
|
807
|
+
payload = JSON.parse(value);
|
|
808
|
+
} else if (flag === "--wait" || flag.startsWith("--wait=")) {
|
|
809
|
+
if (seen.has("--wait"))
|
|
810
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
811
|
+
duplicate --wait`);
|
|
812
|
+
seen.add("--wait");
|
|
813
|
+
const value = flag === "--wait" ? undefined : flag.slice("--wait=".length);
|
|
814
|
+
if (value === undefined)
|
|
815
|
+
waitSeconds = 30;
|
|
816
|
+
else {
|
|
817
|
+
if (!/^\d+$/.test(value))
|
|
818
|
+
throw new Error(MONITOR_WAIT_ERROR);
|
|
819
|
+
const seconds = Number(value);
|
|
820
|
+
if (!Number.isFinite(seconds) || !Number.isInteger(seconds) || seconds > 86400)
|
|
821
|
+
throw new Error(MONITOR_WAIT_ERROR);
|
|
822
|
+
waitSeconds = seconds;
|
|
823
|
+
}
|
|
824
|
+
} else
|
|
825
|
+
throw new Error(`${MONITOR_TEST_USAGE}
|
|
826
|
+
unknown argument: ${flag}`);
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
monitorId,
|
|
830
|
+
...eventType === undefined ? {} : { eventType },
|
|
831
|
+
payload,
|
|
832
|
+
...waitSeconds === undefined ? {} : { waitSeconds }
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
|
|
836
|
+
function waitForMonitorStage(client, eventId, timeoutSeconds) {
|
|
837
|
+
return new Promise((resolve) => {
|
|
838
|
+
let stage = "admitted";
|
|
839
|
+
let settled = false;
|
|
840
|
+
let timer;
|
|
841
|
+
let unsubscribe;
|
|
842
|
+
const finish = () => {
|
|
843
|
+
if (settled)
|
|
844
|
+
return;
|
|
845
|
+
settled = true;
|
|
846
|
+
if (timer !== undefined)
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
unsubscribe?.();
|
|
849
|
+
resolve(stage);
|
|
850
|
+
};
|
|
851
|
+
unsubscribe = client.on("monitor.event", (payload) => {
|
|
852
|
+
if (typeof payload !== "object" || payload === null)
|
|
853
|
+
return;
|
|
854
|
+
const event = payload;
|
|
855
|
+
if (event.eventId !== eventId || typeof event.stage !== "string")
|
|
856
|
+
return;
|
|
857
|
+
stage = event.stage;
|
|
858
|
+
if (TERMINAL_MONITOR_STAGES.has(stage))
|
|
859
|
+
finish();
|
|
860
|
+
});
|
|
861
|
+
if (settled)
|
|
862
|
+
unsubscribe();
|
|
863
|
+
else
|
|
864
|
+
timer = setTimeout(finish, timeoutSeconds * 1000);
|
|
865
|
+
});
|
|
359
866
|
}
|
|
360
867
|
function renderCycle(cycle) {
|
|
361
868
|
const lines = [];
|
|
@@ -370,7 +877,14 @@ function renderCycle(cycle) {
|
|
|
370
877
|
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
878
|
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
879
|
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)" : ""}`);
|
|
880
|
+
if (cycle.agentDisk) {
|
|
881
|
+
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
882
|
+
const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
883
|
+
lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
|
|
884
|
+
}
|
|
373
885
|
lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
|
|
886
|
+
if (cycle.monitorAuthoringLost.length > 0)
|
|
887
|
+
lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
|
|
374
888
|
if (cycle.sessions.length > 0) {
|
|
375
889
|
lines.push("sessions:");
|
|
376
890
|
lines.push("INDEX ORIGIN EPOCH SESSION PENDING UNSETTLED OLDEST");
|
|
@@ -385,11 +899,61 @@ function renderCycle(cycle) {
|
|
|
385
899
|
function cycleExitCode(cycle) {
|
|
386
900
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
387
901
|
}
|
|
902
|
+
function verifyBackupIntegrity(backupPath) {
|
|
903
|
+
let database;
|
|
904
|
+
try {
|
|
905
|
+
database = new Database(backupPath, { readonly: true });
|
|
906
|
+
} catch (error) {
|
|
907
|
+
throw new Error(`Backup is not readable: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
908
|
+
}
|
|
909
|
+
let detail;
|
|
910
|
+
let pageCount;
|
|
911
|
+
try {
|
|
912
|
+
pageCount = database.query("PRAGMA page_count").get()?.page_count ?? 0;
|
|
913
|
+
detail = database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
|
|
914
|
+
} catch (error) {
|
|
915
|
+
throw new Error(`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
916
|
+
} finally {
|
|
917
|
+
database.close();
|
|
918
|
+
}
|
|
919
|
+
if (pageCount === 0)
|
|
920
|
+
throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
|
|
921
|
+
if (detail !== "ok")
|
|
922
|
+
throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
|
|
923
|
+
}
|
|
924
|
+
async function restoreTargetPath(home = gatewayHome()) {
|
|
925
|
+
const configPath = join3(home, "config.json");
|
|
926
|
+
let raw;
|
|
927
|
+
try {
|
|
928
|
+
raw = await readFile3(configPath, "utf8");
|
|
929
|
+
} catch (error) {
|
|
930
|
+
const code = error.code;
|
|
931
|
+
const entryExists = await lstat(configPath).then(() => true, () => false);
|
|
932
|
+
if (code !== "ENOENT" || entryExists)
|
|
933
|
+
throw new Error(`Cannot read ${configPath}; refusing restore.`);
|
|
934
|
+
}
|
|
935
|
+
if (raw === undefined)
|
|
936
|
+
return join3(home, "gateway.db");
|
|
937
|
+
let parsed;
|
|
938
|
+
try {
|
|
939
|
+
parsed = JSON.parse(raw);
|
|
940
|
+
} catch {
|
|
941
|
+
throw new Error(`Cannot parse ${configPath}; refusing restore.`);
|
|
942
|
+
}
|
|
943
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
944
|
+
throw new Error(`Invalid configuration ${configPath}: config must be an object; refusing restore.`);
|
|
945
|
+
const dbPath = parsed.dbPath;
|
|
946
|
+
if (dbPath === undefined)
|
|
947
|
+
return join3(home, "gateway.db");
|
|
948
|
+
if (typeof dbPath !== "string" || dbPath.length === 0)
|
|
949
|
+
throw new Error(`Invalid configuration ${configPath}: dbPath must be a non-empty string; refusing restore.`);
|
|
950
|
+
return dbPath;
|
|
951
|
+
}
|
|
388
952
|
async function restoreDatabase(socket, backupPath) {
|
|
389
953
|
if (!isAbsolute(backupPath))
|
|
390
954
|
throw new Error("ops restore requires an absolute backup path");
|
|
391
955
|
try {
|
|
392
|
-
await
|
|
956
|
+
await stat2(socket);
|
|
393
957
|
throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
|
|
394
958
|
} catch (error) {
|
|
395
959
|
if (error instanceof Error && error.message.startsWith("Refusing restore:"))
|
|
@@ -397,15 +961,8 @@ async function restoreDatabase(socket, backupPath) {
|
|
|
397
961
|
if (error.code !== "ENOENT")
|
|
398
962
|
throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
|
|
399
963
|
}
|
|
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");
|
|
964
|
+
verifyBackupIntegrity(backupPath);
|
|
965
|
+
const databasePath = await restoreTargetPath();
|
|
409
966
|
const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
410
967
|
await copyFile(databasePath, preservedPath);
|
|
411
968
|
await copyFile(backupPath, databasePath);
|
|
@@ -413,6 +970,7 @@ async function restoreDatabase(socket, backupPath) {
|
|
|
413
970
|
console.log(`Copied current database to: ${preservedPath}`);
|
|
414
971
|
console.log(`Restored backup to: ${databasePath}`);
|
|
415
972
|
}
|
|
973
|
+
var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
|
|
416
974
|
async function chat(socket) {
|
|
417
975
|
let client;
|
|
418
976
|
try {
|
|
@@ -437,9 +995,9 @@ async function chat(socket) {
|
|
|
437
995
|
await new Promise((resolve) => {
|
|
438
996
|
const timer = setTimeout(() => {
|
|
439
997
|
turnWaiters.delete(turnId);
|
|
440
|
-
console.error(
|
|
998
|
+
console.error(`(turn timed out after ${CHAT_TURN_TIMEOUT_MS / 60000}m)`);
|
|
441
999
|
resolve();
|
|
442
|
-
},
|
|
1000
|
+
}, CHAT_TURN_TIMEOUT_MS);
|
|
443
1001
|
turnWaiters.set(turnId, () => {
|
|
444
1002
|
clearTimeout(timer);
|
|
445
1003
|
turnWaiters.delete(turnId);
|
|
@@ -549,10 +1107,51 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
549
1107
|
await restoreDatabase(parsed.socket, path);
|
|
550
1108
|
break;
|
|
551
1109
|
}
|
|
1110
|
+
if (command === "restart-stack") {
|
|
1111
|
+
const usage2 = "usage: gajaeway ops restart-stack [--status]";
|
|
1112
|
+
const home = gatewayHome();
|
|
1113
|
+
const flag = parsed.rest[1];
|
|
1114
|
+
if (flag === "--run" && parsed.rest.length === 3 && parsed.rest[2]) {
|
|
1115
|
+
const receipt = await runRestartStack({
|
|
1116
|
+
...options.restartStack?.run,
|
|
1117
|
+
home,
|
|
1118
|
+
id: parsed.rest[2]
|
|
1119
|
+
});
|
|
1120
|
+
for (const line of renderRestartReceipt(receipt))
|
|
1121
|
+
console.log(line);
|
|
1122
|
+
if (receipt.state !== "ok")
|
|
1123
|
+
process.exitCode = 1;
|
|
1124
|
+
} else if (flag === "--status" && parsed.rest.length === 2) {
|
|
1125
|
+
const receipt = await readRestartReceipt(home);
|
|
1126
|
+
if (receipt === undefined)
|
|
1127
|
+
throw new Error("no restart-stack receipt");
|
|
1128
|
+
for (const line of renderRestartReceipt(receipt))
|
|
1129
|
+
console.log(line);
|
|
1130
|
+
if (effectiveRestartState(receipt) !== "ok")
|
|
1131
|
+
process.exitCode = 1;
|
|
1132
|
+
} else if (parsed.rest.length === 1) {
|
|
1133
|
+
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
1134
|
+
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
1135
|
+
console.log("read the outcome with: gajaeway ops restart-stack --status");
|
|
1136
|
+
} else
|
|
1137
|
+
throw new Error(usage2);
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
let redeliverParams;
|
|
1141
|
+
if (command === "redeliver") {
|
|
1142
|
+
if (parsed.rest.length === 2 && path && path !== "--since")
|
|
1143
|
+
redeliverParams = { deliveryId: path };
|
|
1144
|
+
else if (parsed.rest.length === 3 && path === "--since" && parsed.rest[2] && Number.isFinite(Date.parse(parsed.rest[2])))
|
|
1145
|
+
redeliverParams = { since: parsed.rest[2] };
|
|
1146
|
+
else
|
|
1147
|
+
throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
1148
|
+
}
|
|
552
1149
|
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
553
1150
|
try {
|
|
554
1151
|
if (command === "backup" && path)
|
|
555
1152
|
console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
1153
|
+
else if (redeliverParams)
|
|
1154
|
+
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
556
1155
|
else if (command === "integrity")
|
|
557
1156
|
console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
558
1157
|
else if (command === "cycle") {
|
|
@@ -566,7 +1165,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
566
1165
|
console.log(line);
|
|
567
1166
|
process.exitCode = cycleExitCode(cycle);
|
|
568
1167
|
} else
|
|
569
|
-
throw new Error("usage: gajaeway ops backup <path>|cycle [--json]|integrity|restore <backupPath
|
|
1168
|
+
throw new Error("usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|restart-stack [--status]");
|
|
570
1169
|
} finally {
|
|
571
1170
|
await client.close();
|
|
572
1171
|
}
|
|
@@ -595,12 +1194,16 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
595
1194
|
break;
|
|
596
1195
|
}
|
|
597
1196
|
case "monitors": {
|
|
598
|
-
const
|
|
1197
|
+
const [command, ...args2] = parsed.rest;
|
|
1198
|
+
const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
|
|
1199
|
+
const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
|
|
1200
|
+
const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
|
|
599
1201
|
const client = await GajaewayClient.connectSocket(parsed.socket);
|
|
600
1202
|
try {
|
|
601
|
-
const [command, ...args2] = parsed.rest;
|
|
602
1203
|
if (command === "add" && args2[0] === "--json" && args2[1])
|
|
603
1204
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
|
|
1205
|
+
else if (command === "update" && update)
|
|
1206
|
+
console.log(JSON.stringify(await client.request("monitor.update", update.params)));
|
|
604
1207
|
else if (command === "list") {
|
|
605
1208
|
const options2 = listOptions;
|
|
606
1209
|
const result = await client.request("monitor.list");
|
|
@@ -613,22 +1216,20 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
613
1216
|
console.log(JSON.stringify(await client.request("monitor.inspect", { monitorId: args2[0] })));
|
|
614
1217
|
else if (command === "remove" && args2[0])
|
|
615
1218
|
console.log(JSON.stringify(await client.request("monitor.remove", { monitorId: args2[0] })));
|
|
616
|
-
else if (command === "test" &&
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1219
|
+
else if (command === "test" && monitorTest) {
|
|
1220
|
+
const result = await client.request("monitor.test", {
|
|
1221
|
+
monitorId: monitorTest.monitorId,
|
|
1222
|
+
...monitorTest.eventType === undefined ? {} : { eventType: monitorTest.eventType },
|
|
1223
|
+
payload: monitorTest.payload
|
|
1224
|
+
});
|
|
1225
|
+
if (monitorTest.waitSeconds === undefined)
|
|
1226
|
+
console.log(JSON.stringify(result));
|
|
1227
|
+
else {
|
|
1228
|
+
const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
|
|
1229
|
+
console.log(JSON.stringify({ eventId: result.eventId, stage }));
|
|
624
1230
|
}
|
|
625
|
-
console.log(JSON.stringify(await client.request("monitor.test", {
|
|
626
|
-
monitorId: args2[0],
|
|
627
|
-
...eventType ? { eventType } : {},
|
|
628
|
-
payload
|
|
629
|
-
})));
|
|
630
1231
|
} 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]`);
|
|
1232
|
+
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
1233
|
} finally {
|
|
633
1234
|
await client.close();
|
|
634
1235
|
}
|
|
@@ -636,7 +1237,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
636
1237
|
}
|
|
637
1238
|
case "work": {
|
|
638
1239
|
const [command, ...args2] = parsed.rest;
|
|
639
|
-
const usage2 = 'usage: gajaeway work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME]
|
|
1240
|
+
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
1241
|
if (command === "status" || command === "steer") {
|
|
641
1242
|
const name2 = args2[0];
|
|
642
1243
|
const text2 = args2.slice(1).join(" ").trim();
|
|
@@ -670,7 +1271,8 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
670
1271
|
for (const job of result.jobs) {
|
|
671
1272
|
const name2 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
672
1273
|
const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
|
|
673
|
-
|
|
1274
|
+
const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
|
|
1275
|
+
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
1276
|
}
|
|
675
1277
|
}
|
|
676
1278
|
} finally {
|
|
@@ -684,11 +1286,10 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
684
1286
|
let cwd;
|
|
685
1287
|
let resume = false;
|
|
686
1288
|
let model;
|
|
687
|
-
let notify;
|
|
688
1289
|
const textParts = [];
|
|
689
1290
|
for (let i = 1;i < args2.length; i++) {
|
|
690
1291
|
const arg = args2[i];
|
|
691
|
-
if (arg === "--cwd" || arg === "--model" || arg === "--preset"
|
|
1292
|
+
if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
|
|
692
1293
|
const value = args2[++i];
|
|
693
1294
|
if (!value?.trim() || value.startsWith("--"))
|
|
694
1295
|
throw new Error(usage2);
|
|
@@ -696,15 +1297,6 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
696
1297
|
if (cwd !== undefined || !isAbsolute(value))
|
|
697
1298
|
throw new Error(usage2);
|
|
698
1299
|
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
1300
|
} else {
|
|
709
1301
|
if (model !== undefined)
|
|
710
1302
|
throw new Error(`${usage2}
|
|
@@ -725,13 +1317,14 @@ invalid --notify originKey`);
|
|
|
725
1317
|
requestTimeoutMs: command === "run" ? 3600000 : 120000
|
|
726
1318
|
});
|
|
727
1319
|
try {
|
|
1320
|
+
const callerSessionId = process.env.GJC_SESSION_ID?.trim();
|
|
728
1321
|
const params = {
|
|
729
1322
|
name,
|
|
730
1323
|
text,
|
|
731
1324
|
...cwd ? { cwd } : {},
|
|
732
1325
|
...resume ? { resume: true } : {},
|
|
733
1326
|
...model === undefined ? {} : { model },
|
|
734
|
-
...
|
|
1327
|
+
...callerSessionId ? { callerSessionId } : {}
|
|
735
1328
|
};
|
|
736
1329
|
if (command === "start") {
|
|
737
1330
|
const result = await client.request("work.start", params);
|
|
@@ -761,10 +1354,14 @@ job: ${result.jobId} state: ${result.state}`);
|
|
|
761
1354
|
const written = await installServices({
|
|
762
1355
|
binDir: service.binDir,
|
|
763
1356
|
...service.launchAgentsDir === undefined ? {} : { launchAgentsDir: service.launchAgentsDir },
|
|
1357
|
+
...service.unitDir === undefined ? {} : { unitDir: service.unitDir },
|
|
1358
|
+
...service.platform === undefined ? {} : { platform: service.platform },
|
|
764
1359
|
env: process.env,
|
|
765
1360
|
...options.services
|
|
766
1361
|
});
|
|
767
|
-
console.log(`services ${service.action}: wrote ${written.length}
|
|
1362
|
+
console.log(`services ${service.action}: wrote ${written.length} service definitions`);
|
|
1363
|
+
for (const definition of written)
|
|
1364
|
+
console.log(definition);
|
|
768
1365
|
break;
|
|
769
1366
|
}
|
|
770
1367
|
default:
|
|
@@ -787,6 +1384,8 @@ export {
|
|
|
787
1384
|
parseServicesArgs,
|
|
788
1385
|
renderCycle,
|
|
789
1386
|
restoreDatabase,
|
|
1387
|
+
restoreTargetPath,
|
|
790
1388
|
socketPath,
|
|
791
|
-
usageFor
|
|
1389
|
+
usageFor,
|
|
1390
|
+
verifyBackupIntegrity
|
|
792
1391
|
};
|