param([string]$BaseUrl = 'https://rootexai.com', [switch]$Offline) $ErrorActionPreference = 'Stop' if (-not [Uri]::IsWellFormedUriString($BaseUrl,[UriKind]::Absolute) -or ([Uri]$BaseUrl).Scheme -notin @('http','https')) { throw 'Use your rootexai HTTP(S) website URL.' } $npmCommand = Get-Command npm.cmd -ErrorAction Stop $nodeCommand = Get-Command node.exe -ErrorAction Stop & $nodeCommand.Source -e 'if (Number(process.versions.node.split(/[.]/)[0]) < 20) process.exit(1)' if ($LASTEXITCODE -ne 0) { throw 'Install Node.js 20 or newer first.' } $rootexOrigin = $BaseUrl.TrimEnd('/') $rootexManifest = Invoke-RestMethod -Uri ($rootexOrigin + '/downloads/latest.json?build=' + [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) if ($rootexManifest.package -notmatch '^rootex-cli-\d+\.\d+\.\d+\.tgz$' -or $rootexManifest.sha256 -notmatch '^[a-fA-F0-9]{64}$') { throw 'Invalid release manifest.' } $rootexTempDir = Join-Path ([IO.Path]::GetTempPath()) ('rootex-install-' + [guid]::NewGuid().ToString()) New-Item -ItemType Directory -Path $rootexTempDir | Out-Null $rootexPackagePath = Join-Path $rootexTempDir $rootexManifest.package try { Invoke-WebRequest -UseBasicParsing -Uri ($rootexOrigin + '/downloads/' + $rootexManifest.package) -OutFile $rootexPackagePath if ((Get-FileHash -LiteralPath $rootexPackagePath -Algorithm SHA256).Hash -ne $rootexManifest.sha256) { throw 'Package checksum mismatch. Installation stopped.' } if ($Offline) { & $npmCommand.Source install --global --offline --ignore-scripts --no-audit --no-fund $rootexPackagePath } else { & $npmCommand.Source install --global $rootexPackagePath } if ($LASTEXITCODE -ne 0) { throw 'npm installation failed.' } } finally { if (Test-Path -LiteralPath $rootexPackagePath -PathType Leaf) { Remove-Item -LiteralPath $rootexPackagePath } if (Test-Path -LiteralPath $rootexTempDir -PathType Container) { Remove-Item -LiteralPath $rootexTempDir } } # npm's PowerShell shims conflict with Restricted execution policy. Keep the # generated .cmd shims so plain rootex also works without changing user policy. $rootexPrefix = (& $npmCommand.Source prefix --global).Trim() foreach ($rootexAlias in @('rootex','rootexai')) { $rootexShim = Join-Path $rootexPrefix ($rootexAlias + '.ps1') if (Test-Path -LiteralPath $rootexShim) { $rootexContent = Get-Content -LiteralPath $rootexShim -Raw if ($rootexContent -match 'rootex-cli' -and $rootexContent -match 'rootex.mjs') { Remove-Item -LiteralPath $rootexShim } } } # Remove only launchers positively identified as belonging to older Rootex releases. foreach ($rootexLegacy in @('rotex-cli','rootex-cli')) { foreach ($rootexSuffix in @('', '.cmd', '.ps1')) { $rootexLegacyFile = Join-Path $rootexPrefix ($rootexLegacy + $rootexSuffix) if (Test-Path -LiteralPath $rootexLegacyFile -PathType Leaf) { $rootexLegacyContent = Get-Content -LiteralPath $rootexLegacyFile -Raw if ($rootexLegacyContent -match 'rootex-cli' -and $rootexLegacyContent -match 'rootex.mjs') { Remove-Item -LiteralPath $rootexLegacyFile -Force } } } } Write-Host 'Installed. Open a project folder and run rootex or rootexai. Node.js 20+ is required.'