From 78f858d351e2c0643180503d7837ae4a15d65028 Mon Sep 17 00:00:00 2001 From: Mikey Date: Wed, 2 Sep 2026 14:28:18 -0700 Subject: [PATCH] Eliminate statSync calls and filter prefixes first in path completion --- cli/src/__tests__/path-completion.test.ts | 20 ++++++++++++++++- cli/src/utils/path-completion.ts | 27 ++++++++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/cli/src/__tests__/path-completion.test.ts b/cli/src/__tests__/path-completion.test.ts index 8c09dde41a..6a3f751476 100644 --- a/cli/src/__tests__/path-completion.test.ts +++ b/cli/src/__tests__/path-completion.test.ts @@ -1,4 +1,10 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs' +import { + mkdirSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'fs' import os from 'os' import path from 'path' @@ -239,5 +245,17 @@ describe('getPathCompletion', () => { // Files should be ignored, common prefix from directories only expect(result).toBe(path.join(tempDir, 'project-')) }) + + test('completes symlinks pointing to directories', () => { + const targetDir = path.join(tempDir, 'actual-dir') + mkdirSync(targetDir) + try { + symlinkSync(targetDir, path.join(tempDir, 'symlinked-dir')) + const result = getPathCompletion(path.join(tempDir, 'sym')) + expect(result).toBe(path.join(tempDir, 'symlinked-dir') + path.sep) + } catch { + // Skip on systems where symlink creation is unprivileged/unsupported + } + }) }) }) diff --git a/cli/src/utils/path-completion.ts b/cli/src/utils/path-completion.ts index 5a40d7a8f8..333c40fd5a 100644 --- a/cli/src/utils/path-completion.ts +++ b/cli/src/utils/path-completion.ts @@ -48,22 +48,29 @@ export function getPathCompletion(inputPath: string): string | null { // List directories in parent that match the partial try { - const items = readdirSync(parentDir) + const entries = readdirSync(parentDir, { withFileTypes: true }) const matches: string[] = [] - for (const item of items) { + for (const entry of entries) { + const name = entry.name // Skip hidden files unless user typed a dot - if (item.startsWith('.') && !partial.startsWith('.')) continue + if (name.startsWith('.') && !partial.startsWith('.')) continue - const fullPath = path.join(parentDir, item) - try { - if (!statSync(fullPath).isDirectory()) continue - } catch { - continue + // Filter by prefix first before doing any directory resolution + if (!name.toLowerCase().startsWith(partial)) continue + + let isDirectory = entry.isDirectory() + if (!isDirectory && entry.isSymbolicLink()) { + try { + const fullPath = path.join(parentDir, name) + isDirectory = statSync(fullPath).isDirectory() + } catch { + isDirectory = false + } } - if (item.toLowerCase().startsWith(partial)) { - matches.push(item) + if (isDirectory) { + matches.push(name) } }