#Requires -Version 5.1 <# .SYNOPSIS Seed Code CLI v10.1.0 - official Windows remote installer. .DESCRIPTION Installs Seed Code CLI for the CURRENT USER. No administrator rights, no Python, no package manager, and no cloned repository are required: the official standalone release binary is downloaded from GitHub Releases, its SHA256 is verified, and `seedcode` is added to your user PATH. Verification model (verification is NEVER bypassed): 1. The release's SHA256SUMS.txt is the ONE authoritative expected value. It is generated by the release pipeline from the FINAL artifacts (scripts/windows/stage_release.py, then regenerated over the complete asset set by .github/workflows/release.yml) *after* every artifact is built, so the digest always describes the exact binary published. 2. There is deliberately NO second, hand-maintained checksum here. A pinned copy is what previously drifted from a rebuilt binary and produced the "SHA256SUMS.txt and this installer's pinned checksum disagree" abort. 3. The downloaded artifact must be listed in SHA256SUMS.txt and its digest must match, or the install aborts. A missing entry, an unreachable checksum file, or a mismatch all abort - there is no "install anyway" path. Usage (exactly as documented for remote install): irm https://seedcode-cli.vercel.app/install.ps1 | iex Or, to pass options, download-then-run: & ([scriptblock]::Create((irm https://seedcode-cli.vercel.app/install.ps1))) -Version 10.1.0 .PARAMETER Version Release version to install. Defaults to 10.1.0 (the current stable release). .PARAMETER InstallDir Install directory. Defaults to %LOCALAPPDATA%\Programs\SeedCode. .PARAMETER NoPathUpdate Install without modifying PATH. Use `seedcode` via the full path instead. .PARAMETER NoShortcuts Do not create the Desktop and Start Menu shortcuts. They are created by default (they were missing in earlier releases). .PARAMETER Force Reinstall even when the requested version is already installed. .PARAMETER Plain Disable the ANSI/Unicode UI (also happens automatically on unsupported terminals and when output is redirected). .EXAMPLE irm https://seedcode-cli.vercel.app/install.ps1 | iex .NOTES Requires PowerShell 5.1 or newer and an HTTPS-capable connection. This script never sees, stores, or prints an API key; credentials are set up inside the app on first run. #> [CmdletBinding()] param( [string] $Version = "10.1.0", [string] $InstallDir = "", [switch] $NoPathUpdate, [switch] $NoShortcuts, [switch] $Force, [switch] $Plain ) $ErrorActionPreference = "Stop" Set-StrictMode -Version 2.0 # --- constants --------------------------------------------------------------- $Repo = "Alshahriar-07/seedcode-cli" $ReleaseBase = "https://github.com/$Repo/releases/download/v$Version" $SumsUrl = "$ReleaseBase/SHA256SUMS.txt" $ExeName = "seedcode.exe" $UserAgent = "seedcode-cli-installer/$Version" # --- terminal capabilities (ANSI / Unicode with graceful fallback) ------------ $UiRedirected = $true try { $UiRedirected = [Console]::IsOutputRedirected } catch { } $UiAnsi = $false if (-not ($UiRedirected -or $Plain -or $env:NO_COLOR)) { $supportsVt = $false try { $supportsVt = [bool] $Host.UI.SupportsVirtualTerminal } catch { } $UiAnsi = $supportsVt -or ($env:WT_SESSION) -or ($PSVersionTable.PSVersion.Major -ge 7) } $UiUnicode = $false if (-not ($Plain -or $env:NO_COLOR)) { $codepage = 0 try { $codepage = [Console]::OutputEncoding.CodePage } catch { } $UiUnicode = ($codepage -eq 65001) -or ($env:WT_SESSION) -or ($PSVersionTable.PSVersion.Major -ge 7) } # PowerShell hash-literal values must be expressions, not statements, so the # symbols are computed as assignments first and collected afterwards. $symRule = $(if ($UiUnicode) { [string][char]0x2500 } else { "-" }) $symTick = $(if ($UiUnicode) { [string][char]0x2713 } else { "OK" }) $symNode = $(if ($UiUnicode) { [string][char]0x25CF } else { "*" }) $symBranch = $(if ($UiUnicode) { [string][char]0x251C } else { "|" }) $symLast = $(if ($UiUnicode) { [string][char]0x2514 } else { '`' }) $symDot = $(if ($UiUnicode) { [string][char]0x2022 } else { "|" }) $symCross = $(if ($UiUnicode) { [string][char]0x2715 } else { "x" }) $symFull = $(if ($UiUnicode) { [string][char]0x2588 } else { "#" }) $symEmpty = $(if ($UiUnicode) { [string][char]0x2591 } else { "." }) $Sym = @{ Rule = $symRule; Tick = $symTick; Node = $symNode; Branch = $symBranch Last = $symLast; Dot = $symDot; Full = $symFull; Empty = $symEmpty Cross = $symCross } $RuleText = ($Sym.Rule * 38) $AnsiFg = @{ White = "97"; Gray = "90"; Green = "92"; Yellow = "93"; Red = "91"; Cyan = "96"; DarkGray = "37" } function Write-Ui { param([string] $Text, [string] $Color = "Gray", [switch] $NoNewline) if ($UiRedirected) { if ($NoNewline) { [Console]::Write($Text) } else { [Console]::WriteLine($Text) } } elseif ($UiAnsi) { $code = if ($AnsiFg.ContainsKey($Color)) { $AnsiFg[$Color] } else { "0" } [Console]::Write("$([char]27)[$code" + "m$Text$([char]27)[0m") if (-not $NoNewline) { [Console]::WriteLine("") } } else { $mapped = @{ Gray = "Gray"; Green = "Green"; Yellow = "Yellow"; Red = "Red"; Cyan = "Cyan"; DarkGray = "DarkGray" } Write-Host $Text -ForegroundColor $mapped[$Color] } } function Write-Title([string] $Text) { Write-Ui " $Text" "Green" } function Write-Section([string] $Text) { Write-Ui "" ; Write-Ui " $Text" "Cyan" } function Write-Ok([string] $Text) { Write-Ui " $($Sym.Tick) $Text" "Green" } function Write-Step([string] $Text) { Write-Ui " $($Sym.Branch) $Text" "Gray" } function Write-Note([string] $Text) { Write-Ui " $Text" "DarkGray" } function Fail([string] $Text, [string] $Code = "INSTALL_FAILED") { $action = switch ($Code) { "DOWNLOAD_FAILED" { "Check your internet connection and retry." } "CHECKSUM_FAILED" { "The download may be corrupt or the release incomplete. Retry; if it persists, report it." } "VERIFY_FAILED" { "Remove any older seedcode on PATH and retry the installation." } "PERMISSION_DENIED" { "Choose a writable -InstallDir and retry." } "MISSING_DEPENDENCY" { "Install the missing component and retry." } "ENVIRONMENT_FAILED" { "Check the environment (TLS, temp directory, user profile) and retry." } default { "Retry the installation. If it keeps failing, report it with the error code." } } try { [Console]::CursorVisible = $true } catch { } Write-Ui "" Write-Ui " $($Sym.Cross) Installation failed" "Red" Write-Ui "" Write-Ui " Reason:" "Gray" foreach ($line in ($Text -split "`n")) { Write-Ui " $line" "Gray" } Write-Ui "" Write-Ui " Suggested action:" "Gray" Write-Ui " $action" "Gray" Write-Ui "" Write-Ui " Error code: $Code" "DarkGray" Write-Ui "" Write-Ui " Download manually from: https://github.com/$Repo/releases" "DarkGray" Write-Ui "" exit 1 } function Show-Banner { $width = 80 try { if (-not $UiRedirected) { $width = [Console]::WindowWidth } } catch { } Write-Ui "" if ($width -ge 56) { $logo = @( ' _____ _ _____ _', ' / ____| | |/ ____| | |', ' | (___ ___ ___ __| | | ___ __| | ___', ' \___ \ / _ \/ _ \ / _` | | / _ \ / _` |/ _ \', ' ____) | __/ __/ (_| | |___| (_) | (_| | __/', ' |_____/ \___|\___|\__,_|\_____\___/ \__,_|\___|' ) foreach ($line in $logo) { Write-Ui " $line" "Gray" } } else { Write-Ui " SEED CODE CLI" "White" } Write-Ui " SEED CODE CLI $($Sym.Dot) AI CODING AGENT" "White" Write-Ui " Version $Version" "Gray" Write-Ui "" } function Write-Status([string] $Text) { Write-Ui " $($Sym.Node) $Text" "Green" } function Write-Success([string] $Text) { Write-Ui " $($Sym.Tick) $Text" "Green" } function Write-Warning([string] $Text) { Write-Ui " ! $Text" "Yellow" } function Write-ErrorMessage([string] $Text) { Write-Ui " $($Sym.Cross) $Text" "Red" } function Show-Progress { param([int] $Current, [int] $Total = 7, [string] $Label = "") if ($Total -le 0) { $Total = 1 } $barWidth = 24 $filled = [int]($barWidth * $Current / $Total) $bar = ($Sym.Full * $filled) + ($Sym.Empty * ($barWidth - $filled)) Write-Ui " [$bar] $Current/$Total $Label" "Gray" } function Show-Stage { param([int] $Number, [string] $Label) Show-Progress -Current $Number -Total 7 -Label $Label } function Show-Spinner { param([string] $Text, [scriptblock] $Action, [object] $Argument) if ($null -eq $Action) { return $null } if ($UiRedirected -or -not $UiAnsi) { return (& $Action $Argument) } $runspace = $null try { $runspace = [PowerShell]::Create() [void] $runspace.AddScript($Action.ToString()) if ($null -ne $Argument) { [void] $runspace.AddArgument($Argument) } $async = $runspace.BeginInvoke() $frames = @('|', '/', '-', '\') $index = 0 while (-not $async.IsCompleted) { [Console]::Write("`r $($frames[$index % 4]) $Text") Start-Sleep -Milliseconds 100 $index++ } $output = $runspace.EndInvoke($async) try { [Console]::Write("`r" + (" " * 60) + "`r") } catch { } if ($null -ne $output -and $output.Count -gt 0) { return $output[$output.Count - 1] } return $null } catch { # The spinner is cosmetic: never let it break the installation. try { [Console]::Write("`r" + (" " * 60) + "`r") } catch { } return (& $Action $Argument) } finally { if ($null -ne $runspace) { try { $runspace.Dispose() } catch { } } } } function Draw-Progress { param([long] $Received, [long] $Total) if ($UiRedirected -or -not $UiAnsi) { return } $pct = if ($Total -gt 0) { [int](100 * $Received / $Total) } else { 0 } $width = 24 $filled = if ($Total -gt 0) { [int]($width * $Received / $Total) } else { 0 } $bar = ($Sym.Full * $filled) + ($Sym.Empty * ($width - $filled)) try { [Console]::CursorVisible = $false } catch { } Write-Ui (" $($Sym.Branch) $bar {0,3}% ({1}/{2})" -f $pct, (Format-MB $Received), (Format-MB $Total)) -NoNewline Write-Ui "`r" "Gray" -NoNewline } function Format-MB([long] $Bytes) { if ($Bytes -ge 1MB) { return ("{0:N1} MB" -f ($Bytes / 1MB)) } if ($Bytes -ge 1KB) { return ("{0:N0} KB" -f ($Bytes / 1KB)) } return "$Bytes B" } function Clear-ProgressLine { if ($UiRedirected -or -not $UiAnsi) { return } Write-Ui (" " * 100) -NoNewline Write-Ui "`r" -NoNewline try { [Console]::CursorVisible = $true } catch { } } # --- environment -------------------------------------------------------------- function Enable-Tls12 { # Windows PowerShell 5.1 defaults to TLS 1.0 on some hosts; GitHub needs 1.2+. try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { Fail "Could not enable TLS 1.2 on this host: $($_.Exception.Message)" } } function Get-Arch { # PROCESSOR_ARCHITEW6432 is set when a 32-bit host reports a 64-bit machine. $arch = $env:PROCESSOR_ARCHITECTURE if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 } switch ($arch) { "AMD64" { return "x64" } "ARM64" { return "arm64" } default { Fail "Unsupported processor architecture '$arch'. Seed Code ships x64 and arm64 Windows builds." } } } function Get-DefaultInstallDir { $base = $env:LOCALAPPDATA if (-not $base) { $base = [Environment]::GetFolderPath("LocalApplicationData") } if (-not $base) { Fail "Could not resolve %LOCALAPPDATA% for a per-user installation." } return (Join-Path $base "Programs\SeedCode") } # One SHA256SUMS.txt entry, as a regex: # ^<64 hex digits>[*]$ # The separator is deliberately NOT assumed to be a single space: GNU # sha256sum writes two spaces, other tools emit tabs, and a checksum file # staged on Windows can arrive with CRLF line endings. '*' is the binary-mode # marker some sha256sum builds write; it is not part of the file name. $SumsEntryPattern = '^([0-9A-Fa-f]{64})[ \t]+[*]?(.+?)[ \t]*$' function Get-RemoteText([string] $Url) { try { $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 60 $content = $response.Content # GitHub serves release assets as application/octet-stream, and on # Windows PowerShell 5.1 Invoke-WebRequest then hands back a byte[] # instead of text. Decoding it here is what makes the checksum lookup # see real lines instead of a string of byte values. if ($content -is [byte[]]) { $content = [Text.Encoding]::UTF8.GetString($content) } return [string] $content } catch { return $null } } function Get-ExpectedHash($SumsText, [string] $FileName) { # $SumsText is untyped on purpose: Windows PowerShell 5.1 hands back a # byte[] for a GitHub release asset (served as application/octet-stream), # and a [string] parameter would coerce it to "System.Byte[]" before the # check below could ever run. # Returns the expected lowercase SHA256 for $FileName, or $null when the # release does not list it. Parsing is whitespace-tolerant (spaces, tabs, # CRLF) and matches the file name exactly, case-insensitively; the hash # itself must still be 64 hex digits or the line is ignored. if (-not $SumsText) { return $null } if ($SumsText -is [byte[]]) { $SumsText = [Text.Encoding]::UTF8.GetString($SumsText) } $want = "$FileName".Trim() if (-not $want) { return $null } foreach ($line in ([string] $SumsText -split "\r?\n")) { $trimmed = $line.Trim() if (-not $trimmed) { continue } if ($trimmed.StartsWith("#")) { continue } # comment/digest header lines $match = [regex]::Match($trimmed, $SumsEntryPattern) if (-not $match.Success) { continue } $listed = $match.Groups[2].Value.Trim() if ($listed -ieq $want) { return $match.Groups[1].Value.ToLowerInvariant() } } return $null } function Save-RemoteFileWithProgress([string] $Url, [string] $Destination, [string] $DisplayName) { # Real, byte-driven download progress (HTTP streaming - no fake animation, # no artificial delays). Falls back to a plain single-line download when # the terminal cannot render it (redirected output, no ANSI). $request = [Net.HttpWebRequest]::Create($Url) $request.UserAgent = $UserAgent $request.Timeout = 60000 $request.ReadWriteTimeout = 300000 $request.AllowAutoRedirect = $true try { $response = $request.GetResponse() } catch { Fail "Could not download $Url`n $($_.Exception.Message)" } try { $stream = $response.GetResponseStream() $output = [IO.File]::Open($Destination, [IO.FileMode]::Create, [IO.FileAccess]::Write) try { $buffer = New-Object byte[] 262144 $received = [long] 0 $total = $response.ContentLength $lastPct = -1 while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) { $output.Write($buffer, 0, $read) $received += $read if (($total -gt 0) -and ($UiAnsi -and -not $UiRedirected)) { $pct = [int](100 * $received / $total) if ($pct -ne $lastPct) { $lastPct = $pct Draw-Progress $received $total } } } } finally { $stream.Dispose() $output.Dispose() } } finally { $response.Close() } Clear-ProgressLine if (-not (Test-Path -LiteralPath $Destination)) { Fail "Download produced no file: $Url" } } function Test-InstalledVersion([string] $ExePath, [string] $Expected) { if (-not (Test-Path -LiteralPath $ExePath)) { return $false } try { $reported = (& $ExePath --version 2>&1 | Out-String).Trim() } catch { return $false } if (-not $reported) { return $false } # Boundary-checked match, not a substring search: '19.3.2' must not pass # for '9.3.2', '9.3.21' must not pass for '9.3.2', and the optional # v/V version marker must be a single marker, not part of a longer word. # The same rules are implemented in install.sh (version_ok) and # setup.iss (VersionInOutput). $pattern = [regex]::Escape($Expected) $plain = [regex]::IsMatch($reported, "(?&1 | Out-String).Trim() } catch { $reported = "(failed to run)" } if ($reported -notlike "*$Version*") { Fail "The installed binary did not report version $Version (it said: '$reported')." "VERIFY_FAILED" } Write-Ok "seedcode --version -> $reported" $onPath = $false $resolvedTo = "" try { $found = Get-Command seedcode -ErrorAction SilentlyContinue if ($found) { $onPath = $true $resolvedTo = [string] $found.Source } } catch { $onPath = $false } # A different `seedcode` earlier on PATH keeps being invoked instead of the one # just installed (the "old executable still runs" failure mode). Say so out # loud instead of letting the user believe the upgrade took effect. if ($onPath -and $resolvedTo -and -not ($resolvedTo -like "$InstallDir\*")) { Write-Ui "" Write-Warning "Another seedcode is earlier on PATH:" Write-Ui " $resolvedTo" "DarkGray" Write-Ui " (this install: $TargetExe)" "DarkGray" Write-Ui " Remove the older copy, or run this one directly:" "DarkGray" Write-Ui " & `"$TargetExe`" --version" "DarkGray" } try { [Console]::CursorVisible = $true } catch { } Write-Ui "" Write-Ui " $RuleText" "DarkGray" Write-Ui " $($Sym.Tick) Seed Code CLI installed successfully!" "Green" Write-Ui "" Write-Ui " Version: $Version" "Gray" Write-Ui "" Write-Ui " Get started:" "Gray" if ($NoPathUpdate) { Write-Ui " & `"$TargetExe`" --version" "White" Write-Ui " & `"$TargetExe`"" "White" } elseif ($onPath) { Write-Ui " seedcode --version" "White" Write-Ui " seedcode" "White" } else { Write-Ui " seedcode --version (after restarting your terminal)" "White" Write-Ui " seedcode" "White" Write-Ui " PATH changes apply to new sessions." "DarkGray" } Write-Ui "" Write-Ui " Documentation:" "Gray" Write-Ui " https://seedcode-cli.vercel.app/" "DarkGray" Write-Ui "" Write-Ui " Happy coding!" "Green" Write-Ui "" Write-Ui " License: PolyForm Noncommercial License 1.0.0" "DarkGray" Write-Ui ""