Skip to content

Commit 54b8817

Browse files
committed
Add cross-platform release packaging and installers
1 parent e5b13b3 commit 54b8817

7 files changed

Lines changed: 758 additions & 133 deletions

File tree

.github/workflows/release.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- 'v*'
8+
9+
permissions:
10+
contents: write
11+
12+
jobs:
13+
build-release:
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
include:
18+
- runner: ubuntu-latest
19+
artifact_name: free-code-linux-x64
20+
targets: linux-x64,linux-x64-musl
21+
- runner: ubuntu-24.04-arm
22+
artifact_name: free-code-linux-arm64
23+
targets: linux-arm64,linux-arm64-musl
24+
- runner: windows-latest
25+
artifact_name: free-code-windows-x64
26+
targets: windows-x64
27+
- runner: windows-11-arm
28+
artifact_name: free-code-windows-arm64
29+
targets: windows-arm64
30+
- runner: macos-13
31+
artifact_name: free-code-macos-x64
32+
targets: macos-x64
33+
- runner: macos-14
34+
artifact_name: free-code-macos-arm64
35+
targets: macos-arm64
36+
37+
runs-on: ${{ matrix.runner }}
38+
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
42+
43+
- name: Setup Bun
44+
uses: oven-sh/setup-bun@v2
45+
with:
46+
bun-version: 1.3.11
47+
48+
- name: Install dependencies
49+
run: bun install --frozen-lockfile || bun install
50+
51+
- name: Build release archives
52+
run: bun run release:build --targets=${{ matrix.targets }}
53+
54+
- name: Upload workflow artifacts
55+
uses: actions/upload-artifact@v4
56+
with:
57+
name: ${{ matrix.artifact_name }}
58+
path: |
59+
dist/release/*.tar.gz
60+
dist/release/*.zip
61+
62+
publish-release:
63+
if: startsWith(github.ref, 'refs/tags/')
64+
needs: build-release
65+
runs-on: ubuntu-latest
66+
67+
steps:
68+
- name: Download workflow artifacts
69+
uses: actions/download-artifact@v4
70+
with:
71+
path: dist/collected
72+
73+
- name: Flatten release files
74+
run: |
75+
mkdir -p dist/release
76+
find dist/collected -type f -exec cp {} dist/release/ \;
77+
78+
- name: Publish GitHub release assets
79+
uses: softprops/action-gh-release@v2
80+
with:
81+
files: dist/release/*

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ All telemetry stripped. All injected security-prompt guardrails removed. All exp
88
curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash
99
```
1010

11-
> Checks your system, installs Bun if needed, clones, builds with all features enabled, and puts `free-code` on your PATH. Then just `export ANTHROPIC_API_KEY="sk-ant-..."` and run `free-code`.
11+
```powershell
12+
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/paoloanzn/free-code/main/install.ps1 | iex"
13+
```
14+
15+
> The installers now prefer prebuilt release packages for Linux, macOS, and Windows. If a matching release asset is unavailable, they fall back to building from source.
1216
1317
<p align="center">
1418
<img src="assets/screenshot.png" alt="free-code screenshot" width="800" />
@@ -73,7 +77,11 @@ See [FEATURES.md](FEATURES.md) for the full audit of all 88 flags and their stat
7377
curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash
7478
```
7579

76-
This will check your system, install Bun if needed, clone the repo, build the binary with all experimental features enabled, and symlink it as `free-code` on your PATH.
80+
```powershell
81+
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/paoloanzn/free-code/main/install.ps1 | iex"
82+
```
83+
84+
On supported releases, this downloads a prebuilt binary for your platform and installs `free-code` onto your PATH. If no release asset matches, the installer falls back to a source build.
7785

7886
After install, just run:
7987
```bash
@@ -85,15 +93,10 @@ free-code
8593

8694
## Requirements
8795

88-
- [Bun](https://bun.sh) >= 1.3.11
89-
- macOS or Linux (Windows via WSL)
96+
- For prebuilt install: no Bun required
97+
- For source builds: [Bun](https://bun.sh) >= 1.3.11
9098
- An Anthropic API key (set `ANTHROPIC_API_KEY` in your environment)
9199

92-
```bash
93-
# Install Bun if you don't have it
94-
curl -fsSL https://bun.sh/install | bash
95-
```
96-
97100
---
98101

99102
## Build
@@ -128,6 +131,24 @@ bun run compile
128131
| `bun run build:dev:full` | `./cli-dev` | All 45+ experimental flags | The full unlock build |
129132
| `bun run compile` | `./dist/cli` | `VOICE_MODE` only | Alternative output directory |
130133

134+
### Release packaging
135+
136+
```bash
137+
# Build all release archives into ./dist/release
138+
bun run release:build
139+
140+
# Build just a subset while iterating locally
141+
bun run release:build --targets=windows-x64,linux-x64,macos-arm64
142+
```
143+
144+
This generates:
145+
146+
- Versionless install assets such as `free-code-windows-x64.zip` and `free-code-linux-x64.tar.gz`
147+
- `manifest.json` with target metadata
148+
- `checksums.txt` with SHA-256 sums
149+
150+
The repository also includes [release.yml](.github/workflows/release.yml), which builds and publishes these archives automatically when you push a `v*` tag.
151+
131152
### Individual feature flags
132153

133154
You can enable specific flags without the full bundle:

install.ps1

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
$ErrorActionPreference = 'Stop'
2+
3+
$Repo = if ($env:FREE_CODE_REPO) { $env:FREE_CODE_REPO } else { 'paoloanzn/free-code' }
4+
$Version = if ($env:FREE_CODE_VERSION) { $env:FREE_CODE_VERSION } else { 'latest' }
5+
$InstallRoot = if ($env:FREE_CODE_HOME) { $env:FREE_CODE_HOME } else { Join-Path $HOME 'AppData\Local\free-code' }
6+
$BinDir = if ($env:FREE_CODE_BIN_DIR) { $env:FREE_CODE_BIN_DIR } else { Join-Path $InstallRoot 'bin' }
7+
$SourceDir = Join-Path $InstallRoot 'src'
8+
$BunMinVersion = '1.3.11'
9+
10+
function Write-Info($Message) { Write-Host "[*] $Message" -ForegroundColor Cyan }
11+
function Write-Ok($Message) { Write-Host "[+] $Message" -ForegroundColor Green }
12+
function Write-Warn($Message) { Write-Host "[!] $Message" -ForegroundColor Yellow }
13+
function Fail($Message) { throw $Message }
14+
15+
function Get-Arch {
16+
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
17+
switch ($arch) {
18+
'x64' { return 'x64' }
19+
'arm64' { return 'arm64' }
20+
default { Fail "Unsupported Windows architecture: $arch" }
21+
}
22+
}
23+
24+
function Get-AssetName {
25+
$arch = Get-Arch
26+
return "free-code-windows-$arch.zip"
27+
}
28+
29+
function Get-ReleaseUrl {
30+
param([string]$AssetName)
31+
32+
if ($Version -eq 'latest') {
33+
return "https://github.com/$Repo/releases/latest/download/$AssetName"
34+
}
35+
36+
return "https://github.com/$Repo/releases/download/$Version/$AssetName"
37+
}
38+
39+
function Ensure-UserPath {
40+
if (-not (Test-Path -LiteralPath $BinDir)) {
41+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
42+
}
43+
44+
$currentUserPath = [Environment]::GetEnvironmentVariable('Path', 'User')
45+
$pathParts = @()
46+
if ($currentUserPath) {
47+
$pathParts = $currentUserPath -split ';' | Where-Object { $_ }
48+
}
49+
50+
if ($pathParts -contains $BinDir) {
51+
return
52+
}
53+
54+
$newUserPath = if ($currentUserPath) { "$currentUserPath;$BinDir" } else { $BinDir }
55+
[Environment]::SetEnvironmentVariable('Path', $newUserPath, 'User')
56+
Write-Warn "$BinDir was added to your user PATH. Open a new terminal after install."
57+
}
58+
59+
function Install-FromRelease {
60+
$assetName = Get-AssetName
61+
$url = Get-ReleaseUrl -AssetName $assetName
62+
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("free-code-install-" + [guid]::NewGuid().ToString('N'))
63+
$archivePath = Join-Path $tempRoot $assetName
64+
$extractPath = Join-Path $tempRoot 'extract'
65+
66+
New-Item -ItemType Directory -Path $extractPath -Force | Out-Null
67+
68+
try {
69+
Write-Info "Downloading prebuilt package..."
70+
Invoke-WebRequest -Uri $url -OutFile $archivePath
71+
72+
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractPath -Force
73+
74+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
75+
Copy-Item -LiteralPath (Join-Path $extractPath 'free-code\free-code.exe') -Destination (Join-Path $BinDir 'free-code.exe') -Force
76+
77+
Set-Content -LiteralPath (Join-Path $BinDir 'free-code.cmd') -Value "@echo off`r`n""%~dp0\free-code.exe"" %*`r`n" -NoNewline
78+
Set-Content -LiteralPath (Join-Path $BinDir 'free-code.ps1') -Value '$exe = Join-Path $PSScriptRoot ''free-code.exe''; & $exe @args; exit $LASTEXITCODE' -NoNewline
79+
80+
Write-Ok "Installed prebuilt binary to $BinDir\free-code.exe"
81+
return $true
82+
} catch {
83+
Write-Warn "Prebuilt download failed: $($_.Exception.Message)"
84+
return $false
85+
} finally {
86+
if (Test-Path -LiteralPath $tempRoot) {
87+
Remove-Item -LiteralPath $tempRoot -Recurse -Force
88+
}
89+
}
90+
}
91+
92+
function Version-Gte {
93+
param([string]$A, [string]$B)
94+
$versionA = [version]($A -replace '[^0-9\.].*$', '')
95+
$versionB = [version]($B -replace '[^0-9\.].*$', '')
96+
return $versionA -ge $versionB
97+
}
98+
99+
function Ensure-Bun {
100+
$bun = Get-Command bun -ErrorAction SilentlyContinue
101+
if ($bun) {
102+
$version = (& $bun.Source --version).Trim()
103+
if (Version-Gte -A $version -B $BunMinVersion) {
104+
Write-Ok "bun: v$version"
105+
return
106+
}
107+
108+
Write-Warn "bun v$version is too old. Upgrading..."
109+
} else {
110+
Write-Info 'Bun not found. Installing for source fallback...'
111+
}
112+
113+
irm https://bun.sh/install.ps1 | iex
114+
$env:PATH = "$HOME\.bun\bin;$env:PATH"
115+
116+
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
117+
Fail 'Bun installation completed but bun is not on PATH.'
118+
}
119+
}
120+
121+
function Install-FromSource {
122+
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
123+
Fail 'git is required for source fallback but was not found.'
124+
}
125+
126+
Ensure-Bun
127+
128+
if (Test-Path -LiteralPath (Join-Path $SourceDir '.git')) {
129+
Write-Info 'Updating existing source checkout...'
130+
try {
131+
git -C $SourceDir pull --ff-only origin main | Out-Host
132+
} catch {
133+
Write-Warn 'git pull failed, using existing source checkout.'
134+
}
135+
} else {
136+
New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
137+
Write-Info 'Cloning source repository...'
138+
git clone --depth 1 "https://github.com/$Repo.git" $SourceDir | Out-Host
139+
}
140+
141+
Push-Location $SourceDir
142+
try {
143+
Write-Info 'Building from source...'
144+
bun install --frozen-lockfile 2>$null
145+
if ($LASTEXITCODE -ne 0) {
146+
bun install | Out-Host
147+
}
148+
bun run build:dev:full | Out-Host
149+
} finally {
150+
Pop-Location
151+
}
152+
153+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
154+
Copy-Item -LiteralPath (Join-Path $SourceDir 'cli-dev.exe') -Destination (Join-Path $BinDir 'free-code.exe') -Force
155+
Set-Content -LiteralPath (Join-Path $BinDir 'free-code.cmd') -Value "@echo off`r`n""%~dp0\free-code.exe"" %*`r`n" -NoNewline
156+
Set-Content -LiteralPath (Join-Path $BinDir 'free-code.ps1') -Value '$exe = Join-Path $PSScriptRoot ''free-code.exe''; & $exe @args; exit $LASTEXITCODE' -NoNewline
157+
Write-Ok "Installed source-built binary to $BinDir\free-code.exe"
158+
}
159+
160+
Write-Host ''
161+
Write-Host 'free-code Windows installer' -ForegroundColor Cyan
162+
Write-Host ''
163+
164+
if (-not (Install-FromRelease)) {
165+
Write-Warn 'Falling back to source build.'
166+
Install-FromSource
167+
}
168+
169+
Ensure-UserPath
170+
171+
Write-Host ''
172+
Write-Host 'Installation complete.' -ForegroundColor Green
173+
Write-Host "Run: $BinDir\free-code.exe"
174+
Write-Host 'Then set ANTHROPIC_API_KEY and launch free-code.'

0 commit comments

Comments
 (0)