feat: dynamic Node.js version detection from npm registry

- Add get_required_node_version() — fetches engines.node from npm registry
  for @anthropic-ai/claude-code@latest and extracts required major version
- install_node() now installs setup_{major}.x matching Claude Code requirement
- ensure_node() uses dynamic version for checks and error messages
- Windows: Get-RequiredNodeMajor fetches version from npm registry
- Windows: MSI download uses required major version, not hardcoded v22
- Fallback to MIN_NODE_VERSION (18) if npm registry is unreachable
- Future-proof: when Claude Code raises requirement to 26+, scripts auto-adapt

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
delta-cloud-208e
2026-02-26 18:47:08 +00:00
parent b50ad05d93
commit 2d34473378
2 changed files with 84 additions and 36 deletions

View File

@@ -80,7 +80,32 @@ def run_cmd(cmd, **kwargs):
# Node.js check and auto-install
# ============================================================
MIN_NODE_VERSION = (18, 0, 0)
MIN_NODE_VERSION = (18, 0, 0) # fallback; dynamically updated from npm registry
def get_required_node_version():
"""Detect the Node.js version required by Claude Code from npm registry.
Returns the minimum major version as integer (e.g. 24), or falls back to
MIN_NODE_VERSION[0] if detection fails.
"""
try:
import urllib.request
req = urllib.request.Request(
"https://registry.npmjs.org/@anthropic-ai/claude-code/latest",
headers={"Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode("utf-8"))
engines = data.get("engines", {})
node_req = engines.get("node", "")
# Parse ">=18.0.0" or "^24.0.0" etc → extract first number
m = re.search(r"(\d+)", node_req)
if m:
return int(m.group(1))
except Exception:
pass
return MIN_NODE_VERSION[0]
def get_node_version():
@@ -99,8 +124,12 @@ def get_node_version():
def install_node():
"""Auto-install Node.js v24+ using the official nodesource setup (Linux) or brew (macOS)."""
print(f" {Y}Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+ required.{D}")
"""Auto-install Node.js using the official nodesource setup (Linux) or brew (macOS).
Dynamically detects required major version from npm registry.
"""
required_major = get_required_node_version()
print(f" {Y}Node.js v{required_major}+ required.{D}")
if IS_WINDOWS:
print(f" {R}Please install Node.js manually: https://nodejs.org/{D}")
@@ -116,7 +145,7 @@ def install_node():
)
if result.returncode == 0:
ver = get_node_version()
if ver and ver >= MIN_NODE_VERSION:
if ver and ver[0] >= required_major:
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
return True
except FileNotFoundError:
@@ -124,8 +153,8 @@ def install_node():
eprint(f" {R}Install Homebrew first: https://brew.sh/ then: brew install node{D}")
return False
# Linux — nodesource
print(f" Installing Node.js LTS via nodesource...")
# Linux — nodesource with dynamic major version
print(f" Installing Node.js v{required_major} via nodesource...")
try:
# Remove old nodesource list if present (prevents version conflicts)
for old_list in ["/etc/apt/sources.list.d/nodesource.list"]:
@@ -133,34 +162,34 @@ def install_node():
os.remove(old_list)
result = run_cmd(
["bash", "-c", "curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get remove -y nodejs || true && apt-get install -y nodejs"],
["bash", "-c", f"curl -fsSL https://deb.nodesource.com/setup_{required_major}.x | bash - && apt-get remove -y nodejs || true && apt-get install -y nodejs"],
timeout=180, capture_output=True, text=True,
)
if result.returncode == 0:
ver = get_node_version()
if ver and ver >= MIN_NODE_VERSION:
if ver and ver[0] >= required_major:
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
return True
elif ver:
eprint(f" {Y}nodesource installed v{'.'.join(map(str, ver))} but need v{'.'.join(map(str, MIN_NODE_VERSION))}+{D}")
eprint(f" {Y}nodesource installed v{'.'.join(map(str, ver))} but need v{required_major}+{D}")
# Try dnf/yum fallback for RHEL/Fedora
for pkg_mgr in ["dnf", "yum"]:
if shutil.which(pkg_mgr):
result = run_cmd(
["bash", "-c", f"curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - && {pkg_mgr} install -y nodejs"],
["bash", "-c", f"curl -fsSL https://rpm.nodesource.com/setup_{required_major}.x | bash - && {pkg_mgr} install -y nodejs"],
timeout=180, capture_output=True, text=True,
)
if result.returncode == 0:
ver = get_node_version()
if ver and ver >= MIN_NODE_VERSION:
if ver and ver[0] >= required_major:
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
return True
elif ver:
eprint(f" {Y}Installed v{'.'.join(map(str, ver))} but need v{'.'.join(map(str, MIN_NODE_VERSION))}+{D}")
eprint(f" {Y}Installed v{'.'.join(map(str, ver))} but need v{required_major}+{D}")
break
eprint(f" {R}Auto-install failed. Install Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+ manually: https://nodejs.org/{D}")
eprint(f" {R}Auto-install failed. Install Node.js v{required_major}+ manually: https://nodejs.org/{D}")
if result.stderr:
eprint(f" {result.stderr.strip()[:200]}")
return False
@@ -172,6 +201,7 @@ def install_node():
def ensure_node():
"""Check Node.js version, auto-install/update if needed. Returns True if OK."""
ver = get_node_version()
required_major = get_required_node_version()
if ver is None:
print(f" {Y}Node.js not found.{D}")
@@ -180,23 +210,23 @@ def ensure_node():
if ok:
# Re-verify after install — PATH may now point to new binary
ver = get_node_version()
if ver and ver >= MIN_NODE_VERSION:
if ver and ver[0] >= required_major:
return True
eprint(f" {R}Node.js still not available after install. Reopen shell or check PATH.{D}")
return False
return False
else:
eprint(f" {R}Install Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+: https://nodejs.org/{D}")
eprint(f" {R}Install Node.js v{required_major}+: https://nodejs.org/{D}")
return False
if ver < MIN_NODE_VERSION:
print(f" {Y}Node.js v{'.'.join(map(str, ver))} found, need v{'.'.join(map(str, MIN_NODE_VERSION))}+{D}")
if ver[0] < required_major:
print(f" {Y}Node.js v{'.'.join(map(str, ver))} found, need v{required_major}+{D}")
if is_admin():
ok = install_node()
if ok:
# Re-verify after upgrade — PATH may now point to new binary
ver = get_node_version()
if ver and ver >= MIN_NODE_VERSION:
if ver and ver[0] >= required_major:
return True
eprint(f" {R}Node.js version still insufficient after upgrade. Reopen shell or check PATH.{D}")
return False