# Yunagi installer for Windows. # # irm https://getyunagi.com/install.ps1 | iex # # What it does: docs/release/07-build-and-distribution.md section 4.2. # Environment variables (all optional): # YUNAGI_VERSION version to install (default: the "version" in latest.json). "v1.2.3" and "1.2.3" both work. # YUNAGI_BASE_URL where release files are (default: GitHub Releases). Files are read from /v/. # YUNAGI_LATEST_URL where latest.json is (default: the official site). # # Keep this file ASCII only: Windows PowerShell 5.1 reads a file without a BOM as ANSI. # Everything is in functions and the last line calls Install-Yunagi, so a download that is cut off # halfway never runs a half-written command. Setting YUNAGI_INSTALL_NO_RUN=1 loads the functions # without installing (the tests use this). # Where release files and latest.json are read from (docs/release/07 sections 2 and 4.2). $script:DefaultBaseUrl = 'https://github.com/misty-swan/yunagi/releases/download' $script:DefaultLatestUrl = 'https://getyunagi.com/latest.json' function Stop-Install([string]$Message) { throw "install.ps1: $Message" } # Returns 'x86_64' or 'arm64'. Anything else stops the installation. function Get-YunagiArch([string]$Architecture) { switch ($Architecture) { 'X64' { return 'x86_64' } 'Arm64' { return 'arm64' } default { Stop-Install "unsupported architecture: $Architecture (supported: x64, arm64)." } } } # Returns the version without a leading 'v'. Stops when it has an unexpected shape. function ConvertTo-YunagiVersion([string]$Version) { $v = $Version.Trim() if ($v.StartsWith('v')) { $v = $v.Substring(1) } if ($v -notmatch '^[0-9][0-9A-Za-z.+-]*$') { Stop-Install "invalid version: $Version" } return $v } # Reads the hash from the text of a .sha256 file (" " or just ""). function Read-Sha256File([string]$Text) { $first = ($Text.Trim() -split '\s+')[0].ToLowerInvariant() if ($first -notmatch '^[0-9a-f]{64}$') { Stop-Install 'the .sha256 file does not hold a SHA-256 hash.' } return $first } # Stops when the file's SHA-256 differs from the expected one. function Assert-Sha256([string]$Path, [string]$Expected) { $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() if ($actual -ne $Expected) { Stop-Install "SHA-256 mismatch for $(Split-Path -Leaf $Path) (expected $Expected, got $actual). Nothing was installed." } } # Adds Dir to the user PATH when it is not there yet. Returns the new value, or $null when nothing changed. function Get-PathWithEntry([string]$Current, [string]$Dir) { $entries = @() if ($Current) { $entries = @($Current.Split(';') | Where-Object { $_.Trim() -ne '' }) } $wanted = $Dir.TrimEnd('\') foreach ($e in $entries) { # An entry may still hold %VAR% (the registry value is read unexpanded): compare the expanded form too. $expanded = [Environment]::ExpandEnvironmentVariables($e) if ($e.TrimEnd('\') -ieq $wanted -or $expanded.TrimEnd('\') -ieq $wanted) { return $null } } return (@($entries) + $Dir) -join ';' } function Save-File([string]$Url, [string]$Path) { try { Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Path } catch { Stop-Install "could not download $Url ($($_.Exception.Message))" } } function Install-Yunagi { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # the progress bar makes Windows PowerShell 5.1 very slow # Windows PowerShell 5.1 may not offer TLS 1.2 by default [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $arch = Get-YunagiArch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("yunagi-install-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $tmp | Out-Null try { $version = $env:YUNAGI_VERSION if (-not $version) { $latestUrl = if ($env:YUNAGI_LATEST_URL) { $env:YUNAGI_LATEST_URL } else { $script:DefaultLatestUrl } $latestFile = Join-Path $tmp 'latest.json' Save-File $latestUrl $latestFile $version = (Get-Content -Raw -LiteralPath $latestFile | ConvertFrom-Json).version if (-not $version) { Stop-Install "could not read the version from $latestUrl" } } $version = ConvertTo-YunagiVersion $version Write-Host "Installing Yunagi $version (windows, $arch)" $base = if ($env:YUNAGI_BASE_URL) { $env:YUNAGI_BASE_URL } else { $script:DefaultBaseUrl } $base = $base.TrimEnd('/') # a trailing slash would make "//" in the URL $name = "Yunagi_${version}_${arch}-setup.exe" $setup = Join-Path $tmp $name Save-File "$base/v$version/$name" $setup Save-File "$base/v$version/$name.sha256" "$setup.sha256" Assert-Sha256 $setup (Read-Sha256File (Get-Content -Raw -LiteralPath "$setup.sha256")) Write-Host "Verified $name" $installDir = Join-Path $env:LOCALAPPDATA 'Programs\Yunagi' # Only the app itself counts: the name 'Yunagi' also matches the command yunagi.exe (for example # a running MCP server), which does not block the installer. $appPath = Join-Path $installDir 'Yunagi.exe' if (@(Get-Process -Name 'Yunagi' -ErrorAction SilentlyContinue | Where-Object { $_.Path -ieq $appPath }).Count -gt 0) { Stop-Install 'Yunagi is running. Quit it and run this again.' } # NSIS: /S is silent, /D sets the install folder (it must be the last argument, without quotes). $process = Start-Process -FilePath $setup -ArgumentList @('/S', "/D=$installDir") -Wait -PassThru if ($process.ExitCode -ne 0) { Stop-Install "the installer failed (exit code $($process.ExitCode))." } $binDir = Join-Path $installDir 'bin' if (-not (Test-Path -LiteralPath (Join-Path $binDir 'yunagi.exe'))) { Stop-Install "the installer finished but $binDir\yunagi.exe was not found." } # Read and write the registry value directly: [Environment]::GetEnvironmentVariable expands %VAR% # and SetEnvironmentVariable would store the expanded text as a plain string. $envKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true) try { $userPath = [string]$envKey.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $newPath = Get-PathWithEntry $userPath $binDir if ($newPath) { $envKey.SetValue('Path', $newPath, [Microsoft.Win32.RegistryValueKind]::ExpandString) } } finally { $envKey.Close() } if ($newPath) { # Writing the registry directly does not tell running programs (Explorer, which starts new terminals) # that the environment changed. Setting and removing a throwaway user variable through .NET sends that # notice (WM_SETTINGCHANGE), so a terminal opened afterwards sees the new PATH without signing out. [Environment]::SetEnvironmentVariable('YUNAGI_ENV_REFRESH', '1', 'User') [Environment]::SetEnvironmentVariable('YUNAGI_ENV_REFRESH', $null, 'User') } if ($null -ne (Get-PathWithEntry $env:Path $binDir)) { $env:Path = "$env:Path;$binDir" } Write-Host '' Write-Host "Installed Yunagi ${version}:" Write-Host " app $installDir\Yunagi.exe" Write-Host " command $binDir\yunagi.exe (added to your user PATH)" Write-Host ' Start menu Yunagi' if ($newPath) { Write-Host 'Open a new terminal to use the yunagi command.' } } finally { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue } } if (-not $env:YUNAGI_INSTALL_NO_RUN) { Install-Yunagi }