@nordbyte/nordrelay 0.6.0 → 0.8.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 +52 -0
- package/README.md +171 -50
- package/dist/access-control.js +6 -1
- package/dist/activity-events.js +2 -2
- package/dist/adapter-conformance.js +61 -0
- package/dist/bot-preferences.js +1 -0
- package/dist/bot.js +95 -37
- package/dist/channel-adapter.js +44 -11
- package/dist/channel-command-catalog.js +94 -0
- package/dist/channel-command-core.js +60 -0
- package/dist/channel-command-service.js +230 -1
- package/dist/channel-mirror-registry.js +84 -0
- package/dist/channel-peer-prompt.js +95 -0
- package/dist/channel-prompt-engine.js +177 -0
- package/dist/channel-runtime.js +12 -5
- package/dist/channel-turn-lifecycle.js +73 -0
- package/dist/codex-state.js +114 -78
- package/dist/config-metadata.js +82 -8
- package/dist/config.js +79 -7
- package/dist/context-key.js +42 -0
- package/dist/discord-bot.js +173 -342
- package/dist/discord-command-surface.js +11 -73
- package/dist/index.js +29 -0
- package/dist/metrics.js +48 -0
- package/dist/peer-auth.js +85 -0
- package/dist/peer-client.js +288 -0
- package/dist/peer-context.js +21 -0
- package/dist/peer-identity.js +127 -0
- package/dist/peer-readiness.js +77 -0
- package/dist/peer-runtime-service.js +658 -0
- package/dist/peer-server.js +220 -0
- package/dist/peer-store.js +307 -0
- package/dist/peer-types.js +52 -0
- package/dist/relay-runtime-helpers.js +210 -0
- package/dist/relay-runtime.js +79 -274
- package/dist/remote-prompt.js +98 -0
- package/dist/settings-wizard-test.js +216 -0
- package/dist/slack-artifacts.js +165 -0
- package/dist/slack-bot.js +1461 -0
- package/dist/slack-channel-runtime.js +147 -0
- package/dist/slack-command-surface.js +46 -0
- package/dist/slack-diagnostics.js +116 -0
- package/dist/slack-rate-limit.js +139 -0
- package/dist/telegram-command-menu.js +3 -53
- package/dist/telegram-general-commands.js +14 -0
- package/dist/telegram-preference-commands.js +23 -127
- package/dist/user-management-crypto.js +38 -0
- package/dist/user-management-normalize.js +188 -0
- package/dist/user-management-types.js +1 -0
- package/dist/user-management.js +193 -196
- package/dist/web-api-contract.js +16 -0
- package/dist/web-dashboard-access-routes.js +62 -0
- package/dist/web-dashboard-assets.js +1 -0
- package/dist/web-dashboard-pages.js +26 -4
- package/dist/web-dashboard-peer-routes.js +225 -0
- package/dist/web-dashboard-ui.js +1 -0
- package/dist/web-dashboard.js +46 -0
- package/dist/web-state.js +2 -2
- package/dist/webui-assets/dashboard.css +193 -0
- package/dist/webui-assets/dashboard.js +870 -57
- package/package.json +5 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +468 -11
|
@@ -35,6 +35,10 @@ function readRuntimePackageVersion() {
|
|
|
35
35
|
function parseArgs(argv) {
|
|
36
36
|
const copy = [...argv];
|
|
37
37
|
let command = "foreground";
|
|
38
|
+
if (copy[0] === "--help" || copy[0] === "-h") {
|
|
39
|
+
command = "help";
|
|
40
|
+
copy.shift();
|
|
41
|
+
}
|
|
38
42
|
if (copy[0] === "--version" || copy[0] === "-v") {
|
|
39
43
|
command = "version";
|
|
40
44
|
copy.shift();
|
|
@@ -53,6 +57,7 @@ function parseArgs(argv) {
|
|
|
53
57
|
port: undefined,
|
|
54
58
|
restartAfterUpdate: true,
|
|
55
59
|
updateMethod: undefined,
|
|
60
|
+
buildBeforeStart: false,
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
for (let i = 0; i < copy.length; i += 1) {
|
|
@@ -63,6 +68,7 @@ function parseArgs(argv) {
|
|
|
63
68
|
else if (arg === "--host") options.host = requireValue(copy, ++i, arg);
|
|
64
69
|
else if (arg === "--port") options.port = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
65
70
|
else if (arg === "--method") options.updateMethod = requireValue(copy, ++i, arg);
|
|
71
|
+
else if (arg === "--build") options.buildBeforeStart = true;
|
|
66
72
|
else if (arg === "--no-restart") options.restartAfterUpdate = false;
|
|
67
73
|
else if (arg === "--restart") options.restartAfterUpdate = true;
|
|
68
74
|
else if (arg === "--token") options.telegramBotToken = requireValue(copy, ++i, arg);
|
|
@@ -70,10 +76,16 @@ function parseArgs(argv) {
|
|
|
70
76
|
else if (arg === "--enable-discord") options.enableDiscord = true;
|
|
71
77
|
else if (arg === "--discord-token") options.discordBotToken = requireValue(copy, ++i, arg);
|
|
72
78
|
else if (arg === "--discord-client-id") options.discordClientId = requireValue(copy, ++i, arg);
|
|
79
|
+
else if (arg === "--enable-slack") options.enableSlack = true;
|
|
80
|
+
else if (arg === "--slack-bot-token") options.slackBotToken = requireValue(copy, ++i, arg);
|
|
81
|
+
else if (arg === "--slack-app-token") options.slackAppToken = requireValue(copy, ++i, arg);
|
|
82
|
+
else if (arg === "--slack-signing-secret") options.slackSigningSecret = requireValue(copy, ++i, arg);
|
|
73
83
|
else if (arg === "--admin-email") options.adminEmail = requireValue(copy, ++i, arg);
|
|
74
84
|
else if (arg === "--admin-name") options.adminName = requireValue(copy, ++i, arg);
|
|
75
85
|
else if (arg === "--admin-password") options.adminPassword = requireValue(copy, ++i, arg);
|
|
76
86
|
else if (arg === "--telegram-user-id") options.telegramUserId = requireValue(copy, ++i, arg);
|
|
87
|
+
else if (arg === "--slack-user-id") options.slackUserId = requireValue(copy, ++i, arg);
|
|
88
|
+
else if (arg === "--slack-team-id") options.slackTeamId = requireValue(copy, ++i, arg);
|
|
77
89
|
else if (arg === "--state-backend") options.stateBackend = requireValue(copy, ++i, arg);
|
|
78
90
|
else if (arg === "--enable-pi") options.enablePi = true;
|
|
79
91
|
else if (arg === "--enable-hermes") options.enableHermes = true;
|
|
@@ -226,6 +238,7 @@ function formatDashboardUrl(endpoint) {
|
|
|
226
238
|
async function commandStart(options, settings = {}) {
|
|
227
239
|
await mkdirp(options.home);
|
|
228
240
|
loadEnvFiles(options.home);
|
|
241
|
+
await prepareRuntimeForLaunch(options);
|
|
229
242
|
const dashboard = resolveDashboardEndpoint(options);
|
|
230
243
|
|
|
231
244
|
const currentPid = await readPid(options.pidFile);
|
|
@@ -243,7 +256,7 @@ async function commandStart(options, settings = {}) {
|
|
|
243
256
|
});
|
|
244
257
|
|
|
245
258
|
const logFd = fs.openSync(options.logFile, "a");
|
|
246
|
-
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...options.rawFlags], {
|
|
259
|
+
const child = spawn(process.execPath, [SCRIPT_PATH, "foreground", ...runtimeForwardFlags(options.rawFlags)], {
|
|
247
260
|
cwd: RUNTIME_ROOT,
|
|
248
261
|
detached: true,
|
|
249
262
|
env: process.env,
|
|
@@ -410,6 +423,8 @@ async function commandStatus(options) {
|
|
|
410
423
|
console.log(`OpenClaw CLI: ${state.openClawCli || "-"}`);
|
|
411
424
|
console.log(`Claude Code CLI: ${state.claudeCodeCli || "-"}`);
|
|
412
425
|
console.log(`OpenClaw Gateway: ${state.openClawGateway || process.env.OPENCLAW_GATEWAY_URL || "-"}`);
|
|
426
|
+
console.log(`Peers: ${state.peerEnabled ? state.peerUrl || "enabled" : "disabled"}`);
|
|
427
|
+
if (state.peerTlsFingerprint) console.log(`Peer TLS fingerprint: ${state.peerTlsFingerprint}`);
|
|
413
428
|
console.log(`WebUI: ${formatDashboardUrl(dashboard)} (${webStatus})`);
|
|
414
429
|
console.log(`Log: ${options.logFile}`);
|
|
415
430
|
console.log(`WebUI log: ${options.webLogFile}`);
|
|
@@ -675,10 +690,22 @@ async function commandInit(options) {
|
|
|
675
690
|
const discordClientId = enableDiscord === "true"
|
|
676
691
|
? options.discordClientId || process.env.DISCORD_CLIENT_ID || await ask(null, "Discord client ID", "")
|
|
677
692
|
: "";
|
|
693
|
+
const enableSlack = options.enableSlack ? "true" : await askChoice(null, "Enable Slack", "false");
|
|
694
|
+
const slackBotToken = enableSlack === "true"
|
|
695
|
+
? options.slackBotToken || process.env.SLACK_BOT_TOKEN || await ask(null, "Slack bot token", "")
|
|
696
|
+
: "";
|
|
697
|
+
const slackAppToken = enableSlack === "true"
|
|
698
|
+
? options.slackAppToken || process.env.SLACK_APP_TOKEN || await ask(null, "Slack app-level token for Socket Mode", "")
|
|
699
|
+
: "";
|
|
700
|
+
const slackSigningSecret = enableSlack === "true"
|
|
701
|
+
? options.slackSigningSecret || process.env.SLACK_SIGNING_SECRET || await ask(null, "Slack signing secret (optional for Socket Mode)", "")
|
|
702
|
+
: "";
|
|
678
703
|
const adminEmail = options.adminEmail || await ask(null, "Admin email", "");
|
|
679
704
|
const adminName = options.adminName || await ask(null, "Admin name", "Admin");
|
|
680
705
|
const adminPassword = options.adminPassword || await askSecret(null, "Admin password", "");
|
|
681
706
|
const telegramUserId = options.telegramUserId || await ask(null, "Optional Telegram user id to link", "");
|
|
707
|
+
const slackUserId = options.slackUserId || await ask(null, "Optional Slack user id to link", "");
|
|
708
|
+
const slackTeamId = slackUserId ? (options.slackTeamId || await ask(null, "Optional Slack team id for linked user", "")) : "";
|
|
682
709
|
const enableCodex = options.disableCodex ? "false" : await askChoice(null, "Enable Codex", "true");
|
|
683
710
|
const enablePi = options.enablePi ? "true" : await askChoice(null, "Enable Pi", "false");
|
|
684
711
|
const enableHermes = options.enableHermes ? "true" : await askChoice(null, "Enable Hermes", "false");
|
|
@@ -688,7 +715,9 @@ async function commandInit(options) {
|
|
|
688
715
|
|
|
689
716
|
if (enableTelegram === "true" && !telegramBotToken) throw new Error("Telegram bot token is required when Telegram is enabled.");
|
|
690
717
|
if (enableDiscord === "true" && !discordBotToken) throw new Error("Discord bot token is required when Discord is enabled.");
|
|
691
|
-
if (
|
|
718
|
+
if (enableSlack === "true" && !slackBotToken) throw new Error("Slack bot token is required when Slack is enabled.");
|
|
719
|
+
if (enableSlack === "true" && !slackAppToken) throw new Error("Slack app-level token is required for default Socket Mode.");
|
|
720
|
+
if (enableTelegram !== "true" && enableDiscord !== "true" && enableSlack !== "true") throw new Error("At least one chat adapter must be enabled.");
|
|
692
721
|
if (!adminEmail) throw new Error("Admin email is required.");
|
|
693
722
|
if (!adminPassword) throw new Error("Admin password is required.");
|
|
694
723
|
if (enableCodex !== "true" && enablePi !== "true" && enableHermes !== "true" && enableOpenClaw !== "true" && enableClaudeCode !== "true") throw new Error("At least one agent must be enabled.");
|
|
@@ -713,6 +742,13 @@ async function commandInit(options) {
|
|
|
713
742
|
"DISCORD_COMMAND_MODE=both",
|
|
714
743
|
"DISCORD_MESSAGE_CONTENT_ENABLED=true",
|
|
715
744
|
"DISCORD_AUTO_REGISTER_COMMANDS=true",
|
|
745
|
+
`SLACK_ENABLED=${enableSlack}`,
|
|
746
|
+
`SLACK_BOT_TOKEN=${slackBotToken}`,
|
|
747
|
+
`SLACK_APP_TOKEN=${slackAppToken}`,
|
|
748
|
+
`SLACK_SIGNING_SECRET=${slackSigningSecret}`,
|
|
749
|
+
"SLACK_SOCKET_MODE=true",
|
|
750
|
+
"SLACK_MESSAGE_CONTENT_ENABLED=true",
|
|
751
|
+
"SLACK_AUTO_SEND_ARTIFACTS=false",
|
|
716
752
|
`NORDRELAY_CODEX_ENABLED=${enableCodex}`,
|
|
717
753
|
`NORDRELAY_PI_ENABLED=${enablePi}`,
|
|
718
754
|
`NORDRELAY_HERMES_ENABLED=${enableHermes}`,
|
|
@@ -727,6 +763,11 @@ async function commandInit(options) {
|
|
|
727
763
|
"OPENCLAW_DEFAULT_PROFILE=default",
|
|
728
764
|
"CLAUDE_CODE_DEFAULT_PROFILE=default",
|
|
729
765
|
"CLAUDE_CODE_MAX_TURNS=100",
|
|
766
|
+
"NORDRELAY_PEER_ENABLED=false",
|
|
767
|
+
"NORDRELAY_PEER_HOST=127.0.0.1",
|
|
768
|
+
"NORDRELAY_PEER_PORT=31979",
|
|
769
|
+
"NORDRELAY_PEER_TLS_ENABLED=true",
|
|
770
|
+
"NORDRELAY_PEER_REQUIRE_TLS=true",
|
|
730
771
|
`NORDRELAY_STATE_BACKEND=${stateBackend === "sqlite" ? "sqlite" : "json"}`,
|
|
731
772
|
"TELEGRAM_TRANSPORT=polling",
|
|
732
773
|
"TELEGRAM_AUTO_SEND_ARTIFACTS=false",
|
|
@@ -740,6 +781,8 @@ async function commandInit(options) {
|
|
|
740
781
|
displayName: adminName || adminEmail,
|
|
741
782
|
password: adminPassword,
|
|
742
783
|
telegramUserId: telegramUserId ? Number(telegramUserId) : undefined,
|
|
784
|
+
slackUserId: slackUserId || undefined,
|
|
785
|
+
slackTeamId: slackTeamId || undefined,
|
|
743
786
|
});
|
|
744
787
|
console.log(`Wrote ${envPath}`);
|
|
745
788
|
console.log(`Created admin user ${adminEmail}.`);
|
|
@@ -755,6 +798,165 @@ async function createUserStore(home) {
|
|
|
755
798
|
return new mod.UserStore(home);
|
|
756
799
|
}
|
|
757
800
|
|
|
801
|
+
async function peerModules() {
|
|
802
|
+
const required = [
|
|
803
|
+
"peer-store.js",
|
|
804
|
+
"peer-identity.js",
|
|
805
|
+
"peer-client.js",
|
|
806
|
+
];
|
|
807
|
+
for (const file of required) {
|
|
808
|
+
const modulePath = path.join(RUNTIME_ROOT, "dist", file);
|
|
809
|
+
if (!fs.existsSync(modulePath)) {
|
|
810
|
+
throw new Error(`Missing peer runtime. Run \`npm run build\` in ${RUNTIME_ROOT}.`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const [store, identity, client] = await Promise.all(required.map((file) => import(pathToFileURL(path.join(RUNTIME_ROOT, "dist", file)).href)));
|
|
814
|
+
return { store, identity, client };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function parsePeerFlags(argv) {
|
|
818
|
+
const copy = [...argv];
|
|
819
|
+
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
820
|
+
const flags = { subcommand, url: undefined };
|
|
821
|
+
if (["add", "test", "check", "revoke"].includes(subcommand) && copy[0] && !copy[0].startsWith("-")) {
|
|
822
|
+
flags.url = copy.shift();
|
|
823
|
+
flags.id = flags.url;
|
|
824
|
+
}
|
|
825
|
+
for (let i = 0; i < copy.length; i += 1) {
|
|
826
|
+
const arg = copy[i];
|
|
827
|
+
if (arg === "--name") flags.name = requireValue(copy, ++i, arg);
|
|
828
|
+
else if (arg === "--code") flags.code = requireValue(copy, ++i, arg);
|
|
829
|
+
else if (arg === "--expect-fingerprint") flags.expectFingerprint = requireValue(copy, ++i, arg);
|
|
830
|
+
else if (arg === "--public-url") flags.publicUrl = requireValue(copy, ++i, arg);
|
|
831
|
+
else if (arg === "--expires" || arg === "--expires-minutes") flags.expiresMinutes = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
832
|
+
else if (arg === "--scopes") flags.scopes = requireValue(copy, ++i, arg);
|
|
833
|
+
else if (arg === "--agents") flags.agents = requireValue(copy, ++i, arg);
|
|
834
|
+
else if (arg === "--workspaces") flags.workspaces = requireValue(copy, ++i, arg);
|
|
835
|
+
else if (arg === "--workspace-aliases" || arg === "--aliases") flags.workspaceAliases = requireValue(copy, ++i, arg);
|
|
836
|
+
}
|
|
837
|
+
return flags;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function csv(value) {
|
|
841
|
+
return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : undefined;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function aliasMap(value) {
|
|
845
|
+
if (!value) return undefined;
|
|
846
|
+
return Object.fromEntries(value.split(",").map((item) => item.split("=", 2).map((part) => part.trim())).filter(([alias, workspace]) => alias && workspace));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function commandPeer(options) {
|
|
850
|
+
await mkdirp(options.home);
|
|
851
|
+
loadEnvFiles(options.home);
|
|
852
|
+
const flags = parsePeerFlags(options.rawFlags);
|
|
853
|
+
const { store: storeMod, identity: identityMod, client: clientMod } = await peerModules();
|
|
854
|
+
const store = new storeMod.PeerStore(options.home);
|
|
855
|
+
const identity = identityMod.loadOrCreatePeerIdentity(options.home, process.env.NORDRELAY_PEER_NAME);
|
|
856
|
+
|
|
857
|
+
if (flags.subcommand === "identity") {
|
|
858
|
+
console.log(`Node ID: ${identity.public.nodeId}`);
|
|
859
|
+
console.log(`Name: ${identity.public.name}`);
|
|
860
|
+
console.log(`Fingerprint: ${identity.public.fingerprint}`);
|
|
861
|
+
console.log(`Created: ${identity.public.createdAt}`);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (flags.subcommand === "list") {
|
|
866
|
+
const peers = store.listPublic();
|
|
867
|
+
if (peers.length === 0) {
|
|
868
|
+
console.log("No peers configured.");
|
|
869
|
+
console.log("Create an invite with `nordrelay peer invite` or add a peer with `nordrelay peer add <url> --code <code>`.");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
for (const peer of peers) {
|
|
873
|
+
console.log(`${peer.id} ${peer.enabled ? "enabled" : "disabled"} ${peer.name}`);
|
|
874
|
+
console.log(` URL: ${peer.url || "-"}`);
|
|
875
|
+
console.log(` Node: ${peer.nodeId} ${peer.fingerprint}`);
|
|
876
|
+
console.log(` Direction: ${peer.direction}`);
|
|
877
|
+
console.log(` Scopes: ${peer.scopes.join(",") || "-"}`);
|
|
878
|
+
console.log(` Agents: ${peer.allowedAgents.join(",") || "all"}`);
|
|
879
|
+
const aliases = Object.entries(peer.workspaceAliases || {}).map(([alias, workspace]) => `${alias}=${workspace}`).join(",");
|
|
880
|
+
if (aliases) console.log(` Workspace aliases: ${aliases}`);
|
|
881
|
+
if (peer.lastSeenAt) console.log(` Last seen: ${peer.lastSeenAt}`);
|
|
882
|
+
if (peer.lastLatencyMs !== undefined) console.log(` Latency: ${peer.lastLatencyMs}ms`);
|
|
883
|
+
if (peer.remoteVersion) console.log(` Remote version: ${peer.remoteVersion}`);
|
|
884
|
+
if (peer.lastError) console.log(` Last error: ${peer.lastError}`);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (flags.subcommand === "invite") {
|
|
890
|
+
const url = process.env.NORDRELAY_PEER_PUBLIC_URL || `${process.env.NORDRELAY_PEER_TLS_ENABLED === "false" ? "http" : "https"}://${process.env.NORDRELAY_PEER_HOST || "127.0.0.1"}:${process.env.NORDRELAY_PEER_PORT || "31979"}`;
|
|
891
|
+
const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
|
|
892
|
+
if (!peerEnabled) {
|
|
893
|
+
console.log("Warning: peer server is disabled. The invite can be created, but pairing will fail until NORDRELAY_PEER_ENABLED=true and NordRelay is restarted.");
|
|
894
|
+
} else {
|
|
895
|
+
const probe = await clientMod.checkPeerEndpoint(url, { timeoutMs: 2500 });
|
|
896
|
+
if (!probe.ok) {
|
|
897
|
+
console.log(`Warning: peer endpoint is not reachable from this machine: ${probe.detail}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const created = store.createInvitation({
|
|
901
|
+
name: flags.name,
|
|
902
|
+
expiresInMs: Number.isFinite(flags.expiresMinutes) ? flags.expiresMinutes * 60 * 1000 : undefined,
|
|
903
|
+
scopes: csv(flags.scopes),
|
|
904
|
+
allowedAgents: csv(flags.agents),
|
|
905
|
+
allowedWorkspaceRoots: csv(flags.workspaces),
|
|
906
|
+
workspaceAliases: aliasMap(flags.workspaceAliases),
|
|
907
|
+
});
|
|
908
|
+
console.log(`Pairing code: ${created.code}`);
|
|
909
|
+
console.log(`Expires: ${created.invitation.expiresAt}`);
|
|
910
|
+
console.log(`Fingerprint: ${identity.public.fingerprint}`);
|
|
911
|
+
console.log(`Command: nordrelay peer add ${url} --code ${created.code}`);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (flags.subcommand === "add") {
|
|
916
|
+
const url = flags.url || await ask(null, "Peer URL", "");
|
|
917
|
+
const code = flags.code || await ask(null, "Pairing code", "");
|
|
918
|
+
const result = await clientMod.pairPeer({
|
|
919
|
+
url,
|
|
920
|
+
code,
|
|
921
|
+
name: flags.name,
|
|
922
|
+
publicUrl: flags.publicUrl,
|
|
923
|
+
}, identity, store);
|
|
924
|
+
console.log(`Added peer ${result.peer.name} (${result.peer.id}).`);
|
|
925
|
+
console.log(`Node: ${result.peer.nodeId}`);
|
|
926
|
+
console.log(`Fingerprint: ${result.peer.fingerprint}`);
|
|
927
|
+
if (result.tlsFingerprint) console.log(`TLS fingerprint: ${result.tlsFingerprint}`);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (flags.subcommand === "test") {
|
|
932
|
+
const id = flags.id || await ask(null, "Peer id", "");
|
|
933
|
+
const response = await new clientMod.RemoteRelayClient(store).rpc(id, "peer.ping");
|
|
934
|
+
console.log(`Peer ${id} ok: ${JSON.stringify(response)}`);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (flags.subcommand === "check") {
|
|
939
|
+
const url = flags.url || await ask(null, "Peer URL", "");
|
|
940
|
+
const probe = await clientMod.checkPeerEndpoint(url, { expectedTlsFingerprint: flags.expectFingerprint });
|
|
941
|
+
console.log(`Peer endpoint: ${probe.url}`);
|
|
942
|
+
console.log(`Status: ${probe.ok ? "reachable" : "unreachable"}`);
|
|
943
|
+
if (probe.latencyMs !== undefined) console.log(`Latency: ${probe.latencyMs}ms`);
|
|
944
|
+
if (probe.statusCode !== undefined) console.log(`HTTP status: ${probe.statusCode}`);
|
|
945
|
+
if (probe.tlsFingerprint) console.log(`TLS fingerprint: ${probe.tlsFingerprint}`);
|
|
946
|
+
console.log(`Detail: ${probe.detail}`);
|
|
947
|
+
if (!probe.ok) process.exitCode = 1;
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (flags.subcommand === "revoke") {
|
|
952
|
+
const id = flags.id || await ask(null, "Peer id", "");
|
|
953
|
+
console.log(store.revokePeer(id) ? `Revoked peer ${id}.` : `Peer not found: ${id}`);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
throw new Error("Usage: nordrelay peer [identity|list|invite|add|test|check|revoke]");
|
|
958
|
+
}
|
|
959
|
+
|
|
758
960
|
function parseUserFlags(argv) {
|
|
759
961
|
const copy = [...argv];
|
|
760
962
|
const subcommand = copy[0] && !copy[0].startsWith("-") ? copy.shift() : "list";
|
|
@@ -767,6 +969,8 @@ function parseUserFlags(argv) {
|
|
|
767
969
|
else if (arg === "--group" || arg === "--groups") flags.groups = requireValue(copy, ++i, arg);
|
|
768
970
|
else if (arg === "--telegram-user-id") flags.telegramUserId = Number.parseInt(requireValue(copy, ++i, arg), 10);
|
|
769
971
|
else if (arg === "--discord-user-id") flags.discordUserId = requireValue(copy, ++i, arg);
|
|
972
|
+
else if (arg === "--slack-user-id") flags.slackUserId = requireValue(copy, ++i, arg);
|
|
973
|
+
else if (arg === "--slack-team-id") flags.slackTeamId = requireValue(copy, ++i, arg);
|
|
770
974
|
else if (arg === "--user-id") flags.userId = requireValue(copy, ++i, arg);
|
|
771
975
|
}
|
|
772
976
|
return flags;
|
|
@@ -798,8 +1002,8 @@ async function commandUser(options) {
|
|
|
798
1002
|
? ["admin"]
|
|
799
1003
|
: (flags.groups ? flags.groups.split(",").map((item) => item.trim()).filter(Boolean) : ["user"]);
|
|
800
1004
|
const created = flags.subcommand === "create-admin"
|
|
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 });
|
|
1005
|
+
? store.createAdmin({ email, displayName: name, password, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId })
|
|
1006
|
+
: store.createUser({ email, displayName: name, password, groupIds, telegramUserId: flags.telegramUserId, discordUserId: flags.discordUserId, slackUserId: flags.slackUserId, slackTeamId: flags.slackTeamId });
|
|
803
1007
|
console.log(`Created user ${created.user.email} (${created.groups.map((group) => group.name).join(", ")}).`);
|
|
804
1008
|
return;
|
|
805
1009
|
}
|
|
@@ -834,6 +1038,17 @@ async function commandUser(options) {
|
|
|
834
1038
|
return;
|
|
835
1039
|
}
|
|
836
1040
|
|
|
1041
|
+
if (flags.subcommand === "link-slack") {
|
|
1042
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
1043
|
+
const slackUserId = flags.slackUserId || await ask(null, "Slack user id", "");
|
|
1044
|
+
const teamId = flags.slackTeamId || await ask(null, "Slack team id (optional)", "");
|
|
1045
|
+
const user = store.getUserByEmail(email);
|
|
1046
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
1047
|
+
store.linkSlackUser(user.user.id, { slackUserId, teamId: teamId || undefined });
|
|
1048
|
+
console.log(`Linked Slack user ${slackUserId} to ${user.user.email}.`);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
837
1052
|
if (flags.subcommand === "link-code" || flags.subcommand === "telegram-link-code") {
|
|
838
1053
|
const email = flags.email || await ask(null, "Email", "");
|
|
839
1054
|
const user = store.getUserByEmail(email);
|
|
@@ -854,7 +1069,17 @@ async function commandUser(options) {
|
|
|
854
1069
|
return;
|
|
855
1070
|
}
|
|
856
1071
|
|
|
857
|
-
|
|
1072
|
+
if (flags.subcommand === "slack-link-code") {
|
|
1073
|
+
const email = flags.email || await ask(null, "Email", "");
|
|
1074
|
+
const user = store.getUserByEmail(email);
|
|
1075
|
+
if (!user) throw new Error(`User not found: ${email}`);
|
|
1076
|
+
const code = store.createSlackLinkCode(user.user.id);
|
|
1077
|
+
console.log(`Slack link code for ${user.user.email}: ${code.code}`);
|
|
1078
|
+
console.log(`Expires: ${code.expiresAt}`);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
throw new Error("Usage: nordrelay user [list|create-admin|create|reset-password|link-telegram|link-discord|link-slack|link-code|telegram-link-code|discord-link-code|slack-link-code]");
|
|
858
1083
|
}
|
|
859
1084
|
|
|
860
1085
|
async function commandDoctor(options) {
|
|
@@ -864,16 +1089,55 @@ async function commandDoctor(options) {
|
|
|
864
1089
|
const userSnapshot = userStore?.snapshot();
|
|
865
1090
|
const checks = [];
|
|
866
1091
|
checks.push(check("Node.js >= 22", Number.parseInt(process.versions.node.split(".")[0], 10) >= 22, process.version));
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1092
|
+
const telegramRequested = process.env.TELEGRAM_ENABLED !== "false";
|
|
1093
|
+
const discordRequested = process.env.DISCORD_ENABLED === "true";
|
|
1094
|
+
const slackRequested = process.env.SLACK_ENABLED === "true";
|
|
1095
|
+
const slackSocketMode = process.env.SLACK_SOCKET_MODE !== "false";
|
|
1096
|
+
const telegramUsable = telegramRequested && Boolean(process.env.TELEGRAM_BOT_TOKEN);
|
|
1097
|
+
const discordUsable = discordRequested && Boolean(process.env.DISCORD_BOT_TOKEN);
|
|
1098
|
+
const slackUsable = slackRequested && Boolean(process.env.SLACK_BOT_TOKEN) && (slackSocketMode ? Boolean(process.env.SLACK_APP_TOKEN) : Boolean(process.env.SLACK_SIGNING_SECRET));
|
|
1099
|
+
checks.push(check(
|
|
1100
|
+
"Telegram bot token",
|
|
1101
|
+
!telegramRequested || telegramUsable,
|
|
1102
|
+
telegramRequested ? (telegramUsable ? "configured" : "missing; Telegram adapter will be disabled") : "disabled",
|
|
1103
|
+
telegramRequested && !discordUsable && !slackUsable ? "fail" : "warn",
|
|
1104
|
+
));
|
|
1105
|
+
checks.push(check(
|
|
1106
|
+
"Discord bot token",
|
|
1107
|
+
!discordRequested || discordUsable,
|
|
1108
|
+
discordRequested ? (discordUsable ? "configured" : "missing; Discord adapter will be disabled") : "disabled",
|
|
1109
|
+
discordRequested && !telegramUsable && !slackUsable ? "fail" : "warn",
|
|
1110
|
+
));
|
|
1111
|
+
checks.push(check(
|
|
1112
|
+
"Slack bot token",
|
|
1113
|
+
!slackRequested || Boolean(process.env.SLACK_BOT_TOKEN),
|
|
1114
|
+
slackRequested ? (process.env.SLACK_BOT_TOKEN ? "configured" : "missing; Slack adapter will be disabled") : "disabled",
|
|
1115
|
+
slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
|
|
1116
|
+
));
|
|
1117
|
+
checks.push(check(
|
|
1118
|
+
slackSocketMode ? "Slack app token" : "Slack signing secret",
|
|
1119
|
+
!slackRequested || slackUsable,
|
|
1120
|
+
slackRequested ? (slackUsable ? "configured" : `missing; ${slackSocketMode ? "Socket Mode requires SLACK_APP_TOKEN" : "HTTP mode requires SLACK_SIGNING_SECRET"}`) : "disabled",
|
|
1121
|
+
slackRequested && !telegramUsable && !discordUsable ? "fail" : "warn",
|
|
1122
|
+
));
|
|
1123
|
+
checks.push(check(
|
|
1124
|
+
"Usable chat adapter",
|
|
1125
|
+
telegramUsable || discordUsable || slackUsable,
|
|
1126
|
+
[telegramUsable ? "Telegram" : "", discordUsable ? "Discord" : "", slackUsable ? "Slack" : ""].filter(Boolean).join(" and ") || "none",
|
|
1127
|
+
"fail",
|
|
1128
|
+
));
|
|
1129
|
+
checks.push(check("Discord client ID", !discordUsable || Boolean(process.env.DISCORD_CLIENT_ID), discordUsable ? (process.env.DISCORD_CLIENT_ID ? "configured" : "missing; slash command auto-registration disabled") : "disabled", "warn"));
|
|
872
1130
|
checks.push(check("User store", Boolean(userStore), userStore ? userStore.filePath : "missing runtime", userStore ? "pass" : "fail"));
|
|
873
1131
|
checks.push(check("Admin user", Boolean(userSnapshot?.adminConfigured), userSnapshot?.adminConfigured ? "configured" : "missing"));
|
|
874
1132
|
checks.push(check("WebUI login", true, "required for every dashboard request"));
|
|
875
1133
|
checks.push(check("Telegram access", true, "requires linked active users and enabled group chats"));
|
|
876
1134
|
checks.push(check("Discord access", true, "requires linked active users and enabled channels"));
|
|
1135
|
+
checks.push(check("Slack access", true, "requires linked active users and enabled channels"));
|
|
1136
|
+
const peerEnabled = process.env.NORDRELAY_PEER_ENABLED === "true";
|
|
1137
|
+
const peerTlsEnabled = process.env.NORDRELAY_PEER_TLS_ENABLED !== "false";
|
|
1138
|
+
const peerHost = process.env.NORDRELAY_PEER_HOST || "127.0.0.1";
|
|
1139
|
+
checks.push(check("Peer server", peerEnabled, peerEnabled ? `${peerHost}:${process.env.NORDRELAY_PEER_PORT || "31979"}` : "disabled", "warn"));
|
|
1140
|
+
checks.push(check("Peer TLS", !peerEnabled || peerTlsEnabled || isLoopbackName(peerHost), peerTlsEnabled ? "enabled" : "plaintext loopback only", peerEnabled ? "fail" : "warn"));
|
|
877
1141
|
checks.push(check("Codex enabled flag", process.env.NORDRELAY_CODEX_ENABLED !== "false", `NORDRELAY_CODEX_ENABLED=${process.env.NORDRELAY_CODEX_ENABLED ?? "true"}`));
|
|
878
1142
|
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"));
|
|
879
1143
|
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"));
|
|
@@ -978,6 +1242,7 @@ async function checkOpenClawGateway() {
|
|
|
978
1242
|
async function commandWeb(options) {
|
|
979
1243
|
await mkdirp(options.home);
|
|
980
1244
|
loadEnvFiles(options.home);
|
|
1245
|
+
await prepareRuntimeForLaunch(options);
|
|
981
1246
|
await ensureConnectorStartedForWeb(options);
|
|
982
1247
|
await startWebDashboard(options, { detached: false });
|
|
983
1248
|
}
|
|
@@ -1077,6 +1342,7 @@ async function startWebDashboard(options, settings = {}) {
|
|
|
1077
1342
|
async function commandForeground(options) {
|
|
1078
1343
|
await mkdirp(options.home);
|
|
1079
1344
|
loadEnvFiles(options.home);
|
|
1345
|
+
await prepareRuntimeForLaunch(options);
|
|
1080
1346
|
process.chdir(RUNTIME_ROOT);
|
|
1081
1347
|
|
|
1082
1348
|
await writeJsonAtomic(options.stateFile, {
|
|
@@ -1162,6 +1428,157 @@ async function resolveRuntimeEntry() {
|
|
|
1162
1428
|
return null;
|
|
1163
1429
|
}
|
|
1164
1430
|
|
|
1431
|
+
async function prepareRuntimeForLaunch(options) {
|
|
1432
|
+
if (options.buildBeforeStart) {
|
|
1433
|
+
await buildRuntime();
|
|
1434
|
+
options.buildBeforeStart = false;
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
warnIfRuntimeBuildIsStale();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function runtimeForwardFlags(flags) {
|
|
1441
|
+
return flags.filter((flag) => flag !== "--build");
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function buildRuntime() {
|
|
1445
|
+
if (!isSourceRuntime()) {
|
|
1446
|
+
throw new Error(`Runtime build is only available from a source checkout. Current runtime: ${RUNTIME_ROOT}`);
|
|
1447
|
+
}
|
|
1448
|
+
const npm = resolveNpmSpawnCommand();
|
|
1449
|
+
if (!npm) {
|
|
1450
|
+
throw new Error("npm was not found. Install Node.js/npm or add npm to PATH.");
|
|
1451
|
+
}
|
|
1452
|
+
console.log("Building NordRelay runtime...");
|
|
1453
|
+
await runInteractiveStep("Build runtime", npm.command, [...npm.argsPrefix, "run", "build"], {
|
|
1454
|
+
cwd: RUNTIME_ROOT,
|
|
1455
|
+
shell: npm.shell,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function warnIfRuntimeBuildIsStale() {
|
|
1460
|
+
const status = runtimeBuildStatus();
|
|
1461
|
+
if (!status || !status.stale) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const source = status.sourcePath ? path.relative(RUNTIME_ROOT, status.sourcePath) : "source files";
|
|
1465
|
+
const target = status.targetPath ? path.relative(RUNTIME_ROOT, status.targetPath) : "dist";
|
|
1466
|
+
const reason = status.missing
|
|
1467
|
+
? `missing ${target}`
|
|
1468
|
+
: `${source} is newer than ${target}`;
|
|
1469
|
+
console.warn(`Warning: NordRelay runtime build may be stale (${reason}). Run \`nordrelay restart --build\` or \`npm run build\`.`);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function runtimeBuildStatus() {
|
|
1473
|
+
if (!isSourceRuntime()) {
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
const source = newestMtime([
|
|
1477
|
+
path.join(RUNTIME_ROOT, "src"),
|
|
1478
|
+
path.join(RUNTIME_ROOT, "plugins", "nordrelay", "scripts"),
|
|
1479
|
+
path.join(RUNTIME_ROOT, "scripts"),
|
|
1480
|
+
path.join(RUNTIME_ROOT, "package.json"),
|
|
1481
|
+
path.join(RUNTIME_ROOT, "tsconfig.json"),
|
|
1482
|
+
path.join(RUNTIME_ROOT, "tsconfig.webui.json"),
|
|
1483
|
+
]);
|
|
1484
|
+
const distTargets = [
|
|
1485
|
+
path.join(RUNTIME_ROOT, "dist", "index.js"),
|
|
1486
|
+
path.join(RUNTIME_ROOT, "dist", "web-dashboard.js"),
|
|
1487
|
+
path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.js"),
|
|
1488
|
+
path.join(RUNTIME_ROOT, "dist", "webui-assets", "dashboard.css"),
|
|
1489
|
+
];
|
|
1490
|
+
const missingTarget = distTargets.find((target) => !fs.existsSync(target));
|
|
1491
|
+
if (missingTarget) {
|
|
1492
|
+
return {
|
|
1493
|
+
stale: true,
|
|
1494
|
+
missing: true,
|
|
1495
|
+
sourcePath: source.path,
|
|
1496
|
+
sourceMtimeMs: source.mtimeMs,
|
|
1497
|
+
targetPath: missingTarget,
|
|
1498
|
+
targetMtimeMs: 0,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const target = oldestMtime(distTargets);
|
|
1502
|
+
return {
|
|
1503
|
+
stale: source.mtimeMs > target.mtimeMs,
|
|
1504
|
+
missing: false,
|
|
1505
|
+
sourcePath: source.path,
|
|
1506
|
+
sourceMtimeMs: source.mtimeMs,
|
|
1507
|
+
targetPath: target.path,
|
|
1508
|
+
targetMtimeMs: target.mtimeMs,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function isSourceRuntime() {
|
|
1513
|
+
return fs.existsSync(path.join(RUNTIME_ROOT, "src", "index.ts")) &&
|
|
1514
|
+
fs.existsSync(path.join(RUNTIME_ROOT, "scripts", "build-web-assets.mjs"));
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function newestMtime(paths) {
|
|
1518
|
+
let newest = { path: null, mtimeMs: 0 };
|
|
1519
|
+
for (const itemPath of paths) {
|
|
1520
|
+
const candidate = newestMtimeForPath(itemPath);
|
|
1521
|
+
if (candidate.mtimeMs > newest.mtimeMs) {
|
|
1522
|
+
newest = candidate;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return newest;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function newestMtimeForPath(itemPath) {
|
|
1529
|
+
if (!fs.existsSync(itemPath)) {
|
|
1530
|
+
return { path: itemPath, mtimeMs: 0 };
|
|
1531
|
+
}
|
|
1532
|
+
const stat = fs.statSync(itemPath);
|
|
1533
|
+
if (!stat.isDirectory()) {
|
|
1534
|
+
return { path: itemPath, mtimeMs: stat.mtimeMs };
|
|
1535
|
+
}
|
|
1536
|
+
let newest = { path: itemPath, mtimeMs: stat.mtimeMs };
|
|
1537
|
+
for (const entry of fs.readdirSync(itemPath, { withFileTypes: true })) {
|
|
1538
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
const candidate = newestMtimeForPath(path.join(itemPath, entry.name));
|
|
1542
|
+
if (candidate.mtimeMs > newest.mtimeMs) {
|
|
1543
|
+
newest = candidate;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return newest;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function oldestMtime(paths) {
|
|
1550
|
+
let oldest = { path: null, mtimeMs: Number.POSITIVE_INFINITY };
|
|
1551
|
+
for (const itemPath of paths) {
|
|
1552
|
+
const mtimeMs = fs.statSync(itemPath).mtimeMs;
|
|
1553
|
+
if (mtimeMs < oldest.mtimeMs) {
|
|
1554
|
+
oldest = { path: itemPath, mtimeMs };
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return oldest;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
async function runInteractiveStep(label, command, args, settings = {}) {
|
|
1561
|
+
console.log(`${label}: ${formatCommand(command, args)}`);
|
|
1562
|
+
const useShell = Boolean(settings.shell);
|
|
1563
|
+
const child = spawn(useShell ? formatShellCommand(command, args) : command, useShell ? [] : args, {
|
|
1564
|
+
cwd: settings.cwd || RUNTIME_ROOT,
|
|
1565
|
+
env: process.env,
|
|
1566
|
+
shell: useShell,
|
|
1567
|
+
stdio: "inherit",
|
|
1568
|
+
windowsHide: false,
|
|
1569
|
+
});
|
|
1570
|
+
const exit = await new Promise((resolve, reject) => {
|
|
1571
|
+
child.once("error", reject);
|
|
1572
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
1573
|
+
});
|
|
1574
|
+
if (exit.signal) {
|
|
1575
|
+
throw new Error(`${label} stopped with signal ${exit.signal}`);
|
|
1576
|
+
}
|
|
1577
|
+
if (exit.code !== 0) {
|
|
1578
|
+
throw new Error(`${label} failed with exit code ${exit.code ?? "unknown"}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1165
1582
|
async function resolveWebRuntimeEntry() {
|
|
1166
1583
|
const distEntry = path.join(RUNTIME_ROOT, "dist", "web-dashboard.js");
|
|
1167
1584
|
if (fs.existsSync(distEntry)) {
|
|
@@ -1307,6 +1724,10 @@ function isWindowsShellScript(filePath) {
|
|
|
1307
1724
|
return process.platform === "win32" && /\.(?:cmd|bat)$/i.test(filePath);
|
|
1308
1725
|
}
|
|
1309
1726
|
|
|
1727
|
+
function isLoopbackName(host) {
|
|
1728
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1310
1731
|
function validateStateBackend() {
|
|
1311
1732
|
const backend = process.env.NORDRELAY_STATE_BACKEND || "json";
|
|
1312
1733
|
if (backend === "json") return { ok: true, detail: "NORDRELAY_STATE_BACKEND=json" };
|
|
@@ -1350,19 +1771,54 @@ function sleep(ms) {
|
|
|
1350
1771
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1351
1772
|
}
|
|
1352
1773
|
|
|
1774
|
+
function printHelp() {
|
|
1775
|
+
console.log(`${APP_NAME} ${VERSION}`);
|
|
1776
|
+
console.log("");
|
|
1777
|
+
console.log("Usage: nordrelay <command> [options]");
|
|
1778
|
+
console.log("");
|
|
1779
|
+
console.log("Commands:");
|
|
1780
|
+
console.log(" init Create local config and first admin user");
|
|
1781
|
+
console.log(" user Manage users, groups, and channel links");
|
|
1782
|
+
console.log(" peer Manage secure NordRelay peer federation");
|
|
1783
|
+
console.log(" doctor Validate the local setup");
|
|
1784
|
+
console.log(" web, dashboard Start the WebUI and connector");
|
|
1785
|
+
console.log(" start Start the connector");
|
|
1786
|
+
console.log(" stop Stop the connector and WebUI");
|
|
1787
|
+
console.log(" restart Restart the connector");
|
|
1788
|
+
console.log(" status Show connector and WebUI status");
|
|
1789
|
+
console.log(" update Update NordRelay");
|
|
1790
|
+
console.log(" foreground Run the connector in the foreground");
|
|
1791
|
+
console.log(" version Print the installed version");
|
|
1792
|
+
console.log("");
|
|
1793
|
+
console.log("Options:");
|
|
1794
|
+
console.log(" --home <path> Runtime home directory");
|
|
1795
|
+
console.log(" --host <host> WebUI bind host");
|
|
1796
|
+
console.log(" --port <port> WebUI port");
|
|
1797
|
+
console.log(" --build Build source runtime before start/web/restart");
|
|
1798
|
+
console.log(" --force Overwrite existing config during init");
|
|
1799
|
+
console.log(" --help, -h Show this help");
|
|
1800
|
+
console.log(" --version, -v Show the installed version");
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1353
1803
|
async function main() {
|
|
1354
1804
|
const options = parseArgs(process.argv.slice(2));
|
|
1805
|
+
if (options.command === "help") {
|
|
1806
|
+
printHelp();
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1355
1809
|
if (options.command === "start") return commandStart(options);
|
|
1356
1810
|
if (options.command === "stop") return commandStop(options);
|
|
1357
1811
|
if (options.command === "status") return commandStatus(options);
|
|
1358
1812
|
if (options.command === "init") return commandInit(options);
|
|
1359
1813
|
if (options.command === "user") return commandUser(options);
|
|
1814
|
+
if (options.command === "peer") return commandPeer(options);
|
|
1360
1815
|
if (options.command === "doctor") return commandDoctor(options);
|
|
1361
1816
|
if (options.command === "update") return commandUpdate(options);
|
|
1362
1817
|
if (options.command === "web" || options.command === "dashboard") return commandWeb(options);
|
|
1363
1818
|
if (options.command === "restart") {
|
|
1364
1819
|
await mkdirp(options.home);
|
|
1365
1820
|
loadEnvFiles(options.home);
|
|
1821
|
+
await prepareRuntimeForLaunch(options);
|
|
1366
1822
|
const webWasRunning = await isWebDashboardRunning(options);
|
|
1367
1823
|
await commandStop(options);
|
|
1368
1824
|
await commandStart(options);
|
|
@@ -1378,7 +1834,8 @@ async function main() {
|
|
|
1378
1834
|
}
|
|
1379
1835
|
|
|
1380
1836
|
console.error(`Unknown command: ${options.command}`);
|
|
1381
|
-
console.error("Usage: nordrelay [init|user|doctor|web|start|stop|restart|status|update|foreground|version]");
|
|
1837
|
+
console.error("Usage: nordrelay [init|user|peer|doctor|web|start|stop|restart|status|update|foreground|version]");
|
|
1838
|
+
console.error("Run `nordrelay --help` for details.");
|
|
1382
1839
|
process.exitCode = 2;
|
|
1383
1840
|
}
|
|
1384
1841
|
|