Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
// Windows: Use PowerShell
return {
shell: "powershell.exe",
args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command],
args: [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
command,
],
};
} else {
// Unix/macOS: Use login shell to source .bashrc/.zshrc etc.
Expand Down Expand Up @@ -150,6 +158,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => {
cwd,
env: getColorEnv(), // Add enhanced environment for colors
});
childProc.stdin?.end();

// Track this process for foreground cancellation
if (toolCallId && waitForCompletion) {
Expand Down
8 changes: 8 additions & 0 deletions extensions/cli/src/tools/runTerminalCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ describe("runTerminalCommandTool", () => {
/Error \(exit code|Command timed out|not found|not recognized/,
);
});

it("should reject on non-zero exit code even with empty stderr", async () => {
const command = "exit 42";

await expect(runTerminalCommandTool.run({ command })).rejects.toMatch(
/Error \(exit code 42\)/,
);
});
});

describe("platform-specific features", () => {
Expand Down
23 changes: 18 additions & 5 deletions extensions/cli/src/tools/runTerminalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
// Windows: Use PowerShell
return {
shell: "powershell.exe",
args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command],
args: [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
command,
],
};
}

Expand Down Expand Up @@ -190,12 +198,14 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed
// Use same shell logic as core implementation
const { shell, args } = getShellCommand(command);
const child = spawn(shell, args);
// Close stdin immediately as this tool runs non-interactively and should not wait on stdin
child.stdin?.end();
let stdout = "";
let stderr = "";
let timeoutId: NodeJS.Timeout;
let isResolved = false;

// Determine timeout: use provided timeout (capped at 600s), test env variable, or default 120s
// Determine timeout: use provided timeout (capped at 600s), test env variable, or default 180s
let TIMEOUT_MS = 180000; // 180 seconds default
if (timeout !== undefined) {
// Cap at 600 seconds (10 minutes)
Expand All @@ -206,6 +216,8 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed
process.env.TEST_TERMINAL_TIMEOUT
) {
TIMEOUT_MS = parseInt(process.env.TEST_TERMINAL_TIMEOUT, 10);
} else if (process.env.NODE_ENV === "test") {
TIMEOUT_MS = 15000;
}

/**
Expand Down Expand Up @@ -330,9 +342,10 @@ IMPORTANT: To edit files, use Edit/MultiEdit tools instead of bash commands (sed
moveToBackground,
);

// Only reject on non-zero exit code if there's also stderr
if (code !== 0 && stderr) {
reject(`Error (exit code ${code}): ${stderr}`);
// Reject on non-zero exit code
if (code !== 0 && code !== null) {
const errorMessage = stderr || stdout || "Command failed";
reject(`Error (exit code ${code}): ${errorMessage}`);
return;
}

Expand Down
3 changes: 3 additions & 0 deletions extensions/cli/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { resetConsoleOverrides } from "./src/init.js";
process.env.CONTINUE_CLI_ENABLE_TELEMETRY = "0";
process.env.CONTINUE_ALLOW_ANONYMOUS_TELEMETRY = "0";

// Ensure terminal command timeout in tests fires before vitest's 30s test timeout
process.env.TEST_TERMINAL_TIMEOUT = "15000";

// Mock fetch to prevent actual API calls in tests
const originalFetch = global.fetch;
global.fetch = vi
Expand Down
Loading