@gajae-gateway/cli 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/discord-setup.d.ts +41 -0
- package/dist/discord-setup.d.ts.map +1 -0
- package/dist/gjc-install.d.ts +13 -0
- package/dist/gjc-install.d.ts.map +1 -0
- package/dist/main.d.ts +17 -30
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1318 -582
- package/dist/migrate/hermes.d.ts.map +1 -1
- package/dist/migrate.d.ts.map +1 -1
- package/dist/restart-stack.d.ts +27 -13
- package/dist/restart-stack.d.ts.map +1 -1
- package/dist/services.d.ts +9 -6
- package/dist/services.d.ts.map +1 -1
- package/dist/setup.d.ts +14 -3
- package/dist/setup.d.ts.map +1 -1
- package/dist/up.d.ts +20 -0
- package/dist/up.d.ts.map +1 -0
- package/dist/update.d.ts +10 -6
- package/dist/update.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/discord-setup.ts +103 -0
- package/src/gjc-install.ts +148 -0
- package/src/main.ts +296 -151
- package/src/migrate/hermes.ts +0 -2
- package/src/migrate.ts +36 -4
- package/src/restart-stack.ts +309 -92
- package/src/services.ts +75 -23
- package/src/setup.ts +85 -10
- package/src/up.ts +133 -0
- package/src/update.ts +175 -98
- package/dist/upgrade.d.ts +0 -40
- package/dist/upgrade.d.ts.map +0 -1
- package/src/upgrade.ts +0 -205
package/src/services.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { homeLayout } from "@gajae-gateway/home";
|
|
4
5
|
import { parseRuntimeConfig, type RuntimeConfig, RuntimeConfigError } from "@gajae-gateway/protocol";
|
|
5
6
|
|
|
6
7
|
const CONFIG_SCHEMA_VERSION = 1;
|
|
@@ -34,6 +35,10 @@ export interface InstallServicesOptions {
|
|
|
34
35
|
/** Service manager to generate for; defaults to the running platform. */
|
|
35
36
|
readonly platform?: ServicePlatform;
|
|
36
37
|
readonly env?: NodeJS.ProcessEnv;
|
|
38
|
+
/** Render adapter units only when their adapter-<platform>.json exists. */
|
|
39
|
+
readonly configuredAdaptersOnly?: boolean;
|
|
40
|
+
/** Leave unchanged unit files untouched. */
|
|
41
|
+
readonly compareAndWrite?: boolean;
|
|
37
42
|
/** Test seam for the `${SHELL} -lc` invocation used by the default path. */
|
|
38
43
|
readonly loginPathRunner?: LoginPathRunner;
|
|
39
44
|
/** Test seam for counting or inspecting the definition writes. */
|
|
@@ -55,9 +60,17 @@ interface ServiceSpec {
|
|
|
55
60
|
readonly dependsOnGateway: boolean;
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
export const BROKER_UNIT = "gajaeway-broker.service";
|
|
58
64
|
export const GATEWAY_UNIT = "gajaeway-gateway.service";
|
|
59
65
|
|
|
60
66
|
const SERVICE_SPECS: readonly ServiceSpec[] = [
|
|
67
|
+
{
|
|
68
|
+
id: "broker",
|
|
69
|
+
label: "dev.gajaeway.broker",
|
|
70
|
+
binary: "gajaeway-gateway",
|
|
71
|
+
args: ["broker"],
|
|
72
|
+
dependsOnGateway: false,
|
|
73
|
+
},
|
|
61
74
|
{
|
|
62
75
|
id: "gateway",
|
|
63
76
|
label: "dev.gajaeway.gateway",
|
|
@@ -79,6 +92,13 @@ const SERVICE_SPECS: readonly ServiceSpec[] = [
|
|
|
79
92
|
args: [],
|
|
80
93
|
dependsOnGateway: true,
|
|
81
94
|
},
|
|
95
|
+
{
|
|
96
|
+
id: "adapter-telegram",
|
|
97
|
+
label: "dev.gajaeway.adapter-telegram",
|
|
98
|
+
binary: "gajaeway-telegram",
|
|
99
|
+
args: [],
|
|
100
|
+
dependsOnGateway: true,
|
|
101
|
+
},
|
|
82
102
|
{ id: "admin", label: "dev.gajaeway.admin", binary: "gajaeway-admin", args: ["serve"], dependsOnGateway: true },
|
|
83
103
|
];
|
|
84
104
|
|
|
@@ -203,35 +223,50 @@ export function systemdUnitName(spec: ServiceSpec): string {
|
|
|
203
223
|
return `gajaeway-${spec.id}.service`;
|
|
204
224
|
}
|
|
205
225
|
|
|
226
|
+
/**
|
|
227
|
+
* `EnvironmentFile=` and `ConditionPathExists=` take one literal absolute path:
|
|
228
|
+
* systemd does not unquote them, so a quoted value is "not absolute" and the
|
|
229
|
+
* directive is silently ignored. Only `%` specifiers need escaping.
|
|
230
|
+
*/
|
|
231
|
+
function unitPath(path: string): string {
|
|
232
|
+
if (!path.startsWith("/") || /[\n\r\0]/.test(path))
|
|
233
|
+
throw new Error(`unit path must be absolute and single-line: ${path}`);
|
|
234
|
+
return path.replaceAll("%", "%%");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function unitEnvironmentFile(path: string): string {
|
|
238
|
+
return `EnvironmentFile=-${unitPath(path)}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
export function renderSystemdUnit(spec: ServiceSpec, binDir: string, home: string, path: string): string {
|
|
207
242
|
const execStart = [join(binDir, spec.binary), ...spec.args].join(" ");
|
|
208
243
|
const unit = [
|
|
209
244
|
"[Unit]",
|
|
210
245
|
`Description=gajaeway ${spec.id}`,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
246
|
+
...(spec.id === "broker"
|
|
247
|
+
? [`ConditionPathExists=${unitPath(homeLayout(home).brokerProvisionManifest)}`]
|
|
248
|
+
: spec.id === "gateway"
|
|
249
|
+
? [`Wants=${BROKER_UNIT}`, `After=${BROKER_UNIT}`]
|
|
250
|
+
: spec.dependsOnGateway
|
|
251
|
+
? [`BindsTo=${GATEWAY_UNIT}`, `After=${GATEWAY_UNIT}`, `PartOf=${GATEWAY_UNIT}`]
|
|
252
|
+
: []),
|
|
215
253
|
"",
|
|
216
254
|
"[Service]",
|
|
217
255
|
`ExecStart=${execStart}`,
|
|
218
256
|
`WorkingDirectory=${binDir}`,
|
|
219
257
|
unitEnvironment("GAJAEWAY_HOME", home),
|
|
220
258
|
unitEnvironment("PATH", path),
|
|
259
|
+
...(spec.id === "broker" ? [unitEnvironmentFile(join(home, "broker.env"))] : []),
|
|
221
260
|
"Restart=always",
|
|
222
|
-
"RestartSec=2",
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// broker or its session hosts.
|
|
230
|
-
...(spec.dependsOnGateway ? [] : ["KillMode=mixed", "TimeoutStopSec=30s"]),
|
|
261
|
+
spec.id === "broker" ? "RestartSec=2s" : "RestartSec=2",
|
|
262
|
+
...(spec.id === "broker"
|
|
263
|
+
? // The host propagates `gjc sdk broker run`'s exit code: 143 is its clean SIGTERM stop.
|
|
264
|
+
["KillMode=control-group", "TimeoutStopSec=30s", "SuccessExitStatus=143"]
|
|
265
|
+
: spec.id === "gateway"
|
|
266
|
+
? ["KillMode=mixed", "TimeoutStopSec=30s"]
|
|
267
|
+
: []),
|
|
231
268
|
"",
|
|
232
269
|
"[Install]",
|
|
233
|
-
// Enabling the gateway pulls the whole stack in; a dependent is never
|
|
234
|
-
// wanted by default.target on its own, because it cannot run without one.
|
|
235
270
|
`WantedBy=${spec.dependsOnGateway ? GATEWAY_UNIT : "default.target"}`,
|
|
236
271
|
"",
|
|
237
272
|
];
|
|
@@ -256,14 +291,16 @@ export function systemdUnitPath(spec: ServiceSpec, unitDir: string): string {
|
|
|
256
291
|
/**
|
|
257
292
|
* The ordered commands that realign the whole stack.
|
|
258
293
|
*
|
|
259
|
-
* systemd
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* explicit here instead — and `bootout` never appears, because removing a job
|
|
263
|
-
* leaves it with no automatic recovery.
|
|
294
|
+
* systemd restarts the broker before the gateway; gateway dependencies restart
|
|
295
|
+
* their adapters. launchd has no dependency propagation, so every job is
|
|
296
|
+
* kicked explicitly in the same order.
|
|
264
297
|
*/
|
|
265
298
|
export function restartStackCommands(platform: ServicePlatform, uid?: number): readonly (readonly string[])[] {
|
|
266
|
-
if (platform === "linux")
|
|
299
|
+
if (platform === "linux")
|
|
300
|
+
return [
|
|
301
|
+
["systemctl", "--user", "restart", BROKER_UNIT],
|
|
302
|
+
["systemctl", "--user", "restart", GATEWAY_UNIT],
|
|
303
|
+
];
|
|
267
304
|
if (platform === "darwin") {
|
|
268
305
|
const target = uid ?? process.getuid?.() ?? 0;
|
|
269
306
|
return restartOrder().map((spec) => ["launchctl", "kickstart", "-k", `gui/${target}/${spec.label}`]);
|
|
@@ -271,7 +308,7 @@ export function restartStackCommands(platform: ServicePlatform, uid?: number): r
|
|
|
271
308
|
throw new Error(`unsupported service platform: ${platform}`);
|
|
272
309
|
}
|
|
273
310
|
|
|
274
|
-
/**
|
|
311
|
+
/** Broker, gateway, then every job that holds a connection to the gateway. */
|
|
275
312
|
export function restartOrder(): readonly ServiceSpec[] {
|
|
276
313
|
return [
|
|
277
314
|
...SERVICE_SPECS.filter((spec) => !spec.dependsOnGateway),
|
|
@@ -336,7 +373,15 @@ export async function installServices(options: InstallServicesOptions): Promise<
|
|
|
336
373
|
loginPathRunner: options.loginPathRunner,
|
|
337
374
|
});
|
|
338
375
|
await mkdir(targetDir, { recursive: true, mode: 0o700 });
|
|
339
|
-
const
|
|
376
|
+
const specs: ServiceSpec[] = [];
|
|
377
|
+
for (const spec of SERVICE_SPECS) {
|
|
378
|
+
if (options.configuredAdaptersOnly && spec.id.startsWith("adapter-")) {
|
|
379
|
+
const adapter = spec.id.slice("adapter-".length);
|
|
380
|
+
if (!(await Bun.file(join(home, `adapter-${adapter}.json`)).exists())) continue;
|
|
381
|
+
}
|
|
382
|
+
specs.push(spec);
|
|
383
|
+
}
|
|
384
|
+
const definitions = specs.map((spec) =>
|
|
340
385
|
platform === "darwin"
|
|
341
386
|
? {
|
|
342
387
|
path: join(targetDir, `dev.gajaeway.${spec.id}.plist`),
|
|
@@ -346,6 +391,13 @@ export async function installServices(options: InstallServicesOptions): Promise<
|
|
|
346
391
|
);
|
|
347
392
|
const write = options.writeFile;
|
|
348
393
|
for (const definition of definitions) {
|
|
394
|
+
if (options.compareAndWrite) {
|
|
395
|
+
try {
|
|
396
|
+
if ((await readFile(definition.path, "utf8")) === definition.contents) continue;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
349
401
|
if (write) await write(definition.path, definition.contents);
|
|
350
402
|
else {
|
|
351
403
|
await writeFile(definition.path, definition.contents, { encoding: "utf8", mode: 0o600 });
|
package/src/setup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { chmod, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { printDiscordBotReady, printDiscordSetupGuide } from "./discord-setup";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* `gajaeway setup`: guided credential setup for the platform adapters.
|
|
@@ -30,7 +31,7 @@ export const SETUP_ENV = {
|
|
|
30
31
|
} as const;
|
|
31
32
|
|
|
32
33
|
export const SETUP_USAGE =
|
|
33
|
-
"usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status";
|
|
34
|
+
"usage: gajaeway setup [--from-env] [--adapters discord,slack,telegram] [--owner ID[,ID...]] [--discord-app-id ID] | setup --status | setup --broker-agent [--from DIR]";
|
|
34
35
|
|
|
35
36
|
/** Secret file names under `$GAJAEWAY_HOME/secrets`, matching docs/deployment.md. */
|
|
36
37
|
export const SECRET_FILES = {
|
|
@@ -59,16 +60,19 @@ export function redact(text: string, secrets: Iterable<string>): string {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export interface ParsedSetupArgs {
|
|
62
|
-
readonly mode: "setup" | "status";
|
|
63
|
+
readonly mode: "setup" | "status" | "broker-agent";
|
|
63
64
|
readonly fromEnv: boolean;
|
|
64
65
|
readonly adapters?: readonly SetupPlatform[];
|
|
65
66
|
readonly owners: readonly string[];
|
|
66
67
|
readonly discordAppId?: string;
|
|
68
|
+
readonly from?: string;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export function parseSetupArgs(args: readonly string[]): ParsedSetupArgs {
|
|
70
72
|
let fromEnv = false;
|
|
71
73
|
let status = false;
|
|
74
|
+
let brokerAgent = false;
|
|
75
|
+
let from: string | undefined;
|
|
72
76
|
let adapters: SetupPlatform[] | undefined;
|
|
73
77
|
const owners: string[] = [];
|
|
74
78
|
let discordAppId: string | undefined;
|
|
@@ -76,7 +80,13 @@ export function parseSetupArgs(args: readonly string[]): ParsedSetupArgs {
|
|
|
76
80
|
const flag = args[i] as string;
|
|
77
81
|
if (flag === "--from-env") fromEnv = true;
|
|
78
82
|
else if (flag === "--status") status = true;
|
|
79
|
-
else if (flag === "--
|
|
83
|
+
else if (flag === "--broker-agent") brokerAgent = true;
|
|
84
|
+
else if (flag === "--from") {
|
|
85
|
+
const value = args[++i];
|
|
86
|
+
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
87
|
+
throw new SetupError(`${flag} expects a value\n${SETUP_USAGE}`);
|
|
88
|
+
from = value;
|
|
89
|
+
} else if (flag === "--adapters" || flag === "--owner" || flag === "--discord-app-id") {
|
|
80
90
|
const value = args[++i];
|
|
81
91
|
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
82
92
|
throw new SetupError(`${flag} expects a value\n${SETUP_USAGE}`);
|
|
@@ -89,14 +99,20 @@ export function parseSetupArgs(args: readonly string[]): ParsedSetupArgs {
|
|
|
89
99
|
);
|
|
90
100
|
} else throw new SetupError(`unknown option: ${flag.split("=")[0]}\n${SETUP_USAGE}`);
|
|
91
101
|
}
|
|
92
|
-
if (status && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
102
|
+
if (status && (fromEnv || brokerAgent || from !== undefined || adapters || owners.length > 0 || discordAppId))
|
|
93
103
|
throw new SetupError(`--status takes no other options\n${SETUP_USAGE}`);
|
|
104
|
+
if (brokerAgent && (fromEnv || adapters || owners.length > 0 || discordAppId))
|
|
105
|
+
throw new SetupError(
|
|
106
|
+
`--broker-agent is standalone and cannot be combined with adapter setup options\n${SETUP_USAGE}`,
|
|
107
|
+
);
|
|
108
|
+
if (from !== undefined && !brokerAgent) throw new SetupError(`--from requires --broker-agent\n${SETUP_USAGE}`);
|
|
94
109
|
return {
|
|
95
|
-
mode: status ? "status" : "setup",
|
|
110
|
+
mode: status ? "status" : brokerAgent ? "broker-agent" : "setup",
|
|
96
111
|
fromEnv,
|
|
97
112
|
...(adapters ? { adapters } : {}),
|
|
98
113
|
owners,
|
|
99
114
|
...(discordAppId ? { discordAppId } : {}),
|
|
115
|
+
...(from === undefined ? {} : { from }),
|
|
100
116
|
};
|
|
101
117
|
}
|
|
102
118
|
|
|
@@ -182,6 +198,8 @@ export interface ValidatedIdentity {
|
|
|
182
198
|
readonly summary: string;
|
|
183
199
|
/** A user id the platform names as the app owner, when it names one. */
|
|
184
200
|
readonly ownerId?: string;
|
|
201
|
+
/** Application ID (Discord only). */
|
|
202
|
+
readonly appId?: string;
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
async function requestJson(
|
|
@@ -241,7 +259,8 @@ export async function validateDiscord(
|
|
|
241
259
|
const ownerId = !hasTeam && typeof owner?.id === "string" ? owner.id : undefined;
|
|
242
260
|
return {
|
|
243
261
|
platform: "discord",
|
|
244
|
-
summary: `bot ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
262
|
+
summary: `bot named ${String(me.body.username ?? "?")} (${String(me.body.id ?? "?")}) of application ${appId}`,
|
|
263
|
+
appId,
|
|
245
264
|
...(ownerId ? { ownerId } : {}),
|
|
246
265
|
};
|
|
247
266
|
}
|
|
@@ -472,13 +491,16 @@ async function askRequiredSecret(prompter: Prompter, question: string): Promise<
|
|
|
472
491
|
export async function tokensFromPrompts(
|
|
473
492
|
prompter: Prompter,
|
|
474
493
|
parsed: ParsedSetupArgs,
|
|
494
|
+
log?: (line: string) => void,
|
|
475
495
|
): Promise<{ tokens: SetupTokens; owners: string[] }> {
|
|
476
496
|
const wanted = parsed.adapters ?? [];
|
|
477
497
|
const enable = async (platform: SetupPlatform, label: string) =>
|
|
478
498
|
parsed.adapters ? wanted.includes(platform) : await askYes(prompter, `Enable ${label}?`);
|
|
479
499
|
const tokens: { -readonly [K in keyof SetupTokens]: SetupTokens[K] } = {};
|
|
480
500
|
if (await enable("discord", "Discord")) {
|
|
481
|
-
const
|
|
501
|
+
const logger = log ?? (() => {});
|
|
502
|
+
printDiscordSetupGuide(logger);
|
|
503
|
+
const token = await askRequiredSecret(prompter, "Paste your Discord bot token here (input hidden): ");
|
|
482
504
|
const appId = parsed.discordAppId ?? (await prompter.ask("Discord application id (optional): ")).trim();
|
|
483
505
|
tokens.discord = { token, ...(appId ? { appId } : {}) };
|
|
484
506
|
}
|
|
@@ -554,12 +576,40 @@ export function terminalPrompter(): Prompter {
|
|
|
554
576
|
};
|
|
555
577
|
}
|
|
556
578
|
|
|
579
|
+
export interface BrokerAgentRunResult {
|
|
580
|
+
readonly exitCode: number;
|
|
581
|
+
readonly stdout: string;
|
|
582
|
+
readonly stderr: string;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export type BrokerAgentRunner = (command: readonly string[]) => Promise<BrokerAgentRunResult>;
|
|
586
|
+
|
|
587
|
+
async function defaultBrokerAgentRunner(command: readonly string[]): Promise<BrokerAgentRunResult> {
|
|
588
|
+
const child = Bun.spawn([...command], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
589
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
590
|
+
new Response(child.stdout).text(),
|
|
591
|
+
new Response(child.stderr).text(),
|
|
592
|
+
child.exited,
|
|
593
|
+
]);
|
|
594
|
+
return { exitCode, stdout, stderr };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function setupBinDir(options: RunSetupOptions): Promise<string> {
|
|
598
|
+
if (options.binDir !== undefined) return options.binDir;
|
|
599
|
+
const env = options.env ?? process.env;
|
|
600
|
+
if (env.GAJAEWAY_BIN_DIR) return env.GAJAEWAY_BIN_DIR;
|
|
601
|
+
const executable = await realpath(process.execPath).catch(() => undefined);
|
|
602
|
+
if (executable !== undefined && basename(executable).startsWith("gajaeway")) return dirname(executable);
|
|
603
|
+
throw new SetupError("setup --broker-agent requires the installed gajaeway binary or an explicit bin directory");
|
|
604
|
+
}
|
|
557
605
|
export interface RunSetupOptions {
|
|
558
606
|
readonly home: string;
|
|
607
|
+
readonly binDir?: string;
|
|
559
608
|
readonly env?: NodeJS.ProcessEnv;
|
|
560
609
|
readonly fetch?: FetchLike;
|
|
561
610
|
readonly prompter?: () => Prompter;
|
|
562
611
|
readonly log?: (line: string) => void;
|
|
612
|
+
readonly runBrokerAgent?: BrokerAgentRunner;
|
|
563
613
|
}
|
|
564
614
|
|
|
565
615
|
/** The whole command. Returns the lines it printed so tests can assert nothing secret escaped. */
|
|
@@ -570,6 +620,23 @@ export async function runSetup(args: readonly string[], options: RunSetupOptions
|
|
|
570
620
|
log((await enabledAdapters(options.home)).join(","));
|
|
571
621
|
return;
|
|
572
622
|
}
|
|
623
|
+
if (parsed.mode === "broker-agent") {
|
|
624
|
+
const command = [
|
|
625
|
+
join(await setupBinDir(options), "gajaeway-gateway"),
|
|
626
|
+
"broker-agent",
|
|
627
|
+
"provision",
|
|
628
|
+
"--mode",
|
|
629
|
+
"force",
|
|
630
|
+
...(parsed.from === undefined ? [] : ["--from", parsed.from]),
|
|
631
|
+
];
|
|
632
|
+
const result = await (options.runBrokerAgent ?? defaultBrokerAgentRunner)(command);
|
|
633
|
+
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|
|
634
|
+
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
635
|
+
if (result.exitCode === 1 && /lease_held name=broker/.test(`${result.stdout}\n${result.stderr}`))
|
|
636
|
+
log("broker lease is held; stop it with: systemctl --user stop gajaeway-broker");
|
|
637
|
+
process.exitCode = result.exitCode;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
573
640
|
let tokens: SetupTokens;
|
|
574
641
|
let owners: string[];
|
|
575
642
|
if (parsed.fromEnv) {
|
|
@@ -578,7 +645,7 @@ export async function runSetup(args: readonly string[], options: RunSetupOptions
|
|
|
578
645
|
} else {
|
|
579
646
|
const prompter = (options.prompter ?? terminalPrompter)();
|
|
580
647
|
try {
|
|
581
|
-
({ tokens, owners } = await tokensFromPrompts(prompter, parsed));
|
|
648
|
+
({ tokens, owners } = await tokensFromPrompts(prompter, parsed, log));
|
|
582
649
|
} finally {
|
|
583
650
|
prompter.close();
|
|
584
651
|
}
|
|
@@ -587,6 +654,14 @@ export async function runSetup(args: readonly string[], options: RunSetupOptions
|
|
|
587
654
|
try {
|
|
588
655
|
const identities = await validateAll(tokens, options.fetch ?? ((input, init) => fetch(input, init)));
|
|
589
656
|
for (const identity of identities) log(`${identity.platform}: token valid — ${identity.summary}`);
|
|
657
|
+
|
|
658
|
+
// Print Discord invite URL after successful validation
|
|
659
|
+
const discordIdentity = identities.find((i) => i.platform === "discord");
|
|
660
|
+
if (discordIdentity && tokens.discord && discordIdentity.appId) {
|
|
661
|
+
const botName = (discordIdentity.summary.match(/named (\S+)/) ?? [])[1];
|
|
662
|
+
printDiscordBotReady(log, discordIdentity.appId, botName);
|
|
663
|
+
}
|
|
664
|
+
|
|
590
665
|
for (const identity of identities)
|
|
591
666
|
if (identity.ownerId && owners.length === 0) {
|
|
592
667
|
owners.push(identity.ownerId);
|
package/src/up.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { GajaewayClient } from "@gajae-gateway/sdk";
|
|
4
|
+
import type { RunSetupOptions } from "./setup";
|
|
5
|
+
import { enabledAdapters, runSetup } from "./setup";
|
|
6
|
+
|
|
7
|
+
export interface UpOptions extends Omit<RunSetupOptions, "home"> {
|
|
8
|
+
readonly home: string;
|
|
9
|
+
readonly socket?: string;
|
|
10
|
+
/** Skip the setup check and assume config exists. */
|
|
11
|
+
readonly skipSetup?: boolean;
|
|
12
|
+
/** Spawn runner for tests; defaults to Bun.spawn */
|
|
13
|
+
readonly spawn?: (cmd: string, args: string[], opts?: { cwd?: string }) => unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `gajaeway up`: One-command setup and start for first-time users.
|
|
18
|
+
*
|
|
19
|
+
* Checks for existing config.json; if missing, runs interactive setup.
|
|
20
|
+
* Then starts the gateway daemon and enabled adapters in the background.
|
|
21
|
+
* Reports the gateway status and how to stop it.
|
|
22
|
+
*/
|
|
23
|
+
export async function runUp(options: UpOptions): Promise<void> {
|
|
24
|
+
const log = options.log ?? ((line: string) => console.log(line));
|
|
25
|
+
const configPath = join(options.home, "config.json");
|
|
26
|
+
|
|
27
|
+
// Check if setup is needed
|
|
28
|
+
if (!options.skipSetup) {
|
|
29
|
+
let exists = false;
|
|
30
|
+
try {
|
|
31
|
+
await access(configPath);
|
|
32
|
+
exists = true;
|
|
33
|
+
} catch {
|
|
34
|
+
// File does not exist
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!exists) {
|
|
38
|
+
log("No configuration found. Running setup...\n");
|
|
39
|
+
try {
|
|
40
|
+
// Run setup in default (interactive) mode
|
|
41
|
+
await runSetup([], {
|
|
42
|
+
home: options.home,
|
|
43
|
+
env: options.env,
|
|
44
|
+
fetch: options.fetch,
|
|
45
|
+
prompter: options.prompter,
|
|
46
|
+
log,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
log(`Setup failed: ${message}`);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log("\n✓ Configuration saved.\n");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check gateway status
|
|
59
|
+
const socket = options.socket ?? `${options.home}/gateway.sock`;
|
|
60
|
+
let isRunning = false;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const client = await GajaewayClient.connectSocket(socket);
|
|
64
|
+
try {
|
|
65
|
+
await client.status();
|
|
66
|
+
isRunning = true;
|
|
67
|
+
} finally {
|
|
68
|
+
await client.close();
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Gateway is not running; we'll try to start it
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isRunning) {
|
|
75
|
+
log("✓ Gateway is already running.");
|
|
76
|
+
const adapters = await enabledAdapters(options.home);
|
|
77
|
+
log(`✓ Enabled adapters: ${adapters.join(", ")}`);
|
|
78
|
+
log("\nYou can now:");
|
|
79
|
+
log(" • Send a message to your bot on Discord/Slack/Telegram");
|
|
80
|
+
log(" • View status: gajaeway ops cycle");
|
|
81
|
+
log(" • Search memory: gajaeway memory search <query>");
|
|
82
|
+
log("\nStop with: gajaeway shutdown");
|
|
83
|
+
} else {
|
|
84
|
+
// Try to start the gateway and adapters
|
|
85
|
+
log("Starting gateway and adapters...");
|
|
86
|
+
|
|
87
|
+
// Detect if we're in the source checkout (has dist/gajaeway-gateway)
|
|
88
|
+
// vs. running from installed binaries
|
|
89
|
+
const isSourceCheckout = await access(join(options.home, "..", "..", "dist", "gajaeway-gateway"))
|
|
90
|
+
.then(() => true)
|
|
91
|
+
.catch(() => false);
|
|
92
|
+
|
|
93
|
+
const spawn = options.spawn ?? ((cmd: string, args: string[]) => Bun.spawn([cmd, ...args]));
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Start gateway daemon
|
|
97
|
+
// Use bun packages/gateway/src/main.ts if in source checkout, else use binary
|
|
98
|
+
const gatewayCmd = isSourceCheckout ? "bun" : "gajaeway-gateway";
|
|
99
|
+
const gatewayArgs = isSourceCheckout ? ["packages/gateway/src/main.ts", "daemon"] : ["daemon"];
|
|
100
|
+
|
|
101
|
+
const gatewayProcess = spawn(gatewayCmd, gatewayArgs, { cwd: process.cwd() });
|
|
102
|
+
|
|
103
|
+
// Give gateway a moment to start
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
105
|
+
|
|
106
|
+
// Start enabled adapters
|
|
107
|
+
const adapters = await enabledAdapters(options.home);
|
|
108
|
+
for (const adapter of adapters) {
|
|
109
|
+
const adapterCmd = isSourceCheckout ? "dist/gajaeway-" + adapter : "gajaeway-" + adapter;
|
|
110
|
+
const adapterProcess = spawn(adapterCmd, [], { cwd: process.cwd() });
|
|
111
|
+
// Don't wait for adapter; let it run in background
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
log("\n✓ Gateway started.");
|
|
115
|
+
if (adapters.length > 0) {
|
|
116
|
+
log(`✓ Adapters started: ${adapters.join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log("\nYou can now:");
|
|
120
|
+
log(" • Send a message to your bot on Discord/Slack/Telegram");
|
|
121
|
+
log(" • View status: gajaeway ops cycle");
|
|
122
|
+
log(" • Search memory: gajaeway memory search <query>");
|
|
123
|
+
log("\nStop with: gajaeway shutdown");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
+
log(`Failed to start services: ${message}`);
|
|
127
|
+
log("\nAlternative: start manually in separate terminals:");
|
|
128
|
+
log(" Terminal 1: bun packages/gateway/src/main.ts daemon");
|
|
129
|
+
log(" Terminal 2: dist/gajaeway-discord");
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|