refactor(shell): one shell-argument-quoting module for the duplicated quote copies (#30564)

* refactor(shell): one shell-argument-quoting module for the duplicated quote copies

Each former copy now calls the canonical function with its exact spelling; a table test pins
byte-identical output for every copy over hostile inputs. Distinct spellings stay distinct
functions: '\'' vs '"'"', the two plain-word sets, and the cmd.exe doubled quote.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: keep the quoting import at the top of two import-less files

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-18 23:12:27 -07:00
committed by GitHub
co-authored by Claude
parent 59b03cab00
commit 6d8e4cfef2
38 changed files with 348 additions and 235 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
import { quotePosixShellDoubleQuotedApostrophe } from '../shared/shell-argument-quoting'
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime-client'
import {
recoverableOrchestrationArgs,
@@ -208,7 +209,7 @@ function shellQuote(value: string): string {
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
return value
}
return `'${value.replaceAll("'", `'"'"'`)}'`
return quotePosixShellDoubleQuotedApostrophe(value)
}
function stripUnsafeRetryAdvice(message: string, requestId: string): string {
+3 -1
View File
@@ -1,3 +1,5 @@
import { quotePosixShell } from '../shared/shell-argument-quoting'
export function quoteCliCommandArgument(value: string): string {
if (/^[a-zA-Z0-9._:/@-]+$/.test(value)) {
return value
@@ -5,5 +7,5 @@ export function quoteCliCommandArgument(value: string): string {
if (process.platform === 'win32') {
return `"${value.replace(/"/g, '\\"')}"`
}
return `'${value.replaceAll("'", "'\\''")}'`
return quotePosixShell(value)
}
+4 -7
View File
@@ -1,9 +1,6 @@
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from './hook-stdin-contract'
export function quotePosixShellString(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`
}
// Why: guard for a readable executable so a stale entry at a missing script becomes a silent no-op, not an exit-127 failure on every tool call.
export function wrapPosixHookCommand(
scriptPath: string,
@@ -12,15 +9,15 @@ export function wrapPosixHookCommand(
options: { fallbackStdout?: string; requiredEnvVar?: string } = {}
): string {
// Why: single-quote escape so $, `, ", \ in scriptPath stay literal — avoids shell injection from an arbitrary path.
const quoted = quotePosixShellString(scriptPath)
const quoted = quotePosixShell(scriptPath)
const envPrefix = Object.entries(env)
.map(([key, value]) => `${key}='${value.replaceAll("'", "'\\''")}'`)
.map(([key, value]) => `${key}=${quotePosixShell(value)}`)
.join(' ')
const invocation = envPrefix ? `${envPrefix} /bin/sh ${quoted}` : `/bin/sh ${quoted}`
const fallback =
options.fallbackStdout === undefined
? POSIX_HOOK_STDIN_DRAIN_COMMAND
: `printf '%s\\n' ${quotePosixShellString(options.fallbackStdout)}; ${POSIX_HOOK_STDIN_DRAIN_COMMAND}`
: `printf '%s\\n' ${quotePosixShell(options.fallbackStdout)}; ${POSIX_HOOK_STDIN_DRAIN_COMMAND}`
// Why: default form avoids Grok rejecting unset vars or splicing values into shell quotes at load
// time; the child shell checks the current pane env before spawning the managed script.
const guards = [
@@ -2,6 +2,7 @@
// one-line program run by its own executable in Node mode, which prints the link on error output
// instead: the link is captured either way, and no browser opens on the host.
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
import { runProcess } from '@orca/process-host'
export const ANTIGRAVITY_BROWSER_MARKER = '__ORCA_ANTIGRAVITY_AUTH_URL__'
@@ -13,14 +14,15 @@ const HELPER_SOURCE =
const PREFLIGHT_URL = 'https://example.invalid/orca-antigravity-browser-preflight'
// Python splits BROWSER with shlex.
const quote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`
// Node mode is set in the command itself: the provider supervisor strips ELECTRON_RUN_AS_NODE from
// the server, which keeps it out of the agent's own tools.
const executable = process.execPath.replaceAll('\\', '\\\\').replaceAll(':', '\\072')
const shell = `exec /usr/bin/env ELECTRON_RUN_AS_NODE=1 "$(printf %b ${quote(executable)})" -e ${quote(HELPER_SOURCE)} -- "$1"`
const shell = `exec /usr/bin/env ELECTRON_RUN_AS_NODE=1 "$(printf %b ${quotePosixShellDoubleQuotedApostrophe(executable)})" -e ${quotePosixShellDoubleQuotedApostrophe(HELPER_SOURCE)} -- "$1"`
const BROWSER_ARGV = ['/bin/sh', '-c', shell, '--', '%s']
export const ANTIGRAVITY_BROWSER_COMMAND = BROWSER_ARGV.map(quote).join(' ')
export const ANTIGRAVITY_BROWSER_COMMAND = BROWSER_ARGV.map(
quotePosixShellDoubleQuotedApostrophe
).join(' ')
const usable = !/[\r\n\0]|%s/.test(process.execPath)
/** The exact template the server's Python browser controller executes. */
+2 -2
View File
@@ -27,7 +27,7 @@ import {
ANTIGRAVITY_PRE_TOOL_USE_DECISION,
type AntigravityEvent
} from './hook-events'
import { quotePosixShellString } from '../agent-hooks/posix-hook-command'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { getManagedScript, getWindowsWrapperScript } from './hook-script'
import {
buildInstalledConfig,
@@ -64,7 +64,7 @@ function getPosixManagedCommand(scriptPath: string, event: AntigravityEvent): st
event.eventName === 'PreToolUse' ? { fallbackStdout: ANTIGRAVITY_PRE_TOOL_USE_DECISION } : {}
)
// ACP hosts tokenize the command into argv; the shell must be the executable, not `if`.
return `/bin/sh -c ${quotePosixShellString(command)}`
return `/bin/sh -c ${quotePosixShell(command)}`
}
function getManagedCommand(scriptPath: string, event: AntigravityEvent): string {
+2 -2
View File
@@ -16,7 +16,7 @@ import {
} from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { LINUX_CLI_COMMAND_NAME } from './bundled-cli-launcher-path'
import { quoteShell } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
const LAUNCHER_DIRECTORY_NAME = 'launcher'
const LIVE_ENDPOINT_NAME = 'live'
@@ -226,7 +226,7 @@ function buildStableLauncherScript(cacheRootPath: string): string {
return `#!/usr/bin/env bash
${LAUNCHER_MARKER}
shopt -s execfail
launcher_dir=${quoteShell(launcherDirectory)}
launcher_dir=${quotePosixShellDoubleQuotedApostrophe(launcherDirectory)}
deadline=$((SECONDS + ${LAUNCHER_WAIT_SECONDS}))
while (( SECONDS <= deadline )); do
launcher="$launcher_dir/${INSTALLED_ENDPOINT_NAME}"
@@ -4,7 +4,7 @@ import { basename, dirname, join } from 'node:path'
import type { CliInstallStatus } from '../../shared/cli-install-types'
import { isPermissionError } from '../win32-utils'
import { isMissingError } from './cli-install-errors'
import { quoteShell } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
export type EntryIdentity = {
dev: bigint
@@ -171,10 +171,12 @@ export function buildMacPrivilegedSymlinkReadableCommand(args: {
commandPath: string
expected: EntryIdentity
}): string {
const commandPath = quoteShell(args.commandPath)
const changed = quoteShell(`The command at ${args.commandPath} changed while Orca inspected it.`)
const commandPath = quotePosixShellDoubleQuotedApostrophe(args.commandPath)
const changed = quotePosixShellDoubleQuotedApostrophe(
`The command at ${args.commandPath} changed while Orca inspected it.`
)
return (
`if [ -L ${commandPath} ] && [ "$(/usr/bin/stat -f '%d:%i' ${commandPath})" = ${quoteShell(`${args.expected.dev}:${args.expected.ino}`)} ]; ` +
`if [ -L ${commandPath} ] && [ "$(/usr/bin/stat -f '%d:%i' ${commandPath})" = ${quotePosixShellDoubleQuotedApostrophe(`${args.expected.dev}:${args.expected.ino}`)} ]; ` +
`then /bin/chmod -h 755 ${commandPath}; else echo ${changed} >&2; exit 73; fi`
)
}
@@ -194,43 +196,45 @@ export function buildMacPrivilegedSymlinkTransaction(
const heldPath = join(transactionDirectory, basename(args.commandPath))
const publishDirectory = join(transactionDirectory, 'publish')
const publishPath = join(publishDirectory, basename(args.commandPath))
const recoveryMessage = quoteShell(`The displaced entry is preserved at ${heldPath}.`)
const recoveryMessage = quotePosixShellDoubleQuotedApostrophe(
`The displaced entry is preserved at ${heldPath}.`
)
const restore =
`/bin/ln -P ${quoteShell(heldPath)} ${quoteShell(commandDirectory)} && ` +
`/bin/rm ${quoteShell(heldPath)} && /bin/rmdir ${quoteShell(transactionDirectory)}`
`/bin/ln -P ${quotePosixShellDoubleQuotedApostrophe(heldPath)} ${quotePosixShellDoubleQuotedApostrophe(commandDirectory)} && ` +
`/bin/rm ${quotePosixShellDoubleQuotedApostrophe(heldPath)} && /bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(transactionDirectory)}`
const restoreOrPreserve = `if ${restore}; then :; else echo ${recoveryMessage} >&2; exit 74; fi`
const { fileSha256, rawSymlinkTarget, snapshot } = args.inspected
const fileMismatch = fileSha256
? ` || [ "$(/usr/bin/shasum -a 256 ${quoteShell(heldPath)} | /usr/bin/awk '{print $1}')" != ${quoteShell(fileSha256)} ]`
? ` || [ "$(/usr/bin/shasum -a 256 ${quotePosixShellDoubleQuotedApostrophe(heldPath)} | /usr/bin/awk '{print $1}')" != ${quotePosixShellDoubleQuotedApostrophe(fileSha256)} ]`
: ''
const symlinkMismatch = rawSymlinkTarget
? ` || [ "$(/usr/bin/readlink -n ${quoteShell(heldPath)}; /usr/bin/printf x)" != ${quoteShell(`${rawSymlinkTarget}x`)} ]`
? ` || [ "$(/usr/bin/readlink -n ${quotePosixShellDoubleQuotedApostrophe(heldPath)}; /usr/bin/printf x)" != ${quotePosixShellDoubleQuotedApostrophe(`${rawSymlinkTarget}x`)} ]`
: ''
const rejectCaptured = snapshot
? `if [ "$captured" -eq 1 ] && { [ "$(/usr/bin/stat -f '%d:%i' ${quoteShell(heldPath)})" != ${quoteShell(`${snapshot.identity.dev}:${snapshot.identity.ino}`)} ]${fileMismatch}${symlinkMismatch}; }; then ${restoreOrPreserve}; exit 73; fi`
? `if [ "$captured" -eq 1 ] && { [ "$(/usr/bin/stat -f '%d:%i' ${quotePosixShellDoubleQuotedApostrophe(heldPath)})" != ${quotePosixShellDoubleQuotedApostrophe(`${snapshot.identity.dev}:${snapshot.identity.ino}`)} ]${fileMismatch}${symlinkMismatch}; }; then ${restoreOrPreserve}; exit 73; fi`
: `if [ "$captured" -eq 1 ]; then ${restoreOrPreserve}; exit 73; fi`
// Why: macOS applies the umask to symlinks too, so the script publishes under 022 and only the
// transaction directories are created private.
const capture =
`umask 022; /bin/mkdir -p ${quoteShell(commandDirectory)} || exit $?; ` +
`/bin/mkdir -m 700 ${quoteShell(transactionDirectory)} || exit $?; captured=0; ` +
`if [ -e ${quoteShell(args.commandPath)} ] || [ -L ${quoteShell(args.commandPath)} ]; then ` +
`/bin/mv ${quoteShell(args.commandPath)} ${quoteShell(heldPath)} && captured=1 || exit $?; fi; ` +
`umask 022; /bin/mkdir -p ${quotePosixShellDoubleQuotedApostrophe(commandDirectory)} || exit $?; ` +
`/bin/mkdir -m 700 ${quotePosixShellDoubleQuotedApostrophe(transactionDirectory)} || exit $?; captured=0; ` +
`if [ -e ${quotePosixShellDoubleQuotedApostrophe(args.commandPath)} ] || [ -L ${quotePosixShellDoubleQuotedApostrophe(args.commandPath)} ]; then ` +
`/bin/mv ${quotePosixShellDoubleQuotedApostrophe(args.commandPath)} ${quotePosixShellDoubleQuotedApostrophe(heldPath)} && captured=1 || exit $?; fi; ` +
`${rejectCaptured}; `
if (args.action === 'remove') {
return `${capture}if [ "$captured" -eq 1 ]; then /bin/rm ${quoteShell(heldPath)}; fi; /bin/rmdir ${quoteShell(transactionDirectory)}`
return `${capture}if [ "$captured" -eq 1 ]; then /bin/rm ${quotePosixShellDoubleQuotedApostrophe(heldPath)}; fi; /bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(transactionDirectory)}`
}
const rollback =
`/bin/rm -f ${quoteShell(publishPath)}; /bin/rmdir ${quoteShell(publishDirectory)} 2>/dev/null || :; ` +
`if [ "$captured" -eq 1 ]; then ${restoreOrPreserve}; else /bin/rmdir ${quoteShell(transactionDirectory)}; fi; exit 73`
`/bin/rm -f ${quotePosixShellDoubleQuotedApostrophe(publishPath)}; /bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(publishDirectory)} 2>/dev/null || :; ` +
`if [ "$captured" -eq 1 ]; then ${restoreOrPreserve}; else /bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(transactionDirectory)}; fi; exit 73`
return (
`${capture}if /bin/mkdir -m 700 ${quoteShell(publishDirectory)} && ` +
`/bin/ln -s ${quoteShell(args.launcherPath)} ${quoteShell(publishPath)} && ` +
`/bin/ln -P ${quoteShell(publishPath)} ${quoteShell(commandDirectory)}; then ` +
`/bin/rm ${quoteShell(publishPath)}; /bin/rmdir ${quoteShell(publishDirectory)}; ` +
`if [ "$captured" -eq 1 ]; then /bin/rm ${quoteShell(heldPath)}; fi; ` +
`/bin/rmdir ${quoteShell(transactionDirectory)}; else ${rollback}; fi`
`${capture}if /bin/mkdir -m 700 ${quotePosixShellDoubleQuotedApostrophe(publishDirectory)} && ` +
`/bin/ln -s ${quotePosixShellDoubleQuotedApostrophe(args.launcherPath)} ${quotePosixShellDoubleQuotedApostrophe(publishPath)} && ` +
`/bin/ln -P ${quotePosixShellDoubleQuotedApostrophe(publishPath)} ${quotePosixShellDoubleQuotedApostrophe(commandDirectory)}; then ` +
`/bin/rm ${quotePosixShellDoubleQuotedApostrophe(publishPath)}; /bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(publishDirectory)}; ` +
`if [ "$captured" -eq 1 ]; then /bin/rm ${quotePosixShellDoubleQuotedApostrophe(heldPath)}; fi; ` +
`/bin/rmdir ${quotePosixShellDoubleQuotedApostrophe(transactionDirectory)}; else ${rollback}; fi`
)
}
+7 -10
View File
@@ -2,11 +2,8 @@ import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join } from 'node:path'
import { DEV_COMMAND_NAME, DEV_LAUNCHER_DIR } from './cli-install-constants'
import {
escapeWindowsBatchValue,
isAbsoluteForPlatform,
quoteShell
} from './cli-install-path-format'
import { escapeWindowsBatchValue, isAbsoluteForPlatform } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
export async function ensureDevLauncher(args: {
platform: NodeJS.Platform
@@ -58,16 +55,16 @@ export function buildUnixCliLauncher(
if (runtime === 'node') {
return `#!/usr/bin/env sh
set -eu
export ORCA_USER_DATA_PATH=${quoteShell(userDataPath)}
export ORCA_USER_DATA_PATH=${quotePosixShellDoubleQuotedApostrophe(userDataPath)}
unset NODE_OPTIONS NODE_REPL_EXTERNAL_MODULE ELECTRON_RUN_AS_NODE
exec ${quoteShell(execPathValue)} ${quoteShell(cliEntryPath)} "$@"
exec ${quotePosixShellDoubleQuotedApostrophe(execPathValue)} ${quotePosixShellDoubleQuotedApostrophe(cliEntryPath)} "$@"
`
}
return `#!/usr/bin/env bash
set -euo pipefail
ELECTRON=${quoteShell(execPathValue)}
CLI=${quoteShell(cliEntryPath)}
export ORCA_USER_DATA_PATH=${quoteShell(userDataPath)}
ELECTRON=${quotePosixShellDoubleQuotedApostrophe(execPathValue)}
CLI=${quotePosixShellDoubleQuotedApostrophe(cliEntryPath)}
export ORCA_USER_DATA_PATH=${quotePosixShellDoubleQuotedApostrophe(userDataPath)}
if [ -z "\${ORCA_APP_EXECUTABLE:-}" ]; then
export ORCA_APP_EXECUTABLE="$ELECTRON"
export ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT=1
-4
View File
@@ -73,10 +73,6 @@ export function escapeWindowsBatchValue(value: string): string {
return value.replaceAll('"', '""')
}
export function quoteShell(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`
}
export function isAbsoluteForPlatform(platform: NodeJS.Platform, value: string): boolean {
if (platform === 'win32') {
return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\')
+4 -4
View File
@@ -1,4 +1,4 @@
import { quoteShell } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
const APPIMAGE_CLI_SCRIPT = [
'(async()=>{',
@@ -25,7 +25,7 @@ export function extractLegacyAppImageCliWrapperTarget(content: string): string |
export function buildLegacyAppImageCliWrapper(appImagePath: string): string {
return `#!/usr/bin/env bash
set -euo pipefail
APPIMAGE=${quoteShell(appImagePath)}
APPIMAGE=${quotePosixShellDoubleQuotedApostrophe(appImagePath)}
if [ ! -f "$APPIMAGE" ]; then
echo "Orca AppImage not found at $APPIMAGE" >&2
echo "If you moved the AppImage, re-run CLI registration from Orca Settings." >&2
@@ -36,7 +36,7 @@ export ORCA_NODE_REPL_EXTERNAL_MODULE="\${NODE_REPL_EXTERNAL_MODULE-}"
unset NODE_OPTIONS
unset NODE_REPL_EXTERNAL_MODULE
# Why: AppImage mount paths change on each launch; $APPDIR is the runtime mount.
ELECTRON_RUN_AS_NODE=1 exec "$APPIMAGE" -e ${quoteShell(APPIMAGE_CLI_SCRIPT)} -- "$@"
ELECTRON_RUN_AS_NODE=1 exec "$APPIMAGE" -e ${quotePosixShellDoubleQuotedApostrophe(APPIMAGE_CLI_SCRIPT)} -- "$@"
`
}
@@ -45,5 +45,5 @@ function unquoteShell(value: string): string | null {
return null
}
const decoded = value.slice(1, -1).split(`'"'"'`).join("'")
return quoteShell(decoded) === value ? decoded : null
return quotePosixShellDoubleQuotedApostrophe(decoded) === value ? decoded : null
}
+2 -2
View File
@@ -16,7 +16,7 @@ import { pruneAppImageExtractedRoots } from './appimage-extraction-pruning'
import { resolveGenerationStableLauncherPath } from './appimage-generation-stable-launcher'
import { withAppImageRegistrationLock } from './appimage-registration-lock'
import { getBundledLauncherPath } from './bundled-cli-launcher-path'
import { quoteShell } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
// Why: marks a dispatcher this function wrote so repeat serve starts overwrite
// our own file idempotently but never clobber a user's own ~/.local/bin/orca.
@@ -80,7 +80,7 @@ export async function installLinuxBareOrcaDispatcher(
/** Bare-`orca` script that execs the one Linux CLI launcher. */
export function buildBareOrcaCliScript(launcherPath: string): string {
return `#!/usr/bin/env bash\nexec ${quoteShell(launcherPath)} "$@"\n`
return `#!/usr/bin/env bash\nexec ${quotePosixShellDoubleQuotedApostrophe(launcherPath)} "$@"\n`
}
/**
+7 -7
View File
@@ -22,7 +22,7 @@ import {
} from './appimage-stable-launcher'
import { getBundledLauncherPath } from './bundled-cli-launcher-path'
import { buildBareOrcaCliScript } from './linux-bare-orca-dispatcher'
import { quoteShell } from './cli-install-path-format'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
const SHIM_DIR_NAME = 'linux-orca-cli-shim'
@@ -175,12 +175,12 @@ function buildAppImageLiveLauncherScript(
launcherPath: string,
fence: AppImageRuntimeFence
): string {
const runtimePid = quoteShell(String(fence.pid))
const runtimeStartTime = quoteShell(fence.startTime)
const runtimeRoot = quoteShell(fence.runtimeRoot)
const expectedRuntimeIdentity = quoteShell(fence.runtimeIdentity)
const expectedLauncherIdentity = quoteShell(fence.launcherIdentity)
const quotedLauncherPath = quoteShell(launcherPath)
const runtimePid = quotePosixShellDoubleQuotedApostrophe(String(fence.pid))
const runtimeStartTime = quotePosixShellDoubleQuotedApostrophe(fence.startTime)
const runtimeRoot = quotePosixShellDoubleQuotedApostrophe(fence.runtimeRoot)
const expectedRuntimeIdentity = quotePosixShellDoubleQuotedApostrophe(fence.runtimeIdentity)
const expectedLauncherIdentity = quotePosixShellDoubleQuotedApostrophe(fence.launcherIdentity)
const quotedLauncherPath = quotePosixShellDoubleQuotedApostrophe(launcherPath)
return `#!/usr/bin/env bash
runtime_pid=${runtimePid}
runtime_start_time=${runtimeStartTime}
+6 -3
View File
@@ -11,9 +11,9 @@ import {
getPosixDirname,
getWslBridgeMarker,
getWslLauncherMarker,
parseManagedLauncherTarget,
quoteShell
parseManagedLauncherTarget
} from './wsl-cli-scripts'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
import { buildWslCliInstallCommand } from './wsl-cli-registration-command'
import {
buildWslCliStatus,
@@ -242,7 +242,10 @@ export class WslCliInstaller {
// startup reconciliation as opt-in proof, silently undoing this removal.
await this.run(
this.distro as string,
['set -eu', buildManagedLegacyRemoveCommand(quoteShell(legacyCommandPath))].join('\n')
[
'set -eu',
buildManagedLegacyRemoveCommand(quotePosixShellDoubleQuotedApostrophe(legacyCommandPath))
].join('\n')
)
return status
}
+12 -11
View File
@@ -8,9 +8,9 @@ import {
getBridgePathFromCommandPath,
getPosixDirname,
getWslBridgeMarker,
getWslLauncherMarker,
quoteShell
getWslLauncherMarker
} from './wsl-cli-scripts'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
const MANAGED_MARKER = getWslLauncherMarker()
const BRIDGE_MANAGED_MARKER = getWslBridgeMarker()
@@ -25,12 +25,13 @@ export function buildWslCliInstallCommand(
// Why -eu not -euo pipefail: transported via runWslProcess's `sh -s`,
// and no pipe here needs pipefail -- dash on Ubuntu 20.04 lacks the option.
'set -eu',
`mkdir -p ${quoteShell(status.pathDirectory as string)}`,
`mkdir -p ${quoteShell(getPosixDirname(bridgePath))}`,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: WSL status sets pathDirectory to dirname(commandPath) whenever commandPath is set, and the caller requires commandPath.
`mkdir -p ${quotePosixShellDoubleQuotedApostrophe(status.pathDirectory as string)}`,
`mkdir -p ${quotePosixShellDoubleQuotedApostrophe(getPosixDirname(bridgePath))}`,
buildRegistrationLockPrelude(status.commandPath),
`command_tmp=${quoteShell(`${status.commandPath}.tmp`)}.$$`,
`bridge_path=${quoteShell(bridgePath)}`,
`legacy_command_path=${quoteShell(legacyCommandPath)}`,
`command_tmp=${quotePosixShellDoubleQuotedApostrophe(`${status.commandPath}.tmp`)}.$$`,
`bridge_path=${quotePosixShellDoubleQuotedApostrophe(bridgePath)}`,
`legacy_command_path=${quotePosixShellDoubleQuotedApostrophe(legacyCommandPath)}`,
'bridge_tmp="${bridge_path}.tmp.$$"',
'bridge_backup="${bridge_tmp}.backup"',
'bridge_had_original=0',
@@ -40,7 +41,7 @@ export function buildWslCliInstallCommand(
' result=$?',
' set +e',
' if [ "$committed" -ne 1 ]; then',
` if [ "$bridge_had_original" -eq 1 ]; then mv -f "$bridge_backup" ${quoteShell(bridgePath)}; elif [ "$bridge_touched" -eq 1 ]; then rm -f ${quoteShell(bridgePath)}; fi`,
` if [ "$bridge_had_original" -eq 1 ]; then mv -f "$bridge_backup" ${quotePosixShellDoubleQuotedApostrophe(bridgePath)}; elif [ "$bridge_touched" -eq 1 ]; then rm -f ${quotePosixShellDoubleQuotedApostrophe(bridgePath)}; fi`,
' fi',
' rm -f "$command_tmp" "$bridge_tmp" "$bridge_backup"',
' exit "$result"',
@@ -58,10 +59,10 @@ export function buildWslCliInstallCommand(
'chmod 644 "$bridge_tmp"',
buildSafeReplaceGuard(status.commandPath, MANAGED_MARKER),
buildSafeReplaceGuard(bridgePath, BRIDGE_MANAGED_MARKER),
`if [ -f ${quoteShell(bridgePath)} ]; then cp -p ${quoteShell(bridgePath)} "$bridge_backup"; bridge_had_original=1; fi`,
`mv -f "$bridge_tmp" ${quoteShell(bridgePath)}`,
`if [ -f ${quotePosixShellDoubleQuotedApostrophe(bridgePath)} ]; then cp -p ${quotePosixShellDoubleQuotedApostrophe(bridgePath)} "$bridge_backup"; bridge_had_original=1; fi`,
`mv -f "$bridge_tmp" ${quotePosixShellDoubleQuotedApostrophe(bridgePath)}`,
'bridge_touched=1',
`mv -f "$command_tmp" ${quoteShell(status.commandPath)}`,
`mv -f "$command_tmp" ${quotePosixShellDoubleQuotedApostrophe(status.commandPath)}`,
'committed=1',
'rm -f "$bridge_backup"',
buildManagedLegacyRemoveCommand('"$legacy_command_path"'),
+17 -14
View File
@@ -1,3 +1,4 @@
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
import { quotePowerShellLiteral } from '../../shared/powershell-native-argument'
const MANAGED_MARKER = '# Orca managed WSL CLI launcher'
@@ -16,7 +17,11 @@ export function buildWslLauncher(
windowsLauncherPath: string,
bridgePath = '${XDG_DATA_HOME:-$HOME/.local/share}/orca/orca-wsl-bridge.ps1'
): string {
return buildLauncher(windowsLauncherPath, quoteShell(bridgePath), FIND_INTEROP_POWERSHELL)
return buildLauncher(
windowsLauncherPath,
quotePosixShellDoubleQuotedApostrophe(bridgePath),
FIND_INTEROP_POWERSHELL
)
}
/** Launcher that finds its bridge beside itself and PowerShell by Windows path, independent of guest PATH. */
@@ -27,7 +32,7 @@ export function buildColocatedWslLauncher(
return buildLauncher(
windowsLauncherPath,
'"$(dirname -- "$0")/orca-wsl-bridge.ps1"',
`ORCA_POWERSHELL=$(wslpath -u ${quoteShell(windowsPowerShellPath)})
`ORCA_POWERSHELL=$(wslpath -u ${quotePosixShellDoubleQuotedApostrophe(windowsPowerShellPath)})
if [ ! -x "$ORCA_POWERSHELL" ]; then
echo "Orca WSL CLI requires Windows interop and access to $ORCA_POWERSHELL." >&2
exit 1
@@ -45,7 +50,7 @@ function buildLauncher(
set -euo pipefail
${MANAGED_MARKER}
# ORCA_WIN_LAUNCHER_B64=${encodedTarget}
ORCA_WIN_LAUNCHER=${quoteShell(windowsLauncherPath)}
ORCA_WIN_LAUNCHER=${quotePosixShellDoubleQuotedApostrophe(windowsLauncherPath)}
ORCA_BRIDGE_PS1=${bridgePathExpression}
${resolvePowerShell}
# Why: a shell can outlive a deleted worktree; keep explicit CLI selectors and
@@ -200,8 +205,8 @@ export function getBridgePathFromCommandPath(commandPath: string): string {
}
export function buildSafeReplaceGuard(path: string, managedMarker: string): string {
const quotedPath = quoteShell(path)
const quotedMarker = quoteShell(managedMarker)
const quotedPath = quotePosixShellDoubleQuotedApostrophe(path)
const quotedMarker = quotePosixShellDoubleQuotedApostrophe(managedMarker)
return [
`if [ -L ${quotedPath} ]; then`,
' echo "__ORCA_CONFLICT__"',
@@ -218,8 +223,8 @@ export function buildRegistrationLockPrelude(commandPath: string): string {
// Why: the per-distro queue only serializes one Orca process; flock covers
// a second install (e.g. stable + nightly) mutating the same distro files.
return [
`if command -v flock >/dev/null 2>&1 && mkdir -p ${quoteShell(lockDir)} 2>/dev/null; then`,
` exec 9>${quoteShell(`${lockDir}/.orca-wsl-cli.lock`)}`,
`if command -v flock >/dev/null 2>&1 && mkdir -p ${quotePosixShellDoubleQuotedApostrophe(lockDir)} 2>/dev/null; then`,
` exec 9>${quotePosixShellDoubleQuotedApostrophe(`${lockDir}/.orca-wsl-cli.lock`)}`,
' flock -x -w 30 9',
'fi'
].join('\n')
@@ -228,7 +233,7 @@ export function buildRegistrationLockPrelude(commandPath: string): string {
export function buildManagedLegacyRemoveCommand(quotedLegacyCommandPath: string): string {
// Why: remove only the Orca-managed pre-rename wrapper; user-owned `orca`
// commands and symlinks must survive.
return `if [ ! -L ${quotedLegacyCommandPath} ] && [ -f ${quotedLegacyCommandPath} ] && grep -Fq ${quoteShell(MANAGED_MARKER)} ${quotedLegacyCommandPath}; then rm -f ${quotedLegacyCommandPath}; fi`
return `if [ ! -L ${quotedLegacyCommandPath} ] && [ -f ${quotedLegacyCommandPath} ] && grep -Fq ${quotePosixShellDoubleQuotedApostrophe(MANAGED_MARKER)} ${quotedLegacyCommandPath}; then rm -f ${quotedLegacyCommandPath}; fi`
}
export function buildSafeRemoveCommand(commandPath: string, legacyCommandPath?: string): string {
@@ -240,10 +245,12 @@ export function buildSafeRemoveCommand(commandPath: string, legacyCommandPath?:
buildRegistrationLockPrelude(commandPath),
buildSafeReplaceGuard(commandPath, MANAGED_MARKER),
buildSafeReplaceGuard(bridgePath, BRIDGE_MANAGED_MARKER),
`rm -f ${quoteShell(commandPath)} ${quoteShell(bridgePath)}`,
`rm -f ${quotePosixShellDoubleQuotedApostrophe(commandPath)} ${quotePosixShellDoubleQuotedApostrophe(bridgePath)}`,
// Why: leaving a managed legacy `orca` behind lets startup reconciliation
// re-adopt it as opt-in proof and silently undo this removal.
...(legacyCommandPath ? [buildManagedLegacyRemoveCommand(quoteShell(legacyCommandPath))] : [])
...(legacyCommandPath
? [buildManagedLegacyRemoveCommand(quotePosixShellDoubleQuotedApostrophe(legacyCommandPath))]
: [])
].join('\n')
}
@@ -272,7 +279,3 @@ export function getWslLauncherMarker(): string {
export function getWslBridgeMarker(): string {
return BRIDGE_MANAGED_MARKER
}
export function quoteShell(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`
}
+7 -6
View File
@@ -1,5 +1,6 @@
import type { CliInstallStatus } from '../../shared/cli-install-types'
import { getBridgePathFromCommandPath, getPosixDirname, quoteShell } from './wsl-cli-scripts'
import { getBridgePathFromCommandPath, getPosixDirname } from './wsl-cli-scripts'
import { quotePosixShellDoubleQuotedApostrophe } from '../../shared/shell-argument-quoting'
const WSL_COMMAND_NAME = 'orca-ide'
@@ -47,14 +48,14 @@ export async function readWslCliCommandFile(
const output = await run(
distro,
[
`if [ -L ${quoteShell(commandPath)} ]; then`,
`if [ -L ${quotePosixShellDoubleQuotedApostrophe(commandPath)} ]; then`,
' printf __ORCA_NOT_FILE__',
`elif [ ! -e ${quoteShell(commandPath)} ]; then`,
`elif [ ! -e ${quotePosixShellDoubleQuotedApostrophe(commandPath)} ]; then`,
' printf __ORCA_MISSING__',
`elif [ ! -f ${quoteShell(commandPath)} ]; then`,
`elif [ ! -f ${quotePosixShellDoubleQuotedApostrophe(commandPath)} ]; then`,
' printf __ORCA_NOT_FILE__',
'else',
` cat ${quoteShell(commandPath)}`,
` cat ${quotePosixShellDoubleQuotedApostrophe(commandPath)}`,
'fi'
].join('\n'),
{ loginPath: 'none' }
@@ -208,7 +209,7 @@ async function readPathConfigured(
try {
const output = await run(
distro,
`case ":$PATH:" in *:${quoteShell(pathDirectory)}:*) printf yes ;; *) printf no ;; esac`,
`case ":$PATH:" in *:${quotePosixShellDoubleQuotedApostrophe(pathDirectory)}:*) printf yes ;; *) printf no ;; esac`,
{ loginPath: 'preferred' }
)
return output.trim() === 'yes'
+2 -1
View File
@@ -1,3 +1,4 @@
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { join } from 'node:path'
import {
buildManagedCommandHook,
@@ -138,7 +139,7 @@ export function getCodexManagedHookInstallMaterial(): CodexManagedHookInstallMat
}
export function wrapReadablePosixHookCommand(scriptPath: string): string {
const quoted = `'${scriptPath.replaceAll("'", "'\\''")}'`
const quoted = quotePosixShell(scriptPath)
// Why: WSL hooks are written from Windows over UNC where the exec bit is unreliable; a missing script must still own stdin.
return `if [ -f ${quoted} ] && [ -r ${quoted} ]; then /bin/sh ${quoted}; else ${POSIX_HOOK_STDIN_DRAIN_COMMAND}; fi`
}
+2 -8
View File
@@ -1,3 +1,4 @@
import { quotePosixShellUnlessPlain } from '../shared/shell-argument-quoting'
import { spawn, type ChildProcess } from 'node:child_process'
import { spawnProcess } from '@orca/process-host'
import { existsSync } from 'node:fs'
@@ -43,13 +44,6 @@ export type ExternalEditorLaunchSpec =
| ExternalEditorExecutableLaunchSpec
| ExternalEditorShellLaunchSpec
function escapePosixPathForShell(pathValue: string): string {
if (/^[a-zA-Z0-9_./@:-]+$/.test(pathValue)) {
return pathValue
}
return `'${pathValue.replace(/'/g, "'\\''")}'`
}
function escapeWindowsPathForShell(pathValue: string): string {
return /^[a-zA-Z0-9_./@:\\-]+$/.test(pathValue) ? pathValue : `"${pathValue}"`
}
@@ -57,7 +51,7 @@ function escapeWindowsPathForShell(pathValue: string): string {
function escapePathForShell(pathValue: string, platform: NodeJS.Platform): string {
return platform === 'win32'
? escapeWindowsPathForShell(pathValue)
: escapePosixPathForShell(pathValue)
: quotePosixShellUnlessPlain(pathValue)
}
function isWindowsExecutablePath(command: string): boolean {
+2 -5
View File
@@ -1,3 +1,4 @@
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { execFile } from 'node:child_process'
import path from 'node:path'
import { homedir } from 'node:os'
@@ -20,10 +21,6 @@ const WSL_COMMAND_PATH_SENTINEL = '__ORCA_PREFLIGHT_COMMAND_PATH__'
export type PreflightCommandResult = { stdout: string; stderr: string }
export function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
async function withPreflightTimeout<T>(
command: string,
commandPromise: Promise<T>,
@@ -226,7 +223,7 @@ export async function isCommandAvailable(
return (await findRunnableLocalCommand(command)).status === 'available'
}
try {
await execCommandInWslOrThrow(wslTarget, `${shellQuote(command)} --version`)
await execCommandInWslOrThrow(wslTarget, `${quotePosixShell(command)} --version`)
return true
} catch {
return false
@@ -9,9 +9,9 @@ import { execFileCaptureToTermination } from '../git/command-runner/exec-file-ca
import { beginLocalCommandSelection, resolveSelectedLocalCommand } from './command-path-resolver'
import {
execLocalPreflightCommandOrThrow,
findRunnableLocalCommand,
shellQuote
findRunnableLocalCommand
} from './preflight-command-exec'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
vi.mock('../../shared/system-cli-install-dirs', () => ({
getSystemCliInstallDirectories: (_platform: NodeJS.Platform, home: string) => [
@@ -32,7 +32,7 @@ async function fixtureCli(
const binary = path.join(dir, cli)
await writeFile(
binary,
`#!/bin/sh\nprintf '%s\\n' "$*" >> ${shellQuote(`${binary}-calls`)}\n${body}`,
`#!/bin/sh\nprintf '%s\\n' "$*" >> ${quotePosixShell(`${binary}-calls`)}\n${body}`,
{ mode: 0o755 }
)
return binary
@@ -7,9 +7,9 @@ import {
execLocalPreflightCommandOrThrow,
findRunnableLocalCommand,
isCommandAvailable,
isCommandOnPath,
shellQuote
isCommandOnPath
} from './preflight-command-exec'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
const COMMAND = 'orca-22975-gh'
const BROKEN_SHIM = 'echo broken-shim >&2\nexit 126\n'
@@ -20,7 +20,7 @@ async function cliInDirectory(label: string, body = 'echo gh-version-fixture\n')
await mkdir(dir, { recursive: true })
await writeFile(
path.join(dir, COMMAND),
`#!/bin/sh\nprintf '%s\\n' "$@" >> ${shellQuote(path.join(dir, 'argv.txt'))}\n${body}`,
`#!/bin/sh\nprintf '%s\\n' "$@" >> ${quotePosixShell(path.join(dir, 'argv.txt'))}\n${body}`,
{ mode: 0o755 }
)
return dir
@@ -188,8 +188,11 @@ describe.skipIf(process.platform === 'win32')(
it('recovers a working copy after a real launcher timeout', async () => {
const marker = path.join(root, 'descendant-terminated')
const childScript = `trap ${shellQuote(`printf terminated > ${shellQuote(marker)}; exit 0`)} TERM\n/bin/sleep 30 &\nwait\n`
const hung = await cliInDirectory('hung', `/bin/sh -c ${shellQuote(childScript)} &\nwait\n`)
const childScript = `trap ${quotePosixShell(`printf terminated > ${quotePosixShell(marker)}; exit 0`)} TERM\n/bin/sleep 30 &\nwait\n`
const hung = await cliInDirectory(
'hung',
`/bin/sh -c ${quotePosixShell(childScript)} &\nwait\n`
)
const good = await cliInDirectory('good')
vi.stubEnv('PATH', [hung, good].join(path.delimiter))
@@ -1,3 +1,4 @@
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import path from 'node:path'
import { buildPosixFallbackPathPrelude } from '../../shared/posix-version-manager-bin-dirs'
import { buildPosixCommandPathLookupScript } from '../../shared/posix-command-path-lookup'
@@ -19,7 +20,7 @@ export async function detectWslCommandsOnPath(
return new Set()
}
const commandList = uniqueCommands.map(shellQuote).join(' ')
const commandList = uniqueCommands.map(quotePosixShell).join(' ')
const lookupScript = buildPosixCommandPathLookupScript(
{ kind: 'shell-variable', name: 'cmd' },
// Skip Windows mounts DURING the walk, not after it: WSL appends the
@@ -65,10 +66,6 @@ export async function detectWslCommandsOnPath(
}
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
function parseWslDetectedCommands(stdout: string): Set<string> {
const found = new Set<string>()
for (const rawLine of stdout.split(/\r?\n/)) {
+21 -22
View File
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { quotePosixShellDoubleQuotedApostrophe } from '../shared/shell-argument-quoting'
const { statSyncMock } = vi.hoisted(() => ({ statSyncMock: vi.fn() }))
@@ -34,38 +35,36 @@ afterEach(() => {
vi.unstubAllEnvs()
})
describe('quoteForPosixShell', () => {
it('wraps plain values in single quotes', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell('/tmp/orca.deb')).toBe("'/tmp/orca.deb'")
describe('quotePosixShellDoubleQuotedApostrophe', () => {
it('wraps plain values in single quotes', () => {
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/orca.deb')).toBe("'/tmp/orca.deb'")
})
it('quotes spaces', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell('/tmp/Orca Setup.deb')).toBe("'/tmp/Orca Setup.deb'")
it('quotes spaces', () => {
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/Orca Setup.deb')).toBe(
"'/tmp/Orca Setup.deb'"
)
})
it('neutralizes expansion, command substitution, separators, and double quotes', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell('/tmp/$HOME.deb')).toBe("'/tmp/$HOME.deb'")
expect(quoteForPosixShell('/tmp/`id`.deb')).toBe("'/tmp/`id`.deb'")
expect(quoteForPosixShell('/tmp/a;rm -rf ~.deb')).toBe("'/tmp/a;rm -rf ~.deb'")
expect(quoteForPosixShell('/tmp/"quoted".deb')).toBe('\'/tmp/"quoted".deb\'')
it('neutralizes expansion, command substitution, separators, and double quotes', () => {
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/$HOME.deb')).toBe("'/tmp/$HOME.deb'")
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/`id`.deb')).toBe("'/tmp/`id`.deb'")
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/a;rm -rf ~.deb')).toBe(
"'/tmp/a;rm -rf ~.deb'"
)
expect(quotePosixShellDoubleQuotedApostrophe('/tmp/"quoted".deb')).toBe('\'/tmp/"quoted".deb\'')
})
it('closes and reopens the quote around a literal single quote', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell("/tmp/it's.deb")).toBe(`'/tmp/it'"'"'s.deb'`)
it('closes and reopens the quote around a literal single quote', () => {
expect(quotePosixShellDoubleQuotedApostrophe("/tmp/it's.deb")).toBe(`'/tmp/it'"'"'s.deb'`)
})
it('escapes every single quote in a value', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell("'; rm -rf /; '")).toBe(`''"'"'; rm -rf /; '"'"''`)
it('escapes every single quote in a value', () => {
expect(quotePosixShellDoubleQuotedApostrophe("'; rm -rf /; '")).toBe(`''"'"'; rm -rf /; '"'"''`)
})
it('quotes an empty value', async () => {
const { quoteForPosixShell } = await loadCommandModule()
expect(quoteForPosixShell('')).toBe("''")
it('quotes an empty value', () => {
expect(quotePosixShellDoubleQuotedApostrophe('')).toBe("''")
})
})
+7 -6
View File
@@ -1,3 +1,4 @@
import { quotePosixShellDoubleQuotedApostrophe } from '../shared/shell-argument-quoting'
import { statSync } from 'node:fs'
import path from 'node:path'
import type { LinuxRootPackageType } from '../shared/update-status-types'
@@ -26,11 +27,6 @@ export type LinuxPackageInstallCommandResult =
| { ok: true; command: string }
| { ok: false; reason: 'no-sudo' | 'no-package-manager' | 'invalid-package-path' }
/** POSIX single-quoting: the only metacharacter left is `'`, closed and re-opened around a literal. */
export function quoteForPosixShell(value: string): string {
return `'${value.split("'").join(`'"'"'`)}'`
}
/**
* Resolves an executable strictly from the trusted system directories. A symlink inside those
* directories is fine — its target is what `statSync` checks — but nothing outside them is consulted
@@ -86,7 +82,12 @@ export function buildLinuxPackageInstallCommand(
continue
}
// No -y/--noconfirm: the user must see and confirm the privileged transaction.
const tokens = [sudoPath, managerPath, ...candidate.args, quoteForPosixShell(packagePath)]
const tokens = [
sudoPath,
managerPath,
...candidate.args,
quotePosixShellDoubleQuotedApostrophe(packagePath)
]
return { ok: true, command: tokens.join(' ') }
}
return { ok: false, reason: 'no-package-manager' }
+4 -4
View File
@@ -27,9 +27,9 @@ import {
execCommandInWslOrThrow,
execLocalPreflightCommandOrThrow,
findRunnableLocalCommand,
isCommandAvailable,
shellQuote
isCommandAvailable
} from '../ipc/preflight-command-exec'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import {
detectRemoteWindowsTerminalCapabilities,
type RemoteWindowsTerminalCapabilities
@@ -220,7 +220,7 @@ export async function refreshShellPathAndDetectAgents(
async function isGhAuthenticated(probe: CommandRuntime): Promise<boolean> {
try {
await (probe.wslTarget
? execCommandInWslOrThrow(probe.wslTarget, `${shellQuote('gh')} auth status`)
? execCommandInWslOrThrow(probe.wslTarget, `${quotePosixShell('gh')} auth status`)
: execLocalPreflightCommandOrThrow(probe.binary ?? 'gh', ['auth', 'status']))
// Why: for plain-text `gh auth status`, exit 0 means gh did not detect any
// authentication issues for the checked hosts/accounts.
@@ -241,7 +241,7 @@ async function isGhAuthenticated(probe: CommandRuntime): Promise<boolean> {
async function isGlabAuthenticated(probe: CommandRuntime): Promise<boolean> {
try {
await (probe.wslTarget
? execCommandInWslOrThrow(probe.wslTarget, `${shellQuote('glab')} auth status`)
? execCommandInWslOrThrow(probe.wslTarget, `${quotePosixShell('glab')} auth status`)
: execLocalPreflightCommandOrThrow(probe.binary ?? 'glab', ['auth', 'status']))
return true
} catch (error) {
+3 -3
View File
@@ -33,7 +33,7 @@ import {
getHiddenRateLimitWslCwdSetupCommands,
resolveHiddenRateLimitPtyCwd
} from './hidden-rate-limit-pty-cwd'
import { quoteHiddenRateLimitShellValue } from './hidden-rate-limit-shell'
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
const RPC_TIMEOUT_MS = 10_000
@@ -53,9 +53,9 @@ function buildWslCodexCommand(
}
const setupCommands = [
...getHiddenRateLimitWslCwdSetupCommands(),
`export CODEX_HOME=${quoteHiddenRateLimitShellValue(wslInfo.linuxPath)}`
`export CODEX_HOME=${quotePosixShell(wslInfo.linuxPath)}`
].join(' && ')
const execSuffix = `${args.map(quoteHiddenRateLimitShellValue).join(' ')} <&3 >&4 3<&- 4>&-`
const execSuffix = `${args.map(quotePosixShell).join(' ')} <&3 >&4 3<&- 4>&-`
const loginShellCommand = buildWslLoginShellCommand(
[setupCommands, `exec codex ${execSuffix}`].join(' && ')
)
@@ -1,3 +0,0 @@
export function quoteHiddenRateLimitShellValue(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
@@ -1,3 +1,4 @@
import { quotePosixShell } from '../../../shared/shell-argument-quoting'
import {
resolveSetupRunnerCommand,
type SetupRunnerCommandPlatform,
@@ -42,7 +43,7 @@ export function buildObservedSetupCommand(
`printf '\\n${completionPrefix(completionToken)}%s\\n' "$status"`,
'exit "$status"'
].join('; ')
return { command: `bash -lc ${quotePosixArg(script)}` }
return { command: `bash -lc ${quotePosixShell(script)}` }
}
export function createSetupCompletionScanner(
@@ -78,7 +79,3 @@ export function createSetupCompletionScanner(
function completionPrefix(completionToken: string): string {
return `${SETUP_COMPLETION_PREFIX}${completionToken}:`
}
function quotePosixArg(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
+3 -6
View File
@@ -1,3 +1,4 @@
import { quotePosixShell } from '../../shared/shell-argument-quoting'
import type { SshConnectionManager } from './connection/ssh-connection-manager'
import type { SshExecOptions } from './connection/ssh-exec-options'
import { powerShellCommand } from './ssh-remote-powershell'
@@ -266,13 +267,9 @@ function shellEscape(s: string): string {
return '"$HOME"'
}
if (s.startsWith('~/')) {
return `"$HOME"/${shellEscapeRaw(s.slice(2))}`
return `"$HOME"/${quotePosixShell(s.slice(2))}`
}
return shellEscapeRaw(s)
}
function shellEscapeRaw(s: string): string {
return `'${s.replace(/'/g, "'\\''")}'`
return quotePosixShell(s)
}
function powerShellPathExpression(s: string): string {
@@ -1,10 +1,11 @@
import { quotePosixShell } from '../../../../shared/shell-argument-quoting'
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 ? quotePowerShellLiteral(path) : `'${path.replaceAll("'", "'\\''")}'`
return windows ? quotePowerShellLiteral(path) : quotePosixShell(path)
}
/** PowerShell runs a quoted program path only through `&`. */
@@ -1,3 +1,4 @@
import { quotePosixShellUnlessPlain } from '../../../../shared/shell-argument-quoting'
import type { PaneManager } from '@/lib/pane-manager/pane-manager'
import { focusPanePreservingOverlays } from '@/lib/pane-manager/pane-overlay-focus'
@@ -75,9 +76,5 @@ export function shellEscapePath(path: string, targetShell: 'posix' | 'windows'):
return /^[a-zA-Z0-9_./@:\\-]+$/.test(path) ? path : `"${path}"`
}
if (/^[a-zA-Z0-9_./@:-]+$/.test(path)) {
return path
}
return `'${path.replace(/'/g, "'\\''")}'`
return quotePosixShellUnlessPlain(path)
}
+7 -10
View File
@@ -1,6 +1,7 @@
// Resume-command construction for Agent Session History rows: turns a scanned
// session into the shell line that re-enters it, quoted for the target platform
// and (when known) the live tab's shell.
import { quoteCmdDoubleQuoted } from './shell-argument-quoting'
import {
isAntigravityReferenceSession,
antigravityTranscriptReferencePrompt
@@ -42,7 +43,7 @@ export function buildAiVaultResumeCommand(args: {
: sessionId
const sessionArg =
shell === 'cmd'
? quoteWindowsCmdArg(resumeTarget)
? quoteCmdDoubleQuoted(resumeTarget)
: shell
? quoteStartupArg(resumeTarget, shell)
: quoteShellArg(resumeTarget, platform)
@@ -118,15 +119,15 @@ export function buildAiVaultResumeShellCommand(args: {
if (platform === 'win32' && shell === 'cmd') {
// Why: an interactive cmd splits the doubled quotes required by a nested
// `cmd /s /c` wrapper, so queued commands must use direct cmd syntax.
return `${clearPrefix}${cwd ? `cd /d ${quoteWindowsCmdArg(cwd)} && ${resumeCommand}` : resumeCommand}`
return `${clearPrefix}${cwd ? `cd /d ${quoteCmdDoubleQuoted(cwd)} && ${resumeCommand}` : resumeCommand}`
}
if (!cwd) {
return `${clearPrefix}${resumeCommand}`
}
if (platform === 'win32') {
const inner = `${clearPrefix}cd /d ${quoteWindowsCmdArg(cwd)} && ${resumeCommand}`
return `cmd /d /s /c ${quoteWindowsCmdArg(inner)}`
const inner = `${clearPrefix}cd /d ${quoteCmdDoubleQuoted(cwd)} && ${resumeCommand}`
return `cmd /d /s /c ${quoteCmdDoubleQuoted(inner)}`
}
return `cd ${quoteResumeArg(cwd, platform, shell)} && ${resumeCommand}`
@@ -263,7 +264,7 @@ function codexHomeEnvPrefix(
return ''
}
if (platform === 'win32') {
return `set ${quoteWindowsCmdArg(`CODEX_HOME=${codexHome}`)} && `
return `set ${quoteCmdDoubleQuoted(`CODEX_HOME=${codexHome}`)} && `
}
// fish accepts the `NAME=value cmd` prefix (3.1+), but not sh's quoting.
return `CODEX_HOME=${quoteResumeArg(codexHome, platform, shell)} `
@@ -284,9 +285,5 @@ function quoteResumeArg(
* `quoteStartupArg` keeps one spelling regardless of whether a caller happened
* to pass a shell. */
function quoteShellArg(value: string, platform: NodeJS.Platform): string {
return platform === 'win32' ? quoteWindowsCmdArg(value) : quoteStartupArg(value, 'posix')
}
function quoteWindowsCmdArg(value: string): string {
return `"${value.replace(/"/g, '""')}"`
return platform === 'win32' ? quoteCmdDoubleQuoted(value) : quoteStartupArg(value, 'posix')
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { quoteCmdDoubleQuoted, quotePosixShell } from './shell-argument-quoting'
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
import { StringDecoder } from 'node:string_decoder'
import type { EphemeralVmRecipeContext } from './ephemeral-vm-recipe-runner'
@@ -20,9 +21,9 @@ export function quoteShellToken(value: string): string {
// Inside cmd.exe double quotes, `^` is literal; an embedded `"` is escaped
// by doubling it. This token is only displayed for manual cleanup, so it
// must be valid when pasted into cmd.exe.
return `"${value.replace(/"/g, '""')}"`
return quoteCmdDoubleQuoted(value)
}
return `'${value.replace(/'/g, `'\\''`)}'`
return quotePosixShell(value)
}
export async function runRecipeCommand(args: {
+3 -5
View File
@@ -1,3 +1,5 @@
import { quotePosixShell } from './shell-argument-quoting'
export type PosixCommandPathLookupTarget =
| { kind: 'literal'; value: string }
| { kind: 'shell-variable'; name: string }
@@ -100,14 +102,10 @@ export function buildPosixCommandPathLookupScript(
function buildCommandAssignment(target: PosixCommandPathLookupTarget): string {
if (target.kind === 'literal') {
return shellQuote(target.value)
return quotePosixShell(target.value)
}
if (!SHELL_VARIABLE_NAME_PATTERN.test(target.name)) {
throw new Error(`Invalid shell variable name: ${target.name}`)
}
return `\${${target.name}-}`
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
+10 -16
View File
@@ -1,3 +1,4 @@
import { quotePosixShellUnlessPlainNoAt } from './shell-argument-quoting'
import { encodePowerShellCommand } from './powershell-command-encoding'
import { getPosixCodexShellLaunchPreflight } from './codex-shell-function'
import { getPosixClaudeShellFunction } from './claude-shell-function'
@@ -89,9 +90,9 @@ export function createSequencedSetupAgentCommands(args: {
}
function buildPosixSetupCommand(setupCommand: string, markerPath: string, nonce: string): string {
const marker = quotePosixArg(markerPath)
const tmp = quotePosixArg(`${markerPath}.tmp`)
const nonceValue = quotePosixArg(nonce)
const marker = quotePosixShellUnlessPlainNoAt(markerPath)
const tmp = quotePosixShellUnlessPlainNoAt(`${markerPath}.tmp`)
const nonceValue = quotePosixShellUnlessPlainNoAt(nonce)
const script = [
`rm -f ${marker} ${tmp} 2>/dev/null`,
@@ -102,7 +103,7 @@ function buildPosixSetupCommand(setupCommand: string, markerPath: string, nonce:
'exit "$status"'
].join('; ')
return `bash -lc ${quotePosixArg(script)}`
return `bash -lc ${quotePosixShellUnlessPlainNoAt(script)}`
}
function buildPosixStartupScript(
@@ -111,9 +112,9 @@ function buildPosixStartupScript(
nonce: string,
waitTimeoutSeconds: number
): string {
const marker = quotePosixArg(markerPath)
const tmp = quotePosixArg(`${markerPath}.tmp`)
const nonceValue = quotePosixArg(nonce)
const marker = quotePosixShellUnlessPlainNoAt(markerPath)
const tmp = quotePosixShellUnlessPlainNoAt(`${markerPath}.tmp`)
const nonceValue = quotePosixShellUnlessPlainNoAt(nonce)
const timeout = Math.max(1, Math.floor(waitTimeoutSeconds))
const startupSuccessCommand = buildPosixStartupSuccessCommand(startupCommand)
// Why: the PTY launch path feeds this command through an interactive shell,
@@ -131,7 +132,7 @@ function buildPosixStartupScript(
// "Waiting for setup..." as the pane's last line forever.
// Why here and not first: setup may be what puts codex on PATH, and this
// `bash -lc` never reads Orca's shell wrapper, so it defines the function itself.
`if [ "$status" = "0" ]; then echo ${quotePosixArg(SETUP_COMPLETE_MESSAGE)} >&2;\n${getPosixCodexShellLaunchPreflight() + getPosixClaudeShellFunction()}if [ -n "\${${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}:-}" ]; then eval "\$${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}"; exit "$?"; else ${startupSuccessCommand}; fi; fi;`,
`if [ "$status" = "0" ]; then echo ${quotePosixShellUnlessPlainNoAt(SETUP_COMPLETE_MESSAGE)} >&2;\n${getPosixCodexShellLaunchPreflight() + getPosixClaudeShellFunction()}if [ -n "\${${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}:-}" ]; then eval "\$${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}"; exit "$?"; else ${startupSuccessCommand}; fi; fi;`,
'echo "Setup failed; skipping agent startup." >&2;',
'exit "${status:-1}";',
'fi;',
@@ -152,7 +153,7 @@ function buildPosixStartupSuccessCommand(startupCommand: string): string {
hasUnquotedPosixCommandSeparator(startupCommand) ||
hasLeadingPosixEnvAssignment(startupCommand)
) {
return `eval ${quotePosixArg(startupCommand)}; exit "$?"`
return `eval ${quotePosixShellUnlessPlainNoAt(startupCommand)}; exit "$?"`
}
return `exec ${startupCommand}`
}
@@ -297,10 +298,3 @@ function buildWindowsStartupCommand(
function encodePowerShellInvocation(script: string): string {
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`
}
function quotePosixArg(value: string): string {
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
return value
}
return `'${value.replace(/'/g, `'\\''`)}'`
}
+7 -18
View File
@@ -1,3 +1,4 @@
import { quoteCmdDoubleQuoted, quotePosixShellUnlessPlainNoAt } from './shell-argument-quoting'
import { isWindowsAbsolutePathLike } from './cross-platform-path'
import {
buildWindowsCmdRunnerDelayedLaunchCommand,
@@ -48,14 +49,14 @@ export function resolveSetupRunnerCommand(
if (isWslUncPath(runnerScriptPath)) {
const linuxPath = wslUncToLinuxPath(runnerScriptPath)
return {
command: `bash ${quotePosixArg(linuxPath)}`,
command: `bash ${quotePosixShellUnlessPlainNoAt(linuxPath)}`,
runnerScriptPathForShell: linuxPath,
shell: 'posix'
}
}
if (runnerScriptPath.startsWith('/') && !isWindowsAbsolutePathLike(runnerScriptPath)) {
return {
command: `bash ${quotePosixArg(runnerScriptPath)}`,
command: `bash ${quotePosixShellUnlessPlainNoAt(runnerScriptPath)}`,
runnerScriptPathForShell: runnerScriptPath,
shell: 'posix'
}
@@ -68,7 +69,7 @@ export function resolveSetupRunnerCommand(
if (isWslExecutable(shell?.executable)) {
const wslPath = nativeWindowsPathToWslShellPath(runnerScriptPath)
return {
command: `bash ${quotePosixArg(wslPath)}`,
command: `bash ${quotePosixShellUnlessPlainNoAt(wslPath)}`,
runnerScriptPathForShell: wslPath,
shell: 'posix'
}
@@ -76,7 +77,7 @@ export function resolveSetupRunnerCommand(
// Why: queued setup launches can outlive the process that generated them, so convert native paths before handing off to POSIX shells.
const posixPath = nativeWindowsPathToPosixShellPath(runnerScriptPath)
return {
command: `bash ${quotePosixArg(posixPath)}`,
command: `bash ${quotePosixShellUnlessPlainNoAt(posixPath)}`,
runnerScriptPathForShell: posixPath,
shell: 'posix'
}
@@ -90,14 +91,14 @@ export function resolveSetupRunnerCommand(
command:
shell?.family === 'posix' || windowsRunnerPathNeedsCmdGuard(runnerScriptPath)
? buildWindowsCmdRunnerDelayedLaunchCommand(runnerScriptPath)
: `cmd.exe /c ${quoteWindowsArg(runnerScriptPath)}`,
: `cmd.exe /c ${quoteCmdDoubleQuoted(runnerScriptPath)}`,
runnerScriptPathForShell: runnerScriptPath,
shell: 'windows'
}
}
return {
command: `bash ${quotePosixArg(runnerScriptPath)}`,
command: `bash ${quotePosixShellUnlessPlainNoAt(runnerScriptPath)}`,
runnerScriptPathForShell: runnerScriptPath,
shell: 'posix'
}
@@ -119,18 +120,6 @@ export function wslUncToLinuxPath(windowsPath: string): string {
return match?.[2] || '/'
}
function quotePosixArg(value: string): string {
if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
return value
}
return `'${value.replace(/'/g, `'\\''`)}'`
}
function quoteWindowsArg(value: string): string {
return `"${value.replace(/"/g, '""')}"`
}
export function nativeWindowsPathToPosixShellPath(value: string): string {
const driveMatch = value.match(/^([A-Za-z]):[\\/](.*)$/)
if (driveMatch) {
+121 -2
View File
@@ -1,7 +1,110 @@
import { describe, expect, it } from 'vitest'
import { quotePosixShell } from './shell-argument-quoting'
import {
quoteCmdDoubleQuoted,
quotePosixShell,
quotePosixShellDoubleQuotedApostrophe,
quotePosixShellUnlessPlain,
quotePosixShellUnlessPlainNoAt
} from './shell-argument-quoting'
// ── quotePosixShell ──────────────────────────────────────────────────────
// Verbatim bodies of the copies this module replaced, keyed by where each one lived. Every
// former copy must produce byte-identical output to the function that now serves its callers.
const sq = (value: string): string => `'${value.replace(/'/g, "'\\''")}'`
const sqAll = (value: string): string => `'${value.replaceAll("'", "'\\''")}'`
const plainAt = (value: string): string => (/^[a-zA-Z0-9_./@:-]+$/.test(value) ? value : sq(value))
const plainNoAt = (value: string): string => (/^[A-Za-z0-9_./:-]+$/.test(value) ? value : sq(value))
const dq = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`
const FORMER_COPIES: [string, (value: string) => string, (value: string) => string][] = [
['main/ssh/ssh-connection-utils shellEscape', sq, quotePosixShell],
['shared/wsl-login-shell-command quotePosixShell', sq, quotePosixShell],
['main/agent-hooks/posix-hook-command quotePosixShellString', sqAll, quotePosixShell],
['main/codex/codex-hook-definition inline', sqAll, quotePosixShell],
['main/ipc/preflight-command-exec shellQuote', sq, quotePosixShell],
['main/ipc/preflight-wsl-agent-detection shellQuote', sq, quotePosixShell],
['main/rate-limits/hidden-rate-limit-shell', sq, quotePosixShell],
['main/runtime/orchestration/setup-completion-signal quotePosixArg', sq, quotePosixShell],
['main/ssh/ssh-remote-directory-browse shellEscapeRaw', sq, quotePosixShell],
['shared/posix-command-path-lookup shellQuote', sq, quotePosixShell],
['renderer ipynb-kernel-setup-commands POSIX branch', sqAll, quotePosixShell],
['shared/ephemeral-vm-recipe-process POSIX branch', sq, quotePosixShell],
['cli/shell-command-quote POSIX tail', sqAll, quotePosixShell],
['main/external-editor-launch escapePosixPathForShell', plainAt, quotePosixShellUnlessPlain],
['renderer pane-helpers shellEscapePath POSIX', plainAt, quotePosixShellUnlessPlain],
['shared/setup-agent-sequencing quotePosixArg', plainNoAt, quotePosixShellUnlessPlainNoAt],
['shared/setup-runner-command quotePosixArg', plainNoAt, quotePosixShellUnlessPlainNoAt],
['main/cli/cli-install-path-format quoteShell', dq, quotePosixShellDoubleQuotedApostrophe],
['main/cli/wsl-cli-scripts quoteShell', dq, quotePosixShellDoubleQuotedApostrophe],
['main/antigravity/antigravity-browser-helper quote', dq, quotePosixShellDoubleQuotedApostrophe],
[
'cli/orchestration-mutation-recovery shellQuote tail',
dq,
quotePosixShellDoubleQuotedApostrophe
],
[
'main/linux-package-install-command quoteForPosixShell',
(value) => `'${value.split("'").join(`'"'"'`)}'`,
quotePosixShellDoubleQuotedApostrophe
],
[
'shared/ephemeral-vm-recipe-process win32 branch',
(value) => `"${value.replace(/"/g, '""')}"`,
quoteCmdDoubleQuoted
],
[
'shared/setup-runner-command quoteWindowsArg',
(value) => `"${value.replace(/"/g, '""')}"`,
quoteCmdDoubleQuoted
],
[
'shared/ai-vault-resume-command quoteWindowsCmdArg',
(value) => `"${value.replace(/"/g, '""')}"`,
quoteCmdDoubleQuoted
]
]
const HOSTILE_INPUTS = [
'',
"'",
"''",
"it's",
"'; rm -rf /; '",
'plain',
'a@b',
'user@host:/path',
'/tmp/Orca Setup.deb',
'$HOME',
'${PATH}',
'`id`',
'$(id)',
'"quoted"',
'\\',
'trailing\\',
'C:\\Program Files\\Orca\\orca.exe',
'%PATH%',
'!x!',
'^&|<>()',
'~',
'~/dir',
'-n',
'--flag=value',
'a b\tc',
'line1\nline2',
'crlf\r\n',
'nul\u0000byte',
'é☃😀',
'*?[a]{b,c}',
'#comment',
'a;b&&c||d'
]
describe('shell-argument-quoting matches every former copy byte for byte', () => {
describe.each(FORMER_COPIES)('%s', (_site, former, current) => {
it.each(HOSTILE_INPUTS)('%j', (input) => {
expect(current(input)).toBe(former(input))
})
})
})
describe('quotePosixShell', () => {
it('wraps string in single quotes', () => {
@@ -20,3 +123,19 @@ describe('quotePosixShell', () => {
expect(quotePosixShell('foo bar; rm -rf /')).toBe("'foo bar; rm -rf /'")
})
})
describe('the spellings stay distinct', () => {
it('spells an apostrophe two different ways', () => {
expect(quotePosixShell("it's")).toBe(`'it'\\''s'`)
expect(quotePosixShellDoubleQuotedApostrophe("it's")).toBe(`'it'"'"'s'`)
})
it('leaves @ bare only in the plain-with-@ variant', () => {
expect(quotePosixShellUnlessPlain('a@b')).toBe('a@b')
expect(quotePosixShellUnlessPlainNoAt('a@b')).toBe("'a@b'")
})
it('doubles embedded quotes for cmd.exe', () => {
expect(quoteCmdDoubleQuoted('say "hi"')).toBe('"say ""hi"""')
})
})
+27
View File
@@ -1,4 +1,31 @@
// One home for shell-argument quoting. Each spelling below is a separate contract: callers embed,
// compare or parse back these exact bytes, so variants stay separate functions, never flags.
// PowerShell quoting lives in powershell-native-argument.ts; CommandLineToArgvW in @orca/process-host.
/** POSIX sh single-quoting: each `'` closes the quote, is escaped, and reopens it (`'\''`). */
export function quotePosixShell(value: string): string {
return `'${value.replace(/'/g, "'\\''")}'`
}
/** As `quotePosixShell`, but a non-empty word of only `A-Za-z0-9_./@:-` stays bare. */
export function quotePosixShellUnlessPlain(value: string): string {
return /^[a-zA-Z0-9_./@:-]+$/.test(value) ? value : quotePosixShell(value)
}
/** As `quotePosixShellUnlessPlain`, except `@` also forces quoting. */
export function quotePosixShellUnlessPlainNoAt(value: string): string {
return /^[A-Za-z0-9_./:-]+$/.test(value) ? value : quotePosixShell(value)
}
/**
* POSIX sh single-quoting that spells each `'` as `'"'"'`. Installed CLI wrappers are read back
* in this spelling (legacy-appimage-cli-wrapper.ts, wsl-cli-scripts.ts), so it must not change.
*/
export function quotePosixShellDoubleQuotedApostrophe(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`
}
/** cmd.exe double-quoting with `"` doubled. Leaves `%`, `^` and `!` as they are. */
export function quoteCmdDoubleQuoted(value: string): string {
return `"${value.replace(/"/g, '""')}"`
}