summaryrefslogtreecommitdiff
path: root/packages/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/core/prompts.ts12
-rw-r--r--packages/core/src/tools/edit.ts4
-rw-r--r--packages/core/src/tools/shell.ts108
-rw-r--r--packages/core/src/utils/editor.test.ts12
-rw-r--r--packages/core/src/utils/editor.ts14
5 files changed, 102 insertions, 48 deletions
diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts
index ca04fd38..084ab1f3 100644
--- a/packages/core/src/core/prompts.ts
+++ b/packages/core/src/core/prompts.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { LSTool } from '../tools/ls.js';
@@ -131,8 +132,15 @@ You are running outside of a sandbox container, directly on the user's system. F
${(function () {
// note git repo can change so we need to check every time system prompt is generated
- const gitRootCmd = 'git rev-parse --show-toplevel 2>/dev/null || true';
- const gitRoot = execSync(gitRootCmd)?.toString()?.trim();
+ const isWindows = os.platform() === 'win32';
+ const devNull = isWindows ? 'NUL' : '/dev/null';
+ const gitRootCmd = `git rev-parse --show-toplevel 2>${devNull}`;
+ let gitRoot = '';
+ try {
+ gitRoot = execSync(gitRootCmd)?.toString()?.trim();
+ } catch (_e) {
+ // ignore
+ }
if (gitRoot) {
return `
# Git Repository
diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts
index 3240fa30..fdabc5b6 100644
--- a/packages/core/src/tools/edit.ts
+++ b/packages/core/src/tools/edit.ts
@@ -206,6 +206,8 @@ Expectation for required parameters:
try {
currentContent = fs.readFileSync(params.file_path, 'utf8');
+ // Normalize line endings to LF for consistent processing.
+ currentContent = currentContent.replace(/\r\n/g, '\n');
fileExists = true;
} catch (err: unknown) {
if (!isNodeError(err) || err.code !== 'ENOENT') {
@@ -303,6 +305,8 @@ Expectation for required parameters:
try {
currentContent = fs.readFileSync(params.file_path, 'utf8');
+ // Normalize line endings to LF for consistent processing.
+ currentContent = currentContent.replace(/\r\n/g, '\n');
fileExists = true;
} catch (err: unknown) {
if (isNodeError(err) && err.code === 'ENOENT') {
diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts
index e1cde43b..4bae498b 100644
--- a/packages/core/src/tools/shell.ts
+++ b/packages/core/src/tools/shell.ts
@@ -169,20 +169,35 @@ export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
};
}
- // wrap command to append subprocess pids (via pgrep) to temporary file
- const tempFileName = `shell_pgrep_${crypto.randomBytes(6).toString('hex')}.tmp`;
- const tempFilePath = path.join(os.tmpdir(), tempFileName);
+ const isWindows = os.platform() === 'win32';
- let command = params.command.trim();
- if (!command.endsWith('&')) command += ';';
- command = `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
+ // pgrep is not available on Windows, so we can't get background PIDs
+ const command = isWindows
+ ? params.command
+ : (() => {
+ // wrap command to append subprocess pids (via pgrep) to temporary file
+ const tempFileName = `shell_pgrep_${crypto
+ .randomBytes(6)
+ .toString('hex')}.tmp`;
+ const tempFilePath = path.join(os.tmpdir(), tempFileName);
+
+ let command = params.command.trim();
+ if (!command.endsWith('&')) command += ';';
+ return `{ ${command} }; __code=$?; pgrep -g 0 >${tempFilePath} 2>&1; exit $__code;`;
+ })();
// spawn command in specified directory (or project root if not specified)
- const shell = spawn('bash', ['-c', command], {
- stdio: ['ignore', 'pipe', 'pipe'],
- detached: true, // ensure subprocess starts its own process group (esp. in Linux)
- cwd: path.resolve(this.config.getTargetDir(), params.directory || ''),
- });
+ const shell = isWindows
+ ? spawn('cmd.exe', ['/c', command], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ // detached: true, // ensure subprocess starts its own process group (esp. in Linux)
+ cwd: path.resolve(this.config.getTargetDir(), params.directory || ''),
+ })
+ : spawn('bash', ['-c', command], {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ detached: true, // ensure subprocess starts its own process group (esp. in Linux)
+ cwd: path.resolve(this.config.getTargetDir(), params.directory || ''),
+ });
let exited = false;
let stdout = '';
@@ -241,22 +256,27 @@ export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
const abortHandler = async () => {
if (shell.pid && !exited) {
- try {
- // attempt to SIGTERM process group (negative PID)
- // fall back to SIGKILL (to group) after 200ms
- process.kill(-shell.pid, 'SIGTERM');
- await new Promise((resolve) => setTimeout(resolve, 200));
- if (shell.pid && !exited) {
- process.kill(-shell.pid, 'SIGKILL');
- }
- } catch (_e) {
- // if group kill fails, fall back to killing just the main process
+ if (os.platform() === 'win32') {
+ // For Windows, use taskkill to kill the process tree
+ spawn('taskkill', ['/pid', shell.pid.toString(), '/f', '/t']);
+ } else {
try {
- if (shell.pid) {
- shell.kill('SIGKILL');
+ // attempt to SIGTERM process group (negative PID)
+ // fall back to SIGKILL (to group) after 200ms
+ process.kill(-shell.pid, 'SIGTERM');
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ if (shell.pid && !exited) {
+ process.kill(-shell.pid, 'SIGKILL');
}
} catch (_e) {
- console.error(`failed to kill shell process ${shell.pid}: ${_e}`);
+ // if group kill fails, fall back to killing just the main process
+ try {
+ if (shell.pid) {
+ shell.kill('SIGKILL');
+ }
+ } catch (_e) {
+ console.error(`failed to kill shell process ${shell.pid}: ${_e}`);
+ }
}
}
}
@@ -272,26 +292,32 @@ export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
// parse pids (pgrep output) from temporary file and remove it
const backgroundPIDs: number[] = [];
- if (fs.existsSync(tempFilePath)) {
- const pgrepLines = fs
- .readFileSync(tempFilePath, 'utf8')
- .split('\n')
- .filter(Boolean);
- for (const line of pgrepLines) {
- if (!/^\d+$/.test(line)) {
- console.error(`pgrep: ${line}`);
+ if (os.platform() !== 'win32') {
+ const tempFileName = `shell_pgrep_${crypto
+ .randomBytes(6)
+ .toString('hex')}.tmp`;
+ const tempFilePath = path.join(os.tmpdir(), tempFileName);
+ if (fs.existsSync(tempFilePath)) {
+ const pgrepLines = fs
+ .readFileSync(tempFilePath, 'utf8')
+ .split('\n')
+ .filter(Boolean);
+ for (const line of pgrepLines) {
+ if (!/^\d+$/.test(line)) {
+ console.error(`pgrep: ${line}`);
+ }
+ const pid = Number(line);
+ // exclude the shell subprocess pid
+ if (pid !== shell.pid) {
+ backgroundPIDs.push(pid);
+ }
}
- const pid = Number(line);
- // exclude the shell subprocess pid
- if (pid !== shell.pid) {
- backgroundPIDs.push(pid);
+ fs.unlinkSync(tempFilePath);
+ } else {
+ if (!abortSignal.aborted) {
+ console.error('missing pgrep output');
}
}
- fs.unlinkSync(tempFilePath);
- } else {
- if (!abortSignal.aborted) {
- console.error('missing pgrep output');
- }
}
let llmContent = '';
diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts
index 74237c74..20917c0f 100644
--- a/packages/core/src/utils/editor.test.ts
+++ b/packages/core/src/utils/editor.test.ts
@@ -21,7 +21,11 @@ describe('checkHasEditor', () => {
it('should return true for vscode if "code" command exists', () => {
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/code'));
expect(checkHasEditor('vscode')).toBe(true);
- expect(execSync).toHaveBeenCalledWith('which code', { stdio: 'ignore' });
+ const expectedCommand =
+ process.platform === 'win32' ? 'where.exe code.cmd' : 'command -v code';
+ expect(execSync).toHaveBeenCalledWith(expectedCommand, {
+ stdio: 'ignore',
+ });
});
it('should return false for vscode if "code" command does not exist', () => {
@@ -34,7 +38,11 @@ describe('checkHasEditor', () => {
it('should return true for vim if "vim" command exists', () => {
(execSync as Mock).mockReturnValue(Buffer.from('/usr/bin/vim'));
expect(checkHasEditor('vim')).toBe(true);
- expect(execSync).toHaveBeenCalledWith('which vim', { stdio: 'ignore' });
+ const expectedCommand =
+ process.platform === 'win32' ? 'where.exe vim' : 'command -v vim';
+ expect(execSync).toHaveBeenCalledWith(expectedCommand, {
+ stdio: 'ignore',
+ });
});
it('should return false for vim if "vim" command does not exist', () => {
diff --git a/packages/core/src/utils/editor.ts b/packages/core/src/utils/editor.ts
index 447aa0d2..6be5cffb 100644
--- a/packages/core/src/utils/editor.ts
+++ b/packages/core/src/utils/editor.ts
@@ -15,7 +15,10 @@ interface DiffCommand {
function commandExists(cmd: string): boolean {
try {
- execSync(`which ${cmd}`, { stdio: 'ignore' });
+ execSync(
+ process.platform === 'win32' ? `where.exe ${cmd}` : `command -v ${cmd}`,
+ { stdio: 'ignore' },
+ );
return true;
} catch {
return false;
@@ -24,7 +27,9 @@ function commandExists(cmd: string): boolean {
export function checkHasEditor(editor: EditorType): boolean {
if (editor === 'vscode') {
- return commandExists('code');
+ return process.platform === 'win32'
+ ? commandExists('code.cmd')
+ : commandExists('code');
} else if (editor === 'vim') {
return commandExists('vim');
}
@@ -116,7 +121,10 @@ export async function openDiff(
});
} else {
// Use execSync for terminal-based editors like vim
- const command = `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`;
+ const command =
+ process.platform === 'win32'
+ ? `${diffCommand.command} ${diffCommand.args.join(' ')}`
+ : `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`;
execSync(command, {
stdio: 'inherit',
encoding: 'utf8',