ClawX windows path robustness (#171)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import { homedir } from 'os';
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
export { quoteForCmd, needsWinShell, prepareWinSpawn } from './win-shell';
|
||||
|
||||
/**
|
||||
* Expand ~ to home directory
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { getUvMirrorEnv } from './uv-env';
|
||||
import { logger } from './logger';
|
||||
import { quoteForCmd, needsWinShell } from './paths';
|
||||
|
||||
/**
|
||||
* Get the path to the bundled uv binary
|
||||
@@ -88,11 +89,12 @@ export async function installUv(): Promise<void> {
|
||||
*/
|
||||
export async function isPythonReady(): Promise<boolean> {
|
||||
const { bin: uvBin } = resolveUvBin();
|
||||
const useShell = needsWinShell(uvBin);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
try {
|
||||
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
||||
shell: process.platform === 'win32',
|
||||
const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], {
|
||||
shell: useShell,
|
||||
});
|
||||
child.on('close', (code) => resolve(code === 0));
|
||||
child.on('error', () => resolve(false));
|
||||
@@ -111,12 +113,13 @@ async function runPythonInstall(
|
||||
env: Record<string, string | undefined>,
|
||||
label: string,
|
||||
): Promise<void> {
|
||||
const useShell = needsWinShell(uvBin);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stderrChunks: string[] = [];
|
||||
const stdoutChunks: string[] = [];
|
||||
|
||||
const child = spawn(uvBin, ['python', 'install', '3.12'], {
|
||||
shell: process.platform === 'win32',
|
||||
const child = spawn(useShell ? quoteForCmd(uvBin) : uvBin, ['python', 'install', '3.12'], {
|
||||
shell: useShell,
|
||||
env,
|
||||
});
|
||||
|
||||
@@ -201,10 +204,11 @@ export async function setupManagedPython(): Promise<void> {
|
||||
}
|
||||
|
||||
// After installation, verify and log the Python path
|
||||
const verifyShell = needsWinShell(uvBin);
|
||||
try {
|
||||
const findPath = await new Promise<string>((resolve) => {
|
||||
const child = spawn(uvBin, ['python', 'find', '3.12'], {
|
||||
shell: process.platform === 'win32',
|
||||
const child = spawn(verifyShell ? quoteForCmd(uvBin) : uvBin, ['python', 'find', '3.12'], {
|
||||
shell: verifyShell,
|
||||
env: { ...process.env, ...uvEnv },
|
||||
});
|
||||
let output = '';
|
||||
|
||||
65
electron/utils/win-shell.ts
Normal file
65
electron/utils/win-shell.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Windows shell quoting utilities for child_process.spawn().
|
||||
*
|
||||
* When spawn() is called with `shell: true` on Windows, the command and
|
||||
* arguments are concatenated and passed to cmd.exe. Paths containing spaces
|
||||
* must be wrapped in double-quotes to prevent cmd.exe from splitting them
|
||||
* into separate tokens.
|
||||
*
|
||||
* This module is intentionally dependency-free so it can be unit-tested
|
||||
* without mocking Electron.
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Quote a path/value for safe use with Windows cmd.exe (shell: true in spawn).
|
||||
*
|
||||
* When Node.js spawn is called with `shell: true` on Windows, cmd.exe
|
||||
* interprets spaces as argument separators. Wrapping the value in double
|
||||
* quotes prevents this. On non-Windows platforms the value is returned
|
||||
* unchanged so this function can be called unconditionally.
|
||||
*/
|
||||
export function quoteForCmd(value: string): string {
|
||||
if (process.platform !== 'win32') return value;
|
||||
if (!value.includes(' ')) return value;
|
||||
if (value.startsWith('"') && value.endsWith('"')) return value;
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a spawn call needs `shell: true` on Windows.
|
||||
*
|
||||
* Full (absolute) paths can be executed directly by the OS via
|
||||
* CreateProcessW, which handles spaces correctly without a shell.
|
||||
* Simple command names (e.g. 'uv', 'node') need shell for PATH/PATHEXT
|
||||
* resolution on Windows.
|
||||
*/
|
||||
export function needsWinShell(bin: string): boolean {
|
||||
if (process.platform !== 'win32') return false;
|
||||
return !path.win32.isAbsolute(bin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare command and args for spawn(), handling Windows paths with spaces.
|
||||
*
|
||||
* Returns the shell option, the (possibly quoted) command, and the
|
||||
* (possibly quoted) args array ready for child_process.spawn().
|
||||
*/
|
||||
export function prepareWinSpawn(
|
||||
command: string,
|
||||
args: string[],
|
||||
forceShell?: boolean,
|
||||
): { shell: boolean; command: string; args: string[] } {
|
||||
const isWin = process.platform === 'win32';
|
||||
const useShell = forceShell ?? (isWin && !path.win32.isAbsolute(command));
|
||||
|
||||
if (!useShell || !isWin) {
|
||||
return { shell: useShell, command, args };
|
||||
}
|
||||
|
||||
return {
|
||||
shell: true,
|
||||
command: quoteForCmd(command),
|
||||
args: args.map(a => quoteForCmd(a)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user