import { lstat, readdir } from "node:fs/promises"; import { join } from "node:path"; import type { FileTreeEntry, FileTreeResponse } from "../../shared/apiTypes.js"; import { resolveInsideWorkspace } from "./pathSafety.js"; const MAX_ENTRIES = 1000; export async function listWorkspaceTree(rootPath: string, path: string | undefined): Promise { const { target, relativePath } = await resolveInsideWorkspace(rootPath, path); const stat = await lstat(target); if (!stat.isDirectory()) throw new Error("Path is not a directory"); const dirents = await readdir(target, { withFileTypes: true }); const visible = dirents.filter((entry) => entry.name !== ".git" && entry.name !== "node_modules").sort((a, b) => { if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1; return a.name.localeCompare(b.name); }); const selected = visible.slice(0, MAX_ENTRIES); const entries = await Promise.all(selected.map(async (entry): Promise => { const absolute = join(target, entry.name); const childRelative = relativePath === "" ? entry.name : `${relativePath}/${entry.name}`; const childStat = await lstat(absolute); const type: FileTreeEntry["type"] = entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file"; return { name: entry.name, path: childRelative, type, size: childStat.size, modifiedAt: childStat.mtime.toISOString() }; })); return { path: relativePath, entries, scannedAt: new Date().toISOString(), truncated: visible.length > selected.length }; }