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:
@@ -64,17 +64,35 @@ if (-not $hasPython) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Node.js v18+ (Claude Code requires v18+, v20/v22/v24 all work)
|
# Dynamic Node.js version detection from npm registry
|
||||||
$MIN_NODE_MAJOR = 18
|
$MIN_NODE_MAJOR = 18 # fallback
|
||||||
$nodeMajor = Get-NodeMajor
|
|
||||||
|
|
||||||
if ($nodeMajor -ge $MIN_NODE_MAJOR) {
|
function Get-RequiredNodeMajor {
|
||||||
|
# Try to detect required Node.js major version from npm registry
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri "https://registry.npmjs.org/@anthropic-ai/claude-code/latest" -UseBasicParsing -TimeoutSec 10
|
||||||
|
$engines = $response.engines
|
||||||
|
$nodeReq = $engines.node
|
||||||
|
|
||||||
|
# Parse ">=18.0.0" or "^24.0.0" etc → extract first number
|
||||||
|
if ($nodeReq -match '(\d+)') {
|
||||||
|
return [int]$matches[1]
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " Warning: Could not fetch Node.js requirement from npm, using fallback v$MIN_NODE_MAJOR" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
return $MIN_NODE_MAJOR
|
||||||
|
}
|
||||||
|
$nodeMajor = Get-NodeMajor
|
||||||
|
$requiredMajor = Get-RequiredNodeMajor
|
||||||
|
|
||||||
|
if ($nodeMajor -ge $requiredMajor) {
|
||||||
Write-Host " Node.js v$nodeMajor.x OK" -ForegroundColor Green
|
Write-Host " Node.js v$nodeMajor.x OK" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
if ($nodeMajor -gt 0) {
|
if ($nodeMajor -gt 0) {
|
||||||
Write-Host " Node.js v$nodeMajor found, need v$MIN_NODE_MAJOR+. Upgrading..." -ForegroundColor Yellow
|
Write-Host " Node.js v$nodeMajor found, need v$requiredMajor+. Upgrading..." -ForegroundColor Yellow
|
||||||
} else {
|
} else {
|
||||||
Write-Host " Node.js not found. Installing..." -ForegroundColor Yellow
|
Write-Host " Node.js not found. Installing v$requiredMajor+..." -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
$installed = $false
|
$installed = $false
|
||||||
@@ -84,7 +102,7 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) {
|
|||||||
Write-Host " Trying winget (Node.js LTS)..." -ForegroundColor Yellow
|
Write-Host " Trying winget (Node.js LTS)..." -ForegroundColor Yellow
|
||||||
winget install --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements -e 2>$null
|
winget install --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements -e 2>$null
|
||||||
Refresh-Path
|
Refresh-Path
|
||||||
if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) { $installed = $true }
|
if ((Get-NodeMajor) -ge $requiredMajor) { $installed = $true }
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Try Chocolatey
|
# 2. Try Chocolatey
|
||||||
@@ -92,27 +110,27 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) {
|
|||||||
Write-Host " Trying Chocolatey (nodejs-lts)..." -ForegroundColor Yellow
|
Write-Host " Trying Chocolatey (nodejs-lts)..." -ForegroundColor Yellow
|
||||||
choco install nodejs-lts -y 2>$null
|
choco install nodejs-lts -y 2>$null
|
||||||
Refresh-Path
|
Refresh-Path
|
||||||
if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) { $installed = $true }
|
if ((Get-NodeMajor) -ge $requiredMajor) { $installed = $true }
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. Direct MSI download — Node.js 22 LTS (most compatible)
|
# 3. Direct MSI download — dynamically pick the right major version
|
||||||
if (-not $installed) {
|
if (-not $installed) {
|
||||||
Write-Host " Downloading Node.js 22 LTS MSI..." -ForegroundColor Yellow
|
Write-Host " Downloading Node.js v$requiredMajor MSI..." -ForegroundColor Yellow
|
||||||
try {
|
try {
|
||||||
# Try to get latest LTS version dynamically, fallback to known good version
|
# Fetch latest release of the required major version from nodejs.org
|
||||||
$latestLts = "v22.14.0"
|
$latestForMajor = "v$requiredMajor.0.0"
|
||||||
try {
|
try {
|
||||||
$releases = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" -UseBasicParsing -TimeoutSec 10
|
$releases = Invoke-RestMethod -Uri "https://nodejs.org/dist/index.json" -UseBasicParsing -TimeoutSec 10
|
||||||
$lts = $releases | Where-Object { $_.lts -and $_.version -match "^v22\." } | Select-Object -First 1
|
$match = $releases | Where-Object { $_.version -match "^v$requiredMajor\." } | Select-Object -First 1
|
||||||
if ($lts) { $latestLts = $lts.version }
|
if ($match) { $latestForMajor = $match.version }
|
||||||
} catch {}
|
} catch {}
|
||||||
$msiUrl = "https://nodejs.org/dist/$latestLts/node-$latestLts-x64.msi"
|
$msiUrl = "https://nodejs.org/dist/$latestForMajor/node-$latestForMajor-x64.msi"
|
||||||
$msiFile = Join-Path $env:TEMP "node-lts.msi"
|
$msiFile = Join-Path $env:TEMP "node-v$requiredMajor.msi"
|
||||||
Invoke-WebRequest -Uri $msiUrl -OutFile $msiFile -UseBasicParsing
|
Invoke-WebRequest -Uri $msiUrl -OutFile $msiFile -UseBasicParsing
|
||||||
Start-Process msiexec.exe -ArgumentList "/i `"$msiFile`" /quiet /norestart" -Wait
|
Start-Process msiexec.exe -ArgumentList "/i `"$msiFile`" /quiet /norestart" -Wait
|
||||||
Refresh-Path
|
Refresh-Path
|
||||||
Remove-Item $msiFile -Force -ErrorAction SilentlyContinue
|
Remove-Item $msiFile -Force -ErrorAction SilentlyContinue
|
||||||
if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) { $installed = $true }
|
if ((Get-NodeMajor) -ge $requiredMajor) { $installed = $true }
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host " Download failed: $_" -ForegroundColor Red
|
Write-Host " Download failed: $_" -ForegroundColor Red
|
||||||
}
|
}
|
||||||
@@ -121,7 +139,7 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) {
|
|||||||
if (-not $installed) {
|
if (-not $installed) {
|
||||||
Write-Host "" -ForegroundColor Red
|
Write-Host "" -ForegroundColor Red
|
||||||
Write-Host " Could not install Node.js automatically." -ForegroundColor Red
|
Write-Host " Could not install Node.js automatically." -ForegroundColor Red
|
||||||
Write-Host " Install manually: https://nodejs.org/en/download/ (v20 LTS)" -ForegroundColor Yellow
|
Write-Host " Install manually: https://nodejs.org/en/download/ (v$requiredMajor+)" -ForegroundColor Yellow
|
||||||
Write-Host " Then re-run this script." -ForegroundColor Yellow
|
Write-Host " Then re-run this script." -ForegroundColor Yellow
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,32 @@ def run_cmd(cmd, **kwargs):
|
|||||||
# Node.js check and auto-install
|
# 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():
|
def get_node_version():
|
||||||
@@ -99,8 +124,12 @@ def get_node_version():
|
|||||||
|
|
||||||
|
|
||||||
def install_node():
|
def install_node():
|
||||||
"""Auto-install Node.js v24+ using the official nodesource setup (Linux) or brew (macOS)."""
|
"""Auto-install Node.js using the official nodesource setup (Linux) or brew (macOS).
|
||||||
print(f" {Y}Node.js v{'.'.join(map(str, MIN_NODE_VERSION))}+ required.{D}")
|
|
||||||
|
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:
|
if IS_WINDOWS:
|
||||||
print(f" {R}Please install Node.js manually: https://nodejs.org/{D}")
|
print(f" {R}Please install Node.js manually: https://nodejs.org/{D}")
|
||||||
@@ -116,7 +145,7 @@ def install_node():
|
|||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
ver = get_node_version()
|
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}")
|
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
|
||||||
return True
|
return True
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -124,8 +153,8 @@ def install_node():
|
|||||||
eprint(f" {R}Install Homebrew first: https://brew.sh/ then: brew install node{D}")
|
eprint(f" {R}Install Homebrew first: https://brew.sh/ then: brew install node{D}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Linux — nodesource
|
# Linux — nodesource with dynamic major version
|
||||||
print(f" Installing Node.js LTS via nodesource...")
|
print(f" Installing Node.js v{required_major} via nodesource...")
|
||||||
try:
|
try:
|
||||||
# Remove old nodesource list if present (prevents version conflicts)
|
# Remove old nodesource list if present (prevents version conflicts)
|
||||||
for old_list in ["/etc/apt/sources.list.d/nodesource.list"]:
|
for old_list in ["/etc/apt/sources.list.d/nodesource.list"]:
|
||||||
@@ -133,34 +162,34 @@ def install_node():
|
|||||||
os.remove(old_list)
|
os.remove(old_list)
|
||||||
|
|
||||||
result = run_cmd(
|
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,
|
timeout=180, capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
ver = get_node_version()
|
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}")
|
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
|
||||||
return True
|
return True
|
||||||
elif ver:
|
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
|
# Try dnf/yum fallback for RHEL/Fedora
|
||||||
for pkg_mgr in ["dnf", "yum"]:
|
for pkg_mgr in ["dnf", "yum"]:
|
||||||
if shutil.which(pkg_mgr):
|
if shutil.which(pkg_mgr):
|
||||||
result = run_cmd(
|
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,
|
timeout=180, capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
ver = get_node_version()
|
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}")
|
print(f" {G}Node.js v{'.'.join(map(str, ver))} installed{D}")
|
||||||
return True
|
return True
|
||||||
elif ver:
|
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
|
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:
|
if result.stderr:
|
||||||
eprint(f" {result.stderr.strip()[:200]}")
|
eprint(f" {result.stderr.strip()[:200]}")
|
||||||
return False
|
return False
|
||||||
@@ -172,6 +201,7 @@ def install_node():
|
|||||||
def ensure_node():
|
def ensure_node():
|
||||||
"""Check Node.js version, auto-install/update if needed. Returns True if OK."""
|
"""Check Node.js version, auto-install/update if needed. Returns True if OK."""
|
||||||
ver = get_node_version()
|
ver = get_node_version()
|
||||||
|
required_major = get_required_node_version()
|
||||||
|
|
||||||
if ver is None:
|
if ver is None:
|
||||||
print(f" {Y}Node.js not found.{D}")
|
print(f" {Y}Node.js not found.{D}")
|
||||||
@@ -180,23 +210,23 @@ def ensure_node():
|
|||||||
if ok:
|
if ok:
|
||||||
# Re-verify after install — PATH may now point to new binary
|
# Re-verify after install — PATH may now point to new binary
|
||||||
ver = get_node_version()
|
ver = get_node_version()
|
||||||
if ver and ver >= MIN_NODE_VERSION:
|
if ver and ver[0] >= required_major:
|
||||||
return True
|
return True
|
||||||
eprint(f" {R}Node.js still not available after install. Reopen shell or check PATH.{D}")
|
eprint(f" {R}Node.js still not available after install. Reopen shell or check PATH.{D}")
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
else:
|
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
|
return False
|
||||||
|
|
||||||
if ver < MIN_NODE_VERSION:
|
if ver[0] < required_major:
|
||||||
print(f" {Y}Node.js v{'.'.join(map(str, ver))} found, need v{'.'.join(map(str, MIN_NODE_VERSION))}+{D}")
|
print(f" {Y}Node.js v{'.'.join(map(str, ver))} found, need v{required_major}+{D}")
|
||||||
if is_admin():
|
if is_admin():
|
||||||
ok = install_node()
|
ok = install_node()
|
||||||
if ok:
|
if ok:
|
||||||
# Re-verify after upgrade — PATH may now point to new binary
|
# Re-verify after upgrade — PATH may now point to new binary
|
||||||
ver = get_node_version()
|
ver = get_node_version()
|
||||||
if ver and ver >= MIN_NODE_VERSION:
|
if ver and ver[0] >= required_major:
|
||||||
return True
|
return True
|
||||||
eprint(f" {R}Node.js version still insufficient after upgrade. Reopen shell or check PATH.{D}")
|
eprint(f" {R}Node.js version still insufficient after upgrade. Reopen shell or check PATH.{D}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user