From 5b8f8fda07770157eeb591baebc47dbdff97b567 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 21 Mar 2026 20:00:26 +0200 Subject: [PATCH 1/4] fix: bind OAuth server to 127.0.0.1 Prevents Windows Firewall prompt on first login and stops exposing the OAuth callback to the local network. The redirectUrl already uses 127.0.0.1, so this is consistent. --- src/services/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/oauth.ts b/src/services/oauth.ts index d29f6bb..987abfd 100644 --- a/src/services/oauth.ts +++ b/src/services/oauth.ts @@ -87,7 +87,7 @@ const captureAccessTokenFromHTTPServer = (server: PolarEnvironment) => } }); - httpServer?.listen(3333, () => { + httpServer?.listen(3333, "127.0.0.1", () => { open(authorizationUrl); }); }); From f7f0983b622eab23a13e54e9ee185136ccc4895d Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 21 Mar 2026 20:00:36 +0200 Subject: [PATCH 2/4] feat: add Windows support to update command - Add win32 platform detection and Windows ARM64 guard - Use PowerShell Expand-Archive for .zip extraction on Windows (bare tar resolves to Git's GNU tar which breaks on Windows paths) - Escape single quotes in paths passed to PowerShell - Write-first binary replacement pattern for crash-safe self-update (running .exe cannot be overwritten on Windows, only renamed) - Recover interrupted updates on startup by completing pending .new files --- src/cli.ts | 12 ++++++ src/commands/update.ts | 92 +++++++++++++++++++++++++++++++++--------- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 917dbbd..cf698ed 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,7 @@ import { Command } from "@effect/cli"; import { BunContext, BunRuntime } from "@effect/platform-bun"; import { Effect, Layer } from "effect"; +import { existsSync, renameSync, unlinkSync } from "fs"; import { listen } from "./commands/listen"; import { login } from "./commands/login"; import { migrate } from "./commands/migrate"; @@ -32,6 +33,17 @@ const services = Layer.mergeAll( BunContext.layer ); +// Clean up stale files from Windows self-update +if (process.platform === "win32") { + // If .new exists, a previous update was interrupted — complete it + const pendingPath = process.execPath + ".new"; + if (existsSync(pendingPath)) { + try { renameSync(pendingPath, process.execPath); } catch {} + } + try { unlinkSync(process.execPath + ".bak"); } catch {} + try { unlinkSync(pendingPath); } catch {} +} + showUpdateNotice(); checkForUpdateInBackground(); diff --git a/src/commands/update.ts b/src/commands/update.ts index 08ee83b..d7bcd55 100644 --- a/src/commands/update.ts +++ b/src/commands/update.ts @@ -1,6 +1,7 @@ import { Command } from "@effect/cli"; import { Console, Effect, Schema } from "effect"; import { createHash } from "crypto"; +import { renameSync, unlinkSync } from "fs"; import { chmod, mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; @@ -30,6 +31,9 @@ function detectPlatform(): { os: string; arch: string } { case "linux": os = "linux"; break; + case "win32": + os = "windows"; + break; default: throw new Error(`Unsupported OS: ${platform}`); } @@ -50,6 +54,10 @@ function detectPlatform(): { os: string; arch: string } { throw new Error("Linux arm64 is not yet supported"); } + if (os === "windows" && normalizedArch === "arm64") { + throw new Error("Windows arm64 is not yet supported"); + } + return { os, arch: normalizedArch }; } @@ -65,8 +73,12 @@ const downloadAndUpdate = ( const reset = "\x1b[0m"; const { os, arch } = detectPlatform(); + const binaryPath = process.execPath; const platform = `${os}-${arch}`; - const archiveName = `polar-${platform}.tar.gz`; + const isWindows = process.platform === "win32"; + const archiveExt = isWindows ? "zip" : "tar.gz"; + const binaryName = isWindows ? "polar.exe" : "polar"; + const archiveName = `polar-${platform}.${archiveExt}`; const asset = release.assets.find((a) => a.name === archiveName); if (!asset) { @@ -155,36 +167,76 @@ const downloadAndUpdate = ( yield* Console.log(`${dim}Extracting...${reset}`); - const tar = Bun.spawn(["tar", "-xzf", archivePath, "-C", tempDir], { - stdout: "ignore", - stderr: "pipe", - }); + if (isWindows) { + const ps = Bun.spawn( + [ + "powershell", + "-NoProfile", + "-Command", + `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tempDir.replace(/'/g, "''")}' -Force`, + ], + { + stdout: "ignore", + stderr: "pipe", + }, + ); - const tarExitCode = yield* Effect.tryPromise({ - try: () => tar.exited, - catch: () => new Error("Failed to extract archive"), - }); + const psExitCode = yield* Effect.tryPromise({ + try: () => ps.exited, + catch: () => new Error("Failed to extract archive"), + }); - if (tarExitCode !== 0) { - const stderr = yield* Effect.tryPromise({ - try: () => new Response(tar.stderr).text(), - catch: () => new Error("Failed to read tar stderr"), + if (psExitCode !== 0) { + const stderr = yield* Effect.tryPromise({ + try: () => new Response(ps.stderr).text(), + catch: () => new Error("Failed to read extraction stderr"), + }); + return yield* Effect.fail( + new Error(`Failed to extract archive: ${stderr}`), + ); + } + } else { + const tar = Bun.spawn(["tar", "-xzf", archivePath, "-C", tempDir], { + stdout: "ignore", + stderr: "pipe", }); - return yield* Effect.fail( - new Error(`Failed to extract archive: ${stderr}`), - ); + + const tarExitCode = yield* Effect.tryPromise({ + try: () => tar.exited, + catch: () => new Error("Failed to extract archive"), + }); + + if (tarExitCode !== 0) { + const stderr = yield* Effect.tryPromise({ + try: () => new Response(tar.stderr).text(), + catch: () => new Error("Failed to read tar stderr"), + }); + return yield* Effect.fail( + new Error(`Failed to extract archive: ${stderr}`), + ); + } } - const binaryPath = process.execPath; - const newBinaryPath = join(tempDir, "polar"); + const newBinaryPath = join(tempDir, binaryName); yield* Console.log(`${dim}Replacing binary...${reset}`); yield* Effect.tryPromise({ try: async () => { const newBinary = await Bun.file(newBinaryPath).arrayBuffer(); - await Bun.write(binaryPath, newBinary); - await chmod(binaryPath, 0o755); + if (isWindows) { + const bakPath = binaryPath + ".bak"; + const pendingPath = binaryPath + ".new"; + await Bun.write(pendingPath, newBinary); + renameSync(binaryPath, bakPath); + renameSync(pendingPath, binaryPath); + try { + unlinkSync(bakPath); + } catch {} + } else { + await Bun.write(binaryPath, newBinary); + await chmod(binaryPath, 0o755); + } }, catch: (e) => new Error( From 0b2b7516d9d4f426503772ff36ac0c84370a0c17 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 21 Mar 2026 20:00:46 +0200 Subject: [PATCH 3/4] feat: add Windows binary to CI/CD release pipeline - Add bun-windows-x64 matrix entry with zip packaging - Use matrix-driven --outfile and archive format - Checksums and release assets use polar-* wildcard - Add build:binary:windows-x64 script to package.json --- .github/workflows/release.yml | 28 ++++++++++++++++++++++------ package.json | 3 ++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a530beb..e2ebb1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,12 +16,23 @@ jobs: - target: bun-darwin-arm64 artifact: polar-darwin-arm64 os: ubuntu-latest + ext: tar.gz + binary: polar - target: bun-darwin-x64 artifact: polar-darwin-x64 os: ubuntu-latest + ext: tar.gz + binary: polar - target: bun-linux-x64 artifact: polar-linux-x64 os: ubuntu-latest + ext: tar.gz + binary: polar + - target: bun-windows-x64 + artifact: polar-windows-x64 + os: ubuntu-latest + ext: zip + binary: polar.exe runs-on: ${{ matrix.os }} @@ -35,15 +46,20 @@ jobs: - run: bun install - name: Build binary - run: bun build ./src/cli.ts --compile --target=${{ matrix.target }} --outfile polar + run: bun build ./src/cli.ts --compile --target=${{ matrix.target }} --outfile ${{ matrix.binary }} + + - name: Package binary (tar.gz) + if: matrix.ext == 'tar.gz' + run: tar -czf ${{ matrix.artifact }}.tar.gz ${{ matrix.binary }} - - name: Package binary - run: tar -czf ${{ matrix.artifact }}.tar.gz polar + - name: Package binary (zip) + if: matrix.ext == 'zip' + run: zip ${{ matrix.artifact }}.zip ${{ matrix.binary }} - uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }}.tar.gz + path: ${{ matrix.artifact }}.${{ matrix.ext }} release: needs: build @@ -55,12 +71,12 @@ jobs: merge-multiple: true - name: Generate checksums - run: sha256sum *.tar.gz > checksums.txt + run: sha256sum polar-* > checksums.txt - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: files: | - *.tar.gz + polar-* checksums.txt generate_release_notes: true diff --git a/package.json b/package.json index d6785f4..a1a479d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "build:binary": "bun build ./src/cli.ts --compile --outfile polar", "build:binary:darwin-arm64": "bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile polar", "build:binary:darwin-x64": "bun build ./src/cli.ts --compile --target=bun-darwin-x64 --outfile polar", - "build:binary:linux-x64": "bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile polar" + "build:binary:linux-x64": "bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile polar", + "build:binary:windows-x64": "bun build ./src/cli.ts --compile --target=bun-windows-x64 --outfile polar.exe" }, "files": [ "bin", From 60e87acc6557bd58fba791e2bc8fca3aee3f6714 Mon Sep 17 00:00:00 2001 From: Abdelrahman Essawy Date: Sat, 21 Mar 2026 20:00:56 +0200 Subject: [PATCH 4/4] feat: add PowerShell install script for Windows Usage: powershell -ExecutionPolicy ByPass -c "irm .../install.ps1 | iex" Installs to ~/.polar/bin/, verifies SHA256 checksums, validates version response, and updates user PATH. --- install.ps1 | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 install.ps1 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..5d66450 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,108 @@ +# Polar CLI installer for Windows +# Usage: powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/polarsource/cli/main/install.ps1 | iex" + +$ErrorActionPreference = "Stop" + +$Repo = "polarsource/cli" +$BinaryName = "polar.exe" +$InstallDir = Join-Path $HOME ".polar\bin" + +function Write-Info { param($Message) Write-Host "==> $Message" -ForegroundColor Green } +function Write-Warn { param($Message) Write-Host "warning: $Message" -ForegroundColor Yellow } +function Write-Err { param($Message) Write-Host "error: $Message" -ForegroundColor Red; exit 1 } + +# Detect architecture +$Arch = $env:PROCESSOR_ARCHITECTURE +if ($Arch -ne "AMD64") { + Write-Err "Unsupported architecture: $Arch. Only x64 (AMD64) is supported." +} + +$Platform = "windows-x64" + +# Fetch latest version +Write-Info "Fetching latest version..." +try { + $Release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -Headers @{ "User-Agent" = "polar-installer" } + $Version = $Release.tag_name +} catch { + Write-Err "Failed to fetch latest version. Check your network connection." +} +if (-not $Version) { + Write-Err "Failed to determine latest version from GitHub API response." +} +Write-Info "Version: $Version" + +# Set up temp directory +$TempDir = Join-Path $env:TEMP "polar-install-$(Get-Random)" +New-Item -ItemType Directory -Force -Path $TempDir | Out-Null + +try { + # Download archive + $Archive = "polar-$Platform.zip" + $ArchiveUrl = "https://github.com/$Repo/releases/download/$Version/$Archive" + $ArchivePath = Join-Path $TempDir $Archive + + Write-Info "Downloading $BinaryName $Version..." + try { + Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ArchivePath -UseBasicParsing + } catch { + Write-Err "Download failed. Check if a release exists for your platform: $Platform" + } + + # Download checksums + $ChecksumsUrl = "https://github.com/$Repo/releases/download/$Version/checksums.txt" + $ChecksumsPath = Join-Path $TempDir "checksums.txt" + try { + Invoke-WebRequest -Uri $ChecksumsUrl -OutFile $ChecksumsPath -UseBasicParsing + } catch { + Write-Err "Failed to download checksums." + } + + # Verify checksum + Write-Info "Verifying checksum..." + $ActualHash = (Get-FileHash -Path $ArchivePath -Algorithm SHA256).Hash.ToLower() + $ExpectedLine = Get-Content $ChecksumsPath | Where-Object { $_ -match $Archive } + if (-not $ExpectedLine) { + Write-Err "No checksum found for $Archive" + } + $ExpectedHash = ($ExpectedLine -split '\s+')[0].ToLower() + if ($ActualHash -ne $ExpectedHash) { + Write-Err "Checksum mismatch!`n Expected: $ExpectedHash`n Got: $ActualHash" + } + Write-Info "Checksum verified." + + # Extract + Write-Info "Extracting..." + Expand-Archive -LiteralPath $ArchivePath -DestinationPath $TempDir -Force + + # Install + Write-Info "Installing to $InstallDir..." + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + + $SourcePath = Join-Path $TempDir $BinaryName + $DestPath = Join-Path $InstallDir $BinaryName + + try { + Copy-Item -Path $SourcePath -Destination $DestPath -Force + } catch { + Write-Err "Failed to install. If polar is running, close it and try again." + } + + # Update PATH + $CurrentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($CurrentPath -notlike "*$InstallDir*") { + [Environment]::SetEnvironmentVariable("PATH", "$InstallDir;$CurrentPath", "User") + $env:PATH = "$InstallDir;$env:PATH" + } + + Write-Info "Polar CLI $Version installed successfully!" + Write-Host "" + Write-Host " Run 'polar --help' to get started." + Write-Host "" + if ($CurrentPath -notlike "*$InstallDir*") { + Write-Warn "Restart other open terminals for PATH changes to take effect." + } +} finally { + # Cleanup + Remove-Item -Path $TempDir -Recurse -Force -ErrorAction SilentlyContinue +}