Skip to content

Commit 5a5053d

Browse files
betegonclaude
andcommitted
refactor(init): simplify listDir to use readdir like code-scanner
Replace opendir + 4 extracted helpers with a straightforward readdir walk matching the pattern in code-scanner.ts. Simpler, avoids the opendir symlink-following and double-close pitfalls, and uses the same biome suppression approach as collectFiles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c5660b8 commit 5a5053d

File tree

1 file changed

+44
-81
lines changed

1 file changed

+44
-81
lines changed

src/lib/init/local-ops.ts

Lines changed: 44 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -346,98 +346,61 @@ export async function handleLocalOp(
346346
}
347347
}
348348

349-
/** Directory names that are listed but never recursed into. */
350-
const SKIP_DIRS = new Set(["node_modules"]);
351-
352-
/** Return true if a symlink escapes the project directory. */
353-
function isEscapingSymlink(cwd: string, relPath: string): boolean {
354-
try {
355-
safePath(cwd, relPath);
356-
return false;
357-
} catch {
358-
return true;
359-
}
360-
}
361-
362-
/** Whether a directory entry should be recursed into. */
363-
function shouldRecurse(entry: fs.Dirent): boolean {
364-
if (!entry.isDirectory() || entry.isSymbolicLink()) {
365-
return false;
366-
}
367-
return !(entry.name.startsWith(".") || SKIP_DIRS.has(entry.name));
368-
}
349+
async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
350+
const { cwd, params } = payload;
351+
const targetPath = safePath(cwd, params.path);
352+
const maxDepth = params.maxDepth ?? 3;
353+
const maxEntries = params.maxEntries ?? 500;
354+
const recursive = params.recursive ?? false;
369355

370-
type WalkContext = {
371-
cwd: string;
372-
recursive: boolean;
373-
maxDepth: number;
374-
maxEntries: number;
375-
entries: DirEntry[];
376-
};
356+
const entries: DirEntry[] = [];
377357

378-
/** Process a single dirent during directory walking. */
379-
async function processDirEntry(
380-
ctx: WalkContext,
381-
dir: string,
382-
entry: fs.Dirent,
383-
depth: number
384-
): Promise<void> {
385-
const relPath = path.relative(ctx.cwd, path.join(dir, entry.name));
386-
if (entry.isSymbolicLink() && isEscapingSymlink(ctx.cwd, relPath)) {
387-
return;
388-
}
358+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive directory walk is inherently complex but straightforward
359+
async function walk(dir: string, depth: number): Promise<void> {
360+
if (entries.length >= maxEntries || depth > maxDepth) {
361+
return;
362+
}
389363

390-
const type = entry.isDirectory() ? "directory" : "file";
391-
ctx.entries.push({ name: entry.name, path: relPath, type });
364+
let dirEntries: fs.Dirent[];
365+
try {
366+
dirEntries = await fs.promises.readdir(dir, { withFileTypes: true });
367+
} catch {
368+
return;
369+
}
392370

393-
if (ctx.recursive && shouldRecurse(entry)) {
394-
await walkDir(ctx, path.join(dir, entry.name), depth + 1);
395-
}
396-
}
371+
for (const entry of dirEntries) {
372+
if (entries.length >= maxEntries) {
373+
return;
374+
}
397375

398-
async function walkDir(
399-
ctx: WalkContext,
400-
dir: string,
401-
depth: number
402-
): Promise<void> {
403-
if (ctx.entries.length >= ctx.maxEntries || depth > ctx.maxDepth) {
404-
return;
405-
}
376+
const relPath = path.relative(cwd, path.join(dir, entry.name));
406377

407-
let handle: fs.Dir;
408-
try {
409-
handle = await fs.promises.opendir(dir, { bufferSize: 1024 });
410-
} catch {
411-
return;
412-
}
378+
// Skip symlinks that escape the project directory
379+
if (entry.isSymbolicLink()) {
380+
try {
381+
safePath(cwd, relPath);
382+
} catch {
383+
continue;
384+
}
385+
}
413386

414-
// No explicit handle.close() needed: for-await-of auto-closes the Dir
415-
try {
416-
for await (const entry of handle) {
417-
if (ctx.entries.length >= ctx.maxEntries) {
418-
break;
387+
const type = entry.isDirectory() ? "directory" : "file";
388+
entries.push({ name: entry.name, path: relPath, type });
389+
390+
if (
391+
recursive &&
392+
entry.isDirectory() &&
393+
!entry.isSymbolicLink() &&
394+
!entry.name.startsWith(".") &&
395+
entry.name !== "node_modules"
396+
) {
397+
await walk(path.join(dir, entry.name), depth + 1);
419398
}
420-
await processDirEntry(ctx, dir, entry, depth);
421399
}
422-
} catch {
423-
// Directory unreadable (ENOENT, EACCES, etc.) — skip gracefully
424400
}
425-
}
426-
427-
async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
428-
const { cwd, params } = payload;
429-
const targetPath = safePath(cwd, params.path);
430-
431-
const ctx: WalkContext = {
432-
cwd,
433-
recursive: params.recursive ?? false,
434-
maxDepth: params.maxDepth ?? 3,
435-
maxEntries: params.maxEntries ?? 500,
436-
entries: [],
437-
};
438401

439-
await walkDir(ctx, targetPath, 0);
440-
return { ok: true, data: { entries: ctx.entries } };
402+
await walk(targetPath, 0);
403+
return { ok: true, data: { entries } };
441404
}
442405

443406
async function readSingleFile(

0 commit comments

Comments
 (0)