@neuralnomads/codenomad 0.2.6-dev → 0.2.8-dev
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/config/schema.js +2 -0
- package/dist/config/store.js +3 -2
- package/dist/events/bus.js +6 -1
- package/dist/index.js +19 -0
- package/dist/releases/release-monitor.js +100 -0
- package/dist/server/http-server.js +61 -7
- package/dist/server/routes/events.js +8 -0
- package/dist/server/routes/meta.js +95 -1
- package/dist/workspaces/instance-events.js +5 -0
- package/package.json +1 -1
- package/public/assets/index-5OazY0L0.css +1 -0
- package/public/assets/index-INLytMyr.js +1 -0
- package/public/assets/{loading-CA8y-NQU.js → loading-Dv6ZYUje.js} +1 -1
- package/public/assets/main-ClWy4v2r.js +157 -0
- package/public/index.html +5 -4
- package/public/loading.html +5 -4
- package/public/assets/index-xIuBSisM.css +0 -1
- package/public/assets/index-ysIEPg5i.js +0 -1
- package/public/assets/main-CjXO1IoI.js +0 -150
package/dist/config/schema.js
CHANGED
|
@@ -15,6 +15,8 @@ const PreferencesSchema = z.object({
|
|
|
15
15
|
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
|
16
16
|
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
|
17
17
|
showUsageMetrics: z.boolean().default(true),
|
|
18
|
+
autoCleanupBlankSessions: z.boolean().default(true),
|
|
19
|
+
listeningMode: z.enum(["local", "all"]).default("local"),
|
|
18
20
|
});
|
|
19
21
|
const RecentFolderSchema = z.object({
|
|
20
22
|
path: z.string(),
|
package/dist/config/store.js
CHANGED
|
@@ -44,9 +44,10 @@ export class ConfigStore {
|
|
|
44
44
|
this.cache = next;
|
|
45
45
|
this.loaded = true;
|
|
46
46
|
this.persist();
|
|
47
|
+
const published = Boolean(this.eventBus);
|
|
47
48
|
this.eventBus?.publish({ type: "config.appChanged", config: this.cache });
|
|
48
|
-
this.logger.
|
|
49
|
-
this.logger.
|
|
49
|
+
this.logger.debug({ broadcast: published }, "Config SSE event emitted");
|
|
50
|
+
this.logger.trace({ config: this.cache }, "Config payload");
|
|
50
51
|
}
|
|
51
52
|
persist() {
|
|
52
53
|
try {
|
package/dist/events/bus.js
CHANGED
|
@@ -6,7 +6,10 @@ export class EventBus extends EventEmitter {
|
|
|
6
6
|
}
|
|
7
7
|
publish(event) {
|
|
8
8
|
if (event.type !== "instance.event" && event.type !== "instance.eventStatus") {
|
|
9
|
-
this.logger?.debug({ event }, "Publishing workspace event");
|
|
9
|
+
this.logger?.debug({ type: event.type }, "Publishing workspace event");
|
|
10
|
+
if (this.logger?.isLevelEnabled("trace")) {
|
|
11
|
+
this.logger.trace({ event }, "Workspace event payload");
|
|
12
|
+
}
|
|
10
13
|
}
|
|
11
14
|
return super.emit(event.type, event);
|
|
12
15
|
}
|
|
@@ -22,6 +25,7 @@ export class EventBus extends EventEmitter {
|
|
|
22
25
|
this.on("instance.dataChanged", handler);
|
|
23
26
|
this.on("instance.event", handler);
|
|
24
27
|
this.on("instance.eventStatus", handler);
|
|
28
|
+
this.on("app.releaseAvailable", handler);
|
|
25
29
|
return () => {
|
|
26
30
|
this.off("workspace.created", handler);
|
|
27
31
|
this.off("workspace.started", handler);
|
|
@@ -33,6 +37,7 @@ export class EventBus extends EventEmitter {
|
|
|
33
37
|
this.off("instance.dataChanged", handler);
|
|
34
38
|
this.off("instance.event", handler);
|
|
35
39
|
this.off("instance.eventStatus", handler);
|
|
40
|
+
this.off("app.releaseAvailable", handler);
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { InstanceStore } from "./storage/instance-store";
|
|
|
16
16
|
import { InstanceEventBridge } from "./workspaces/instance-events";
|
|
17
17
|
import { createLogger } from "./logger";
|
|
18
18
|
import { launchInBrowser } from "./launcher";
|
|
19
|
+
import { startReleaseMonitor } from "./releases/release-monitor";
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const packageJson = require("../package.json");
|
|
21
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -97,9 +98,26 @@ async function main() {
|
|
|
97
98
|
const serverMeta = {
|
|
98
99
|
httpBaseUrl: `http://${options.host}:${options.port}`,
|
|
99
100
|
eventsUrl: `/api/events`,
|
|
101
|
+
host: options.host,
|
|
102
|
+
listeningMode: options.host === "0.0.0.0" ? "all" : "local",
|
|
103
|
+
port: options.port,
|
|
100
104
|
hostLabel: options.host,
|
|
101
105
|
workspaceRoot: options.rootDir,
|
|
106
|
+
addresses: [],
|
|
102
107
|
};
|
|
108
|
+
const releaseMonitor = startReleaseMonitor({
|
|
109
|
+
currentVersion: packageJson.version,
|
|
110
|
+
logger: logger.child({ component: "release-monitor" }),
|
|
111
|
+
onUpdate: (release) => {
|
|
112
|
+
if (release) {
|
|
113
|
+
serverMeta.latestRelease = release;
|
|
114
|
+
eventBus.publish({ type: "app.releaseAvailable", release });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
delete serverMeta.latestRelease;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
103
121
|
const server = createHttpServer({
|
|
104
122
|
host: options.host,
|
|
105
123
|
port: options.port,
|
|
@@ -143,6 +161,7 @@ async function main() {
|
|
|
143
161
|
catch (error) {
|
|
144
162
|
logger.error({ err: error }, "Workspace manager shutdown failed");
|
|
145
163
|
}
|
|
164
|
+
releaseMonitor.stop();
|
|
146
165
|
logger.info("Exiting process");
|
|
147
166
|
process.exit(0);
|
|
148
167
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { fetch } from "undici";
|
|
2
|
+
const RELEASES_API_URL = "https://api.github.com/repos/NeuralNomadsAI/CodeNomad/releases/latest";
|
|
3
|
+
export function startReleaseMonitor(options) {
|
|
4
|
+
let stopped = false;
|
|
5
|
+
const refreshRelease = async () => {
|
|
6
|
+
if (stopped)
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
const release = await fetchLatestRelease(options);
|
|
10
|
+
options.onUpdate(release);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
options.logger.warn({ err: error }, "Failed to refresh release information");
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
void refreshRelease();
|
|
17
|
+
return {
|
|
18
|
+
stop() {
|
|
19
|
+
stopped = true;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function fetchLatestRelease(options) {
|
|
24
|
+
const response = await fetch(RELEASES_API_URL, {
|
|
25
|
+
headers: {
|
|
26
|
+
Accept: "application/vnd.github+json",
|
|
27
|
+
"User-Agent": "CodeNomad-CLI",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Release API responded with ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
const json = (await response.json());
|
|
34
|
+
const tagFromServer = json.tag_name || json.name;
|
|
35
|
+
if (!tagFromServer) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const normalizedVersion = stripTagPrefix(tagFromServer);
|
|
39
|
+
if (!normalizedVersion) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const current = parseVersion(options.currentVersion);
|
|
43
|
+
const remote = parseVersion(normalizedVersion);
|
|
44
|
+
if (compareVersions(remote, current) <= 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
version: normalizedVersion,
|
|
49
|
+
tag: tagFromServer,
|
|
50
|
+
url: json.html_url ?? `https://github.com/NeuralNomadsAI/CodeNomad/releases/tag/${encodeURIComponent(tagFromServer)}`,
|
|
51
|
+
channel: json.prerelease || normalizedVersion.includes("-") ? "dev" : "stable",
|
|
52
|
+
publishedAt: json.published_at ?? json.created_at,
|
|
53
|
+
notes: json.body,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function stripTagPrefix(tag) {
|
|
57
|
+
if (!tag)
|
|
58
|
+
return null;
|
|
59
|
+
const trimmed = tag.trim();
|
|
60
|
+
if (!trimmed)
|
|
61
|
+
return null;
|
|
62
|
+
return trimmed.replace(/^v/i, "");
|
|
63
|
+
}
|
|
64
|
+
function parseVersion(value) {
|
|
65
|
+
const normalized = stripTagPrefix(value) ?? "0.0.0";
|
|
66
|
+
const [core, prerelease = null] = normalized.split("-", 2);
|
|
67
|
+
const [major = 0, minor = 0, patch = 0] = core.split(".").map((segment) => {
|
|
68
|
+
const parsed = Number.parseInt(segment, 10);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
major,
|
|
73
|
+
minor,
|
|
74
|
+
patch,
|
|
75
|
+
prerelease,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function compareVersions(a, b) {
|
|
79
|
+
if (a.major !== b.major) {
|
|
80
|
+
return a.major > b.major ? 1 : -1;
|
|
81
|
+
}
|
|
82
|
+
if (a.minor !== b.minor) {
|
|
83
|
+
return a.minor > b.minor ? 1 : -1;
|
|
84
|
+
}
|
|
85
|
+
if (a.patch !== b.patch) {
|
|
86
|
+
return a.patch > b.patch ? 1 : -1;
|
|
87
|
+
}
|
|
88
|
+
const aPre = a.prerelease && a.prerelease.length > 0 ? a.prerelease : null;
|
|
89
|
+
const bPre = b.prerelease && b.prerelease.length > 0 ? b.prerelease : null;
|
|
90
|
+
if (aPre === bPre) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (!aPre) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
if (!bPre) {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
return aPre.localeCompare(bPre);
|
|
100
|
+
}
|
|
@@ -11,9 +11,12 @@ import { registerFilesystemRoutes } from "./routes/filesystem";
|
|
|
11
11
|
import { registerMetaRoutes } from "./routes/meta";
|
|
12
12
|
import { registerEventRoutes } from "./routes/events";
|
|
13
13
|
import { registerStorageRoutes } from "./routes/storage";
|
|
14
|
+
const DEFAULT_HTTP_PORT = 9898;
|
|
14
15
|
export function createHttpServer(deps) {
|
|
15
16
|
const app = Fastify({ logger: false });
|
|
16
17
|
const proxyLogger = deps.logger.child({ component: "proxy" });
|
|
18
|
+
const apiLogger = deps.logger.child({ component: "http" });
|
|
19
|
+
const sseLogger = deps.logger.child({ component: "sse" });
|
|
17
20
|
const sseClients = new Set();
|
|
18
21
|
const registerSseClient = (cleanup) => {
|
|
19
22
|
sseClients.add(cleanup);
|
|
@@ -25,6 +28,28 @@ export function createHttpServer(deps) {
|
|
|
25
28
|
}
|
|
26
29
|
sseClients.clear();
|
|
27
30
|
};
|
|
31
|
+
app.addHook("onRequest", (request, _reply, done) => {
|
|
32
|
+
;
|
|
33
|
+
request.__logMeta = {
|
|
34
|
+
start: process.hrtime.bigint(),
|
|
35
|
+
};
|
|
36
|
+
done();
|
|
37
|
+
});
|
|
38
|
+
app.addHook("onResponse", (request, reply, done) => {
|
|
39
|
+
const meta = request.__logMeta;
|
|
40
|
+
const durationMs = meta ? Number((process.hrtime.bigint() - meta.start) / BigInt(1000000)) : undefined;
|
|
41
|
+
const base = {
|
|
42
|
+
method: request.method,
|
|
43
|
+
url: request.url,
|
|
44
|
+
status: reply.statusCode,
|
|
45
|
+
durationMs,
|
|
46
|
+
};
|
|
47
|
+
apiLogger.debug(base, "HTTP request completed");
|
|
48
|
+
if (apiLogger.isLevelEnabled("trace")) {
|
|
49
|
+
apiLogger.trace({ ...base, params: request.params, query: request.query, body: request.body }, "HTTP request payload");
|
|
50
|
+
}
|
|
51
|
+
done();
|
|
52
|
+
});
|
|
28
53
|
app.register(cors, {
|
|
29
54
|
origin: true,
|
|
30
55
|
credentials: true,
|
|
@@ -42,7 +67,7 @@ export function createHttpServer(deps) {
|
|
|
42
67
|
registerConfigRoutes(app, { configStore: deps.configStore, binaryRegistry: deps.binaryRegistry });
|
|
43
68
|
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser });
|
|
44
69
|
registerMetaRoutes(app, { serverMeta: deps.serverMeta });
|
|
45
|
-
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient });
|
|
70
|
+
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient, logger: sseLogger });
|
|
46
71
|
registerStorageRoutes(app, {
|
|
47
72
|
instanceStore: deps.instanceStore,
|
|
48
73
|
eventBus: deps.eventBus,
|
|
@@ -58,15 +83,37 @@ export function createHttpServer(deps) {
|
|
|
58
83
|
return {
|
|
59
84
|
instance: app,
|
|
60
85
|
start: async () => {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
const attemptListen = async (requestedPort) => {
|
|
87
|
+
const addressInfo = await app.listen({ port: requestedPort, host: deps.host });
|
|
88
|
+
return { addressInfo, requestedPort };
|
|
89
|
+
};
|
|
90
|
+
const autoPortRequested = deps.port === 0;
|
|
91
|
+
const primaryPort = autoPortRequested ? DEFAULT_HTTP_PORT : deps.port;
|
|
92
|
+
const shouldRetryWithEphemeral = (error) => {
|
|
93
|
+
if (!autoPortRequested)
|
|
94
|
+
return false;
|
|
95
|
+
const err = error;
|
|
96
|
+
return Boolean(err && err.code === "EADDRINUSE");
|
|
97
|
+
};
|
|
98
|
+
let listenResult;
|
|
99
|
+
try {
|
|
100
|
+
listenResult = await attemptListen(primaryPort);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!shouldRetryWithEphemeral(error)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
deps.logger.warn({ err: error, port: primaryPort }, "Preferred port unavailable, retrying on ephemeral port");
|
|
107
|
+
listenResult = await attemptListen(0);
|
|
108
|
+
}
|
|
109
|
+
let actualPort = listenResult.requestedPort;
|
|
110
|
+
if (typeof listenResult.addressInfo === "string") {
|
|
64
111
|
try {
|
|
65
|
-
const parsed = new URL(addressInfo);
|
|
66
|
-
actualPort = Number(parsed.port) ||
|
|
112
|
+
const parsed = new URL(listenResult.addressInfo);
|
|
113
|
+
actualPort = Number(parsed.port) || listenResult.requestedPort;
|
|
67
114
|
}
|
|
68
115
|
catch {
|
|
69
|
-
actualPort =
|
|
116
|
+
actualPort = listenResult.requestedPort;
|
|
70
117
|
}
|
|
71
118
|
}
|
|
72
119
|
else {
|
|
@@ -78,6 +125,9 @@ export function createHttpServer(deps) {
|
|
|
78
125
|
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host;
|
|
79
126
|
const serverUrl = `http://${displayHost}:${actualPort}`;
|
|
80
127
|
deps.serverMeta.httpBaseUrl = serverUrl;
|
|
128
|
+
deps.serverMeta.host = deps.host;
|
|
129
|
+
deps.serverMeta.port = actualPort;
|
|
130
|
+
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local";
|
|
81
131
|
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening");
|
|
82
132
|
console.log(`CodeNomad Server is ready at ${serverUrl}`);
|
|
83
133
|
return { port: actualPort, url: serverUrl, displayHost };
|
|
@@ -132,6 +182,10 @@ async function proxyWorkspaceRequest(args) {
|
|
|
132
182
|
const queryIndex = (request.raw.url ?? "").indexOf("?");
|
|
133
183
|
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : "";
|
|
134
184
|
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`;
|
|
185
|
+
logger.debug({ workspaceId, method: request.method, targetUrl }, "Proxying request to instance");
|
|
186
|
+
if (logger.isLevelEnabled("trace")) {
|
|
187
|
+
logger.trace({ workspaceId, targetUrl, body: request.body }, "Instance proxy payload");
|
|
188
|
+
}
|
|
135
189
|
return reply.from(targetUrl, {
|
|
136
190
|
onError: (proxyReply, { error }) => {
|
|
137
191
|
logger.error({ err: error, workspaceId, targetUrl }, "Failed to proxy workspace request");
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
let nextClientId = 0;
|
|
1
2
|
export function registerEventRoutes(app, deps) {
|
|
2
3
|
app.get("/api/events", (request, reply) => {
|
|
4
|
+
const clientId = ++nextClientId;
|
|
5
|
+
deps.logger.debug({ clientId }, "SSE client connected");
|
|
3
6
|
const origin = request.headers.origin ?? "*";
|
|
4
7
|
reply.raw.setHeader("Access-Control-Allow-Origin", origin);
|
|
5
8
|
reply.raw.setHeader("Access-Control-Allow-Credentials", "true");
|
|
@@ -9,6 +12,10 @@ export function registerEventRoutes(app, deps) {
|
|
|
9
12
|
reply.raw.flushHeaders?.();
|
|
10
13
|
reply.hijack();
|
|
11
14
|
const send = (event) => {
|
|
15
|
+
deps.logger.debug({ clientId, type: event.type }, "SSE event dispatched");
|
|
16
|
+
if (deps.logger.isLevelEnabled("trace")) {
|
|
17
|
+
deps.logger.trace({ clientId, event }, "SSE event payload");
|
|
18
|
+
}
|
|
12
19
|
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
13
20
|
};
|
|
14
21
|
const unsubscribe = deps.eventBus.onEvent(send);
|
|
@@ -23,6 +30,7 @@ export function registerEventRoutes(app, deps) {
|
|
|
23
30
|
clearInterval(heartbeat);
|
|
24
31
|
unsubscribe();
|
|
25
32
|
reply.raw.end?.();
|
|
33
|
+
deps.logger.debug({ clientId }, "SSE client disconnected");
|
|
26
34
|
};
|
|
27
35
|
const unregister = deps.registerClient(close);
|
|
28
36
|
const handleClose = () => {
|
|
@@ -1,3 +1,97 @@
|
|
|
1
|
+
import os from "os";
|
|
1
2
|
export function registerMetaRoutes(app, deps) {
|
|
2
|
-
app.get("/api/meta", async () => deps.serverMeta);
|
|
3
|
+
app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta));
|
|
4
|
+
}
|
|
5
|
+
function buildMetaResponse(meta) {
|
|
6
|
+
const port = resolvePort(meta);
|
|
7
|
+
const addresses = port > 0 ? resolveAddresses(port, meta.host) : [];
|
|
8
|
+
return {
|
|
9
|
+
...meta,
|
|
10
|
+
port,
|
|
11
|
+
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
|
|
12
|
+
addresses,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function resolvePort(meta) {
|
|
16
|
+
if (Number.isInteger(meta.port) && meta.port > 0) {
|
|
17
|
+
return meta.port;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(meta.httpBaseUrl);
|
|
21
|
+
const port = Number(parsed.port);
|
|
22
|
+
return Number.isInteger(port) && port > 0 ? port : 0;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveAddresses(port, host) {
|
|
29
|
+
const interfaces = os.networkInterfaces();
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const results = [];
|
|
32
|
+
const addAddress = (ip, scope) => {
|
|
33
|
+
if (!ip || ip === "0.0.0.0")
|
|
34
|
+
return;
|
|
35
|
+
const key = `ipv4-${ip}`;
|
|
36
|
+
if (seen.has(key))
|
|
37
|
+
return;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
results.push({ ip, family: "ipv4", scope, url: `http://${ip}:${port}` });
|
|
40
|
+
};
|
|
41
|
+
const normalizeFamily = (value) => {
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const lowered = value.toLowerCase();
|
|
44
|
+
if (lowered === "ipv4") {
|
|
45
|
+
return "ipv4";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (value === 4)
|
|
49
|
+
return "ipv4";
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
if (host === "0.0.0.0") {
|
|
53
|
+
// Enumerate system interfaces (IPv4 only)
|
|
54
|
+
for (const entries of Object.values(interfaces)) {
|
|
55
|
+
if (!entries)
|
|
56
|
+
continue;
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const family = normalizeFamily(entry.family);
|
|
59
|
+
if (!family)
|
|
60
|
+
continue;
|
|
61
|
+
if (!entry.address || entry.address === "0.0.0.0")
|
|
62
|
+
continue;
|
|
63
|
+
const scope = entry.internal ? "loopback" : "external";
|
|
64
|
+
addAddress(entry.address, scope);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Always include loopback address
|
|
69
|
+
addAddress("127.0.0.1", "loopback");
|
|
70
|
+
// Include explicitly configured host if it was IPv4
|
|
71
|
+
if (isIPv4Address(host) && host !== "0.0.0.0") {
|
|
72
|
+
const isLoopback = host.startsWith("127.");
|
|
73
|
+
addAddress(host, isLoopback ? "loopback" : "external");
|
|
74
|
+
}
|
|
75
|
+
const scopeWeight = { external: 0, internal: 1, loopback: 2 };
|
|
76
|
+
return results.sort((a, b) => {
|
|
77
|
+
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope];
|
|
78
|
+
if (scopeDelta !== 0)
|
|
79
|
+
return scopeDelta;
|
|
80
|
+
return a.ip.localeCompare(b.ip);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function isIPv4Address(value) {
|
|
84
|
+
if (!value)
|
|
85
|
+
return false;
|
|
86
|
+
const parts = value.split(".");
|
|
87
|
+
if (parts.length !== 4)
|
|
88
|
+
return false;
|
|
89
|
+
return parts.every((part) => {
|
|
90
|
+
if (part.length === 0 || part.length > 3)
|
|
91
|
+
return false;
|
|
92
|
+
if (!/^[0-9]+$/.test(part))
|
|
93
|
+
return false;
|
|
94
|
+
const num = Number(part);
|
|
95
|
+
return Number.isInteger(num) && num >= 0 && num <= 255;
|
|
96
|
+
});
|
|
3
97
|
}
|
|
@@ -122,6 +122,10 @@ export class InstanceEventBridge {
|
|
|
122
122
|
}
|
|
123
123
|
try {
|
|
124
124
|
const event = JSON.parse(payload);
|
|
125
|
+
this.options.logger.debug({ workspaceId, eventType: event.type }, "Instance SSE event received");
|
|
126
|
+
if (this.options.logger.isLevelEnabled("trace")) {
|
|
127
|
+
this.options.logger.trace({ workspaceId, event }, "Instance SSE event payload");
|
|
128
|
+
}
|
|
125
129
|
this.options.eventBus.publish({ type: "instance.event", instanceId: workspaceId, event });
|
|
126
130
|
}
|
|
127
131
|
catch (error) {
|
|
@@ -129,6 +133,7 @@ export class InstanceEventBridge {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
publishStatus(instanceId, status, reason) {
|
|
136
|
+
this.options.logger.debug({ instanceId, status, reason }, "Instance SSE status updated");
|
|
132
137
|
this.options.eventBus.publish({ type: "instance.eventStatus", instanceId, status, reason });
|
|
133
138
|
}
|
|
134
139
|
delay(duration, signal) {
|