fix: use shell=True on Windows for subprocess calls (npm/claude are .cmd)

Without shell=True, subprocess.run(["npm", ...]) fails on Windows because
npm and claude are .cmd batch files, not .exe. Added run_cmd() wrapper that
sets shell=True only on Windows, keeping Linux/macOS behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 06:24:24 -05:00
parent 4ca665f26f
commit c466223882

View File

@@ -69,6 +69,13 @@ def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def run_cmd(cmd, **kwargs):
"""run_cmd wrapper: uses shell=True on Windows for .cmd/.bat commands (npm, claude, etc.)."""
if IS_WINDOWS:
kwargs.setdefault("shell", True)
return subprocess.run(cmd, **kwargs)
# ============================================================
# Node.js check and auto-install
# ============================================================
@@ -79,7 +86,7 @@ MIN_NODE_VERSION = (24, 13, 0)
def get_node_version():
"""Get installed Node.js version as tuple, or None."""
try:
result = subprocess.run(
result = run_cmd(
["node", "--version"],
capture_output=True, text=True, timeout=10,
)
@@ -103,7 +110,7 @@ def install_node():
if IS_MACOS:
print(f" Installing Node.js via Homebrew...")
try:
result = subprocess.run(
result = run_cmd(
["brew", "install", "node"],
timeout=120, capture_output=True, text=True,
)
@@ -125,7 +132,7 @@ def install_node():
if os.path.isfile(old_list):
os.remove(old_list)
result = subprocess.run(
result = run_cmd(
["bash", "-c", "curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt-get install -y nodejs"],
timeout=180, capture_output=True, text=True,
)
@@ -140,7 +147,7 @@ def install_node():
# Try dnf/yum fallback for RHEL/Fedora
for pkg_mgr in ["dnf", "yum"]:
if shutil.which(pkg_mgr):
result = subprocess.run(
result = run_cmd(
["bash", "-c", f"curl -fsSL https://rpm.nodesource.com/setup_24.x | bash - && {pkg_mgr} install -y nodejs"],
timeout=180, capture_output=True, text=True,
)
@@ -213,7 +220,7 @@ def ensure_claude_code(target_version=None):
pkg = "@anthropic-ai/claude-code" + (f"@{target_version}" if target_version else "")
try:
result = subprocess.run(
result = run_cmd(
["npm", "install", "-g", pkg],
capture_output=True, text=True, timeout=300,
)
@@ -291,7 +298,7 @@ def get_installed_version():
# 2. claude --version
try:
result = subprocess.run(
result = run_cmd(
["claude", "--version"],
capture_output=True, text=True, timeout=10,
)
@@ -362,13 +369,13 @@ def git_pull():
"""Pull latest changes from remote (shallow fetch for minimal download)."""
try:
# Shallow fetch + reset — downloads only latest commit, not full history
result = subprocess.run(
result = run_cmd(
["git", "fetch", "--depth", "1", "origin", "master"],
cwd=REPO_ROOT, capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
# Fallback to regular pull
result = subprocess.run(
result = run_cmd(
["git", "pull", "--quiet"],
cwd=REPO_ROOT, capture_output=True, text=True, timeout=60,
)
@@ -377,7 +384,7 @@ def git_pull():
return True
# Reset to fetched state
subprocess.run(
run_cmd(
["git", "reset", "--hard", "origin/master"],
cwd=REPO_ROOT, capture_output=True, text=True, timeout=10,
)
@@ -414,7 +421,7 @@ def _setup_sparse_checkout():
return
# Enable sparse checkout
subprocess.run(
run_cmd(
["git", "config", "core.sparseCheckout", "true"],
cwd=REPO_ROOT, capture_output=True,
)
@@ -432,7 +439,7 @@ def _setup_sparse_checkout():
f.write("\n".join(patterns) + "\n")
# Apply sparse checkout
subprocess.run(
run_cmd(
["git", "checkout", "HEAD", "--", "."],
cwd=REPO_ROOT, capture_output=True, timeout=30,
)
@@ -461,7 +468,7 @@ def install_cli_js(version, cli_js_path):
os.chmod(cli_js_path, 0o755)
# Syntax check
result = subprocess.run(
result = run_cmd(
["node", "--check", cli_js_path],
capture_output=True, text=True, timeout=30,
)
@@ -748,7 +755,7 @@ def _set_user_env_windows(config):
for key, val in env_vars.items():
try:
subprocess.run(["setx", key, val], capture_output=True, check=True)
run_cmd(["setx", key, val], capture_output=True, check=True)
os.environ[key] = val
except Exception:
pass