fix: update uv with install manager

This commit is contained in:
Alishahryar1
2026-05-29 16:35:10 -07:00
parent cebdc02a32
commit 4631ff9c49
3 changed files with 375 additions and 12 deletions
+183 -1
View File
@@ -14,6 +14,7 @@ $ErrorActionPreference = "Stop"
$RepoGitUrl = "git+https://github.com/Alishahryar1/free-claude-code.git"
$PythonVersion = "3.14.0"
$MinUvVersion = "0.11.0"
$UvInstallUrl = "https://astral.sh/uv/install.ps1"
function Show-Usage {
@@ -103,6 +104,184 @@ function Assert-CommandAvailable {
}
}
function Invoke-ProbeCommand {
param(
[string] $FilePath,
[string[]] $Arguments = @()
)
$previousErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & $FilePath @Arguments 2>$null
return [pscustomobject] @{
ExitCode = $LASTEXITCODE
Output = ($output | Out-String)
}
}
catch {
return [pscustomobject] @{
ExitCode = 1
Output = ""
}
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
}
}
function Get-InstalledUvVersion {
$version = ""
$selfVersionProbe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("self", "version", "--short")
if ($selfVersionProbe.ExitCode -eq 0) {
$version = $selfVersionProbe.Output.Trim()
}
if ([string]::IsNullOrWhiteSpace($version)) {
$versionProbe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("--version")
if (($versionProbe.ExitCode -eq 0) -and ($versionProbe.Output -match '^uv\s+([^\s]+)')) {
$version = $Matches[1]
}
}
if ([string]::IsNullOrWhiteSpace($version)) {
throw "Unable to determine uv version."
}
return $version
}
function Test-UvVersionAtLeast {
param(
[string] $Version,
[string] $Minimum
)
$normalizedVersion = $Version -replace '[-+].*$', ''
$normalizedMinimum = $Minimum -replace '[-+].*$', ''
return ([version] $normalizedVersion) -ge ([version] $normalizedMinimum)
}
function Test-UvVersionSatisfiesMinimum {
$version = Get-InstalledUvVersion
return Test-UvVersionAtLeast -Version $version -Minimum $MinUvVersion
}
function Assert-MinUvVersion {
if ($DryRun) {
return
}
$version = Get-InstalledUvVersion
if (-not (Test-UvVersionAtLeast -Version $version -Minimum $MinUvVersion)) {
throw "uv $MinUvVersion or newer is required; found uv $version. Upgrade uv with its installer or package manager, then rerun this installer."
}
}
function Test-UvSelfUpdateSupported {
$probe = Invoke-ProbeCommand -FilePath "uv" -Arguments @("self", "update", "--dry-run")
return $probe.ExitCode -eq 0
}
function Test-UvInstalledByScoop {
if (-not (Get-Command scoop -ErrorAction SilentlyContinue)) {
return $false
}
$probe = Invoke-ProbeCommand -FilePath "scoop" -Arguments @("list", "uv")
return ($probe.ExitCode -eq 0) -and ($probe.Output -match '(^|\s)uv(\s|$)')
}
function Test-UvInstalledByWinget {
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
return $false
}
$probe = Invoke-ProbeCommand -FilePath "winget" -Arguments @("list", "--id", "astral-sh.uv", "-e")
return ($probe.ExitCode -eq 0) -and ($probe.Output -match 'astral-sh\.uv')
}
function Test-UvInstalledByPipx {
if (-not (Get-Command pipx -ErrorAction SilentlyContinue)) {
return $false
}
$probe = Invoke-ProbeCommand -FilePath "pipx" -Arguments @("list")
return ($probe.ExitCode -eq 0) -and ($probe.Output -match '(?m)\bpackage uv\b')
}
function Test-UvInstalledInActiveVirtualenv {
if ([string]::IsNullOrWhiteSpace($env:VIRTUAL_ENV)) {
return $false
}
$uvCommand = Get-Command uv -ErrorAction SilentlyContinue
if (-not $uvCommand) {
return $false
}
$uvPath = [IO.Path]::GetFullPath($uvCommand.Source)
$venvPath = ([IO.Path]::GetFullPath($env:VIRTUAL_ENV)).TrimEnd(
[IO.Path]::DirectorySeparatorChar,
[IO.Path]::AltDirectorySeparatorChar
)
$nativePrefix = "$venvPath$([IO.Path]::DirectorySeparatorChar)"
$alternatePrefix = "$venvPath$([IO.Path]::AltDirectorySeparatorChar)"
return $uvPath.StartsWith($nativePrefix, [StringComparison]::OrdinalIgnoreCase) -or
$uvPath.StartsWith($alternatePrefix, [StringComparison]::OrdinalIgnoreCase)
}
function Update-ExistingUv {
if (Test-UvSelfUpdateSupported) {
Invoke-InstallCommand -FilePath "uv" -Arguments @("self", "update")
return
}
if (Test-UvInstalledByScoop) {
Invoke-InstallCommand -FilePath "scoop" -Arguments @("update", "uv")
return
}
if (Test-UvInstalledByWinget) {
Invoke-InstallCommand -FilePath "winget" -Arguments @(
"upgrade",
"--id",
"astral-sh.uv",
"-e",
"--accept-package-agreements",
"--accept-source-agreements"
)
return
}
if (Test-UvInstalledByPipx) {
Invoke-InstallCommand -FilePath "pipx" -Arguments @("upgrade", "uv")
return
}
if (Test-UvInstalledInActiveVirtualenv) {
Invoke-InstallCommand -FilePath "python" -Arguments @("-m", "pip", "install", "--upgrade", "uv")
return
}
if (Test-UvVersionSatisfiesMinimum) {
Write-Host "uv is already installed and satisfies >=$MinUvVersion; skipping automatic uv update because the install source was not detected."
return
}
$version = "unknown"
try {
$version = Get-InstalledUvVersion
}
catch {
$version = "unknown"
}
throw "uv $MinUvVersion or newer is required; found uv $version. The existing uv install source was not detected. Upgrade uv manually with the package manager that installed it, then rerun this installer."
}
function Install-ClaudeIfMissing {
if (Get-Command claude -ErrorAction SilentlyContinue) {
Write-Host "Claude Code already found on PATH; skipping install."
@@ -117,7 +296,8 @@ function Install-OrUpdateUv {
Add-UvToPath
if (Get-Command uv -ErrorAction SilentlyContinue) {
Invoke-InstallCommand -FilePath "uv" -Arguments @("self", "update")
Update-ExistingUv
Assert-MinUvVersion
return
}
@@ -127,6 +307,8 @@ function Install-OrUpdateUv {
if ((-not $DryRun) -and (-not (Get-Command uv -ErrorAction SilentlyContinue))) {
throw "uv was installed, but it is not available on PATH. Open a new terminal or add uv's bin directory to PATH."
}
Assert-MinUvVersion
}
function Get-PackageSpec {
+104 -1
View File
@@ -3,6 +3,7 @@ set -eu
REPO_GIT_URL="git+https://github.com/Alishahryar1/free-claude-code.git"
PYTHON_VERSION="3.14.0"
MIN_UV_VERSION="0.11.0"
UV_INSTALL_URL="https://astral.sh/uv/install.sh"
dry_run=0
@@ -99,6 +100,105 @@ require_command() {
fi
}
current_uv_version() {
version=$(uv self version --short 2>/dev/null || true)
if [ -z "$version" ]; then
version=$(uv --version 2>/dev/null | sed 's/^uv //; s/ .*//' || true)
fi
[ -n "$version" ] || return 1
printf '%s\n' "$version"
}
version_ge() {
current=${1%%[-+]*}
minimum=${2%%[-+]*}
old_ifs=$IFS
IFS=.
set -- $current
current_major=${1:-0}
current_minor=${2:-0}
current_patch=${3:-0}
set -- $minimum
minimum_major=${1:-0}
minimum_minor=${2:-0}
minimum_patch=${3:-0}
IFS=$old_ifs
[ "$current_major" -gt "$minimum_major" ] && return 0
[ "$current_major" -lt "$minimum_major" ] && return 1
[ "$current_minor" -gt "$minimum_minor" ] && return 0
[ "$current_minor" -lt "$minimum_minor" ] && return 1
[ "$current_patch" -ge "$minimum_patch" ]
}
uv_version_satisfies_minimum() {
version=$(current_uv_version) || return 1
version_ge "$version" "$MIN_UV_VERSION"
}
validate_uv_version() {
[ "$dry_run" -eq 1 ] && return 0
version=$(current_uv_version) || fail "Unable to determine uv version."
if ! version_ge "$version" "$MIN_UV_VERSION"; then
fail "uv $MIN_UV_VERSION or newer is required; found uv $version. Upgrade uv with its installer or package manager, then rerun this installer."
fi
}
uv_self_update_supported() {
uv self update --dry-run >/dev/null 2>&1
}
uv_installed_by_homebrew() {
command -v brew >/dev/null 2>&1 && brew list --versions uv >/dev/null 2>&1
}
uv_installed_by_pipx() {
command -v pipx >/dev/null 2>&1 && pipx list 2>/dev/null | grep -Eq '(^|[[:space:]])package uv([[:space:]]|$)'
}
uv_installed_in_active_virtualenv() {
[ -n "${VIRTUAL_ENV:-}" ] || return 1
uv_path=$(command -v uv)
case "$uv_path" in
"$VIRTUAL_ENV"/*) return 0 ;;
*) return 1 ;;
esac
}
update_existing_uv() {
if uv_self_update_supported; then
run uv self update
return 0
fi
if uv_installed_by_homebrew; then
run brew upgrade uv
return 0
fi
if uv_installed_by_pipx; then
run pipx upgrade uv
return 0
fi
if uv_installed_in_active_virtualenv; then
run python -m pip install --upgrade uv
return 0
fi
if uv_version_satisfies_minimum; then
printf 'uv is already installed and satisfies >=%s; skipping automatic uv update because the install source was not detected.\n' "$MIN_UV_VERSION"
return 0
fi
version=$(current_uv_version 2>/dev/null || printf 'unknown')
fail "uv $MIN_UV_VERSION or newer is required; found uv $version. The existing uv install source was not detected. Upgrade uv manually with the package manager that installed it, then rerun this installer."
}
install_claude_if_missing() {
if command -v claude >/dev/null 2>&1; then
printf 'Claude Code already found on PATH; skipping install.\n'
@@ -113,7 +213,8 @@ install_or_update_uv() {
add_uv_to_path
if command -v uv >/dev/null 2>&1; then
run uv self update
update_existing_uv
validate_uv_version
return 0
fi
@@ -123,6 +224,8 @@ install_or_update_uv() {
if [ "$dry_run" -eq 0 ] && ! command -v uv >/dev/null 2>&1; then
fail "uv was installed, but it is not available on PATH. Open a new terminal or add uv's bin directory to PATH."
fi
validate_uv_version
}
parse_args() {
+88 -10
View File
@@ -42,17 +42,51 @@ def test_install_sh_installs_claude_only_when_missing() -> None:
def test_install_sh_installs_missing_uv_without_self_update() -> None:
body = _braced_body(_script_text("install.sh"), "install_or_update_uv()")
text = _script_text("install.sh")
body = _braced_body(text, "install_or_update_uv()")
assert "if command -v uv >/dev/null 2>&1; then" in body
assert body.count("run uv self update") == 1
assert "update_existing_uv" in body
assert "run uv self update" not in body
update_index = body.index("run uv self update")
return_index = body.index("return 0", update_index)
update_index = body.index("update_existing_uv")
validate_existing_index = body.index("validate_uv_version", update_index)
installer_index = body.index("run_uv_installer")
validate_installed_index = body.index("validate_uv_version", installer_index)
verification_index = body.index('if [ "$dry_run" -eq 0 ] && ! command -v uv')
assert update_index < return_index < installer_index < verification_index
assert update_index < validate_existing_index < installer_index
assert installer_index < verification_index < validate_installed_index
def test_install_sh_updates_uv_with_detected_source() -> None:
text = _script_text("install.sh")
update_body = _braced_body(text, "update_existing_uv()")
assert "uv self update --dry-run" in text
assert update_body.count("run uv self update") == 1
assert update_body.index("uv_self_update_supported") < update_body.index(
"run uv self update"
)
assert "brew list --versions uv" in text
assert "run brew upgrade uv" in update_body
assert "pipx list" in text
assert "run pipx upgrade uv" in update_body
assert "VIRTUAL_ENV" in text
assert "run python -m pip install --upgrade uv" in update_body
assert "uv_version_satisfies_minimum" in update_body
assert "install source was not detected" in update_body
def test_install_sh_validates_minimum_uv_version() -> None:
text = _script_text("install.sh")
validate_body = _braced_body(text, "validate_uv_version()")
assert 'MIN_UV_VERSION="0.11.0"' in text
assert "uv self version --short" in text
assert "version_ge" in validate_body
assert "uv $MIN_UV_VERSION or newer is required" in validate_body
def test_install_ps1_installs_claude_only_when_missing() -> None:
@@ -76,15 +110,59 @@ def test_install_ps1_installs_claude_only_when_missing() -> None:
def test_install_ps1_installs_missing_uv_without_self_update() -> None:
body = _braced_body(_script_text("install.ps1"), "function Install-OrUpdateUv")
text = _script_text("install.ps1")
body = _braced_body(text, "function Install-OrUpdateUv")
self_update = 'Invoke-InstallCommand -FilePath "uv" -Arguments @("self", "update")'
assert "if (Get-Command uv -ErrorAction SilentlyContinue)" in body
assert body.count(self_update) == 1
assert "Update-ExistingUv" in body
assert self_update not in body
update_index = body.index(self_update)
return_index = body.index("return", update_index)
update_index = body.index("Update-ExistingUv")
validate_existing_index = body.index("Assert-MinUvVersion", update_index)
installer_index = body.index("Invoke-UvInstaller")
verification_index = body.index("if ((-not $DryRun)")
validate_installed_index = body.index("Assert-MinUvVersion", installer_index)
assert update_index < return_index < installer_index < verification_index
assert update_index < validate_existing_index < installer_index
assert installer_index < verification_index < validate_installed_index
def test_install_ps1_updates_uv_with_detected_source() -> None:
text = _script_text("install.ps1")
update_body = _braced_body(text, "function Update-ExistingUv")
self_update = 'Invoke-InstallCommand -FilePath "uv" -Arguments @("self", "update")'
assert '"self", "update", "--dry-run"' in text
assert update_body.count(self_update) == 1
assert update_body.index("Test-UvSelfUpdateSupported") < update_body.index(
self_update
)
assert (
'Invoke-InstallCommand -FilePath "scoop" -Arguments @("update", "uv")'
in update_body
)
assert '"winget"' in update_body
assert '"astral-sh.uv"' in update_body
assert '"--accept-package-agreements"' in update_body
assert (
'Invoke-InstallCommand -FilePath "pipx" -Arguments @("upgrade", "uv")'
in update_body
)
assert (
'Invoke-InstallCommand -FilePath "python" -Arguments @("-m", "pip", "install", "--upgrade", "uv")'
in update_body
)
assert "Test-UvVersionSatisfiesMinimum" in update_body
assert "install source was not detected" in update_body
def test_install_ps1_validates_minimum_uv_version() -> None:
text = _script_text("install.ps1")
validate_body = _braced_body(text, "function Assert-MinUvVersion")
assert '$MinUvVersion = "0.11.0"' in text
assert '"self", "version", "--short"' in text
assert "[version]" in text
assert "uv $MinUvVersion or newer is required" in validate_body