mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
224 lines
10 KiB
PowerShell
224 lines
10 KiB
PowerShell
# Installs the T3 Code CLI from a GitHub Release archive on Windows. Needs
|
|
# only PowerShell 5.1+; no Node, npm, or compiler.
|
|
#
|
|
# irm https://t3.codes/install.ps1 | iex
|
|
#
|
|
# Environment:
|
|
# T3CODE_CHANNEL release train to follow: stable, nightly, or preview
|
|
# (default: stable; preview is a maintainers' test train)
|
|
# T3CODE_VERSION exact version to install (overrides T3CODE_CHANNEL)
|
|
# T3CODE_HOME T3 home directory (default: ~\.t3)
|
|
# T3CODE_INSTALL_BIN_DIR where t3.exe is linked (default: ~\.local\bin)
|
|
# T3CODE_RELEASE_BASE_URL mirror for releases/download (default: GitHub)
|
|
#
|
|
# The archive is unpacked into $T3CODE_HOME\runtime\versions\<version>, the
|
|
# same layout `t3 service install` uses, so the service reuses this download.
|
|
$ErrorActionPreference = "Stop"
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
|
|
$repo = "pingdotgg/t3code"
|
|
$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL.TrimEnd("/") } else { "https://github.com/$repo/releases/download" }
|
|
$t3Home = if ($env:T3CODE_HOME) { $env:T3CODE_HOME } else { Join-Path $HOME ".t3" }
|
|
$binDir = if ($env:T3CODE_INSTALL_BIN_DIR) { $env:T3CODE_INSTALL_BIN_DIR } else { Join-Path $HOME ".local\bin" }
|
|
|
|
function Fail([string] $message) {
|
|
Write-Error "t3 install: $message"
|
|
exit 1
|
|
}
|
|
|
|
$terminal = -not [Console]::IsErrorRedirected -and $env:TERM -ne "dumb"
|
|
$interactive = $terminal -and $Host.UI.SupportsVirtualTerminal
|
|
if ($interactive) {
|
|
# Legacy code pages may support ANSI escapes but not the logo or bar glyphs.
|
|
$glyphs = -join [char[]](0x2588, 0x2580, 0x2584, 0x25A0, 0x00B7)
|
|
$encoding = [Console]::Error.Encoding
|
|
$interactive = $encoding.GetString($encoding.GetBytes($glyphs)) -eq $glyphs
|
|
}
|
|
$esc = [char]27
|
|
$reset = $bold = $muted = $accent = $green = ""
|
|
if ($interactive -and -not $env:NO_COLOR) {
|
|
$reset = "$esc[0m"; $bold = "$esc[1m"; $muted = "$esc[2m"
|
|
$accent = "$esc[94m"; $green = "$esc[32m"
|
|
}
|
|
function Step([string] $message) {
|
|
if ($interactive) { [Console]::Error.Write("`r$esc[2K $muted$message$reset") }
|
|
else { [Console]::Error.WriteLine(" $message") }
|
|
}
|
|
function Draw-Download([long] $bytes, [long] $total) {
|
|
if (-not $interactive) { return }
|
|
$columns = [Console]::WindowWidth
|
|
if ($columns -le 0) { $columns = 80 }
|
|
if ($total -gt 0 -and $columns -ge 9) {
|
|
$percent = [Math]::Min(100, [Math]::Floor($bytes * 100.0 / $total))
|
|
$width = [Math]::Min(32, $columns - 8)
|
|
$filled = [int][Math]::Floor($percent * $width / 100)
|
|
$bar = ([string][char]0x25A0) * $filled
|
|
$rest = ([string][char]0x00B7) * ($width - $filled)
|
|
$sizes = (" {0:F1} / {1:F1} MB" -f ($bytes / 1MB), ($total / 1MB))
|
|
if ($width + 7 + $sizes.Length -ge $columns) { $sizes = "" }
|
|
[Console]::Error.Write(("`r$esc[2K $accent$bar$reset$muted$rest$reset {0,3}%" -f $percent) + "$muted$sizes$reset")
|
|
} else {
|
|
$line = if ($columns -ge 32) { " Downloading {0:F1} MB" -f ($bytes / 1MB) } else { " {0:F1} MB" -f ($bytes / 1MB) }
|
|
[Console]::Error.Write("`r$esc[2K" + $line.Substring(0, [Math]::Min($line.Length, $columns - 1)))
|
|
}
|
|
}
|
|
function Fetch([string] $uri, [string] $destination, [switch] $progress) {
|
|
if (-not $progress -or -not $interactive) {
|
|
$ProgressPreference = if ($progress -and $terminal) { "Continue" } else { "SilentlyContinue" }
|
|
Invoke-WebRequest -Uri $uri -OutFile $destination -UseBasicParsing
|
|
return
|
|
}
|
|
# Read the response once, displaying actual bytes received at most ten times a second.
|
|
Add-Type -AssemblyName System.Net.Http
|
|
$client = New-Object System.Net.Http.HttpClient
|
|
$response = $source = $file = $null
|
|
try {
|
|
$response = $client.GetAsync($uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult()
|
|
$response.EnsureSuccessStatusCode() | Out-Null
|
|
$source = $response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
|
|
$file = [IO.File]::Create($destination)
|
|
$buffer = New-Object byte[] 65536
|
|
$bytes = 0L
|
|
$clock = [Diagnostics.Stopwatch]::StartNew()
|
|
Draw-Download 0 $response.Content.Headers.ContentLength
|
|
while (($count = $source.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
|
$file.Write($buffer, 0, $count)
|
|
$bytes += $count
|
|
if ($clock.ElapsedMilliseconds -ge 100) {
|
|
Draw-Download $bytes $response.Content.Headers.ContentLength
|
|
$clock.Restart()
|
|
}
|
|
}
|
|
Draw-Download $bytes $bytes
|
|
[Console]::Error.WriteLine()
|
|
} finally {
|
|
if ($file) { $file.Dispose() }
|
|
if ($source) { $source.Dispose() }
|
|
if ($response) { $response.Dispose() }
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
if ($interactive) {
|
|
# Block characters are constructed so this file also loads correctly in PowerShell 5.1.
|
|
$mark = @(
|
|
"########## ######## ",
|
|
" ### _##^ ",
|
|
" ### ####_ ",
|
|
" ### _ ###",
|
|
" ### #######^ "
|
|
)
|
|
[Console]::Error.WriteLine()
|
|
for ($i = 0; $i -lt $mark.Length; $i++) {
|
|
$row = $mark[$i].Replace('#', [char]0x2588).Replace('^', [char]0x2580).Replace('_', [char]0x2584)
|
|
$label = if ($i -eq 1) { " ${bold}T3 Code$reset" } elseif ($i -eq 2) { " ${muted}CLI installer$reset" } else { "" }
|
|
[Console]::Error.WriteLine(" $bold$row$reset$label")
|
|
}
|
|
[Console]::Error.WriteLine()
|
|
}
|
|
Step "Finding your release..."
|
|
|
|
# PROCESSOR_ARCHITEW6432 reports the real machine when a 32-bit PowerShell
|
|
# runs under WOW64; RuntimeInformation needs .NET 4.7.1+, which 5.1 hosts
|
|
# may lack.
|
|
$rawArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
|
$arch = switch ($rawArch) {
|
|
"AMD64" { "x64" }
|
|
"ARM64" { "arm64" }
|
|
default { Fail "unsupported architecture $rawArch" }
|
|
}
|
|
|
|
$channel = if ($env:T3CODE_CHANNEL) { $env:T3CODE_CHANNEL } else { "stable" }
|
|
$version = $env:T3CODE_VERSION
|
|
if (-not $version) {
|
|
# Tags are v<semver>; the channel is the prerelease identifier, or none for
|
|
# stable. Only tags of the requested train are considered, so a stable
|
|
# install can never pick up a nightly or preview build by accident.
|
|
$tagPattern = switch ($channel) {
|
|
"stable" { '^v\d+\.\d+\.\d+$' }
|
|
"nightly" { '^v\d+\.\d+\.\d+-nightly\.\d+\.\d+$' }
|
|
"preview" { '^v\d+\.\d+\.\d+-preview\.\d+\.\d+$' }
|
|
default { Fail "T3CODE_CHANNEL must be stable, nightly, or preview" }
|
|
}
|
|
$releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100" -Headers @{ "User-Agent" = "t3-install" }
|
|
$tag = ($releases | Where-Object { -not $_.draft -and $_.tag_name -match $tagPattern } | Select-Object -First 1).tag_name
|
|
if (-not $tag) { Fail "could not find a $channel release; set T3CODE_VERSION" }
|
|
$version = $tag.Substring(1)
|
|
}
|
|
if ($version -match '-preview\.') {
|
|
Write-Warning "t3 $version is a preview build. Preview builds are cut by maintainers from unreleased branches to exercise the release pipeline. They can be broken, receive no fixes, and are never offered as updates. Set T3CODE_CHANNEL=stable (the default) for a supported build."
|
|
if ($channel -ne "preview" -and -not $env:T3CODE_VERSION) {
|
|
Fail "refusing a preview build that was not explicitly requested"
|
|
}
|
|
}
|
|
|
|
$stem = "t3-$version-win32-$arch"
|
|
$archive = "$stem.zip"
|
|
$versionsDir = Join-Path $t3Home "runtime\versions"
|
|
$targetDir = Join-Path $versionsDir $version
|
|
$marker = Join-Path $targetDir ".install-complete"
|
|
|
|
if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) {
|
|
Step "Version $version is already downloaded."
|
|
} else {
|
|
New-Item -ItemType Directory -Force -Path $versionsDir | Out-Null
|
|
$staging = Join-Path $versionsDir (".staging-" + [System.IO.Path]::GetRandomFileName())
|
|
New-Item -ItemType Directory -Path $staging | Out-Null
|
|
try {
|
|
if ($interactive) { [Console]::Error.Write("`r$esc[2K") }
|
|
[Console]::Error.WriteLine(" ${muted}Installing$reset T3 Code $bold$version$reset`n")
|
|
Step "Downloading..."
|
|
try {
|
|
Fetch "$baseUrl/v$version/SHA256SUMS" (Join-Path $staging "SHA256SUMS")
|
|
} catch {
|
|
$status = $_.Exception.Response.StatusCode.value__
|
|
if ($status -eq 404) {
|
|
Fail "t3 $version has no release archive for win32-$arch; releases before the self-contained CLI can only be installed with 'npm install -g t3@$version'"
|
|
}
|
|
throw
|
|
}
|
|
Fetch "$baseUrl/v$version/$archive" (Join-Path $staging $archive) -progress
|
|
|
|
Step "Verifying the download..."
|
|
|
|
$expected = (Get-Content (Join-Path $staging "SHA256SUMS") | Where-Object { $_ -match "\s\*?$([regex]::Escape($archive))$" } | Select-Object -First 1)
|
|
if (-not $expected) { Fail "$archive is not listed in SHA256SUMS" }
|
|
$expected = ($expected -split "\s+")[0].ToLowerInvariant()
|
|
$actual = (Get-FileHash -Algorithm SHA256 (Join-Path $staging $archive)).Hash.ToLowerInvariant()
|
|
if ($actual -ne $expected) { Fail "checksum mismatch for $archive" }
|
|
|
|
Step "Extracting T3 Code..."
|
|
# The archive module reads the global preference, not the caller's local scope.
|
|
$savedProgress = $global:ProgressPreference
|
|
try {
|
|
$global:ProgressPreference = "SilentlyContinue"
|
|
Expand-Archive -Path (Join-Path $staging $archive) -DestinationPath $staging -Force
|
|
} finally { $global:ProgressPreference = $savedProgress }
|
|
# The archive wraps everything in one directory named after its stem.
|
|
Get-ChildItem (Join-Path $staging $stem) | Move-Item -Destination $staging
|
|
Remove-Item (Join-Path $staging $stem), (Join-Path $staging $archive), (Join-Path $staging "SHA256SUMS") -Recurse -Force
|
|
|
|
& (Join-Path $staging "t3.exe") --version | Out-Null
|
|
if ($LASTEXITCODE -ne 0) { Fail "the downloaded executable does not run" }
|
|
Set-Content -Path (Join-Path $staging ".install-complete") -Value $version -NoNewline
|
|
|
|
if (Test-Path $targetDir) { Remove-Item $targetDir -Recurse -Force }
|
|
Move-Item $staging $targetDir
|
|
} catch {
|
|
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
|
|
throw
|
|
}
|
|
}
|
|
|
|
Step "Setting up the t3 command..."
|
|
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
|
|
$shim = Join-Path $binDir "t3.cmd"
|
|
# UTF-8 without a BOM: cmd.exe reads the shim as-is, and ASCII would corrupt
|
|
# non-ASCII characters in the user's home path.
|
|
[System.IO.File]::WriteAllText($shim, "@echo off`r`n`"$(Join-Path $targetDir 't3.exe')`" %*", (New-Object System.Text.UTF8Encoding $false))
|
|
if ($interactive) { [Console]::Error.Write("`r$esc[2K") }
|
|
[Console]::Error.WriteLine(" ${green}Installed T3 Code $version$reset`n")
|
|
if (($env:PATH -split ";") -notcontains $binDir) {
|
|
Write-Host " Add $binDir to your PATH, then run ${bold}t3$reset.`n"
|
|
} else {
|
|
Write-Host " Run ${bold}t3$reset to get started.`n"
|
|
}
|