@nordbyte/nordrelay 0.5.2 → 0.6.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/.env.example +63 -11
- package/README.md +90 -19
- package/dist/access-control.js +1 -0
- package/dist/activity-events.js +44 -0
- package/dist/audit-log.js +40 -2
- package/dist/bot-rendering.js +10 -7
- package/dist/bot.js +458 -5
- package/dist/channel-actions.js +7 -2
- package/dist/channel-adapter.js +34 -7
- package/dist/channel-command-service.js +156 -0
- package/dist/channel-turn-service.js +237 -0
- package/dist/config-metadata.js +78 -13
- package/dist/config.js +77 -7
- package/dist/context-key.js +77 -5
- package/dist/discord-artifacts.js +165 -0
- package/dist/discord-bot.js +2014 -0
- package/dist/discord-channel-runtime.js +133 -0
- package/dist/discord-command-surface.js +119 -0
- package/dist/discord-rate-limit.js +141 -0
- package/dist/index.js +16 -5
- package/dist/job-store.js +127 -0
- package/dist/metrics.js +41 -0
- package/dist/relay-external-activity-monitor.js +47 -6
- package/dist/relay-runtime.js +986 -281
- package/dist/runtime-cache.js +57 -0
- package/dist/session-locks.js +10 -7
- package/dist/support-bundle.js +1 -0
- package/dist/telegram-access-commands.js +15 -2
- package/dist/telegram-access-middleware.js +16 -3
- package/dist/telegram-agent-commands.js +25 -0
- package/dist/telegram-artifact-commands.js +46 -0
- package/dist/telegram-diagnostics-command.js +5 -50
- package/dist/telegram-general-commands.js +2 -6
- package/dist/telegram-operational-commands.js +14 -6
- package/dist/telegram-queue-commands.js +74 -4
- package/dist/telegram-support-command.js +7 -0
- package/dist/telegram-update-commands.js +27 -0
- package/dist/user-management.js +208 -0
- package/dist/web-api-contract.js +9 -0
- package/dist/web-dashboard-access-routes.js +74 -1
- package/dist/web-dashboard-artifact-routes.js +3 -3
- package/dist/web-dashboard-assets.js +2 -0
- package/dist/web-dashboard-pages.js +97 -13
- package/dist/web-dashboard-runtime-routes.js +53 -8
- package/dist/web-dashboard-session-routes.js +27 -20
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +148 -6
- package/dist/web-state.js +33 -2
- package/dist/webui-assets/dashboard.css +75 -1
- package/dist/webui-assets/dashboard.js +358 -47
- package/package.json +3 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +210 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordbyte/nordrelay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Remote control plane for coding agents across messaging channels.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Ricardo",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"claude-code",
|
|
24
24
|
"claude",
|
|
25
25
|
"telegram",
|
|
26
|
+
"discord",
|
|
26
27
|
"bot",
|
|
27
28
|
"automation"
|
|
28
29
|
],
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"@anthropic-ai/claude-agent-sdk": "^0.2.140",
|
|
64
65
|
"@grammyjs/auto-retry": "^2.0.2",
|
|
65
66
|
"@openai/codex-sdk": "^0.130.0",
|
|
67
|
+
"discord.js": "^14.26.4",
|
|
66
68
|
"grammy": "^1.41.1",
|
|
67
69
|
"zod": "^4.4.3"
|
|
68
70
|
},
|
|
@@ -66,6 +66,10 @@ function parseArgs(argv) {
|
|
|
66
66
|
else if (arg === "--no-restart") options.restartAfterUpdate = false;
|
|
67
67
|
else if (arg === "--restart") options.restartAfterUpdate = true;
|
|
68
68
|
else if (arg === "--token") options.telegramBotToken = requireValue(copy, ++i, arg);
|
|
69
|
+
else if (arg === "--disable-telegram") options.disableTelegram = true;
|
|
70
|
+
else if (arg === "--enable-discord") options.enableDiscord = true;
|
|
71
|
+
else if (arg === "--discord-token") options.discordBotToken = requireValue(copy, ++i, arg);
|
|
72
|
+
else if (arg === "--discord-client-id") options.discordClientId = requireValue(copy, ++i, arg);
|
|
69
73
|
else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
|
|
70
74
|
else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
|
|
71
75
|
else if (arg === "--admin-password") options.adminPassword = requireValue(copy, ++i, arg);
|
|
@@ -81,6 +85,9 @@ function parseArgs(argv) {
|
|
|
81
85
|
options.pidFile = path.join(options.home, "nordrelay.pid");
|
|
82
86
|
options.stateFile = path.join(options.home, "state.json");
|
|
83
87
|
options.logFile = path.join(options.home, "nordrelay.log");
|
|
88
|
+
options.webPidFile = path.join(options.home, "nordrelay-web.pid");
|
|
89
|
+
options.webStateFile = path.join(options.home, "web-state.json");
|
|
90
|
+
options.webLogFile = path.join(options.home, "nordrelay-web.log");
|
|
84
91
|
return options;
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -175,6 +182,26 @@ async function readPid(pidFile) {
|
|
|
175
182
|
}
|
|
176
183
|
}
|
|
177
184
|
|
|
185
|
+
async function readWebState(options) {
|
|
186
|
+
return await readJson(options.webStateFile, {});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readWebPid(options) {
|
|
190
|
+
return await readPid(options.webPidFile);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function isWebDashboardRunning(options) {
|
|
194
|
+
return isProcessRunning(await readWebPid(options));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function writeWebState(options, patch) {
|
|
198
|
+
await writeJsonAtomic(options.webStateFile, {
|
|
199
|
+
updatedAt: nowIso(),
|
|
200
|
+
logFile: options.webLogFile,
|
|
201
|
+
...patch,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
178
205
|
function resolveDashboardEndpoint(options, settings = {}) {
|
|
179
206
|
const host = options.host || process.env.NORDRELAY_DASHBOARD_HOST || "127.0.0.1";
|
|
180
207
|
const rawPort = options.port ?? Number.parseInt(process.env.NORDRELAY_DASHBOARD_PORT || "31878", 10);
|
|
@@ -233,7 +260,9 @@ async function commandStart(options, settings = {}) {
|
|
|
233
260
|
console.log(`Workspace: ${state.workspace || "-"}`);
|
|
234
261
|
console.log(`Mode: ${state.sessionMode || "per Telegram context"}`);
|
|
235
262
|
if (!settings.skipWebHint) {
|
|
236
|
-
|
|
263
|
+
const webPid = await readWebPid(options);
|
|
264
|
+
const webHint = isProcessRunning(webPid) ? `(running with PID ${webPid})` : "(run `nordrelay web` to start it)";
|
|
265
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} ${webHint}`);
|
|
237
266
|
}
|
|
238
267
|
console.log(`Log: ${options.logFile}`);
|
|
239
268
|
return;
|
|
@@ -251,7 +280,9 @@ async function commandStart(options, settings = {}) {
|
|
|
251
280
|
|
|
252
281
|
console.log(`Started ${APP_NAME} ${VERSION} with PID ${child.pid}`);
|
|
253
282
|
if (!settings.skipWebHint) {
|
|
254
|
-
|
|
283
|
+
const webPid = await readWebPid(options);
|
|
284
|
+
const webHint = isProcessRunning(webPid) ? `(running with PID ${webPid})` : "(run `nordrelay web` to start it)";
|
|
285
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} ${webHint}`);
|
|
255
286
|
}
|
|
256
287
|
console.log(`Startup is still in progress. Log: ${options.logFile}`);
|
|
257
288
|
}
|
|
@@ -286,7 +317,51 @@ async function waitForState(stateFile, pid, timeoutMs) {
|
|
|
286
317
|
return await readJson(stateFile);
|
|
287
318
|
}
|
|
288
319
|
|
|
289
|
-
async function
|
|
320
|
+
async function stopWebDashboard(options, settings = {}) {
|
|
321
|
+
const pid = await readWebPid(options);
|
|
322
|
+
if (!isProcessRunning(pid)) {
|
|
323
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
324
|
+
const state = await readWebState(options);
|
|
325
|
+
if (state.status === "running" || state.status === "starting") {
|
|
326
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
327
|
+
}
|
|
328
|
+
if (!settings.quiet) {
|
|
329
|
+
console.log("WebUI is not running.");
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
process.kill(pid, "SIGTERM");
|
|
335
|
+
for (let i = 0; i < 40; i += 1) {
|
|
336
|
+
if (!isProcessRunning(pid)) break;
|
|
337
|
+
await sleep(250);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (isProcessRunning(pid)) {
|
|
341
|
+
process.kill(pid, "SIGKILL");
|
|
342
|
+
for (let i = 0; i < 20; i += 1) {
|
|
343
|
+
if (!isProcessRunning(pid)) break;
|
|
344
|
+
await sleep(250);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isProcessRunning(pid)) {
|
|
349
|
+
console.log(`WebUI PID ${pid} did not exit after SIGTERM/SIGKILL.`);
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
354
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
355
|
+
if (!settings.quiet) {
|
|
356
|
+
console.log(`Stopped WebUI PID ${pid}.`);
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function commandStop(options, settings = {}) {
|
|
362
|
+
if (!settings.keepWeb) {
|
|
363
|
+
await stopWebDashboard(options);
|
|
364
|
+
}
|
|
290
365
|
const pid = await readPid(options.pidFile);
|
|
291
366
|
if (!isProcessRunning(pid)) {
|
|
292
367
|
console.log("Connector is not running.");
|
|
@@ -313,10 +388,19 @@ async function commandStatus(options) {
|
|
|
313
388
|
loadEnvFiles(options.home);
|
|
314
389
|
const dashboard = resolveDashboardEndpoint(options);
|
|
315
390
|
const pid = await readPid(options.pidFile);
|
|
391
|
+
const webPid = await readWebPid(options);
|
|
316
392
|
const state = await readJson(options.stateFile, {});
|
|
393
|
+
const webState = await readWebState(options);
|
|
317
394
|
const running = isProcessRunning(pid);
|
|
395
|
+
const webRunning = isProcessRunning(webPid);
|
|
396
|
+
const webStatus = webRunning ? "running" : webState.status === "running" || webState.status === "starting" ? "stale" : webState.status || "stopped";
|
|
397
|
+
if (!webRunning && (webState.status === "running" || webState.status === "starting")) {
|
|
398
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
399
|
+
await writeWebState(options, { status: "stopped", pid: null });
|
|
400
|
+
}
|
|
318
401
|
console.log(`Status: ${state.status || (running ? "running" : "stopped")}`);
|
|
319
402
|
console.log(`PID: ${pid || "-"} (${running ? "running" : "not running"})`);
|
|
403
|
+
console.log(`WebUI PID: ${webPid || "-"} (${webRunning ? "running" : "not running"})`);
|
|
320
404
|
console.log(`Workspace: ${state.workspace || "-"}`);
|
|
321
405
|
console.log(`Mode: ${state.sessionMode || "per Telegram context"}`);
|
|
322
406
|
console.log(`Auth: ${state.authenticated === undefined ? "-" : state.authenticated ? "yes" : "no"}`);
|
|
@@ -326,8 +410,9 @@ async function commandStatus(options) {
|
|
|
326
410
|
console.log(`OpenClaw CLI: ${state.openClawCli || "-"}`);
|
|
327
411
|
console.log(`Claude Code CLI: ${state.claudeCodeCli || "-"}`);
|
|
328
412
|
console.log(`OpenClaw Gateway: ${state.openClawGateway || process.env.OPENCLAW_GATEWAY_URL || "-"}`);
|
|
329
|
-
console.log(`WebUI: ${formatDashboardUrl(dashboard)}`);
|
|
413
|
+
console.log(`WebUI: ${formatDashboardUrl(dashboard)} (${webStatus})`);
|
|
330
414
|
console.log(`Log: ${options.logFile}`);
|
|
415
|
+
console.log(`WebUI log: ${options.webLogFile}`);
|
|
331
416
|
if (state.error) console.log(`Error: ${state.error}`);
|
|
332
417
|
}
|
|
333
418
|
|
|
@@ -579,9 +664,17 @@ async function commandInit(options) {
|
|
|
579
664
|
return;
|
|
580
665
|
}
|
|
581
666
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
await ask(null, "Telegram bot token", "")
|
|
667
|
+
const enableTelegram = options.disableTelegram ? "false" : await askChoice(null, "Enable Telegram", "true");
|
|
668
|
+
const telegramBotToken = enableTelegram === "true"
|
|
669
|
+
? options.telegramBotToken || process.env.TELEGRAM_BOT_TOKEN || await ask(null, "Telegram bot token", "")
|
|
670
|
+
: "";
|
|
671
|
+
const enableDiscord = options.enableDiscord ? "true" : await askChoice(null, "Enable Discord", "false");
|
|
672
|
+
const discordBotToken = enableDiscord === "true"
|
|
673
|
+
? options.discordBotToken || process.env.DISCORD_BOT_TOKEN || await ask(null, "Discord bot token", "")
|
|
674
|
+
: "";
|
|
675
|
+
const discordClientId = enableDiscord === "true"
|
|
676
|
+
? options.discordClientId || process.env.DISCORD_CLIENT_ID || await ask(null, "Discord client ID", "")
|
|
677
|
+
: "";
|
|
585
678
|
const adminEmail = options.adminEmail || await ask(null, "Admin email", "");
|
|
586
679
|
const adminName = options.adminName || await ask(null, "Admin name", "Admin");
|
|
587
680
|
const adminPassword = options.adminPassword || await askSecret(null, "Admin password", "");
|
|
@@ -593,7 +686,9 @@ async function commandInit(options) {
|
|
|
593
686
|
const enableClaudeCode = options.enableClaudeCode ? "true" : await askChoice(null, "Enable Claude Code", "false");
|
|
594
687
|
const stateBackend = options.stateBackend || await askChoice(null, "State backend (json/sqlite)", "json");
|
|
595
688
|
|
|
596
|
-
if (!telegramBotToken) throw new Error("Telegram bot token is required.");
|
|
689
|
+
if (enableTelegram === "true" && !telegramBotToken) throw new Error("Telegram bot token is required when Telegram is enabled.");
|
|
690
|
+
if (enableDiscord === "true" && !discordBotToken) throw new Error("Discord bot token is required when Discord is enabled.");
|
|
691
|
+
if (enableTelegram !== "true" && enableDiscord !== "true") throw new Error("At least one chat adapter must be enabled.");
|
|
597
692
|
if (!adminEmail) throw new Error("Admin email is required.");
|
|
598
693
|
if (!adminPassword) throw new Error("Admin password is required.");
|
|
599
694
|
if (enableCodex !== "true" && enablePi !== "true" && enableHermes !== "true" && enableOpenClaw !== "true" && enableClaudeCode !== "true") throw new Error("At least one agent must be enabled.");
|
|
@@ -610,7 +705,14 @@ async function commandInit(options) {
|
|
|
610
705
|
const lines = [
|
|
611
706
|
"# NordRelay local runtime config.",
|
|
612
707
|
"# Keep this file private; it contains bot credentials.",
|
|
708
|
+
`TELEGRAM_ENABLED=${enableTelegram}`,
|
|
613
709
|
`TELEGRAM_BOT_TOKEN=${telegramBotToken}`,
|
|
710
|
+
`DISCORD_ENABLED=${enableDiscord}`,
|
|
711
|
+
`DISCORD_BOT_TOKEN=${discordBotToken}`,
|
|
712
|
+
`DISCORD_CLIENT_ID=${discordClientId}`,
|
|
713
|
+
"DISCORD_COMMAND_MODE=both",
|
|
714
|
+
"DISCORD_MESSAGE_CONTENT_ENABLED=true",
|
|
715
|
+
"DISCORD_AUTO_REGISTER_COMMANDS=true",
|
|
614
716
|
`NORDRELAY_CODEX_ENABLED=${enableCodex}`,
|
|
615
717
|
`NORDRELAY_PI_ENABLED=${enablePi}`,
|
|
616
718
|
`NORDRELAY_HERMES_ENABLED=${enableHermes}`,
|
|
@@ -664,6 +766,7 @@ function parseUserFlags(argv) {
|
|
|
664
766
|
else if (arg === "--password") flags.password = requireValue(copy, ++i, arg);
|
|
665
767
|
else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
|
|
666
768
|
else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
769
|
+
else if (arg === "--discord-user-id") flags.discordUserId = requireValue(copy, ++i, arg);
|
|
667
770
|
else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
|
|
668
771
|
}
|
|
669
772
|
return flags;
|
|
@@ -695,8 +798,8 @@ async function commandUser(options) {
|
|
|
695
798
|
? ["admin"]
|
|
696
799
|
: (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
|
|
697
800
|
const created = flags.subcommand === "create-admin"
|
|
698
|
-
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId })
|
|
699
|
-
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId });
|
|
801
|
+
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId })
|
|
802
|
+
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId });
|
|
700
803
|
console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
|
|
701
804
|
return;
|
|
702
805
|
}
|
|
@@ -721,7 +824,17 @@ async function commandUser(options) {
|
|
|
721
824
|
return;
|
|
722
825
|
}
|
|
723
826
|
|
|
724
|
-
if (flags.subcommand === "link-
|
|
827
|
+
if (flags.subcommand === "link-discord") {
|
|
828
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
829
|
+
const discordUserId = flags.discordUserId || await ask(null, "Discord user id", "");
|
|
830
|
+
const user = store.getUserByEmail(email);
|
|
831
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
832
|
+
store.linkDiscordUser(user.user.id, { discordUserId });
|
|
833
|
+
console.log(`Linked Discord user ${discordUserId} to ${user.user.email}.`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (flags.subcommand === "link-code" || flags.subcommand === "telegram-link-code") {
|
|
725
838
|
const email = flags.email || await ask(null, "Email", "");
|
|
726
839
|
const user = store.getUserByEmail(email);
|
|
727
840
|
if (!user) throw new Error(`User not found: ${email}`);
|
|
@@ -731,7 +844,17 @@ async function commandUser(options) {
|
|
|
731
844
|
return;
|
|
732
845
|
}
|
|
733
846
|
|
|
734
|
-
|
|
847
|
+
if (flags.subcommand === "discord-link-code") {
|
|
848
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
849
|
+
const user = store.getUserByEmail(email);
|
|
850
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
851
|
+
const code = store.createDiscordLinkCode(user.user.id);
|
|
852
|
+
console.log(`Discord link code for ${user.user.email}: ${code.code}`);
|
|
853
|
+
console.log(`Expires: ${code.expiresAt}`);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-discord|link-code|telegram-link-code|discord-link-code]");
|
|
735
858
|
}
|
|
736
859
|
|
|
737
860
|
async function commandDoctor(options) {
|
|
@@ -741,11 +864,16 @@ async function commandDoctor(options) {
|
|
|
741
864
|
const userSnapshot = userStore?.snapshot();
|
|
742
865
|
const checks = [];
|
|
743
866
|
checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
|
|
744
|
-
|
|
867
|
+
const telegramEnabled = process.env.TELEGRAM_ENABLED !== "false";
|
|
868
|
+
const discordEnabled = process.env.DISCORD_ENABLED === "true";
|
|
869
|
+
checks.push(check("Telegram bot token", !telegramEnabled || Boolean(process.env.TELEGRAM_BOT_TOKEN), telegramEnabled ? (process.env.TELEGRAM_BOT_TOKEN ? "configured" : "missing") : "disabled", telegramEnabled ? "fail" : "warn"));
|
|
870
|
+
checks.push(check("Discord bot token", !discordEnabled || Boolean(process.env.DISCORD_BOT_TOKEN), discordEnabled ? (process.env.DISCORD_BOT_TOKEN ? "configured" : "missing") : "disabled", discordEnabled ? "fail" : "warn"));
|
|
871
|
+
checks.push(check("Discord client ID", !discordEnabled || Boolean(process.env.DISCORD_CLIENT_ID), discordEnabled ? (process.env.DISCORD_CLIENT_ID ? "configured" : "missing; slash command auto-registration disabled") : "disabled", "warn"));
|
|
745
872
|
checks.push(check("User store", Boolean(userStore), userStore ? userStore.filePath : "missing runtime", userStore ? "pass" : "fail"));
|
|
746
873
|
checks.push(check("Admin user", Boolean(userSnapshot?.adminConfigured), userSnapshot?.adminConfigured ? "configured" : "missing"));
|
|
747
874
|
checks.push(check("WebUI login", true, "required for every dashboard request"));
|
|
748
875
|
checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
|
|
876
|
+
checks.push(check("Discord access", true, "requires linked active users and enabled channels"));
|
|
749
877
|
checks.push(check("Codex enabled flag", process.env.NORDRELAY_CODEX_ENABLED !== "false", `NORDRELAY_CODEX_ENABLED=${process.env.NORDRELAY_CODEX_ENABLED ?? "true"}`));
|
|
750
878
|
checks.push(check("Pi enabled flag", process.env.NORDRELAY_PI_ENABLED === "true" || process.env.NORDRELAY_PI_ENABLED === undefined, `NORDRELAY_PI_ENABLED=${process.env.NORDRELAY_PI_ENABLED ?? "false"}`, process.env.NORDRELAY_PI_ENABLED === "true" ? "pass" : "warn"));
|
|
751
879
|
checks.push(check("Hermes enabled flag", process.env.NORDRELAY_HERMES_ENABLED === "true", `NORDRELAY_HERMES_ENABLED=${process.env.NORDRELAY_HERMES_ENABLED ?? "false"}`, process.env.NORDRELAY_HERMES_ENABLED === "true" ? "pass" : "warn"));
|
|
@@ -850,8 +978,21 @@ async function checkOpenClawGateway() {
|
|
|
850
978
|
async function commandWeb(options) {
|
|
851
979
|
await mkdirp(options.home);
|
|
852
980
|
loadEnvFiles(options.home);
|
|
853
|
-
const { host, port } = resolveDashboardEndpoint(options, { strict: true });
|
|
854
981
|
await ensureConnectorStartedForWeb(options);
|
|
982
|
+
await startWebDashboard(options, { detached: false });
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function startWebDashboard(options, settings = {}) {
|
|
986
|
+
await mkdirp(options.home);
|
|
987
|
+
loadEnvFiles(options.home);
|
|
988
|
+
const { host, port } = resolveDashboardEndpoint(options, { strict: true });
|
|
989
|
+
const currentPid = await readWebPid(options);
|
|
990
|
+
if (isProcessRunning(currentPid)) {
|
|
991
|
+
console.log(`NordRelay dashboard already running with PID ${currentPid}.`);
|
|
992
|
+
console.log(`NordRelay dashboard: ${formatDashboardUrl({ host, port })}`);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
855
996
|
const entry = await resolveWebRuntimeEntry();
|
|
856
997
|
if (!entry) {
|
|
857
998
|
throw new Error(`Missing dashboard runtime. Run \`npm install\` and \`npm run build\` in ${RUNTIME_ROOT}.`);
|
|
@@ -864,12 +1005,43 @@ async function commandWeb(options) {
|
|
|
864
1005
|
NORDRELAY_DASHBOARD_HOST: host,
|
|
865
1006
|
NORDRELAY_DASHBOARD_PORT: String(port),
|
|
866
1007
|
};
|
|
1008
|
+
await writeWebState(options, {
|
|
1009
|
+
status: "starting",
|
|
1010
|
+
pid: null,
|
|
1011
|
+
host,
|
|
1012
|
+
port,
|
|
1013
|
+
url: formatDashboardUrl({ host, port }),
|
|
1014
|
+
});
|
|
1015
|
+
const stdio = settings.detached
|
|
1016
|
+
? ["ignore", fs.openSync(options.webLogFile, "a"), fs.openSync(options.webLogFile, "a")]
|
|
1017
|
+
: "inherit";
|
|
867
1018
|
const child = spawn(entry.command, [...entry.args, "--host", host, "--port", String(port), "--home", options.home], {
|
|
868
1019
|
cwd: RUNTIME_ROOT,
|
|
869
1020
|
env,
|
|
870
|
-
|
|
1021
|
+
detached: Boolean(settings.detached),
|
|
1022
|
+
stdio,
|
|
1023
|
+
});
|
|
1024
|
+
await fsp.writeFile(options.webPidFile, `${child.pid}\n`);
|
|
1025
|
+
await writeWebState(options, {
|
|
1026
|
+
status: "running",
|
|
1027
|
+
pid: child.pid,
|
|
1028
|
+
host,
|
|
1029
|
+
port,
|
|
1030
|
+
url: formatDashboardUrl({ host, port }),
|
|
871
1031
|
});
|
|
872
1032
|
|
|
1033
|
+
if (settings.detached) {
|
|
1034
|
+
child.unref();
|
|
1035
|
+
if (Array.isArray(stdio)) {
|
|
1036
|
+
fs.closeSync(stdio[1]);
|
|
1037
|
+
fs.closeSync(stdio[2]);
|
|
1038
|
+
}
|
|
1039
|
+
console.log(`NordRelay dashboard started with PID ${child.pid}.`);
|
|
1040
|
+
console.log(`NordRelay dashboard: ${formatDashboardUrl({ host, port })}`);
|
|
1041
|
+
console.log(`WebUI log: ${options.webLogFile}`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
873
1045
|
const forwardSignal = (signal) => {
|
|
874
1046
|
if (isProcessRunning(child.pid)) {
|
|
875
1047
|
child.kill(signal);
|
|
@@ -878,9 +1050,23 @@ async function commandWeb(options) {
|
|
|
878
1050
|
process.once("SIGINT", () => forwardSignal("SIGINT"));
|
|
879
1051
|
process.once("SIGTERM", () => forwardSignal("SIGTERM"));
|
|
880
1052
|
|
|
881
|
-
const exit = await new Promise((resolve) => {
|
|
1053
|
+
const exit = await new Promise((resolve, reject) => {
|
|
1054
|
+
child.once("error", reject);
|
|
882
1055
|
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
883
1056
|
});
|
|
1057
|
+
const pid = await readWebPid(options);
|
|
1058
|
+
if (pid === child.pid) {
|
|
1059
|
+
await fsp.rm(options.webPidFile, { force: true });
|
|
1060
|
+
}
|
|
1061
|
+
await writeWebState(options, {
|
|
1062
|
+
status: exit.code === 0 ? "stopped" : "error",
|
|
1063
|
+
pid: null,
|
|
1064
|
+
host,
|
|
1065
|
+
port,
|
|
1066
|
+
url: formatDashboardUrl({ host, port }),
|
|
1067
|
+
exitCode: exit.code,
|
|
1068
|
+
signal: exit.signal,
|
|
1069
|
+
});
|
|
884
1070
|
if (exit.signal) {
|
|
885
1071
|
process.kill(process.pid, exit.signal);
|
|
886
1072
|
return;
|
|
@@ -1175,8 +1361,15 @@ async function main() {
|
|
|
1175
1361
|
if (options.command === "update") return commandUpdate(options);
|
|
1176
1362
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
1177
1363
|
if (options.command === "restart") {
|
|
1364
|
+
await mkdirp(options.home);
|
|
1365
|
+
loadEnvFiles(options.home);
|
|
1366
|
+
const webWasRunning = await isWebDashboardRunning(options);
|
|
1178
1367
|
await commandStop(options);
|
|
1179
|
-
|
|
1368
|
+
await commandStart(options);
|
|
1369
|
+
if (webWasRunning && process.exitCode !== 1) {
|
|
1370
|
+
await startWebDashboard(options, { detached: true });
|
|
1371
|
+
}
|
|
1372
|
+
return;
|
|
1180
1373
|
}
|
|
1181
1374
|
if (options.command === "foreground") return commandForeground(options);
|
|
1182
1375
|
if (options.command === "--version" || options.command === "version") {
|