diff --git a/claude/uclaude_update.ps1 b/claude/uclaude_update.ps1 index 8b87230..f0364cd 100644 --- a/claude/uclaude_update.ps1 +++ b/claude/uclaude_update.ps1 @@ -64,17 +64,35 @@ if (-not $hasPython) { } } -# Node.js v18+ (Claude Code requires v18+, v20/v22/v24 all work) -$MIN_NODE_MAJOR = 18 -$nodeMajor = Get-NodeMajor +# Dynamic Node.js version detection from npm registry +$MIN_NODE_MAJOR = 18 # fallback -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 } else { 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 { - Write-Host " Node.js not found. Installing..." -ForegroundColor Yellow + Write-Host " Node.js not found. Installing v$requiredMajor+..." -ForegroundColor Yellow } $installed = $false @@ -84,7 +102,7 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) { Write-Host " Trying winget (Node.js LTS)..." -ForegroundColor Yellow winget install --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements -e 2>$null Refresh-Path - if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) { $installed = $true } + if ((Get-NodeMajor) -ge $requiredMajor) { $installed = $true } } # 2. Try Chocolatey @@ -92,27 +110,27 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) { Write-Host " Trying Chocolatey (nodejs-lts)..." -ForegroundColor Yellow choco install nodejs-lts -y 2>$null 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) { - Write-Host " Downloading Node.js 22 LTS MSI..." -ForegroundColor Yellow + Write-Host " Downloading Node.js v$requiredMajor MSI..." -ForegroundColor Yellow try { - # Try to get latest LTS version dynamically, fallback to known good version - $latestLts = "v22.14.0" + # Fetch latest release of the required major version from nodejs.org + $latestForMajor = "v$requiredMajor.0.0" try { $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 - if ($lts) { $latestLts = $lts.version } + $match = $releases | Where-Object { $_.version -match "^v$requiredMajor\." } | Select-Object -First 1 + if ($match) { $latestForMajor = $match.version } } catch {} - $msiUrl = "https://nodejs.org/dist/$latestLts/node-$latestLts-x64.msi" - $msiFile = Join-Path $env:TEMP "node-lts.msi" + $msiUrl = "https://nodejs.org/dist/$latestForMajor/node-$latestForMajor-x64.msi" + $msiFile = Join-Path $env:TEMP "node-v$requiredMajor.msi" Invoke-WebRequest -Uri $msiUrl -OutFile $msiFile -UseBasicParsing Start-Process msiexec.exe -ArgumentList "/i `"$msiFile`" /quiet /norestart" -Wait Refresh-Path Remove-Item $msiFile -Force -ErrorAction SilentlyContinue - if ((Get-NodeMajor) -ge $MIN_NODE_MAJOR) { $installed = $true } + if ((Get-NodeMajor) -ge $requiredMajor) { $installed = $true } } catch { Write-Host " Download failed: $_" -ForegroundColor Red } @@ -121,7 +139,7 @@ if ($nodeMajor -ge $MIN_NODE_MAJOR) { if (-not $installed) { Write-Host "" -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 exit 1 } diff --git a/claude/uclaude_updater.py b/claude/uclaude_updater.py index 1076032..3182926 100644 --- a/claude/uclaude_updater.py +++ b/claude/uclaude_updater.py @@ -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