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 ()]
1618param (
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
6166function 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
6875Usage:
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
7784Unix / 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
381388function Format-ReleaseApiError ($Uri , $ErrorRecord ) {
@@ -387,57 +394,36 @@ function Format-ReleaseApiError($Uri, $ErrorRecord) {
387394}
388395
389396function 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
428415function 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 `n The 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 `n The 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) {
469455if (-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
478467Wait-ReleaseAssets $Version $asset
479468if ($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