Limit literal PATH probes to agent detection

This commit is contained in:
Chen
2026-10-28 17:51:53 -07:00
committed by Jinjing
parent b2fb4d1d38
commit f54448bfb7
2 changed files with 16 additions and 16 deletions
+1 -9
View File
@@ -80,7 +80,6 @@ describe('isCommandOnLocalPath', () => {
dir = await mkdtemp(path.join(tmpdir(), 'cmd-resolver-win32-'))
// Match PATHEXT case in fixtures; real Windows handles filename case while Linux test filesystems do not.
await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n')
await writeFile(path.join(dir, 'literal-cli'), '')
})
afterAll(async () => {
await rm(dir, { recursive: true, force: true })
@@ -113,14 +112,6 @@ describe('isCommandOnLocalPath', () => {
).resolves.toBe(true)
})
it('matches an extensionless executable alongside PATHEXT candidates', async () => {
await expect(
isCommandOnLocalPath('literal-cli', {
platform: 'win32',
env: { Path: dir, PATHEXT: '.EXE' }
})
).resolves.toBe(true)
})
})
// Executable-but-broken shims can shadow working binaries; retain later PATH candidates (#22975).
@@ -223,6 +214,7 @@ describe('isCommandOnLocalPath', () => {
it('lists every PATHEXT permutation on win32, in resolver order', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'cmd-list-win32-'))
try {
await writeFile(path.join(dir, 'tool'), '')
await writeFile(path.join(dir, 'tool.CMD'), '@echo off\n')
await writeFile(path.join(dir, 'tool.EXE'), '')
await expect(
+15 -7
View File
@@ -29,14 +29,22 @@ function readEnvCaseInsensitive(env: NodeJS.ProcessEnv, key: string): string | u
return undefined
}
function getWindowsExtensions(env: NodeJS.ProcessEnv): string[] {
function getWindowsPathSearchExtensions(env: NodeJS.ProcessEnv, command: string): string[] {
const pathext = readEnvCaseInsensitive(env, 'PATHEXT') ?? '.EXE;.CMD;.BAT;.COM'
const extensions = pathext.split(';').filter((ext) => ext.length > 0)
// CLI fallback resolution also checks a literal executable name, even without an extension.
extensions.unshift('')
// Explicit extensions also permit exact-name matches alongside PATHEXT variants.
if (command.includes('.')) {
extensions.unshift('')
}
return extensions
}
/** Agent CLI fallback also checks a literal executable name without an extension. */
function getWindowsBatchDetectionExtensions(env: NodeJS.ProcessEnv, command: string): string[] {
const extensions = getWindowsPathSearchExtensions(env, command)
return extensions.includes('') ? extensions : ['', ...extensions]
}
type LocalCommandSelection = {
scope: string
selected?: { binary: string; stamp: string; cwd?: string }
@@ -169,8 +177,8 @@ export async function isCommandOnLocalPath(
/** Batched PATH lookup for detection: the subset of `commands` present on PATH.
* Reads each absolute PATH directory once instead of stat()ing every command-extension
* candidate (#34636); hits still go through `isExecutableFile`, so symlink, directory,
* permission and case semantics match `isCommandOnLocalPath`. */
* candidate (#34636); hits still go through `isExecutableFile`, preserving symlink, directory,
* permission and case semantics. It also matches literal Windows CLI names used by agent fallback. */
export async function findCommandsOnLocalPath(
commands: readonly string[],
options: ResolveCommandOptions = {}
@@ -231,7 +239,7 @@ export async function findCommandsOnLocalPath(
)
for (const command of bareCommands) {
const extensions = isWin ? getWindowsExtensions(env) : ['']
const extensions = isWin ? getWindowsBatchDetectionExtensions(env, command) : ['']
// Deduplicate candidates per command: two commands (for example `tool` and
// `tool.CMD`) may resolve to the same on-disk file, and each must still be found.
const seenCandidates = new Set<string>()
@@ -307,7 +315,7 @@ async function findLocalCommandPaths(
const pathDirs = (readEnvCaseInsensitive(env, 'PATH') ?? '').split(delimiter)
// Separators bypass PATH; Windows searches cwd first.
const searchDirs = hasPathSeparator ? [''] : isWin ? [cwd, ...pathDirs] : pathDirs
const extensions = isWin ? getWindowsExtensions(env) : ['']
const extensions = isWin ? getWindowsPathSearchExtensions(env, command) : ['']
const found: string[] = []
const seen = new Set<string>()