1179 lines
41 KiB
PowerShell
1179 lines
41 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Install jcode on Windows.
|
|
.DESCRIPTION
|
|
Downloads the latest jcode release and installs it to %LOCALAPPDATA%\jcode\bin.
|
|
|
|
One-liner install:
|
|
irm https://jcode.sh/install.ps1 | iex
|
|
|
|
Or download and run (allows parameters):
|
|
& ([scriptblock]::Create((irm https://jcode.sh/install.ps1)))
|
|
.PARAMETER InstallDir
|
|
Override the installation directory (default: $env:LOCALAPPDATA\jcode\bin)
|
|
.PARAMETER Version
|
|
Override the version tag to install. Required when using a local artifact path.
|
|
.PARAMETER ArtifactExePath
|
|
Use a local jcode.exe artifact instead of downloading from GitHub.
|
|
.PARAMETER ArtifactTgzPath
|
|
Use a local jcode .tar.gz artifact instead of downloading from GitHub.
|
|
.PARAMETER BuildFromSource
|
|
If no prebuilt release asset is available, explicitly allow a source build.
|
|
Source builds require Git, Rust, and the Visual Studio C++ Build Tools.
|
|
.PARAMETER ConfigureAlacritty
|
|
Install Alacritty through winget when it is not already available.
|
|
.PARAMETER ConfigureHotkey
|
|
Configure the optional global launch hotkey.
|
|
.PARAMETER SkipAlacrittySetup
|
|
Deprecated compatibility switch. Alacritty setup is opt-in by default.
|
|
.PARAMETER SkipHotkeySetup
|
|
Deprecated compatibility switch. Hotkey setup is opt-in by default.
|
|
#>
|
|
param(
|
|
[string]$InstallDir,
|
|
[string]$Version,
|
|
[string]$ArtifactExePath,
|
|
[string]$ArtifactTgzPath,
|
|
[switch]$BuildFromSource,
|
|
[switch]$ConfigureAlacritty,
|
|
[switch]$ConfigureHotkey,
|
|
[switch]$SkipAlacrittySetup,
|
|
[switch]$SkipHotkeySetup
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
if ($PSVersionTable.PSVersion.Major -lt 5) {
|
|
Write-Host "error: PowerShell 5.1 or later is required" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$Repo = "1jehuang/jcode"
|
|
$ReleaseMetadataBase = if ($env:JCODE_RELEASE_METADATA_BASE) {
|
|
$env:JCODE_RELEASE_METADATA_BASE.TrimEnd('/')
|
|
} else {
|
|
"https://jcode.sh/releases"
|
|
}
|
|
|
|
if (-not $InstallDir) {
|
|
$localAppData = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) }
|
|
if (-not $localAppData -and $env:USERPROFILE) { $localAppData = Join-Path $env:USERPROFILE "AppData\Local" }
|
|
$InstallDir = Join-Path $localAppData "jcode\bin"
|
|
}
|
|
|
|
$JcodeHome = if ($env:JCODE_HOME) {
|
|
$env:JCODE_HOME
|
|
} elseif ($env:USERPROFILE) {
|
|
Join-Path $env:USERPROFILE ".jcode"
|
|
} else {
|
|
Join-Path ([Environment]::GetFolderPath("UserProfile")) ".jcode"
|
|
}
|
|
|
|
$HotkeyDir = Join-Path $JcodeHome "hotkey"
|
|
$SetupHintsPath = Join-Path $JcodeHome "setup_hints.json"
|
|
|
|
function Write-Info($msg) { Write-Host $msg -ForegroundColor Blue }
|
|
function Write-Err($msg) { throw "error: $msg" }
|
|
function Write-Warn($msg) { Write-Host "warning: $msg" -ForegroundColor Yellow }
|
|
|
|
function ConvertFrom-JcodeWebContent($Content) {
|
|
if ($null -eq $Content) { return "" }
|
|
|
|
# Windows PowerShell 5.1 returns Byte[] for some text responses when the
|
|
# server uses application/octet-stream (including jcode.sh metadata and
|
|
# GitHub release checksum manifests). Casting Byte[] directly to [string]
|
|
# produces a space-separated list of decimal bytes instead of the text.
|
|
if ($Content -is [byte[]]) {
|
|
return [System.Text.Encoding]::UTF8.GetString($Content)
|
|
}
|
|
|
|
return [string]$Content
|
|
}
|
|
|
|
function Test-JcodeReleaseTag([string]$Tag) {
|
|
return [bool]($Tag -match '^v[0-9]+\.[0-9]+\.[0-9]+(?:[+.-][A-Za-z0-9.-]+)?$')
|
|
}
|
|
|
|
function Resolve-JcodeReleaseTagFromUri([string]$Uri) {
|
|
if (-not $Uri) { return $null }
|
|
if ($Uri -match '/releases/tag/([^/?#]+)') {
|
|
return [Uri]::UnescapeDataString($Matches[1])
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Get-LatestJcodeReleaseTag {
|
|
# Avoid api.github.com here. Its unauthenticated limit is only 60 requests
|
|
# per public IP per hour, so installs are unreliable behind shared NAT/VPNs.
|
|
$metadataTag = $null
|
|
try {
|
|
$metadataResponse = Invoke-WebRequest -UseBasicParsing -Uri "$ReleaseMetadataBase/latest/version"
|
|
$candidate = (ConvertFrom-JcodeWebContent -Content $metadataResponse.Content).Trim()
|
|
if (Test-JcodeReleaseTag $candidate) { $metadataTag = $candidate }
|
|
} catch {}
|
|
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Method Head -Uri "https://github.com/$Repo/releases/latest"
|
|
$baseResponse = $response.BaseResponse
|
|
$resolvedUri = $null
|
|
|
|
if ($baseResponse) {
|
|
$responseUriProperty = $baseResponse.PSObject.Properties['ResponseUri']
|
|
if ($responseUriProperty -and $responseUriProperty.Value) {
|
|
$resolvedUri = [string]$responseUriProperty.Value
|
|
}
|
|
|
|
if (-not $resolvedUri) {
|
|
$requestMessageProperty = $baseResponse.PSObject.Properties['RequestMessage']
|
|
if ($requestMessageProperty -and $requestMessageProperty.Value) {
|
|
$resolvedUri = [string]$requestMessageProperty.Value.RequestUri
|
|
}
|
|
}
|
|
}
|
|
|
|
$tag = Resolve-JcodeReleaseTagFromUri $resolvedUri
|
|
if ($tag) { return $tag }
|
|
} catch {
|
|
if (-not $metadataTag) {
|
|
Write-Err "Failed to determine latest version: $_"
|
|
}
|
|
}
|
|
|
|
if ($metadataTag) {
|
|
Write-Warn "GitHub release lookup unavailable; using cached jcode.sh metadata ($metadataTag)."
|
|
return $metadataTag
|
|
}
|
|
Write-Err "Failed to determine latest version"
|
|
}
|
|
|
|
function Get-JcodeReleaseDownloadBases([string]$ReleaseTag) {
|
|
$bases = New-Object System.Collections.Generic.List[string]
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri "$ReleaseMetadataBase/$ReleaseTag/download-bases"
|
|
foreach ($line in ((ConvertFrom-JcodeWebContent -Content $response.Content) -split "`r?`n")) {
|
|
$candidate = $line.Trim().TrimEnd('/')
|
|
if ($candidate -match '^https://\S+$' -and -not $bases.Contains($candidate)) {
|
|
$bases.Add($candidate)
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
$githubBase = "https://github.com/$Repo/releases/download/$ReleaseTag"
|
|
if (-not $bases.Contains($githubBase)) { $bases.Add($githubBase) }
|
|
return $bases.ToArray()
|
|
}
|
|
|
|
function Get-JcodeSha256FromManifest {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$ManifestText,
|
|
[Parameter(Mandatory = $true)][string]$AssetName
|
|
)
|
|
|
|
foreach ($line in ($ManifestText -split "`r?`n")) {
|
|
if ($line -match '^\s*([0-9a-fA-F]{64})\s+\*?(.+?)\s*$') {
|
|
$candidateName = [System.IO.Path]::GetFileName($Matches[2])
|
|
if ($candidateName -eq $AssetName) {
|
|
return $Matches[1].ToLowerInvariant()
|
|
}
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Get-ReleaseChecksum([string]$ReleaseTag, [string]$AssetName) {
|
|
$lastError = $null
|
|
foreach ($checksumUrl in @(
|
|
"$ReleaseMetadataBase/$ReleaseTag/SHA256SUMS",
|
|
"https://github.com/$Repo/releases/download/$ReleaseTag/SHA256SUMS"
|
|
)) {
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl
|
|
$expected = Get-JcodeSha256FromManifest -ManifestText (ConvertFrom-JcodeWebContent -Content $response.Content) -AssetName $AssetName
|
|
if ($expected) { return $expected }
|
|
} catch {
|
|
$lastError = $_
|
|
}
|
|
}
|
|
|
|
if ($lastError) {
|
|
Write-Err "Could not download SHA256SUMS for $ReleaseTag. Refusing to install an unverified download: $lastError"
|
|
}
|
|
Write-Err "SHA256SUMS for $ReleaseTag does not list $AssetName"
|
|
}
|
|
|
|
function Assert-JcodeFileChecksum([string]$FilePath, [string]$ExpectedSha256, [string]$AssetName) {
|
|
try {
|
|
$actual = (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
} catch {
|
|
Write-Err "Could not calculate SHA256 for ${AssetName}: $_"
|
|
}
|
|
|
|
if ($actual -ne $ExpectedSha256) {
|
|
Remove-Item -LiteralPath $FilePath -Force -ErrorAction SilentlyContinue
|
|
Write-Err "SHA256 verification failed for $AssetName (expected $ExpectedSha256, got $actual)"
|
|
}
|
|
|
|
Write-Info "Verified SHA256: $AssetName"
|
|
return $actual
|
|
}
|
|
|
|
function Get-JcodeLocalAppDataDir {
|
|
if ($env:LOCALAPPDATA) {
|
|
return $env:LOCALAPPDATA
|
|
}
|
|
|
|
$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
|
if ($localAppData) {
|
|
return $localAppData
|
|
}
|
|
|
|
if ($env:USERPROFILE) {
|
|
return (Join-Path $env:USERPROFILE "AppData\Local")
|
|
}
|
|
|
|
return (Join-Path ([Environment]::GetFolderPath("UserProfile")) "AppData\Local")
|
|
}
|
|
|
|
function Get-DefaultJcodeInstallDir {
|
|
return (Join-Path (Get-JcodeLocalAppDataDir) "jcode\bin")
|
|
}
|
|
|
|
function ConvertTo-JcodePathKey([string]$PathValue) {
|
|
if (-not $PathValue) {
|
|
return ""
|
|
}
|
|
|
|
$clean = [Environment]::ExpandEnvironmentVariables($PathValue.Trim().Trim('"'))
|
|
if (-not $clean) {
|
|
return ""
|
|
}
|
|
|
|
try {
|
|
$clean = [System.IO.Path]::GetFullPath($clean)
|
|
} catch {
|
|
}
|
|
|
|
|
|
$clean = $clean.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
|
|
return $clean.ToUpperInvariant()
|
|
}
|
|
|
|
function Split-JcodePathList([string]$PathValue) {
|
|
if (-not $PathValue) {
|
|
return @()
|
|
}
|
|
|
|
$entries = @()
|
|
foreach ($entry in ($PathValue -split ';')) {
|
|
$clean = $entry.Trim().Trim('"')
|
|
if ($clean) {
|
|
$entries += $clean
|
|
}
|
|
}
|
|
return $entries
|
|
}
|
|
|
|
function Join-JcodePathList([string[]]$Entries) {
|
|
if (-not $Entries -or $Entries.Count -eq 0) {
|
|
return ""
|
|
}
|
|
|
|
return ($Entries -join ';')
|
|
}
|
|
|
|
function Get-JcodeManagedPathKeys([string]$InstallDir) {
|
|
$keys = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
|
foreach ($candidate in @($InstallDir, (Get-DefaultJcodeInstallDir))) {
|
|
$key = ConvertTo-JcodePathKey $candidate
|
|
if ($key) {
|
|
[void]$keys.Add($key)
|
|
}
|
|
}
|
|
return $keys
|
|
}
|
|
|
|
function Resolve-JcodePathUpdate {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
[AllowNull()][string]$CurrentPath,
|
|
[switch]$RemoveOnly
|
|
)
|
|
|
|
$managedKeys = Get-JcodeManagedPathKeys -InstallDir $InstallDir
|
|
$nextEntries = @()
|
|
$removedManaged = 0
|
|
|
|
foreach ($entry in (Split-JcodePathList $CurrentPath)) {
|
|
$key = ConvertTo-JcodePathKey $entry
|
|
if (-not $key) {
|
|
continue
|
|
}
|
|
|
|
if ($managedKeys.Contains($key)) {
|
|
$removedManaged += 1
|
|
continue
|
|
}
|
|
|
|
$nextEntries += $entry
|
|
}
|
|
|
|
if (-not $RemoveOnly) {
|
|
$nextEntries = @($InstallDir) + $nextEntries
|
|
}
|
|
|
|
$nextPath = Join-JcodePathList $nextEntries
|
|
$changed = ($nextPath -ne ([string]$CurrentPath))
|
|
|
|
return [pscustomobject]@{
|
|
Path = $nextPath
|
|
Changed = $changed
|
|
RemovedManagedEntries = $removedManaged
|
|
RemovedDuplicateEntries = 0
|
|
AddedLauncherEntry = (-not $RemoveOnly)
|
|
InstallDir = $InstallDir
|
|
}
|
|
}
|
|
|
|
function Send-JcodeEnvironmentChangedBroadcast {
|
|
if ($env:JCODE_DISABLE_ENV_BROADCAST -eq "1") {
|
|
return $false
|
|
}
|
|
|
|
if (-not ("Jcode.EnvironmentBroadcast" -as [type])) {
|
|
Add-Type -TypeDefinition @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
namespace Jcode {
|
|
public static class EnvironmentBroadcast {
|
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
|
public static extern IntPtr SendMessageTimeout(
|
|
IntPtr hWnd,
|
|
UInt32 Msg,
|
|
UIntPtr wParam,
|
|
string lParam,
|
|
UInt32 fuFlags,
|
|
UInt32 uTimeout,
|
|
out UIntPtr lpdwResult);
|
|
}
|
|
}
|
|
"@
|
|
}
|
|
|
|
$result = [UIntPtr]::Zero
|
|
[Jcode.EnvironmentBroadcast]::SendMessageTimeout([IntPtr]0xffff, 0x001A, [UIntPtr]::Zero, "Environment", 0x0002, 5000, [ref]$result) | Out-Null
|
|
return $true
|
|
}
|
|
|
|
function Set-JcodeUserPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
[AllowNull()][string]$CurrentPath,
|
|
[scriptblock]$SetUserPathAction,
|
|
[scriptblock]$BroadcastAction,
|
|
[bool]$Broadcast = $true
|
|
)
|
|
|
|
if (-not $PSBoundParameters.ContainsKey('CurrentPath')) {
|
|
$CurrentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
}
|
|
|
|
$update = Resolve-JcodePathUpdate -InstallDir $InstallDir -CurrentPath $CurrentPath
|
|
$broadcasted = $false
|
|
|
|
if ($update.Changed) {
|
|
if ($SetUserPathAction) {
|
|
& $SetUserPathAction $update.Path
|
|
} else {
|
|
[Environment]::SetEnvironmentVariable("Path", $update.Path, "User")
|
|
}
|
|
|
|
if ($Broadcast) {
|
|
if ($BroadcastAction) {
|
|
& $BroadcastAction | Out-Null
|
|
} else {
|
|
Send-JcodeEnvironmentChangedBroadcast | Out-Null
|
|
}
|
|
$broadcasted = $true
|
|
}
|
|
}
|
|
|
|
$update | Add-Member -NotePropertyName Broadcasted -NotePropertyValue $broadcasted
|
|
return $update
|
|
}
|
|
|
|
function Remove-JcodeUserPath {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$InstallDir,
|
|
[AllowNull()][string]$CurrentPath,
|
|
[scriptblock]$SetUserPathAction,
|
|
[scriptblock]$BroadcastAction,
|
|
[bool]$Broadcast = $true
|
|
)
|
|
|
|
if (-not $PSBoundParameters.ContainsKey('CurrentPath')) {
|
|
$CurrentPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
}
|
|
|
|
$update = Resolve-JcodePathUpdate -InstallDir $InstallDir -CurrentPath $CurrentPath -RemoveOnly
|
|
$broadcasted = $false
|
|
|
|
if ($update.Changed) {
|
|
if ($SetUserPathAction) {
|
|
& $SetUserPathAction $update.Path
|
|
} else {
|
|
[Environment]::SetEnvironmentVariable("Path", $update.Path, "User")
|
|
}
|
|
|
|
if ($Broadcast) {
|
|
if ($BroadcastAction) {
|
|
& $BroadcastAction | Out-Null
|
|
} else {
|
|
Send-JcodeEnvironmentChangedBroadcast | Out-Null
|
|
}
|
|
$broadcasted = $true
|
|
}
|
|
}
|
|
|
|
$update | Add-Member -NotePropertyName Broadcasted -NotePropertyValue $broadcasted
|
|
return $update
|
|
}
|
|
|
|
function Set-JcodeProcessPath([string]$InstallDir) {
|
|
$update = Resolve-JcodePathUpdate -InstallDir $InstallDir -CurrentPath $env:Path
|
|
$env:Path = $update.Path
|
|
return $update
|
|
}
|
|
|
|
function Remove-JcodeStaleLauncherBackups {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$LauncherDir
|
|
)
|
|
|
|
Get-ChildItem -LiteralPath $LauncherDir -Filter '.jcode-launcher-old-*.exe' -File -Force -ErrorAction SilentlyContinue |
|
|
Remove-Item -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Install-JcodeLauncher {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$SourcePath,
|
|
[Parameter(Mandatory = $true)][string]$LauncherPath
|
|
)
|
|
|
|
$launcherDir = Split-Path -Parent $LauncherPath
|
|
New-Item -ItemType Directory -Path $launcherDir -Force | Out-Null
|
|
|
|
$operationId = [guid]::NewGuid().ToString('N')
|
|
$tempLauncher = Join-Path $launcherDir (".jcode-launcher-{0}.tmp.exe" -f $operationId)
|
|
$oldLauncher = Join-Path $launcherDir (".jcode-launcher-old-{0}.exe" -f $operationId)
|
|
$movedExistingLauncher = $false
|
|
try {
|
|
Copy-Item -Path $SourcePath -Destination $tempLauncher -Force
|
|
if (Test-Path -LiteralPath $LauncherPath) {
|
|
# Windows will not overwrite a loaded executable, but it does allow
|
|
# the directory entry to be renamed while the process keeps running
|
|
# from its existing file handle. Move the old launcher aside first,
|
|
# then atomically put the new binary at the stable PATH location.
|
|
Move-Item -LiteralPath $LauncherPath -Destination $oldLauncher
|
|
$movedExistingLauncher = $true
|
|
}
|
|
|
|
try {
|
|
Move-Item -LiteralPath $tempLauncher -Destination $LauncherPath
|
|
} catch {
|
|
if ($movedExistingLauncher -and -not (Test-Path -LiteralPath $LauncherPath)) {
|
|
Move-Item -LiteralPath $oldLauncher -Destination $LauncherPath
|
|
$movedExistingLauncher = $false
|
|
}
|
|
throw
|
|
}
|
|
|
|
if ($movedExistingLauncher) {
|
|
# Removal succeeds immediately for an idle launcher. If an older
|
|
# jcode process still has the renamed executable loaded, Windows
|
|
# keeps it until that process exits and the next install cleans it.
|
|
Remove-Item -LiteralPath $oldLauncher -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Only prune backups after the stable path contains the new launcher.
|
|
# Doing this before replacement could delete another concurrent
|
|
# installer's rollback file during its short rename window.
|
|
Remove-JcodeStaleLauncherBackups -LauncherDir $launcherDir
|
|
} finally {
|
|
Remove-Item -LiteralPath $tempLauncher -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
return $LauncherPath
|
|
}
|
|
|
|
function Resolve-OptionalPath([string]$PathValue) {
|
|
if (-not $PathValue) {
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
return (Resolve-Path -LiteralPath $PathValue -ErrorAction Stop).Path
|
|
} catch {
|
|
Write-Err "Provided path does not exist: $PathValue"
|
|
}
|
|
}
|
|
|
|
function Stop-ProcessTree([int]$ProcessId) {
|
|
try {
|
|
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ParentProcessId -eq $ProcessId } |
|
|
ForEach-Object { Stop-ProcessTree -ProcessId $_.ProcessId }
|
|
} catch {}
|
|
|
|
try {
|
|
Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
|
|
} catch {}
|
|
}
|
|
|
|
function Invoke-ProcessWithTimeout {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$FilePath,
|
|
[string[]]$ArgumentList = @(),
|
|
[Parameter(Mandatory = $true)][int]$TimeoutSeconds,
|
|
[Parameter(Mandatory = $true)][string]$FriendlyName,
|
|
[switch]$CaptureOutput
|
|
)
|
|
|
|
$startParams = @{
|
|
FilePath = $FilePath
|
|
ArgumentList = $ArgumentList
|
|
PassThru = $true
|
|
NoNewWindow = $true
|
|
}
|
|
|
|
$stdoutPath = $null
|
|
$stderrPath = $null
|
|
if ($CaptureOutput) {
|
|
$stdoutPath = Join-Path $env:TEMP ("jcode-{0}-{1}-stdout.log" -f $FriendlyName, [guid]::NewGuid().ToString('N'))
|
|
$stderrPath = Join-Path $env:TEMP ("jcode-{0}-{1}-stderr.log" -f $FriendlyName, [guid]::NewGuid().ToString('N'))
|
|
$startParams.RedirectStandardOutput = $stdoutPath
|
|
$startParams.RedirectStandardError = $stderrPath
|
|
}
|
|
|
|
$process = Start-Process @startParams
|
|
# Wait-Process did not gain -Timeout until newer PowerShell releases. Use
|
|
# the underlying .NET Process API so the documented PowerShell 5.1 minimum
|
|
# is real rather than only passing on PowerShell 7.
|
|
$timedOut = -not $process.WaitForExit($TimeoutSeconds * 1000)
|
|
if ($timedOut) {
|
|
Stop-ProcessTree -ProcessId $process.Id
|
|
return [pscustomobject]@{
|
|
TimedOut = $true
|
|
ExitCode = $null
|
|
StdoutPath = $stdoutPath
|
|
StderrPath = $stderrPath
|
|
}
|
|
}
|
|
|
|
# Ensure redirected streams have finished flushing before callers inspect
|
|
# their files, then refresh ExitCode from the completed process.
|
|
$process.WaitForExit()
|
|
$process.Refresh()
|
|
return [pscustomobject]@{
|
|
TimedOut = $false
|
|
ExitCode = $process.ExitCode
|
|
StdoutPath = $stdoutPath
|
|
StderrPath = $stderrPath
|
|
}
|
|
}
|
|
|
|
function Write-LogTail([string]$Path, [string]$Label) {
|
|
if (-not $Path -or -not (Test-Path $Path)) {
|
|
return
|
|
}
|
|
|
|
$lines = Get-Content -Path $Path -Tail 40 -ErrorAction SilentlyContinue
|
|
if ($lines -and $lines.Count -gt 0) {
|
|
Write-Warn "$Label (last 40 lines):"
|
|
$lines | ForEach-Object { Write-Host $_ }
|
|
}
|
|
}
|
|
|
|
function Test-CommandExists([string]$CommandName) {
|
|
return [bool](Get-Command $CommandName -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Test-AlacrittyInstalled {
|
|
return [bool](Find-AlacrittyPath)
|
|
}
|
|
|
|
function Find-AlacrittyPath {
|
|
$candidates = @(
|
|
"C:\Program Files\Alacritty\alacritty.exe",
|
|
"C:\Program Files (x86)\Alacritty\alacritty.exe"
|
|
)
|
|
|
|
if ($env:LOCALAPPDATA) {
|
|
$candidates += (Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Links\alacritty.exe")
|
|
}
|
|
|
|
foreach ($candidate in $candidates) {
|
|
if ($candidate -and (Test-Path $candidate)) {
|
|
return $candidate
|
|
}
|
|
}
|
|
|
|
try {
|
|
$command = Get-Command alacritty -ErrorAction Stop
|
|
if ($command -and $command.Source) {
|
|
return $command.Source
|
|
}
|
|
} catch {}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Install-Alacritty {
|
|
if (Test-AlacrittyInstalled) {
|
|
Write-Info "Alacritty is already installed"
|
|
return $true
|
|
}
|
|
|
|
if (-not (Test-CommandExists "winget")) {
|
|
Write-Warn "winget was not found, so Alacritty could not be installed automatically"
|
|
Write-Warn "Install App Installer / winget from Microsoft, then run: winget install -e --id Alacritty.Alacritty"
|
|
return $false
|
|
}
|
|
|
|
Write-Info "Installing Alacritty..."
|
|
$wingetArgs = @(
|
|
"install",
|
|
"-e",
|
|
"--id", "Alacritty.Alacritty",
|
|
"--accept-source-agreements",
|
|
"--accept-package-agreements",
|
|
"--disable-interactivity"
|
|
)
|
|
|
|
$wingetResult = Invoke-ProcessWithTimeout -FilePath "winget" -ArgumentList $wingetArgs -TimeoutSeconds 180 -FriendlyName "winget-install"
|
|
if ($wingetResult.TimedOut) {
|
|
Write-Warn "Alacritty install timed out after 180 seconds; skipping automatic setup"
|
|
return $false
|
|
}
|
|
|
|
if ($wingetResult.ExitCode -ne 0) {
|
|
Write-Warn "Alacritty install failed (winget exit code: $($wingetResult.ExitCode))"
|
|
return $false
|
|
}
|
|
|
|
$alacrittyPath = Find-AlacrittyPath
|
|
if (-not $alacrittyPath) {
|
|
Write-Warn "Alacritty install finished, but alacritty.exe was not found on PATH yet"
|
|
return $false
|
|
}
|
|
|
|
Write-Info "Alacritty installed: $alacrittyPath"
|
|
return $true
|
|
}
|
|
|
|
function Stop-JcodeHotkeyListeners {
|
|
try {
|
|
Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.CommandLine -like '*jcode-hotkey*' } |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
} catch {}
|
|
|
|
try {
|
|
$currentPid = $PID
|
|
Get-CimInstance Win32_Process -Filter "Name = 'jcode.exe'" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.ProcessId -ne $currentPid -and $_.CommandLine -like '*--listen-windows-hotkey*' } |
|
|
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
} catch {}
|
|
}
|
|
|
|
function ConvertFrom-JcodeVersionOutput([string]$Output) {
|
|
if (-not $Output) {
|
|
return $null
|
|
}
|
|
|
|
# A genuinely fresh profile may print the one-time telemetry notice before
|
|
# the version. When output is captured by PowerShell, terminal control
|
|
# sequences can also leave the final `jcode v...` on the same logical line.
|
|
if ($Output -match '(?i)\bjcode\s+v?([0-9][0-9A-Za-z.+-]*)') {
|
|
return "v$($Matches[1])"
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Get-JcodeVersionFromBinary([string]$BinaryPath) {
|
|
if (-not $BinaryPath -or -not (Test-Path -LiteralPath $BinaryPath)) {
|
|
return $null
|
|
}
|
|
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
try {
|
|
# Fresh profiles emit the one-time telemetry notice on stderr. Under
|
|
# Windows PowerShell with ErrorActionPreference=Stop, native stderr is
|
|
# promoted to a terminating NativeCommandError even when the process
|
|
# succeeds. Capture both streams without letting that notice abort the
|
|
# version probe.
|
|
$ErrorActionPreference = 'Continue'
|
|
$output = (& $BinaryPath --version 2>&1 | Out-String).Trim()
|
|
$exitCode = $LASTEXITCODE
|
|
if ($exitCode -ne 0) {
|
|
return $null
|
|
}
|
|
return (ConvertFrom-JcodeVersionOutput $output)
|
|
} catch {
|
|
return $null
|
|
} finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
}
|
|
|
|
function Assert-JcodeBinaryCandidate {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$BinaryPath,
|
|
[Parameter(Mandatory = $true)][string]$ExpectedVersion
|
|
)
|
|
|
|
if ($env:JCODE_INSTALL_SKIP_BINARY_VALIDATION -eq "1") {
|
|
return $null
|
|
}
|
|
|
|
$reportedVersion = Get-JcodeVersionFromBinary $BinaryPath
|
|
if (-not $reportedVersion) {
|
|
Write-Err "Downloaded jcode binary could not run '--version'. It may be corrupt, quarantined by antivirus, or built for the wrong architecture."
|
|
}
|
|
|
|
$expectedNumber = $ExpectedVersion.TrimStart('v')
|
|
if ($reportedVersion.TrimStart('v') -ne $expectedNumber) {
|
|
Write-Err "Downloaded binary reports $reportedVersion, but the installer requested $ExpectedVersion"
|
|
}
|
|
|
|
Write-Info "Validated jcode binary: $reportedVersion"
|
|
return $reportedVersion
|
|
}
|
|
|
|
function Test-JcodeMsvcBuildToolsAvailable {
|
|
if (Get-Command link.exe -ErrorAction SilentlyContinue) {
|
|
return $true
|
|
}
|
|
|
|
$programFilesX86 = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86)
|
|
if (-not $programFilesX86) {
|
|
return $false
|
|
}
|
|
|
|
$vswhere = Join-Path $programFilesX86 "Microsoft Visual Studio\Installer\vswhere.exe"
|
|
if (-not (Test-Path -LiteralPath $vswhere)) {
|
|
return $false
|
|
}
|
|
|
|
try {
|
|
$linkPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find 'VC\Tools\MSVC\**\bin\Hostx64\x64\link.exe' 2>$null | Select-Object -First 1
|
|
return [bool]$linkPath
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Assert-JcodeSourceBuildPrerequisites {
|
|
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
Write-Err "Git is required for -BuildFromSource. Install it with: winget install -e --id Git.Git"
|
|
}
|
|
if (-not (Get-Command cargo -ErrorAction SilentlyContinue) -or -not (Get-Command rustc -ErrorAction SilentlyContinue)) {
|
|
Write-Err "Rust is required for -BuildFromSource. Install it from https://rustup.rs, then open a new PowerShell window."
|
|
}
|
|
|
|
$rustHost = ""
|
|
try {
|
|
$rustHost = (& rustc -vV 2>$null | Select-String '^host:' | Select-Object -First 1).ToString()
|
|
} catch {}
|
|
|
|
if ($rustHost -match 'pc-windows-msvc' -and -not (Test-JcodeMsvcBuildToolsAvailable)) {
|
|
Write-Err "The MSVC linker (link.exe) was not found. Install Visual Studio 2022 Build Tools with the 'Desktop development with C++' workload, then open a new PowerShell window before using -BuildFromSource."
|
|
}
|
|
}
|
|
|
|
function Set-SetupHintsState([bool]$AlacrittyConfigured, [bool]$HotkeyConfigured) {
|
|
New-Item -ItemType Directory -Path $JcodeHome -Force | Out-Null
|
|
|
|
$state = @{
|
|
launch_count = 0
|
|
hotkey_configured = $HotkeyConfigured
|
|
hotkey_dismissed = $HotkeyConfigured
|
|
alacritty_configured = $AlacrittyConfigured
|
|
alacritty_dismissed = $AlacrittyConfigured
|
|
desktop_shortcut_created = $false
|
|
mac_ghostty_guided = $false
|
|
mac_ghostty_dismissed = $false
|
|
}
|
|
|
|
if (Test-Path $SetupHintsPath) {
|
|
try {
|
|
$existing = Get-Content $SetupHintsPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
|
foreach ($property in $existing.PSObject.Properties) {
|
|
$state[$property.Name] = $property.Value
|
|
}
|
|
} catch {
|
|
Write-Warn "Could not read existing setup hints state; overwriting it"
|
|
}
|
|
}
|
|
|
|
if ($AlacrittyConfigured) {
|
|
$state.alacritty_configured = $true
|
|
$state.alacritty_dismissed = $true
|
|
}
|
|
|
|
if ($HotkeyConfigured) {
|
|
$state.hotkey_configured = $true
|
|
$state.hotkey_dismissed = $true
|
|
}
|
|
|
|
$state | ConvertTo-Json | Set-Content -Path $SetupHintsPath -Encoding UTF8
|
|
}
|
|
|
|
function Get-JcodeHotkeyShortcutScript([string]$StartupShortcutPath, [string]$JcodeExePath) {
|
|
$escapedShortcutPath = $StartupShortcutPath.Replace("'", "''")
|
|
$escapedExePath = $JcodeExePath.Replace("'", "''")
|
|
$listenerArguments = "-NoProfile -ExecutionPolicy RemoteSigned -WindowStyle Hidden -Command `"& '$escapedExePath' setup-hotkey --listen-windows-hotkey`""
|
|
$escapedListenerArguments = $listenerArguments.Replace("'", "''")
|
|
$shortcutLines = @(
|
|
'$ErrorActionPreference = ''Stop''',
|
|
'$shell = New-Object -ComObject WScript.Shell',
|
|
"`$shortcut = `$shell.CreateShortcut('$escapedShortcutPath')",
|
|
"`$shortcut.TargetPath = 'powershell.exe'",
|
|
"`$shortcut.Arguments = '$escapedListenerArguments'",
|
|
"`$shortcut.Description = 'jcode global launch hotkey listener'",
|
|
'$shortcut.WindowStyle = 7',
|
|
'$shortcut.Save()',
|
|
"Write-Output 'OK'"
|
|
)
|
|
return ($shortcutLines -join "`r`n")
|
|
}
|
|
|
|
function Install-JcodeHotkey([string]$JcodeExePath) {
|
|
New-Item -ItemType Directory -Path $HotkeyDir -Force | Out-Null
|
|
$skipProcessLifecycle = (
|
|
$env:JCODE_WINDOWS_SETUP_SKIP_EXTERNALS -eq "1" -or
|
|
$env:JCODE_WINDOWS_SETUP_SKIP_PROCESS_LIFECYCLE -eq "1"
|
|
)
|
|
if (-not $skipProcessLifecycle) {
|
|
Stop-JcodeHotkeyListeners
|
|
}
|
|
|
|
# Upgrade cleanup: v0.47 and earlier wrote a generated PowerShell listener.
|
|
# The first-party listener now lives in jcode.exe itself and is launched via
|
|
# `jcode setup-hotkey --listen-windows-hotkey` from a login shortcut.
|
|
Remove-Item -Path (Join-Path $HotkeyDir "jcode-hotkey.ps1") -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -Path (Join-Path $HotkeyDir "jcode-hotkey-launcher.vbs") -Force -ErrorAction SilentlyContinue
|
|
$startupDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup"
|
|
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
|
|
$startupShortcutPath = Join-Path $startupDir "jcode-hotkey.lnk"
|
|
$shortcutScript = Get-JcodeHotkeyShortcutScript -StartupShortcutPath $startupShortcutPath -JcodeExePath $JcodeExePath
|
|
|
|
if ($env:JCODE_WINDOWS_SETUP_SKIP_EXTERNALS -eq "1") {
|
|
Set-Content -Path (Join-Path $HotkeyDir "jcode-hotkey-shortcut.ps1") -Value $shortcutScript -Encoding UTF8
|
|
Write-Info "Configured Alt+; and the Copilot key to launch jcode"
|
|
return $true
|
|
}
|
|
|
|
$shortcutOutput = & powershell -NoProfile -Command $shortcutScript
|
|
if ($LASTEXITCODE -ne 0 -or -not ($shortcutOutput -match 'OK')) {
|
|
Write-Warn "Created hotkey files, but could not create the Startup shortcut"
|
|
return $false
|
|
}
|
|
|
|
$escapedExePath = $JcodeExePath.Replace("'", "''")
|
|
$launchHotkeyCommand = "Start-Process -FilePath '$escapedExePath' -ArgumentList @('setup-hotkey', '--listen-windows-hotkey') -WindowStyle Hidden"
|
|
if (-not $skipProcessLifecycle) {
|
|
& powershell -NoProfile -ExecutionPolicy RemoteSigned -WindowStyle Hidden -Command $launchHotkeyCommand | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Warn "Hotkey will start on next login, but could not be launched immediately"
|
|
}
|
|
}
|
|
|
|
Write-Info "Configured Alt+; and the Copilot key to launch jcode"
|
|
return $true
|
|
}
|
|
function Resolve-JcodeWindowsArtifact([string[]]$ArchitectureCandidates) {
|
|
$sawX64 = $false
|
|
|
|
foreach ($arch in @($ArchitectureCandidates)) {
|
|
if (-not $arch) { continue }
|
|
switch -Regex ($arch.Trim()) {
|
|
'^(Arm64|ARM64|AARCH64|aarch64)$' { return "jcode-windows-aarch64" }
|
|
'^(X64|AMD64|x86_64)$' { $sawX64 = $true }
|
|
}
|
|
}
|
|
|
|
if ($sawX64) { return "jcode-windows-x86_64" }
|
|
return $null
|
|
}
|
|
|
|
function Get-JcodeWindowsArtifact {
|
|
$candidates = @()
|
|
|
|
try {
|
|
$runtimeArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
|
if ($runtimeArch) { $candidates += [string]$runtimeArch }
|
|
} catch {}
|
|
|
|
foreach ($envArch in @($env:PROCESSOR_ARCHITECTURE, $env:PROCESSOR_ARCHITEW6432)) {
|
|
if ($envArch) { $candidates += [string]$envArch }
|
|
}
|
|
|
|
$artifact = Resolve-JcodeWindowsArtifact $candidates
|
|
if ($artifact) { return $artifact }
|
|
|
|
$displayArch = if ($candidates.Count -gt 0) { $candidates -join ", " } else { "<unknown>" }
|
|
Write-Err "Unsupported architecture: $displayArch (supported: x86_64, ARM64)"
|
|
}
|
|
|
|
function Invoke-JcodeInstall {
|
|
$Artifact = Get-JcodeWindowsArtifact
|
|
|
|
$ResolvedArtifactExePath = Resolve-OptionalPath $ArtifactExePath
|
|
$ResolvedArtifactTgzPath = Resolve-OptionalPath $ArtifactTgzPath
|
|
|
|
if ($ResolvedArtifactExePath -and $ResolvedArtifactTgzPath) {
|
|
Write-Err "Provide only one of -ArtifactExePath or -ArtifactTgzPath"
|
|
}
|
|
|
|
if (-not $Version) {
|
|
if ($ResolvedArtifactExePath) {
|
|
$Version = Get-JcodeVersionFromBinary $ResolvedArtifactExePath
|
|
if (-not $Version) {
|
|
Write-Err "Could not detect a jcode version from '$ResolvedArtifactExePath'. Pass -Version explicitly if this is a trusted local build."
|
|
}
|
|
Write-Info "Detected local artifact version: $Version"
|
|
} elseif ($ResolvedArtifactTgzPath) {
|
|
Write-Err "-Version is required when using -ArtifactTgzPath"
|
|
} else {
|
|
Write-Info "Fetching latest release..."
|
|
$Version = Get-LatestJcodeReleaseTag
|
|
}
|
|
}
|
|
|
|
if (-not $Version) { Write-Err "Failed to determine latest version" }
|
|
|
|
$VersionNum = $Version.TrimStart('v')
|
|
$DownloadBases = Get-JcodeReleaseDownloadBases $Version
|
|
|
|
$BuildsDir = Join-Path (Get-JcodeLocalAppDataDir) "jcode\builds"
|
|
$StableDir = Join-Path $BuildsDir "stable"
|
|
$VersionDir = Join-Path $BuildsDir "versions\$VersionNum"
|
|
$LauncherPath = Join-Path $InstallDir "jcode.exe"
|
|
|
|
$Existing = ""
|
|
if (Test-Path $LauncherPath) {
|
|
try { $Existing = & $LauncherPath --version 2>$null | Select-Object -First 1 } catch {}
|
|
}
|
|
|
|
if ($Existing) {
|
|
if ($Existing -match [regex]::Escape($VersionNum)) {
|
|
Write-Info "jcode $Version is already installed - reinstalling"
|
|
} else {
|
|
Write-Info "Updating jcode $Existing -> $Version"
|
|
}
|
|
} else {
|
|
Write-Info "Installing jcode $Version"
|
|
}
|
|
Write-Info " launcher: $LauncherPath"
|
|
|
|
foreach ($d in @($InstallDir, $StableDir, $VersionDir)) {
|
|
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
|
}
|
|
|
|
$TempDir = Join-Path $env:TEMP "jcode-install-$(Get-Random)"
|
|
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
|
|
|
|
try {
|
|
$DownloadMode = ""
|
|
$DownloadPath = Join-Path $TempDir "jcode.download"
|
|
$DownloadedAssetName = $null
|
|
|
|
if ($ResolvedArtifactExePath) {
|
|
Write-Info "Using local artifact exe: $ResolvedArtifactExePath"
|
|
Copy-Item -Path $ResolvedArtifactExePath -Destination $DownloadPath -Force
|
|
$DownloadMode = "bin"
|
|
} elseif ($ResolvedArtifactTgzPath) {
|
|
Write-Info "Using local artifact archive: $ResolvedArtifactTgzPath"
|
|
Copy-Item -Path $ResolvedArtifactTgzPath -Destination $DownloadPath -Force
|
|
$DownloadMode = "tar"
|
|
} else {
|
|
foreach ($candidate in @(
|
|
@{ Name = "$Artifact.exe"; Mode = "bin" },
|
|
@{ Name = "$Artifact.tar.gz"; Mode = "tar" }
|
|
)) {
|
|
foreach ($base in $DownloadBases) {
|
|
try {
|
|
Write-Info "Downloading $($candidate.Name) from $base..."
|
|
Invoke-WebRequest -UseBasicParsing -Uri "$base/$($candidate.Name)" -OutFile $DownloadPath
|
|
$DownloadMode = $candidate.Mode
|
|
$DownloadedAssetName = $candidate.Name
|
|
break
|
|
} catch {}
|
|
}
|
|
if ($DownloadMode) { break }
|
|
}
|
|
}
|
|
|
|
if (-not $ResolvedArtifactExePath -and -not $ResolvedArtifactTgzPath -and $DownloadMode) {
|
|
$downloadedAssetName = if ($DownloadMode -eq "bin") { "$Artifact.exe" } else { "$Artifact.tar.gz" }
|
|
$expectedSha256 = Get-ReleaseChecksum -ReleaseTag $Version -AssetName $downloadedAssetName
|
|
Assert-JcodeFileChecksum -FilePath $DownloadPath -ExpectedSha256 $expectedSha256 -AssetName $downloadedAssetName | Out-Null
|
|
}
|
|
|
|
$DestBin = Join-Path $VersionDir "jcode.exe"
|
|
|
|
if ($DownloadMode -eq "tar") {
|
|
Write-Info "Extracting..."
|
|
tar xzf $DownloadPath -C $TempDir 2>$null
|
|
$SrcBin = Join-Path $TempDir "$Artifact.exe"
|
|
if (-not (Test-Path $SrcBin)) {
|
|
Write-Err "Downloaded archive did not contain expected binary: $Artifact.exe"
|
|
}
|
|
Move-Item -Path $SrcBin -Destination $DestBin -Force
|
|
} elseif ($DownloadMode -eq "bin") {
|
|
Move-Item -Path $DownloadPath -Destination $DestBin -Force
|
|
} else {
|
|
if (-not $BuildFromSource) {
|
|
$releaseUrl = "https://github.com/$Repo/releases/tag/$Version"
|
|
Write-Err "No prebuilt $Artifact asset was found in $Version. Check $releaseUrl or rerun the downloaded script with -BuildFromSource. The installer will not start a long source build automatically."
|
|
}
|
|
|
|
Write-Info "No prebuilt asset found for $Artifact in $Version; -BuildFromSource was requested"
|
|
Assert-JcodeSourceBuildPrerequisites
|
|
|
|
$SrcDir = Join-Path $TempDir "jcode-src"
|
|
Write-Info "Cloning $Repo at $Version..."
|
|
$gitCloneResult = Invoke-ProcessWithTimeout -FilePath "git" -ArgumentList @(
|
|
"clone",
|
|
"--depth", "1",
|
|
"--branch", $Version,
|
|
"https://github.com/$Repo.git",
|
|
$SrcDir
|
|
) -TimeoutSeconds 600 -FriendlyName "git-clone" -CaptureOutput
|
|
if ($gitCloneResult.TimedOut) {
|
|
Write-LogTail -Path $gitCloneResult.StdoutPath -Label "git stdout"
|
|
Write-LogTail -Path $gitCloneResult.StderrPath -Label "git stderr"
|
|
Write-Err "git clone timed out after 600 seconds"
|
|
}
|
|
if ($gitCloneResult.ExitCode -ne 0) {
|
|
Write-LogTail -Path $gitCloneResult.StdoutPath -Label "git stdout"
|
|
Write-LogTail -Path $gitCloneResult.StderrPath -Label "git stderr"
|
|
Write-Err "Failed to clone $Repo at $Version (exit code: $($gitCloneResult.ExitCode))"
|
|
}
|
|
|
|
Write-Info "Building jcode from source (this can take several minutes)..."
|
|
$cargoResult = Invoke-ProcessWithTimeout -FilePath "cargo" -ArgumentList @(
|
|
"build", "--release", "--locked", "-p", "jcode", "--bin", "jcode",
|
|
"--manifest-path", (Join-Path $SrcDir "Cargo.toml")
|
|
) -TimeoutSeconds 1800 -FriendlyName "cargo-build" -CaptureOutput
|
|
if ($cargoResult.TimedOut) {
|
|
Write-LogTail -Path $cargoResult.StdoutPath -Label "cargo stdout"
|
|
Write-LogTail -Path $cargoResult.StderrPath -Label "cargo stderr"
|
|
Write-Err "cargo build timed out after 1800 seconds"
|
|
}
|
|
if ($cargoResult.ExitCode -ne 0) {
|
|
Write-LogTail -Path $cargoResult.StdoutPath -Label "cargo stdout"
|
|
Write-LogTail -Path $cargoResult.StderrPath -Label "cargo stderr"
|
|
Write-Err "cargo build failed (exit code: $($cargoResult.ExitCode))"
|
|
}
|
|
|
|
$BuiltBin = Join-Path $SrcDir "target\release\jcode.exe"
|
|
if (-not (Test-Path $BuiltBin)) { Write-Err "Built binary not found at $BuiltBin" }
|
|
Copy-Item -Path $BuiltBin -Destination $DestBin -Force
|
|
}
|
|
|
|
Assert-JcodeBinaryCandidate -BinaryPath $DestBin -ExpectedVersion $Version | Out-Null
|
|
|
|
$StableBin = Join-Path $StableDir "jcode.exe"
|
|
Copy-Item -Path $DestBin -Destination $StableBin -Force
|
|
Set-Content -Path (Join-Path $BuildsDir "stable-version") -Value $VersionNum
|
|
Install-JcodeLauncher -SourcePath $StableBin -LauncherPath $LauncherPath | Out-Null
|
|
} finally {
|
|
Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
# Gracefully reload any running background server onto the freshly installed
|
|
# binary (issue #291). `server reload` only reloads a genuinely-older daemon,
|
|
# hands its live sessions to the new process, and is a no-op when nothing is
|
|
# running, so it is safe to call unconditionally. Best-effort: never fail the
|
|
# install over it.
|
|
if ($env:JCODE_SKIP_SERVER_RELOAD -ne "1") {
|
|
try {
|
|
& $LauncherPath server reload 2>$null | Out-Null
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
$userPathUpdate = Set-JcodeUserPath -InstallDir $InstallDir
|
|
if ($userPathUpdate.Changed) {
|
|
Write-Info "Updated user PATH with $InstallDir"
|
|
if ($userPathUpdate.RemovedManagedEntries -gt 0 -or $userPathUpdate.RemovedDuplicateEntries -gt 0) {
|
|
Write-Info " removed $($userPathUpdate.RemovedManagedEntries) stale jcode PATH entr$(if ($userPathUpdate.RemovedManagedEntries -eq 1) { 'y' } else { 'ies' }) and $($userPathUpdate.RemovedDuplicateEntries) duplicate entr$(if ($userPathUpdate.RemovedDuplicateEntries -eq 1) { 'y' } else { 'ies' })"
|
|
}
|
|
} else {
|
|
Write-Info "User PATH already contains $InstallDir"
|
|
}
|
|
|
|
Set-JcodeProcessPath -InstallDir $InstallDir | Out-Null
|
|
|
|
$installedAlacritty = $false
|
|
$configuredHotkey = $false
|
|
$shouldSetupAlacritty = [bool]($ConfigureAlacritty -and -not $SkipAlacrittySetup)
|
|
$shouldSetupHotkey = [bool]($ConfigureHotkey -and -not $SkipHotkeySetup)
|
|
|
|
if ($ConfigureAlacritty -and $SkipAlacrittySetup) {
|
|
Write-Warn "Both -ConfigureAlacritty and -SkipAlacrittySetup were provided; skipping Alacritty setup"
|
|
}
|
|
if ($ConfigureHotkey -and $SkipHotkeySetup) {
|
|
Write-Warn "Both -ConfigureHotkey and -SkipHotkeySetup were provided; skipping hotkey setup"
|
|
}
|
|
|
|
if ($shouldSetupAlacritty) {
|
|
$installedAlacritty = Install-Alacritty
|
|
} else {
|
|
$installedAlacritty = Test-AlacrittyInstalled
|
|
Write-Info "Optional Alacritty setup not requested"
|
|
}
|
|
|
|
if ($shouldSetupHotkey) {
|
|
$configuredHotkey = Install-JcodeHotkey -JcodeExePath $LauncherPath
|
|
} else {
|
|
Write-Info "Optional global hotkey setup not requested"
|
|
}
|
|
|
|
Set-SetupHintsState -AlacrittyConfigured:(Test-AlacrittyInstalled) -HotkeyConfigured:$configuredHotkey
|
|
|
|
Write-Host ""
|
|
Write-Info "jcode $Version installed successfully!"
|
|
Write-Host ""
|
|
|
|
if (Test-AlacrittyInstalled) {
|
|
$alacrittyPath = Find-AlacrittyPath
|
|
if ($alacrittyPath) {
|
|
Write-Info "Alacritty ready: $alacrittyPath"
|
|
}
|
|
}
|
|
|
|
if ($configuredHotkey) {
|
|
Write-Info "Global launch keys ready: Alt+; and the Copilot key open jcode"
|
|
Write-Host ""
|
|
} elseif (-not $ConfigureHotkey) {
|
|
Write-Info "Optional: run 'jcode setup-hotkey' to configure global launch hotkeys and terminal preferences."
|
|
Write-Host ""
|
|
}
|
|
|
|
if (Get-Command jcode -ErrorAction SilentlyContinue) {
|
|
Write-Info "Run 'jcode' to get started."
|
|
} else {
|
|
Write-Host " Open a new terminal window, then run:"
|
|
Write-Host ""
|
|
Write-Host " jcode" -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
if ($env:JCODE_INSTALL_PS1_IMPORT_ONLY -ne "1") {
|
|
Invoke-JcodeInstall
|
|
}
|