Skip to content

Commit 57d3282

Browse files
committed
fix: handle built-in windows commands in validation check
1 parent d1f98b0 commit 57d3282

File tree

3 files changed

+113
-17
lines changed

3 files changed

+113
-17
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ import { strict as assert } from 'assert'
22
import * as mockfs from 'mock-fs'
33
import * as sinon from 'sinon'
44
import { ExecuteBash } from './executeBash'
5-
import { Features } from '@aws/language-server-runtimes/server-interface/server'
65
import { TestFeatures } from '@aws/language-server-runtimes/testing'
76
import { TextDocument } from 'vscode-languageserver-textdocument'
87
import { URI } from 'vscode-uri'
9-
import { InitializeParams } from '@aws/language-server-runtimes/protocol'
108

119
describe('ExecuteBash Tool', () => {
1210
let features: TestFeatures

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -559,25 +559,67 @@ export class ExecuteBash {
559559
return [str, false]
560560
}
561561

562-
private static async whichCommand(logger: Logging, cmd: string): Promise<string> {
563-
const { command, args } = IS_WINDOWS_PLATFORM
564-
? { command: 'where', args: [cmd] }
565-
: { command: 'sh', args: ['-c', `command -v ${cmd}`] }
566-
const cp = new processUtils.ChildProcess(logger, command, args, {
567-
collect: true,
568-
waitForStreams: true,
569-
})
570-
const result = await cp.run()
562+
private static async whichCommand(logger: Logging, cmd: string): Promise<void> {
563+
if (IS_WINDOWS_PLATFORM) {
564+
await this.resolveWindowsCommand(logger, cmd)
565+
} else {
566+
await this.resolveUnixCommand(logger, cmd)
567+
}
568+
}
569+
570+
private static async resolveWindowsCommand(logger: Logging, cmd: string): Promise<void> {
571+
// 1. Check for external command or alias
572+
try {
573+
const whereProc = new processUtils.ChildProcess(logger, 'where', [cmd], {
574+
collect: true,
575+
waitForStreams: true,
576+
})
577+
const result = await whereProc.run()
578+
const output = result.stdout.trim()
579+
580+
if (result.exitCode === 0 && output) {
581+
return
582+
}
583+
} catch (err) {
584+
logger.debug(`'where ${cmd}' failed: ${(err as Error).message}`)
585+
}
571586

572-
if (result.exitCode !== 0) {
573-
throw new Error(`Command '${cmd}' not found on PATH.`)
587+
// 2. Check for built-in command
588+
try {
589+
const helpProc = new processUtils.ChildProcess(logger, 'cmd.exe', ['/c', 'help', cmd], {
590+
collect: true,
591+
waitForStreams: true,
592+
})
593+
const result = await helpProc.run()
594+
const output = result.stdout.trim()
595+
596+
if (output && !output.includes('This command is not supported by the help utility')) {
597+
return
598+
}
599+
} catch (err) {
600+
logger.debug(`'help ${cmd}' failed: ${(err as Error).message}`)
574601
}
575602

576-
const output = result.stdout.trim()
577-
if (!output) {
578-
throw new Error(`Command '${cmd}' found but '${command} ${args.join(' ')}' returned empty output.`)
603+
throw new Error(`Command '${cmd}' not found as executable or Windows built-in command`)
604+
}
605+
606+
private static async resolveUnixCommand(logger: Logging, cmd: string): Promise<void> {
607+
try {
608+
const proc = new processUtils.ChildProcess(logger, 'sh', ['-c', `command -v ${cmd}`], {
609+
collect: true,
610+
waitForStreams: true,
611+
})
612+
const result = await proc.run()
613+
const output = result.stdout.trim()
614+
615+
if (result.exitCode === 0 && output) {
616+
return
617+
}
618+
} catch (err) {
619+
logger.debug(`'command -v ${cmd}' failed: ${(err as Error).message}`)
579620
}
580-
return output
621+
622+
throw new Error(`Command '${cmd}' not found as executable or shell built-in`)
581623
}
582624

583625
public async queueDescription(command: string, updates: WritableStream) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { strict as assert } from 'assert'
2+
import { split } from 'shlex'
3+
4+
describe('shlex.split for Windows command parsing', () => {
5+
it('should correctly split a git commit command with quotes', () => {
6+
const command = 'git commit -m "test commit"'
7+
const result = split(command)
8+
assert.deepEqual(result, ['git', 'commit', '-m', 'test commit'])
9+
})
10+
11+
it('should handle AWS CLI commands with JSON payloads', () => {
12+
const command =
13+
'aws lambda invoke --function-name test --payload \'{"firstName": "John", "lastName": "Smith"}\' output.json'
14+
const result = split(command)
15+
assert.deepEqual(result, [
16+
'aws',
17+
'lambda',
18+
'invoke',
19+
'--function-name',
20+
'test',
21+
'--payload',
22+
'{"firstName": "John", "lastName": "Smith"}',
23+
'output.json',
24+
])
25+
})
26+
27+
it('should handle multiline commands', () => {
28+
const command = `aws lambda invoke \\
29+
--function-name test \\
30+
--payload '{"firstName": "John", "lastName": "Smith"}' \\
31+
output.json`
32+
const result = split(command)
33+
assert.deepEqual(result, [
34+
'aws',
35+
'lambda',
36+
'invoke',
37+
'--function-name',
38+
'test',
39+
'--payload',
40+
'{"firstName": "John", "lastName": "Smith"}',
41+
'output.json',
42+
])
43+
})
44+
45+
it('should handle PowerShell commands with complex quoting', () => {
46+
const command = 'powershell -Command "& {Get-Process | Where-Object {$_.CPU -gt 10}}"'
47+
const result = split(command)
48+
assert.deepEqual(result, ['powershell', '-Command', '& {Get-Process | Where-Object {$_.CPU -gt 10}}'])
49+
})
50+
51+
it('should handle commands with environment variables', () => {
52+
const command = 'echo %PATH% && echo $HOME'
53+
const result = split(command)
54+
assert.deepEqual(result, ['echo', '%PATH%', '&&', 'echo', '$HOME'])
55+
})
56+
})

0 commit comments

Comments
 (0)