fix(windows): reuse shared PowerShell literal quoting at every hand-rolled escaper (#23083)

Co-authored-by: Orca Worker <orca-worker@localhost>
This commit is contained in:
OrcaWin
2026-09-25 23:13:31 -07:00
committed by GitHub
co-authored by Orca Worker
parent 80ff6c4e3e
commit 4cafa50ec0
21 changed files with 123 additions and 68 deletions
@@ -638,6 +638,15 @@ describe('wrapWindowsHookCommand', () => {
)
})
it('doubles typographic single quotes in the script path literal', () => {
const decoded = decodeWindowsHookCommand(
wrapWindowsHookCommand('C:\\Users\\O\u2019Brien\\.orca\\agent-hooks\\codex-hook.cmd')
)
expect(decoded).toContain(
"Test-Path -LiteralPath 'C:\\Users\\O\u2019\u2019Brien\\.orca\\agent-hooks\\codex-hook.cmd'"
)
})
it('scopes environment variables inside the encoded launcher', () => {
const command = wrapWindowsHookCommand('C:\\hooks\\copilot-hook.ps1', {
ORCA_COPILOT_HOOK_EVENT: 'UserPromptSubmit'
+4 -7
View File
@@ -12,6 +12,7 @@ import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { randomUUID } from 'node:crypto'
import type { AgentHookSource } from '../../shared/agent-hook-relay'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
import { grantDirAcl, isPermissionError } from '../win32-utils'
import { resolveHooksJsonWritePath } from './hook-config-write-path'
import { writeRollingFileBackup } from '../rolling-file-backup'
@@ -108,10 +109,6 @@ export function getSharedManagedScriptPath(scriptFileName: string): string {
export { wrapPosixHookCommand } from './posix-hook-command'
export function quotePowerShellString(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
export {
wrapWindowsPowerShellEncodedCommand,
WINDOWS_POWERSHELL_HOOK_SWITCHES
@@ -134,14 +131,14 @@ export function buildWindowsHookPowerShellCommand(
options: { fallbackStdout?: string } = {}
): string {
// Why: the encoded launcher protects paths across Windows shells and drains stdin when the config points at a missing script.
const quoted = quotePowerShellString(scriptPath)
const quoted = quotePowerShellLiteral(scriptPath)
const envPrefix = Object.entries(env)
.map(([key, value]) => `$env:${key} = ${quotePowerShellString(value)}; `)
.map(([key, value]) => `$env:${key} = ${quotePowerShellLiteral(value)}; `)
.join('')
const fallback =
options.fallbackStdout === undefined
? ''
: `Write-Output ${quotePowerShellString(options.fallbackStdout)}; `
: `Write-Output ${quotePowerShellLiteral(options.fallbackStdout)}; `
// Why the order: answer first (a gate event reads silence as deny), then the shared
// env guard, and only then own stdin — outside an Orca pane the caller may abandon the
// pipe, and ReadToEnd would strand the launcher there forever (#11549).
+2 -2
View File
@@ -6,13 +6,13 @@ import {
getSharedManagedScriptPath,
isPlainObject,
MANAGED_HOOK_TIMEOUT_SECONDS,
quotePowerShellString,
removeManagedCommands,
wrapWindowsPowerShellEncodedCommand,
type HookCommandConfig,
type HookDefinition,
type HooksConfig
} from '../agent-hooks/installer-utils'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
import { wrapRuntimeHomeHookCommand } from '../agent-hooks/runtime-home-hook-command'
import { wrapWindowsDirectCmdHookCommand } from '../agent-hooks/windows-direct-cmd-hook-command'
import { isGitBashAvailable } from '../git-bash'
@@ -192,7 +192,7 @@ export function getWindowsManagedLifecycleHook(
}
const scriptFileName = win32.basename(scriptPath)
// Why: runtime profile resolution keeps the managed entry portable across users (STA-3348).
const quotedRelativePath = quotePowerShellString(`.orca\\agent-hooks\\${scriptFileName}`)
const quotedRelativePath = quotePowerShellLiteral(`.orca\\agent-hooks\\${scriptFileName}`)
// Why: compat consumers require neutral JSON even when the managed script is missing (#14818).
const innerCommand =
`$scriptPath = Join-Path $env:USERPROFILE ${quotedRelativePath}; ` +
+20 -1
View File
@@ -4,7 +4,11 @@ const runProcessMock = vi.hoisted(() => vi.fn())
vi.mock('../../shared/child-process/run-process', () => ({ runProcess: runProcessMock }))
import { runMacPrivilegedCommand, runWindowsPathCommand } from './cli-privileged-processes'
import {
runMacPrivilegedCommand,
runWindowsPathCommand,
writeWindowsUserPath
} from './cli-privileged-processes'
describe('Windows CLI PATH process boundary', () => {
beforeEach(() => runProcessMock.mockReset())
@@ -55,6 +59,21 @@ describe('Windows CLI PATH process boundary', () => {
stderr: 'UnauthorizedAccessException'
})
})
it('doubles typographic single quotes in the PATH literal', async () => {
runProcessMock.mockResolvedValue({
code: 0,
signal: null,
stdout: '',
stderr: '',
timedOut: false
})
await writeWindowsUserPath('C:\\O\u2019Brien\\bin')
expect(runProcessMock.mock.calls[0][0].args.at(-1)).toBe(
"[Environment]::SetEnvironmentVariable('Path', 'C:\\O\u2019\u2019Brien\\bin', 'User')"
)
})
})
describe('macOS CLI privileged process boundary', () => {
+2 -5
View File
@@ -1,4 +1,5 @@
import { runProcess } from '../../shared/child-process/run-process'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
import { WINDOWS_PATH_WRITE_TIMEOUT_MS } from './cli-install-constants'
export async function runMacPrivilegedCommand(command: string): Promise<void> {
@@ -21,7 +22,7 @@ export async function writeWindowsUserPath(value: string): Promise<void> {
await runWindowsPathCommand([
'-NoProfile',
'-Command',
`[Environment]::SetEnvironmentVariable('Path', ${quotePowerShell(value)}, 'User')`
`[Environment]::SetEnvironmentVariable('Path', ${quotePowerShellLiteral(value)}, 'User')`
])
}
@@ -49,7 +50,3 @@ function processFailure(
Object.assign(error, { code: result.code, stderr: result.stderr })
return error
}
function quotePowerShell(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
@@ -67,6 +67,18 @@ describe('windows mobile firewall', () => {
expect(script).toContain('$localPrefixLength = [int]$ip.PrefixLength')
})
it('doubles typographic single quotes in the executable literal', async () => {
const runPowerShell = vi.fn().mockResolvedValue('not json')
await inspectWindowsMobileFirewall(
6768,
'192.168.0.108',
environment(runPowerShell, { executablePath: 'C:\\Users\\O\u2019Brien\\Orca\\Orca.exe' })
)
expect(runPowerShell.mock.calls[0]![0]).toContain(
"-Program 'C:\\Users\\O\u2019\u2019Brien\\Orca\\Orca.exe'"
)
})
it('treats an overlapping inbound Block rule as overriding a matching Allow rule', async () => {
const runPowerShell = vi.fn().mockResolvedValue(
JSON.stringify({
+7 -10
View File
@@ -5,6 +5,7 @@ import type {
WindowsMobileFirewallStatus,
WindowsNetworkCategory
} from '../../shared/windows-mobile-firewall'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
import { hasSufficientWindowsFirewallRemoteScope } from './windows-firewall-remote-scope'
const FIREWALL_RULE_NAME = 'Orca.MobilePairing'
@@ -158,15 +159,11 @@ function parseNetworkCategory(value: string): WindowsNetworkCategory {
return 'unknown'
}
function quotePowerShell(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
function buildInspectionScript(port: number, executablePath: string, address?: string): string {
const addressLookup = address
? `
try {
$ip = Get-NetIPAddress -IPAddress ${quotePowerShell(address)} -ErrorAction Stop | Select-Object -First 1
$ip = Get-NetIPAddress -IPAddress ${quotePowerShellLiteral(address)} -ErrorAction Stop | Select-Object -First 1
$localAddress = [string]$ip.IPAddress
$localPrefixLength = [int]$ip.PrefixLength
$profile = Get-NetConnectionProfile -InterfaceIndex $ip.InterfaceIndex -ErrorAction Stop | Select-Object -First 1
@@ -180,7 +177,7 @@ try {
return `$ErrorActionPreference = 'Stop'
$matchingRuleScopes = @()
$blockingRuleDetected = $false
$rules = @(Get-NetFirewallApplicationFilter -PolicyStore ActiveStore -Program ${quotePowerShell(executablePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' })
$rules = @(Get-NetFirewallApplicationFilter -PolicyStore ActiveStore -Program ${quotePowerShellLiteral(executablePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' })
foreach ($rule in $rules) {
$portFilter = $rule | Get-NetFirewallPortFilter
$protocol = [string]$portFilter.Protocol
@@ -215,7 +212,7 @@ function buildRepairScript(port: number, executablePath: string): string {
// Removal deliberately ignores the Block rule's remote-address scope,
// mirroring the fail-closed inspection (the phone address is unknown).
return `$ErrorActionPreference = 'Stop'
$blockingRules = @(Get-NetFirewallApplicationFilter -Program ${quotePowerShell(executablePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Block' })
$blockingRules = @(Get-NetFirewallApplicationFilter -Program ${quotePowerShellLiteral(executablePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' -and $_.Action -eq 'Block' })
foreach ($rule in $blockingRules) {
$portFilter = $rule | Get-NetFirewallPortFilter
$protocol = [string]$portFilter.Protocol
@@ -225,8 +222,8 @@ foreach ($rule in $blockingRules) {
$rule | Remove-NetFirewallRule
}
}
Get-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -ErrorAction SilentlyContinue | Remove-NetFirewallRule
New-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -DisplayName ${quotePowerShell(FIREWALL_RULE_DISPLAY_NAME)} -Description 'Allows Orca Mobile to connect to this Orca desktop on private networks.' -Direction Inbound -Action Allow -Enabled True -Profile Private -Protocol TCP -LocalPort ${port} -Program ${quotePowerShell(executablePath)} -EdgeTraversalPolicy Block | Out-Null`
Get-NetFirewallRule -Name ${quotePowerShellLiteral(FIREWALL_RULE_NAME)} -ErrorAction SilentlyContinue | Remove-NetFirewallRule
New-NetFirewallRule -Name ${quotePowerShellLiteral(FIREWALL_RULE_NAME)} -DisplayName ${quotePowerShellLiteral(FIREWALL_RULE_DISPLAY_NAME)} -Description 'Allows Orca Mobile to connect to this Orca desktop on private networks.' -Direction Inbound -Action Allow -Enabled True -Profile Private -Protocol TCP -LocalPort ${port} -Program ${quotePowerShellLiteral(executablePath)} -EdgeTraversalPolicy Block | Out-Null`
}
// Why the elevated child keeps `-EncodedCommand` while the local runner does not: `Start-Process
@@ -237,7 +234,7 @@ New-NetFirewallRule -Name ${quotePowerShell(FIREWALL_RULE_NAME)} -DisplayName ${
function buildElevationScript(powershellPath: string, encodedRepairScript: string): string {
return `$ErrorActionPreference = 'Stop'
try {
$process = Start-Process -FilePath ${quotePowerShell(powershellPath)} -ArgumentList @('-NoProfile', '-NonInteractive', '-EncodedCommand', '${encodedRepairScript}') -Verb RunAs -Wait -PassThru
$process = Start-Process -FilePath ${quotePowerShellLiteral(powershellPath)} -ArgumentList @('-NoProfile', '-NonInteractive', '-EncodedCommand', '${encodedRepairScript}') -Verb RunAs -Wait -PassThru
[pscustomobject]@{ launched = $true; exitCode = $process.ExitCode } | ConvertTo-Json -Compress
} catch {
[pscustomobject]@{ launched = $false; nativeErrorCode = $_.Exception.NativeErrorCode } | ConvertTo-Json -Compress
@@ -91,6 +91,17 @@ describe('writeFileToClipboard', () => {
expect(args.join(' ')).toContain("Set-Clipboard -LiteralPath '/repo/o''brien.png'")
})
it('doubles typographic single quotes, which PowerShell also treats as delimiters', async () => {
const runCommand = vi.fn(async (_command: string, _args: string[]) => {})
await writeFileToClipboard(
'/repo/o\u2019brien.png',
makeDeps({ platform: 'win32', runCommand })
)
expect(runCommand.mock.calls[0][1].join(' ')).toContain(
"Set-Clipboard -LiteralPath '/repo/o\u2019\u2019brien.png'"
)
})
it('reports a failure (never throws) when PowerShell rejects on Windows', async () => {
const runCommand = vi.fn(async (_command: string, _args: string[]) => {
throw new Error('powershell.exe not found')
+4 -4
View File
@@ -1,5 +1,6 @@
import { isAbsolute } from 'node:path'
import { pathToFileURL } from 'node:url'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
export type ClipboardFileResult = { ok: boolean; reason?: string }
@@ -47,15 +48,14 @@ export async function writeFileToClipboard(
if (deps.platform === 'win32') {
// Set-Clipboard -LiteralPath populates CF_HDROP, which Explorer pastes as a
// file. Single-quote escaping for the PowerShell string literal. Guard the
// spawn so a missing/erroring PowerShell surfaces as a result, not a throw.
const escaped = clipboardPath.replace(/'/g, "''")
// file. Guard the spawn so a missing/erroring PowerShell surfaces as a
// result, not a throw.
try {
await deps.runCommand('powershell.exe', [
'-NoProfile',
'-NonInteractive',
'-Command',
`Set-Clipboard -LiteralPath '${escaped}'`
`Set-Clipboard -LiteralPath ${quotePowerShellLiteral(clipboardPath)}`
])
return { ok: true }
} catch {
@@ -13,6 +13,7 @@ describe('copyable setup commands', () => {
expect(windows('C:\\My Env\\python.exe')).toBe(`& 'C:\\My Env\\python.exe'${pip}`)
expect(windows('C:\\Dev&Test\\python.exe')).toBe(`& 'C:\\Dev&Test\\python.exe'${pip}`)
expect(windows("C:\\Bob's\\python.exe")).toBe(`& 'C:\\Bob''s\\python.exe'${pip}`)
expect(windows('C:\\Bob\u2019s\\python.exe')).toBe(`& 'C:\\Bob\u2019\u2019s\\python.exe'${pip}`)
})
it('creates the venv and installs into it only when creation succeeded', () => {
@@ -1,9 +1,10 @@
import { getRendererAppPlatform } from '@/lib/renderer-app-platform'
import { venvInterpreterSegments } from '../../../../shared/notebook-venv-location'
import { quotePowerShellLiteral } from '../../../../shared/powershell-native-argument'
/** Quotes a path for a shell; single quotes are literal in POSIX shells and PowerShell. */
function shellQuote(path: string, windows: boolean): string {
return windows ? `'${path.replaceAll("'", "''")}'` : `'${path.replaceAll("'", "'\\''")}'`
return windows ? quotePowerShellLiteral(path) : `'${path.replaceAll("'", "'\\''")}'`
}
/** PowerShell runs a quoted program path only through `&`. */
+14
View File
@@ -122,6 +122,20 @@ describe('createSequencedSetupAgentCommands', () => {
)
})
it('doubles typographic single quotes in native Windows gate literals', () => {
const commands = createSequencedSetupAgentCommands({
runnerScriptPath: 'C:\\O\u2019Brien\\.git\\orca\\setup-runner.cmd',
platform: 'windows',
startupCommand: 'codex',
nonce: 'nonce-3'
})
const decoded = Buffer.from(
commands.setupCommand.split('-EncodedCommand ')[1] ?? '',
'base64'
).toString('utf16le')
expect(decoded).toContain("$runner = 'C:\\O\u2019\u2019Brien\\.git\\orca\\setup-runner.cmd'")
})
it('leaves the failure and timeout messages as the only other outcomes', () => {
const script =
createSequencedSetupAgentCommands({
+7 -10
View File
@@ -7,6 +7,7 @@ import {
type SetupRunnerShell
} from './setup-runner-command'
import { createNonSecureContextUuid } from './non-secure-context-uuid'
import { quotePowerShellLiteral } from './powershell-native-argument'
const DEFAULT_WAIT_TIMEOUT_SECONDS = 2 * 60 * 60
// Exported so the gate and its tests share one definition.
@@ -193,10 +194,10 @@ function buildWindowsSetupCommand(
): string {
// Why: delayed expansion keeps path metacharacters as data when cmd invokes the batch runner.
const script = [
`$runner = ${quotePowerShellString(runnerScriptPath)}`,
`$marker = ${quotePowerShellString(markerPath)}`,
`$runner = ${quotePowerShellLiteral(runnerScriptPath)}`,
`$marker = ${quotePowerShellLiteral(markerPath)}`,
'$tmp = $marker + ".tmp"',
`$nonce = ${quotePowerShellString(nonce)}`,
`$nonce = ${quotePowerShellLiteral(nonce)}`,
'Remove-Item -LiteralPath $marker, $tmp -Force -ErrorAction SilentlyContinue',
'$processInfo = [System.Diagnostics.ProcessStartInfo]::new()',
'$processInfo.FileName = $env:ComSpec',
@@ -245,13 +246,13 @@ function buildWindowsStartupCommand(
'"session (" + $_.FullyQualifiedErrorId + "). A startup command that runs a .ps1 " + ' +
'"may be blocked.") }',
'$ProgressPreference = $orcaProgress',
`$marker = ${quotePowerShellString(markerPath)}`,
`$marker = ${quotePowerShellLiteral(markerPath)}`,
'if ([string]::IsNullOrWhiteSpace($marker)) {',
' [Console]::Error.WriteLine("Missing setup marker path.")',
' exit 1',
'}',
'$tmp = $marker + ".tmp"',
`$nonce = ${quotePowerShellString(nonce)}`,
`$nonce = ${quotePowerShellLiteral(nonce)}`,
`$deadline = (Get-Date).AddSeconds(${timeout})`,
'[Console]::Error.WriteLine("Waiting for setup to finish before starting agent...")',
'while ($true) {',
@@ -269,7 +270,7 @@ function buildWindowsStartupCommand(
' [Console]::Error.WriteLine("Missing sequenced startup command.")',
' exit 1',
' }',
` [Console]::Error.WriteLine(${quotePowerShellString(SETUP_COMPLETE_MESSAGE)})`,
` [Console]::Error.WriteLine(${quotePowerShellLiteral(SETUP_COMPLETE_MESSAGE)})`,
' Invoke-Expression $startup',
' if ($global:LASTEXITCODE -ne $null) { exit $global:LASTEXITCODE }',
' if (-not $?) { exit 1 }',
@@ -301,10 +302,6 @@ function quotePosixArg(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
function quotePowerShellString(value: string): string {
return `'${value.replace(/'/g, "''")}'`
}
export function getSetupAgentSequenceShellForTests(
runnerScriptPath: string,
platform: SetupRunnerCommandPlatform
@@ -9,6 +9,7 @@ import {
createSequencedSetupAgentCommands,
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV
} from './setup-agent-sequencing'
import { quotePowerShellLiteral } from './powershell-native-argument'
const TEMP_DIRS: string[] = []
const WINDOWS_PROCESS_TEST_TIMEOUT_MS = 30_000
@@ -49,14 +50,14 @@ describe.skipIf(process.platform !== 'win32')('Windows setup-agent sequencing',
[
'param([string]$Value)',
'$utf8 = [System.Text.UTF8Encoding]::new($false)',
`[System.IO.File]::AppendAllText('${quotePowerShell(logPath)}', $Value + [Environment]::NewLine, $utf8)`
`[System.IO.File]::AppendAllText(${quotePowerShellLiteral(logPath)}, $Value + [Environment]::NewLine, $utf8)`
].join('\r\n'),
'utf8'
)
const commands = createSequencedSetupAgentCommands({
runnerScriptPath,
startupCommand: `& '${quotePowerShell(startupScriptPath)}' '${quotePowerShell(prompt)}'`,
startupCommand: `& ${quotePowerShellLiteral(startupScriptPath)} ${quotePowerShellLiteral(prompt)}`,
platform: 'windows',
nonce: 'windows-sequence',
waitTimeoutSeconds: 2
@@ -167,10 +168,6 @@ function spawnWindowsCommand(
})
}
function quotePowerShell(value: string): string {
return value.replace(/'/g, "''")
}
function waitForExit(
child: ReturnType<typeof spawn>
): Promise<{ code: number | null; stderr: string }> {
@@ -65,6 +65,12 @@ describe('tokenizeStartupCommand spans (windows shells)', () => {
})
})
describe('powershell startup argument quoting', () => {
it('doubles typographic single quotes, which PowerShell also treats as delimiters', () => {
expect(quoteStartupArg('it\u2019s', 'powershell')).toBe("'it\u2019\u2019s'")
})
})
describe('one Unix startup dialect', () => {
it('clears variables with a self-contained branch, not a per-shell builtin', () => {
// Why not `unset`/`set -e` alone, and why not a wrapper-defined helper:
+2 -1
View File
@@ -1,4 +1,5 @@
import { tokenizeCustomCommandTemplate, type CommandTokenSpan } from './commit-message-prompt'
import { quotePowerShellLiteral } from './powershell-native-argument'
/**
* `'posix'` covers every Unix shell Orca can type into, fish included — not
@@ -217,7 +218,7 @@ function quotePortableUnixArg(value: string): string {
export function quoteStartupArg(value: string, shell: AgentStartupShell): string {
if (shell === 'powershell') {
return `'${value.replace(/'/g, "''")}'`
return quotePowerShellLiteral(value)
}
if (shell === 'cmd') {
return `"${value.replace(/([\^&|<>()%!"])/g, '^$1')}"`
@@ -33,4 +33,11 @@ describe('buildWindowsCmdRunnerDelayedLaunchCommand', () => {
expect(script).toContain("$runner = 'C:\\work (x86)\\se&tup.cmd'")
expect(script).toContain('/d /s /v:on /c ""!ORCA_SETUP_RUNNER!""')
})
it('doubles typographic single quotes in the runner path literal', () => {
const script = decodePayload(
buildWindowsCmdRunnerDelayedLaunchCommand('C:\\O\u2019Brien\\setup.cmd')
)
expect(script).toContain("$runner = 'C:\\O\u2019\u2019Brien\\setup.cmd'")
})
})
@@ -1,4 +1,5 @@
import { encodePowerShellCommand } from './powershell-command-encoding'
import { quotePowerShellLiteral } from './powershell-native-argument'
// Why: `cmd.exe /c "<path>"` is typed into the terminal's shell, so the path is parsed twice.
// cmd expands %VAR% even inside quotes (no escape exists on the command line), and PowerShell
@@ -18,7 +19,7 @@ export function windowsRunnerPathNeedsCmdGuard(runnerScriptPath: string): boolea
*/
export function buildWindowsCmdRunnerDelayedLaunchCommand(runnerScriptPath: string): string {
const script = [
`$runner = ${quotePowerShellString(runnerScriptPath)}`,
`$runner = ${quotePowerShellLiteral(runnerScriptPath)}`,
// Why: an empty value would silently degrade to `cmd /c ""`, which exits 0 without running setup.
'if ([string]::IsNullOrEmpty($runner)) { exit 1 }',
'$processInfo = [System.Diagnostics.ProcessStartInfo]::new()',
@@ -39,7 +40,3 @@ export function buildWindowsCmdRunnerDelayedLaunchCommand(runnerScriptPath: stri
// base64 stays: this string is typed into a shell, which is the whole point of the guard above.
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`
}
function quotePowerShellString(value: string): string {
return `'${value.replace(/'/g, "''")}'`
}
+3 -6
View File
@@ -10,6 +10,7 @@ import {
stopOrcaRuntime,
type CliResult
} from './computer-cli-driver'
import { quotePowerShellLiteral } from '../../../src/shared/powershell-native-argument'
const execFileAsync = promisify(execFile)
let textEditTempDir: string | null = null
@@ -185,7 +186,7 @@ export async function ensureNotepadLaunched(): Promise<void> {
'-NoProfile',
'-NonInteractive',
'-Command',
`Start-Process notepad.exe -ArgumentList ${powerShellSingleQuoted(filePath)}`
`Start-Process notepad.exe -ArgumentList ${quotePowerShellLiteral(filePath)}`
])
notepadAppSelector = `pid:${await findNotepadWindowPid(filePath)}`
}
@@ -232,7 +233,7 @@ function delay(ms: number): Promise<void> {
async function findNotepadWindowPid(filePath: string): Promise<number> {
const targetName = filePath.split(/[\\/]/).at(-1) ?? filePath
const script = [
`$targetName = ${powerShellSingleQuoted(targetName)}`,
`$targetName = ${quotePowerShellLiteral(targetName)}`,
'$deadline = (Get-Date).AddSeconds(15)',
'$target = $null',
'while ((Get-Date) -lt $deadline -and $null -eq $target) {',
@@ -260,10 +261,6 @@ async function findNotepadWindowPid(filePath: string): Promise<number> {
return Number.parseInt(result.stdout.trim(), 10)
}
function powerShellSingleQuoted(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
function escapeAppleScript(value: string): string {
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')
}
@@ -5,6 +5,7 @@ import { test, expect } from './helpers/orca-app'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import { waitForActiveTerminalManager } from './helpers/terminal'
import { analyzeRasterCursorCells, type RasterCursorCell } from './terminal-cursor-raster-probe'
import { quotePowerShellLiteral } from '../../src/shared/powershell-native-argument'
type ShellCase = {
label: string
@@ -168,10 +169,6 @@ function isUnexpectedVisibleCursorFrame(frame: ScreenSnapshot): boolean {
return !isInputCursorRow(frame, frame.marker.cellY)
}
function quotePowerShellSingleQuoted(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
async function createShellTab(
page: Page,
shellOverride: ShellCase['shellOverride']
@@ -251,7 +248,7 @@ async function prepareCodexTerminal(
const launchCommand =
shellCase.shellOverride === 'powershell.exe'
? `Set-Location -LiteralPath ${quotePowerShellSingleQuoted(process.cwd())}; ${shellCase.codexCommand}`
? `Set-Location -LiteralPath ${quotePowerShellLiteral(process.cwd())}; ${shellCase.codexCommand}`
: shellCase.codexCommand
await page.keyboard.insertText(launchCommand)
await page.keyboard.press('Enter')
+3 -5
View File
@@ -1,13 +1,11 @@
import { quotePowerShellLiteral } from '../../src/shared/powershell-native-argument'
function quotePosixShellArg(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`
}
function quotePowerShellArg(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
function quoteTerminalArg(value: string): string {
return process.platform === 'win32' ? quotePowerShellArg(value) : quotePosixShellArg(value)
return process.platform === 'win32' ? quotePowerShellLiteral(value) : quotePosixShellArg(value)
}
export function nodeTerminalCommand(args: readonly string[]): string {