@gajae-gateway/cli 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +6 -32
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1195 -580
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +21 -11
- 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 +178 -142
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +258 -90
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +170 -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.0",
|
|
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";
|
|
150
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
|
+
}
|
|
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)
|
|
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)
|
|
1472
1784
|
return;
|
|
1473
|
-
|
|
1474
|
-
const args = await capture(["ps", "-ww", "-o", "args=", "-p", String(pid)]);
|
|
1475
|
-
if (lstart === undefined || args === undefined)
|
|
1476
|
-
return;
|
|
1477
|
-
const startedAt = Date.parse(lstart.trim());
|
|
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,14 @@ 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
|
-
const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now());
|
|
1882
|
+
const receipt = existing?.id === options.id && existing.state === "queued" ? existing : newReceipt(options.id, platform, options.uid, now(), options.trigger ?? "manual");
|
|
1537
1883
|
receipt.state = "running";
|
|
1538
1884
|
receipt.supervisorPid = process.pid;
|
|
1539
1885
|
await writeReceipt(options.home, receipt);
|
|
@@ -1544,6 +1890,12 @@ async function runRestartStack(options) {
|
|
|
1544
1890
|
continue;
|
|
1545
1891
|
}
|
|
1546
1892
|
try {
|
|
1893
|
+
if (step.label === "dev.gajaeway.broker" && !await Bun.file(homeLayout(options.home).brokerProvisionManifest).exists()) {
|
|
1894
|
+
step.result = "skipped";
|
|
1895
|
+
step.detail = "broker agent is not provisioned";
|
|
1896
|
+
await writeReceipt(options.home, receipt);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1547
1899
|
const installed = await isInstalled(step.label);
|
|
1548
1900
|
if (!installed) {
|
|
1549
1901
|
step.result = "skipped";
|
|
@@ -1551,17 +1903,68 @@ async function runRestartStack(options) {
|
|
|
1551
1903
|
await writeReceipt(options.home, receipt);
|
|
1552
1904
|
continue;
|
|
1553
1905
|
}
|
|
1554
|
-
if (step.
|
|
1555
|
-
|
|
1556
|
-
const
|
|
1557
|
-
step.
|
|
1558
|
-
|
|
1906
|
+
if (step.label === "dev.gajaeway.broker" && receipt.trigger === "update") {
|
|
1907
|
+
const stopCommand = serviceStopCommand(platform, options.uid);
|
|
1908
|
+
const stopStatus = await runner(stopCommand);
|
|
1909
|
+
if (stopStatus !== 0 && !(platform === "darwin" && await probe(step.label) === undefined))
|
|
1910
|
+
throw new Error(`${stopCommand.join(" ")} exited with status ${stopStatus}`);
|
|
1911
|
+
const provisionCommand = [
|
|
1912
|
+
join7(binDir, "gajaeway-gateway"),
|
|
1913
|
+
"broker-agent",
|
|
1914
|
+
"provision",
|
|
1915
|
+
"--mode",
|
|
1916
|
+
"update",
|
|
1917
|
+
"--lease-wait-ms",
|
|
1918
|
+
"35000"
|
|
1919
|
+
];
|
|
1920
|
+
const provisionStatus = await runner(provisionCommand);
|
|
1921
|
+
step.agentSync = {
|
|
1922
|
+
command: provisionCommand,
|
|
1923
|
+
exitStatus: provisionStatus,
|
|
1924
|
+
...provisionStatus === 1 || provisionStatus === 3 ? { error: `broker-agent provision exited with status ${provisionStatus}` } : provisionStatus === 4 ? { error: "broker_agent_unrecoverable" } : {}
|
|
1925
|
+
};
|
|
1926
|
+
if (provisionStatus === 4) {
|
|
1927
|
+
delete step.command;
|
|
1928
|
+
step.exitStatus = 4;
|
|
1559
1929
|
step.result = "failed";
|
|
1560
|
-
step.detail =
|
|
1930
|
+
step.detail = "broker left stopped: broker_agent_unrecoverable; run gajaeway setup --broker-agent";
|
|
1931
|
+
} else if (provisionStatus === 0 || provisionStatus === 1 || provisionStatus === 3) {
|
|
1932
|
+
const startCommand = serviceBootstrapCommand(options, platform, step.label);
|
|
1933
|
+
step.command = startCommand;
|
|
1934
|
+
issuedAt = now();
|
|
1935
|
+
const startStatus = await runner(startCommand);
|
|
1936
|
+
step.exitStatus = startStatus;
|
|
1937
|
+
if (startStatus !== 0) {
|
|
1938
|
+
step.result = "failed";
|
|
1939
|
+
step.detail = `${startCommand.join(" ")} exited with status ${startStatus}`;
|
|
1940
|
+
} else
|
|
1941
|
+
await verifyStep(step, issuedAt);
|
|
1942
|
+
} else {
|
|
1943
|
+
delete step.command;
|
|
1944
|
+
step.exitStatus = provisionStatus;
|
|
1945
|
+
step.result = "failed";
|
|
1946
|
+
step.detail = `broker-agent provision exited with unexpected status ${provisionStatus}`;
|
|
1947
|
+
}
|
|
1948
|
+
} else {
|
|
1949
|
+
if (step.command !== undefined) {
|
|
1950
|
+
issuedAt = now();
|
|
1951
|
+
let command = step.command;
|
|
1952
|
+
let status = await runner(command);
|
|
1953
|
+
if (status !== 0 && receipt.trigger === "update" && platform === "darwin" && await probe(step.label) === undefined) {
|
|
1954
|
+
command = serviceBootstrapCommand(options, platform, step.label);
|
|
1955
|
+
step.command = command;
|
|
1956
|
+
issuedAt = now();
|
|
1957
|
+
status = await runner(command);
|
|
1958
|
+
}
|
|
1959
|
+
step.exitStatus = status;
|
|
1960
|
+
if (status !== 0) {
|
|
1961
|
+
step.result = "failed";
|
|
1962
|
+
step.detail = `${command.join(" ")} exited with status ${status}`;
|
|
1963
|
+
}
|
|
1561
1964
|
}
|
|
1965
|
+
if (step.result === "pending")
|
|
1966
|
+
await verifyStep(step, issuedAt);
|
|
1562
1967
|
}
|
|
1563
|
-
if (step.result === "pending")
|
|
1564
|
-
await verifyStep(step, issuedAt);
|
|
1565
1968
|
} catch (error) {
|
|
1566
1969
|
step.result = "failed";
|
|
1567
1970
|
step.detail = error instanceof Error ? error.message : String(error);
|
|
@@ -1577,29 +1980,53 @@ async function runRestartStack(options) {
|
|
|
1577
1980
|
await writeReceipt(options.home, receipt);
|
|
1578
1981
|
return receipt;
|
|
1579
1982
|
async function verifyStep(step, since) {
|
|
1983
|
+
const spec = serviceSpecs().find((candidate) => candidate.label === step.label);
|
|
1984
|
+
if (spec === undefined)
|
|
1985
|
+
throw new Error(`unknown service label: ${step.label}`);
|
|
1986
|
+
const leaseName = SERVICE_LEASES[spec.id];
|
|
1987
|
+
const binary = join7(binDir, spec.binary);
|
|
1988
|
+
const binaryModifiedAt = leaseName === undefined ? undefined : await modifiedAt(binary);
|
|
1580
1989
|
const deadline = now() + timeoutMs;
|
|
1581
1990
|
for (;; ) {
|
|
1582
1991
|
const running = await probe(step.label);
|
|
1583
|
-
|
|
1584
|
-
if (running !== undefined)
|
|
1585
|
-
binaryModifiedAt = await modifiedAt(running.binary);
|
|
1992
|
+
const owner = leaseName === undefined ? undefined : await readLeaseOwner(options.home, leaseName);
|
|
1993
|
+
if (running !== undefined)
|
|
1586
1994
|
step.pid = running.pid;
|
|
1587
|
-
|
|
1588
|
-
step.binary =
|
|
1995
|
+
if (leaseName !== undefined) {
|
|
1996
|
+
step.binary = binary;
|
|
1589
1997
|
step.binaryModifiedAt = new Date(binaryModifiedAt).toISOString();
|
|
1590
|
-
if (
|
|
1998
|
+
if (owner !== undefined)
|
|
1999
|
+
step.processStartedAt = owner.startedAt;
|
|
2000
|
+
const startedAt = owner === undefined ? Number.NaN : Date.parse(owner.startedAt);
|
|
2001
|
+
if (running !== undefined && owner !== undefined && running.pid === owner.pid && Number.isFinite(startedAt) && startedAt >= Math.max(since, binaryModifiedAt)) {
|
|
1591
2002
|
step.result = "ok";
|
|
1592
2003
|
delete step.detail;
|
|
1593
2004
|
return;
|
|
1594
2005
|
}
|
|
2006
|
+
} else if (running !== undefined && running.pid !== originalPids.get(step.label)) {
|
|
2007
|
+
step.result = "ok";
|
|
2008
|
+
delete step.detail;
|
|
2009
|
+
return;
|
|
1595
2010
|
}
|
|
1596
2011
|
if (now() >= deadline) {
|
|
1597
2012
|
if (running === undefined) {
|
|
1598
2013
|
step.result = "failed";
|
|
1599
2014
|
step.detail = "no running process after restart";
|
|
2015
|
+
} else if (leaseName === undefined) {
|
|
2016
|
+
step.result = "stale";
|
|
2017
|
+
step.detail = "MainPID did not change after restart";
|
|
2018
|
+
} else if (owner === undefined) {
|
|
2019
|
+
step.result = "stale";
|
|
2020
|
+
step.detail = "no lease owner after restart";
|
|
2021
|
+
} else if (running.pid !== owner.pid) {
|
|
2022
|
+
step.result = "stale";
|
|
2023
|
+
step.detail = "lease owner PID does not match service MainPID";
|
|
2024
|
+
} else if (Date.parse(owner.startedAt) < binaryModifiedAt) {
|
|
2025
|
+
step.result = "stale";
|
|
2026
|
+
step.detail = "process started before the deployed binary was modified";
|
|
1600
2027
|
} else {
|
|
1601
2028
|
step.result = "stale";
|
|
1602
|
-
step.detail =
|
|
2029
|
+
step.detail = "lease owner predates this restart sequence";
|
|
1603
2030
|
}
|
|
1604
2031
|
return;
|
|
1605
2032
|
}
|
|
@@ -1610,35 +2037,40 @@ async function runRestartStack(options) {
|
|
|
1610
2037
|
function cliCommand() {
|
|
1611
2038
|
return Bun.main.startsWith("/$bunfs/") ? [process.execPath] : [process.execPath, Bun.main];
|
|
1612
2039
|
}
|
|
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
2040
|
async function launchRestartStack(options) {
|
|
1624
2041
|
const platform = options.platform ?? currentPlatform();
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
const receipt = newReceipt(randomUUID(), platform, options.uid, Date.now());
|
|
2042
|
+
if (await leaseHeld(options.home, "update"))
|
|
2043
|
+
throw new LeaseHeldError("update", await readLeaseOwner(options.home, "update"));
|
|
2044
|
+
const receipt = newReceipt(randomUUID2(), platform, options.uid, Date.now(), options.trigger ?? "manual");
|
|
1629
2045
|
await writeReceipt(options.home, receipt);
|
|
1630
2046
|
const worker = [...options.worker ?? [...cliCommand(), "ops", "restart-stack"], "--run", receipt.id];
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2047
|
+
const argv = supervisorArgv(platform, worker, options.home, receipt.id, options.binDir);
|
|
2048
|
+
let child;
|
|
2049
|
+
try {
|
|
2050
|
+
child = Bun.spawn(argv, {
|
|
2051
|
+
detached: true,
|
|
2052
|
+
stdin: "ignore",
|
|
2053
|
+
stdout: "ignore",
|
|
2054
|
+
stderr: "ignore",
|
|
2055
|
+
env: {
|
|
2056
|
+
...options.env ?? process.env,
|
|
2057
|
+
GAJAEWAY_HOME: options.home,
|
|
2058
|
+
...options.binDir === undefined ? {} : { GAJAEWAY_BIN_DIR: options.binDir }
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
receipt.state = "failed";
|
|
2063
|
+
receipt.finishedAt = new Date().toISOString();
|
|
2064
|
+
receipt.detail = `supervisor ${argv[0]} could not be started (${error instanceof Error ? error.message : String(error)}); without a service manager here, restart through the host's own supervisor`;
|
|
2065
|
+
for (const step of receipt.steps)
|
|
2066
|
+
step.result = "skipped";
|
|
2067
|
+
await writeReceipt(options.home, receipt);
|
|
2068
|
+
throw new Error(`restart-stack ${receipt.id}: failed: ${receipt.detail}`);
|
|
2069
|
+
}
|
|
1638
2070
|
child.unref();
|
|
1639
2071
|
return { receipt, supervisorPid: child.pid };
|
|
1640
2072
|
}
|
|
1641
|
-
function supervisorArgv(platform, worker, home, id) {
|
|
2073
|
+
function supervisorArgv(platform, worker, home, id, binDir) {
|
|
1642
2074
|
if (platform === "darwin")
|
|
1643
2075
|
return [...worker];
|
|
1644
2076
|
return [
|
|
@@ -1648,26 +2080,29 @@ function supervisorArgv(platform, worker, home, id) {
|
|
|
1648
2080
|
"--quiet",
|
|
1649
2081
|
`--unit=gajaeway-restart-stack-${id}`,
|
|
1650
2082
|
`--setenv=GAJAEWAY_HOME=${home}`,
|
|
2083
|
+
...binDir === undefined ? [] : [`--setenv=GAJAEWAY_BIN_DIR=${binDir}`],
|
|
1651
2084
|
"--",
|
|
1652
2085
|
...worker
|
|
1653
2086
|
];
|
|
1654
2087
|
}
|
|
1655
|
-
function effectiveRestartState(receipt) {
|
|
1656
|
-
if (receipt.state === "running" && !
|
|
2088
|
+
async function effectiveRestartState(receipt, home) {
|
|
2089
|
+
if (receipt.state === "running" && !await leaseHeld(home, "update"))
|
|
1657
2090
|
return "interrupted";
|
|
1658
2091
|
return receipt.state;
|
|
1659
2092
|
}
|
|
1660
|
-
function renderRestartReceipt(receipt) {
|
|
2093
|
+
function renderRestartReceipt(receipt, state = receipt.state) {
|
|
1661
2094
|
const lines = [
|
|
1662
|
-
`restart-stack ${receipt.id}: ${
|
|
2095
|
+
`restart-stack ${receipt.id}: ${state}`,
|
|
1663
2096
|
`requestedAt: ${receipt.requestedAt}`,
|
|
1664
|
-
`finishedAt: ${receipt.finishedAt ?? "-"}
|
|
2097
|
+
`finishedAt: ${receipt.finishedAt ?? "-"}`,
|
|
2098
|
+
...receipt.detail === undefined ? [] : [`detail: ${receipt.detail}`]
|
|
1665
2099
|
];
|
|
1666
2100
|
for (const step of receipt.steps) {
|
|
1667
2101
|
const facts = [
|
|
1668
2102
|
step.pid === undefined ? undefined : `pid=${step.pid}`,
|
|
1669
2103
|
step.processStartedAt === undefined ? undefined : `started=${step.processStartedAt}`,
|
|
1670
2104
|
step.binaryModifiedAt === undefined ? undefined : `binary=${step.binaryModifiedAt}`,
|
|
2105
|
+
step.agentSync?.error === undefined ? undefined : `agentSync.error=${step.agentSync.error}`,
|
|
1671
2106
|
step.detail
|
|
1672
2107
|
].filter((fact) => fact !== undefined);
|
|
1673
2108
|
lines.push(`${step.label}: ${step.result}${facts.length > 0 ? ` ${facts.join(" ")}` : ""}`);
|
|
@@ -1676,9 +2111,74 @@ function renderRestartReceipt(receipt) {
|
|
|
1676
2111
|
}
|
|
1677
2112
|
|
|
1678
2113
|
// src/setup.ts
|
|
1679
|
-
import { randomUUID as
|
|
1680
|
-
import { chmod as
|
|
1681
|
-
import { join as
|
|
2114
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2115
|
+
import { chmod as chmod3, mkdir as mkdir5, readFile as readFile6, realpath, rename as rename3, rm as rm2, writeFile as writeFile4 } from "fs/promises";
|
|
2116
|
+
import { basename, dirname as dirname2, join as join8 } from "path";
|
|
2117
|
+
|
|
2118
|
+
// src/discord-setup.ts
|
|
2119
|
+
var DEVELOPER_PORTAL_URL = "https://discord.com/developers/applications";
|
|
2120
|
+
var DISCORD_BOT_SCOPES = ["bot", "applications.commands"];
|
|
2121
|
+
var DISCORD_BOT_PERMISSIONS = 2048 | 16384 | 33554432 | 65536 | 131072 | 8192 | 64;
|
|
2122
|
+
function printDiscordSetupGuide(log) {
|
|
2123
|
+
log("");
|
|
2124
|
+
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");
|
|
2125
|
+
log(" Getting your Discord bot token");
|
|
2126
|
+
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");
|
|
2127
|
+
log("");
|
|
2128
|
+
log("1. Open the Discord Developer Portal:");
|
|
2129
|
+
log(` ${DEVELOPER_PORTAL_URL}`);
|
|
2130
|
+
log("");
|
|
2131
|
+
log("2. Click 'New Application' and choose a name (e.g., 'My gajae-way agent')");
|
|
2132
|
+
log("");
|
|
2133
|
+
log("3. In the left menu, click 'Bot' \u2192 'Add Bot'");
|
|
2134
|
+
log("");
|
|
2135
|
+
log("4. Under 'TOKEN', click 'Reset Token' \u2192 'Yes, do it!'");
|
|
2136
|
+
log("");
|
|
2137
|
+
log("5. Click 'Copy' to copy your bot token (it starts with 'MTI...')");
|
|
2138
|
+
log("");
|
|
2139
|
+
log("6. (Important) Scroll down to 'GATEWAY INTENTS' and enable 'MESSAGE CONTENT INTENT'");
|
|
2140
|
+
log(" This allows the bot to read message text, not just usernames.");
|
|
2141
|
+
log("");
|
|
2142
|
+
log("7. Click 'Save Changes'");
|
|
2143
|
+
log("");
|
|
2144
|
+
log("8. Come back here and paste your token below");
|
|
2145
|
+
log("");
|
|
2146
|
+
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");
|
|
2147
|
+
log("");
|
|
2148
|
+
}
|
|
2149
|
+
function generateDiscordInviteUrl(appId) {
|
|
2150
|
+
const scopeParam = DISCORD_BOT_SCOPES.join("+");
|
|
2151
|
+
const permissionsParam = String(DISCORD_BOT_PERMISSIONS);
|
|
2152
|
+
return `https://discord.com/oauth2/authorize?client_id=${appId}&scope=${scopeParam}&permissions=${permissionsParam}`;
|
|
2153
|
+
}
|
|
2154
|
+
function printDiscordBotReady(log, appId, botName) {
|
|
2155
|
+
const inviteUrl = generateDiscordInviteUrl(appId);
|
|
2156
|
+
log("");
|
|
2157
|
+
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");
|
|
2158
|
+
log(" \u2713 Discord bot token validated");
|
|
2159
|
+
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");
|
|
2160
|
+
log("");
|
|
2161
|
+
log(`Application ID: ${appId}`);
|
|
2162
|
+
if (botName) {
|
|
2163
|
+
log(`Bot name: ${botName}`);
|
|
2164
|
+
}
|
|
2165
|
+
log("");
|
|
2166
|
+
log("To invite your bot to your Discord server, click this link:");
|
|
2167
|
+
log("");
|
|
2168
|
+
log(` ${inviteUrl}`);
|
|
2169
|
+
log("");
|
|
2170
|
+
log("Then:");
|
|
2171
|
+
log(" 1. Choose the server where you want to add the bot");
|
|
2172
|
+
log(" 2. Click 'Authorize'");
|
|
2173
|
+
log(" 3. Complete the CAPTCHA");
|
|
2174
|
+
log("");
|
|
2175
|
+
log("Once authorized, send the bot a DM and start chatting!");
|
|
2176
|
+
log("");
|
|
2177
|
+
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");
|
|
2178
|
+
log("");
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/setup.ts
|
|
1682
2182
|
var SETUP_PLATFORMS = ["discord", "slack", "telegram"];
|
|
1683
2183
|
var SETUP_ENV = {
|
|
1684
2184
|
discordToken: "GAJAEWAY_DISCORD_TOKEN",
|
|
@@ -1688,7 +2188,7 @@ var SETUP_ENV = {
|
|
|
1688
2188
|
telegramToken: "GAJAEWAY_TELEGRAM_TOKEN",
|
|
1689
2189
|
ownerIds: "GAJAEWAY_OWNER_IDS"
|
|
1690
2190
|
};
|
|
1691
|
-
var SETUP_USAGE = "usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status";
|
|
2191
|
+
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
2192
|
var SECRET_FILES2 = {
|
|
1693
2193
|
discord: "discord-token",
|
|
1694
2194
|
slackBot: "slack-bot-token",
|
|
@@ -1713,6 +2213,8 @@ function redact(text, secrets) {
|
|
|
1713
2213
|
function parseSetupArgs(args) {
|
|
1714
2214
|
let fromEnv = false;
|
|
1715
2215
|
let status = false;
|
|
2216
|
+
let brokerAgent = false;
|
|
2217
|
+
let from;
|
|
1716
2218
|
let adapters;
|
|
1717
2219
|
const owners = [];
|
|
1718
2220
|
let discordAppId;
|
|
@@ -1722,7 +2224,15 @@ function parseSetupArgs(args) {
|
|
|
1722
2224
|
fromEnv = true;
|
|
1723
2225
|
else if (flag === "--status")
|
|
1724
2226
|
status = true;
|
|
1725
|
-
else if (flag === "--
|
|
2227
|
+
else if (flag === "--broker-agent")
|
|
2228
|
+
brokerAgent = true;
|
|
2229
|
+
else if (flag === "--from") {
|
|
2230
|
+
const value = args[++i];
|
|
2231
|
+
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
2232
|
+
throw new SetupError(`${flag} expects a value
|
|
2233
|
+
${SETUP_USAGE}`);
|
|
2234
|
+
from = value;
|
|
2235
|
+
} else if (flag === "--adapters" || flag === "--owner" || flag === "--discord-app-id") {
|
|
1726
2236
|
const value = args[++i];
|
|
1727
2237
|
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
1728
2238
|
throw new SetupError(`${flag} expects a value
|
|
@@ -1739,15 +2249,22 @@ ${SETUP_USAGE}`);
|
|
|
1739
2249
|
throw new SetupError(`unknown option: ${flag.split("=")[0]}
|
|
1740
2250
|
${SETUP_USAGE}`);
|
|
1741
2251
|
}
|
|
1742
|
-
if (status && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
2252
|
+
if (status && (fromEnv || brokerAgent || from !== undefined || adapters || owners.length > 0 || discordAppId))
|
|
1743
2253
|
throw new SetupError(`--status takes no other options
|
|
2254
|
+
${SETUP_USAGE}`);
|
|
2255
|
+
if (brokerAgent && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
2256
|
+
throw new SetupError(`--broker-agent is standalone and cannot be combined with adapter setup options
|
|
2257
|
+
${SETUP_USAGE}`);
|
|
2258
|
+
if (from !== undefined && !brokerAgent)
|
|
2259
|
+
throw new SetupError(`--from requires --broker-agent
|
|
1744
2260
|
${SETUP_USAGE}`);
|
|
1745
2261
|
return {
|
|
1746
|
-
mode: status ? "status" : "setup",
|
|
2262
|
+
mode: status ? "status" : brokerAgent ? "broker-agent" : "setup",
|
|
1747
2263
|
fromEnv,
|
|
1748
2264
|
...adapters ? { adapters } : {},
|
|
1749
2265
|
owners,
|
|
1750
|
-
...discordAppId ? { discordAppId } : {}
|
|
2266
|
+
...discordAppId ? { discordAppId } : {},
|
|
2267
|
+
...from === undefined ? {} : { from }
|
|
1751
2268
|
};
|
|
1752
2269
|
}
|
|
1753
2270
|
function splitList(value) {
|
|
@@ -1845,7 +2362,8 @@ async function validateDiscord(credentials, fetcher) {
|
|
|
1845
2362
|
const ownerId = !hasTeam && typeof owner?.id === "string" ? owner.id : undefined;
|
|
1846
2363
|
return {
|
|
1847
2364
|
platform: "discord",
|
|
1848
|
-
summary: `bot ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
2365
|
+
summary: `bot named ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
2366
|
+
appId,
|
|
1849
2367
|
...ownerId ? { ownerId } : {}
|
|
1850
2368
|
};
|
|
1851
2369
|
}
|
|
@@ -1899,19 +2417,19 @@ async function validateAll(tokens, fetcher) {
|
|
|
1899
2417
|
return identities;
|
|
1900
2418
|
}
|
|
1901
2419
|
async function writePrivateFile(path, contents) {
|
|
1902
|
-
const temporary = `${path}.${
|
|
2420
|
+
const temporary = `${path}.${randomUUID3()}.tmp`;
|
|
1903
2421
|
try {
|
|
1904
2422
|
await writeFile4(temporary, contents, { flag: "wx", mode: 384 });
|
|
1905
|
-
await
|
|
1906
|
-
await
|
|
2423
|
+
await chmod3(temporary, 384);
|
|
2424
|
+
await rename3(temporary, path);
|
|
1907
2425
|
} finally {
|
|
1908
|
-
await
|
|
2426
|
+
await rm2(temporary, { force: true });
|
|
1909
2427
|
}
|
|
1910
2428
|
}
|
|
1911
2429
|
async function readJsonObject(path) {
|
|
1912
2430
|
let text;
|
|
1913
2431
|
try {
|
|
1914
|
-
text = await
|
|
2432
|
+
text = await readFile6(path, "utf8");
|
|
1915
2433
|
} catch (error) {
|
|
1916
2434
|
if (error.code === "ENOENT")
|
|
1917
2435
|
return;
|
|
@@ -1929,21 +2447,21 @@ async function readJsonObject(path) {
|
|
|
1929
2447
|
}
|
|
1930
2448
|
async function applySetup(options) {
|
|
1931
2449
|
const { home, tokens } = options;
|
|
1932
|
-
const secretsDir =
|
|
1933
|
-
const configPath =
|
|
1934
|
-
const adapterPath = (platform) =>
|
|
2450
|
+
const secretsDir = join8(home, "secrets");
|
|
2451
|
+
const configPath = join8(home, "config.json");
|
|
2452
|
+
const adapterPath = (platform) => join8(home, `adapter-${platform}.json`);
|
|
1935
2453
|
const existingConfig = await readJsonObject(configPath);
|
|
1936
2454
|
const existingAdapters = new Map;
|
|
1937
2455
|
for (const platform of SETUP_PLATFORMS)
|
|
1938
2456
|
if (tokens[platform])
|
|
1939
2457
|
existingAdapters.set(platform, await readJsonObject(adapterPath(platform)));
|
|
1940
|
-
await
|
|
1941
|
-
await
|
|
1942
|
-
await
|
|
1943
|
-
await
|
|
2458
|
+
await mkdir5(home, { recursive: true, mode: 448 });
|
|
2459
|
+
await chmod3(home, 448);
|
|
2460
|
+
await mkdir5(secretsDir, { recursive: true, mode: 448 });
|
|
2461
|
+
await chmod3(secretsDir, 448);
|
|
1944
2462
|
const written = [];
|
|
1945
2463
|
const secret = async (name, value) => {
|
|
1946
|
-
const path =
|
|
2464
|
+
const path = join8(secretsDir, name);
|
|
1947
2465
|
await writePrivateFile(path, `${value}
|
|
1948
2466
|
`);
|
|
1949
2467
|
written.push(path);
|
|
@@ -1990,7 +2508,7 @@ async function applySetup(options) {
|
|
|
1990
2508
|
async function enabledAdapters(home) {
|
|
1991
2509
|
const enabled = [];
|
|
1992
2510
|
for (const platform of SETUP_PLATFORMS) {
|
|
1993
|
-
if (await Bun.file(
|
|
2511
|
+
if (await Bun.file(join8(home, `adapter-${platform}.json`)).exists())
|
|
1994
2512
|
enabled.push(platform);
|
|
1995
2513
|
}
|
|
1996
2514
|
return enabled;
|
|
@@ -2011,12 +2529,14 @@ async function askRequiredSecret(prompter, question) {
|
|
|
2011
2529
|
return answer;
|
|
2012
2530
|
}
|
|
2013
2531
|
}
|
|
2014
|
-
async function tokensFromPrompts(prompter, parsed) {
|
|
2532
|
+
async function tokensFromPrompts(prompter, parsed, log) {
|
|
2015
2533
|
const wanted = parsed.adapters ?? [];
|
|
2016
2534
|
const enable = async (platform, label) => parsed.adapters ? wanted.includes(platform) : await askYes(prompter, `Enable ${label}?`);
|
|
2017
2535
|
const tokens = {};
|
|
2018
2536
|
if (await enable("discord", "Discord")) {
|
|
2019
|
-
const
|
|
2537
|
+
const logger = log ?? (() => {});
|
|
2538
|
+
printDiscordSetupGuide(logger);
|
|
2539
|
+
const token = await askRequiredSecret(prompter, "Paste your Discord bot token here (input hidden): ");
|
|
2020
2540
|
const appId = parsed.discordAppId ?? (await prompter.ask("Discord application id (optional): ")).trim();
|
|
2021
2541
|
tokens.discord = { token, ...appId ? { appId } : {} };
|
|
2022
2542
|
}
|
|
@@ -2085,6 +2605,28 @@ function terminalPrompter() {
|
|
|
2085
2605
|
close: () => stdin.pause()
|
|
2086
2606
|
};
|
|
2087
2607
|
}
|
|
2608
|
+
async function defaultBrokerAgentRunner(command) {
|
|
2609
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
2610
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2611
|
+
new Response(child.stdout).text(),
|
|
2612
|
+
new Response(child.stderr).text(),
|
|
2613
|
+
child.exited
|
|
2614
|
+
]);
|
|
2615
|
+
return { exitCode, stdout, stderr };
|
|
2616
|
+
}
|
|
2617
|
+
async function setupBinDir(options) {
|
|
2618
|
+
if (options.binDir !== undefined)
|
|
2619
|
+
return options.binDir;
|
|
2620
|
+
const env = options.env ?? process.env;
|
|
2621
|
+
if (env.GAJAEWAY_BIN_DIR)
|
|
2622
|
+
return env.GAJAEWAY_BIN_DIR;
|
|
2623
|
+
const executable = await realpath(process.execPath).catch(() => {
|
|
2624
|
+
return;
|
|
2625
|
+
});
|
|
2626
|
+
if (executable !== undefined && basename(executable).startsWith("gajaeway"))
|
|
2627
|
+
return dirname2(executable);
|
|
2628
|
+
throw new SetupError("setup --broker-agent requires the installed gajaeway binary or an explicit bin directory");
|
|
2629
|
+
}
|
|
2088
2630
|
async function runSetup(args, options) {
|
|
2089
2631
|
const log = options.log ?? ((line) => console.log(line));
|
|
2090
2632
|
const parsed = parseSetupArgs(args);
|
|
@@ -2092,6 +2634,26 @@ async function runSetup(args, options) {
|
|
|
2092
2634
|
log((await enabledAdapters(options.home)).join(","));
|
|
2093
2635
|
return;
|
|
2094
2636
|
}
|
|
2637
|
+
if (parsed.mode === "broker-agent") {
|
|
2638
|
+
const command = [
|
|
2639
|
+
join8(await setupBinDir(options), "gajaeway-gateway"),
|
|
2640
|
+
"broker-agent",
|
|
2641
|
+
"provision",
|
|
2642
|
+
"--mode",
|
|
2643
|
+
"force",
|
|
2644
|
+
...parsed.from === undefined ? [] : ["--from", parsed.from]
|
|
2645
|
+
];
|
|
2646
|
+
const result = await (options.runBrokerAgent ?? defaultBrokerAgentRunner)(command);
|
|
2647
|
+
if (result.stdout.length > 0)
|
|
2648
|
+
process.stdout.write(result.stdout);
|
|
2649
|
+
if (result.stderr.length > 0)
|
|
2650
|
+
process.stderr.write(result.stderr);
|
|
2651
|
+
if (result.exitCode === 1 && /lease_held name=broker/.test(`${result.stdout}
|
|
2652
|
+
${result.stderr}`))
|
|
2653
|
+
log("broker lease is held; stop it with: systemctl --user stop gajaeway-broker");
|
|
2654
|
+
process.exitCode = result.exitCode;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2095
2657
|
let tokens;
|
|
2096
2658
|
let owners;
|
|
2097
2659
|
if (parsed.fromEnv) {
|
|
@@ -2100,7 +2662,7 @@ async function runSetup(args, options) {
|
|
|
2100
2662
|
} else {
|
|
2101
2663
|
const prompter = (options.prompter ?? terminalPrompter)();
|
|
2102
2664
|
try {
|
|
2103
|
-
({ tokens, owners } = await tokensFromPrompts(prompter, parsed));
|
|
2665
|
+
({ tokens, owners } = await tokensFromPrompts(prompter, parsed, log));
|
|
2104
2666
|
} finally {
|
|
2105
2667
|
prompter.close();
|
|
2106
2668
|
}
|
|
@@ -2110,6 +2672,11 @@ async function runSetup(args, options) {
|
|
|
2110
2672
|
const identities = await validateAll(tokens, options.fetch ?? ((input, init) => fetch(input, init)));
|
|
2111
2673
|
for (const identity of identities)
|
|
2112
2674
|
log(`${identity.platform}: token valid \u2014 ${identity.summary}`);
|
|
2675
|
+
const discordIdentity = identities.find((i) => i.platform === "discord");
|
|
2676
|
+
if (discordIdentity && tokens.discord && discordIdentity.appId) {
|
|
2677
|
+
const botName = (discordIdentity.summary.match(/named (\S+)/) ?? [])[1];
|
|
2678
|
+
printDiscordBotReady(log, discordIdentity.appId, botName);
|
|
2679
|
+
}
|
|
2113
2680
|
for (const identity of identities)
|
|
2114
2681
|
if (identity.ownerId && owners.length === 0) {
|
|
2115
2682
|
owners.push(identity.ownerId);
|
|
@@ -2129,72 +2696,146 @@ async function runSetup(args, options) {
|
|
|
2129
2696
|
|
|
2130
2697
|
// src/update.ts
|
|
2131
2698
|
import {
|
|
2132
|
-
chmod as
|
|
2699
|
+
chmod as chmod5,
|
|
2133
2700
|
copyFile,
|
|
2134
|
-
mkdir as
|
|
2701
|
+
mkdir as mkdir7,
|
|
2702
|
+
mkdtemp as mkdtemp2,
|
|
2703
|
+
readdir as readdir3,
|
|
2704
|
+
readFile as readFile7,
|
|
2705
|
+
realpath as realpath2,
|
|
2706
|
+
rename as rename5,
|
|
2707
|
+
rm as rm4,
|
|
2708
|
+
stat as stat2,
|
|
2709
|
+
writeFile as writeFile6
|
|
2710
|
+
} from "fs/promises";
|
|
2711
|
+
import { tmpdir } from "os";
|
|
2712
|
+
import { basename as basename2, dirname as dirname3, join as join10 } from "path";
|
|
2713
|
+
|
|
2714
|
+
// src/gjc-install.ts
|
|
2715
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2716
|
+
import {
|
|
2717
|
+
access as access2,
|
|
2718
|
+
chmod as chmod4,
|
|
2719
|
+
lstat as lstat2,
|
|
2720
|
+
mkdir as mkdir6,
|
|
2135
2721
|
mkdtemp,
|
|
2136
|
-
open,
|
|
2137
2722
|
readdir as readdir2,
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
stat as stat2,
|
|
2723
|
+
readlink as readlink2,
|
|
2724
|
+
rename as rename4,
|
|
2725
|
+
rm as rm3,
|
|
2726
|
+
symlink,
|
|
2143
2727
|
writeFile as writeFile5
|
|
2144
2728
|
} 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
|
-
|
|
2729
|
+
import { join as join9 } from "path";
|
|
2730
|
+
var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
2731
|
+
async function defaultRunner(command, cwd) {
|
|
2732
|
+
const child = Bun.spawn([...command], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
2733
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2734
|
+
new Response(child.stdout).text(),
|
|
2735
|
+
new Response(child.stderr).text(),
|
|
2736
|
+
child.exited
|
|
2737
|
+
]);
|
|
2738
|
+
return { exitCode, stdout, stderr };
|
|
2739
|
+
}
|
|
2740
|
+
function assertSuccessful(command, result) {
|
|
2741
|
+
if (result.exitCode !== 0) {
|
|
2742
|
+
const detail = result.stderr.trim();
|
|
2743
|
+
throw new Error(`${command.join(" ")} exited with status ${result.exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function versionFromOutput(output) {
|
|
2747
|
+
return output.match(/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/)?.[0];
|
|
2748
|
+
}
|
|
2749
|
+
async function verifyGjc(path, cwd, version, runner) {
|
|
2750
|
+
await access2(path);
|
|
2751
|
+
const command = [path, "--version"];
|
|
2752
|
+
const result = await runner(command, cwd);
|
|
2753
|
+
assertSuccessful(command, result);
|
|
2754
|
+
const reported = versionFromOutput(`${result.stdout}
|
|
2755
|
+
${result.stderr}`);
|
|
2756
|
+
if (reported !== version)
|
|
2757
|
+
throw new Error(`gjc version verification failed: expected ${version}, got ${reported ?? "unknown"}`);
|
|
2758
|
+
}
|
|
2759
|
+
async function currentVersion(prefix, current) {
|
|
2760
|
+
try {
|
|
2761
|
+
const target = await readlink2(current);
|
|
2762
|
+
if (target.includes("/") || !VERSION_PATTERN.test(target))
|
|
2763
|
+
return;
|
|
2764
|
+
const targetPath = join9(prefix, target);
|
|
2765
|
+
if (!(await lstat2(targetPath)).isDirectory())
|
|
2766
|
+
return;
|
|
2767
|
+
return target;
|
|
2768
|
+
} catch {
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
async function pruneVersions(prefix, keep) {
|
|
2773
|
+
const retained = new Set(keep);
|
|
2774
|
+
const entries2 = await readdir2(prefix, { withFileTypes: true });
|
|
2775
|
+
const versions = entries2.filter((entry) => entry.isDirectory() && VERSION_PATTERN.test(entry.name)).map((entry) => entry.name).sort((left, right) => Bun.semver.order(right, left));
|
|
2776
|
+
for (const version of versions) {
|
|
2777
|
+
if (retained.size >= 2)
|
|
2778
|
+
break;
|
|
2779
|
+
retained.add(version);
|
|
2780
|
+
}
|
|
2781
|
+
for (const version of versions)
|
|
2782
|
+
if (!retained.has(version))
|
|
2783
|
+
await rm3(join9(prefix, version), { recursive: true, force: true });
|
|
2784
|
+
}
|
|
2785
|
+
async function installPinnedGjc(options) {
|
|
2786
|
+
const { home, version } = options;
|
|
2787
|
+
if (!VERSION_PATTERN.test(version))
|
|
2788
|
+
throw new Error(`invalid pinned gjc version: ${version}`);
|
|
2789
|
+
const layout = homeLayout(home);
|
|
2790
|
+
const runner = options.runner ?? defaultRunner;
|
|
2791
|
+
await mkdir6(layout.gjcPrefix, { recursive: true, mode: 448 });
|
|
2792
|
+
await chmod4(layout.gjcPrefix, 448);
|
|
2793
|
+
const versionDir = join9(layout.gjcPrefix, version);
|
|
2794
|
+
const previousVersion = await currentVersion(layout.gjcPrefix, layout.gjcCurrent);
|
|
2795
|
+
let staging;
|
|
2796
|
+
try {
|
|
2797
|
+
let installed = false;
|
|
2798
|
+
try {
|
|
2799
|
+
const info = await lstat2(versionDir);
|
|
2800
|
+
if (!info.isDirectory())
|
|
2801
|
+
throw new Error(`gjc version path is not a directory: ${versionDir}`);
|
|
2802
|
+
installed = true;
|
|
2803
|
+
} catch (error) {
|
|
2804
|
+
if (error.code !== "ENOENT")
|
|
2805
|
+
throw error;
|
|
2178
2806
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2807
|
+
if (!installed) {
|
|
2808
|
+
staging = await mkdtemp(join9(layout.gjcPrefix, ".staging-"));
|
|
2809
|
+
await chmod4(staging, 448);
|
|
2810
|
+
await writeFile5(join9(staging, "package.json"), `${JSON.stringify({ private: true, dependencies: { "gajae-code": version } }, null, 2)}
|
|
2811
|
+
`, { mode: 384 });
|
|
2812
|
+
const installCommand = ["bun", "install", "--production"];
|
|
2813
|
+
assertSuccessful(installCommand, await runner(installCommand, staging));
|
|
2814
|
+
const installedBin = join9(staging, "node_modules", ".bin", "gjc");
|
|
2815
|
+
await access2(installedBin);
|
|
2816
|
+
const binDir = join9(staging, "bin");
|
|
2817
|
+
await mkdir6(binDir, { mode: 448 });
|
|
2818
|
+
await symlink("../node_modules/.bin/gjc", join9(binDir, "gjc"), "file");
|
|
2819
|
+
await verifyGjc(join9(binDir, "gjc"), staging, version, runner);
|
|
2820
|
+
await rename4(staging, versionDir);
|
|
2821
|
+
staging = undefined;
|
|
2822
|
+
} else {
|
|
2823
|
+
await verifyGjc(join9(versionDir, "bin", "gjc"), versionDir, version, runner);
|
|
2824
|
+
}
|
|
2825
|
+
await pruneVersions(layout.gjcPrefix, [version, ...previousVersion === undefined ? [] : [previousVersion]]);
|
|
2826
|
+
const temporaryLink = join9(layout.gjcPrefix, `.current-${randomUUID4()}`);
|
|
2827
|
+
try {
|
|
2828
|
+
await symlink(version, temporaryLink, "dir");
|
|
2829
|
+
await rename4(temporaryLink, layout.gjcCurrent);
|
|
2830
|
+
} finally {
|
|
2831
|
+
await rm3(temporaryLink, { force: true });
|
|
2832
|
+
}
|
|
2833
|
+
return layout.gjcExecutable();
|
|
2834
|
+
} finally {
|
|
2835
|
+
if (staging !== undefined)
|
|
2836
|
+
await rm3(staging, { recursive: true, force: true });
|
|
2196
2837
|
}
|
|
2197
|
-
}
|
|
2838
|
+
}
|
|
2198
2839
|
|
|
2199
2840
|
// src/update.ts
|
|
2200
2841
|
var UPDATE_USAGE = "usage: gajaeway update [--check] [--force] [--bin-dir DIR] [--no-restart]";
|
|
@@ -2262,13 +2903,12 @@ var CORE_BINARIES = ["gajaeway", "gajaeway-gateway"];
|
|
|
2262
2903
|
var MIN_BINARY_BYTES = 1048576;
|
|
2263
2904
|
var MAX_ASSET_BYTES = 512 * 1024 * 1024;
|
|
2264
2905
|
var RELEASE_MARKER = ".gajaeway-release";
|
|
2265
|
-
var LOCK_FILE = ".gajaeway-update.lock";
|
|
2266
2906
|
function releaseMarkerPath(binDir) {
|
|
2267
|
-
return
|
|
2907
|
+
return join10(binDir, RELEASE_MARKER);
|
|
2268
2908
|
}
|
|
2269
2909
|
async function readInstalledRelease(binDir) {
|
|
2270
2910
|
try {
|
|
2271
|
-
const version = releaseVersion(await
|
|
2911
|
+
const version = releaseVersion(await readFile7(releaseMarkerPath(binDir), "utf8"));
|
|
2272
2912
|
return version;
|
|
2273
2913
|
} catch {
|
|
2274
2914
|
return;
|
|
@@ -2277,12 +2917,12 @@ async function readInstalledRelease(binDir) {
|
|
|
2277
2917
|
async function resolveBinDir(flag, execPath = process.execPath) {
|
|
2278
2918
|
if (flag !== undefined)
|
|
2279
2919
|
return flag;
|
|
2280
|
-
const real = await
|
|
2920
|
+
const real = await realpath2(execPath).catch(() => {
|
|
2281
2921
|
return;
|
|
2282
2922
|
});
|
|
2283
2923
|
if (real === undefined)
|
|
2284
2924
|
return;
|
|
2285
|
-
return
|
|
2925
|
+
return basename2(real).startsWith("gajaeway") ? dirname3(real) : undefined;
|
|
2286
2926
|
}
|
|
2287
2927
|
function renderUpdate(result) {
|
|
2288
2928
|
const lines = [];
|
|
@@ -2302,8 +2942,71 @@ function renderUpdate(result) {
|
|
|
2302
2942
|
lines.push(`restart: queued ${result.restartQueued} \u2014 check with: gajaeway ops restart-stack --status`);
|
|
2303
2943
|
else if (result.restartSkipped)
|
|
2304
2944
|
lines.push("restart: skipped (--no-restart)");
|
|
2945
|
+
if (result.brokerNotEnabled)
|
|
2946
|
+
lines.push("broker unit installed, not enabled: run gajaeway setup --broker-agent");
|
|
2305
2947
|
return lines;
|
|
2306
2948
|
}
|
|
2949
|
+
async function readPinnedGjcVersion(path) {
|
|
2950
|
+
let parsed;
|
|
2951
|
+
try {
|
|
2952
|
+
parsed = JSON.parse(await readFile7(path, "utf8"));
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
throw new Error(`could not read pinned gjc version from ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2955
|
+
}
|
|
2956
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
2957
|
+
throw new Error(`invalid release package metadata at ${path}`);
|
|
2958
|
+
const gjc = parsed.gjc;
|
|
2959
|
+
if (typeof gjc !== "object" || gjc === null || Array.isArray(gjc))
|
|
2960
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
2961
|
+
const version = gjc.version;
|
|
2962
|
+
if (typeof version !== "string" || version.trim() === "")
|
|
2963
|
+
throw new Error(`release package metadata at ${path} has no gjc.version`);
|
|
2964
|
+
return version;
|
|
2965
|
+
}
|
|
2966
|
+
async function defaultUpdateCommandRunner(command) {
|
|
2967
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
|
|
2968
|
+
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
2969
|
+
if (exitCode !== 0) {
|
|
2970
|
+
const detail = stderr.trim();
|
|
2971
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
2972
|
+
}
|
|
2973
|
+
return exitCode;
|
|
2974
|
+
}
|
|
2975
|
+
async function installUpdatedServices(options) {
|
|
2976
|
+
const { home, binDir, platform, noRestart, deps } = options;
|
|
2977
|
+
const brokerProvisioned = await exists(homeLayout(home).brokerProvisionManifest);
|
|
2978
|
+
const env = { ...deps.env ?? process.env, GAJAEWAY_HOME: home };
|
|
2979
|
+
await installServices({
|
|
2980
|
+
binDir,
|
|
2981
|
+
platform,
|
|
2982
|
+
env,
|
|
2983
|
+
configuredAdaptersOnly: true,
|
|
2984
|
+
compareAndWrite: true,
|
|
2985
|
+
...deps.serviceOptions
|
|
2986
|
+
});
|
|
2987
|
+
if (platform === "linux") {
|
|
2988
|
+
const run = deps.serviceRunner ?? defaultUpdateCommandRunner;
|
|
2989
|
+
const execute = async (command) => {
|
|
2990
|
+
const exitCode = await run(command);
|
|
2991
|
+
if (exitCode !== 0)
|
|
2992
|
+
throw new Error(`${command.join(" ")} exited with status ${exitCode}`);
|
|
2993
|
+
};
|
|
2994
|
+
await execute(["systemctl", "--user", "daemon-reload"]);
|
|
2995
|
+
if (!noRestart) {
|
|
2996
|
+
const enableUnits = [];
|
|
2997
|
+
for (const spec of serviceSpecs()) {
|
|
2998
|
+
if (spec.id === "broker" && !brokerProvisioned)
|
|
2999
|
+
continue;
|
|
3000
|
+
if (spec.id.startsWith("adapter-") && !await Bun.file(join10(home, `adapter-${spec.id.slice(8)}.json`)).exists())
|
|
3001
|
+
continue;
|
|
3002
|
+
enableUnits.push(systemdUnitName(spec));
|
|
3003
|
+
}
|
|
3004
|
+
if (enableUnits.length > 0)
|
|
3005
|
+
await execute(["systemctl", "--user", "enable", ...enableUnits]);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return !brokerProvisioned;
|
|
3009
|
+
}
|
|
2307
3010
|
async function runUpdate(options) {
|
|
2308
3011
|
const deps = options.deps ?? {};
|
|
2309
3012
|
const platform = deps.platform ?? process.platform;
|
|
@@ -2326,46 +3029,66 @@ async function runUpdate(options) {
|
|
|
2326
3029
|
return { current, latest, tag: release.tag, repo, checkedOnly: true, upToDate: false, installed: [] };
|
|
2327
3030
|
if (binDir === undefined)
|
|
2328
3031
|
throw new Error("run the update from a compiled gajaeway binary, or pass --bin-dir DIR");
|
|
2329
|
-
|
|
3032
|
+
let lease;
|
|
2330
3033
|
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));
|
|
3034
|
+
lease = await acquireLease(options.home, "update");
|
|
3035
|
+
console.info(`lease_acquired name=update pid=${process.pid}`);
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
if (error instanceof LeaseHeldError) {
|
|
3038
|
+
console.error(error.message);
|
|
3039
|
+
throw new Error(`another gajaeway update is running (${error.message})`, { cause: error });
|
|
2352
3040
|
}
|
|
2353
|
-
|
|
3041
|
+
console.error(`lease_failed name=update detail=${error instanceof Error ? error.message : String(error)}`);
|
|
3042
|
+
throw error;
|
|
3043
|
+
}
|
|
3044
|
+
const installed = await (async () => {
|
|
3045
|
+
let staged;
|
|
3046
|
+
try {
|
|
3047
|
+
staged = await stageRelease(asset, deps);
|
|
3048
|
+
const installedNames = await knownBinariesIn(binDir);
|
|
3049
|
+
const toInstall = binariesToInstall(staged.names, installedNames);
|
|
3050
|
+
const missingCore = CORE_BINARIES.filter((name) => !toInstall.includes(name));
|
|
3051
|
+
if (missingCore.length > 0)
|
|
3052
|
+
throw new Error(`release tarball is missing core binaries: ${missingCore.join(", ")}`);
|
|
3053
|
+
await assertStagedBinariesUsable(staged, toInstall);
|
|
3054
|
+
const gjcVersion = await readPinnedGjcVersion(join10(staged.dir, "package.json"));
|
|
3055
|
+
await installPinnedGjc({
|
|
3056
|
+
home: options.home,
|
|
3057
|
+
version: gjcVersion,
|
|
3058
|
+
...deps.gjcInstallRunner === undefined ? {} : { runner: deps.gjcInstallRunner }
|
|
3059
|
+
});
|
|
3060
|
+
const stamp = new Date((deps.now ?? Date.now)()).toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
3061
|
+
const backupDir = join10(binDir, `backup-${current}-${stamp}Z`);
|
|
3062
|
+
await mkdir7(backupDir, { recursive: false });
|
|
3063
|
+
for (const name of toInstall) {
|
|
3064
|
+
const target = join10(binDir, name);
|
|
3065
|
+
if (await exists(target))
|
|
3066
|
+
await copyFile(target, join10(backupDir, name));
|
|
3067
|
+
}
|
|
3068
|
+
for (const name of toInstall) {
|
|
3069
|
+
const stagedPath = join10(staged.dir, name);
|
|
3070
|
+
const tempPath = join10(binDir, `.update-${name}`);
|
|
3071
|
+
await copyFile(stagedPath, tempPath);
|
|
3072
|
+
await chmod5(tempPath, 493);
|
|
3073
|
+
await rename5(tempPath, join10(binDir, name));
|
|
3074
|
+
}
|
|
3075
|
+
await writeFile6(releaseMarkerPath(binDir), `${latest}
|
|
2354
3076
|
`, "utf8");
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
3077
|
+
const brokerNotEnabled = await installUpdatedServices({
|
|
3078
|
+
home: options.home,
|
|
3079
|
+
binDir,
|
|
3080
|
+
platform,
|
|
3081
|
+
noRestart: options.args.noRestart,
|
|
3082
|
+
deps
|
|
3083
|
+
});
|
|
3084
|
+
return { toInstall, backupDir, brokerNotEnabled };
|
|
3085
|
+
} finally {
|
|
3086
|
+
if (staged !== undefined)
|
|
3087
|
+
await rm4(staged.dir, { recursive: true, force: true });
|
|
3088
|
+
await lease.release();
|
|
3089
|
+
}
|
|
3090
|
+
})();
|
|
3091
|
+
if (options.args.noRestart)
|
|
2369
3092
|
return {
|
|
2370
3093
|
current,
|
|
2371
3094
|
latest,
|
|
@@ -2373,26 +3096,37 @@ async function runUpdate(options) {
|
|
|
2373
3096
|
repo,
|
|
2374
3097
|
checkedOnly: false,
|
|
2375
3098
|
upToDate: false,
|
|
2376
|
-
installed: toInstall,
|
|
2377
|
-
backupDir,
|
|
2378
|
-
|
|
3099
|
+
installed: installed.toInstall,
|
|
3100
|
+
backupDir: installed.backupDir,
|
|
3101
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
3102
|
+
restartSkipped: true
|
|
2379
3103
|
};
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
3104
|
+
const receipt = await (deps.queueRestart ?? defaultQueueRestart)(options.home, binDir, "update");
|
|
3105
|
+
return {
|
|
3106
|
+
current,
|
|
3107
|
+
latest,
|
|
3108
|
+
tag: release.tag,
|
|
3109
|
+
repo,
|
|
3110
|
+
checkedOnly: false,
|
|
3111
|
+
upToDate: false,
|
|
3112
|
+
installed: installed.toInstall,
|
|
3113
|
+
backupDir: installed.backupDir,
|
|
3114
|
+
brokerNotEnabled: installed.brokerNotEnabled,
|
|
3115
|
+
restartQueued: receipt.receiptId
|
|
3116
|
+
};
|
|
2383
3117
|
}
|
|
2384
3118
|
function binariesToInstall(stagedNames, installedNames) {
|
|
2385
3119
|
const installed = new Set(installedNames);
|
|
2386
3120
|
return KNOWN_BINARIES.filter((name) => stagedNames.includes(name) && (installed.has(name) || CORE_BINARIES.includes(name)));
|
|
2387
3121
|
}
|
|
2388
3122
|
async function knownBinariesIn(dir) {
|
|
2389
|
-
const entries2 = await
|
|
3123
|
+
const entries2 = await readdir3(dir).catch(() => []);
|
|
2390
3124
|
const names = new Set(entries2);
|
|
2391
3125
|
return KNOWN_BINARIES.filter((name) => names.has(name));
|
|
2392
3126
|
}
|
|
2393
3127
|
async function assertStagedBinariesUsable(staged, toInstall) {
|
|
2394
3128
|
for (const name of toInstall) {
|
|
2395
|
-
const info = await stat2(
|
|
3129
|
+
const info = await stat2(join10(staged.dir, name));
|
|
2396
3130
|
if (!info.isFile())
|
|
2397
3131
|
throw new Error(`staged ${name} is not a regular file`);
|
|
2398
3132
|
if (info.size < MIN_BINARY_BYTES)
|
|
@@ -2408,10 +3142,10 @@ async function exists(path) {
|
|
|
2408
3142
|
}
|
|
2409
3143
|
}
|
|
2410
3144
|
async function stageRelease(asset, deps) {
|
|
2411
|
-
const dir = await
|
|
3145
|
+
const dir = await mkdtemp2(join10(tmpdir(), "gajaeway-update-"));
|
|
2412
3146
|
const tarball = await (deps.downloadAsset ?? defaultDownloadAsset)(asset);
|
|
2413
3147
|
await (deps.extractTarball ?? defaultExtractTarball)(tarball, dir);
|
|
2414
|
-
const names = (await
|
|
3148
|
+
const names = (await readdir3(dir)).filter((name) => KNOWN_BINARIES.includes(name));
|
|
2415
3149
|
return { dir, names };
|
|
2416
3150
|
}
|
|
2417
3151
|
async function defaultFetchLatestRelease(repo) {
|
|
@@ -2437,8 +3171,8 @@ async function defaultDownloadAsset(asset) {
|
|
|
2437
3171
|
return bytes;
|
|
2438
3172
|
}
|
|
2439
3173
|
async function defaultExtractTarball(tarball, intoDir) {
|
|
2440
|
-
const tarballPath =
|
|
2441
|
-
await
|
|
3174
|
+
const tarballPath = join10(intoDir, "release.tar.gz");
|
|
3175
|
+
await writeFile6(tarballPath, tarball);
|
|
2442
3176
|
const process2 = Bun.spawn(["tar", "-xzf", tarballPath, "-C", intoDir], {
|
|
2443
3177
|
stdout: "ignore",
|
|
2444
3178
|
stderr: "pipe"
|
|
@@ -2446,186 +3180,19 @@ async function defaultExtractTarball(tarball, intoDir) {
|
|
|
2446
3180
|
const exitCode = await process2.exited;
|
|
2447
3181
|
if (exitCode !== 0) {
|
|
2448
3182
|
const detail = await new Response(process2.stderr).text();
|
|
2449
|
-
throw new Error(`extracting ${
|
|
3183
|
+
throw new Error(`extracting ${basename2(tarballPath)} failed (tar exit ${exitCode}): ${detail.trim()}`);
|
|
2450
3184
|
}
|
|
2451
|
-
await
|
|
3185
|
+
await rm4(tarballPath, { force: true });
|
|
2452
3186
|
}
|
|
2453
|
-
async function defaultQueueRestart(home) {
|
|
2454
|
-
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
3187
|
+
async function defaultQueueRestart(home, binDir, trigger) {
|
|
3188
|
+
const { receipt, supervisorPid } = await launchRestartStack({ home, binDir, trigger });
|
|
2455
3189
|
return { receiptId: receipt.id, supervisorPid };
|
|
2456
3190
|
}
|
|
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
3191
|
|
|
2628
3192
|
// src/main.ts
|
|
3193
|
+
async function connectGatewaySocket(path, options = {}) {
|
|
3194
|
+
return GajaewayClient.connectSocketWithBackoff(path, { ...options, deadlineMs: 30000 });
|
|
3195
|
+
}
|
|
2629
3196
|
function socketPath(home = process.env.GAJAEWAY_HOME) {
|
|
2630
3197
|
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
2631
3198
|
}
|
|
@@ -2644,7 +3211,7 @@ var COMMANDS = [
|
|
|
2644
3211
|
"update",
|
|
2645
3212
|
"setup"
|
|
2646
3213
|
];
|
|
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
|
|
3214
|
+
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 ... (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
3215
|
var USAGE_EXIT_CODE = 2;
|
|
2649
3216
|
function usageFor(command) {
|
|
2650
3217
|
return command !== undefined && COMMANDS.includes(command) ? undefined : CLI_USAGE;
|
|
@@ -2848,11 +3415,23 @@ function renderCycle(cycle) {
|
|
|
2848
3415
|
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
3416
|
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
3417
|
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)" : ""}`);
|
|
3418
|
+
if (cycle.memoryGitIndexLock) {
|
|
3419
|
+
const { path, ageMs } = cycle.memoryGitIndexLock;
|
|
3420
|
+
lines.push(`memory_git_index_lock: ${path} age=${Math.round(ageMs)}ms`);
|
|
3421
|
+
}
|
|
2851
3422
|
if (cycle.agentDisk) {
|
|
2852
3423
|
const { path, freeBytes, totalBytes } = cycle.agentDisk;
|
|
2853
3424
|
const gib = (bytes) => (bytes / 1024 ** 3).toFixed(1);
|
|
2854
3425
|
lines.push(freeBytes === null || totalBytes === null ? `agent_disk: ${path} unobservable` : `agent_disk: ${path} free=${gib(freeBytes)}GiB total=${gib(totalBytes)}GiB`);
|
|
2855
3426
|
}
|
|
3427
|
+
if (cycle.brokerAgent !== null) {
|
|
3428
|
+
lines.push(`broker_agent: ${cycle.brokerAgent.status}`);
|
|
3429
|
+
for (const entry of cycle.brokerAgent.entries)
|
|
3430
|
+
if (entry.state !== "in_sync")
|
|
3431
|
+
lines.push(`broker_agent ${entry.name}: kind=${entry.kind} state=${entry.state}`);
|
|
3432
|
+
}
|
|
3433
|
+
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";
|
|
3434
|
+
lines.push(`slo: seven-day=${sloState}`);
|
|
2856
3435
|
lines.push(`monitors: ${cycle.monitorEvents.length ? cycle.monitorEvents.map((m) => `${m.stage}=${m.count}`).join(" ") : "none"}`);
|
|
2857
3436
|
if (cycle.monitorAuthoringLost.length > 0)
|
|
2858
3437
|
lines.push(`monitor authoring lost: ${cycle.monitorAuthoringLost.map((m) => `${m.eventType}=${m.consecutive} (last ${m.lastFiredAt})`).join(" ")}`);
|
|
@@ -2870,82 +3449,110 @@ function renderCycle(cycle) {
|
|
|
2870
3449
|
function cycleExitCode(cycle) {
|
|
2871
3450
|
return cycle.gates.length > 0 ? 1 : 0;
|
|
2872
3451
|
}
|
|
2873
|
-
function
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
let pageCount;
|
|
2882
|
-
try {
|
|
2883
|
-
pageCount = database.query("PRAGMA page_count").get()?.page_count ?? 0;
|
|
2884
|
-
detail = database.query("PRAGMA integrity_check").get()?.integrity_check ?? "unknown";
|
|
2885
|
-
} catch (error) {
|
|
2886
|
-
throw new Error(`Backup is not a readable SQLite database: ${backupPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
2887
|
-
} finally {
|
|
2888
|
-
database.close();
|
|
2889
|
-
}
|
|
2890
|
-
if (pageCount === 0)
|
|
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})`);
|
|
3452
|
+
function renderSlo(result) {
|
|
3453
|
+
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";
|
|
3454
|
+
const lines = [message];
|
|
3455
|
+
for (const day of result.days)
|
|
3456
|
+
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}`);
|
|
3457
|
+
for (const window of result.windows)
|
|
3458
|
+
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}`);
|
|
3459
|
+
return lines;
|
|
2894
3460
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3461
|
+
function sloExitCode(result) {
|
|
3462
|
+
return result.ok ? 0 : 1;
|
|
3463
|
+
}
|
|
3464
|
+
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]";
|
|
3465
|
+
function parseOpsSloArgs(args) {
|
|
3466
|
+
let days = 7;
|
|
3467
|
+
let json = false;
|
|
3468
|
+
const seen = new Set;
|
|
3469
|
+
for (let index = 0;index < args.length; index++) {
|
|
3470
|
+
const flag = args[index];
|
|
3471
|
+
if (flag === "--json") {
|
|
3472
|
+
if (json)
|
|
3473
|
+
throw new Error(OPS_USAGE);
|
|
3474
|
+
json = true;
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (flag !== "--days" || seen.has(flag))
|
|
3478
|
+
throw new Error(OPS_USAGE);
|
|
3479
|
+
seen.add(flag);
|
|
3480
|
+
const value = args[++index];
|
|
3481
|
+
if (!value || !/^\d+$/.test(value))
|
|
3482
|
+
throw new Error(OPS_USAGE);
|
|
3483
|
+
days = Number(value);
|
|
3484
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > 3650)
|
|
3485
|
+
throw new Error(OPS_USAGE);
|
|
3486
|
+
}
|
|
3487
|
+
return { params: { days }, json };
|
|
3488
|
+
}
|
|
3489
|
+
function parseOpsDeliveriesArgs(args) {
|
|
3490
|
+
const params = {};
|
|
3491
|
+
const seen = new Set;
|
|
3492
|
+
let json = false;
|
|
3493
|
+
for (let index = 0;index < args.length; index++) {
|
|
3494
|
+
const flag = args[index];
|
|
3495
|
+
if (flag === "--json") {
|
|
3496
|
+
if (json)
|
|
3497
|
+
throw new Error(OPS_USAGE);
|
|
3498
|
+
json = true;
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
if (flag !== "--since" && flag !== "--state" && flag !== "--limit")
|
|
3502
|
+
throw new Error(OPS_USAGE);
|
|
3503
|
+
if (seen.has(flag))
|
|
3504
|
+
throw new Error(OPS_USAGE);
|
|
3505
|
+
seen.add(flag);
|
|
3506
|
+
const value = args[++index];
|
|
3507
|
+
if (!value || value.startsWith("--"))
|
|
3508
|
+
throw new Error(OPS_USAGE);
|
|
3509
|
+
if (flag === "--since") {
|
|
3510
|
+
if (!Number.isFinite(Date.parse(value)))
|
|
3511
|
+
throw new Error(OPS_USAGE);
|
|
3512
|
+
params.since = value;
|
|
3513
|
+
} else if (flag === "--state")
|
|
3514
|
+
params.state = value;
|
|
3515
|
+
else {
|
|
3516
|
+
if (!/^\d+$/.test(value))
|
|
3517
|
+
throw new Error(OPS_USAGE);
|
|
3518
|
+
const limit = Number(value);
|
|
3519
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
3520
|
+
throw new Error(OPS_USAGE);
|
|
3521
|
+
params.limit = limit;
|
|
3522
|
+
}
|
|
2913
3523
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
console.log(`Validated SQLite backup: ${backupPath}`);
|
|
2941
|
-
console.log(`Copied current database to: ${preservedPath}`);
|
|
2942
|
-
console.log(`Restored backup to: ${databasePath}`);
|
|
3524
|
+
return { params, json };
|
|
3525
|
+
}
|
|
3526
|
+
function parseWorkJobsArgs(args, usage) {
|
|
3527
|
+
const params = {};
|
|
3528
|
+
for (let index = 0;index < args.length; index++) {
|
|
3529
|
+
const flag = args[index];
|
|
3530
|
+
const value = args[++index];
|
|
3531
|
+
if (flag !== "--limit" && flag !== "--offset" || value === undefined || !/^\d+$/.test(value))
|
|
3532
|
+
throw new Error(usage);
|
|
3533
|
+
const key = flag === "--limit" ? "limit" : "offset";
|
|
3534
|
+
if (params[key] !== undefined)
|
|
3535
|
+
throw new Error(usage);
|
|
3536
|
+
const number = Number(value);
|
|
3537
|
+
if (!Number.isSafeInteger(number) || key === "limit" && (number < 1 || number > WORK_JOBS_MAX_LIMIT))
|
|
3538
|
+
throw new Error(usage);
|
|
3539
|
+
params[key] = number;
|
|
3540
|
+
}
|
|
3541
|
+
return params;
|
|
3542
|
+
}
|
|
3543
|
+
function renderDeliveries(result) {
|
|
3544
|
+
if (result.rows.length === 0)
|
|
3545
|
+
return ["deliveries: none"];
|
|
3546
|
+
return [
|
|
3547
|
+
`deliveries: ${result.rows.length}`,
|
|
3548
|
+
...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)}`)
|
|
3549
|
+
];
|
|
2943
3550
|
}
|
|
2944
3551
|
var CHAT_TURN_TIMEOUT_MS = 30 * 60000;
|
|
2945
3552
|
async function chat(socket) {
|
|
2946
3553
|
let client;
|
|
2947
3554
|
try {
|
|
2948
|
-
client = await
|
|
3555
|
+
client = await connectGatewaySocket(socket);
|
|
2949
3556
|
} catch {
|
|
2950
3557
|
console.error(`Unable to connect to gateway socket ${socket}. Start the daemon out-of-band first.`);
|
|
2951
3558
|
process.exitCode = 1;
|
|
@@ -3002,6 +3609,10 @@ async function chat(socket) {
|
|
|
3002
3609
|
}
|
|
3003
3610
|
async function main(args = process.argv.slice(2), options = {}) {
|
|
3004
3611
|
const parsed = parseArgs(args);
|
|
3612
|
+
if (parsed.command === "--version" && parsed.rest.length === 0) {
|
|
3613
|
+
console.log(package_default.version);
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3005
3616
|
const usage = usageFor(parsed.command);
|
|
3006
3617
|
if (usage !== undefined) {
|
|
3007
3618
|
console.error(usage);
|
|
@@ -3010,7 +3621,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3010
3621
|
try {
|
|
3011
3622
|
switch (parsed.command) {
|
|
3012
3623
|
case "status": {
|
|
3013
|
-
const client = await
|
|
3624
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3014
3625
|
try {
|
|
3015
3626
|
console.log(JSON.stringify(await client.status()));
|
|
3016
3627
|
} finally {
|
|
@@ -3019,7 +3630,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3019
3630
|
break;
|
|
3020
3631
|
}
|
|
3021
3632
|
case "shutdown": {
|
|
3022
|
-
const client = await
|
|
3633
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3023
3634
|
try {
|
|
3024
3635
|
await client.shutdown();
|
|
3025
3636
|
} finally {
|
|
@@ -3038,7 +3649,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3038
3649
|
break;
|
|
3039
3650
|
case "sessions": {
|
|
3040
3651
|
const listOptions = parsed.rest[0] === "list" ? parseListOptions(parsed.rest.slice(1), SESSION_COLUMNS) : undefined;
|
|
3041
|
-
const client = await
|
|
3652
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3042
3653
|
try {
|
|
3043
3654
|
const result = await client.request("session.list");
|
|
3044
3655
|
const [command, selector] = parsed.rest;
|
|
@@ -3073,30 +3684,12 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3073
3684
|
break;
|
|
3074
3685
|
}
|
|
3075
3686
|
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
|
-
}
|
|
3687
|
+
const [command, path] = parsed.rest;
|
|
3688
|
+
if (command === undefined || !["backup", "deliveries", "redeliver", "integrity", "slo", "cycle", "restart-stack"].includes(command))
|
|
3689
|
+
throw new Error(`unknown ops command: ${command ?? "(missing)"}
|
|
3690
|
+
${OPS_USAGE}`);
|
|
3691
|
+
const deliveries = command === "deliveries" ? parseOpsDeliveriesArgs(parsed.rest.slice(1)) : undefined;
|
|
3692
|
+
const slo = command === "slo" ? parseOpsSloArgs(parsed.rest.slice(1)) : undefined;
|
|
3100
3693
|
if (command === "restart-stack") {
|
|
3101
3694
|
const usage2 = "usage: gajaeway ops restart-stack [--status]";
|
|
3102
3695
|
const home = gatewayHome();
|
|
@@ -3115,9 +3708,10 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3115
3708
|
const receipt = await readRestartReceipt(home);
|
|
3116
3709
|
if (receipt === undefined)
|
|
3117
3710
|
throw new Error("no restart-stack receipt");
|
|
3118
|
-
|
|
3711
|
+
const state = await effectiveRestartState(receipt, home);
|
|
3712
|
+
for (const line of renderRestartReceipt(receipt, state))
|
|
3119
3713
|
console.log(line);
|
|
3120
|
-
if (
|
|
3714
|
+
if (state !== "ok")
|
|
3121
3715
|
process.exitCode = 1;
|
|
3122
3716
|
} else if (parsed.rest.length === 1) {
|
|
3123
3717
|
const { receipt, supervisorPid } = await launchRestartStack({ ...options.restartStack?.launch, home });
|
|
@@ -3136,11 +3730,18 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3136
3730
|
else
|
|
3137
3731
|
throw new Error("usage: gajaeway ops redeliver <deliveryId>|--since <iso>");
|
|
3138
3732
|
}
|
|
3139
|
-
const client = await
|
|
3733
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3140
3734
|
try {
|
|
3141
3735
|
if (command === "backup" && path)
|
|
3142
3736
|
console.log(JSON.stringify(await client.request("ops.backup", { path })));
|
|
3143
|
-
else if (
|
|
3737
|
+
else if (deliveries) {
|
|
3738
|
+
const result = await client.opsDeliveries(deliveries.params);
|
|
3739
|
+
if (deliveries.json)
|
|
3740
|
+
console.log(JSON.stringify(result));
|
|
3741
|
+
else
|
|
3742
|
+
for (const line of renderDeliveries(result))
|
|
3743
|
+
console.log(line);
|
|
3744
|
+
} else if (redeliverParams)
|
|
3144
3745
|
console.log(JSON.stringify(await client.request("ops.redeliver", redeliverParams)));
|
|
3145
3746
|
else if (command === "integrity")
|
|
3146
3747
|
console.log(JSON.stringify(await client.request("ops.integrity")));
|
|
@@ -3154,8 +3755,16 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3154
3755
|
for (const line of renderCycle(cycle))
|
|
3155
3756
|
console.log(line);
|
|
3156
3757
|
process.exitCode = cycleExitCode(cycle);
|
|
3758
|
+
} else if (slo) {
|
|
3759
|
+
const result = await client.opsSlo(slo.params);
|
|
3760
|
+
if (slo.json)
|
|
3761
|
+
console.log(JSON.stringify(result));
|
|
3762
|
+
else
|
|
3763
|
+
for (const line of renderSlo(result))
|
|
3764
|
+
console.log(line);
|
|
3765
|
+
process.exitCode = sloExitCode(result);
|
|
3157
3766
|
} else
|
|
3158
|
-
throw new Error(
|
|
3767
|
+
throw new Error(OPS_USAGE);
|
|
3159
3768
|
} finally {
|
|
3160
3769
|
await client.close();
|
|
3161
3770
|
}
|
|
@@ -3165,7 +3774,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3165
3774
|
const usage2 = "usage: gajaeway memory audit|memory autolink|memory search <query>";
|
|
3166
3775
|
if ((parsed.rest[0] === "audit" || parsed.rest[0] === "autolink") && parsed.rest.length > 1)
|
|
3167
3776
|
throw new Error(`${usage2} (unknown argument: ${parsed.rest[1]})`);
|
|
3168
|
-
const client = await
|
|
3777
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3169
3778
|
try {
|
|
3170
3779
|
if (parsed.rest[0] === "audit") {
|
|
3171
3780
|
const result = await client.request("memory.audit");
|
|
@@ -3188,7 +3797,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3188
3797
|
const listOptions = command === "list" ? parseListOptions(args2, MONITOR_COLUMNS) : undefined;
|
|
3189
3798
|
const update = command === "update" ? parseMonitorUpdateArgs(args2) : undefined;
|
|
3190
3799
|
const monitorTest = command === "test" ? parseMonitorTestArgs(args2) : undefined;
|
|
3191
|
-
const client = await
|
|
3800
|
+
const client = await connectGatewaySocket(parsed.socket);
|
|
3192
3801
|
try {
|
|
3193
3802
|
if (command === "add" && args2[0] === "--json" && args2[1])
|
|
3194
3803
|
console.log(JSON.stringify(await client.request("monitor.add", JSON.parse(args2[1]))));
|
|
@@ -3238,7 +3847,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3238
3847
|
const text2 = args2.slice(1).join(" ").trim();
|
|
3239
3848
|
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
3849
|
throw new Error(usage2);
|
|
3241
|
-
const client2 = await
|
|
3850
|
+
const client2 = await connectGatewaySocket(parsed.socket);
|
|
3242
3851
|
try {
|
|
3243
3852
|
if (command === "status") {
|
|
3244
3853
|
console.log(JSON.stringify(await client2.request("work.status", { name: name2 })));
|
|
@@ -3257,6 +3866,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3257
3866
|
let force = false;
|
|
3258
3867
|
let allDead = false;
|
|
3259
3868
|
let name2;
|
|
3869
|
+
let jobsPage;
|
|
3260
3870
|
if (command === "retire") {
|
|
3261
3871
|
for (let i = 0;i < args2.length; i++) {
|
|
3262
3872
|
const arg = args2[i];
|
|
@@ -3280,9 +3890,9 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3280
3890
|
if (allDead && name2)
|
|
3281
3891
|
throw new Error(usage2);
|
|
3282
3892
|
} else if (args2.length !== 0) {
|
|
3283
|
-
|
|
3893
|
+
jobsPage = parseWorkJobsArgs(args2, usage2);
|
|
3284
3894
|
}
|
|
3285
|
-
const client2 = await
|
|
3895
|
+
const client2 = await connectGatewaySocket(parsed.socket);
|
|
3286
3896
|
try {
|
|
3287
3897
|
if (command === "retire") {
|
|
3288
3898
|
const result = await client2.request("work.retire", {
|
|
@@ -3297,13 +3907,18 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3297
3907
|
console.log(result.retired ? `retired: ${result.sessionKey} session=${result.sessionId} closed=${result.closed}${forced}` : `not retired: ${result.reason}`);
|
|
3298
3908
|
}
|
|
3299
3909
|
} else {
|
|
3300
|
-
const result = await client2.request("work.jobs");
|
|
3910
|
+
const result = await client2.request("work.jobs", jobsPage);
|
|
3301
3911
|
for (const job of result.jobs) {
|
|
3302
3912
|
const name3 = job.lane_key.startsWith("work-") ? job.lane_key.slice("work-".length) : job.lane_key;
|
|
3303
3913
|
const state = job.quarantined ? `HELD: quarantined reason=${job.reason} historical_state=${job.state}` : job.state;
|
|
3304
3914
|
const head = job.last_commit ? `${job.last_commit.sha.slice(0, 7)}@${job.last_commit.committed_at} ${JSON.stringify(job.last_commit.subject)}` : "-";
|
|
3305
3915
|
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
3916
|
}
|
|
3917
|
+
const shownEnd = result.offset + result.jobs.length;
|
|
3918
|
+
if (result.jobs.length === 0)
|
|
3919
|
+
console.error(`jobs: none at offset ${result.offset} of ${result.total}`);
|
|
3920
|
+
else if (result.offset > 0 || shownEnd < result.total)
|
|
3921
|
+
console.error(`jobs ${result.offset + 1}-${shownEnd} of ${result.total}${shownEnd < result.total ? ` (next: --offset ${shownEnd})` : ""}`);
|
|
3307
3922
|
}
|
|
3308
3923
|
} finally {
|
|
3309
3924
|
await client2.close();
|
|
@@ -3343,7 +3958,7 @@ async function main(args = process.argv.slice(2), options = {}) {
|
|
|
3343
3958
|
const text = textParts.join(" ").trim();
|
|
3344
3959
|
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) || !text)
|
|
3345
3960
|
throw new Error(usage2);
|
|
3346
|
-
const client = await
|
|
3961
|
+
const client = await connectGatewaySocket(parsed.socket, {
|
|
3347
3962
|
requestTimeoutMs: command === "run" ? 3600000 : 120000
|
|
3348
3963
|
});
|
|
3349
3964
|
try {
|
|
@@ -3431,9 +4046,9 @@ export {
|
|
|
3431
4046
|
parseArgs,
|
|
3432
4047
|
parseServicesArgs,
|
|
3433
4048
|
renderCycle,
|
|
3434
|
-
|
|
3435
|
-
|
|
4049
|
+
renderDeliveries,
|
|
4050
|
+
renderSlo,
|
|
4051
|
+
sloExitCode,
|
|
3436
4052
|
socketPath,
|
|
3437
|
-
usageFor
|
|
3438
|
-
verifyBackupIntegrity
|
|
4053
|
+
usageFor
|
|
3439
4054
|
};
|