@neuralnomads/codenomad 0.7.1 → 0.7.3
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/background-processes/manager.js +36 -3
- package/dist/config/binaries.js +32 -2
- package/dist/index.js +10 -3
- package/dist/server/http-server.js +8 -2
- package/dist/server/routes/meta.js +4 -1
- package/dist/workspaces/manager.js +19 -4
- package/dist/workspaces/runtime.js +34 -3
- package/package.json +1 -1
- package/public/assets/{main-C3qD3Vm8.js → main-DjTN43FU.js} +13 -13
- package/public/index.html +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import { createWriteStream, existsSync, promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { randomBytes } from "crypto";
|
|
@@ -33,10 +33,12 @@ export class BackgroundProcessManager {
|
|
|
33
33
|
const processDir = await this.ensureProcessDir(workspaceId, id);
|
|
34
34
|
const outputPath = path.join(processDir, OUTPUT_FILE);
|
|
35
35
|
const outputStream = createWriteStream(outputPath, { flags: "a" });
|
|
36
|
-
const
|
|
36
|
+
const { shellCommand, shellArgs, spawnOptions } = this.buildShellSpawn(command);
|
|
37
|
+
const child = spawn(shellCommand, shellArgs, {
|
|
37
38
|
cwd: workspace.path,
|
|
38
39
|
stdio: ["ignore", "pipe", "pipe"],
|
|
39
40
|
detached: process.platform !== "win32",
|
|
41
|
+
...spawnOptions,
|
|
40
42
|
});
|
|
41
43
|
child.on("exit", () => {
|
|
42
44
|
this.killProcessTree(child, "SIGTERM");
|
|
@@ -211,7 +213,17 @@ export class BackgroundProcessManager {
|
|
|
211
213
|
const pid = child.pid;
|
|
212
214
|
if (!pid)
|
|
213
215
|
return;
|
|
214
|
-
if (process.platform
|
|
216
|
+
if (process.platform === "win32") {
|
|
217
|
+
const args = this.buildWindowsTaskkillArgs(pid, signal);
|
|
218
|
+
try {
|
|
219
|
+
spawnSync("taskkill", args, { stdio: "ignore" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Fall back to killing the direct child.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
215
227
|
try {
|
|
216
228
|
process.kill(-pid, signal);
|
|
217
229
|
return;
|
|
@@ -254,6 +266,27 @@ export class BackgroundProcessManager {
|
|
|
254
266
|
clearTimeout(killTimeout);
|
|
255
267
|
}
|
|
256
268
|
}
|
|
269
|
+
buildShellSpawn(command) {
|
|
270
|
+
if (process.platform === "win32") {
|
|
271
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
272
|
+
return {
|
|
273
|
+
shellCommand: comspec,
|
|
274
|
+
shellArgs: ["/d", "/s", "/c", command],
|
|
275
|
+
spawnOptions: { windowsVerbatimArguments: true },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// Keep bash for macOS/Linux.
|
|
279
|
+
return { shellCommand: "bash", shellArgs: ["-c", command] };
|
|
280
|
+
}
|
|
281
|
+
buildWindowsTaskkillArgs(pid, signal) {
|
|
282
|
+
// Default to graceful termination (no /F), then force kill when we escalate.
|
|
283
|
+
const force = signal === "SIGKILL";
|
|
284
|
+
const args = ["/PID", String(pid), "/T"];
|
|
285
|
+
if (force) {
|
|
286
|
+
args.push("/F");
|
|
287
|
+
}
|
|
288
|
+
return args;
|
|
289
|
+
}
|
|
257
290
|
statusFromExit(code) {
|
|
258
291
|
if (code === null)
|
|
259
292
|
return "stopped";
|
package/dist/config/binaries.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { buildSpawnSpec } from "../workspaces/runtime";
|
|
1
3
|
export class BinaryRegistry {
|
|
2
4
|
constructor(configStore, eventBus, logger) {
|
|
3
5
|
this.configStore = configStore;
|
|
@@ -99,8 +101,36 @@ export class BinaryRegistry {
|
|
|
99
101
|
this.eventBus?.publish({ type: "config.binariesChanged", binaries: this.mapRecords() });
|
|
100
102
|
}
|
|
101
103
|
validateRecord(record) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
const inputPath = record.path;
|
|
105
|
+
if (!inputPath) {
|
|
106
|
+
return { valid: false, error: "Missing binary path" };
|
|
107
|
+
}
|
|
108
|
+
const spec = buildSpawnSpec(inputPath, ["--version"]);
|
|
109
|
+
try {
|
|
110
|
+
const result = spawnSync(spec.command, spec.args, {
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
windowsVerbatimArguments: Boolean(spec.options.windowsVerbatimArguments),
|
|
113
|
+
});
|
|
114
|
+
if (result.error) {
|
|
115
|
+
return { valid: false, error: result.error.message };
|
|
116
|
+
}
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
const stderr = result.stderr?.trim();
|
|
119
|
+
const stdout = result.stdout?.trim();
|
|
120
|
+
const combined = stderr || stdout;
|
|
121
|
+
const error = combined ? `Exited with code ${result.status}: ${combined}` : `Exited with code ${result.status}`;
|
|
122
|
+
return { valid: false, error };
|
|
123
|
+
}
|
|
124
|
+
const stdout = (result.stdout ?? "").trim();
|
|
125
|
+
const firstLine = stdout.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
126
|
+
const normalized = firstLine?.trim();
|
|
127
|
+
const versionMatch = normalized?.match(/([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/);
|
|
128
|
+
const version = versionMatch?.[1];
|
|
129
|
+
return { valid: true, version };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return { valid: false, error: error instanceof Error ? error.message : String(error) };
|
|
133
|
+
}
|
|
104
134
|
}
|
|
105
135
|
buildFallbackRecord(path) {
|
|
106
136
|
return {
|
package/dist/index.js
CHANGED
|
@@ -77,10 +77,16 @@ function parsePort(input) {
|
|
|
77
77
|
return value;
|
|
78
78
|
}
|
|
79
79
|
function resolveHost(input) {
|
|
80
|
-
|
|
80
|
+
const trimmed = input?.trim();
|
|
81
|
+
if (!trimmed)
|
|
82
|
+
return DEFAULT_HOST;
|
|
83
|
+
if (trimmed === "0.0.0.0") {
|
|
81
84
|
return "0.0.0.0";
|
|
82
85
|
}
|
|
83
|
-
|
|
86
|
+
if (trimmed === "localhost") {
|
|
87
|
+
return DEFAULT_HOST;
|
|
88
|
+
}
|
|
89
|
+
return trimmed;
|
|
84
90
|
}
|
|
85
91
|
async function main() {
|
|
86
92
|
const options = parseCliOptions(process.argv.slice(2));
|
|
@@ -94,11 +100,12 @@ async function main() {
|
|
|
94
100
|
};
|
|
95
101
|
logger.info({ options: logOptions }, "Starting CodeNomad CLI server");
|
|
96
102
|
const eventBus = new EventBus(eventLogger);
|
|
103
|
+
const isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
97
104
|
const serverMeta = {
|
|
98
105
|
httpBaseUrl: `http://${options.host}:${options.port}`,
|
|
99
106
|
eventsUrl: `/api/events`,
|
|
100
107
|
host: options.host,
|
|
101
|
-
listeningMode: options.host
|
|
108
|
+
listeningMode: isLoopbackHost(options.host) ? "local" : "all",
|
|
102
109
|
port: options.port,
|
|
103
110
|
hostLabel: options.host,
|
|
104
111
|
workspaceRoot: options.rootDir,
|
|
@@ -56,6 +56,7 @@ export function createHttpServer(deps) {
|
|
|
56
56
|
done();
|
|
57
57
|
});
|
|
58
58
|
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"]);
|
|
59
|
+
const isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
59
60
|
app.register(cors, {
|
|
60
61
|
origin: (origin, cb) => {
|
|
61
62
|
if (!origin) {
|
|
@@ -77,6 +78,11 @@ export function createHttpServer(deps) {
|
|
|
77
78
|
cb(null, true);
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
// When we bind to a non-loopback host (e.g., 0.0.0.0 or LAN IP), allow cross-origin UI access.
|
|
82
|
+
if (deps.host === "0.0.0.0" || !isLoopbackHost(deps.host)) {
|
|
83
|
+
cb(null, true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
80
86
|
cb(null, false);
|
|
81
87
|
},
|
|
82
88
|
credentials: true,
|
|
@@ -212,12 +218,12 @@ export function createHttpServer(deps) {
|
|
|
212
218
|
actualPort = address.port;
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
|
-
const displayHost = deps.host === "
|
|
221
|
+
const displayHost = deps.host === "127.0.0.1" ? "localhost" : deps.host;
|
|
216
222
|
const serverUrl = `http://${displayHost}:${actualPort}`;
|
|
217
223
|
deps.serverMeta.httpBaseUrl = serverUrl;
|
|
218
224
|
deps.serverMeta.host = deps.host;
|
|
219
225
|
deps.serverMeta.port = actualPort;
|
|
220
|
-
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local";
|
|
226
|
+
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" || !isLoopbackHost(deps.host) ? "all" : "local";
|
|
221
227
|
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening");
|
|
222
228
|
console.log(`CodeNomad Server is ready at ${serverUrl}`);
|
|
223
229
|
return { port: actualPort, url: serverUrl, displayHost };
|
|
@@ -8,7 +8,7 @@ function buildMetaResponse(meta) {
|
|
|
8
8
|
return {
|
|
9
9
|
...meta,
|
|
10
10
|
port,
|
|
11
|
-
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
|
|
11
|
+
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
|
12
12
|
addresses,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
@@ -25,6 +25,9 @@ function resolvePort(meta) {
|
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function isLoopbackHost(host) {
|
|
29
|
+
return host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
30
|
+
}
|
|
28
31
|
function resolveAddresses(port, host) {
|
|
29
32
|
const interfaces = os.networkInterfaces();
|
|
30
33
|
const seen = new Set();
|
|
@@ -170,12 +170,14 @@ export class WorkspaceManager {
|
|
|
170
170
|
try {
|
|
171
171
|
const result = spawnSync(locator, [identifier], { encoding: "utf8" });
|
|
172
172
|
if (result.status === 0 && result.stdout) {
|
|
173
|
-
const
|
|
173
|
+
const candidates = result.stdout
|
|
174
174
|
.split(/\r?\n/)
|
|
175
175
|
.map((line) => line.trim())
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
.filter((line) => line.length > 0)
|
|
177
|
+
.filter((line) => !/^INFO:/i.test(line));
|
|
178
|
+
if (candidates.length > 0) {
|
|
179
|
+
const resolved = this.pickBinaryCandidate(candidates);
|
|
180
|
+
this.options.logger.debug({ identifier, resolved, candidates }, "Resolved binary path from system PATH");
|
|
179
181
|
return resolved;
|
|
180
182
|
}
|
|
181
183
|
}
|
|
@@ -188,6 +190,19 @@ export class WorkspaceManager {
|
|
|
188
190
|
}
|
|
189
191
|
return identifier;
|
|
190
192
|
}
|
|
193
|
+
pickBinaryCandidate(candidates) {
|
|
194
|
+
if (process.platform !== "win32") {
|
|
195
|
+
return candidates[0] ?? "";
|
|
196
|
+
}
|
|
197
|
+
const extensionPreference = [".exe", ".cmd", ".bat", ".ps1"];
|
|
198
|
+
for (const ext of extensionPreference) {
|
|
199
|
+
const match = candidates.find((candidate) => candidate.toLowerCase().endsWith(ext));
|
|
200
|
+
if (match) {
|
|
201
|
+
return match;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return candidates[0] ?? "";
|
|
205
|
+
}
|
|
191
206
|
detectBinaryVersion(resolvedPath) {
|
|
192
207
|
if (!resolvedPath) {
|
|
193
208
|
return undefined;
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { existsSync, statSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"]);
|
|
5
|
+
export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"]);
|
|
6
|
+
export function buildSpawnSpec(binaryPath, args) {
|
|
7
|
+
if (process.platform !== "win32") {
|
|
8
|
+
return { command: binaryPath, args, options: {} };
|
|
9
|
+
}
|
|
10
|
+
const extension = path.extname(binaryPath).toLowerCase();
|
|
11
|
+
if (WINDOWS_CMD_EXTENSIONS.has(extension)) {
|
|
12
|
+
const comspec = process.env.ComSpec || "cmd.exe";
|
|
13
|
+
// cmd.exe requires the full command as a single string.
|
|
14
|
+
// Using the ""<script> <args>"" pattern ensures paths with spaces are handled.
|
|
15
|
+
const commandLine = `""${binaryPath}" ${args.join(" ")}"`;
|
|
16
|
+
return {
|
|
17
|
+
command: comspec,
|
|
18
|
+
args: ["/d", "/s", "/c", commandLine],
|
|
19
|
+
options: { windowsVerbatimArguments: true },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (WINDOWS_POWERSHELL_EXTENSIONS.has(extension)) {
|
|
23
|
+
// powershell.exe ships with Windows. (pwsh may not.)
|
|
24
|
+
return {
|
|
25
|
+
command: "powershell.exe",
|
|
26
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args],
|
|
27
|
+
options: {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { command: binaryPath, args, options: {} };
|
|
31
|
+
}
|
|
4
32
|
const SENSITIVE_ENV_KEY = /(PASSWORD|TOKEN|SECRET)/i;
|
|
5
33
|
function redactEnvironment(env) {
|
|
6
34
|
const redacted = {};
|
|
@@ -44,19 +72,22 @@ export class WorkspaceRuntime {
|
|
|
44
72
|
return combined.join("\n");
|
|
45
73
|
};
|
|
46
74
|
return new Promise((resolve, reject) => {
|
|
47
|
-
const
|
|
75
|
+
const spec = buildSpawnSpec(options.binaryPath, args);
|
|
76
|
+
const commandLine = [spec.command, ...spec.args].join(" ");
|
|
48
77
|
this.logger.info({
|
|
49
78
|
workspaceId: options.workspaceId,
|
|
50
79
|
folder: options.folder,
|
|
51
80
|
binary: options.binaryPath,
|
|
52
|
-
|
|
81
|
+
spawnCommand: spec.command,
|
|
82
|
+
spawnArgs: spec.args,
|
|
53
83
|
commandLine,
|
|
54
84
|
env: redactEnvironment(env),
|
|
55
85
|
}, "Launching OpenCode process");
|
|
56
|
-
const child = spawn(
|
|
86
|
+
const child = spawn(spec.command, spec.args, {
|
|
57
87
|
cwd: options.folder,
|
|
58
88
|
env,
|
|
59
89
|
stdio: ["ignore", "pipe", "pipe"],
|
|
90
|
+
...spec.options,
|
|
60
91
|
});
|
|
61
92
|
const managed = { child, requestedStop: false };
|
|
62
93
|
this.processes.set(options.workspaceId, managed);
|