@akira-tl/forgerelay 1.2.4 → 1.3.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/CHANGELOG.md +32 -0
- package/README.md +19 -11
- package/dist/cli/config/domains/context-cli.js +86 -0
- package/dist/cli/config/domains/domain-cli.js +468 -0
- package/dist/cli/config/general.js +174 -0
- package/dist/cli/config/inspect.js +29 -26
- package/dist/cli/config/migrate.js +11 -24
- package/dist/cli/config/scope.js +35 -0
- package/dist/cli/connect/relay.js +284 -0
- package/dist/cli/core/command-tree.js +68 -0
- package/dist/cli/core/serve-options.js +71 -0
- package/dist/cli/init/setup-config.js +26 -2
- package/dist/cli/init.js +37 -8
- package/dist/cli/maintenance-prune.js +1 -1
- package/dist/cli/maintenance.js +6 -6
- package/dist/cli/mcp/external-mcp.js +29 -17
- package/dist/cli/mcp/status.js +2 -2
- package/dist/cli/setup-support.js +3 -2
- package/dist/cli/system/status.js +35 -0
- package/dist/cli.js +132 -272
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +2 -2
- package/dist/mcp/server/core/schemas.js +2 -10
- package/dist/mcp/server/operations/runtime/operation-runtime.js +11 -7
- package/dist/runtime/config/config.js +30 -29
- package/dist/runtime/config/definition/general-config.js +22 -6
- package/dist/runtime/config/external-mcp-config.js +4 -3
- package/dist/runtime/config/resolution/resolver.js +7 -4
- package/dist/runtime/config/user-config.js +9 -6
- package/dist/runtime/config/validation/paths.js +12 -0
- package/dist/runtime/config/validation/ports.js +9 -0
- package/dist/subagents/profiles.js +37 -0
- package/dist/workspaces/bootstrap.js +31 -14
- package/dist/workspaces/context.js +159 -7
- package/dist/workspaces/relay/auth/cli-test-support.js +22 -0
- package/dist/workspaces/resources/context-sources.js +29 -0
- package/dist/workspaces/resources/resource-monitor.js +29 -6
- package/dist/workspaces/resources/skills.js +15 -10
- package/dist/workspaces/sessions.js +4 -2
- package/dist/workspaces/state/project-context.js +34 -8
- package/dist/workspaces.js +5 -2
- package/docs/chatgpt-coding-workflow.md +23 -20
- package/docs/configuration.md +40 -27
- package/docs/gotchas.md +8 -6
- package/docs/roadmap.md +1 -1
- package/package.json +2 -2
- package/schemas/v1/config.project-local.schema.json +74 -0
- package/schemas/v1/config.project.schema.json +74 -0
- package/schemas/v1/config.user.schema.json +59 -4
- package/scripts/ci/config-v2-product-acceptance.mjs +17 -12
- package/scripts/debug/runtime.mjs +19 -3
- package/scripts/debug/runtime.test.mjs +3 -0
- package/scripts/debug/serve.mjs +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_INSTRUCTION_NAMES, DEFAULT_SKILL_PATHS, DEFAULT_SYSTEM_INSTRUCTIONS_PATH, } from "../../runtime/config/definition/general-config.js";
|
|
1
2
|
export function applySetupConfig(current, selection) {
|
|
2
3
|
const next = {
|
|
3
4
|
...current,
|
|
@@ -5,6 +6,8 @@ export function applySetupConfig(current, selection) {
|
|
|
5
6
|
allowedRoots: [...selection.allowedRoots],
|
|
6
7
|
};
|
|
7
8
|
applyNetworkSelection(next, selection.network);
|
|
9
|
+
if (selection.context)
|
|
10
|
+
applyAgentContextSelection(next, selection.context);
|
|
8
11
|
if (selection.advanced)
|
|
9
12
|
applyAdvancedSelection(next, selection.advanced);
|
|
10
13
|
return next;
|
|
@@ -21,8 +24,29 @@ function applyNetworkSelection(config, selection) {
|
|
|
21
24
|
}
|
|
22
25
|
config.publicBaseUrl = selection.publicBaseUrl;
|
|
23
26
|
}
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
}
|
|
28
|
+
function applyAgentContextSelection(config, selection) {
|
|
29
|
+
if (selection.systemInstructionsPath === DEFAULT_SYSTEM_INSTRUCTIONS_PATH) {
|
|
30
|
+
delete config.systemInstructionsPath;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
config.systemInstructionsPath = selection.systemInstructionsPath;
|
|
34
|
+
}
|
|
35
|
+
if (sameStringList(selection.instructionNames, DEFAULT_INSTRUCTION_NAMES)) {
|
|
36
|
+
delete config.instructionNames;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
config.instructionNames = [...selection.instructionNames];
|
|
40
|
+
}
|
|
41
|
+
if (sameStringList(selection.skillPaths, DEFAULT_SKILL_PATHS)) {
|
|
42
|
+
delete config.skillPaths;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
config.skillPaths = [...selection.skillPaths];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function sameStringList(left, right) {
|
|
49
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
26
50
|
}
|
|
27
51
|
function applyAdvancedSelection(config, selection) {
|
|
28
52
|
if (selection.port === 7676)
|
package/dist/cli/init.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import * as prompts from "@clack/prompts";
|
|
3
2
|
import { publicEndpointUrl } from "../mcp/oauth/public-url.js";
|
|
4
|
-
import {
|
|
3
|
+
import { normalizeAllowedRootPaths } from "../runtime/config/validation/paths.js";
|
|
5
4
|
import { detectLauncherCommandShell, } from "../runtime/shell/command-shell-runtime.js";
|
|
6
5
|
import { seedShellInstructionFiles, shellInstructionFamiliesToSeed, } from "../runtime/instructions/shell-instructions.js";
|
|
7
6
|
import { installManagedLanguageServers, installedManagedLanguageServers, managedLanguageServerOptions, } from "../lsp/runtime/managed-language-servers.js";
|
|
8
7
|
import { generateInstanceId, generateOwnerToken, loadForgeRelayFiles, writeForgeRelayAuth, writeForgeRelayConfig, } from "../runtime/config/user-config.js";
|
|
9
|
-
import { generalConfigDefinition } from "../runtime/config/definition/general-config.js";
|
|
8
|
+
import { DEFAULT_INSTRUCTION_NAMES, DEFAULT_SKILL_PATHS, DEFAULT_SYSTEM_INSTRUCTIONS_PATH, generalConfigDefinition, } from "../runtime/config/definition/general-config.js";
|
|
10
9
|
import { configSchemaId } from "../runtime/config/definition/schema.js";
|
|
11
10
|
import { classifyClientFacingBaseUrl, compactPublicBaseUrlConfig, hasInsecureLanBaseUrl, isLoopbackBindAddress, normalizePublicBaseUrlsInput, SetupCancelledError, textPrompt, validateHttpsProxyBaseUrls, validateLanClientFacingBaseUrls, validatePort, } from "./setup-support.js";
|
|
12
11
|
import { commandShellCompatibilityWarning, commandShellSetupOptions, customPinnedPreference, defaultCommandShellSetupChoice, followLauncherPreference, pinnedFamilyPreference, preservePinnedPreference, shellFamiliesForCustomSelection, } from "./shell/setup.js";
|
|
13
|
-
import { applySetupConfig } from "./init/setup-config.js";
|
|
12
|
+
import { applySetupConfig, } from "./init/setup-config.js";
|
|
13
|
+
function parseSetupList(value) {
|
|
14
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
function validateSetupList(value) {
|
|
17
|
+
return value && parseSetupList(value).length > 0 ? undefined : "Enter at least one comma-separated value.";
|
|
18
|
+
}
|
|
14
19
|
export async function runInit({ force, advanced, version }) {
|
|
15
20
|
const files = loadForgeRelayFiles();
|
|
16
21
|
if (!force && !advanced && files.configExists && files.authExists) {
|
|
@@ -27,10 +32,33 @@ export async function runInit({ force, advanced, version }) {
|
|
|
27
32
|
defaultValue: defaultRoots,
|
|
28
33
|
validate: (value) => value?.trim() ? undefined : "Enter at least one project root.",
|
|
29
34
|
});
|
|
30
|
-
const allowedRoots = rootsAnswer
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
const allowedRoots = normalizeAllowedRootPaths(rootsAnswer.split(","));
|
|
36
|
+
const defaultSystemInstructionsPath = files.config.systemInstructionsPath ?? DEFAULT_SYSTEM_INSTRUCTIONS_PATH;
|
|
37
|
+
const systemInstructionsPath = (await textPrompt({
|
|
38
|
+
message: `Which system instruction file should ForgeRelay load? Press Enter to use ${defaultSystemInstructionsPath}`,
|
|
39
|
+
placeholder: defaultSystemInstructionsPath,
|
|
40
|
+
defaultValue: defaultSystemInstructionsPath,
|
|
41
|
+
validate: (value) => value?.trim() ? undefined : "Enter an instruction file path.",
|
|
42
|
+
})).trim();
|
|
43
|
+
const defaultInstructionNames = (files.config.instructionNames ?? DEFAULT_INSTRUCTION_NAMES).join(", ");
|
|
44
|
+
const instructionNames = parseSetupList(await textPrompt({
|
|
45
|
+
message: `Which project instruction filenames should ForgeRelay discover? Press Enter to use ${defaultInstructionNames}`,
|
|
46
|
+
placeholder: defaultInstructionNames,
|
|
47
|
+
defaultValue: defaultInstructionNames,
|
|
48
|
+
validate: validateSetupList,
|
|
49
|
+
}));
|
|
50
|
+
const defaultSkillPaths = (files.config.skillPaths ?? DEFAULT_SKILL_PATHS).join(", ");
|
|
51
|
+
const skillPaths = parseSetupList(await textPrompt({
|
|
52
|
+
message: `Which Skill directories should ForgeRelay scan? Press Enter to use ${defaultSkillPaths}`,
|
|
53
|
+
placeholder: defaultSkillPaths,
|
|
54
|
+
defaultValue: defaultSkillPaths,
|
|
55
|
+
validate: validateSetupList,
|
|
56
|
+
}));
|
|
57
|
+
const contextSelection = {
|
|
58
|
+
systemInstructionsPath,
|
|
59
|
+
instructionNames,
|
|
60
|
+
skillPaths,
|
|
61
|
+
};
|
|
34
62
|
const defaultPort = String(files.config.port ?? 7676);
|
|
35
63
|
const port = advanced
|
|
36
64
|
? Number(await textPrompt({
|
|
@@ -231,6 +259,7 @@ export async function runInit({ force, advanced, version }) {
|
|
|
231
259
|
mode: networkMode,
|
|
232
260
|
...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
|
|
233
261
|
},
|
|
262
|
+
context: contextSelection,
|
|
234
263
|
...(advancedSelection ? { advanced: advancedSelection } : {}),
|
|
235
264
|
});
|
|
236
265
|
const auth = {
|
|
@@ -91,7 +91,7 @@ export function pruneMaintenanceState(stateDir, policy, now = new Date()) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
export function printMaintenancePruneReport(report) {
|
|
94
|
-
console.log("ForgeRelay
|
|
94
|
+
console.log("ForgeRelay system prune");
|
|
95
95
|
console.log(`State directory: ${report.stateDir}`);
|
|
96
96
|
console.log(`Historical retention: ${report.historicalAuthorized ? `authorized before ${report.cutoff}` : "not authorized (unlimited)"}`);
|
|
97
97
|
console.log(`Orphan administrative cleanup: ${report.administrativeAuthorized ? "authorized" : "not authorized"}`);
|
package/dist/cli/maintenance.js
CHANGED
|
@@ -23,12 +23,12 @@ export function runMaintenanceCommand(args, env = process.env) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
if (subcommand !== "inspect" && subcommand !== "prune") {
|
|
26
|
-
throw new Error(`Unknown maintenance command: ${subcommand}`);
|
|
26
|
+
throw new Error(`Unknown system maintenance command: ${subcommand}`);
|
|
27
27
|
}
|
|
28
28
|
const json = rest.includes("--json");
|
|
29
29
|
const unknown = rest.filter((value) => value !== "--json");
|
|
30
30
|
if (unknown.length > 0)
|
|
31
|
-
throw new Error(`Unknown
|
|
31
|
+
throw new Error(`Unknown system ${subcommand} option: ${unknown[0]}`);
|
|
32
32
|
const inspection = inspectMaintenanceState(env);
|
|
33
33
|
if (subcommand === "inspect") {
|
|
34
34
|
if (json) {
|
|
@@ -567,7 +567,7 @@ function errorMessage(error) {
|
|
|
567
567
|
return error instanceof Error ? error.message : String(error);
|
|
568
568
|
}
|
|
569
569
|
function printMaintenanceReport(report) {
|
|
570
|
-
console.log(`ForgeRelay
|
|
570
|
+
console.log(`ForgeRelay system inspection`);
|
|
571
571
|
console.log(`State directory: ${report.stateDir}`);
|
|
572
572
|
console.log(`Database: ${report.database}`);
|
|
573
573
|
console.log(`Retention: durable history ${report.policy.durableHistory}; orphaned administrative cleanup ${report.policy.orphanedAdministrativeState ? "enabled" : "disabled"}`);
|
|
@@ -595,11 +595,11 @@ function formatBytes(bytes) {
|
|
|
595
595
|
}
|
|
596
596
|
function printMaintenanceHelp() {
|
|
597
597
|
console.log([
|
|
598
|
-
"ForgeRelay
|
|
598
|
+
"ForgeRelay system",
|
|
599
599
|
"",
|
|
600
600
|
"Usage:",
|
|
601
|
-
" forgerelay
|
|
602
|
-
" forgerelay
|
|
601
|
+
" forgerelay system inspect [--json]",
|
|
602
|
+
" forgerelay system prune [--json]",
|
|
603
603
|
"",
|
|
604
604
|
"Inspection is read-only. Durable history is retained without an age limit unless retention.historyDays is explicitly configured.",
|
|
605
605
|
"Prune is manual owner maintenance and removes only categories authorized by the configured retention policy.",
|
|
@@ -6,14 +6,17 @@ import { computeScopeUnion, selectClientAuthMethod, } from "@modelcontextprotoco
|
|
|
6
6
|
import { externalMcpCredentialIdentity, } from "../../runtime/config/external-mcp-auth-store.js";
|
|
7
7
|
import { ExternalMcpInteractiveOAuthProvider, beginExternalMcpInteractiveOAuth, finishExternalMcpInteractiveOAuth, } from "../../mcp/operations/external-mcp/external-mcp-oauth.js";
|
|
8
8
|
import { ExternalMcpError, ExternalMcpGateway, } from "../../mcp/operations/external-mcp/external-mcp.js";
|
|
9
|
-
import { findExternalMcpServerStatus, formatAuth,
|
|
9
|
+
import { findExternalMcpServerStatus, formatAuth, formatExternalMcpStatus, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./status.js";
|
|
10
10
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
11
11
|
export async function runExternalMcpCommand(args, dependencies = {}) {
|
|
12
12
|
const [subcommand, ...rest] = args;
|
|
13
13
|
switch (subcommand) {
|
|
14
|
+
case "status":
|
|
15
|
+
await runExternalMcpStatus(parseMcpScopeArgs("status", rest), dependencies);
|
|
16
|
+
return;
|
|
14
17
|
case "list":
|
|
15
18
|
case "ls":
|
|
16
|
-
await
|
|
19
|
+
await runExternalMcpStatus(parseMcpScopeArgs("list", rest), dependencies);
|
|
17
20
|
return;
|
|
18
21
|
case "test":
|
|
19
22
|
await runExternalMcpTest(parseMcpTargetArgs("test", rest), dependencies);
|
|
@@ -31,7 +34,7 @@ export async function runExternalMcpCommand(args, dependencies = {}) {
|
|
|
31
34
|
printMcpHelp();
|
|
32
35
|
return;
|
|
33
36
|
default:
|
|
34
|
-
throw new Error(`Unknown mcp command: ${subcommand}`);
|
|
37
|
+
throw new Error(`Unknown connect mcp command: ${subcommand}`);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
function parseMcpScopeArgs(command, args) {
|
|
@@ -51,7 +54,7 @@ function parseMcpScopeArgs(command, args) {
|
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
53
56
|
if (arg.startsWith("-"))
|
|
54
|
-
throw new Error(`Unknown mcp ${command} option: ${arg}`);
|
|
57
|
+
throw new Error(`Unknown connect mcp ${command} option: ${arg}`);
|
|
55
58
|
rest.push(arg);
|
|
56
59
|
}
|
|
57
60
|
if (global && projectRoot)
|
|
@@ -65,7 +68,7 @@ function parseMcpScopeArgs(command, args) {
|
|
|
65
68
|
function parseMcpTargetArgs(command, args) {
|
|
66
69
|
const parsed = parseMcpScopeArgs(command, args);
|
|
67
70
|
if (parsed.rest.length !== 1) {
|
|
68
|
-
throw new Error(`Usage: forgerelay mcp ${command} <server> [--project <path>|--global]`);
|
|
71
|
+
throw new Error(`Usage: forgerelay connect mcp ${command} <server> [--project <path>|--global]`);
|
|
69
72
|
}
|
|
70
73
|
return {
|
|
71
74
|
server: parsed.rest[0],
|
|
@@ -73,13 +76,22 @@ function parseMcpTargetArgs(command, args) {
|
|
|
73
76
|
...(parsed.global ? { global: true } : {}),
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
|
-
async function
|
|
77
|
-
if (options.rest.length >
|
|
78
|
-
throw new Error("Usage: forgerelay mcp
|
|
79
|
+
async function runExternalMcpStatus(options, dependencies) {
|
|
80
|
+
if (options.rest.length > 1) {
|
|
81
|
+
throw new Error("Usage: forgerelay connect mcp status [server] [--project <path>|--global]");
|
|
79
82
|
}
|
|
80
83
|
const scope = await resolveExternalMcpScope(options, dependencies);
|
|
81
84
|
const status = inspectExternalMcpStatus(scope);
|
|
82
|
-
|
|
85
|
+
const serverName = options.rest[0];
|
|
86
|
+
if (serverName) {
|
|
87
|
+
const server = findExternalMcpServerStatus(status, serverName);
|
|
88
|
+
if (!server)
|
|
89
|
+
throw new Error(`Unknown configured External MCP server: ${serverName}.`);
|
|
90
|
+
console.log(formatExternalMcpStatus({ ...status, servers: [server] }));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.log(formatExternalMcpStatus(status));
|
|
94
|
+
}
|
|
83
95
|
if (status.configIssues > 0) {
|
|
84
96
|
throw new Error("External MCP configuration or credential status contains issues.");
|
|
85
97
|
}
|
|
@@ -144,8 +156,8 @@ function printTestTarget(scope, server) {
|
|
|
144
156
|
}
|
|
145
157
|
function externalMcpAuthCommand(scope, server) {
|
|
146
158
|
return scope.mode === "global"
|
|
147
|
-
? `forgerelay mcp auth ${cliArgument(server)} --global`
|
|
148
|
-
: `forgerelay mcp auth ${cliArgument(server)} --project ${cliArgument(scope.projectRoot)}`;
|
|
159
|
+
? `forgerelay connect mcp auth ${cliArgument(server)} --global`
|
|
160
|
+
: `forgerelay connect mcp auth ${cliArgument(server)} --project ${cliArgument(scope.projectRoot)}`;
|
|
149
161
|
}
|
|
150
162
|
function cliArgument(value) {
|
|
151
163
|
return /^[A-Za-z0-9_./:\\-]+$/.test(value) ? value : JSON.stringify(value);
|
|
@@ -340,7 +352,7 @@ function hasStaticAuthorizationHeader(headers) {
|
|
|
340
352
|
function assertInteractive(dependencies) {
|
|
341
353
|
const interactive = dependencies.isInteractive ?? (Boolean(input.isTTY) && Boolean(output.isTTY));
|
|
342
354
|
if (!interactive) {
|
|
343
|
-
throw new Error("forgerelay mcp auth requires an interactive terminal for browser/callback authorization.");
|
|
355
|
+
throw new Error("forgerelay connect mcp auth requires an interactive terminal for browser/callback authorization.");
|
|
344
356
|
}
|
|
345
357
|
}
|
|
346
358
|
async function promptMaskedCallback(dependencies) {
|
|
@@ -474,12 +486,12 @@ function isHeadlessEnvironment(env) {
|
|
|
474
486
|
}
|
|
475
487
|
function printMcpHelp() {
|
|
476
488
|
console.log([
|
|
477
|
-
"ForgeRelay mcp",
|
|
489
|
+
"ForgeRelay connect mcp",
|
|
478
490
|
"",
|
|
479
491
|
"Usage:",
|
|
480
|
-
" forgerelay mcp
|
|
481
|
-
" forgerelay mcp test <server> [--project <path>|--global]",
|
|
482
|
-
" forgerelay mcp auth <server> [--project <path>|--global]",
|
|
483
|
-
" forgerelay mcp logout <server> [--project <path>|--global]",
|
|
492
|
+
" forgerelay connect mcp status [server] [--project <path>|--global]",
|
|
493
|
+
" forgerelay connect mcp test <server> [--project <path>|--global]",
|
|
494
|
+
" forgerelay connect mcp auth <server> [--project <path>|--global]",
|
|
495
|
+
" forgerelay connect mcp logout <server> [--project <path>|--global]",
|
|
484
496
|
].join("\n"));
|
|
485
497
|
}
|
package/dist/cli/mcp/status.js
CHANGED
|
@@ -72,7 +72,7 @@ export function inspectExternalMcpStatus(scope) {
|
|
|
72
72
|
export function findExternalMcpServerStatus(status, name) {
|
|
73
73
|
return status.servers.find((server) => server.name === name);
|
|
74
74
|
}
|
|
75
|
-
export function
|
|
75
|
+
export function formatExternalMcpStatus(status) {
|
|
76
76
|
const lines = ["External MCP", ""];
|
|
77
77
|
if (status.scope.mode === "global") {
|
|
78
78
|
lines.push("Scope: global");
|
|
@@ -147,7 +147,7 @@ export function formatExternalMcpDoctor(status) {
|
|
|
147
147
|
` Credential store: ${status.credentialStore}`,
|
|
148
148
|
` Config issues: ${status.configIssues}`,
|
|
149
149
|
" Hot reload: active",
|
|
150
|
-
" Active checks: not run (use `forgerelay mcp test <server>`)",
|
|
150
|
+
" Active checks: not run (use `forgerelay connect mcp test <server>`)",
|
|
151
151
|
].join("\n");
|
|
152
152
|
}
|
|
153
153
|
export function formatAuth(status) {
|
|
@@ -2,6 +2,7 @@ import { isIP } from "node:net";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as prompts from "@clack/prompts";
|
|
4
4
|
import { satisfies } from "semver";
|
|
5
|
+
import { isIntegerPort, LISTEN_PORT_MIN, PORT_MAX } from "../runtime/config/validation/ports.js";
|
|
5
6
|
const SUPPORTED_NODE_RANGE = ">=20.12 <27";
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
export function isNullConfigValue(value) {
|
|
@@ -118,9 +119,9 @@ export async function textPrompt(options) {
|
|
|
118
119
|
}
|
|
119
120
|
export function validatePort(value) {
|
|
120
121
|
const port = Number(value);
|
|
121
|
-
return
|
|
122
|
+
return isIntegerPort(port, LISTEN_PORT_MIN, PORT_MAX)
|
|
122
123
|
? undefined
|
|
123
|
-
:
|
|
124
|
+
: `Enter a port between ${LISTEN_PORT_MIN} and ${PORT_MAX}.`;
|
|
124
125
|
}
|
|
125
126
|
export function isLoopbackBindAddress(value) {
|
|
126
127
|
const host = value.trim().toLowerCase();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { resolveRuntimeConfigFacts } from "../../runtime/config/config.js";
|
|
2
|
+
import { inspectRuntimeLease } from "../../runtime/state/runtime-lease.js";
|
|
3
|
+
export function inspectSystemStatus(env = process.env) {
|
|
4
|
+
const config = resolveRuntimeConfigFacts(env);
|
|
5
|
+
return {
|
|
6
|
+
...(config.instanceId ? { instanceId: config.instanceId } : {}),
|
|
7
|
+
stateDir: config.stateDir,
|
|
8
|
+
host: config.host,
|
|
9
|
+
port: config.port,
|
|
10
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
11
|
+
lease: inspectRuntimeLease(config.stateDir),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function runSystemStatus(env = process.env) {
|
|
15
|
+
const status = inspectSystemStatus(env);
|
|
16
|
+
console.log([
|
|
17
|
+
"ForgeRelay system status",
|
|
18
|
+
"",
|
|
19
|
+
`Instance: ${status.instanceId ?? "not initialized"}`,
|
|
20
|
+
`Runtime: ${formatRuntimeState(status.lease)}`,
|
|
21
|
+
...(status.lease.pid === undefined ? [] : [`PID: ${status.lease.pid}`]),
|
|
22
|
+
`State dir: ${status.stateDir}`,
|
|
23
|
+
`Configured bind: ${status.host}:${status.port}`,
|
|
24
|
+
`Configured public URL: ${status.publicBaseUrl}`,
|
|
25
|
+
].join("\n"));
|
|
26
|
+
}
|
|
27
|
+
function formatRuntimeState(lease) {
|
|
28
|
+
if (lease.malformed)
|
|
29
|
+
return "unknown (malformed runtime lease)";
|
|
30
|
+
if (lease.active)
|
|
31
|
+
return "running";
|
|
32
|
+
if (lease.stale)
|
|
33
|
+
return "not running (stale runtime lease present)";
|
|
34
|
+
return "not running";
|
|
35
|
+
}
|