From 5028914adaa91d41f33e1674bd309c077c9b9fd2 Mon Sep 17 00:00:00 2001 From: Orinks Date: Fri, 16 Jan 2026 20:04:57 -0500 Subject: [PATCH 1/2] feat: add Windows shell detection and cross-platform improvements - Add shared shell detection utility in common/src/util/detect-shell.ts - Update CLI to re-export shell detection from common - SDK now uses detected shell (PowerShell/cmd.exe/bash) instead of hardcoding - Report actual detected shell in systemInfo for better LLM guidance - Fix git commit syntax to use cross-platform format (no HEREDOC) - Add shell-specific Windows notes (PowerShell vs cmd.exe guidance) - Update WINDOWS.md documentation with shell detection info --- WINDOWS.md | 42 +++-- cli/src/utils/detect-shell.ts | 135 +++----------- .../tools/params/tool/run-terminal-command.ts | 23 +-- common/src/util/detect-shell.ts | 165 ++++++++++++++++++ .../src/system-prompt/prompts.ts | 44 ++++- sdk/src/run-state.ts | 3 +- sdk/src/tools/run-terminal-command.ts | 15 +- 7 files changed, 287 insertions(+), 140 deletions(-) create mode 100644 common/src/util/detect-shell.ts diff --git a/WINDOWS.md b/WINDOWS.md index 9d0414ddc..fc71686a6 100644 --- a/WINDOWS.md +++ b/WINDOWS.md @@ -79,34 +79,50 @@ Codebuff checks GitHub for the latest release on first run. This fails when: --- +### Shell Detection & PowerShell Support + +Codebuff now automatically detects your shell (PowerShell, cmd.exe, bash, etc.) and adapts its behavior accordingly. + +**PowerShell users benefit from:** +- Many Unix-like commands work natively (`mkdir`, `rm`, `cp`, `mv`, `ls`, `cat`) +- Better compatibility with cross-platform commands +- Codebuff generates shell-appropriate commands based on detection + +**To check your detected shell:** +Look at the "Shell:" line in the system info at the start of each conversation. + +**Recommendation:** Use PowerShell instead of cmd.exe for the best Windows experience. + +--- + ### Issue: Git Commands Fail on Windows **Symptom**: Git operations (commit, rebase, complex commands) fail with syntax errors or unexpected behavior. -**Cause**: -Codebuff uses Windows `cmd.exe` for command execution, which: -- Does not support bash syntax (HEREDOC, process substitution) -- Has limited quote escaping compared to bash -- Cannot execute complex git commands that work in Git Bash +**Cause** (now largely fixed): +Older versions of Codebuff used HEREDOC syntax for git commits, which doesn't work on Windows. This has been fixed - Codebuff now uses cross-platform compatible syntax. -**Solutions**: +**If you still experience issues:** + +1. **Ensure you have the latest Codebuff version**: + ```powershell + npm update -g codebuff + ``` -1. **Install Git for Windows** (if not already installed): +2. **Install Git for Windows** (if not already installed): - Download from https://git-scm.com/download/win - Ensures git commands are available in PATH -2. **Use Git Bash terminal** instead of PowerShell: - - Git Bash provides better compatibility with bash-style commands - - Launch Git Bash and run `codebuff` from there +3. **Use PowerShell** instead of cmd.exe: + - PowerShell has better command compatibility + - Codebuff detects PowerShell and adjusts accordingly -3. **Or use WSL (Windows Subsystem for Linux)**: +4. **Or use WSL (Windows Subsystem for Linux)**: - Provides full Linux environment with native bash - Install: `wsl --install` in PowerShell (Admin) - Run codebuff inside WSL for best compatibility -**Note**: Even when running in Git Bash, Codebuff spawns commands using `cmd.exe`. Using WSL provides the most reliable experience for git operations. - **Reference**: Issue [#274](https://github.com/CodebuffAI/codebuff/issues/274) --- diff --git a/cli/src/utils/detect-shell.ts b/cli/src/utils/detect-shell.ts index f86d0a407..74323dd4b 100644 --- a/cli/src/utils/detect-shell.ts +++ b/cli/src/utils/detect-shell.ts @@ -1,115 +1,32 @@ -import { execSync } from 'child_process' +/** + * CLI-specific shell detection wrapper. + * Re-exports from common/src/util/detect-shell.ts with CLI environment integration. + */ +import { + detectShell as detectShellFromCommon, + clearShellCache, + getShellArgs, + SHELL_COMMAND_ARGS, +} from '@codebuff/common/util/detect-shell' import type { CliEnv } from '../types/env' import { getCliEnv } from './env' -type KnownShell = - | 'bash' - | 'zsh' - | 'fish' - | 'cmd.exe' - | 'powershell' - | 'unknown' - -type ShellName = KnownShell | string - -let cachedShell: ShellName | null = null - -const SHELL_ALIASES: Record = { - bash: 'bash', - zsh: 'zsh', - fish: 'fish', - cmd: 'cmd.exe', - 'cmd.exe': 'cmd.exe', - pwsh: 'powershell', - powershell: 'powershell', - 'powershell.exe': 'powershell', -} - +import type { + KnownShell, + ShellName, + ShellDetectionEnv, +} from '@codebuff/common/util/detect-shell' + +// Re-export types and utilities from common +export type { KnownShell, ShellName, ShellDetectionEnv } +export { clearShellCache, getShellArgs, SHELL_COMMAND_ARGS } + +/** + * Detects the user's shell using CLI environment variables. + * This is a convenience wrapper around the common detectShell function + * that automatically uses the CLI environment. + */ export function detectShell(env: CliEnv = getCliEnv()): ShellName { - if (cachedShell) { - return cachedShell - } - - const detected = - detectFromEnvironment(env) ?? detectViaParentProcessInspection() ?? 'unknown' - cachedShell = detected - return detected -} - -function detectFromEnvironment(env: CliEnv): ShellName | null { - const candidates: Array = [] - - if (process.platform === 'win32') { - candidates.push(env.COMSPEC, env.SHELL) - } else { - candidates.push(env.SHELL) - } - - for (const candidate of candidates) { - const normalized = normalizeCandidate(candidate) - if (normalized) { - return normalized - } - } - - return null -} - -function detectViaParentProcessInspection(): ShellName | null { - try { - if (process.platform === 'win32') { - const parentProcess = execSync( - 'wmic process get ParentProcessId,CommandLine', - { stdio: 'pipe' }, - ) - .toString() - .toLowerCase() - - if (parentProcess.includes('powershell')) return 'powershell' - if (parentProcess.includes('cmd.exe')) return 'cmd.exe' - } else { - const parentProcess = execSync(`ps -p ${process.ppid} -o comm=`, { - stdio: 'pipe', - }) - .toString() - .trim() - const normalized = normalizeCandidate(parentProcess) - if (normalized) return normalized - } - } catch { - // Ignore inspection errors - } - - return null -} - -function normalizeCandidate(value?: string | null): ShellName | null { - if (!value) { - return null - } - - const trimmed = value.trim() - if (!trimmed) { - return null - } - - const lower = trimmed.toLowerCase() - const parts = lower.split(/[/\\]/) - const last = parts.pop() ?? lower - const base = last.endsWith('.exe') ? last.slice(0, -4) : last - - if (SHELL_ALIASES[base]) { - return SHELL_ALIASES[base] - } - - if (SHELL_ALIASES[last]) { - return SHELL_ALIASES[last] - } - - if (base.endsWith('sh')) { - return base - } - - return null + return detectShellFromCommon(env) } diff --git a/common/src/tools/params/tool/run-terminal-command.ts b/common/src/tools/params/tool/run-terminal-command.ts index 4bd53f0c2..b1509632c 100644 --- a/common/src/tools/params/tool/run-terminal-command.ts +++ b/common/src/tools/params/tool/run-terminal-command.ts @@ -62,20 +62,8 @@ When the user requests a new git commit, please follow these steps closely: Generated with Codebuff 🤖 Co-Authored-By: Codebuff \`\`\` - To maintain proper formatting, use cross-platform compatible commit messages: - **For Unix/bash shells:** - \`\`\` - git commit -m "$(cat <<'EOF' - Your commit message here. - - 🤖 Generated with Codebuff - Co-Authored-By: Codebuff - EOF - )" - \`\`\` - - **For Windows Command Prompt:** + **For bash/PowerShell:** Use multi-line \`-m\` format: \`\`\` git commit -m "Your commit message here. @@ -83,7 +71,14 @@ When the user requests a new git commit, please follow these steps closely: Co-Authored-By: Codebuff " \`\`\` - Always detect the platform and use the appropriate syntax. HEREDOC syntax (\`<<'EOF'\`) only works in bash/Unix shells and will fail on Windows Command Prompt. + **For cmd.exe (Windows Command Prompt):** Use the file-based approach to avoid escaping issues: + 1. Use \`write_file\` to create \`.codebuff-commit-msg.txt\` with the full commit message + 2. Run \`git commit -F .codebuff-commit-msg.txt\` + 3. Run \`del .codebuff-commit-msg.txt\` to clean up + + This file-based approach completely avoids cmd.exe's complex escaping rules for quotes, newlines, and special characters. + + **Important:** Do NOT use HEREDOC syntax (\`<<'EOF'\`) - it only works in bash and will fail on Windows. **Important details** diff --git a/common/src/util/detect-shell.ts b/common/src/util/detect-shell.ts new file mode 100644 index 000000000..ba59d13cb --- /dev/null +++ b/common/src/util/detect-shell.ts @@ -0,0 +1,165 @@ +import { execSync } from 'child_process' + +export type KnownShell = + | 'bash' + | 'zsh' + | 'fish' + | 'cmd.exe' + | 'powershell' + | 'unknown' + +export type ShellName = KnownShell | string + +/** + * Environment variables used for shell detection. + * This is a subset that works across CLI and SDK contexts. + * The index signature allows compatibility with NodeJS.ProcessEnv. + */ +export interface ShellDetectionEnv { + SHELL?: string + COMSPEC?: string + [key: string]: string | undefined +} + +const SHELL_ALIASES: Record = { + bash: 'bash', + zsh: 'zsh', + fish: 'fish', + cmd: 'cmd.exe', + 'cmd.exe': 'cmd.exe', + pwsh: 'powershell', + powershell: 'powershell', + 'powershell.exe': 'powershell', +} + +/** + * Shell arguments for command execution. + * Maps shell names to their command-line argument for executing a command string. + */ +export const SHELL_COMMAND_ARGS: Record = { + 'cmd.exe': ['/c'], + powershell: ['-Command'], + bash: ['-c'], + zsh: ['-c'], + fish: ['-c'], + unknown: ['-c'], // Default to Unix-style +} + +/** + * Get the command-line arguments needed to execute a command in the given shell. + */ +export function getShellArgs(shell: ShellName): string[] { + return SHELL_COMMAND_ARGS[shell] ?? SHELL_COMMAND_ARGS['unknown'] +} + +let cachedShell: ShellName | null = null + +/** + * Detects the user's shell from environment variables and parent process inspection. + * Results are cached for the lifetime of the process. + * + * @param env - Environment variables to use for detection (defaults to process.env) + * @param useCache - Whether to use cached result (defaults to true) + */ +export function detectShell( + env: ShellDetectionEnv = process.env, + useCache: boolean = true, +): ShellName { + if (useCache && cachedShell) { + return cachedShell + } + + const detected = + detectFromEnvironment(env) ?? detectViaParentProcessInspection() ?? 'unknown' + + if (useCache) { + cachedShell = detected + } + + return detected +} + +/** + * Clears the cached shell detection result. + * Useful for testing or when the shell might have changed. + */ +export function clearShellCache(): void { + cachedShell = null +} + +function detectFromEnvironment(env: ShellDetectionEnv): ShellName | null { + const candidates: Array = [] + + if (process.platform === 'win32') { + candidates.push(env.COMSPEC, env.SHELL) + } else { + candidates.push(env.SHELL) + } + + for (const candidate of candidates) { + const normalized = normalizeCandidate(candidate) + if (normalized) { + return normalized + } + } + + return null +} + +function detectViaParentProcessInspection(): ShellName | null { + try { + if (process.platform === 'win32') { + const parentProcess = execSync( + 'wmic process get ParentProcessId,CommandLine', + { stdio: 'pipe' }, + ) + .toString() + .toLowerCase() + + if (parentProcess.includes('powershell')) return 'powershell' + if (parentProcess.includes('cmd.exe')) return 'cmd.exe' + } else { + const parentProcess = execSync(`ps -p ${process.ppid} -o comm=`, { + stdio: 'pipe', + }) + .toString() + .trim() + const normalized = normalizeCandidate(parentProcess) + if (normalized) return normalized + } + } catch { + // Ignore inspection errors + } + + return null +} + +function normalizeCandidate(value?: string | null): ShellName | null { + if (!value) { + return null + } + + const trimmed = value.trim() + if (!trimmed) { + return null + } + + const lower = trimmed.toLowerCase() + const parts = lower.split(/[/\\]/) + const last = parts.pop() ?? lower + const base = last.endsWith('.exe') ? last.slice(0, -4) : last + + if (SHELL_ALIASES[base]) { + return SHELL_ALIASES[base] + } + + if (SHELL_ALIASES[last]) { + return SHELL_ALIASES[last] + } + + if (base.endsWith('sh')) { + return base + } + + return null +} diff --git a/packages/agent-runtime/src/system-prompt/prompts.ts b/packages/agent-runtime/src/system-prompt/prompts.ts index 13add3df6..1bc47f0de 100644 --- a/packages/agent-runtime/src/system-prompt/prompts.ts +++ b/packages/agent-runtime/src/system-prompt/prompts.ts @@ -158,6 +158,43 @@ ${truncationNote} `.trim() } +/** + * Get Windows-specific notes based on the detected shell. + * PowerShell supports many Unix-like commands, so guidance differs from cmd.exe. + */ +function getWindowsNote(shell: string): string { + if (shell === 'powershell') { + return ` +Note: The user is running PowerShell on Windows. +PowerShell supports many Unix-like commands: \`mkdir\`, \`rm\`, \`cp\`, \`mv\`, \`ls\`, \`cat\` all work. +However, some differences remain: +- Use \`Select-String\` instead of \`grep\` for text searching, or install ripgrep (\`rg\`) +- Use \`Get-ChildItem\` or \`ls\` for directory listing +- Multi-line strings in commands work with double quotes +- HEREDOC syntax (\`<<'EOF'\`) does NOT work - use simple multi-line strings instead +`.trim() + } + + // Default cmd.exe note + return ` +Note: The user is running Windows Command Prompt (cmd.exe). +Many Unix commands are different on cmd.exe: +- Use \`mkdir\` instead of \`mkdir -p\` (mkdir creates parent dirs automatically on Windows) +- Use \`findstr\` instead of \`grep\` +- Use \`dir\` instead of \`ls\` +- Use \`move\` instead of \`mv\` +- Use \`del\` instead of \`rm\` +- Use \`copy\` instead of \`cp\` +- Use \`type\` instead of \`cat\` +- HEREDOC syntax (\`<<'EOF'\`) does NOT work +- **cmd.exe has complex escaping rules** - for commands with quotes, special chars (& | < > ^), or multi-line strings: + - Use the \`write_file\` tool to create a temp file with the content, then reference it + - For git commits: write message to \`.codebuff-commit-msg.txt\`, run \`git commit -F .codebuff-commit-msg.txt\`, then \`del .codebuff-commit-msg.txt\` + - This file-based approach completely avoids escaping issues +`.trim() +} + +// Kept for backwards compatibility - used when shell is not known const windowsNote = ` Note: many commands in the terminal are different on Windows. For example, the mkdir command is \`mkdir\` instead of \`mkdir -p\`. Instead of grep, use \`findstr\`. Instead of \`ls\` use \`dir\` to list files. Instead of \`mv\` use \`move\`. Instead of \`rm\` use \`del\`. Instead of \`cp\` use \`copy\`. Unless the user is in Powershell, in which case you should use the Powershell commands instead. @@ -167,12 +204,17 @@ export const getSystemInfoPrompt = (fileContext: ProjectFileContext) => { const { fileTree, shellConfigFiles, systemInfo } = fileContext const flattenedNodes = flattenTree(fileTree) const lastReadFilePaths = getLastReadFilePaths(flattenedNodes, 20) + + // Use shell-specific Windows notes when available + const windowsNotes = systemInfo.platform === 'win32' + ? getWindowsNote(systemInfo.shell) + '\n' + : '' return ` # System Info Operating System: ${systemInfo.platform} -${systemInfo.platform === 'win32' ? windowsNote + '\n' : ''} +${windowsNotes} Shell: ${systemInfo.shell} diff --git a/sdk/src/run-state.ts b/sdk/src/run-state.ts index 14676ea34..2a29c5dd9 100644 --- a/sdk/src/run-state.ts +++ b/sdk/src/run-state.ts @@ -12,6 +12,7 @@ import { getAllFilePaths, } from '@codebuff/common/project-file-tree' import { getInitialSessionState } from '@codebuff/common/types/session-state' +import { detectShell } from '@codebuff/common/util/detect-shell' import { getErrorObject } from '@codebuff/common/util/error' import { cloneDeep } from 'lodash' import z from 'zod/v4' @@ -502,7 +503,7 @@ export async function initialSessionState( shellConfigFiles: {}, systemInfo: { platform: process.platform, - shell: process.platform === 'win32' ? 'cmd.exe' : 'bash', + shell: detectShell(), nodeVersion: process.version, arch: process.arch, homedir: os.homedir(), diff --git a/sdk/src/tools/run-terminal-command.ts b/sdk/src/tools/run-terminal-command.ts index dd2c974b9..0cc38775f 100644 --- a/sdk/src/tools/run-terminal-command.ts +++ b/sdk/src/tools/run-terminal-command.ts @@ -7,6 +7,10 @@ import { stripColors, truncateStringWithMessage, } from '../../../common/src/util/string' +import { + detectShell, + getShellArgs, +} from '@codebuff/common/util/detect-shell' import type { CodebuffToolOutput } from '../../../common/src/tools/list' @@ -31,8 +35,15 @@ export function runTerminalCommand({ return new Promise((resolve, reject) => { const isWindows = os.platform() === 'win32' - const shell = isWindows ? 'cmd.exe' : 'bash' - const shellArgs = isWindows ? ['/c'] : ['-c'] + // Detect the user's actual shell instead of hardcoding cmd.exe/bash + const detectedShell = detectShell() + // Map shell names to executables (powershell needs special handling) + const shell = detectedShell === 'powershell' + ? (isWindows ? 'powershell.exe' : 'pwsh') + : (detectedShell === 'unknown' + ? (isWindows ? 'cmd.exe' : 'bash') + : detectedShell) + const shellArgs = getShellArgs(detectedShell) // Resolve cwd to absolute path const resolvedCwd = path.resolve(cwd) From f3da2ab8ba88aed713a12314132557be922fa75a Mon Sep 17 00:00:00 2001 From: Orinks Date: Fri, 16 Jan 2026 20:24:56 -0500 Subject: [PATCH 2/2] feat: add explicit cmd.exe escaping rules to system prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add detailed escaping rules for special chars (^ for & | < > ^, %% for %) - Explain that nested quotes and multi-line strings are unreliable - Recommend file-based approach for complex content - Add practical examples for git commits and complex echo 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../src/system-prompt/prompts.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/agent-runtime/src/system-prompt/prompts.ts b/packages/agent-runtime/src/system-prompt/prompts.ts index 1bc47f0de..c4bb18471 100644 --- a/packages/agent-runtime/src/system-prompt/prompts.ts +++ b/packages/agent-runtime/src/system-prompt/prompts.ts @@ -178,7 +178,8 @@ However, some differences remain: // Default cmd.exe note return ` Note: The user is running Windows Command Prompt (cmd.exe). -Many Unix commands are different on cmd.exe: + +**Command equivalents:** - Use \`mkdir\` instead of \`mkdir -p\` (mkdir creates parent dirs automatically on Windows) - Use \`findstr\` instead of \`grep\` - Use \`dir\` instead of \`ls\` @@ -186,11 +187,23 @@ Many Unix commands are different on cmd.exe: - Use \`del\` instead of \`rm\` - Use \`copy\` instead of \`cp\` - Use \`type\` instead of \`cat\` -- HEREDOC syntax (\`<<'EOF'\`) does NOT work -- **cmd.exe has complex escaping rules** - for commands with quotes, special chars (& | < > ^), or multi-line strings: - - Use the \`write_file\` tool to create a temp file with the content, then reference it - - For git commits: write message to \`.codebuff-commit-msg.txt\`, run \`git commit -F .codebuff-commit-msg.txt\`, then \`del .codebuff-commit-msg.txt\` - - This file-based approach completely avoids escaping issues + +**cmd.exe escaping rules (IMPORTANT):** +- Escape special characters \`& | < > ^\` with a caret: \`^&\`, \`^|\`, \`^<\`, \`^>\`, \`^^\` +- For literal \`%\`, use \`%%\` (e.g., \`echo 50%% complete\`) +- Double quotes work for strings, but nested quotes are complex - avoid when possible +- HEREDOC syntax (\`<<'EOF'\`) does NOT work at all +- Multi-line strings in commands are unreliable - use file-based approach instead + +**Recommended: File-based approach for complex content:** +For commands involving quotes, special characters, or multi-line content, avoid escaping entirely: +1. Use \`write_file\` tool to create a temp file with the content +2. Run the command referencing that file +3. Delete the temp file with \`del\` + +Examples: +- **Git commits:** write message to \`.codebuff-commit-msg.txt\`, run \`git commit -F .codebuff-commit-msg.txt\`, then \`del .codebuff-commit-msg.txt\` +- **Complex echo:** write content to \`.codebuff-temp.txt\`, run \`type .codebuff-temp.txt\`, then \`del .codebuff-temp.txt\` `.trim() }