Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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
Expand All @@ -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
108 changes: 108 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();

Expand Down
92 changes: 72 additions & 20 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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 };
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/services/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const captureAccessTokenFromHTTPServer = (server: PolarEnvironment) =>
}
});

httpServer?.listen(3333, () => {
httpServer?.listen(3333, "127.0.0.1", () => {
open(authorizationUrl);
});
});
Expand Down