Skip to content

Commit 6d32e09

Browse files
authored
Merge pull request #94 from NeuralNomadsAI/dev
Release 0.9.0
2 parents 503cb3a + 0c9284e commit 6d32e09

21 files changed

Lines changed: 605 additions & 113 deletions

File tree

‎package-lock.json‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codenomad-workspace",
3-
"version": "0.8.1",
3+
"version": "0.9.0",
44
"private": true,
55
"description": "CodeNomad monorepo workspace",
66
"workspaces": {

‎packages/electron-app/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@neuralnomads/codenomad-electron-app",
3-
"version": "0.8.1",
3+
"version": "0.9.0",
44
"description": "CodeNomad - AI coding assistant",
55
"author": {
66
"name": "Neural Nomads",

‎packages/server/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/server/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@neuralnomads/codenomad",
3-
"version": "0.8.1",
3+
"version": "0.9.0",
44
"description": "CodeNomad Server",
55
"author": {
66
"name": "Neural Nomads",

‎packages/server/src/api-types.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ export interface FileSystemListResponse {
9595
metadata: FileSystemListingMetadata
9696
}
9797

98+
export interface FileSystemCreateFolderRequest {
99+
/**
100+
* Path identifier for the currently browsed directory.
101+
* Matches the `path` parameter used for `/api/filesystem`.
102+
*/
103+
parentPath?: string
104+
/** Single folder name (no separators). */
105+
name: string
106+
}
107+
108+
export interface FileSystemCreateFolderResponse {
109+
/**
110+
* Path identifier that can be passed back to `/api/filesystem` to browse the new folder.
111+
* Relative for restricted listings, absolute for unrestricted.
112+
*/
113+
path: string
114+
/** Absolute folder path on the server host. */
115+
absolutePath: string
116+
}
117+
98118
export const WINDOWS_DRIVES_ROOT = "__drives__"
99119

100120
export interface WorkspaceFileResponse {

‎packages/server/src/filesystem/browser.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from "fs"
22
import os from "os"
33
import path from "path"
44
import {
5+
FileSystemCreateFolderResponse,
56
FileSystemEntry,
67
FileSystemListResponse,
78
FileSystemListingMetadata,
@@ -56,6 +57,30 @@ export class FileSystemBrowser {
5657
return this.listRestrictedWithMetadata(targetPath, includeFiles)
5758
}
5859

60+
createFolder(parentPath: string | undefined, folderName: string): FileSystemCreateFolderResponse {
61+
const name = this.normalizeFolderName(folderName)
62+
63+
if (this.unrestricted) {
64+
const resolvedParent = this.resolveUnrestrictedPath(parentPath)
65+
if (this.isWindows && resolvedParent === WINDOWS_DRIVES_ROOT) {
66+
throw new Error("Cannot create folders at drive root")
67+
}
68+
this.assertDirectoryExists(resolvedParent)
69+
const absolutePath = this.resolveAbsoluteChild(resolvedParent, name)
70+
fs.mkdirSync(absolutePath)
71+
return { path: absolutePath, absolutePath }
72+
}
73+
74+
const normalizedParent = this.normalizeRelativePath(parentPath)
75+
const parentAbsolute = this.toRestrictedAbsolute(normalizedParent)
76+
this.assertDirectoryExists(parentAbsolute)
77+
78+
const relativePath = this.buildRelativePath(normalizedParent, name)
79+
const absolutePath = this.toRestrictedAbsolute(relativePath)
80+
fs.mkdirSync(absolutePath)
81+
return { path: relativePath, absolutePath }
82+
}
83+
5984
readFile(relativePath: string): string {
6085
if (this.unrestricted) {
6186
throw new Error("readFile is not available in unrestricted mode")
@@ -157,6 +182,41 @@ export class FileSystemBrowser {
157182
return { entries, metadata }
158183
}
159184

185+
private normalizeFolderName(input: string): string {
186+
const name = input.trim()
187+
if (!name) {
188+
throw new Error("Folder name is required")
189+
}
190+
191+
if (name === "." || name === "..") {
192+
throw new Error("Invalid folder name")
193+
}
194+
195+
if (name.startsWith("~")) {
196+
throw new Error("Invalid folder name")
197+
}
198+
199+
if (name.includes("/") || name.includes("\\")) {
200+
throw new Error("Folder name must not include path separators")
201+
}
202+
203+
if (name.includes("\u0000")) {
204+
throw new Error("Invalid folder name")
205+
}
206+
207+
return name
208+
}
209+
210+
private assertDirectoryExists(directory: string) {
211+
if (!fs.existsSync(directory)) {
212+
throw new Error(`Directory does not exist: ${directory}`)
213+
}
214+
const stats = fs.statSync(directory)
215+
if (!stats.isDirectory()) {
216+
throw new Error(`Path is not a directory: ${directory}`)
217+
}
218+
}
219+
160220
private readDirectoryEntries(directory: string, options: DirectoryReadOptions): FileSystemEntry[] {
161221
const dirents = fs.readdirSync(directory, { withFileTypes: true })
162222
const results: FileSystemEntry[] = []

‎packages/server/src/server/routes/filesystem.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const FilesystemQuerySchema = z.object({
1111
includeFiles: z.coerce.boolean().optional(),
1212
})
1313

14+
const FilesystemCreateFolderSchema = z.object({
15+
parentPath: z.string().optional(),
16+
name: z.string(),
17+
})
18+
1419
export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps) {
1520
app.get("/api/filesystem", async (request, reply) => {
1621
const query = FilesystemQuerySchema.parse(request.query ?? {})
@@ -24,4 +29,26 @@ export function registerFilesystemRoutes(app: FastifyInstance, deps: RouteDeps)
2429
return { error: (error as Error).message }
2530
}
2631
})
32+
33+
app.post("/api/filesystem/folders", async (request, reply) => {
34+
const body = FilesystemCreateFolderSchema.parse(request.body ?? {})
35+
36+
try {
37+
const created = deps.fileSystemBrowser.createFolder(body.parentPath, body.name)
38+
reply.code(201)
39+
return created
40+
} catch (error) {
41+
const err = error as NodeJS.ErrnoException
42+
if (err?.code === "EEXIST") {
43+
reply.code(409).type("text/plain").send("Folder already exists")
44+
return
45+
}
46+
if (err?.code === "EACCES" || err?.code === "EPERM") {
47+
reply.code(403).type("text/plain").send("Permission denied")
48+
return
49+
}
50+
51+
reply.code(400).type("text/plain").send((error as Error).message)
52+
}
53+
})
2754
}

0 commit comments

Comments
 (0)