Skip to content

Commit 80949db

Browse files
committed
feat: use bash for CLI command execution on all platforms
Switch from PowerShell/CMD to bash (Git Bash on Windows, native bash on Unix) for all CLI command execution. This provides consistent cross-platform behavior and eliminates Windows-specific issues. Key changes: - Add find_bash_windows() to locate Git Bash executable - Add run_bash_command() helper for cross-platform bash execution - Replace PowerShell execution with bash in execute_dependency() - Replace PowerShell/CMD with bash in configure_mcp_server() for HTTP transport - Unify Windows and Unix code paths to use run_bash_command() Benefits: - Consistent cross-platform CLI execution via bash - Fixes URL escaping issues with & characters in MCP server URLs - Eliminates PowerShell false-positive non-zero exit codes - Simpler and more maintainable codebase with unified execution logic
1 parent de3e33c commit 80949db

File tree

3 files changed

+236
-181
lines changed

3 files changed

+236
-181
lines changed

scripts/setup_environment.py

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,81 @@ def find_command_robust(cmd: str, fallback_paths: list[str] | None = None) -> st
436436
return None
437437

438438

439+
def find_bash_windows() -> str | None:
440+
"""Find Git Bash on Windows.
441+
442+
Git Bash is required for Claude Code on Windows and provides consistent
443+
cross-platform bash behavior for CLI command execution.
444+
445+
Returns:
446+
Full path to bash.exe if found, None otherwise.
447+
"""
448+
# Check CLAUDE_CODE_GIT_BASH_PATH env var
449+
env_path = os.environ.get('CLAUDE_CODE_GIT_BASH_PATH')
450+
if env_path and Path(env_path).exists():
451+
return str(Path(env_path).resolve())
452+
453+
# Check if bash is in PATH
454+
bash_path = find_command('bash.exe')
455+
if bash_path:
456+
return bash_path
457+
458+
# Check common locations
459+
common_paths = [
460+
r'C:\Program Files\Git\bin\bash.exe',
461+
r'C:\Program Files\Git\usr\bin\bash.exe',
462+
r'C:\Program Files (x86)\Git\bin\bash.exe',
463+
r'C:\Program Files (x86)\Git\usr\bin\bash.exe',
464+
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\bin\bash.exe'),
465+
os.path.expandvars(r'%LOCALAPPDATA%\Programs\Git\usr\bin\bash.exe'),
466+
]
467+
468+
for path in common_paths:
469+
expanded = os.path.expandvars(path)
470+
if Path(expanded).exists():
471+
return str(Path(expanded).resolve())
472+
473+
return None
474+
475+
476+
def run_bash_command(
477+
command: str,
478+
capture_output: bool = True,
479+
login_shell: bool = False,
480+
) -> subprocess.CompletedProcess[str]:
481+
"""Execute command via bash (Git Bash on Windows, native bash on Unix).
482+
483+
Provides consistent cross-platform behavior for CLI command execution.
484+
Uses Git Bash on Windows and native bash on Unix systems.
485+
486+
Args:
487+
command: The bash command string to execute
488+
capture_output: Whether to capture stdout/stderr
489+
login_shell: Whether to use login shell (-l flag)
490+
491+
Returns:
492+
subprocess.CompletedProcess with the result
493+
"""
494+
if sys.platform == 'win32':
495+
bash_path = find_bash_windows()
496+
else:
497+
bash_path = shutil.which('bash')
498+
499+
if not bash_path:
500+
error('Bash not found!')
501+
return subprocess.CompletedProcess([], 1, '', 'bash not found')
502+
503+
args = [bash_path]
504+
if login_shell:
505+
args.append('-l')
506+
args.extend(['-c', command])
507+
508+
try:
509+
return subprocess.run(args, capture_output=capture_output, text=True)
510+
except FileNotFoundError:
511+
return subprocess.CompletedProcess(args, 1, '', f'bash not found: {bash_path}')
512+
513+
439514
def expand_tildes_in_command(command: str) -> str:
440515
"""Expand tilde paths in a shell command.
441516
@@ -2544,7 +2619,8 @@ def execute_dependency(dep: str) -> bool:
25442619
parts_with_force = parts[:3] + ['--force'] + parts[3:]
25452620
result = run_command(parts_with_force, capture_output=False)
25462621
else:
2547-
result = run_command(['powershell', '-Command', dep], capture_output=False)
2622+
# Use bash for consistent cross-platform behavior
2623+
result = run_bash_command(dep, capture_output=False)
25482624
else:
25492625
if parts[0] == 'uv' and len(parts) >= 3 and parts[1] == 'tool' and parts[2] == 'install':
25502626
dep_with_force = dep.replace('uv tool install', 'uv tool install --force')
@@ -3338,72 +3414,35 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
33383414
if header:
33393415
base_cmd.extend(['--header', header])
33403416

3341-
# Try with PowerShell environment reload on Windows
3417+
# Windows HTTP transport - use bash for consistent cross-platform behavior
3418+
# This eliminates PowerShell's exit code quirks and CMD escaping issues
33423419
if system == 'Windows':
33433420
# Build explicit PATH including Node.js location
3344-
# This fixes Windows 10+ PATH propagation bug where MSI registry updates
3345-
# don't propagate to running processes via WM_SETTINGCHANGE
33463421
nodejs_path = r'C:\Program Files\nodejs'
33473422
current_path = os.environ.get('PATH', '')
33483423

3349-
# Ensure Node.js is in PATH
3424+
# Use POSIX path separator (:) since we're running in bash
33503425
if Path(nodejs_path).exists() and nodejs_path not in current_path:
3351-
explicit_path = f'{nodejs_path};{current_path}'
3426+
explicit_path = f'{nodejs_path}:{current_path}'
33523427
else:
33533428
explicit_path = current_path
33543429

3355-
# On Windows, we need to spawn a completely new shell process
3356-
# Use explicit PATH instead of reading from registry
3357-
# Build env flags for PowerShell command
33583430
env_flags = ' '.join(f'--env "{e}"' for e in env_list) if env_list else ''
33593431
env_part = f' {env_flags}' if env_flags else ''
3432+
header_part = f' --header "{header}"' if header else ''
33603433

3361-
ps_script = f'''
3362-
$env:Path = "{explicit_path}"
3363-
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} "{url}"
3364-
exit $LASTEXITCODE
3365-
'''
3366-
if header:
3367-
ps_script = f'''
3368-
$env:Path = "{explicit_path}"
3369-
& "{claude_cmd}" mcp add --scope {scope} {name}{env_part} --transport {transport} --header "{header}" "{url}"
3370-
exit $LASTEXITCODE
3371-
'''
3372-
result = run_command(
3373-
[
3374-
'powershell',
3375-
'-NoProfile',
3376-
'-Command',
3377-
ps_script,
3378-
],
3379-
capture_output=True,
3434+
bash_cmd = (
3435+
f'export PATH="{explicit_path}:$PATH" && '
3436+
f'"{claude_cmd}" mcp add --scope {scope} {name}{env_part} '
3437+
f'--transport {transport}{header_part} "{url}"'
33803438
)
33813439

3382-
# Also try with direct execution using shell=True
3383-
if result.returncode != 0:
3384-
info('Trying direct execution...')
3385-
# Use shell=True with double-quoted URL for Windows CMD compatibility
3386-
# This ensures & in URLs is not interpreted as a command separator
3387-
url_quoted = f'"{url}"'
3388-
header_part = f' --header "{header}"' if header else ''
3389-
cmd_str = (
3390-
f'"{claude_cmd}" mcp add --scope {scope} {name}{env_part} '
3391-
f'--transport {transport}{header_part} {url_quoted}'
3392-
)
3393-
result = subprocess.run(cmd_str, shell=True, capture_output=True, text=True)
3440+
result = run_bash_command(bash_cmd, capture_output=True, login_shell=True)
33943441
else:
3395-
# On Unix, spawn new bash with updated PATH
3442+
# On Unix, use bash with updated PATH (consistent with Windows)
33963443
parent_dir = Path(claude_cmd).parent
33973444
bash_cmd = f'export PATH="{parent_dir}:$PATH" && ' + ' '.join(shlex.quote(str(arg)) for arg in base_cmd)
3398-
result = run_command(
3399-
[
3400-
'bash',
3401-
'-l',
3402-
'-c',
3403-
bash_cmd,
3404-
],
3405-
capture_output=True,
3406-
)
3445+
result = run_bash_command(bash_cmd, capture_output=True, login_shell=True)
34073446
elif command:
34083447
# Stdio transport (command)
34093448

@@ -3454,20 +3493,18 @@ def configure_mcp_server(server: dict[str, Any]) -> bool:
34543493
time.sleep(2)
34553494

34563495
# Direct execution with full path
3457-
# On Windows with HTTP transport, use shell=True with double-quoted URL
3496+
# On Windows with HTTP transport, use bash for consistent behavior
34583497
if sys.platform == 'win32' and transport and url:
3459-
# Use shell=True with double-quoted URL for Windows CMD compatibility
3460-
# This ensures & in URLs is not interpreted as a command separator
3461-
url_quoted = f'"{url}"'
34623498
env_flags = ' '.join(f'--env "{e}"' for e in env_list) if env_list else ''
34633499
env_part_retry = f' {env_flags}' if env_flags else ''
34643500
header_part = f' --header "{header}"' if header else ''
3465-
cmd_str = (
3501+
3502+
bash_cmd = (
34663503
f'"{claude_cmd}" mcp add --scope {scope} {name}{env_part_retry} '
3467-
f'--transport {transport}{header_part} {url_quoted}'
3504+
f'--transport {transport}{header_part} "{url}"'
34683505
)
3469-
info(f'Retrying with direct command: {cmd_str}')
3470-
result = subprocess.run(cmd_str, shell=True, capture_output=False, text=True)
3506+
info(f'Retrying with bash command: {bash_cmd}')
3507+
result = run_bash_command(bash_cmd, capture_output=False)
34713508
else:
34723509
info(f"Retrying with direct command: {' '.join(str(arg) for arg in base_cmd)}")
34733510
result = run_command(base_cmd, capture_output=False) # Show output for debugging

0 commit comments

Comments
 (0)