Skip to content

Commit 5da97d7

Browse files
committed
fix: point native install scripts at real release assets
install.sh and install.ps1 were carried over from the Python pythinker-cli repo: they resolved v-prefixed tags and downloaded pythinker-VERSION-.tar.gz / PythinkerSetup-.exe assets that this repo never publishes, so both installers polled GitHub for ~6 minutes and failed without downloading. Resolve the version from the CDN latest file (GitHub API fallback), download pythinker-code-<target>.zip from the @pythoughts/pythinker-code@X.Y.Z release tag, verify SHA-256, and install the extracted binary. Windows now extracts pythinker.exe to LOCALAPPDATA\Programs\Pythinker and persists it on the user PATH instead of running the removed Inno Setup installer.
1 parent 313d8c4 commit 5da97d7

2 files changed

Lines changed: 151 additions & 141 deletions

File tree

apps/pythinker-web/public/install.ps1

Lines changed: 69 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
# Pythinker Code — native Windows installer bootstrap.
1+
# Pythinker Code — native Windows installer.
22
#
3-
# Downloads the latest PythinkerSetup-x.y.z.exe from GitHub Releases, verifies
4-
# its SHA-256 file, and runs the per-user Inno Setup installer silently.
3+
# Downloads the native single-file binary (pythinker-code-win32-<arch>.zip)
4+
# from the GitHub Release matching the CDN's latest version, verifies its
5+
# SHA-256, and installs pythinker.exe to %LOCALAPPDATA%\Programs\Pythinker
6+
# (added to the user PATH).
57
#
68
# Usage:
79
# irm https://pythinker.com/install.ps1 | iex
810
#
911
# To pin a version when running the hosted script, set:
10-
# $env:PYTHINKER_VERSION = "0.27.0"; irm https://pythinker.com/install.ps1 | iex
12+
# $env:PYTHINKER_VERSION = "0.6.0"; irm https://pythinker.com/install.ps1 | iex
1113
#
1214
# Or run the script directly:
13-
# .\install.ps1 -Version 0.27.0
15+
# .\install.ps1 -Version 0.6.0
1416

