@neuralnomads/codenomad 0.7.1 → 0.7.2
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.
|
@@ -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 {
|
|
@@ -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);
|