@gajae-gateway/cli 0.1.8 → 0.2.1
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/discord-setup.d.ts +41 -0
- package/dist/discord-setup.d.ts.map +1 -0
- package/dist/gjc-install.d.ts +13 -0
- package/dist/gjc-install.d.ts.map +1 -0
- package/dist/main.d.ts +17 -30
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1318 -582
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +27 -13
- package/dist/restart-stack.d.ts.map +1 -1
- package/dist/services.d.ts +9 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/setup.d.ts +14 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/up.d.ts +20 -0
- package/dist/up.d.ts.map +1 -0
- package/dist/update.d.ts +10 -6
- package/dist/update.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/discord-setup.ts +103 -0
- package/src/gjc-install.ts +148 -0
- package/src/main.ts +296 -151
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +309 -92
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +175 -98
- package/dist/upgrade.d.ts +0 -40
- package/dist/upgrade.d.ts.map +0 -1
- package/src/upgrade.ts +0 -205
package/dist/main.js
CHANGED
|
@@ -2,11 +2,63 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/main.ts
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { isAbsolute, join as join10 } from "path";
|
|
8
|
-
import { LOOPBACK_ORIGIN, originKey as originKey2 } from "@gajae-gateway/protocol";
|
|
5
|
+
import { isAbsolute } from "path";
|
|
6
|
+
import { LOOPBACK_ORIGIN, originKey as originKey2, WORK_JOBS_MAX_LIMIT } from "@gajae-gateway/protocol";
|
|
9
7
|
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
8
|
+
// package.json
|
|
9
|
+
var package_default = {
|
|
10
|
+
name: "@gajae-gateway/cli",
|
|
11
|
+
version: "0.2.1",
|
|
12
|
+
type: "module",
|
|
13
|
+
description: "Owner CLI for the gajaeway gateway: status, sessions, chat, monitors, and worker lanes over the gateway socket. Requires the Bun runtime (uses Bun.file/Bun.spawn).",
|
|
14
|
+
license: "AGPL-3.0-only",
|
|
15
|
+
author: "Yeachan Heo",
|
|
16
|
+
repository: {
|
|
17
|
+
type: "git",
|
|
18
|
+
url: "git+https://github.com/Yeachan-Heo/gajae-way.git",
|
|
19
|
+
directory: "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
homepage: "https://github.com/Yeachan-Heo/gajae-way/tree/main/packages/cli#readme",
|
|
22
|
+
bugs: "https://github.com/Yeachan-Heo/gajae-way/issues",
|
|
23
|
+
keywords: [
|
|
24
|
+
"gajaeway",
|
|
25
|
+
"cli",
|
|
26
|
+
"gateway"
|
|
27
|
+
],
|
|
28
|
+
files: [
|
|
29
|
+
"dist",
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
main: "./dist/main.js",
|
|
33
|
+
types: "./src/main.ts",
|
|
34
|
+
exports: {
|
|
35
|
+
".": {
|
|
36
|
+
bun: "./src/main.ts",
|
|
37
|
+
types: "./src/main.ts",
|
|
38
|
+
default: "./dist/main.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
bin: {
|
|
42
|
+
gajaeway: "./dist/main.js"
|
|
43
|
+
},
|
|
44
|
+
publishConfig: {
|
|
45
|
+
access: "public"
|
|
46
|
+
},
|
|
47
|
+
scripts: {
|
|
48
|
+
build: "rm -rf dist && bun build src/main.ts --outdir dist --target bun --format esm --external @gajae-gateway/protocol --external @gajae-gateway/sdk && tsc -p tsconfig.build.json",
|
|
49
|
+
prepublishOnly: "bun run build"
|
|
50
|
+
},
|
|
51
|
+
dependencies: {
|
|
52
|
+
"@gajae-gateway/sdk": "workspace:*",
|
|
53
|
+
"@gajae-gateway/protocol": "workspace:*"
|
|
54
|
+
},
|
|
55
|
+
engines: {
|
|
56
|
+
bun: ">=1.4.0"
|
|
57
|
+
},
|
|
58
|
+
devDependencies: {
|
|
59
|
+
"@gajae-gateway/home": "workspace:*"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
10
62
|
|
|
11
63
|
// src/list.ts
|
|
12
64
|
import { originKey } from "@gajae-gateway/protocol";
|
|
@@ -145,11 +197,203 @@ function renderList(columns, rows, options, envelope) {
|
|
|
145
197
|
}
|
|
146
198
|
|
|
147
199
|
// src/migrate.ts
|
|
148
|
-
import {
|
|
149
|
-
import {
|
|
200
|
+
import { randomUUID } from "crypto";
|
|
201
|
+
import { chmod, mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile } from "fs/promises";
|
|
202
|
+
import { join as join5 } from "path";
|
|
203
|
+
|
|
204
|
+
// ../home/src/layout.ts
|
|
205
|
+
import { join } from "path";
|
|
206
|
+
function homeLayout(home) {
|
|
207
|
+
const leasesDir = join(home, "leases");
|
|
208
|
+
const brokerDir = join(home, "broker");
|
|
209
|
+
const brokerProvisionDir = join(brokerDir, "provision");
|
|
210
|
+
const gjcPrefix = join(home, "gjc");
|
|
211
|
+
const gjcCurrent = join(gjcPrefix, "current");
|
|
212
|
+
return {
|
|
213
|
+
home,
|
|
214
|
+
leasesDir,
|
|
215
|
+
lease: (name) => join(leasesDir, name),
|
|
216
|
+
brokerAgentDir: join(brokerDir, "agent"),
|
|
217
|
+
brokerProvisionDir,
|
|
218
|
+
brokerProvisionManifest: join(brokerProvisionDir, "manifest.json"),
|
|
219
|
+
backupsDir: join(home, "backups"),
|
|
220
|
+
gjcPrefix,
|
|
221
|
+
gjcCurrent,
|
|
222
|
+
gjcExecutable: (version) => join(version === undefined ? gjcCurrent : join(gjcPrefix, version), "bin", "gjc"),
|
|
223
|
+
socket: join(home, "gateway.sock"),
|
|
224
|
+
db: join(home, "gateway.db")
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ../home/src/lease.ts
|
|
228
|
+
import { dlopen, FFIType, read } from "bun:ffi";
|
|
229
|
+
import { constants } from "fs";
|
|
230
|
+
import { mkdir, open, readFile } from "fs/promises";
|
|
231
|
+
var LOCK_EX = 2;
|
|
232
|
+
var LOCK_NB = 4;
|
|
233
|
+
var F_GETFD = 1;
|
|
234
|
+
var FD_CLOEXEC = 1;
|
|
235
|
+
var MAX_OWNER_BYTES = 4096;
|
|
236
|
+
var PLATFORMS = {
|
|
237
|
+
darwin: { library: "/usr/lib/libSystem.B.dylib", errnoSymbol: "__error", oCloexec: 16777216, ewouldblock: 35 },
|
|
238
|
+
linux: { library: "libc.so.6", errnoSymbol: "__errno_location", oCloexec: 524288, ewouldblock: 11 }
|
|
239
|
+
};
|
|
240
|
+
var libc;
|
|
241
|
+
var heldHandles = new Set;
|
|
242
|
+
function loadLibc(name) {
|
|
243
|
+
if (libc)
|
|
244
|
+
return libc;
|
|
245
|
+
const platform = PLATFORMS[process.platform];
|
|
246
|
+
if (!platform)
|
|
247
|
+
throw new LeaseError("lease_unsupported", name, "dlopen", process.platform);
|
|
248
|
+
const symbols = {
|
|
249
|
+
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
250
|
+
fcntl: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 },
|
|
251
|
+
[platform.errnoSymbol]: { args: [], returns: FFIType.ptr }
|
|
252
|
+
};
|
|
253
|
+
const library = dlopen(platform.library, symbols);
|
|
254
|
+
const errnoLocation = library.symbols[platform.errnoSymbol];
|
|
255
|
+
libc = {
|
|
256
|
+
platform,
|
|
257
|
+
flock: (fd, operation) => library.symbols.flock(fd, operation),
|
|
258
|
+
getfd: (fd) => library.symbols.fcntl(fd, F_GETFD),
|
|
259
|
+
errno: () => read.i32(errnoLocation(), 0)
|
|
260
|
+
};
|
|
261
|
+
return libc;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
class LeaseHeldError extends Error {
|
|
265
|
+
name;
|
|
266
|
+
owner;
|
|
267
|
+
code = "lease_held";
|
|
268
|
+
constructor(name, owner) {
|
|
269
|
+
super(`lease_held name=${name}${owner ? ` owner_pid=${owner.pid} since=${owner.startedAt}` : ""}`);
|
|
270
|
+
this.name = name;
|
|
271
|
+
this.owner = owner;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
150
274
|
|
|
275
|
+
class LeaseError extends Error {
|
|
276
|
+
code;
|
|
277
|
+
name;
|
|
278
|
+
syscall;
|
|
279
|
+
errno;
|
|
280
|
+
constructor(code, name, syscall, errno) {
|
|
281
|
+
super(`${code} name=${name} syscall=${syscall} errno=${errno}`);
|
|
282
|
+
this.code = code;
|
|
283
|
+
this.name = name;
|
|
284
|
+
this.syscall = syscall;
|
|
285
|
+
this.errno = errno;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function openLeaseFile(path, name, lib, create) {
|
|
289
|
+
const flags = constants.O_RDWR | constants.O_NOFOLLOW | lib.platform.oCloexec | (create ? constants.O_CREAT : 0);
|
|
290
|
+
let handle;
|
|
291
|
+
try {
|
|
292
|
+
handle = await open(path, flags, 384);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
const code = error.code ?? String(error);
|
|
295
|
+
throw new LeaseError("lease_failed", name, "open", code);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
const info = await handle.stat();
|
|
299
|
+
if (!info.isFile() || info.nlink !== 1)
|
|
300
|
+
throw new LeaseError("lease_failed", name, "fstat", "not_regular_file");
|
|
301
|
+
const fdFlags = lib.getfd(handle.fd);
|
|
302
|
+
if (fdFlags < 0 || (fdFlags & FD_CLOEXEC) === 0)
|
|
303
|
+
throw new LeaseError("lease_failed", name, "fcntl", fdFlags < 0 ? lib.errno() : "cloexec_missing");
|
|
304
|
+
return handle;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
await handle.close();
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function tryLock(handle, name, lib) {
|
|
311
|
+
if (lib.flock(handle.fd, LOCK_EX | LOCK_NB) === 0)
|
|
312
|
+
return true;
|
|
313
|
+
const errno = lib.errno();
|
|
314
|
+
if (errno === lib.platform.ewouldblock)
|
|
315
|
+
return false;
|
|
316
|
+
throw new LeaseError("lease_failed", name, "flock", errno);
|
|
317
|
+
}
|
|
318
|
+
function parseOwner(raw) {
|
|
319
|
+
if (!raw.trim() || raw.length > MAX_OWNER_BYTES)
|
|
320
|
+
return;
|
|
321
|
+
try {
|
|
322
|
+
const value = JSON.parse(raw);
|
|
323
|
+
if (Number.isSafeInteger(value.pid) && typeof value.token === "string" && typeof value.startedAt === "string")
|
|
324
|
+
return { pid: value.pid, token: value.token, startedAt: value.startedAt };
|
|
325
|
+
} catch {}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
async function readLeaseOwner(home, name) {
|
|
329
|
+
try {
|
|
330
|
+
return parseOwner(await readFile(homeLayout(home).lease(name), "utf8"));
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function leaseHeld(home, name) {
|
|
336
|
+
const lib = loadLibc(name);
|
|
337
|
+
let handle;
|
|
338
|
+
try {
|
|
339
|
+
handle = await openLeaseFile(homeLayout(home).lease(name), name, lib, false);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof LeaseError && error.syscall === "open" && error.errno === "ENOENT")
|
|
342
|
+
return false;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
return !tryLock(handle, name, lib);
|
|
347
|
+
} finally {
|
|
348
|
+
await handle.close();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function acquireLease(home, name) {
|
|
352
|
+
const lib = loadLibc(name);
|
|
353
|
+
const layout = homeLayout(home);
|
|
354
|
+
try {
|
|
355
|
+
await mkdir(layout.leasesDir, { recursive: true, mode: 448 });
|
|
356
|
+
} catch (error) {
|
|
357
|
+
throw new LeaseError("lease_failed", name, "mkdir", error.code ?? String(error));
|
|
358
|
+
}
|
|
359
|
+
const path = layout.lease(name);
|
|
360
|
+
const handle = await openLeaseFile(path, name, lib, true);
|
|
361
|
+
try {
|
|
362
|
+
if (!tryLock(handle, name, lib))
|
|
363
|
+
throw new LeaseHeldError(name, await readLeaseOwner(home, name));
|
|
364
|
+
const owner = { pid: process.pid, token: crypto.randomUUID(), startedAt: new Date().toISOString() };
|
|
365
|
+
const bytes = Buffer.from(`${JSON.stringify(owner)}
|
|
366
|
+
`);
|
|
367
|
+
try {
|
|
368
|
+
await handle.truncate(0);
|
|
369
|
+
let offset = 0;
|
|
370
|
+
while (offset < bytes.length) {
|
|
371
|
+
const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, offset);
|
|
372
|
+
if (bytesWritten === 0)
|
|
373
|
+
throw new Error("short write");
|
|
374
|
+
offset += bytesWritten;
|
|
375
|
+
}
|
|
376
|
+
await handle.sync();
|
|
377
|
+
} catch (error) {
|
|
378
|
+
throw new LeaseError("lease_failed", name, "write", error.code ?? String(error));
|
|
379
|
+
}
|
|
380
|
+
heldHandles.add(handle);
|
|
381
|
+
let released = false;
|
|
382
|
+
const release = async () => {
|
|
383
|
+
if (released)
|
|
384
|
+
return;
|
|
385
|
+
released = true;
|
|
386
|
+
heldHandles.delete(handle);
|
|
387
|
+
await handle.close();
|
|
388
|
+
};
|
|
389
|
+
return { name, token: owner.token, path, release, [Symbol.asyncDispose]: release };
|
|
390
|
+
} catch (error) {
|
|
391
|
+
await handle.close();
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
151
395
|
// src/migrate/hermes.ts
|
|
152
|
-
import { join as
|
|
396
|
+
import { join as join3 } from "path";
|
|
153
397
|
|
|
154
398
|
// src/migrate/plan.ts
|
|
155
399
|
import { eventTypeOrigin, validateOriginRef } from "@gajae-gateway/protocol";
|
|
@@ -385,9 +629,9 @@ function emitImportedAxis(extract) {
|
|
|
385
629
|
}
|
|
386
630
|
|
|
387
631
|
// src/migrate/source-fs.ts
|
|
388
|
-
import { lstat, readdir, readFile } from "fs/promises";
|
|
632
|
+
import { lstat, readdir, readFile as readFile2 } from "fs/promises";
|
|
389
633
|
import { homedir } from "os";
|
|
390
|
-
import { join, relative } from "path";
|
|
634
|
+
import { join as join2, relative } from "path";
|
|
391
635
|
|
|
392
636
|
class SourceTree {
|
|
393
637
|
root;
|
|
@@ -419,14 +663,14 @@ class SourceTree {
|
|
|
419
663
|
if (!info)
|
|
420
664
|
return;
|
|
421
665
|
this.consume(path);
|
|
422
|
-
return
|
|
666
|
+
return readFile2(path, "utf8");
|
|
423
667
|
}
|
|
424
668
|
async bytes(path) {
|
|
425
669
|
const info = await regularFile(path);
|
|
426
670
|
if (!info)
|
|
427
671
|
return;
|
|
428
672
|
this.consume(path);
|
|
429
|
-
return new Uint8Array(await
|
|
673
|
+
return new Uint8Array(await readFile2(path));
|
|
430
674
|
}
|
|
431
675
|
}
|
|
432
676
|
async function regularFile(path) {
|
|
@@ -454,16 +698,16 @@ async function entries(path) {
|
|
|
454
698
|
async function walkFiles(dir, prefix = "") {
|
|
455
699
|
const out = [];
|
|
456
700
|
for (const name of await entries(dir)) {
|
|
457
|
-
const full =
|
|
701
|
+
const full = join2(dir, name);
|
|
458
702
|
const info = await lstat(full).catch(() => {
|
|
459
703
|
return;
|
|
460
704
|
});
|
|
461
705
|
if (!info)
|
|
462
706
|
continue;
|
|
463
707
|
if (info.isDirectory())
|
|
464
|
-
out.push(...await walkFiles(full,
|
|
708
|
+
out.push(...await walkFiles(full, join2(prefix, name)));
|
|
465
709
|
else if (info.isFile())
|
|
466
|
-
out.push(
|
|
710
|
+
out.push(join2(prefix, name));
|
|
467
711
|
}
|
|
468
712
|
return out;
|
|
469
713
|
}
|
|
@@ -487,7 +731,7 @@ function parseDotenv(text) {
|
|
|
487
731
|
return values;
|
|
488
732
|
}
|
|
489
733
|
function expandHome(path) {
|
|
490
|
-
return path === "~" ? homedir() : path.startsWith("~/") ?
|
|
734
|
+
return path === "~" ? homedir() : path.startsWith("~/") ? join2(homedir(), path.slice(2)) : path;
|
|
491
735
|
}
|
|
492
736
|
function isRecord(value) {
|
|
493
737
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -502,7 +746,7 @@ var HERMES_ENTRY_DELIMITER = `
|
|
|
502
746
|
\xA7
|
|
503
747
|
`;
|
|
504
748
|
async function detectHermes(root) {
|
|
505
|
-
return await regularFile(
|
|
749
|
+
return await regularFile(join3(root, "config.yaml")) !== undefined || await isDirectory(join3(root, "memories")) || await regularFile(join3(root, "cron", "jobs.json")) !== undefined;
|
|
506
750
|
}
|
|
507
751
|
var TOKEN_KEYS = {
|
|
508
752
|
DISCORD_BOT_TOKEN: "discord",
|
|
@@ -520,7 +764,7 @@ async function readHermes(tree, home) {
|
|
|
520
764
|
const root = tree.root;
|
|
521
765
|
const extract = emptyExtract("hermes", root);
|
|
522
766
|
for (const name of ["SOUL.md", "AGENTS.md"]) {
|
|
523
|
-
const path =
|
|
767
|
+
const path = join3(root, name);
|
|
524
768
|
const text = await tree.text(path);
|
|
525
769
|
if (text === undefined)
|
|
526
770
|
continue;
|
|
@@ -534,8 +778,8 @@ async function readHermes(tree, home) {
|
|
|
534
778
|
mtime: info?.mtime
|
|
535
779
|
});
|
|
536
780
|
}
|
|
537
|
-
const memoryDir =
|
|
538
|
-
const userPath =
|
|
781
|
+
const memoryDir = join3(root, "memories");
|
|
782
|
+
const userPath = join3(memoryDir, "USER.md");
|
|
539
783
|
const userText = await tree.text(userPath);
|
|
540
784
|
if (userText !== undefined) {
|
|
541
785
|
const info = await regularFile(userPath);
|
|
@@ -548,7 +792,7 @@ async function readHermes(tree, home) {
|
|
|
548
792
|
mtime: info?.mtime
|
|
549
793
|
});
|
|
550
794
|
}
|
|
551
|
-
const memoryPath =
|
|
795
|
+
const memoryPath = join3(memoryDir, "MEMORY.md");
|
|
552
796
|
const memoryText = await tree.text(memoryPath);
|
|
553
797
|
if (memoryText !== undefined) {
|
|
554
798
|
const info = await regularFile(memoryPath);
|
|
@@ -560,13 +804,12 @@ async function readHermes(tree, home) {
|
|
|
560
804
|
content: entriesToMarkdown("Long-term memory (imported from Hermes)", memoryText),
|
|
561
805
|
mtime: info?.mtime
|
|
562
806
|
});
|
|
563
|
-
emitImportedAxis(extract);
|
|
564
807
|
}
|
|
565
|
-
const envPath =
|
|
808
|
+
const envPath = join3(root, ".env");
|
|
566
809
|
const envText = await tree.text(envPath);
|
|
567
810
|
const env = envText === undefined ? new Map : parseDotenv(envText);
|
|
568
811
|
const consumedEnv = new Set;
|
|
569
|
-
const configPath =
|
|
812
|
+
const configPath = join3(root, "config.yaml");
|
|
570
813
|
const configText = await tree.text(configPath);
|
|
571
814
|
let config = {};
|
|
572
815
|
if (configText !== undefined) {
|
|
@@ -693,7 +936,7 @@ function readHermesChannelPolicy(config, env, channels, consumedConfig, consumed
|
|
|
693
936
|
}
|
|
694
937
|
}
|
|
695
938
|
async function readHermesCron(tree, env, consumedEnv, extract) {
|
|
696
|
-
const path =
|
|
939
|
+
const path = join3(tree.root, "cron", "jobs.json");
|
|
697
940
|
const text = await tree.text(path);
|
|
698
941
|
if (text === undefined)
|
|
699
942
|
return;
|
|
@@ -793,11 +1036,11 @@ function hermesTarget(job, env, consumedEnv, extract, source) {
|
|
|
793
1036
|
return resolved[0];
|
|
794
1037
|
}
|
|
795
1038
|
async function readHermesSkills(tree, extract) {
|
|
796
|
-
const skillsDir =
|
|
1039
|
+
const skillsDir = join3(tree.root, "skills");
|
|
797
1040
|
if (!await isDirectory(skillsDir))
|
|
798
1041
|
return;
|
|
799
1042
|
tree.consume(skillsDir);
|
|
800
|
-
const bundledText = await tree.text(
|
|
1043
|
+
const bundledText = await tree.text(join3(skillsDir, ".bundled_manifest"));
|
|
801
1044
|
const bundled = new Set((bundledText ?? "").split(/\r?\n/).map((line) => line.split(":")[0]?.trim() ?? "").filter(Boolean));
|
|
802
1045
|
const files = (await walkFiles(skillsDir)).filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
803
1046
|
const skillRoots = files.filter((file) => file.endsWith("/SKILL.md") || file === "SKILL.md").map((file) => file.slice(0, -"/SKILL.md".length));
|
|
@@ -806,7 +1049,7 @@ async function readHermesSkills(tree, extract) {
|
|
|
806
1049
|
const name = skillRoot.split("/").at(-1) ?? skillRoot;
|
|
807
1050
|
if (bundled.has(name))
|
|
808
1051
|
continue;
|
|
809
|
-
await copySkill(tree,
|
|
1052
|
+
await copySkill(tree, join3(skillsDir, skillRoot), name, extract, seen);
|
|
810
1053
|
}
|
|
811
1054
|
if (bundled.size)
|
|
812
1055
|
extract.warnings.push(`${bundled.size} Hermes-bundled skill(s) skipped; they ship with Hermes, not with your data`);
|
|
@@ -824,7 +1067,7 @@ async function copySkill(tree, dir, name, extract, seen) {
|
|
|
824
1067
|
}
|
|
825
1068
|
seen.add(name);
|
|
826
1069
|
for (const file of await walkFiles(dir)) {
|
|
827
|
-
const path =
|
|
1070
|
+
const path = join3(dir, file);
|
|
828
1071
|
const content = await tree.bytes(path);
|
|
829
1072
|
if (content === undefined)
|
|
830
1073
|
continue;
|
|
@@ -839,17 +1082,17 @@ async function copySkill(tree, dir, name, extract, seen) {
|
|
|
839
1082
|
}
|
|
840
1083
|
|
|
841
1084
|
// src/migrate/openclaw.ts
|
|
842
|
-
import { join as
|
|
1085
|
+
import { join as join4 } from "path";
|
|
843
1086
|
var OPENCLAW_CONFIG = "config.json";
|
|
844
1087
|
async function detectOpenClaw(root) {
|
|
845
|
-
const configPath =
|
|
1088
|
+
const configPath = join4(root, OPENCLAW_CONFIG);
|
|
846
1089
|
const stat = await regularFile(configPath);
|
|
847
1090
|
return stat !== undefined;
|
|
848
1091
|
}
|
|
849
1092
|
async function readOpenClaw(tree, _home) {
|
|
850
1093
|
const extract = emptyExtract("openclaw", tree.root);
|
|
851
|
-
const configPath =
|
|
852
|
-
const envPath =
|
|
1094
|
+
const configPath = join4(tree.root, OPENCLAW_CONFIG);
|
|
1095
|
+
const envPath = join4(tree.root, ".env");
|
|
853
1096
|
let config = {};
|
|
854
1097
|
const configText = await tree.text(configPath);
|
|
855
1098
|
if (configText) {
|
|
@@ -999,25 +1242,25 @@ function buildOpenClawMonitor(name, spec) {
|
|
|
999
1242
|
});
|
|
1000
1243
|
}
|
|
1001
1244
|
async function readOpenClawSkills(tree, skillsConfig, extract) {
|
|
1002
|
-
const skillsDir =
|
|
1245
|
+
const skillsDir = join4(tree.root, "skills");
|
|
1003
1246
|
const hasSkillsDir = await isDirectory(skillsDir);
|
|
1004
1247
|
for (const [skillName] of Object.entries(skillsConfig)) {
|
|
1005
1248
|
if (!hasSkillsDir)
|
|
1006
1249
|
continue;
|
|
1007
|
-
const skillPath =
|
|
1250
|
+
const skillPath = join4(skillsDir, `${skillName}.md`);
|
|
1008
1251
|
const skillContent = await tree.text(skillPath);
|
|
1009
1252
|
if (skillContent) {
|
|
1010
1253
|
extract.files.push({
|
|
1011
1254
|
category: "skills",
|
|
1012
1255
|
sources: [skillPath],
|
|
1013
|
-
target:
|
|
1256
|
+
target: join4(".gjc", "skills", `${slug(skillName)}.md`),
|
|
1014
1257
|
content: skillContent
|
|
1015
1258
|
});
|
|
1016
1259
|
}
|
|
1017
1260
|
}
|
|
1018
1261
|
}
|
|
1019
1262
|
async function readOpenClawMemory(tree, _memoryConfig, extract) {
|
|
1020
|
-
const memoryDir =
|
|
1263
|
+
const memoryDir = join4(tree.root, "memory");
|
|
1021
1264
|
const hasMemoryDir = await isDirectory(memoryDir);
|
|
1022
1265
|
if (!hasMemoryDir) {
|
|
1023
1266
|
return;
|
|
@@ -1025,13 +1268,13 @@ async function readOpenClawMemory(tree, _memoryConfig, extract) {
|
|
|
1025
1268
|
const files = await walkFiles(memoryDir);
|
|
1026
1269
|
for (const file of files) {
|
|
1027
1270
|
if (file.endsWith(".md")) {
|
|
1028
|
-
const fullPath =
|
|
1271
|
+
const fullPath = join4(memoryDir, file);
|
|
1029
1272
|
const content = await tree.text(fullPath);
|
|
1030
1273
|
if (content) {
|
|
1031
1274
|
extract.files.push({
|
|
1032
1275
|
category: "memory",
|
|
1033
1276
|
sources: [fullPath],
|
|
1034
|
-
target:
|
|
1277
|
+
target: join4("memory", "imported.md"),
|
|
1035
1278
|
content
|
|
1036
1279
|
});
|
|
1037
1280
|
}
|
|
@@ -1041,8 +1284,25 @@ async function readOpenClawMemory(tree, _memoryConfig, extract) {
|
|
|
1041
1284
|
|
|
1042
1285
|
// src/migrate.ts
|
|
1043
1286
|
async function migrate(options) {
|
|
1287
|
+
const targetHome = expandHome(options.target ?? process.env.GAJAEWAY_HOME ?? `${process.env.HOME ?? "~"}/.gajaeway`);
|
|
1288
|
+
if (options.dryRun)
|
|
1289
|
+
return migrateUnlocked(options, targetHome);
|
|
1290
|
+
let lease;
|
|
1291
|
+
try {
|
|
1292
|
+
lease = await acquireLease(targetHome, "memory");
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
if (error instanceof LeaseHeldError)
|
|
1295
|
+
throw new Error(`${error.message}; stop the gateway first before importing memory data`, { cause: error });
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
await migrateUnlocked(options, targetHome);
|
|
1300
|
+
} finally {
|
|
1301
|
+
await lease.release();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
async function migrateUnlocked(options, targetHome) {
|
|
1044
1305
|
const sourceRoot = expandHome(options.source ?? ".");
|
|
1045
|
-
const targetHome = expandHome(options.target ?? `${process.env.HOME ?? "~"}/.gajaeway`);
|
|
1046
1306
|
let sourceType;
|
|
1047
1307
|
if (await detectHermes(sourceRoot)) {
|
|
1048
1308
|
sourceType = "hermes";
|
|
@@ -1099,22 +1359,34 @@ Migration summary:`);
|
|
|
1099
1359
|
}
|
|
1100
1360
|
return;
|
|
1101
1361
|
}
|
|
1102
|
-
await
|
|
1362
|
+
await mkdir2(targetHome, { recursive: true });
|
|
1103
1363
|
const channels = emptyChannelPlan();
|
|
1104
1364
|
emitChannels(extract, channels, targetHome, sourceType);
|
|
1105
1365
|
emitMonitors(extract);
|
|
1106
1366
|
emitImportedAxis(extract);
|
|
1107
1367
|
for (const file of extract.files) {
|
|
1108
|
-
const filePath =
|
|
1109
|
-
await
|
|
1110
|
-
|
|
1368
|
+
const filePath = join5(targetHome, file.target);
|
|
1369
|
+
await mkdir2(join5(filePath, ".."), { recursive: true });
|
|
1370
|
+
if (file.secret) {
|
|
1371
|
+
const temporary = `${filePath}.${randomUUID()}.tmp`;
|
|
1372
|
+
try {
|
|
1373
|
+
await writeFile(temporary, file.content, { mode: 384 });
|
|
1374
|
+
await chmod(temporary, 384);
|
|
1375
|
+
await rename(temporary, filePath);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
await rm(temporary, { force: true });
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
} else {
|
|
1381
|
+
await writeFile(filePath, file.content);
|
|
1382
|
+
}
|
|
1111
1383
|
}
|
|
1112
1384
|
for (const merge of extract.merges) {
|
|
1113
|
-
const filePath =
|
|
1114
|
-
await
|
|
1385
|
+
const filePath = join5(targetHome, merge.target);
|
|
1386
|
+
await mkdir2(join5(filePath, ".."), { recursive: true });
|
|
1115
1387
|
let existing;
|
|
1116
1388
|
try {
|
|
1117
|
-
const text = await
|
|
1389
|
+
const text = await readFile3(filePath, "utf-8");
|
|
1118
1390
|
existing = JSON.parse(text);
|
|
1119
1391
|
} catch {}
|
|
1120
1392
|
const merged = merge.merge(existing);
|
|
@@ -1145,21 +1417,29 @@ function parseMigrateArgs(args) {
|
|
|
1145
1417
|
}
|
|
1146
1418
|
|
|
1147
1419
|
// src/restart-stack.ts
|
|
1148
|
-
import { randomUUID } from "crypto";
|
|
1149
|
-
import { access, mkdir as
|
|
1420
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1421
|
+
import { access, mkdir as mkdir4, readFile as readFile5, rename as rename2, stat, writeFile as writeFile3 } from "fs/promises";
|
|
1150
1422
|
import { homedir as homedir3 } from "os";
|
|
1151
|
-
import { join as
|
|
1423
|
+
import { dirname, join as join7 } from "path";
|
|
1152
1424
|
|
|
1153
1425
|
// src/services.ts
|
|
1154
|
-
import { chmod, mkdir as
|
|
1426
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
1155
1427
|
import { homedir as homedir2 } from "os";
|
|
1156
|
-
import { join as
|
|
1428
|
+
import { join as join6 } from "path";
|
|
1157
1429
|
import { parseRuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol";
|
|
1158
1430
|
var CONFIG_SCHEMA_VERSION = 1;
|
|
1159
1431
|
var FALLBACK_PATH_DIRS = ["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
1160
1432
|
var LOGIN_PATH_COMMAND = 'printf %s "$PATH"';
|
|
1433
|
+
var BROKER_UNIT = "gajaeway-broker.service";
|
|
1161
1434
|
var GATEWAY_UNIT = "gajaeway-gateway.service";
|
|
1162
1435
|
var SERVICE_SPECS = [
|
|
1436
|
+
{
|
|
1437
|
+
id: "broker",
|
|
1438
|
+
label: "dev.gajaeway.broker",
|
|
1439
|
+
binary: "gajaeway-gateway",
|
|
1440
|
+
args: ["broker"],
|
|
1441
|
+
dependsOnGateway: false
|
|
1442
|
+
},
|
|
1163
1443
|
{
|
|
1164
1444
|
id: "gateway",
|
|
1165
1445
|
label: "dev.gajaeway.gateway",
|
|
@@ -1181,16 +1461,23 @@ var SERVICE_SPECS = [
|
|
|
1181
1461
|
args: [],
|
|
1182
1462
|
dependsOnGateway: true
|
|
1183
1463
|
},
|
|
1464
|
+
{
|
|
1465
|
+
id: "adapter-telegram",
|
|
1466
|
+
label: "dev.gajaeway.adapter-telegram",
|
|
1467
|
+
binary: "gajaeway-telegram",
|
|
1468
|
+
args: [],
|
|
1469
|
+
dependsOnGateway: true
|
|
1470
|
+
},
|
|
1184
1471
|
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true }
|
|
1185
1472
|
];
|
|
1186
1473
|
function serviceSpecs() {
|
|
1187
1474
|
return SERVICE_SPECS;
|
|
1188
1475
|
}
|
|
1189
1476
|
function homeForEnvironment(env) {
|
|
1190
|
-
return env.GAJAEWAY_HOME ||
|
|
1477
|
+
return env.GAJAEWAY_HOME || join6(env.HOME || homedir2(), ".gajaeway");
|
|
1191
1478
|
}
|
|
1192
1479
|
function expandHome2(value, home) {
|
|
1193
|
-
return value.startsWith("~/") ?
|
|
1480
|
+
return value.startsWith("~/") ? join6(home, value.slice(2)) : value;
|
|
1194
1481
|
}
|
|
1195
1482
|
function normalizedEntries(entries2, home) {
|
|
1196
1483
|
const seen = new Set;
|
|
@@ -1209,9 +1496,9 @@ function normalizedEntries(entries2, home) {
|
|
|
1209
1496
|
function mergePathEntries(binDir, userHome, loginPath) {
|
|
1210
1497
|
return normalizedEntries([
|
|
1211
1498
|
expandHome2(binDir, userHome),
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1499
|
+
join6(userHome, "bin"),
|
|
1500
|
+
join6(userHome, ".local", "bin"),
|
|
1501
|
+
join6(userHome, ".bun", "bin"),
|
|
1215
1502
|
...loginPath.replace(/(?:\r\n|\n)+$/, "").split(":"),
|
|
1216
1503
|
...FALLBACK_PATH_DIRS
|
|
1217
1504
|
], userHome);
|
|
@@ -1254,7 +1541,7 @@ function plistString(value) {
|
|
|
1254
1541
|
return `<string>${escapeXml(value)}</string>`;
|
|
1255
1542
|
}
|
|
1256
1543
|
function renderLaunchAgent(spec, binDir, home, path) {
|
|
1257
|
-
const program =
|
|
1544
|
+
const program = join6(binDir, spec.binary);
|
|
1258
1545
|
const argumentsXml = [program, ...spec.args].map(plistString).join("");
|
|
1259
1546
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1260
1547
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -1268,8 +1555,8 @@ function renderLaunchAgent(spec, binDir, home, path) {
|
|
|
1268
1555
|
</dict>
|
|
1269
1556
|
<key>RunAtLoad</key><true/>
|
|
1270
1557
|
<key>KeepAlive</key><true/>
|
|
1271
|
-
<key>StandardOutPath</key>${plistString(
|
|
1272
|
-
<key>StandardErrorPath</key>${plistString(
|
|
1558
|
+
<key>StandardOutPath</key>${plistString(join6(home, `${spec.id}.stdout.log`))}
|
|
1559
|
+
<key>StandardErrorPath</key>${plistString(join6(home, `${spec.id}.stderr.log`))}
|
|
1273
1560
|
</dict></plist>
|
|
1274
1561
|
`;
|
|
1275
1562
|
}
|
|
@@ -1279,21 +1566,30 @@ function unitEnvironment(name, value) {
|
|
|
1279
1566
|
function systemdUnitName(spec) {
|
|
1280
1567
|
return `gajaeway-${spec.id}.service`;
|
|
1281
1568
|
}
|
|
1569
|
+
function unitPath(path) {
|
|
1570
|
+
if (!path.startsWith("/") || /[\n\r\0]/.test(path))
|
|
1571
|
+
throw new Error(`unit path must be absolute and single-line: ${path}`);
|
|
1572
|
+
return path.replaceAll("%", "%%");
|
|
1573
|
+
}
|
|
1574
|
+
function unitEnvironmentFile(path) {
|
|
1575
|
+
return `EnvironmentFile=-${unitPath(path)}`;
|
|
1576
|
+
}
|
|
1282
1577
|
function renderSystemdUnit(spec, binDir, home, path) {
|
|
1283
|
-
const execStart = [
|
|
1578
|
+
const execStart = [join6(binDir, spec.binary), ...spec.args].join(" ");
|
|
1284
1579
|
const unit = [
|
|
1285
1580
|
"[Unit]",
|
|
1286
1581
|
`Description=gajaeway ${spec.id}`,
|
|
1287
|
-
...spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
|
|
1582
|
+
...spec.id === "broker" ? [`ConditionPathExists=${unitPath(homeLayout(home).brokerProvisionManifest)}`] : spec.id === "gateway" ? [`Wants=${BROKER_UNIT}`, `After=${BROKER_UNIT}`] : spec.dependsOnGateway ? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`] : [],
|
|
1288
1583
|
"",
|
|
1289
1584
|
"[Service]",
|
|
1290
1585
|
`ExecStart=${execStart}`,
|
|
1291
1586
|
`WorkingDirectory=${binDir}`,
|
|
1292
1587
|
unitEnvironment("GAJAEWAY_HOME", home),
|
|
1293
1588
|
unitEnvironment("PATH", path),
|
|
1589
|
+
...spec.id === "broker" ? [unitEnvironmentFile(join6(home, "broker.env"))] : [],
|
|
1294
1590
|
"Restart=always",
|
|
1295
|
-
"RestartSec=2",
|
|
1296
|
-
...spec.
|
|
1591
|
+
spec.id === "broker" ? "RestartSec=2s" : "RestartSec=2",
|
|
1592
|
+
...spec.id === "broker" ? ["KillMode=control-group", "TimeoutStopSec=30s", "SuccessExitStatus=143"] : spec.id === "gateway" ? ["KillMode=mixed", "TimeoutStopSec=30s"] : [],
|
|
1297
1593
|
"",
|
|
1298
1594
|
"[Install]",
|
|
1299
1595
|
`WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
|
|
@@ -1303,18 +1599,21 @@ function renderSystemdUnit(spec, binDir, home, path) {
|
|
|
1303
1599
|
`);
|
|
1304
1600
|
}
|
|
1305
1601
|
function systemdUnitDir(env, userHome) {
|
|
1306
|
-
const base = env.XDG_CONFIG_HOME ||
|
|
1307
|
-
return
|
|
1602
|
+
const base = env.XDG_CONFIG_HOME || join6(userHome, ".config");
|
|
1603
|
+
return join6(base, "systemd", "user");
|
|
1308
1604
|
}
|
|
1309
1605
|
function launchAgentPlistPath(spec, launchAgentsDir) {
|
|
1310
|
-
return
|
|
1606
|
+
return join6(launchAgentsDir, `dev.gajaeway.${spec.id}.plist`);
|
|
1311
1607
|
}
|
|
1312
1608
|
function systemdUnitPath(spec, unitDir) {
|
|
1313
|
-
return
|
|
1609
|
+
return join6(unitDir, systemdUnitName(spec));
|
|
1314
1610
|
}
|
|
1315
1611
|
function restartStackCommands(platform, uid) {
|
|
1316
1612
|
if (platform === "linux")
|
|
1317
|
-
return [
|
|
1613
|
+
return [
|
|
1614
|
+
["systemctl", "--user", "restart", BROKER_UNIT],
|
|
1615
|
+
["systemctl", "--user", "restart", GATEWAY_UNIT]
|
|
1616
|
+
];
|
|
1318
1617
|
if (platform === "darwin") {
|
|
1319
1618
|
const target = uid ?? process.getuid?.() ?? 0;
|
|
1320
1619
|
return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
|
|
@@ -1338,10 +1637,10 @@ function currentPlatform() {
|
|
|
1338
1637
|
throw new Error(`unsupported service platform: ${process.platform}`);
|
|
1339
1638
|
}
|
|
1340
1639
|
async function readRuntime(home) {
|
|
1341
|
-
const configPath =
|
|
1640
|
+
const configPath = join6(home, "config.json");
|
|
1342
1641
|
let raw;
|
|
1343
1642
|
try {
|
|
1344
|
-
raw = await
|
|
1643
|
+
raw = await readFile4(configPath, "utf8");
|
|
1345
1644
|
} catch (error) {
|
|
1346
1645
|
throw new Error(`could not read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1347
1646
|
}
|
|
@@ -1371,7 +1670,7 @@ async function installServices(options) {
|
|
|
1371
1670
|
const userHome = env.HOME || homedir2();
|
|
1372
1671
|
const home = homeForEnvironment(env);
|
|
1373
1672
|
const binDir = expandHome2(options.binDir, userHome);
|
|
1374
|
-
const targetDir = platform === "darwin" ? expandHome2(options.launchAgentsDir ??
|
|
1673
|
+
const targetDir = platform === "darwin" ? expandHome2(options.launchAgentsDir ?? join6(userHome, "Library", "LaunchAgents"), userHome) : expandHome2(options.unitDir ?? systemdUnitDir(env, userHome), userHome);
|
|
1375
1674
|
const runtime = await readRuntime(home);
|
|
1376
1675
|
const path = await resolvePath({
|
|
1377
1676
|
binDir,
|
|
@@ -1380,18 +1679,36 @@ async function installServices(options) {
|
|
|
1380
1679
|
env,
|
|
1381
1680
|
loginPathRunner: options.loginPathRunner
|
|
1382
1681
|
});
|
|
1383
|
-
await
|
|
1384
|
-
const
|
|
1385
|
-
|
|
1682
|
+
await mkdir3(targetDir, { recursive: true, mode: 448 });
|
|
1683
|
+
const specs = [];
|
|
1684
|
+
for (const spec of SERVICE_SPECS) {
|
|
1685
|
+
if (options.configuredAdaptersOnly && spec.id.startsWith("adapter-")) {
|
|
1686
|
+
const adapter = spec.id.slice("adapter-".length);
|
|
1687
|
+
if (!await Bun.file(join6(home, `adapter-${adapter}.json`)).exists())
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
specs.push(spec);
|
|
1691
|
+
}
|
|
1692
|
+
const definitions = specs.map((spec) => platform === "darwin" ? {
|
|
1693
|
+
path: join6(targetDir, `dev.gajaeway.${spec.id}.plist`),
|
|
1386
1694
|
contents: renderLaunchAgent(spec, binDir, home, path)
|
|
1387
|
-
} : { path:
|
|
1695
|
+
} : { path: join6(targetDir, systemdUnitName(spec)), contents: renderSystemdUnit(spec, binDir, home, path) });
|
|
1388
1696
|
const write = options.writeFile;
|
|
1389
1697
|
for (const definition of definitions) {
|
|
1698
|
+
if (options.compareAndWrite) {
|
|
1699
|
+
try {
|
|
1700
|
+
if (await readFile4(definition.path, "utf8") === definition.contents)
|
|
1701
|
+
continue;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
if (error.code !== "ENOENT")
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1390
1707
|
if (write)
|
|
1391
1708
|
await write(definition.path, definition.contents);
|
|
1392
1709
|
else {
|
|
1393
1710
|
await writeFile2(definition.path, definition.contents, { encoding: "utf8", mode: 384 });
|
|
1394
|
-
await
|
|
1711
|
+
await chmod2(definition.path, 384);
|
|
1395
1712
|
}
|
|
1396
1713
|
}
|
|
1397
1714
|
return definitions.map((definition) => definition.path);
|
|
@@ -1405,12 +1722,12 @@ var RECEIPT_FILE = "restart-stack.json";
|
|
|
1405
1722
|
var VERIFY_TIMEOUT_MS = 90000;
|
|
1406
1723
|
var POLL_MS = 1000;
|
|
1407
1724
|
function restartReceiptPath(home) {
|
|
1408
|
-
return
|
|
1725
|
+
return join7(home, RECEIPT_FILE);
|
|
1409
1726
|
}
|
|
1410
1727
|
async function readRestartReceipt(home) {
|
|
1411
1728
|
let raw;
|
|
1412
1729
|
try {
|
|
1413
|
-
raw = await
|
|
1730
|
+
raw = await readFile5(restartReceiptPath(home), "utf8");
|
|
1414
1731
|
} catch (error) {
|
|
1415
1732
|
if (error.code === "ENOENT")
|
|
1416
1733
|
return;
|
|
@@ -1419,23 +1736,24 @@ async function readRestartReceipt(home) {
|
|
|
1419
1736
|
return JSON.parse(raw);
|
|
1420
1737
|
}
|
|
1421
1738
|
async function writeReceipt(home, receipt) {
|
|
1422
|
-
await
|
|
1739
|
+
await mkdir4(home, { recursive: true });
|
|
1423
1740
|
const path = restartReceiptPath(home);
|
|
1424
1741
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
1425
1742
|
await writeFile3(temporary, `${JSON.stringify(receipt, null, 2)}
|
|
1426
1743
|
`, { mode: 384 });
|
|
1427
|
-
await
|
|
1744
|
+
await rename2(temporary, path);
|
|
1428
1745
|
}
|
|
1429
|
-
function newReceipt(id, platform, uid, now) {
|
|
1746
|
+
function newReceipt(id, platform, uid, now, trigger) {
|
|
1430
1747
|
const commands = restartStackCommands(platform, uid);
|
|
1431
1748
|
return {
|
|
1432
1749
|
id,
|
|
1750
|
+
trigger,
|
|
1433
1751
|
state: "queued",
|
|
1434
1752
|
platform,
|
|
1435
1753
|
requestedAt: new Date(now).toISOString(),
|
|
1436
1754
|
steps: restartOrder().map((spec, index) => ({
|
|
1437
1755
|
label: spec.label,
|
|
1438
|
-
...platform === "darwin" ? { command: commands[index] } :
|
|
1756
|
+
...platform === "darwin" ? { command: commands[index] } : spec.id === "broker" ? { command: commands[0] } : spec.id === "gateway" ? { command: commands[1] } : {},
|
|
1439
1757
|
result: "pending"
|
|
1440
1758
|
}))
|
|
1441
1759
|
};
|
|
@@ -1454,33 +1772,17 @@ async function capture(command) {
|
|
|
1454
1772
|
const output = await new Response(child.stdout).text();
|
|
1455
1773
|
return await child.exited === 0 ? output : undefined;
|
|
1456
1774
|
}
|
|
1457
|
-
function defaultServiceProbe(platform) {
|
|
1775
|
+
function defaultServiceProbe(platform, uid = process.getuid?.() ?? 0) {
|
|
1458
1776
|
return async (label) => {
|
|
1459
1777
|
const spec = serviceSpecs().find((candidate) => candidate.label === label);
|
|
1460
1778
|
if (spec === undefined)
|
|
1461
1779
|
throw new Error(`unknown service label: ${label}`);
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
pid = match ? Number(match[1]) : undefined;
|
|
1467
|
-
} else {
|
|
1468
|
-
const shown = await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
|
|
1469
|
-
pid = shown === undefined ? undefined : Number(shown.trim());
|
|
1470
|
-
}
|
|
1471
|
-
if (pid === undefined || !Number.isInteger(pid) || pid <= 0)
|
|
1472
|
-
return;
|
|
1473
|
-
const lstart = await capture(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
1474
|
-
const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
|
|
1475
|
-
if (lstart === undefined || args === undefined)
|
|
1780
|
+
const shown = platform === "darwin" ? await capture(["launchctl", "print", `gui/${uid}/${label}`]) : await capture(["systemctl", "--user", "show", "-p", "MainPID", "--value", systemdUnitName(spec)]);
|
|
1781
|
+
const rawPid = platform === "darwin" ? shown?.match(/^\s*pid = (\d+);?\s*$/m)?.[1] : shown?.trim();
|
|
1782
|
+
const pid = rawPid === undefined ? undefined : Number(rawPid);
|
|
1783
|
+
if (pid === undefined || !Number.isSafeInteger(pid) || pid <= 0)
|
|
1476
1784
|
return;
|
|
1477
|
-
|
|
1478
|
-
if (!Number.isFinite(startedAt))
|
|
1479
|
-
return;
|
|
1480
|
-
const suffix = spec.args.length > 0 ? ` ${spec.args.join(" ")}` : "";
|
|
1481
|
-
const line = args.trim();
|
|
1482
|
-
const binary = suffix && line.endsWith(suffix) ? line.slice(0, -suffix.length) : line;
|
|
1483
|
-
return { pid, startedAt, binary };
|
|
1785
|
+
return { pid };
|
|
1484
1786
|
};
|
|
1485
1787
|
}
|
|
1486
1788
|
async function defaultBinaryModifiedAt(path) {
|
|
@@ -1493,7 +1795,7 @@ function defaultIsServiceInstalled(options) {
|
|
|
1493
1795
|
return false;
|
|
1494
1796
|
if (options.platform === "darwin") {
|
|
1495
1797
|
const userHome2 = options.env.HOME || homedir3();
|
|
1496
|
-
const agentsDir = options.launchAgentsDir ||
|
|
1798
|
+
const agentsDir = options.launchAgentsDir || join7(userHome2, "Library", "LaunchAgents");
|
|
1497
1799
|
const plistPath = launchAgentPlistPath(spec, agentsDir);
|
|
1498
1800
|
try {
|
|
1499
1801
|
await access(plistPath);
|
|
@@ -1504,23 +1806,61 @@ function defaultIsServiceInstalled(options) {
|
|
|
1504
1806
|
}
|
|
1505
1807
|
const userHome = options.env.HOME || homedir3();
|
|
1506
1808
|
const unitDirPath = options.unitDir || systemdUnitDir(options.env, userHome);
|
|
1507
|
-
const
|
|
1809
|
+
const unitPath2 = systemdUnitPath(spec, unitDirPath);
|
|
1508
1810
|
try {
|
|
1509
|
-
await access(
|
|
1811
|
+
await access(unitPath2);
|
|
1510
1812
|
return true;
|
|
1511
1813
|
} catch {
|
|
1512
1814
|
return false;
|
|
1513
1815
|
}
|
|
1514
1816
|
};
|
|
1515
1817
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1818
|
+
var SERVICE_LEASES = {
|
|
1819
|
+
broker: "broker",
|
|
1820
|
+
gateway: "gateway",
|
|
1821
|
+
"adapter-discord": "adapter-discord",
|
|
1822
|
+
"adapter-slack": "adapter-slack",
|
|
1823
|
+
"adapter-telegram": "adapter-telegram"
|
|
1824
|
+
};
|
|
1825
|
+
function serviceStopCommand(platform, uid) {
|
|
1826
|
+
if (platform === "linux")
|
|
1827
|
+
return ["systemctl", "--user", "stop", BROKER_UNIT];
|
|
1828
|
+
const target = uid ?? process.getuid?.() ?? 0;
|
|
1829
|
+
return ["launchctl", "bootout", `gui/${target}/dev.gajaeway.broker`];
|
|
1830
|
+
}
|
|
1831
|
+
function serviceBootstrapCommand(options, platform, label) {
|
|
1832
|
+
if (platform === "linux")
|
|
1833
|
+
return ["systemctl", "--user", "start", BROKER_UNIT];
|
|
1834
|
+
const target = options.uid ?? process.getuid?.() ?? 0;
|
|
1835
|
+
const userHome = options.env?.HOME || homedir3();
|
|
1836
|
+
const agentsDir = options.launchAgentsDir ?? join7(userHome, "Library", "LaunchAgents");
|
|
1837
|
+
const spec = serviceSpecs().find((candidate) => candidate.label === label);
|
|
1838
|
+
if (spec === undefined)
|
|
1839
|
+
throw new Error(`unknown service label: ${label}`);
|
|
1840
|
+
return ["launchctl", "bootstrap", `gui/${target}`, launchAgentPlistPath(spec, agentsDir)];
|
|
1518
1841
|
}
|
|
1519
|
-
var PS_START_SKEW_MS = 1000;
|
|
1520
1842
|
async function runRestartStack(options) {
|
|
1843
|
+
let lease;
|
|
1844
|
+
try {
|
|
1845
|
+
lease = await acquireLease(options.home, "update");
|
|
1846
|
+
console.info(`lease_acquired name=update pid=${process.pid}`);
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
if (error instanceof LeaseHeldError)
|
|
1849
|
+
console.error(error.message);
|
|
1850
|
+
else
|
|
1851
|
+
console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
try {
|
|
1855
|
+
return await runRestartStackWithLease(options);
|
|
1856
|
+
} finally {
|
|
1857
|
+
await lease.release();
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async function runRestartStackWithLease(options) {
|
|
1521
1861
|
const platform = options.platform ?? currentPlatform();
|
|
1522
1862
|
const runner = options.runner ?? defaultCommandRunner;
|
|
1523
|
-
const probe = options.probe ?? defaultServiceProbe(platform);
|
|
1863
|
+
const probe = options.probe ?? defaultServiceProbe(platform, options.uid);
|
|
1524
1864
|
const modifiedAt = options.binaryModifiedAt ?? defaultBinaryModifiedAt;
|
|
1525
1865
|
const isInstalled = options.isServiceInstalled ?? defaultIsServiceInstalled({
|
|
1526
1866
|
platform,
|
|
@@ -1532,8 +1872,16 @@ async function runRestartStack(options) {
|
|
|
1532
1872
|
const sleep = options.sleep ?? ((ms) => Bun.sleep(ms));
|
|
1533
1873
|
const timeoutMs = options.verifyTimeoutMs ?? VERIFY_TIMEOUT_MS;
|
|
1534
1874
|
const pollMs = options.pollMs ?? POLL_MS;
|
|
1875
|
+
const binDir = options.binDir ?? process.env.GAJAEWAY_BIN_DIR ?? dirname(process.execPath);
|
|
1876
|
+
const originalPids = new Map;
|
|
1877
|
+
for (const spec of restartOrder()) {
|
|
1878
|
+
if (SERVICE_LEASES[spec.id] === undefined && await isInstalled(spec.label))
|
|
1879
|
+
originalPids.set(spec.label, (await probe(spec.label))?.pid);
|
|
1880
|
+
}
|
|
1535
1881
|
const existing = await readRestartReceipt(options.home);
|
|
1536
|
-
|
|
1882
|
+
if (existing?.id === options.id && existing.state !== "queued")
|
|
1883
|
+
throw new Error(`restart-stack ${options.id}: receipt is ${existing.state}; refusing to start worker`);
|
|
1884
|
+
const receipt = existing?.id === options.id ? existing : newReceipt(options.id, platform, options.uid, now(), options.trigger ?? "manual");
|
|
1537
1885
|
receipt.state = "running";
|
|
1538
1886
|
receipt.supervisorPid = process.pid;
|
|
1539
1887
|
await writeReceipt(options.home, receipt);
|
|
@@ -1544,6 +1892,12 @@ async function runRestartStack(options) {
|
|
|
1544
1892
|
continue;
|
|
1545
1893
|
}
|
|
1546
1894
|
try {
|
|
1895
|
+
if (step.label === "dev.gajaeway.broker" && !await Bun.file(homeLayout(options.home).brokerProvisionManifest).exists()) {
|
|
1896
|
+
step.result = "skipped";
|
|
1897
|
+
step.detail = "broker agent is not provisioned";
|
|
1898
|
+
await writeReceipt(options.home, receipt);
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1547
1901
|
const installed = await isInstalled(step.label);
|
|
1548
1902
|
if (!installed) {
|
|
1549
1903
|
step.result = "skipped";
|
|
@@ -1551,17 +1905,68 @@ async function runRestartStack(options) {
|
|
|
1551
1905
|
await writeReceipt(options.home, receipt);
|
|
1552
1906
|
continue;
|
|
1553
1907
|
}
|
|
1554
|
-
if (step.
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
step.
|
|
1558
|
-
|
|
1908
|
+
if (step.label === "dev.gajaeway.broker" && receipt.trigger === "update") {
|
|
1909
|
+
const stopCommand = serviceStopCommand(platform, options.uid);
|
|
1910
|
+
const stopStatus = await runner(stopCommand);
|
|
1911
|
+
if (stopStatus !== 0 && !(platform === "darwin" && await probe(step.label) === undefined))
|
|
1912
|
+
throw new Error(`${stopCommand.join(" ")} exited with status ${stopStatus}`);
|
|
1913
|
+
const provisionCommand = [
|
|
1914
|
+
join7(binDir, "gajaeway-gateway"),
|
|
1915
|
+
"broker-agent",
|
|
1916
|
+
"provision",
|
|
1917
|
+
"--mode",
|
|
1918
|
+
"update",
|
|
1919
|
+
"--lease-wait-ms",
|
|
1920
|
+
"35000"
|
|
1921
|
+
];
|
|
1922
|
+
const provisionStatus = await runner(provisionCommand);
|
|
1923
|
+
step.agentSync = {
|
|
1924
|
+
command: provisionCommand,
|
|
1925
|
+
exitStatus: provisionStatus,
|
|
1926
|
+
...provisionStatus === 1 || provisionStatus === 3 ? { error: `broker-agent provision exited with status ${provisionStatus}` } : provisionStatus === 4 ? { error: "broker_agent_unrecoverable" } : {}
|
|
1927
|
+
};
|
|
1928
|
+
if (provisionStatus === 4) {
|
|
1929
|
+
delete step.command;
|
|
1930
|
+
step.exitStatus = 4;
|
|
1559
1931
|
step.result = "failed";
|
|
1560
|
-
step.detail =
|
|
1932
|
+
step.detail = "broker left stopped: broker_agent_unrecoverable; run gajaeway setup --broker-agent";
|
|
1933
|
+
} else if (provisionStatus === 0 || provisionStatus === 1 || provisionStatus === 3) {
|
|
1934
|
+
const startCommand = serviceBootstrapCommand(options, platform, step.label);
|
|
1935
|
+
step.command = startCommand;
|
|
1936
|
+
issuedAt = now();
|
|
1937
|
+
const startStatus = await runner(startCommand);
|
|
1938
|
+
step.exitStatus = startStatus;
|
|
1939
|
+
if (startStatus !== 0) {
|
|
1940
|
+
step.result = "failed";
|
|
1941
|
+
step.detail = `${startCommand.join(" ")} exited with status ${startStatus}`;
|
|
1942
|
+
} else
|
|
1943
|
+
await verifyStep(step, issuedAt);
|
|
1944
|
+
} else {
|
|
1945
|
+
delete step.command;
|
|
1946
|
+
step.exitStatus = provisionStatus;
|
|
1947
|
+
step.result = "failed";
|
|
1948
|
+
step.detail = `broker-agent provision exited with unexpected status ${provisionStatus}`;
|
|
1561
1949
|
}
|
|
1950
|
+
} else {
|
|
1951
|
+
if (step.command !== undefined) {
|
|
1952
|
+
issuedAt = now();
|
|
1953
|
+
let command = step.command;
|
|
1954
|
+
let status = await runner(command);
|
|
1955
|
+
if (status !== 0 && receipt.trigger === "update" && platform === "darwin" && await probe(step.label) === undefined) {
|
|
1956
|
+
command = serviceBootstrapCommand(options, platform, step.label);
|
|
1957
|
+
step.command = command;
|
|
1958
|
+
issuedAt = now();
|
|
1959
|
+
status = await runner(command);
|
|
1960
|
+
}
|
|
1961
|
+
step.exitStatus = status;
|
|
1962
|
+
if (status !== 0) {
|
|
1963
|
+
step.result = "failed";
|
|
1964
|
+
step.detail = `${command.join(" ")} exited with status ${status}`;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (step.result === "pending")
|
|
1968
|
+
await verifyStep(step, issuedAt);
|
|
1562
1969
|
}
|
|
1563
|
-
if (step.result === "pending")
|
|
1564
|
-
await verifyStep(step, issuedAt);
|
|
1565
1970
|
} catch (error) {
|
|
1566
1971
|
step.result = "failed";
|
|
1567
1972
|
step.detail = error instanceof Error ? error.message : String(error);
|
|
@@ -1577,29 +1982,53 @@ async function runRestartStack(options) {
|
|
|
1577
1982
|
await writeReceipt(options.home, receipt);
|
|
1578
1983
|
return receipt;
|
|
1579
1984
|
async function verifyStep(step, since) {
|
|
1985
|
+
const spec = serviceSpecs().find((candidate) => candidate.label === step.label);
|
|
1986
|
+
if (spec === undefined)
|
|
1987
|
+
throw new Error(`unknown service label: ${step.label}`);
|
|
1988
|
+
const leaseName = SERVICE_LEASES[spec.id];
|
|
1989
|
+
const binary = join7(binDir, spec.binary);
|
|
1990
|
+
const binaryModifiedAt = leaseName === undefined ? undefined : await modifiedAt(binary);
|
|
1580
1991
|
const deadline = now() + timeoutMs;
|
|
1581
1992
|
for (;; ) {
|
|
1582
1993
|
const running = await probe(step.label);
|
|
1583
|
-
|
|
1584
|
-
if (running !== undefined)
|
|
1585
|
-
binaryModifiedAt = await modifiedAt(running.binary);
|
|
1994
|
+
const owner = leaseName === undefined ? undefined : await readLeaseOwner(options.home, leaseName);
|
|
1995
|
+
if (running !== undefined)
|
|
1586
1996
|
step.pid = running.pid;
|
|
1587
|
-
|
|
1588
|
-
step.binary =
|
|
1997
|
+
if (leaseName !== undefined) {
|
|
1998
|
+
step.binary = binary;
|
|
1589
1999
|
step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
|
|
1590
|
-
if (
|
|
2000
|
+
if (owner !== undefined)
|
|
2001
|
+
step.processStartedAt = owner.startedAt;
|
|
2002
|
+
const startedAt = owner === undefined ? Number.NaN : Date.parse(owner.startedAt);
|
|
2003
|
+
if (running !== undefined && owner !== undefined && running.pid === owner.pid && Number.isFinite(startedAt) && startedAt >= Math.max(since, binaryModifiedAt)) {
|
|
1591
2004
|
step.result = "ok";
|
|
1592
2005
|
delete step.detail;
|
|
1593
2006
|
return;
|
|
1594
2007
|
}
|
|
2008
|
+
} else if (running !== undefined && running.pid !== originalPids.get(step.label)) {
|
|
2009
|
+
step.result = "ok";
|
|
2010
|
+
delete step.detail;
|
|
2011
|
+
return;
|
|
1595
2012
|
}
|
|
1596
2013
|
if (now() >= deadline) {
|
|
1597
2014
|
if (running === undefined) {
|
|
1598
2015
|
step.result = "failed";
|
|
1599
2016
|
step.detail = "no running process after restart";
|
|
2017
|
+
} else if (leaseName === undefined) {
|
|
2018
|
+
step.result = "stale";
|
|
2019
|
+
step.detail = "MainPID did not change after restart";
|
|
2020
|
+
} else if (owner === undefined) {
|
|
2021
|
+
step.result = "stale";
|
|
2022
|
+
step.detail = "no lease owner after restart";
|
|
2023
|
+
} else if (running.pid !== owner.pid) {
|
|
2024
|
+
step.result = "stale";
|
|
2025
|
+
step.detail = "lease owner PID does not match service MainPID";
|
|
2026
|
+
} else if (Date.parse(owner.startedAt) < binaryModifiedAt) {
|
|
2027
|
+
step.result = "stale";
|
|
2028
|
+
step.detail = "process started before the deployed binary was modified";
|
|
1600
2029
|
} else {
|
|
1601
2030
|
step.result = "stale";
|
|
1602
|
-
step.detail =
|
|
2031
|
+
step.detail = "lease owner predates this restart sequence";
|
|
1603
2032
|
}
|
|
1604
2033
|
return;
|
|
1605
2034
|
}
|
|
@@ -1607,38 +2036,80 @@ async function runRestartStack(options) {
|
|
|
1607
2036
|
}
|
|
1608
2037
|
}
|
|
1609
2038
|
}
|
|
2039
|
+
var SYSTEMD_RUN_TIMEOUT_MS = 30000;
|
|
1610
2040
|
function cliCommand() {
|
|
1611
2041
|
return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
|
|
1612
2042
|
}
|
|
1613
|
-
function alive(pid) {
|
|
1614
|
-
if (pid === undefined)
|
|
1615
|
-
return false;
|
|
1616
|
-
try {
|
|
1617
|
-
process.kill(pid, 0);
|
|
1618
|
-
return true;
|
|
1619
|
-
} catch {
|
|
1620
|
-
return false;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
2043
|
async function launchRestartStack(options) {
|
|
1624
2044
|
const platform = options.platform ?? currentPlatform();
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
|
|
2045
|
+
if (await leaseHeld(options.home, "update"))
|
|
2046
|
+
throw new LeaseHeldError("update", await readLeaseOwner(options.home, "update"));
|
|
2047
|
+
const receipt = newReceipt(randomUUID2(), platform, options.uid, Date.now(), options.trigger ?? "manual");
|
|
1629
2048
|
await writeReceipt(options.home, receipt);
|
|
1630
2049
|
const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
|
|
1631
|
-
const
|
|
2050
|
+
const argv = supervisorArgv(platform, worker, options.home, receipt.id, options.binDir);
|
|
2051
|
+
const spawnOptions = {
|
|
1632
2052
|
detached: true,
|
|
1633
2053
|
stdin: "ignore",
|
|
1634
2054
|
stdout: "ignore",
|
|
1635
2055
|
stderr: "ignore",
|
|
1636
|
-
env: {
|
|
1637
|
-
|
|
2056
|
+
env: {
|
|
2057
|
+
...options.env ?? process.env,
|
|
2058
|
+
GAJAEWAY_HOME: options.home,
|
|
2059
|
+
...options.binDir === undefined ? {} : { GAJAEWAY_BIN_DIR: options.binDir }
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
let child;
|
|
2063
|
+
try {
|
|
2064
|
+
child = platform === "linux" ? (options.systemdRun ?? ((command) => Bun.spawn([...command], spawnOptions)))(argv) : Bun.spawn(argv, spawnOptions);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
const detail = platform === "linux" ? `supervisor systemd-run could not be started (${error instanceof Error ? error.message : String(error)}); without a service manager here, restart through the host's own supervisor` : `supervisor ${argv[0]} could not be started (${error instanceof Error ? error.message : String(error)})`;
|
|
2067
|
+
await failLaunch(options.home, receipt, detail);
|
|
2068
|
+
throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
|
|
2069
|
+
}
|
|
2070
|
+
if (platform === "linux") {
|
|
2071
|
+
const outcome = await waitForLauncher(child, options.systemdRunTimeoutMs ?? SYSTEMD_RUN_TIMEOUT_MS);
|
|
2072
|
+
if (outcome.kind === "timed_out") {
|
|
2073
|
+
const detail = `supervisor systemd-run timed out after ${options.systemdRunTimeoutMs ?? SYSTEMD_RUN_TIMEOUT_MS}ms; handoff outcome is indeterminate`;
|
|
2074
|
+
await failLaunch(options.home, receipt, detail);
|
|
2075
|
+
throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
|
|
2076
|
+
}
|
|
2077
|
+
if (outcome.exitStatus !== 0) {
|
|
2078
|
+
const detail = `supervisor systemd-run exited with status ${outcome.exitStatus}`;
|
|
2079
|
+
await failLaunch(options.home, receipt, detail);
|
|
2080
|
+
throw new Error(`restart-stack ${receipt.id}: failed: ${detail}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
1638
2083
|
child.unref();
|
|
1639
2084
|
return { receipt, supervisorPid: child.pid };
|
|
1640
2085
|
}
|
|
1641
|
-
function
|
|
2086
|
+
async function failLaunch(home, receipt, detail) {
|
|
2087
|
+
receipt.state = "failed";
|
|
2088
|
+
receipt.finishedAt = new Date().toISOString();
|
|
2089
|
+
receipt.detail = detail;
|
|
2090
|
+
for (const step of receipt.steps)
|
|
2091
|
+
step.result = "skipped";
|
|
2092
|
+
await writeReceipt(home, receipt);
|
|
2093
|
+
}
|
|
2094
|
+
async function waitForLauncher(child, timeoutMs) {
|
|
2095
|
+
let timer;
|
|
2096
|
+
const outcome = await Promise.race([
|
|
2097
|
+
child.exited.then((exitStatus) => ({ kind: "exited", exitStatus })),
|
|
2098
|
+
new Promise((resolve) => {
|
|
2099
|
+
timer = setTimeout(() => resolve({ kind: "timed_out" }), timeoutMs);
|
|
2100
|
+
})
|
|
2101
|
+
]);
|
|
2102
|
+
if (timer !== undefined)
|
|
2103
|
+
clearTimeout(timer);
|
|
2104
|
+
if (outcome.kind === "timed_out") {
|
|
2105
|
+
child.kill("SIGKILL");
|
|
2106
|
+
child.exited.catch(() => {
|
|
2107
|
+
return;
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
return outcome;
|
|
2111
|
+
}
|
|
2112
|
+
function supervisorArgv(platform, worker, home, id, binDir) {
|
|
1642
2113
|
if (platform === "darwin")
|
|
1643
2114
|
return [...worker];
|
|
1644
2115
|
return [
|
|
@@ -1646,28 +2117,32 @@ function supervisorArgv(platform, worker, home, id) {
|
|
|
1646
2117
|
"--user",
|
|
1647
2118
|
"--collect",
|
|
1648
2119
|
"--quiet",
|
|
2120
|
+
"--service-type=exec",
|
|
1649
2121
|
`--unit=gajaeway-restart-stack-${id}`,
|
|
1650
2122
|
`--setenv=GAJAEWAY_HOME=${home}`,
|
|
2123
|
+
...binDir === undefined ? [] : [`--setenv=GAJAEWAY_BIN_DIR=${binDir}`],
|
|
1651
2124
|
"--",
|
|
1652
2125
|
...worker
|
|
1653
2126
|
];
|
|
1654
2127
|
}
|
|
1655
|
-
function effectiveRestartState(receipt) {
|
|
1656
|
-
if (receipt.state === "running" && !
|
|
2128
|
+
async function effectiveRestartState(receipt, home) {
|
|
2129
|
+
if (receipt.state === "running" && !await leaseHeld(home, "update"))
|
|
1657
2130
|
return "interrupted";
|
|
1658
2131
|
return receipt.state;
|
|
1659
2132
|
}
|
|
1660
|
-
function renderRestartReceipt(receipt) {
|
|
2133
|
+
function renderRestartReceipt(receipt, state = receipt.state) {
|
|
1661
2134
|
const lines = [
|
|
1662
|
-
`restart-stack ${receipt.id}: ${
|
|
2135
|
+
`restart-stack ${receipt.id}: ${state}`,
|
|
1663
2136
|
`requestedAt: ${receipt.requestedAt}`,
|
|
1664
|
-
`finishedAt: ${receipt.finishedAt ?? "-"}
|
|
2137
|
+
`finishedAt: ${receipt.finishedAt ?? "-"}`,
|
|
2138
|
+
...receipt.detail === undefined ? [] : [`detail: ${receipt.detail}`]
|
|
1665
2139
|
];
|
|
1666
2140
|
for (const step of receipt.steps) {
|
|
1667
2141
|
const facts = [
|
|
1668
2142
|
step.pid === undefined ? undefined : `pid=${step.pid}`,
|
|
1669
2143
|
step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
|
|
1670
2144
|
step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
|
|
2145
|
+
step.agentSync?.error === undefined ? undefined : `agentSync.error=${step.agentSync.error}`,
|
|
1671
2146
|
step.detail
|
|
1672
2147
|
].filter((fact) => fact !== undefined);
|
|
1673
2148
|
lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
|
|
@@ -1676,9 +2151,74 @@ function renderRestartReceipt(receipt) {
|
|
|
1676
2151
|
}
|
|
1677
2152
|
|
|
1678
2153
|
// src/setup.ts
|
|
1679
|
-
import { randomUUID as
|
|
1680
|
-
import { chmod as
|
|
1681
|
-
import { join as
|
|
2154
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2155
|
+
import { chmod as chmod3, mkdir as mkdir5, readFile as readFile6, realpath, rename as rename3, rm as rm2, writeFile as writeFile4 } from "fs/promises";
|
|
2156
|
+
import { basename, dirname as dirname2, join as join8 } from "path";
|
|
2157
|
+
|
|
2158
|
+
// src/discord-setup.ts
|
|
2159
|
+
var DEVELOPER_PORTAL_URL = "https://discord.com/developers/applications";
|
|
2160
|
+
var DISCORD_BOT_SCOPES = ["bot", "applications.commands"];
|
|
2161
|
+
var DISCORD_BOT_PERMISSIONS = 2048 | 16384 | 33554432 | 65536 | 131072 | 8192 | 64;
|
|
2162
|
+
function printDiscordSetupGuide(log) {
|
|
2163
|
+
log("");
|
|
2164
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2165
|
+
log(" Getting your Discord bot token");
|
|
2166
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2167
|
+
log("");
|
|
2168
|
+
log("1. Open the Discord Developer Portal:");
|
|
2169
|
+
log(` ${DEVELOPER_PORTAL_URL}`);
|
|
2170
|
+
log("");
|
|
2171
|
+
log("2. Click 'New Application' and choose a name (e.g., 'My gajae-way agent')");
|
|
2172
|
+
log("");
|
|
2173
|
+
log("3. In the left menu, click 'Bot' \u2192 'Add Bot'");
|
|
2174
|
+
log("");
|
|
2175
|
+
log("4. Under 'TOKEN', click 'Reset Token' \u2192 'Yes, do it!'");
|
|
2176
|
+
log("");
|
|
2177
|
+
log("5. Click 'Copy' to copy your bot token (it starts with 'MTI...')");
|
|
2178
|
+
log("");
|
|
2179
|
+
log("6. (Important) Scroll down to 'GATEWAY INTENTS' and enable 'MESSAGE CONTENT INTENT'");
|
|
2180
|
+
log(" This allows the bot to read message text, not just usernames.");
|
|
2181
|
+
log("");
|
|
2182
|
+
log("7. Click 'Save Changes'");
|
|
2183
|
+
log("");
|
|
2184
|
+
log("8. Come back here and paste your token below");
|
|
2185
|
+
log("");
|
|
2186
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2187
|
+
log("");
|
|
2188
|
+
}
|
|
2189
|
+
function generateDiscordInviteUrl(appId) {
|
|
2190
|
+
const scopeParam = DISCORD_BOT_SCOPES.join("+");
|
|
2191
|
+
const permissionsParam = String(DISCORD_BOT_PERMISSIONS);
|
|
2192
|
+
return `https://discord.com/oauth2/authorize?client_id=${appId}&scope=${scopeParam}&permissions=${permissionsParam}`;
|
|
2193
|
+
}
|
|
2194
|
+
function printDiscordBotReady(log, appId, botName) {
|
|
2195
|
+
const inviteUrl = generateDiscordInviteUrl(appId);
|
|
2196
|
+
log("");
|
|
2197
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2198
|
+
log(" \u2713 Discord bot token validated");
|
|
2199
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2200
|
+
log("");
|
|
2201
|
+
log(`Application ID: ${appId}`);
|
|
2202
|
+
if (botName) {
|
|
2203
|
+
log(`Bot name: ${botName}`);
|
|
2204
|
+
}
|
|
2205
|
+
log("");
|
|
2206
|
+
log("To invite your bot to your Discord server, click this link:");
|
|
2207
|
+
log("");
|
|
2208
|
+
log(` ${inviteUrl}`);
|
|
2209
|
+
log("");
|
|
2210
|
+
log("Then:");
|
|
2211
|
+
log(" 1. Choose the server where you want to add the bot");
|
|
2212
|
+
log(" 2. Click 'Authorize'");
|
|
2213
|
+
log(" 3. Complete the CAPTCHA");
|
|
2214
|
+
log("");
|
|
2215
|
+
log("Once authorized, send the bot a DM and start chatting!");
|
|
2216
|
+
log("");
|
|
2217
|
+
log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
2218
|
+
log("");
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// src/setup.ts
|
|
1682
2222
|
var SETUP_PLATFORMS = ["discord", "slack", "telegram"];
|
|
1683
2223
|
var SETUP_ENV = {
|
|
1684
2224
|
discordToken: "GAJAEWAY_DISCORD_TOKEN",
|
|
@@ -1688,7 +2228,7 @@ var SETUP_ENV = {
|
|
|
1688
2228
|
telegramToken: "GAJAEWAY_TELEGRAM_TOKEN",
|
|
1689
2229
|
ownerIds: "GAJAEWAY_OWNER_IDS"
|
|
1690
2230
|
};
|
|
1691
|
-
var SETUP_USAGE = "usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status";
|
|
2231
|
+
var SETUP_USAGE = "usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status | setup --broker-agent [--from DIR]";
|
|
1692
2232
|
var SECRET_FILES2 = {
|
|
1693
2233
|
discord: "discord-token",
|
|
1694
2234
|
slackBot: "slack-bot-token",
|
|
@@ -1713,6 +2253,8 @@ function redact(text, secrets) {
|
|
|
1713
2253
|
function parseSetupArgs(args) {
|
|
1714
2254
|
let fromEnv = false;
|
|
1715
2255
|
let status = false;
|
|
2256
|
+
let brokerAgent = false;
|
|
2257
|
+
let from;
|
|
1716
2258
|
let adapters;
|
|
1717
2259
|
const owners = [];
|
|
1718
2260
|
let discordAppId;
|
|
@@ -1722,7 +2264,15 @@ function parseSetupArgs(args) {
|
|
|
1722
2264
|
fromEnv = true;
|
|
1723
2265
|
else if (flag === "--status")
|
|
1724
2266
|
status = true;
|
|
1725
|
-
else if (flag === "--
|
|
2267
|
+
else if (flag === "--broker-agent")
|
|
2268
|
+
brokerAgent = true;
|
|
2269
|
+
else if (flag === "--from") {
|
|
2270
|
+
const value = args[++i];
|
|
2271
|
+
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
2272
|
+
throw new SetupError(`${flag} expects a value
|
|
2273
|
+
${SETUP_USAGE}`);
|
|
2274
|
+
from = value;
|
|
2275
|
+
} else if (flag === "--adapters" || flag === "--owner" || flag === "--discord-app-id") {
|
|
1726
2276
|
const value = args[++i];
|
|
1727
2277
|
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
1728
2278
|
throw new SetupError(`${flag} expects a value
|
|
@@ -1739,15 +2289,22 @@ ${SETUP_USAGE}`);
|
|
|
1739
2289
|
throw new SetupError(`unknown option: ${flag.split("=")[0]}
|
|
1740
2290
|
${SETUP_USAGE}`);
|
|
1741
2291
|
}
|
|
1742
|
-
if (status && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
2292
|
+
if (status && (fromEnv || brokerAgent || from !== undefined || adapters || owners.length > 0 || discordAppId))
|
|
1743
2293
|
throw new SetupError(`--status takes no other options
|
|
2294
|
+
${SETUP_USAGE}`);
|
|
2295
|
+
if (brokerAgent && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
2296
|
+
throw new SetupError(`--broker-agent is standalone and cannot be combined with adapter setup options
|
|
2297
|
+
${SETUP_USAGE}`);
|
|
2298
|
+
if (from !== undefined && !brokerAgent)
|
|
2299
|
+
throw new SetupError(`--from requires --broker-agent
|
|
1744
2300
|
${SETUP_USAGE}`);
|
|
1745
2301
|
return {
|
|
1746
|
-
mode: status ? "status" : "setup",
|
|
2302
|
+
mode: status ? "status" : brokerAgent ? "broker-agent" : "setup",
|
|
1747
2303
|
fromEnv,
|
|
1748
2304
|
...adapters ? { adapters } : {},
|
|
1749
2305
|
owners,
|
|
1750
|
-
...discordAppId ? { discordAppId } : {}
|
|
2306
|
+
...discordAppId ? { discordAppId } : {},
|
|
2307
|
+
...from === undefined ? {} : { from }
|
|
1751
2308
|
};
|
|
1752
2309
|
}
|
|
1753
2310
|
function splitList(value) {
|
|
@@ -1845,7 +2402,8 @@ async function validateDiscord(credentials, fetcher) {
|
|
|
1845
2402
|
const ownerId = !hasTeam && typeof owner?.id === "string" ? owner.id : undefined;
|
|
1846
2403
|
return {
|
|
1847
2404
|
platform: "discord",
|
|
1848
|
-
summary: `bot ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
2405
|
+
summary: `bot named ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
2406
|
+
appId,
|
|
1849
2407
|
...ownerId ? { ownerId } : {}
|
|
1850
2408
|
};
|
|
1851
2409
|
}
|
|
@@ -1899,19 +2457,19 @@ async function validateAll(tokens, fetcher) {
|
|
|
1899
2457
|
return identities;
|
|
1900
2458
|
}
|
|
1901
2459
|
async function writePrivateFile(path, contents) {
|
|
1902
|
-
const temporary = `${path}.${
|
|
2460
|
+
const temporary = `${path}.${randomUUID3()}.tmp`;
|
|
1903
2461
|
try {
|
|
1904
2462
|
await writeFile4(temporary, contents, { flag: "wx", mode: 384 });
|
|
1905
|
-
await
|
|
1906
|
-
await
|
|
2463
|
+
await chmod3(temporary, 384);
|
|
2464
|
+
await rename3(temporary, path);
|
|
1907
2465
|
} finally {
|
|
1908
|
-
await
|
|
2466
|
+
await rm2(temporary, { force: true });
|
|
1909
2467
|
}
|
|
1910
2468
|
}
|
|
1911
2469
|
async function readJsonObject(path) {
|
|
1912
2470
|
let text;
|
|
1913
2471
|
try {
|
|
1914
|
-
text = await
|
|
2472
|
+
text = await readFile6(path, "utf8");
|
|
1915
2473
|
} catch (error) {
|
|
1916
2474
|
if (error.code === "ENOENT")
|
|
1917
2475
|
return;
|
|
@@ -1929,21 +2487,21 @@ async function readJsonObject(path) {
|
|
|
1929
2487
|
}
|
|
1930
2488
|
async function applySetup(options) {
|
|
1931
2489
|
const { home, tokens } = options;
|
|
1932
|
-
const secretsDir =
|
|
1933
|
-
const configPath =
|
|
1934
|
-
const adapterPath = (platform) =>
|
|
2490
|
+
const secretsDir = join8(home, "secrets");
|
|
2491
|
+
const configPath = join8(home, "config.json");
|
|
2492
|
+
const adapterPath = (platform) => join8(home, `adapter-${platform}.json`);
|
|
1935
2493
|
const existingConfig = await readJsonObject(configPath);
|
|
1936
2494
|
const existingAdapters = new Map;
|
|
1937
2495
|
for (const platform of SETUP_PLATFORMS)
|
|
1938
2496
|
if (tokens[platform])
|
|
1939
2497
|
existingAdapters.set(platform, await readJsonObject(adapterPath(platform)));
|
|
1940
|
-
await
|
|
1941
|
-
await
|
|
1942
|
-
await
|
|
1943
|
-
await
|
|
2498
|
+
await mkdir5(home, { recursive: true, mode: 448 });
|
|
2499
|
+
await chmod3(home, 448);
|
|
2500
|
+
await mkdir5(secretsDir, { recursive: true, mode: 448 });
|
|
2501
|
+
await chmod3(secretsDir, 448);
|
|
1944
2502
|
const written = [];
|
|
1945
2503
|
const secret = async (name, value) => {
|
|
1946
|
-
const path =
|
|
2504
|
+
const path = join8(secretsDir, name);
|
|
1947
2505
|
await writePrivateFile(path, `${value}
|
|
1948
2506
|
`);
|
|
1949
2507
|
written.push(path);
|
|
@@ -1990,7 +2548,7 @@ async function applySetup(options) {
|
|
|
1990
2548
|
async function enabledAdapters(home) {
|
|
1991
2549
|
const enabled = [];
|
|
1992
2550
|
for (const platform of SETUP_PLATFORMS) {
|
|
1993
|
-
if (await Bun.file(
|
|
2551
|
+
if (await Bun.file(join8(home, `adapter-${platform}.json`)).exists())
|
|
1994
2552
|
enabled.push(platform);
|
|
1995
2553
|
}
|
|
1996
2554
|
return enabled;
|
|
@@ -2011,12 +2569,14 @@ async function askRequiredSecret(prompter, question) {
|
|
|
2011
2569
|
return answer;
|
|
2012
2570
|
}
|
|
2013
2571
|
}
|
|
2014
|
-
async function tokensFromPrompts(prompter, parsed) {
|
|
2572
|
+
async function tokensFromPrompts(prompter, parsed, log) {
|
|
2015
2573
|
const wanted = parsed.adapters ?? [];
|
|
2016
2574
|
const enable = async (platform, label) => parsed.adapters ? wanted.includes(platform) : await askYes(prompter, `Enable ${label}?`);
|
|
2017
2575
|
const tokens = {};
|
|
2018
2576
|
if (await enable("discord", "Discord")) {
|
|
2019
|
-
const
|
|
2577
|
+
const logger = log ?? (() => {});
|
|
2578
|
+
printDiscordSetupGuide(logger);
|
|
2579
|
+
const token = await askRequiredSecret(prompter, "Paste your Discord bot token here (input hidden): ");
|
|
2020
2580
|
const appId = parsed.discordAppId ?? (await prompter.ask("Discord application id (optional): ")).trim();
|
|
2021
2581
|
tokens.discord = { token, ...appId ? { appId } : {} };
|
|
2022
2582
|
}
|
|
@@ -2085,6 +2645,28 @@ function terminalPrompter() {
|
|
|
2085
2645
|
close: () => stdin.pause()
|
|
2086
2646
|
};
|
|
2087
2647
|
}
|
|
2648
|
+
async function defaultBrokerAgentRunner(command) {
|
|
2649
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
2650
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2651
|
+
new Response(child.stdout).text(),
|
|
2652
|
+
new Response(child.stderr).text(),
|
|
2653
|
+
child.exited
|
|
2654
|
+
]);
|
|
2655
|
+
return { exitCode, stdout, stderr };
|
|
2656
|
+
}
|
|
2657
|
+
async function setupBinDir(options) {
|
|
2658
|
+
if (options.binDir !== undefined)
|
|
2659
|
+
return options.binDir;
|
|
2660
|
+
const env = options.env ?? process.env;
|
|
2661
|
+
if (env.GAJAEWAY_BIN_DIR)
|
|
2662
|
+
return env.GAJAEWAY_BIN_DIR;
|
|
2663
|
+
const executable = await realpath(process.execPath).catch(() => {
|
|
2664
|
+
return;
|
|
2665
|
+
});
|
|
2666
|
+
if (executable !== undefined && basename(executable).startsWith("gajaeway"))
|
|
2667
|
+
return dirname2(executable);
|
|
2668
|
+
throw new SetupError("setup --broker-agent requires the installed gajaeway binary or an explicit bin directory");
|
|
2669
|
+
}
|
|
2088
2670
|
async function runSetup(args, options) {
|
|
2089
2671
|
const log = options.log ?? ((line) => console.log(line));
|
|
2090
2672
|
const parsed = parseSetupArgs(args);
|
|
@@ -2092,6 +2674,26 @@ async function runSetup(args, options) {
|
|
|
2092
2674
|
log((await enabledAdapters(options.home)).join(","));
|
|
2093
2675
|
return;
|
|
2094
2676
|
}
|
|
2677
|
+
if (parsed.mode === "broker-agent") {
|
|
2678
|
+
const command = [
|
|
2679
|
+
join8(await setupBinDir(options), "gajaeway-gateway"),
|
|
2680
|
+
"broker-agent",
|
|
2681
|
+
"provision",
|
|
2682
|
+
"--mode",
|
|
2683
|
+
"force",
|
|
2684
|
+
...parsed.from === undefined ? [] : ["--from", parsed.from]
|
|
2685
|
+
];
|
|
2686
|
+
const result = await (options.runBrokerAgent ?? defaultBrokerAgentRunner)(command);
|
|
2687
|
+
if (result.stdout.length > 0)
|
|
2688
|
+
process.stdout.write(result.stdout);
|
|
2689
|
+
if (result.stderr.length > 0)
|
|
2690
|
+
process.stderr.write(result.stderr);
|
|
2691
|
+
if (result.exitCode === 1 && /lease_held name=broker/.test(`${result.stdout}
|
|
2692
|
+
${result.stderr}`))
|
|
2693
|
+
log("broker lease is held; stop it with: systemctl --user stop gajaeway-broker");
|
|
2694
|
+
process.exitCode = result.exitCode;
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2095
2697
|
let tokens;
|
|
2096
2698
|
let owners;
|
|
2097
2699
|
if (parsed.fromEnv) {
|
|
@@ -2100,7 +2702,7 @@ async function runSetup(args, options) {
|
|
|
2100
2702
|
} else {
|
|
2101
2703
|
const prompter = (options.prompter ?? terminalPrompter)();
|
|
2102
2704
|
try {
|
|
2103
|
-
({ tokens, owners } = await tokensFromPrompts(prompter, parsed));
|
|
2705
|
+
({ tokens, owners } = await tokensFromPrompts(prompter, parsed, log));
|
|
2104
2706
|
} finally {
|
|
2105
2707
|
prompter.close();
|
|
2106
2708
|
}
|
|
@@ -2110,6 +2712,11 @@ async function runSetup(args, options) {
|
|
|
2110
2712
|
const identities = await validateAll(tokens, options.fetch ?? ((input, init) => fetch(input, init)));
|
|
2111
2713
|
for (const identity of identities)
|
|
2112
2714
|
log(`${identity.platform}: token valid \u2014 ${identity.summary}`);
|
|
2715
|
+
const discordIdentity = identities.find((i) => i.platform === "discord");
|
|
2716
|
+
if (discordIdentity && tokens.discord && discordIdentity.appId) {
|
|
2717
|
+
const botName = (discordIdentity.summary.match(/named (\S+)/) ?? [])[1];
|
|
2718
|
+
printDiscordBotReady(log, discordIdentity.appId, botName);
|
|
2719
|
+
}
|
|
2113
2720
|
for (const identity of identities)
|
|
2114
2721
|
if (identity.ownerId && owners.length === 0) {
|
|
2115
2722
|
owners.push(identity.ownerId);
|
|
@@ -2129,77 +2736,152 @@ async function runSetup(args, options) {
|
|
|
2129
2736
|
|
|
2130
2737
|
// src/update.ts
|
|
2131
2738
|
import {
|
|
2132
|
-
chmod as
|
|
2739
|
+
chmod as chmod5,
|
|
2133
2740
|
copyFile,
|
|
2134
|
-
mkdir as
|
|
2741
|
+
mkdir as mkdir7,
|
|
2742
|
+
mkdtemp as mkdtemp2,
|
|
2743
|
+
readdir as readdir3,
|
|
2744
|
+
readFile as readFile7,
|
|
2745
|
+
realpath as realpath2,
|
|
2746
|
+
rename as rename5,
|
|
2747
|
+
rm as rm4,
|
|
2748
|
+
stat as stat2,
|
|
2749
|
+
writeFile as writeFile6
|
|
2750
|
+
} from "fs/promises";
|
|
2751
|
+
import { tmpdir } from "os";
|
|
2752
|
+
import { basename as basename2, dirname as dirname3, join as join10 } from "path";
|
|
2753
|
+
|
|
2754
|
+
// src/gjc-install.ts
|
|
2755
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2756
|
+
import {
|
|
2757
|
+
access as access2,
|
|
2758
|
+
chmod as chmod4,
|
|
2759
|
+
lstat as lstat2,
|
|
2760
|
+
mkdir as mkdir6,
|
|
2135
2761
|
mkdtemp,
|
|
2136
|
-
open,
|
|
2137
2762
|
readdir as readdir2,
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
stat as stat2,
|
|
2763
|
+
readlink as readlink2,
|
|
2764
|
+
rename as rename4,
|
|
2765
|
+
rm as rm3,
|
|
2766
|
+
symlink,
|
|
2143
2767
|
writeFile as writeFile5
|
|
2144
2768
|
} from "fs/promises";
|
|
2145
|
-
import {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2769
|
+
import { join as join9 } from "path";
|
|
2770
|
+
var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2771
|
+
async function defaultRunner(command, cwd) {
|
|
2772
|
+
const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
2773
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2774
|
+
new Response(child.stdout).text(),
|
|
2775
|
+
new Response(child.stderr).text(),
|
|
2776
|
+
child.exited
|
|
2777
|
+
]);
|
|
2778
|
+
return { exitCode, stdout, stderr };
|
|
2779
|
+
}
|
|
2780
|
+
function assertSuccessful(command, result) {
|
|
2781
|
+
if (result.exitCode !== 0) {
|
|
2782
|
+
const detail = result.stderr.trim();
|
|
2783
|
+
throw new Error(`${command.join(" ")} exited with status ${result.exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function versionFromOutput(output) {
|
|
2787
|
+
return output.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/)?.[0];
|
|
2788
|
+
}
|
|
2789
|
+
async function verifyGjc(path, cwd, version, runner) {
|
|
2790
|
+
await access2(path);
|
|
2791
|
+
const command = [path, "--version"];
|
|
2792
|
+
const result = await runner(command, cwd);
|
|
2793
|
+
assertSuccessful(command, result);
|
|
2794
|
+
const reported = versionFromOutput(`${result.stdout}
|
|
2795
|
+
${result.stderr}`);
|
|
2796
|
+
if (reported !== version)
|
|
2797
|
+
throw new Error(`gjc version verification failed: expected ${version}, got ${reported ?? "unknown"}`);
|
|
2798
|
+
}
|
|
2799
|
+
async function currentVersion(prefix, current) {
|
|
2800
|
+
try {
|
|
2801
|
+
const target = await readlink2(current);
|
|
2802
|
+
if (target.includes("/") || !VERSION_PATTERN.test(target))
|
|
2803
|
+
return;
|
|
2804
|
+
const targetPath = join9(prefix, target);
|
|
2805
|
+
if (!(await lstat2(targetPath)).isDirectory())
|
|
2806
|
+
return;
|
|
2807
|
+
return target;
|
|
2808
|
+
} catch {
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
async function pruneVersions(prefix, keep) {
|
|
2813
|
+
const retained = new Set(keep);
|
|
2814
|
+
const entries2 = await readdir2(prefix, { withFileTypes: true });
|
|
2815
|
+
const versions = entries2.filter((entry) => entry.isDirectory() && VERSION_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => Bun.semver.order(right, left));
|
|
2816
|
+
for (const version of versions) {
|
|
2817
|
+
if (retained.size >= 2)
|
|
2818
|
+
break;
|
|
2819
|
+
retained.add(version);
|
|
2820
|
+
}
|
|
2821
|
+
for (const version of versions)
|
|
2822
|
+
if (!retained.has(version))
|
|
2823
|
+
await rm3(join9(prefix, version), { recursive: true, force: true });
|
|
2824
|
+
}
|
|
2825
|
+
async function installPinnedGjc(options) {
|
|
2826
|
+
const { home, version } = options;
|
|
2827
|
+
if (!VERSION_PATTERN.test(version))
|
|
2828
|
+
throw new Error(`invalid pinned gjc version: ${version}`);
|
|
2829
|
+
const layout = homeLayout(home);
|
|
2830
|
+
const runner = options.runner ?? defaultRunner;
|
|
2831
|
+
await mkdir6(layout.gjcPrefix, { recursive: true, mode: 448 });
|
|
2832
|
+
await chmod4(layout.gjcPrefix, 448);
|
|
2833
|
+
const versionDir = join9(layout.gjcPrefix, version);
|
|
2834
|
+
const previousVersion = await currentVersion(layout.gjcPrefix, layout.gjcCurrent);
|
|
2835
|
+
let staging;
|
|
2836
|
+
try {
|
|
2837
|
+
let installed = false;
|
|
2838
|
+
try {
|
|
2839
|
+
const info = await lstat2(versionDir);
|
|
2840
|
+
if (!info.isDirectory())
|
|
2841
|
+
throw new Error(`gjc version path is not a directory: ${versionDir}`);
|
|
2842
|
+
installed = true;
|
|
2843
|
+
} catch (error) {
|
|
2844
|
+
if (error.code !== "ENOENT")
|
|
2845
|
+
throw error;
|
|
2178
2846
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2847
|
+
if (!installed) {
|
|
2848
|
+
staging = await mkdtemp(join9(layout.gjcPrefix, ".staging-"));
|
|
2849
|
+
await chmod4(staging, 448);
|
|
2850
|
+
await writeFile5(join9(staging, "package.json"), `${JSON.stringify({ private: true, dependencies: { "gajae-code": version } }, null, 2)}
|
|
2851
|
+
`, { mode: 384 });
|
|
2852
|
+
const installCommand = ["bun", "install", "--production"];
|
|
2853
|
+
assertSuccessful(installCommand, await runner(installCommand, staging));
|
|
2854
|
+
const installedBin = join9(staging, "node_modules", ".bin", "gjc");
|
|
2855
|
+
await access2(installedBin);
|
|
2856
|
+
const binDir = join9(staging, "bin");
|
|
2857
|
+
await mkdir6(binDir, { mode: 448 });
|
|
2858
|
+
await symlink("../node_modules/.bin/gjc", join9(binDir, "gjc"), "file");
|
|
2859
|
+
await verifyGjc(join9(binDir, "gjc"), staging, version, runner);
|
|
2860
|
+
await rename4(staging, versionDir);
|
|
2861
|
+
staging = undefined;
|
|
2862
|
+
} else {
|
|
2863
|
+
await verifyGjc(join9(versionDir, "bin", "gjc"), versionDir, version, runner);
|
|
2864
|
+
}
|
|
2865
|
+
await pruneVersions(layout.gjcPrefix, [version, ...previousVersion === undefined ? [] : [previousVersion]]);
|
|
2866
|
+
const temporaryLink = join9(layout.gjcPrefix, `.current-${randomUUID4()}`);
|
|
2867
|
+
try {
|
|
2868
|
+
await symlink(version, temporaryLink, "dir");
|
|
2869
|
+
await rename4(temporaryLink, layout.gjcCurrent);
|
|
2870
|
+
} finally {
|
|
2871
|
+
await rm3(temporaryLink, { force: true });
|
|
2872
|
+
}
|
|
2873
|
+
return layout.gjcExecutable();
|
|
2874
|
+
} finally {
|
|
2875
|
+
if (staging !== undefined)
|
|
2876
|
+
await rm3(staging, { recursive: true, force: true });
|
|
2196
2877
|
}
|
|
2197
|
-
}
|
|
2878
|
+
}
|
|
2198
2879
|
|
|
2199
2880
|
// src/update.ts
|
|
2200
2881
|
var UPDATE_USAGE = "usage: gajaeway update [--check] [--force] [--bin-dir DIR] [--no-restart]";
|
|
2201
2882
|
var DEFAULT_RELEASE_REPO = "Yeachan-Heo/gajae-way";
|
|
2202
2883
|
var RELEASE_REPO_OVERRIDE_ENV = "GAJAEWAY_UPDATE_REPO";
|
|
2884
|
+
var PIN_MANIFEST = "package.json";
|
|
2203
2885
|
function releaseRepo(env = process.env) {
|
|
2204
2886
|
const override = env[RELEASE_REPO_OVERRIDE_ENV]?.trim();
|
|
2205
2887
|
return override ? override : declaredReleaseRepo();
|
|
@@ -2262,13 +2944,12 @@ var CORE_BINARIES = ["gajaeway", "gajaeway-gateway"];
|
|
|
2262
2944
|
var MIN_BINARY_BYTES = 1048576;
|
|
2263
2945
|
var MAX_ASSET_BYTES = 512 * 1024 * 1024;
|
|
2264
2946
|
var RELEASE_MARKER = ".gajaeway-release";
|
|
2265
|
-
var LOCK_FILE = ".gajaeway-update.lock";
|
|
2266
2947
|
function releaseMarkerPath(binDir) {
|
|
2267
|
-
return
|
|
2948
|
+
return join10(binDir, RELEASE_MARKER);
|
|
2268
2949
|
}
|
|
2269
2950
|
async function readInstalledRelease(binDir) {
|
|
2270
2951
|
try {
|
|
2271
|
-
const version = releaseVersion(await
|
|
2952
|
+
const version = releaseVersion(await readFile7(releaseMarkerPath(binDir), "utf8"));
|
|
2272
2953
|
return version;
|
|
2273
2954
|
} catch {
|
|
2274
2955
|
return;
|
|
@@ -2277,12 +2958,12 @@ async function readInstalledRelease(binDir) {
|
|
|
2277
2958
|
async function resolveBinDir(flag, execPath = process.execPath) {
|
|
2278
2959
|
if (flag !== undefined)
|
|
2279
2960
|
return flag;
|
|
2280
|
-
const real = await
|
|
2961
|
+
const real = await realpath2(execPath).catch(() => {
|
|
2281
2962
|
return;
|
|
2282
2963
|
});
|
|
2283
2964
|
if (real === undefined)
|
|
2284
2965
|
return;
|
|
2285
|
-
return
|
|
2966
|
+
return basename2(real).startsWith("gajaeway") ? dirname3(real) : undefined;
|
|
2286
2967
|
}
|
|
2287
2968
|
function renderUpdate(result) {
|
|
2288
2969
|
const lines = [];
|
|
@@ -2302,8 +2983,71 @@ function renderUpdate(result) {
|
|
|
2302
2983
|
lines.push(`restart: queued ${result.restartQueued} \u2014 check with: gajaeway ops restart-stack --status`);
|
|
2303
2984
|
else if (result.restartSkipped)
|
|
2304
2985
|
lines.push("restart: skipped (--no-restart)");
|
|
2986
|
+
if (result.brokerNotEnabled)
|
|
2987
|
+
lines.push("broker unit installed, not enabled: run gajaeway setup --broker-agent");
|
|
2305
2988
|
return lines;
|
|
2306
2989
|
}
|
|
2990
|
+
async function readPinnedGjcVersion(path) {
|
|
2991
|
+
let parsed;
|
|
2992
|
+
try {
|
|
2993
|
+
parsed = JSON.parse(await readFile7(path, "utf8"));
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
throw new Error(`could not read pinned gjc version from ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2996
|
+
}
|
|
2997
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
2998
|
+
throw new Error(`invalid release package metadata at ${path}`);
|
|
2999
|
+
const gjc = parsed.gjc;
|
|
3000
|
+
if (typeof gjc !== "object" || gjc === null || Array.isArray(gjc))
|
|
3001
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
3002
|
+
const version = gjc.version;
|
|
3003
|
+
if (typeof version !== "string" || version.trim() === "")
|
|
3004
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
3005
|
+
return version;
|
|
3006
|
+
}
|
|
3007
|
+
async function defaultUpdateCommandRunner(command) {
|
|
3008
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
3009
|
+
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
3010
|
+
if (exitCode !== 0) {
|
|
3011
|
+
const detail = stderr.trim();
|
|
3012
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
3013
|
+
}
|
|
3014
|
+
return exitCode;
|
|
3015
|
+
}
|
|
3016
|
+
async function installUpdatedServices(options) {
|
|
3017
|
+
const { home, binDir, platform, noRestart, deps } = options;
|
|
3018
|
+
const brokerProvisioned = await exists(homeLayout(home).brokerProvisionManifest);
|
|
3019
|
+
const env = { ...deps.env ?? process.env, GAJAEWAY_HOME: home };
|
|
3020
|
+
await installServices({
|
|
3021
|
+
binDir,
|
|
3022
|
+
platform,
|
|
3023
|
+
env,
|
|
3024
|
+
configuredAdaptersOnly: true,
|
|
3025
|
+
compareAndWrite: true,
|
|
3026
|
+
...deps.serviceOptions
|
|
3027
|
+
});
|
|
3028
|
+
if (platform === "linux") {
|
|
3029
|
+
const run = deps.serviceRunner ?? defaultUpdateCommandRunner;
|
|
3030
|
+
const execute = async (command) => {
|
|
3031
|
+
const exitCode = await run(command);
|
|
3032
|
+
if (exitCode !== 0)
|
|
3033
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}`);
|
|
3034
|
+
};
|
|
3035
|
+
await execute(["systemctl", "--user", "daemon-reload"]);
|
|
3036
|
+
if (!noRestart) {
|
|
3037
|
+
const enableUnits = [];
|
|
3038
|
+
for (const spec of serviceSpecs()) {
|
|
3039
|
+
if (spec.id === "broker" && !brokerProvisioned)
|
|
3040
|
+
continue;
|
|
3041
|
+
if (spec.id.startsWith("adapter-") && !await Bun.file(join10(home, `adapter-${spec.id.slice(8)}.json`)).exists())
|
|
3042
|
+
continue;
|
|
3043
|
+
enableUnits.push(systemdUnitName(spec));
|
|
3044
|
+
}
|
|
3045
|
+
if (enableUnits.length > 0)
|
|
3046
|
+
await execute(["systemctl", "--user", "enable", ...enableUnits]);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return !brokerProvisioned;
|
|
3050
|
+
}
|
|
2307
3051
|
async function runUpdate(options) {
|
|
2308
3052
|
const deps = options.deps ?? {};
|
|
2309
3053
|
const platform = deps.platform ?? process.platform;
|
|
@@ -2326,46 +3070,67 @@ async function runUpdate(options) {
|
|
|
2326
3070
|
return { current, latest, tag: release.tag, repo, checkedOnly: true, upToDate: false, installed: [] };
|
|
2327
3071
|
if (binDir === undefined)
|
|
2328
3072
|
throw new Error("run the update from a compiled gajaeway binary, or pass --bin-dir DIR");
|
|
2329
|
-
|
|
3073
|
+
let lease;
|
|
2330
3074
|
try {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
throw new Error(`
|
|
2337
|
-
await assertStagedBinariesUsable(staged, toInstall);
|
|
2338
|
-
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
2339
|
-
const backupDir = join8(binDir, `backup-${current}-${stamp}Z`);
|
|
2340
|
-
await mkdir5(backupDir, { recursive: false });
|
|
2341
|
-
for (const name of toInstall) {
|
|
2342
|
-
const target = join8(binDir, name);
|
|
2343
|
-
if (await exists(target))
|
|
2344
|
-
await copyFile(target, join8(backupDir, name));
|
|
2345
|
-
}
|
|
2346
|
-
for (const name of toInstall) {
|
|
2347
|
-
const stagedPath = join8(staged.dir, name);
|
|
2348
|
-
const tempPath = join8(binDir, `.update-${name}`);
|
|
2349
|
-
await copyFile(stagedPath, tempPath);
|
|
2350
|
-
await chmod3(tempPath, 493);
|
|
2351
|
-
await rename3(tempPath, join8(binDir, name));
|
|
3075
|
+
lease = await acquireLease(options.home, "update");
|
|
3076
|
+
console.info(`lease_acquired name=update pid=${process.pid}`);
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
if (error instanceof LeaseHeldError) {
|
|
3079
|
+
console.error(error.message);
|
|
3080
|
+
throw new Error(`another gajaeway update is running (${error.message})`, { cause: error });
|
|
2352
3081
|
}
|
|
2353
|
-
|
|
3082
|
+
console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
|
|
3083
|
+
throw error;
|
|
3084
|
+
}
|
|
3085
|
+
const installed = await (async () => {
|
|
3086
|
+
let staged;
|
|
3087
|
+
try {
|
|
3088
|
+
staged = await stageRelease(asset, deps);
|
|
3089
|
+
const installedNames = await knownBinariesIn(binDir);
|
|
3090
|
+
const toInstall = binariesToInstall(staged.names, installedNames);
|
|
3091
|
+
const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
|
|
3092
|
+
if (missingCore.length > 0)
|
|
3093
|
+
throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
|
|
3094
|
+
await assertStagedBinariesUsable(staged, toInstall);
|
|
3095
|
+
const gjcVersion = await readPinnedGjcVersion(join10(staged.dir, PIN_MANIFEST));
|
|
3096
|
+
await installPinnedGjc({
|
|
3097
|
+
home: options.home,
|
|
3098
|
+
version: gjcVersion,
|
|
3099
|
+
...deps.gjcInstallRunner === undefined ? {} : { runner: deps.gjcInstallRunner }
|
|
3100
|
+
});
|
|
3101
|
+
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
3102
|
+
const backupDir = join10(binDir, `backup-${current}-${stamp}Z`);
|
|
3103
|
+
await mkdir7(backupDir, { recursive: false });
|
|
3104
|
+
const replaced = [...toInstall, PIN_MANIFEST];
|
|
3105
|
+
for (const name of replaced) {
|
|
3106
|
+
const target = join10(binDir, name);
|
|
3107
|
+
if (await exists(target))
|
|
3108
|
+
await copyFile(target, join10(backupDir, name));
|
|
3109
|
+
}
|
|
3110
|
+
for (const name of replaced) {
|
|
3111
|
+
const stagedPath = join10(staged.dir, name);
|
|
3112
|
+
const tempPath = join10(binDir, `.update-${name}`);
|
|
3113
|
+
await copyFile(stagedPath, tempPath);
|
|
3114
|
+
await chmod5(tempPath, name === PIN_MANIFEST ? 420 : 493);
|
|
3115
|
+
await rename5(tempPath, join10(binDir, name));
|
|
3116
|
+
}
|
|
3117
|
+
await writeFile6(releaseMarkerPath(binDir), `${latest}
|
|
2354
3118
|
`, "utf8");
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3119
|
+
const brokerNotEnabled = await installUpdatedServices({
|
|
3120
|
+
home: options.home,
|
|
3121
|
+
binDir,
|
|
3122
|
+
platform,
|
|
3123
|
+
noRestart: options.args.noRestart,
|
|
3124
|
+
deps
|
|
3125
|
+
});
|
|
3126
|
+
return { toInstall, backupDir, brokerNotEnabled };
|
|
3127
|
+
} finally {
|
|
3128
|
+
if (staged !== undefined)
|
|
3129
|
+
await rm4(staged.dir, { recursive: true, force: true });
|
|
3130
|
+
await lease.release();
|
|
3131
|
+
}
|
|
3132
|
+
})();
|
|
3133
|
+
if (options.args.noRestart)
|
|
2369
3134
|
return {
|
|
2370
3135
|
current,
|
|
2371
3136
|
latest,
|
|
@@ -2373,26 +3138,37 @@ async function runUpdate(options) {
|
|
|
2373
3138
|
repo,
|
|
2374
3139
|
checkedOnly: false,
|
|
2375
3140
|
upToDate: false,
|
|
2376
|
-
installed: toInstall,
|
|
2377
|
-
backupDir,
|
|
2378
|
-
|
|
3141
|
+
installed: installed.toInstall,
|
|
3142
|
+
backupDir: installed.backupDir,
|
|
3143
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
3144
|
+
restartSkipped: true
|
|
2379
3145
|
};
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3146
|
+
const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home, binDir, "update");
|
|
3147
|
+
return {
|
|
3148
|
+
current,
|
|
3149
|
+
latest,
|
|
3150
|
+
tag: release.tag,
|
|
3151
|
+
repo,
|
|
3152
|
+
checkedOnly: false,
|
|
3153
|
+
upToDate: false,
|
|
3154
|
+
installed: installed.toInstall,
|
|
3155
|
+
backupDir: installed.backupDir,
|
|
3156
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
3157
|
+
restartQueued: receipt.receiptId
|
|
3158
|
+
};
|
|
2383
3159
|
}
|
|
2384
3160
|
function binariesToInstall(stagedNames, installedNames) {
|
|
2385
3161
|
const installed = new Set(installedNames);
|
|
2386
3162
|
return KNOWN_BINARIES.filter((name) => stagedNames.includes(name) && (installed.has(name) || CORE_BINARIES.includes(name)));
|
|
2387
3163
|
}
|
|
2388
3164
|
async function knownBinariesIn(dir) {
|
|
2389
|
-
const entries2 = await
|
|
3165
|
+
const entries2 = await readdir3(dir).catch(() => []);
|
|
2390
3166
|
const names = new Set(entries2);
|
|
2391
3167
|
return KNOWN_BINARIES.filter((name) => names.has(name));
|
|
2392
3168
|
}
|
|
2393
3169
|
async function assertStagedBinariesUsable(staged, toInstall) {
|
|
2394
3170
|
for (const name of toInstall) {
|
|
2395
|
-
const info = await stat2(
|
|
3171
|
+
const info = await stat2(join10(staged.dir, name));
|
|
2396
3172
|
if (!info.isFile())
|
|
2397
3173
|
throw new Error(`staged ${name} is not a regular file`);
|
|
2398
3174
|
if (info.size < MIN_BINARY_BYTES)
|
|
@@ -2408,10 +3184,10 @@ async function exists(path) {
|
|
|
2408
3184
|
}
|
|
2409
3185
|
}
|
|
2410
3186
|
async function stageRelease(asset, deps) {
|
|
2411
|
-
const dir = await
|
|
3187
|
+
const dir = await mkdtemp2(join10(tmpdir(), "gajaeway-update-"));
|
|
2412
3188
|
const tarball = await (deps.downloadAsset ?? defaultDownloadAsset)(asset);
|
|
2413
3189
|
await (deps.extractTarball ?? defaultExtractTarball)(tarball, dir);
|
|
2414
|
-
const names = (await
|
|
3190
|
+
const names = (await readdir3(dir)).filter((name) => KNOWN_BINARIES.includes(name));
|
|
2415
3191
|
return { dir, names };
|
|
2416
3192
|
}
|
|
2417
3193
|
async function defaultFetchLatestRelease(repo) {
|
|
@@ -2437,8 +3213,8 @@ async function defaultDownloadAsset(asset) {
|
|
|
2437
3213
|
return bytes;
|
|
2438
3214
|
}
|
|
2439
3215
|
async function defaultExtractTarball(tarball, intoDir) {
|
|
2440
|
-
const tarballPath =
|
|
2441
|
-
await
|
|
3216
|
+
const tarballPath = join10(intoDir, "release.tar.gz");
|
|
3217
|
+
await writeFile6(tarballPath, tarball);
|
|
2442
3218
|
const process2 = Bun.spawn(["tar", "-xzf", tarballPath, "-C", intoDir], {
|
|
2443
3219
|
stdout: "ignore",
|
|
2444
3220
|
stderr: "pipe"
|
|
@@ -2446,186 +3222,19 @@ async function defaultExtractTarball(tarball, intoDir) {
|
|
|
2446
3222
|
const exitCode = await process2.exited;
|
|
2447
3223
|
if (exitCode !== 0) {
|
|
2448
3224
|
const detail = await new Response(process2.stderr).text();
|
|
2449
|
-
throw new Error(`extracting ${
|
|
3225
|
+
throw new Error(`extracting ${basename2(tarballPath)} failed (tar exit ${exitCode}): ${detail.trim()}`);
|
|
2450
3226
|
}
|
|
2451
|
-
await
|
|
3227
|
+
await rm4(tarballPath, { force: true });
|
|
2452
3228
|
}
|
|
2453
|
-
async function defaultQueueRestart(home) {
|
|
2454
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
3229
|
+
async function defaultQueueRestart(home, binDir, trigger) {
|
|
3230
|
+
const { receipt, supervisorPid } = await launchRestartStack({ home, binDir, trigger });
|
|
2455
3231
|
return { receiptId: receipt.id, supervisorPid };
|
|
2456
3232
|
}
|
|
2457
|
-
async function acquireUpdateLock(binDir) {
|
|
2458
|
-
const lockPath = join8(binDir, LOCK_FILE);
|
|
2459
|
-
for (let attempt = 0;attempt < 2; attempt++) {
|
|
2460
|
-
try {
|
|
2461
|
-
const handle = await open(lockPath, "wx");
|
|
2462
|
-
try {
|
|
2463
|
-
await handle.write(`${JSON.stringify({ pid: process.pid, at: new Date().toISOString() })}
|
|
2464
|
-
`);
|
|
2465
|
-
} finally {
|
|
2466
|
-
await handle.close();
|
|
2467
|
-
}
|
|
2468
|
-
return async () => {
|
|
2469
|
-
await rm2(lockPath, { force: true });
|
|
2470
|
-
};
|
|
2471
|
-
} catch (error) {
|
|
2472
|
-
if (!isFileExistsError(error))
|
|
2473
|
-
throw error;
|
|
2474
|
-
const holder = await readLockHolder(lockPath);
|
|
2475
|
-
if (holder !== undefined && isProcessAlive(holder)) {
|
|
2476
|
-
throw new Error(`another gajaeway update is running (pid ${holder}); refusing to run concurrently`);
|
|
2477
|
-
}
|
|
2478
|
-
await rm2(lockPath, { force: true });
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
throw new Error(`could not acquire the update lock at ${lockPath}`);
|
|
2482
|
-
}
|
|
2483
|
-
function isFileExistsError(error) {
|
|
2484
|
-
return error !== null && typeof error === "object" && error.code === "EEXIST";
|
|
2485
|
-
}
|
|
2486
|
-
async function readLockHolder(lockPath) {
|
|
2487
|
-
try {
|
|
2488
|
-
const parsed = JSON.parse(await readFile6(lockPath, "utf8"));
|
|
2489
|
-
return typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
2490
|
-
} catch {
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
function isProcessAlive(pid) {
|
|
2495
|
-
try {
|
|
2496
|
-
process.kill(pid, 0);
|
|
2497
|
-
return true;
|
|
2498
|
-
} catch (error) {
|
|
2499
|
-
return error.code === "EPERM";
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
// src/upgrade.ts
|
|
2504
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
2505
|
-
import { dirname as dirname2, join as join9 } from "path";
|
|
2506
|
-
async function readGatewayPinnedGjcVersion(home) {
|
|
2507
|
-
const candidates = [
|
|
2508
|
-
join9(home, "..", "..", "packages", "gateway", "package.json"),
|
|
2509
|
-
join9(dirname2(process.execPath), "package.json")
|
|
2510
|
-
];
|
|
2511
|
-
for (const path of candidates) {
|
|
2512
|
-
try {
|
|
2513
|
-
const version = JSON.parse(await readFile7(path, "utf-8")).gjc?.version;
|
|
2514
|
-
if (typeof version === "string" && version.length > 0)
|
|
2515
|
-
return version;
|
|
2516
|
-
} catch {}
|
|
2517
|
-
}
|
|
2518
|
-
throw new Error(`Failed to read pinned GJC version from any of ${candidates.join(", ")}`);
|
|
2519
|
-
}
|
|
2520
|
-
async function checkLatestCiStatus(repo) {
|
|
2521
|
-
try {
|
|
2522
|
-
const branchProc = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
|
|
2523
|
-
stdout: "pipe",
|
|
2524
|
-
stderr: "pipe"
|
|
2525
|
-
});
|
|
2526
|
-
if (branchProc.exitCode !== 0) {
|
|
2527
|
-
throw new Error("Could not determine current git branch");
|
|
2528
|
-
}
|
|
2529
|
-
const branch = branchProc.stdout?.toString().trim();
|
|
2530
|
-
if (!branch) {
|
|
2531
|
-
throw new Error("Empty branch name");
|
|
2532
|
-
}
|
|
2533
|
-
const runProc = Bun.spawnSync(["gh", "run", "list", "-R", repo, "--branch", branch, "--limit", "1", "--json", "conclusion,status"], {
|
|
2534
|
-
stdout: "pipe",
|
|
2535
|
-
stderr: "pipe"
|
|
2536
|
-
});
|
|
2537
|
-
if (runProc.exitCode !== 0) {
|
|
2538
|
-
throw new Error(`gh run list failed: ${runProc.stderr?.toString()}`);
|
|
2539
|
-
}
|
|
2540
|
-
const output = runProc.stdout?.toString() || "[]";
|
|
2541
|
-
const runs = JSON.parse(output);
|
|
2542
|
-
if (!Array.isArray(runs) || runs.length === 0) {
|
|
2543
|
-
throw new Error("No CI runs found for this branch");
|
|
2544
|
-
}
|
|
2545
|
-
const latestRun = runs[0];
|
|
2546
|
-
if (!latestRun.conclusion || !latestRun.status) {
|
|
2547
|
-
throw new Error(`Latest CI run status is incomplete: ${JSON.stringify(latestRun)}`);
|
|
2548
|
-
}
|
|
2549
|
-
if (latestRun.status === "in_progress") {
|
|
2550
|
-
throw new Error("CI is currently running; wait for completion");
|
|
2551
|
-
}
|
|
2552
|
-
return latestRun.conclusion === "success";
|
|
2553
|
-
} catch (error) {
|
|
2554
|
-
throw new Error(`Failed to check CI status: ${error instanceof Error ? error.message : String(error)}`);
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
async function performUpgrade(options) {
|
|
2558
|
-
const readVersion = options.readPinnedVersion || readGatewayPinnedGjcVersion;
|
|
2559
|
-
const targetVersion = options.gjcVersion ?? await readVersion(options.home);
|
|
2560
|
-
try {
|
|
2561
|
-
const checkCi = options.checkCiStatus || checkLatestCiStatus;
|
|
2562
|
-
let ciPassed = false;
|
|
2563
|
-
try {
|
|
2564
|
-
ciPassed = await checkCi("Yeachan-Heo/gajae-way");
|
|
2565
|
-
} catch (ciError) {
|
|
2566
|
-
const ciDetail = ciError instanceof Error ? ciError.message : String(ciError);
|
|
2567
|
-
return { status: "failed", detail: `CI status check failed: ${ciDetail}` };
|
|
2568
|
-
}
|
|
2569
|
-
if (!ciPassed) {
|
|
2570
|
-
return {
|
|
2571
|
-
status: "failed",
|
|
2572
|
-
detail: `Cannot upgrade: latest CI run did not pass. Fix failures and try again.`
|
|
2573
|
-
};
|
|
2574
|
-
}
|
|
2575
|
-
const gjcProc = Bun.spawnSync(["gjc", "--version"], {
|
|
2576
|
-
stdout: "pipe",
|
|
2577
|
-
stderr: "pipe"
|
|
2578
|
-
});
|
|
2579
|
-
if (gjcProc.exitCode !== 0) {
|
|
2580
|
-
return { status: "failed", detail: "Failed to get current gjc version" };
|
|
2581
|
-
}
|
|
2582
|
-
const currentVersionMatch = ((gjcProc.stdout?.toString() || "") + (gjcProc.stderr?.toString() || "")).match(/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/);
|
|
2583
|
-
if (!currentVersionMatch) {
|
|
2584
|
-
return { status: "failed", detail: "Could not parse current gjc version" };
|
|
2585
|
-
}
|
|
2586
|
-
const currentVersion = currentVersionMatch.slice(1, 4).join(".");
|
|
2587
|
-
if (currentVersion === targetVersion) {
|
|
2588
|
-
return { status: "ok", detail: `Already running gjc ${targetVersion}` };
|
|
2589
|
-
}
|
|
2590
|
-
const installProc = Bun.spawnSync(["bun", "add", "-g", `gajae-code@${targetVersion}`], {
|
|
2591
|
-
stdout: "pipe",
|
|
2592
|
-
stderr: "pipe",
|
|
2593
|
-
cwd: join9(options.home, "workspace")
|
|
2594
|
-
});
|
|
2595
|
-
if (installProc.exitCode !== 0) {
|
|
2596
|
-
return {
|
|
2597
|
-
status: "failed",
|
|
2598
|
-
detail: `Failed to install gjc ${targetVersion}: ${installProc.stderr?.toString()}`
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
const verifyProc = Bun.spawnSync(["gjc", "--version"], {
|
|
2602
|
-
stdout: "pipe",
|
|
2603
|
-
stderr: "pipe"
|
|
2604
|
-
});
|
|
2605
|
-
if (verifyProc.exitCode !== 0) {
|
|
2606
|
-
return { status: "failed", detail: "Failed to verify gjc installation" };
|
|
2607
|
-
}
|
|
2608
|
-
const verifiedVersionMatch = ((verifyProc.stdout?.toString() || "") + (verifyProc.stderr?.toString() || "")).match(/(?:gjc\/)?(\d+)\.(\d+)\.(\d+)/);
|
|
2609
|
-
if (!verifiedVersionMatch) {
|
|
2610
|
-
return { status: "failed", detail: "Could not verify installed gjc version" };
|
|
2611
|
-
}
|
|
2612
|
-
const verifiedVersion = verifiedVersionMatch.slice(1, 4).join(".");
|
|
2613
|
-
if (verifiedVersion !== targetVersion) {
|
|
2614
|
-
return {
|
|
2615
|
-
status: "failed",
|
|
2616
|
-
detail: `Verification failed: expected ${targetVersion}, got ${verifiedVersion}`
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
|
-
return { status: "ok", detail: `Successfully upgraded gjc from ${currentVersion} to ${targetVersion}` };
|
|
2620
|
-
} catch (error) {
|
|
2621
|
-
return {
|
|
2622
|
-
status: "failed",
|
|
2623
|
-
detail: `Upgrade failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2624
|
-
};
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
3233
|
|
|
2628
3234
|
// src/main.ts
|
|
3235
|
+
async function connectGatewaySocket(path, options = {}) {
|
|
3236
|
+
return GajaewayClient.connectSocketWithBackoff(path, { ...options, deadlineMs: 30000 });
|
|
3237
|
+
}
|
|
2629
3238
|
function socketPath(home = process.env.GAJAEWAY_HOME) {
|
|
2630
3239
|
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
2631
3240
|
}
|
|
@@ -2644,7 +3253,7 @@ var COMMANDS = [
|
|
|
2644
3253
|
"update",
|
|
2645
3254
|
"setup"
|
|
2646
3255
|
];
|
|
2647
|
-
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] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops
|
|
3256
|
+
var CLI_USAGE = "usage: gajaeway [--version] [--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 ... (settle <eventId>|retry <eventId>|test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs [--limit N] [--offset N]|ops backup <path>|ops deliveries [--since ISO] [--state S] [--limit N] [--json]|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]|migrate [--source PATH] [--target PATH] [--dry-run]|update [--check] [--force] [--bin-dir DIR] [--no-restart]|setup [--from-env] [--adapters discord,slack,telegram] [--owner ID] [--discord-app-id ID]|setup --status|setup --broker-agent [--from DIR] (work run waits for a response; caller timeout does not end the attempt); cron timezone is an IANA zone and defaults to the gateway host's local timezone";
|
|
2648
3257
|
var USAGE_EXIT_CODE = 2;
|
|
2649
3258
|
function usageFor(command) {
|
|
2650
3259
|
return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
|
|
@@ -2753,6 +3362,13 @@ function parseMonitorUpdateArgs(args) {
|
|
|
2753
3362
|
var MONITOR_TEST_USAGE = "usage: gajaeway monitors test <id> [--type T] [--payload J] [--wait[=SECONDS]]";
|
|
2754
3363
|
var MONITOR_WAIT_ERROR = `${MONITOR_TEST_USAGE}
|
|
2755
3364
|
--wait expects a finite non-negative integer up to 86400`;
|
|
3365
|
+
var MONITOR_OPERATOR_USAGE = "usage: gajaeway monitors settle <eventId>|retry <eventId>";
|
|
3366
|
+
function parseMonitorEventOperatorArgs(args) {
|
|
3367
|
+
const eventId = args[0];
|
|
3368
|
+
if (args.length !== 1 || !eventId?.trim() || eventId.startsWith("--"))
|
|
3369
|
+
throw new Error(MONITOR_OPERATOR_USAGE);
|
|
3370
|
+
return eventId;
|
|
3371
|
+
}
|
|
2756
3372
|
function parseMonitorTestArgs(args) {
|
|
2757
3373
|
const [monitorId, ...flags] = args;
|
|
2758
3374
|
if (!monitorId || monitorId.startsWith("--"))
|
|
@@ -2803,7 +3419,7 @@ unknown argument: ${flag}`);
|
|
|
2803
3419
|
...waitSeconds === undefined ? {} : { waitSeconds }
|
|
2804
3420
|
};
|
|
2805
3421
|
}
|
|
2806
|
-
var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry"]);
|
|
3422
|
+
var TERMINAL_MONITOR_STAGES = new Set(["delivered", "authored_no_delivery", "failed", "failed_no_retry", "skipped"]);
|
|
2807
3423
|
function waitForMonitorStage(client, eventId, timeoutSeconds) {
|
|
2808
3424
|
return new Promise((resolve) => {
|
|
2809
3425
|
let stage = "admitted";
|
|
@@ -2848,11 +3464,23 @@ function renderCycle(cycle) {
|
|
|
2848
3464
|
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 ?? "-"}`);
|
|
2849
3465
|
lines.push(`deliveries: pending=${cycle.deliveries.pending} inflight=${cycle.deliveries.inflight} confirmed=${cycle.deliveries.confirmed} failed_ambiguous=${cycle.deliveries.failedAmbiguous} expired=${cycle.deliveries.expired}`);
|
|
2850
3466
|
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)" : ""}`);
|
|
3467
|
+
if (cycle.memoryGitIndexLock) {
|
|
3468
|
+
const { path, ageMs } = cycle.memoryGitIndexLock;
|
|
3469
|
+
lines.push(`memory_git_index_lock: ${path} age=${Math.round(ageMs)}ms`);
|
|
3470
|
+
}
|
|
2851
3471
|
if (cycle.agentDisk) {
|
|
2852
3472
|
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
2853
3473
|
const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
2854
3474
|
lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
|
|
2855
3475
|
}
|
|
3476
|
+
if (cycle.brokerAgent !== null) {
|
|
3477
|
+
lines.push(`broker_agent: ${cycle.brokerAgent.status}`);
|
|
3478
|
+
for (const entry of cycle.brokerAgent.entries)
|
|
3479
|
+
if (entry.state !== "in_sync")
|
|
3480
|
+
lines.push(`broker_agent ${entry.name}: kind=${entry.kind} state=${entry.state}`);
|
|
3481
|
+
}
|
|
3482
|
+
const sloState = cycle.slo.ok ? "ok" : cycle.slo.days.some((day) => day.status === "failed") ? "failed" : cycle.slo.days.some((day) => day.status === "insufficient") ? "insufficient" : "pending";
|
|
3483
|
+
lines.push(`slo: seven-day=${sloState}`);
|
|
2856
3484
|
lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
|
|
2857
3485
|
if (cycle.monitorAuthoringLost.length > 0)
|
|
2858
3486
|
lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
|
|
@@ -2870,82 +3498,110 @@ function renderCycle(cycle) {
|
|
|
2870
3498
|
function cycleExitCode(cycle) {
|
|
2871
3499
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
2872
3500
|
}
|
|
2873
|
-
function
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3501
|
+
function renderSlo(result) {
|
|
3502
|
+
const message = result.ok ? `SLO ok: ${result.days.length} consecutive UTC days` : result.days.some((day) => day.status === "failed") ? "SLO failed: counted failure events or journal overflow" : result.days.some((day) => day.status === "insufficient") ? "SLO insufficient: coverage or durable evidence is incomplete" : "SLO pending: an update window is still running; rerun later";
|
|
3503
|
+
const lines = [message];
|
|
3504
|
+
for (const day of result.days)
|
|
3505
|
+
lines.push(`${day.date} status=${day.status} coverage=${day.complete ? "complete" : "incomplete"} alive_ms=${day.aliveMs} max_gap_ms=${day.maxUncoveredGapMs} broker_unavailable=${day.broker_unavailable} persona_bind_failed=${day.persona_bind_failed} excluded=${day.excluded}`);
|
|
3506
|
+
for (const window of result.windows)
|
|
3507
|
+
lines.push(`window id=${window.id} trigger=${window.trigger} state=${window.state} requested=${window.requestedAt} finished=${window.finishedAt ?? "-"} verified=${window.verified} eligible=${window.eligibleStart ?? "-"}..${window.eligibleEnd ?? "-"} over_cap_ms=${window.overCapMs}`);
|
|
3508
|
+
return lines;
|
|
3509
|
+
}
|
|
3510
|
+
function sloExitCode(result) {
|
|
3511
|
+
return result.ok ? 0 : 1;
|
|
3512
|
+
}
|
|
3513
|
+
var OPS_USAGE = "usage: gajaeway ops backup <path>|deliveries [--since ISO] [--state S] [--limit N] [--json]|redeliver <deliveryId>|redeliver --since <iso>|slo [--days N] [--json]|cycle [--json]|integrity|restart-stack [--status]";
|
|
3514
|
+
function parseOpsSloArgs(args) {
|
|
3515
|
+
let days = 7;
|
|
3516
|
+
let json = false;
|
|
3517
|
+
const seen = new Set;
|
|
3518
|
+
for (let index = 0;index < args.length; index++) {
|
|
3519
|
+
const flag = args[index];
|
|
3520
|
+
if (flag === "--json") {
|
|
3521
|
+
if (json)
|
|
3522
|
+
throw new Error(OPS_USAGE);
|
|
3523
|
+
json = true;
|
|
3524
|
+
continue;
|
|
3525
|
+
}
|
|
3526
|
+
if (flag !== "--days" || seen.has(flag))
|
|
3527
|
+
throw new Error(OPS_USAGE);
|
|
3528
|
+
seen.add(flag);
|
|
3529
|
+
const value = args[++index];
|
|
3530
|
+
if (!value || !/^\d+$/.test(value))
|
|
3531
|
+
throw new Error(OPS_USAGE);
|
|
3532
|
+
days = Number(value);
|
|
3533
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > 3650)
|
|
3534
|
+
throw new Error(OPS_USAGE);
|
|
2889
3535
|
}
|
|
2890
|
-
|
|
2891
|
-
throw new Error(`Backup is an empty SQLite database: ${backupPath}`);
|
|
2892
|
-
if (detail !== "ok")
|
|
2893
|
-
throw new Error(`Backup failed SQLite integrity_check: ${backupPath} (${detail})`);
|
|
3536
|
+
return { params: { days }, json };
|
|
2894
3537
|
}
|
|
2895
|
-
|
|
2896
|
-
const
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3538
|
+
function parseOpsDeliveriesArgs(args) {
|
|
3539
|
+
const params = {};
|
|
3540
|
+
const seen = new Set;
|
|
3541
|
+
let json = false;
|
|
3542
|
+
for (let index = 0;index < args.length; index++) {
|
|
3543
|
+
const flag = args[index];
|
|
3544
|
+
if (flag === "--json") {
|
|
3545
|
+
if (json)
|
|
3546
|
+
throw new Error(OPS_USAGE);
|
|
3547
|
+
json = true;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
if (flag !== "--since" && flag !== "--state" && flag !== "--limit")
|
|
3551
|
+
throw new Error(OPS_USAGE);
|
|
3552
|
+
if (seen.has(flag))
|
|
3553
|
+
throw new Error(OPS_USAGE);
|
|
3554
|
+
seen.add(flag);
|
|
3555
|
+
const value = args[++index];
|
|
3556
|
+
if (!value || value.startsWith("--"))
|
|
3557
|
+
throw new Error(OPS_USAGE);
|
|
3558
|
+
if (flag === "--since") {
|
|
3559
|
+
if (!Number.isFinite(Date.parse(value)))
|
|
3560
|
+
throw new Error(OPS_USAGE);
|
|
3561
|
+
params.since = value;
|
|
3562
|
+
} else if (flag === "--state")
|
|
3563
|
+
params.state = value;
|
|
3564
|
+
else {
|
|
3565
|
+
if (!/^\d+$/.test(value))
|
|
3566
|
+
throw new Error(OPS_USAGE);
|
|
3567
|
+
const limit = Number(value);
|
|
3568
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
3569
|
+
throw new Error(OPS_USAGE);
|
|
3570
|
+
params.limit = limit;
|
|
3571
|
+
}
|
|
2905
3572
|
}
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3573
|
+
return { params, json };
|
|
3574
|
+
}
|
|
3575
|
+
function parseWorkJobsArgs(args, usage) {
|
|
3576
|
+
const params = {};
|
|
3577
|
+
for (let index = 0;index < args.length; index++) {
|
|
3578
|
+
const flag = args[index];
|
|
3579
|
+
const value = args[++index];
|
|
3580
|
+
if (flag !== "--limit" && flag !== "--offset" || value === undefined || !/^\d+$/.test(value))
|
|
3581
|
+
throw new Error(usage);
|
|
3582
|
+
const key = flag === "--limit" ? "limit" : "offset";
|
|
3583
|
+
if (params[key] !== undefined)
|
|
3584
|
+
throw new Error(usage);
|
|
3585
|
+
const number = Number(value);
|
|
3586
|
+
if (!Number.isSafeInteger(number) || key === "limit" && (number < 1 || number > WORK_JOBS_MAX_LIMIT))
|
|
3587
|
+
throw new Error(usage);
|
|
3588
|
+
params[key] = number;
|
|
2913
3589
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
if (
|
|
2918
|
-
return
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
async function restoreDatabase(socket, backupPath) {
|
|
2924
|
-
if (!isAbsolute(backupPath))
|
|
2925
|
-
throw new Error("ops restore requires an absolute backup path");
|
|
2926
|
-
try {
|
|
2927
|
-
await stat3(socket);
|
|
2928
|
-
throw new Error(`Refusing restore: gateway socket ${socket} exists. Stop the daemon before restoring.`);
|
|
2929
|
-
} catch (error) {
|
|
2930
|
-
if (error instanceof Error && error.message.startsWith("Refusing restore:"))
|
|
2931
|
-
throw error;
|
|
2932
|
-
if (error.code !== "ENOENT")
|
|
2933
|
-
throw new Error(`Cannot verify gateway socket ${socket}; refusing restore.`);
|
|
2934
|
-
}
|
|
2935
|
-
verifyBackupIntegrity(backupPath);
|
|
2936
|
-
const databasePath = await restoreTargetPath();
|
|
2937
|
-
const preservedPath = `${databasePath}.pre-restore-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
2938
|
-
await copyFile2(databasePath, preservedPath);
|
|
2939
|
-
await copyFile2(backupPath, databasePath);
|
|
2940
|
-
console.log(`Validated SQLite backup: ${backupPath}`);
|
|
2941
|
-
console.log(`Copied current database to: ${preservedPath}`);
|
|
2942
|
-
console.log(`Restored backup to: ${databasePath}`);
|
|
3590
|
+
return params;
|
|
3591
|
+
}
|
|
3592
|
+
function renderDeliveries(result) {
|
|
3593
|
+
if (result.rows.length === 0)
|
|
3594
|
+
return ["deliveries: none"];
|
|
3595
|
+
return [
|
|
3596
|
+
`deliveries: ${result.rows.length}`,
|
|
3597
|
+
...result.rows.map((row) => `${row.createdAt} ${row.state} delivery=${row.deliveryId} turn=${row.turnId} origin=${row.originKey} conversation=${row.conversationId} text=${JSON.stringify(row.textPrefix)}`)
|
|
3598
|
+
];
|
|
2943
3599
|
}
|
|
2944
3600
|
var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
|
|
2945
3601
|
async function chat(socket) {
|
|
2946
3602
|
let client;
|
|
2947
3603
|
try {
|
|
2948
|
-
client = await
|
|
3604
|
+
client = await connectGatewaySocket(socket);
|
|
2949
3605
|
} catch {
|
|
2950
3606
|
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
2951
3607
|
process.exitCode = 1;
|
|
@@ -3000,8 +3656,31 @@ async function chat(socket) {
|
|
|
3000
3656
|
await client.close();
|
|
3001
3657
|
}
|
|
3002
3658
|
}
|
|
3659
|
+
async function pollMemoryOp(client, opRef, maxWaitMs = 300000, pollIntervalMs = 1000) {
|
|
3660
|
+
const startTime = Date.now();
|
|
3661
|
+
let operationElapsedMs = 0;
|
|
3662
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
3663
|
+
const status = await client.request("memory.status", { opRef });
|
|
3664
|
+
operationElapsedMs = status.elapsedMs;
|
|
3665
|
+
if (status.status === "completed") {
|
|
3666
|
+
return { status: "completed", elapsedMs: status.elapsedMs, result: status.result };
|
|
3667
|
+
}
|
|
3668
|
+
if (status.status === "failed") {
|
|
3669
|
+
return { status: "failed", elapsedMs: status.elapsedMs, error: status.error };
|
|
3670
|
+
}
|
|
3671
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
3672
|
+
}
|
|
3673
|
+
const waitedMs = Date.now() - startTime;
|
|
3674
|
+
console.error(`memory operation still running (${Math.round(operationElapsedMs / 1000)}s operation elapsed; CLI wait ended after ${Math.round(waitedMs / 1000)}s).
|
|
3675
|
+
Check status: gajaeway memory status ${opRef}`);
|
|
3676
|
+
return { status: "still_running", elapsedMs: operationElapsedMs };
|
|
3677
|
+
}
|
|
3003
3678
|
async function main(args = process.argv.slice(2), options = {}) {
|
|
3004
3679
|
const parsed = parseArgs(args);
|
|
3680
|
+
if (parsed.command === "--version" && parsed.rest.length === 0) {
|
|
3681
|
+
console.log(package_default.version);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3005
3684
|
const usage = usageFor(parsed.command);
|
|
3006
3685
|
if (usage !== undefined) {
|
|
3007
3686
|
console.error(usage);
|
|
@@ -3010,7 +3689,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3010
3689
|
try {
|
|
3011
3690
|
switch (parsed.command) {
|
|
3012
3691
|
case "status": {
|
|
3013
|
-
const client = await
|
|
3692
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3014
3693
|
try {
|
|
3015
3694
|
console.log(JSON.stringify(await client.status()));
|
|
3016
3695
|
} finally {
|
|
@@ -3019,7 +3698,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3019
3698
|
break;
|
|
3020
3699
|
}
|
|
3021
3700
|
case "shutdown": {
|
|
3022
|
-
const client = await
|
|
3701
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3023
3702
|
try {
|
|
3024
3703
|
await client.shutdown();
|
|
3025
3704
|
} finally {
|
|
@@ -3038,7 +3717,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3038
3717
|
break;
|
|
3039
3718
|
case "sessions": {
|
|
3040
3719
|
const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
3041
|
-
const client = await
|
|
3720
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3042
3721
|
try {
|
|
3043
3722
|
const result = await client.request("session.list");
|
|
3044
3723
|
const [command, selector] = parsed.rest;
|
|
@@ -3073,30 +3752,12 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3073
3752
|
break;
|
|
3074
3753
|
}
|
|
3075
3754
|
case "ops": {
|
|
3076
|
-
const [command, path
|
|
3077
|
-
if (command === "
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
} else if (path && path !== "--gjc") {
|
|
3083
|
-
throw new Error("usage: gajaeway ops upgrade [--gjc X.Y.Z]");
|
|
3084
|
-
}
|
|
3085
|
-
const result = await performUpgrade({ home, gjcVersion });
|
|
3086
|
-
console.log(result.detail);
|
|
3087
|
-
if (result.status !== "ok") {
|
|
3088
|
-
process.exitCode = 1;
|
|
3089
|
-
break;
|
|
3090
|
-
}
|
|
3091
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
3092
|
-
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
3093
|
-
console.log("read the outcome with: gajaeway ops restart-stack --status");
|
|
3094
|
-
break;
|
|
3095
|
-
}
|
|
3096
|
-
if (command === "restore" && path) {
|
|
3097
|
-
await restoreDatabase(parsed.socket, path);
|
|
3098
|
-
break;
|
|
3099
|
-
}
|
|
3755
|
+
const [command, path] = parsed.rest;
|
|
3756
|
+
if (command === undefined || !["backup", "deliveries", "redeliver", "integrity", "slo", "cycle", "restart-stack"].includes(command))
|
|
3757
|
+
throw new Error(`unknown ops command: ${command ?? "(missing)"}
|
|
3758
|
+
${OPS_USAGE}`);
|
|
3759
|
+
const deliveries = command === "deliveries" ? parseOpsDeliveriesArgs(parsed.rest.slice(1)) : undefined;
|
|
3760
|
+
const slo = command === "slo" ? parseOpsSloArgs(parsed.rest.slice(1)) : undefined;
|
|
3100
3761
|
if (command === "restart-stack") {
|
|
3101
3762
|
const usage2 = "usage: gajaeway ops restart-stack [--status]";
|
|
3102
3763
|
const home = gatewayHome();
|
|
@@ -3115,9 +3776,10 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3115
3776
|
const receipt = await readRestartReceipt(home);
|
|
3116
3777
|
if (receipt === undefined)
|
|
3117
3778
|
throw new Error("no restart-stack receipt");
|
|
3118
|
-
|
|
3779
|
+
const state = await effectiveRestartState(receipt, home);
|
|
3780
|
+
for (const line of renderRestartReceipt(receipt, state))
|
|
3119
3781
|
console.log(line);
|
|
3120
|
-
if (
|
|
3782
|
+
if (state !== "ok")
|
|
3121
3783
|
process.exitCode = 1;
|
|
3122
3784
|
} else if (parsed.rest.length === 1) {
|
|
3123
3785
|
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
@@ -3136,11 +3798,18 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3136
3798
|
else
|
|
3137
3799
|
throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
3138
3800
|
}
|
|
3139
|
-
const client = await
|
|
3801
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3140
3802
|
try {
|
|
3141
3803
|
if (command === "backup" && path)
|
|
3142
3804
|
console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
3143
|
-
else if (
|
|
3805
|
+
else if (deliveries) {
|
|
3806
|
+
const result = await client.opsDeliveries(deliveries.params);
|
|
3807
|
+
if (deliveries.json)
|
|
3808
|
+
console.log(JSON.stringify(result));
|
|
3809
|
+
else
|
|
3810
|
+
for (const line of renderDeliveries(result))
|
|
3811
|
+
console.log(line);
|
|
3812
|
+
} else if (redeliverParams)
|
|
3144
3813
|
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
3145
3814
|
else if (command === "integrity")
|
|
3146
3815
|
console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
@@ -3154,28 +3823,68 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3154
3823
|
for (const line of renderCycle(cycle))
|
|
3155
3824
|
console.log(line);
|
|
3156
3825
|
process.exitCode = cycleExitCode(cycle);
|
|
3826
|
+
} else if (slo) {
|
|
3827
|
+
const result = await client.opsSlo(slo.params);
|
|
3828
|
+
if (slo.json)
|
|
3829
|
+
console.log(JSON.stringify(result));
|
|
3830
|
+
else
|
|
3831
|
+
for (const line of renderSlo(result))
|
|
3832
|
+
console.log(line);
|
|
3833
|
+
process.exitCode = sloExitCode(result);
|
|
3157
3834
|
} else
|
|
3158
|
-
throw new Error(
|
|
3835
|
+
throw new Error(OPS_USAGE);
|
|
3159
3836
|
} finally {
|
|
3160
3837
|
await client.close();
|
|
3161
3838
|
}
|
|
3162
3839
|
break;
|
|
3163
3840
|
}
|
|
3164
3841
|
case "memory": {
|
|
3165
|
-
const usage2 = "usage: gajaeway memory audit|
|
|
3842
|
+
const usage2 = "usage: gajaeway memory audit|autolink|search <query>|status <opRef>";
|
|
3166
3843
|
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
3167
3844
|
throw new Error(`${usage2} (unknown argument: ${parsed.rest[1]})`);
|
|
3168
|
-
|
|
3845
|
+
if (parsed.rest[0] === "status" && parsed.rest.length < 2)
|
|
3846
|
+
throw new Error(`${usage2} (status requires opRef)`);
|
|
3847
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3169
3848
|
try {
|
|
3170
3849
|
if (parsed.rest[0] === "audit") {
|
|
3171
|
-
const
|
|
3172
|
-
|
|
3173
|
-
if (
|
|
3850
|
+
const initResp = await client.request("memory.audit");
|
|
3851
|
+
const poll = await pollMemoryOp(client, initResp.opRef);
|
|
3852
|
+
if (poll.status === "failed") {
|
|
3853
|
+
console.error(`audit failed after ${poll.elapsedMs}ms: ${poll.error}`);
|
|
3174
3854
|
process.exitCode = 1;
|
|
3855
|
+
} else if (poll.status === "still_running") {
|
|
3856
|
+
process.exitCode = 75;
|
|
3857
|
+
} else {
|
|
3858
|
+
const auditResult = poll.result;
|
|
3859
|
+
console.log(JSON.stringify(auditResult?.issues ?? []));
|
|
3860
|
+
console.error(`audit completed in ${poll.elapsedMs}ms`);
|
|
3861
|
+
if (!auditResult?.ok)
|
|
3862
|
+
process.exitCode = 1;
|
|
3863
|
+
}
|
|
3175
3864
|
} else if (parsed.rest[0] === "autolink") {
|
|
3176
|
-
|
|
3865
|
+
const initResp = await client.request("memory.autolink");
|
|
3866
|
+
const poll = await pollMemoryOp(client, initResp.opRef);
|
|
3867
|
+
if (poll.status === "failed") {
|
|
3868
|
+
console.error(`autolink failed after ${poll.elapsedMs}ms: ${poll.error}`);
|
|
3869
|
+
process.exitCode = 1;
|
|
3870
|
+
} else if (poll.status === "still_running") {
|
|
3871
|
+
process.exitCode = 75;
|
|
3872
|
+
} else {
|
|
3873
|
+
console.log(JSON.stringify(poll.result));
|
|
3874
|
+
}
|
|
3177
3875
|
} else if (parsed.rest[0] === "search" && parsed.rest.slice(1).join(" ")) {
|
|
3178
3876
|
console.log(JSON.stringify(await client.request("memory.search", { query: parsed.rest.slice(1).join(" ") })));
|
|
3877
|
+
} else if (parsed.rest[0] === "status" && parsed.rest[1]) {
|
|
3878
|
+
const opRef = parsed.rest[1];
|
|
3879
|
+
const result = await client.request("memory.status", { opRef });
|
|
3880
|
+
if (result.status === "failed") {
|
|
3881
|
+
console.error(`operation ${opRef} failed after ${result.elapsedMs}ms: ${result.error}`);
|
|
3882
|
+
process.exitCode = 1;
|
|
3883
|
+
} else if (result.status === "completed") {
|
|
3884
|
+
console.log(JSON.stringify(result));
|
|
3885
|
+
} else {
|
|
3886
|
+
console.log(JSON.stringify({ status: result.status, elapsedMs: result.elapsedMs }));
|
|
3887
|
+
}
|
|
3179
3888
|
} else
|
|
3180
3889
|
throw new Error(usage2);
|
|
3181
3890
|
} finally {
|
|
@@ -3188,7 +3897,8 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3188
3897
|
const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
|
|
3189
3898
|
const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
|
|
3190
3899
|
const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
|
|
3191
|
-
const
|
|
3900
|
+
const operatorEventId = command === "settle" || command === "retry" ? parseMonitorEventOperatorArgs(args2) : undefined;
|
|
3901
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3192
3902
|
try {
|
|
3193
3903
|
if (command === "add" && args2[0] === "--json" && args2[1])
|
|
3194
3904
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
|
|
@@ -3223,8 +3933,15 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3223
3933
|
const stage = await waitForMonitorStage(client, result.eventId, monitorTest.waitSeconds);
|
|
3224
3934
|
console.log(JSON.stringify({ eventId: result.eventId, stage }));
|
|
3225
3935
|
}
|
|
3936
|
+
} else if ((command === "settle" || command === "retry") && operatorEventId !== undefined) {
|
|
3937
|
+
const result = await client.request(`monitor.${command}`, {
|
|
3938
|
+
eventId: operatorEventId
|
|
3939
|
+
});
|
|
3940
|
+
console.log(JSON.stringify(result));
|
|
3941
|
+
if (!result.done)
|
|
3942
|
+
process.exitCode = 1;
|
|
3226
3943
|
} else
|
|
3227
|
-
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]]`);
|
|
3944
|
+
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>|settle <eventId>|retry <eventId>|test <id> [--type T] [--payload J] [--wait[=SECONDS]]`);
|
|
3228
3945
|
} finally {
|
|
3229
3946
|
await client.close();
|
|
3230
3947
|
}
|
|
@@ -3238,7 +3955,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3238
3955
|
const text2 = args2.slice(1).join(" ").trim();
|
|
3239
3956
|
if (!name2 || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name2) || (command === "status" ? args2.length !== 1 : !text2 || args2.slice(1).some((arg) => arg.startsWith("--"))))
|
|
3240
3957
|
throw new Error(usage2);
|
|
3241
|
-
const client2 = await
|
|
3958
|
+
const client2 = await connectGatewaySocket(parsed.socket);
|
|
3242
3959
|
try {
|
|
3243
3960
|
if (command === "status") {
|
|
3244
3961
|
console.log(JSON.stringify(await client2.request("work.status", { name: name2 })));
|
|
@@ -3257,6 +3974,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3257
3974
|
let force = false;
|
|
3258
3975
|
let allDead = false;
|
|
3259
3976
|
let name2;
|
|
3977
|
+
let jobsPage;
|
|
3260
3978
|
if (command === "retire") {
|
|
3261
3979
|
for (let i = 0;i < args2.length; i++) {
|
|
3262
3980
|
const arg = args2[i];
|
|
@@ -3280,9 +3998,9 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3280
3998
|
if (allDead && name2)
|
|
3281
3999
|
throw new Error(usage2);
|
|
3282
4000
|
} else if (args2.length !== 0) {
|
|
3283
|
-
|
|
4001
|
+
jobsPage = parseWorkJobsArgs(args2, usage2);
|
|
3284
4002
|
}
|
|
3285
|
-
const client2 = await
|
|
4003
|
+
const client2 = await connectGatewaySocket(parsed.socket);
|
|
3286
4004
|
try {
|
|
3287
4005
|
if (command === "retire") {
|
|
3288
4006
|
const result = await client2.request("work.retire", {
|
|
@@ -3297,13 +4015,18 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3297
4015
|
console.log(result.retired ? `retired: ${result.sessionKey} session=${result.sessionId} closed=${result.closed}${forced}` : `not retired: ${result.reason}`);
|
|
3298
4016
|
}
|
|
3299
4017
|
} else {
|
|
3300
|
-
const result = await client2.request("work.jobs");
|
|
4018
|
+
const result = await client2.request("work.jobs", jobsPage);
|
|
3301
4019
|
for (const job of result.jobs) {
|
|
3302
4020
|
const name3 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
3303
4021
|
const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
|
|
3304
4022
|
const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
|
|
3305
4023
|
console.log(`${name3} ${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}` : ""}`);
|
|
3306
4024
|
}
|
|
4025
|
+
const shownEnd = result.offset + result.jobs.length;
|
|
4026
|
+
if (result.jobs.length === 0)
|
|
4027
|
+
console.error(`jobs: none at offset ${result.offset} of ${result.total}`);
|
|
4028
|
+
else if (result.offset > 0 || shownEnd < result.total)
|
|
4029
|
+
console.error(`jobs ${result.offset + 1}-${shownEnd} of ${result.total}${shownEnd < result.total ? ` (next: --offset ${shownEnd})` : ""}`);
|
|
3307
4030
|
}
|
|
3308
4031
|
} finally {
|
|
3309
4032
|
await client2.close();
|
|
@@ -3315,11 +4038,13 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3315
4038
|
const name = args2[0];
|
|
3316
4039
|
let cwd;
|
|
3317
4040
|
let resume = false;
|
|
4041
|
+
let repo;
|
|
4042
|
+
let base;
|
|
3318
4043
|
let model;
|
|
3319
4044
|
const textParts = [];
|
|
3320
4045
|
for (let i = 1;i < args2.length; i++) {
|
|
3321
4046
|
const arg = args2[i];
|
|
3322
|
-
if (arg === "--cwd" || arg === "--model" || arg === "--preset") {
|
|
4047
|
+
if (arg === "--cwd" || arg === "--repo" || arg === "--base" || arg === "--model" || arg === "--preset") {
|
|
3323
4048
|
const value = args2[++i];
|
|
3324
4049
|
if (!value?.trim() || value.startsWith("--"))
|
|
3325
4050
|
throw new Error(usage2);
|
|
@@ -3327,6 +4052,14 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3327
4052
|
if (cwd !== undefined || !isAbsolute(value))
|
|
3328
4053
|
throw new Error(usage2);
|
|
3329
4054
|
cwd = value;
|
|
4055
|
+
} else if (arg === "--repo") {
|
|
4056
|
+
if (repo !== undefined)
|
|
4057
|
+
throw new Error(usage2);
|
|
4058
|
+
repo = value;
|
|
4059
|
+
} else if (arg === "--base") {
|
|
4060
|
+
if (base !== undefined)
|
|
4061
|
+
throw new Error(usage2);
|
|
4062
|
+
base = value;
|
|
3330
4063
|
} else {
|
|
3331
4064
|
if (model !== undefined)
|
|
3332
4065
|
throw new Error(`${usage2}
|
|
@@ -3343,7 +4076,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3343
4076
|
const text = textParts.join(" ").trim();
|
|
3344
4077
|
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text)
|
|
3345
4078
|
throw new Error(usage2);
|
|
3346
|
-
const client = await
|
|
4079
|
+
const client = await connectGatewaySocket(parsed.socket, {
|
|
3347
4080
|
requestTimeoutMs: command === "run" ? 3600000 : 120000
|
|
3348
4081
|
});
|
|
3349
4082
|
try {
|
|
@@ -3352,6 +4085,8 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3352
4085
|
name,
|
|
3353
4086
|
text,
|
|
3354
4087
|
...cwd ? { cwd } : {},
|
|
4088
|
+
...repo ? { repo } : {},
|
|
4089
|
+
...base ? { base } : {},
|
|
3355
4090
|
...resume ? { resume: true } : {},
|
|
3356
4091
|
...model === undefined ? {} : { model },
|
|
3357
4092
|
...callerSessionId ? { callerSessionId } : {}
|
|
@@ -3430,10 +4165,11 @@ export {
|
|
|
3430
4165
|
main,
|
|
3431
4166
|
parseArgs,
|
|
3432
4167
|
parseServicesArgs,
|
|
4168
|
+
pollMemoryOp,
|
|
3433
4169
|
renderCycle,
|
|
3434
|
-
|
|
3435
|
-
|
|
4170
|
+
renderDeliveries,
|
|
4171
|
+
renderSlo,
|
|
4172
|
+
sloExitCode,
|
|
3436
4173
|
socketPath,
|
|
3437
|
-
usageFor
|
|
3438
|
-
verifyBackupIntegrity
|
|
4174
|
+
usageFor
|
|
3439
4175
|
};
|