π File detail
tools/PowerShellTool/commandSemantics.ts
π― Use case
This module implements the βPowerShellToolβ tool (Power Shell) β something the model can call at runtime alongside other agent tools. On the API surface it exposes CommandSemantic and interpretCommandResult β mainly functions, hooks, or classes. What the file header says: Command semantics configuration for interpreting exit codes in PowerShell. PowerShell-native cmdlets do NOT need exit-code semantics: - Select-String (grep equivalent) exits 0 on no-match (returns $null) - Compare-Object (diff equivalent) exits 0 regardless - Test-Path exits 0 re.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Command semantics configuration for interpreting exit codes in PowerShell. PowerShell-native cmdlets do NOT need exit-code semantics: - Select-String (grep equivalent) exits 0 on no-match (returns $null) - Compare-Object (diff equivalent) exits 0 regardless - Test-Path exits 0 regardless (returns bool via pipeline) Native cmdlets signal failure via terminating errors ($?), not exit codes. However, EXTERNAL executables invoked from PowerShell DO set $LASTEXITCODE, and many use non-zero codes to convey information rather than failure: - grep.exe / rg.exe (Git for Windows, scoop, etc.): 1 = no match - findstr.exe (Windows native): 1 = no match - robocopy.exe (Windows native): 0-7 = success, 8+ = error (notorious!) Without this module, PowerShellTool throws ShellError on any non-zero exit, so `robocopy` reporting "files copied successfully" (exit 1) shows as an error.
π€ Exports (heuristic)
CommandSemanticinterpretCommandResult
π₯οΈ Source preview
/**
* Command semantics configuration for interpreting exit codes in PowerShell.
*
* PowerShell-native cmdlets do NOT need exit-code semantics:
* - Select-String (grep equivalent) exits 0 on no-match (returns $null)
* - Compare-Object (diff equivalent) exits 0 regardless
* - Test-Path exits 0 regardless (returns bool via pipeline)
* Native cmdlets signal failure via terminating errors ($?), not exit codes.
*
* However, EXTERNAL executables invoked from PowerShell DO set $LASTEXITCODE,
* and many use non-zero codes to convey information rather than failure:
* - grep.exe / rg.exe (Git for Windows, scoop, etc.): 1 = no match
* - findstr.exe (Windows native): 1 = no match
* - robocopy.exe (Windows native): 0-7 = success, 8+ = error (notorious!)
*
* Without this module, PowerShellTool throws ShellError on any non-zero exit,
* so `robocopy` reporting "files copied successfully" (exit 1) shows as an error.
*/
export type CommandSemantic = (
exitCode: number,
stdout: string,
stderr: string,
) => {
isError: boolean
message?: string
}
/**
* Default semantic: treat only 0 as success, everything else as error
*/
const DEFAULT_SEMANTIC: CommandSemantic = (exitCode, _stdout, _stderr) => ({
isError: exitCode !== 0,
message:
exitCode !== 0 ? `Command failed with exit code ${exitCode}` : undefined,
})
/**
* grep / ripgrep: 0 = matches found, 1 = no matches, 2+ = error
*/
const GREP_SEMANTIC: CommandSemantic = (exitCode, _stdout, _stderr) => ({
isError: exitCode >= 2,
message: exitCode === 1 ? 'No matches found' : undefined,
})
/**
* Command-specific semantics for external executables.
* Keys are lowercase command names WITHOUT .exe suffix.
*
* Deliberately omitted:
* - 'diff': Ambiguous. Windows PowerShell 5.1 aliases `diff` β Compare-Object
* (exit 0 on differ), but PS Core / Git for Windows may resolve to diff.exe
* (exit 1 on differ). Cannot reliably interpret.
* - 'fc': Ambiguous. PowerShell aliases `fc` β Format-Custom (a native cmdlet),
* but `fc.exe` is the Windows file compare utility (exit 1 = files differ).
* Same aliasing problem as `diff`.
* - 'find': Ambiguous. Windows find.exe (text search) vs Unix find.exe
* (file search via Git for Windows) have different semantics.
* - 'test', '[': Not PowerShell constructs.
* - 'select-string', 'compare-object', 'test-path': Native cmdlets exit 0.
*/
const COMMAND_SEMANTICS: Map<string, CommandSemantic> = new Map([
// External grep/ripgrep (Git for Windows, scoop, choco)
['grep', GREP_SEMANTIC],
['rg', GREP_SEMANTIC],
// findstr.exe: Windows native text search
// 0 = match found, 1 = no match, 2 = error
['findstr', GREP_SEMANTIC],
// robocopy.exe: Windows native robust file copy
// Exit codes are a BITFIELD β 0-7 are success, 8+ indicates at least one failure:
// 0 = no files copied, no mismatch, no failures (already in sync)
// 1 = files copied successfully
// 2 = extra files/dirs detected (no copy)
// 4 = mismatched files/dirs detected
// 8 = some files/dirs could not be copied (copy errors)
// 16 = serious error (robocopy did not copy any files)
// This is the single most common "CI failed but nothing's wrong" Windows gotcha.
[
'robocopy',
(exitCode, _stdout, _stderr) => ({
isError: exitCode >= 8,
message:
exitCode === 0
? 'No files copied (already in sync)'
: exitCode >= 1 && exitCode < 8
? exitCode & 1
? 'Files copied successfully'
: 'Robocopy completed (no errors)'
: undefined,
}),
],
])
/**
* Extract the command name from a single pipeline segment.
* Strips leading `&` / `.` call operators and `.exe` suffix, lowercases.
*/
function extractBaseCommand(segment: string): string {
// Strip PowerShell call operators: & "cmd", . "cmd"
// (& and . at segment start followed by whitespace invoke the next token)
const stripped = segment.trim().replace(/^[&.]\s+/, '')
const firstToken = stripped.split(/\s+/)[0] || ''
// Strip surrounding quotes if command was invoked as & "grep.exe"
const unquoted = firstToken.replace(/^["']|["']$/g, '')
// Strip path: C:\bin\grep.exe β grep.exe, .\rg.exe β rg.exe
const basename = unquoted.split(/[\\/]/).pop() || unquoted
// Strip .exe suffix (Windows is case-insensitive)
return basename.toLowerCase().replace(/\.exe$/, '')
}
/**
* Extract the primary command from a PowerShell command line.
* Takes the LAST pipeline segment since that determines the exit code.
*
* Heuristic split on `;` and `|` β may get it wrong for quoted strings or
* complex constructs. Do NOT depend on this for security; it's only used
* for exit-code interpretation (false negatives just fall back to default).
*/
function heuristicallyExtractBaseCommand(command: string): string {
const segments = command.split(/[;|]/).filter(s => s.trim())
const last = segments[segments.length - 1] || command
return extractBaseCommand(last)
}
/**
* Interpret command result based on semantic rules
*/
export function interpretCommandResult(
command: string,
exitCode: number,
stdout: string,
stderr: string,
): {
isError: boolean
message?: string
} {
const baseCommand = heuristicallyExtractBaseCommand(command)
const semantic = COMMAND_SEMANTICS.get(baseCommand) ?? DEFAULT_SEMANTIC
return semantic(exitCode, stdout, stderr)
}