mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
Verified on native Windows awin at the exact PR head with Electron CDP/Playwright: the Claude sign-in console is visible, cancellation after console launch restores Add Account state, and the login process/PID/temp cleanup completes.
109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
import { win32 } from 'node:path'
|
|
|
|
/** Full path to cmd.exe for GUI and service-launched processes. */
|
|
export function getCmdExePath(): string {
|
|
return (
|
|
process.env.ComSpec ||
|
|
win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'cmd.exe')
|
|
)
|
|
}
|
|
|
|
export function isWindowsBatchScript(commandPath: string): boolean {
|
|
return process.platform === 'win32' && /\.(cmd|bat)$/i.test(commandPath)
|
|
}
|
|
|
|
export const WINDOWS_BATCH_UNSAFE_ARGUMENTS_ERROR = 'UNSAFE_WINDOWS_BATCH_ARGUMENTS'
|
|
|
|
export class UnsafeWindowsBatchArgumentsError extends Error {
|
|
constructor() {
|
|
super(WINDOWS_BATCH_UNSAFE_ARGUMENTS_ERROR)
|
|
this.name = 'UnsafeWindowsBatchArgumentsError'
|
|
}
|
|
}
|
|
|
|
// Why: cmd.exe re-parses the command line, and these are the characters that can
|
|
// start a new command or expand a variable out of an otherwise inert argument.
|
|
// `(`/`)` are deliberately absent: they only group commands, and grouping cannot
|
|
// chain anything without one of the separators below, so rejecting them merely
|
|
// broke every `C:\Program Files (x86)\...` shim and paren-bearing worktree path.
|
|
const WINDOWS_BATCH_UNSAFE_CHARACTERS = ['&', '|', '<', '>', '^', '"', '%', '!'] as const
|
|
|
|
/** The rejected characters, spelled for error messages so they cannot drift from the guard. */
|
|
export const WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL = WINDOWS_BATCH_UNSAFE_CHARACTERS.join(' ')
|
|
|
|
const UNSAFE_WINDOWS_BATCH_SYNTAX = new RegExp(
|
|
`[${WINDOWS_BATCH_UNSAFE_CHARACTERS.map((character) => character.replace(/[\\^\]-]/, '\\$&')).join('')}\\r\\n]`
|
|
)
|
|
|
|
function hasUnsafeWindowsBatchSyntax(value: string): boolean {
|
|
return UNSAFE_WINDOWS_BATCH_SYNTAX.test(value)
|
|
}
|
|
|
|
function assertWindowsCmdSafeTokens(values: readonly string[]): void {
|
|
if (values.some(hasUnsafeWindowsBatchSyntax)) {
|
|
throw new UnsafeWindowsBatchArgumentsError()
|
|
}
|
|
}
|
|
|
|
export type GetSpawnArgsForWindowsOptions = {
|
|
/**
|
|
* GUI launchers (Open In apps) should not leave a lingering Command Prompt.
|
|
* `start "" /B` returns immediately and keeps console-subsystem children of
|
|
* `.cmd`/`.bat` shims from allocating a fresh visible prompt window.
|
|
*
|
|
* Opt-in only: `start` re-parses the command line, so callers whose argv can
|
|
* carry quoted operands (VS Code `--remote` authorities and remote paths with
|
|
* spaces) must leave this off.
|
|
*/
|
|
detachedGui?: boolean
|
|
}
|
|
|
|
export function getSpawnArgsForWindows(
|
|
command: string,
|
|
args: string[],
|
|
options: GetSpawnArgsForWindowsOptions = {}
|
|
): { spawnCmd: string; spawnArgs: string[] } {
|
|
if (isWindowsBatchScript(command)) {
|
|
assertWindowsCmdSafeTokens([command, ...args])
|
|
|
|
// Why: separate argv entries let Node quote spaces without breaking cmd.
|
|
if (options.detachedGui) {
|
|
// Why: `start` launches a batch target through a nested `cmd /K`, which
|
|
// stays resident after the script ends — `/B` only suppresses a *new*
|
|
// console, so the shim leaks a hidden cmd.exe. Handing `start` an inner
|
|
// `cmd /d /c` makes that interpreter exit with the script.
|
|
//
|
|
// Window title must be an *empty argv entry* (`''`). libuv's Windows
|
|
// quoter turns empty into `""` on the CreateProcess command line — the
|
|
// empty title `start` requires so a later quoted path is not eaten as
|
|
// the title. The two-character string `'""'` is wrong: libuv re-escapes
|
|
// it to `"\"\""`. (Default ComSpec has no spaces, so the bad form often
|
|
// still "works"; quoted Program Files paths are where it breaks.)
|
|
const cmdExePath = getCmdExePath()
|
|
return {
|
|
spawnCmd: cmdExePath,
|
|
spawnArgs: ['/d', '/c', 'start', '', '/B', cmdExePath, '/d', '/c', command, ...args]
|
|
}
|
|
}
|
|
return { spawnCmd: getCmdExePath(), spawnArgs: ['/d', '/c', command, ...args] }
|
|
}
|
|
return { spawnCmd: command, spawnArgs: args }
|
|
}
|
|
|
|
/**
|
|
* Inverse of `detachedGui`'s `start "" /B`: a visible console that waits.
|
|
* Title is the empty argv entry so libuv emits `""` rather than `"\"\""`.
|
|
*/
|
|
export function wrapWindowsStartWait(
|
|
spawnCmd: string,
|
|
spawnArgs: string[]
|
|
): { spawnCmd: string; spawnArgs: string[] } {
|
|
// Why: `start` reparses every target through cmd.exe, including .exe paths.
|
|
assertWindowsCmdSafeTokens([spawnCmd, ...spawnArgs])
|
|
const cmdExePath = getCmdExePath()
|
|
return {
|
|
spawnCmd: cmdExePath,
|
|
spawnArgs: ['/d', '/c', 'start', '', '/wait', spawnCmd, ...spawnArgs]
|
|
}
|
|
}
|