1517
[CmdletBinding()]
1618
param(
@@ -21,6 +23,9 @@ param(
2123
$ErrorActionPreference = "Stop"
2224

2325
$Repo = "Pythoughts-labs/pythinker-code"
26+
# CDN source of truth for the latest published version — same endpoint the
27+
# in-app updater reads, so a fresh install and an auto-update always agree.
28+
$CdnLatestUrl = "https://code.pythinker.com/pythinker-code/latest"
2429
$InstallShUrl = "https://pythinker.com/install.sh"
2530
$InstallPs1Url = "https://pythinker.com/install.ps1"
2631
$NoColor = $env:NO_COLOR
@@ -60,19 +65,21 @@ $script:AntennaSpinActive = $false
6065

6166
function Show-Usage {
6267
@"
63-
Pythinker Code — native Windows installer bootstrap.
68+
Pythinker Code — native Windows installer.
6469
65-
Downloads the latest PythinkerSetup-x.y.z.exe from GitHub Releases, verifies
66-
its SHA-256 file, and runs the per-user Inno Setup installer silently.
70+
Downloads the native single-file binary (pythinker-code-win32-<arch>.zip)
71+
from the GitHub Release matching the CDN's latest version, verifies its
72+
SHA-256, and installs pythinker.exe to %LOCALAPPDATA%\Programs\Pythinker
73+
(added to the user PATH).
6774
6875
Usage:
6976
irm $InstallPs1Url | iex
7077
7178
# Pin a version:
72-
`$env:PYTHINKER_VERSION = "0.27.0"; irm $InstallPs1Url | iex
79+
`$env:PYTHINKER_VERSION = "0.6.0"; irm $InstallPs1Url | iex
7380
7481
# Or run directly:
75-
.\install.ps1 -Version 0.27.0
82+
.\install.ps1 -Version 0.6.0
7683
7784
Unix / macOS / Linux users:
7885
curl -fsSL $InstallShUrl | bash
@@ -257,10 +264,10 @@ function Write-Logo {
257264
if ($useAnim) { Write-LogoAnimated } else { Write-LogoStatic }
258265
}
259266

260-
function Print-Intro($version, $asset) {
267+
function Print-Intro($version, $platformDisplay, $asset) {
261268
Write-Logo
262269
Write-Host (" {0,-11} {1}" -f "Version", $version)
263-
Write-Host (" {0,-11} {1}" -f "Platform", "Windows x64")
270+
Write-Host (" {0,-11} {1}" -f "Platform", $platformDisplay)
264271
Write-Host (" {0,-11} {1}" -f "Package", $asset)
265272
Write-Host ""
266273
}
@@ -367,15 +374,15 @@ function Print-Done {
367374
}
368375
}
369376

370-
function Test-ReleaseHasInstaller($release) {
371-
if ($release.draft -or $release.prerelease) { return $null }
372-
$tag = [string]$release.tag_name
373-
if (-not $tag) { return $null }
374-
$candidate = $tag.TrimStart('v')
375-
$exe = "PythinkerSetup-$candidate.exe"
377+
# Releases are tagged "@pythoughts/pythinker-code@X.Y.Z"; the "@" and "/"
378+
# must be percent-encoded in download and API URLs.
379+
function Get-ReleaseTag($version) { return "@pythoughts/pythinker-code@$version" }
380+
function Get-EncodedReleaseTag($version) { return [uri]::EscapeDataString((Get-ReleaseTag $version)) }
381+
382+
function Test-ReleaseHasAsset($release, $asset) {
383+
if ($release.draft -or $release.prerelease) { return $false }
376384
$names = @($release.assets | ForEach-Object { [string]$_.name })
377-
if (($names -contains $exe) -and ($names -contains "$exe.sha256")) { return $candidate }
378-
return $null
385+
return (($names -contains $asset) -and ($names -contains "$asset.sha256"))
379386
}
380387

381388
function Format-ReleaseApiError($Uri, $ErrorRecord) {
@@ -387,57 +394,36 @@ function Format-ReleaseApiError($Uri, $ErrorRecord) {
387394
}
388395

389396
function Get-LatestVersion {
397+
# The CDN is the source of truth (same endpoint the in-app updater reads).
398+
# Fall back to the GitHub API if the CDN is unreachable.
399+
try {
400+
$raw = (Invoke-RestMethod -UseBasicParsing -Uri $CdnLatestUrl)
401+
$candidate = ([string]$raw).Trim()
402+
if ($candidate -match '^\d+\.\d+\.\d+$') { return $candidate }
403+
} catch {}
390404
$latestApi = "https://api.github.com/repos/$Repo/releases/latest"
391-
$listApi = "https://api.github.com/repos/$Repo/releases?per_page=100"
392-
$delay = 4
393-
$elapsed = 0
394-
$maxElapsed = 360
395-
$lastApiError = $null
396-
while ($true) {
397-
try {
398-
$latest = Invoke-RestMethod -UseBasicParsing -Uri $latestApi
399-
$found = Test-ReleaseHasInstaller $latest
400-
if ($found) { return $found }
401-
} catch {
402-
$lastApiError = Format-ReleaseApiError $latestApi $_
403-
}
404-
try {
405-
$releases = Invoke-RestMethod -UseBasicParsing -Uri $listApi
406-
foreach ($release in @($releases)) {
407-
$found = Test-ReleaseHasInstaller $release
408-
if ($found) { return $found }
409-
}
410-
} catch {
411-
$lastApiError = Format-ReleaseApiError $listApi $_
412-
}
413-
if ($elapsed -ge $maxElapsed) {
414-
$detail = if ($lastApiError) { " Last API error: $lastApiError" } else { "" }
415-
Fail "no published release has a ready Windows installer asset after ~${maxElapsed}s; try again shortly or pin `$env:PYTHINKER_VERSION.$detail"
416-
}
417-
if ($useAnim) {
418-
Write-Host -NoNewline ("${ESC}[$($script:ProgressRow);1H${ESC}[K ${DIM}Waiting${RESET} release assets, retrying in ${BAR}${delay}s${RESET}")
419-
} else {
420-
Write-Host " Waiting for release assets, retrying in ${delay}s"
421-
}
422-
Start-Sleep -Seconds $delay
423-
$elapsed += $delay
424-
$delay = [Math]::Min($delay * 2, 120)
405+
try {
406+
$latest = Invoke-RestMethod -UseBasicParsing -Uri $latestApi
407+
$tag = [string]$latest.tag_name
408+
if ($tag -match '^@pythoughts/pythinker-code@(\d+\.\d+\.\d+)$') { return $Matches[1] }
409+
Fail "could not parse latest release tag '$tag' from $latestApi"
410+
} catch {
411+
Fail "could not resolve the latest version: $(Format-ReleaseApiError $latestApi $_)"
425412
}
426413
}
427414

428415
function Wait-ReleaseAssets($version, $asset) {
429-
$api = "https://api.github.com/repos/$Repo/releases/tags/v$version"
416+
$api = "https://api.github.com/repos/$Repo/releases/tags/$(Get-EncodedReleaseTag $version)"
430417
$delay = 4
431418
$elapsed = 0
432419
$maxElapsed = 360
433420
while ($true) {
434421
try {
435422
$release = Invoke-RestMethod -UseBasicParsing -Uri $api
436-
$names = @($release.assets | ForEach-Object { [string]$_.name })
437-
if (($names -contains $asset) -and ($names -contains "$asset.sha256")) { return }
423+
if (Test-ReleaseHasAsset $release $asset) { return }
438424
} catch {}
439425
if ($elapsed -ge $maxElapsed) {
440-
Fail "release assets for v$version are not available after ~${maxElapsed}s: https://github.com/$Repo/releases/download/v$version/$asset`nThe latest release may still be publishing. Try again shortly, or pin a known-good version with -Version X.Y.Z"
426+
Fail "release assets for $version are not available after ~${maxElapsed}s: https://github.com/$Repo/releases/download/$(Get-EncodedReleaseTag $version)/$asset`nThe latest release may still be publishing. Try again shortly, or pin a known-good version with -Version X.Y.Z"
441427
}
442428
if ($useAnim) {
443429
Write-Host -NoNewline ("${ESC}[$($script:ProgressRow);1H${ESC}[K ${DIM}Waiting${RESET} release assets, retrying in ${BAR}${delay}s${RESET}")
@@ -469,12 +455,15 @@ if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
469455
if (-not $Version) { $Version = Get-LatestVersion }
470456
$Version = $Version.TrimStart('v')
471457

472-
$asset = "PythinkerSetup-$Version.exe"
473-
$baseUrl = "https://github.com/$Repo/releases/download/v$Version"
458+
$archLabel = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { 'arm64' } else { 'x64' }
459+
$target = "win32-$archLabel"
460+
461+
$asset = "pythinker-code-$target.zip"
462+
$baseUrl = "https://github.com/$Repo/releases/download/$(Get-EncodedReleaseTag $Version)"
474463
$installerUrl = "$baseUrl/$asset"
475464
$shaUrl = "$installerUrl.sha256"
476465

477-
Print-Intro $Version $asset
466+
Print-Intro $Version "Windows $archLabel" $asset
478467
Wait-ReleaseAssets $Version $asset
479468
if ($useAnim) { Write-Host -NoNewline ("${ESC}[$($script:ProgressRow);1H${ESC}[K") }
480469

@@ -495,24 +484,28 @@ try {
495484
}
496485
Phase-Ok "Verifying"
497486

498-
$installerArgs = @(
499-
'/SILENT',
500-
'/NORESTART',
501-
'/CURRENTUSER',
502-
'/CLOSEAPPLICATIONS',
503-
'/NORESTARTAPPLICATIONS'
504-
)
505-
$process = Start-Process -FilePath $installerPath -ArgumentList $installerArgs -Wait -PassThru
506-
if ($process.ExitCode -ne 0) {
507-
Fail "installer exited with code $($process.ExitCode)"
487+
# The release zip contains a single pythinker.exe at its root.
488+
$extractDir = Join-Path $tempDir "extracted"
489+
Expand-Archive -LiteralPath $installerPath -DestinationPath $extractDir -Force
490+
$binary = Join-Path $extractDir "pythinker.exe"
491+
if (-not (Test-Path $binary)) {
492+
Fail "archive did not contain pythinker.exe"
508493
}
509-
Phase-Ok "Installing"
510494

511495
$installDir = Join-Path $env:LOCALAPPDATA "Programs\Pythinker"
512-
if (Test-Path (Join-Path $installDir "pythinker.exe")) {
513-
if (($env:PATH -split ';') -notcontains $installDir) {
514-
$env:PATH = "$installDir;$env:PATH"
515-
}
496+
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
497+
Copy-Item -LiteralPath $binary -Destination (Join-Path $installDir "pythinker.exe") -Force
498+
Phase-Ok "Installing"
499+
500+
# Persist the install dir on the user PATH, and make it available in
501+
# this session so `pythinker` works without reopening the shell.
502+
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
503+
if (($userPath -split ';') -notcontains $installDir) {
504+
$newPath = if ($userPath) { "$installDir;$userPath" } else { $installDir }
505+
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
506+
}
507+
if (($env:PATH -split ';') -notcontains $installDir) {
508+
$env:PATH = "$installDir;$env:PATH"
516509
}
517510

518511
Print-Done

0 commit comments

Comments
 (0)