@neuralnomads/codenomad 0.8.1 → 0.9.1
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 +1 -0
- package/dist/filesystem/browser.js +48 -0
- package/dist/index.js +26 -16
- package/dist/server/routes/filesystem.js +24 -0
- package/dist/workspaces/manager.js +10 -7
- package/dist/workspaces/runtime.js +98 -22
- package/package.json +1 -1
- package/public/assets/index-DszBjqMJ.css +1 -0
- package/public/assets/{loading-BZSKFGkV.js → loading-A1Zxq2rg.js} +1 -1
- package/public/assets/main-4pIHKAk7.js +184 -0
- package/public/index.html +3 -3
- package/public/loading.html +3 -3
- package/public/ui-version.json +1 -1
- package/public/assets/index-CbktiZIE.css +0 -1
- package/public/assets/main-D5c_2IWi.js +0 -184
- /package/public/assets/{index-ipdyZsa8.js → index-t5GTLZC8.js} +0 -0
package/dist/config/schema.js
CHANGED
|
@@ -12,6 +12,7 @@ const PreferencesSchema = z.object({
|
|
|
12
12
|
lastUsedBinary: z.string().optional(),
|
|
13
13
|
environmentVariables: z.record(z.string()).default({}),
|
|
14
14
|
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
|
15
|
+
modelThinkingSelections: z.record(z.string(), z.string()).default({}),
|
|
15
16
|
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
|
16
17
|
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
|
17
18
|
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
|
@@ -30,6 +30,26 @@ export class FileSystemBrowser {
|
|
|
30
30
|
}
|
|
31
31
|
return this.listRestrictedWithMetadata(targetPath, includeFiles);
|
|
32
32
|
}
|
|
33
|
+
createFolder(parentPath, folderName) {
|
|
34
|
+
const name = this.normalizeFolderName(folderName);
|
|
35
|
+
if (this.unrestricted) {
|
|
36
|
+
const resolvedParent = this.resolveUnrestrictedPath(parentPath);
|
|
37
|
+
if (this.isWindows && resolvedParent === WINDOWS_DRIVES_ROOT) {
|
|
38
|
+
throw new Error("Cannot create folders at drive root");
|
|
39
|
+
}
|
|
40
|
+
this.assertDirectoryExists(resolvedParent);
|
|
41
|
+
const absolutePath = this.resolveAbsoluteChild(resolvedParent, name);
|
|
42
|
+
fs.mkdirSync(absolutePath);
|
|
43
|
+
return { path: absolutePath, absolutePath };
|
|
44
|
+
}
|
|
45
|
+
const normalizedParent = this.normalizeRelativePath(parentPath);
|
|
46
|
+
const parentAbsolute = this.toRestrictedAbsolute(normalizedParent);
|
|
47
|
+
this.assertDirectoryExists(parentAbsolute);
|
|
48
|
+
const relativePath = this.buildRelativePath(normalizedParent, name);
|
|
49
|
+
const absolutePath = this.toRestrictedAbsolute(relativePath);
|
|
50
|
+
fs.mkdirSync(absolutePath);
|
|
51
|
+
return { path: relativePath, absolutePath };
|
|
52
|
+
}
|
|
33
53
|
readFile(relativePath) {
|
|
34
54
|
if (this.unrestricted) {
|
|
35
55
|
throw new Error("readFile is not available in unrestricted mode");
|
|
@@ -117,6 +137,34 @@ export class FileSystemBrowser {
|
|
|
117
137
|
};
|
|
118
138
|
return { entries, metadata };
|
|
119
139
|
}
|
|
140
|
+
normalizeFolderName(input) {
|
|
141
|
+
const name = input.trim();
|
|
142
|
+
if (!name) {
|
|
143
|
+
throw new Error("Folder name is required");
|
|
144
|
+
}
|
|
145
|
+
if (name === "." || name === "..") {
|
|
146
|
+
throw new Error("Invalid folder name");
|
|
147
|
+
}
|
|
148
|
+
if (name.startsWith("~")) {
|
|
149
|
+
throw new Error("Invalid folder name");
|
|
150
|
+
}
|
|
151
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
152
|
+
throw new Error("Folder name must not include path separators");
|
|
153
|
+
}
|
|
154
|
+
if (name.includes("\u0000")) {
|
|
155
|
+
throw new Error("Invalid folder name");
|
|
156
|
+
}
|
|
157
|
+
return name;
|
|
158
|
+
}
|
|
159
|
+
assertDirectoryExists(directory) {
|
|
160
|
+
if (!fs.existsSync(directory)) {
|
|
161
|
+
throw new Error(`Directory does not exist: ${directory}`);
|
|
162
|
+
}
|
|
163
|
+
const stats = fs.statSync(directory);
|
|
164
|
+
if (!stats.isDirectory()) {
|
|
165
|
+
throw new Error(`Path is not a directory: ${directory}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
120
168
|
readDirectoryEntries(directory, options) {
|
|
121
169
|
const dirents = fs.readdirSync(directory, { withFileTypes: true });
|
|
122
170
|
const results = [];
|
package/dist/index.js
CHANGED
|
@@ -205,22 +205,32 @@ async function main() {
|
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
207
|
shuttingDown = true;
|
|
208
|
-
logger.info("Received shutdown signal,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
208
|
+
logger.info("Received shutdown signal, stopping workspaces and server");
|
|
209
|
+
const shutdownWorkspaces = (async () => {
|
|
210
|
+
try {
|
|
211
|
+
instanceEventBridge.shutdown();
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger.warn({ err: error }, "Instance event bridge shutdown failed");
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await workspaceManager.shutdown();
|
|
218
|
+
logger.info("Workspace manager shutdown complete");
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
logger.error({ err: error }, "Workspace manager shutdown failed");
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
const shutdownHttp = (async () => {
|
|
225
|
+
try {
|
|
226
|
+
await server.stop();
|
|
227
|
+
logger.info("HTTP server stopped");
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.error({ err: error }, "Failed to stop HTTP server");
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
await Promise.allSettled([shutdownWorkspaces, shutdownHttp]);
|
|
224
234
|
// no-op: remote UI manifest replaces GitHub release monitor
|
|
225
235
|
logger.info("Exiting process");
|
|
226
236
|
process.exit(0);
|
|
@@ -3,6 +3,10 @@ const FilesystemQuerySchema = z.object({
|
|
|
3
3
|
path: z.string().optional(),
|
|
4
4
|
includeFiles: z.coerce.boolean().optional(),
|
|
5
5
|
});
|
|
6
|
+
const FilesystemCreateFolderSchema = z.object({
|
|
7
|
+
parentPath: z.string().optional(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
});
|
|
6
10
|
export function registerFilesystemRoutes(app, deps) {
|
|
7
11
|
app.get("/api/filesystem", async (request, reply) => {
|
|
8
12
|
const query = FilesystemQuerySchema.parse(request.query ?? {});
|
|
@@ -16,4 +20,24 @@ export function registerFilesystemRoutes(app, deps) {
|
|
|
16
20
|
return { error: error.message };
|
|
17
21
|
}
|
|
18
22
|
});
|
|
23
|
+
app.post("/api/filesystem/folders", async (request, reply) => {
|
|
24
|
+
const body = FilesystemCreateFolderSchema.parse(request.body ?? {});
|
|
25
|
+
try {
|
|
26
|
+
const created = deps.fileSystemBrowser.createFolder(body.parentPath, body.name);
|
|
27
|
+
reply.code(201);
|
|
28
|
+
return created;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const err = error;
|
|
32
|
+
if (err?.code === "EEXIST") {
|
|
33
|
+
reply.code(409).type("text/plain").send("Folder already exists");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
37
|
+
reply.code(403).type("text/plain").send("Permission denied");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
reply.code(400).type("text/plain").send(error.message);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
19
43
|
}
|
|
@@ -136,16 +136,19 @@ export class WorkspaceManager {
|
|
|
136
136
|
}
|
|
137
137
|
async shutdown() {
|
|
138
138
|
this.options.logger.info("Shutting down all workspaces");
|
|
139
|
+
const stopTasks = [];
|
|
139
140
|
for (const [id, workspace] of this.workspaces) {
|
|
140
|
-
if (workspace.pid) {
|
|
141
|
-
this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown");
|
|
142
|
-
await this.runtime.stop(id).catch((error) => {
|
|
143
|
-
this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown");
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
141
|
+
if (!workspace.pid) {
|
|
147
142
|
this.options.logger.debug({ workspaceId: id }, "Workspace already stopped");
|
|
143
|
+
continue;
|
|
148
144
|
}
|
|
145
|
+
this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown");
|
|
146
|
+
stopTasks.push(this.runtime.stop(id).catch((error) => {
|
|
147
|
+
this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown");
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
if (stopTasks.length > 0) {
|
|
151
|
+
await Promise.allSettled(stopTasks);
|
|
149
152
|
}
|
|
150
153
|
this.workspaces.clear();
|
|
151
154
|
this.opencodeAuth.clear();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import { existsSync, statSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
export const WINDOWS_CMD_EXTENSIONS = new Set([".cmd", ".bat"]);
|
|
@@ -83,10 +83,12 @@ export class WorkspaceRuntime {
|
|
|
83
83
|
commandLine,
|
|
84
84
|
env: redactEnvironment(env),
|
|
85
85
|
}, "Launching OpenCode process");
|
|
86
|
+
const detached = process.platform !== "win32";
|
|
86
87
|
const child = spawn(spec.command, spec.args, {
|
|
87
88
|
cwd: options.folder,
|
|
88
89
|
env,
|
|
89
90
|
stdio: ["ignore", "pipe", "pipe"],
|
|
91
|
+
detached,
|
|
90
92
|
...spec.options,
|
|
91
93
|
});
|
|
92
94
|
const managed = { child, requestedStop: false };
|
|
@@ -202,10 +204,90 @@ export class WorkspaceRuntime {
|
|
|
202
204
|
managed.requestedStop = true;
|
|
203
205
|
const child = managed.child;
|
|
204
206
|
this.logger.info({ workspaceId }, "Stopping OpenCode process");
|
|
207
|
+
const pid = child.pid;
|
|
208
|
+
if (!pid) {
|
|
209
|
+
this.logger.warn({ workspaceId }, "Workspace process missing PID; cannot stop");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const isAlreadyExited = () => child.exitCode !== null || child.signalCode !== null;
|
|
213
|
+
const tryKillPosixGroup = (signal) => {
|
|
214
|
+
try {
|
|
215
|
+
// Negative PID targets the process group (POSIX).
|
|
216
|
+
process.kill(-pid, signal);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const err = error;
|
|
221
|
+
if (err?.code === "ESRCH") {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
this.logger.debug({ workspaceId, pid, err }, "Failed to signal POSIX process group");
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const tryKillSinglePid = (signal) => {
|
|
229
|
+
try {
|
|
230
|
+
process.kill(pid, signal);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const err = error;
|
|
235
|
+
if (err?.code === "ESRCH") {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
this.logger.debug({ workspaceId, pid, err }, "Failed to signal workspace PID");
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const tryTaskkill = (force) => {
|
|
243
|
+
const args = ["/PID", String(pid), "/T"];
|
|
244
|
+
if (force) {
|
|
245
|
+
args.push("/F");
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const result = spawnSync("taskkill", args, { encoding: "utf8" });
|
|
249
|
+
const exitCode = result.status;
|
|
250
|
+
if (exitCode === 0) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
// If the PID is already gone, treat it as success.
|
|
254
|
+
const stderr = (result.stderr ?? "").toString().toLowerCase();
|
|
255
|
+
const stdout = (result.stdout ?? "").toString().toLowerCase();
|
|
256
|
+
const combined = `${stdout}\n${stderr}`;
|
|
257
|
+
if (combined.includes("not found") || combined.includes("no running instance") || combined.includes("process") && combined.includes("not")) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
this.logger.debug({ workspaceId, pid, exitCode, stderr: result.stderr, stdout: result.stdout }, "taskkill failed");
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
this.logger.debug({ workspaceId, pid, err: error }, "taskkill failed to execute");
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const sendStopSignal = (signal) => {
|
|
269
|
+
if (process.platform === "win32") {
|
|
270
|
+
// Best-effort: terminate the whole process tree rooted at pid.
|
|
271
|
+
// Use /F only for escalation.
|
|
272
|
+
tryTaskkill(signal === "SIGKILL");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Prefer process-group signaling so wrapper launchers (bun/node) don't orphan the real server.
|
|
276
|
+
const groupOk = tryKillPosixGroup(signal);
|
|
277
|
+
if (!groupOk) {
|
|
278
|
+
// Fallback to direct PID kill.
|
|
279
|
+
tryKillSinglePid(signal);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
205
282
|
await new Promise((resolve, reject) => {
|
|
283
|
+
let escalationTimer = null;
|
|
206
284
|
const cleanup = () => {
|
|
207
285
|
child.removeListener("exit", onExit);
|
|
208
286
|
child.removeListener("error", onError);
|
|
287
|
+
if (escalationTimer) {
|
|
288
|
+
clearTimeout(escalationTimer);
|
|
289
|
+
escalationTimer = null;
|
|
290
|
+
}
|
|
209
291
|
};
|
|
210
292
|
const onExit = () => {
|
|
211
293
|
cleanup();
|
|
@@ -215,30 +297,24 @@ export class WorkspaceRuntime {
|
|
|
215
297
|
cleanup();
|
|
216
298
|
reject(error);
|
|
217
299
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
resolve();
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
};
|
|
227
|
-
child.once("exit", onExit);
|
|
228
|
-
child.once("error", onError);
|
|
229
|
-
if (resolveIfAlreadyExited()) {
|
|
300
|
+
if (isAlreadyExited()) {
|
|
301
|
+
this.logger.debug({ workspaceId, exitCode: child.exitCode, signal: child.signalCode }, "Process already exited");
|
|
302
|
+
cleanup();
|
|
303
|
+
resolve();
|
|
230
304
|
return;
|
|
231
305
|
}
|
|
232
|
-
|
|
233
|
-
child.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
306
|
+
child.once("exit", onExit);
|
|
307
|
+
child.once("error", onError);
|
|
308
|
+
this.logger.debug({ workspaceId, pid, detached: process.platform !== "win32" }, "Sending SIGTERM to workspace process (tree/group)");
|
|
309
|
+
sendStopSignal("SIGTERM");
|
|
310
|
+
escalationTimer = setTimeout(() => {
|
|
311
|
+
escalationTimer = null;
|
|
312
|
+
if (isAlreadyExited()) {
|
|
313
|
+
this.logger.debug({ workspaceId, pid }, "Workspace exited before SIGKILL escalation");
|
|
314
|
+
return;
|
|
241
315
|
}
|
|
316
|
+
this.logger.warn({ workspaceId, pid }, "Process did not stop after SIGTERM, escalating");
|
|
317
|
+
sendStopSignal("SIGKILL");
|
|
242
318
|
}, 2000);
|
|
243
319
|
});
|
|
244
320
|
}
|