From 9718e50840c184e776064b4f61a317a72cdc032e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 9 Feb 2026 13:28:25 +0700 Subject: [PATCH] Update files --- .../src/server/file-explorer/service.test.ts | 34 +++++++++++++++++++ .../src/server/file-explorer/service.ts | 26 ++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/server/file-explorer/service.test.ts diff --git a/packages/server/src/server/file-explorer/service.test.ts b/packages/server/src/server/file-explorer/service.test.ts new file mode 100644 index 000000000..7f5598400 --- /dev/null +++ b/packages/server/src/server/file-explorer/service.test.ts @@ -0,0 +1,34 @@ +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { listDirectoryEntries } from "./service.js"; + +async function createTempDir(prefix: string): Promise { + return mkdtemp(path.join(os.tmpdir(), prefix)); +} + +describe("file explorer service", () => { + it("lists directory entries even when a dangling symlink exists", async () => { + const root = await createTempDir("paseo-file-explorer-"); + + try { + await mkdir(path.join(root, "packages", "server"), { recursive: true }); + const serverDir = path.join(root, "packages", "server"); + await writeFile(path.join(serverDir, "README.md"), "# server\n", "utf-8"); + await symlink("CLAUDE.md", path.join(serverDir, "AGENTS.md")); + + const result = await listDirectoryEntries({ + root, + relativePath: "packages/server", + }); + + expect(result.path).toBe("packages/server"); + const names = result.entries.map((entry) => entry.name); + expect(names).toContain("README.md"); + expect(names).not.toContain("AGENTS.md"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/server/src/server/file-explorer/service.ts b/packages/server/src/server/file-explorer/service.ts index 9d067fec9..9d40c7ff3 100644 --- a/packages/server/src/server/file-explorer/service.ts +++ b/packages/server/src/server/file-explorer/service.ts @@ -96,15 +96,32 @@ export async function listDirectoryEntries({ const dirents = await fs.readdir(directoryPath, { withFileTypes: true }); - const entries = await Promise.all( + const entriesWithNulls = await Promise.all( dirents.map(async (dirent) => { const targetPath = path.join(directoryPath, dirent.name); const kind: ExplorerEntryKind = dirent.isDirectory() ? "directory" : "file"; - return buildEntryPayload({ root, targetPath, name: dirent.name, kind }); + try { + return await buildEntryPayload({ + root, + targetPath, + name: dirent.name, + kind, + }); + } catch (error) { + // Directories can contain dangling links (e.g. AGENTS.md -> CLAUDE.md). + // Skip entries whose targets disappeared instead of failing the whole listing. + if (isMissingEntryError(error)) { + return null; + } + throw error; + } }) ); + const entries = entriesWithNulls.filter( + (entry): entry is FileExplorerEntry => entry !== null + ); entries.sort((a, b) => { const modifiedComparison = @@ -236,6 +253,11 @@ async function buildEntryPayload({ }; } +function isMissingEntryError(error: unknown): boolean { + const code = (error as NodeJS.ErrnoException | null)?.code; + return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP"; +} + function normalizeRelativePath({ root, targetPath,