Skip to content

Commit 42b7338

Browse files
committed
fix: use bash shell for POSIX commands on Windows
Add shell option to execAsync so callers can specify bash for POSIX commands that don't work with cmd.exe (Windows default). Fixed: - LocalBaseRuntime.spawnBackground: nohup/setsid commands - WorktreeRuntime.deleteWorkspace: rm -rf command
1 parent 8e86f94 commit 42b7338

File tree

3 files changed

+18
-5
lines changed

3 files changed

+18
-5
lines changed

src/node/runtime/LocalBaseRuntime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ export abstract class LocalBaseRuntime implements Runtime {
376376
});
377377

378378
try {
379-
using proc = execAsync(spawnCommand);
379+
// Use bash shell explicitly - spawnCommand uses POSIX commands (nohup, setsid)
380+
using proc = execAsync(spawnCommand, { shell: getBashPath() });
380381
const result = await proc.result;
381382

382383
const pid = parseInt(result.stdout.trim(), 10);

src/node/runtime/WorktreeRuntime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
import { listLocalBranches } from "@/node/git";
1313
import { checkInitHookExists } from "./initHook";
1414
import { execAsync } from "@/node/utils/disposableExec";
15+
import { getBashPath } from "@/node/utils/main/bashPath";
1516
import { getProjectName } from "@/node/utils/runtime/helpers";
1617
import { getErrorMessage } from "@/common/utils/errors";
1718
import { expandTilde } from "./tildeExpansion";
@@ -283,8 +284,8 @@ export class WorktreeRuntime extends LocalBaseRuntime {
283284
// Ignore prune errors - we'll still try rm -rf
284285
}
285286

286-
// Force delete the directory
287-
using rmProc = execAsync(`rm -rf "${deletedPath}"`);
287+
// Force delete the directory (use bash shell for rm -rf on Windows)
288+
using rmProc = execAsync(`rm -rf "${deletedPath}"`, { shell: getBashPath() });
288289
await rmProc.result;
289290

290291
return { success: true, deletedPath };

src/node/utils/disposableExec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,27 @@ class DisposableExec implements Disposable {
113113
}
114114
}
115115

116+
/**
117+
* Options for execAsync.
118+
*/
119+
export interface ExecAsyncOptions {
120+
/** Shell to use for command execution. If not specified, uses system default (cmd.exe on Windows). */
121+
shell?: string;
122+
}
123+
116124
/**
117125
* Execute command with automatic cleanup via `using` declaration.
118126
* Prevents zombie processes by ensuring child is reaped even on error.
119127
*
120128
* @example
121129
* using proc = execAsync("git status");
122130
* const { stdout } = await proc.result;
131+
*
132+
* // With explicit shell (needed for POSIX commands on Windows)
133+
* using proc = execAsync("nohup bash -c ...", { shell: getBashPath() });
123134
*/
124-
export function execAsync(command: string): DisposableExec {
125-
const child = exec(command);
135+
export function execAsync(command: string, options?: ExecAsyncOptions): DisposableExec {
136+
const child = exec(command, { shell: options?.shell });
126137
const promise = new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
127138
let stdout = "";
128139
let stderr = "";

0 commit comments

Comments
 (0)