Skip to content

Commit e956dfb

Browse files
snomiaoclaude
andcommitted
ci: package installers for all platforms
- Windows: .zip with clx.exe, install.ps1, uninstall.ps1 (adds to PATH + Start Menu) - macOS: .tar.gz with clx + install.sh, code-signed in CI - Linux: .tar.gz + .deb package (installs to /usr/local/bin/clx) - Add universal install scripts: - Unix: curl -fsSL .../install.sh | bash - Windows: irm .../install.ps1 | iex Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b1c69f7 commit e956dfb

3 files changed

Lines changed: 182 additions & 17 deletions

File tree

.github/workflows/release-rust.yml

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ on:
66
tags:
77
- "v*"
88

9-
env:
10-
IS_PRERELEASE: ${{ contains(github.ref_name, '-') }}
11-
129
jobs:
1310
# Create the release first, then all builds upload to it
1411
create-release:
@@ -59,11 +56,28 @@ jobs:
5956
working-directory: rs
6057
run: cargo build -p capslockx-windows -p clx-screen-reader --release
6158

62-
- name: Stage release binaries
59+
- name: Package Windows installer zip
6360
shell: pwsh
6461
run: |
65-
Copy-Item rs\target\release\clx-rust.exe clx-rust.exe
66-
Copy-Item rs\target\release\clx-screen-reader.exe clx-screen-reader.exe
62+
$VERSION = "${{ github.ref_name }}"
63+
$STAGE = "CapsLockX"
64+
New-Item -ItemType Directory -Force -Path $STAGE
65+
Copy-Item rs\target\release\clx-rust.exe "$STAGE\clx.exe"
66+
Copy-Item rs\target\release\clx-screen-reader.exe "$STAGE\clx-screen-reader.exe"
67+
Copy-Item scripts\install.ps1 "$STAGE\install.ps1"
68+
# Create uninstaller
69+
@"
70+
# CapsLockX uninstaller
71+
`$InstallDir = "`$env:LOCALAPPDATA\CapsLockX"
72+
`$StartMenu = [Environment]::GetFolderPath("StartMenu")
73+
Remove-Item -Recurse -Force `$InstallDir -ErrorAction SilentlyContinue
74+
Remove-Item (Join-Path `$StartMenu "CapsLockX.lnk") -ErrorAction SilentlyContinue
75+
`$UserPath = [Environment]::GetEnvironmentVariable("Path", "User")
76+
`$NewPath = (`$UserPath -split ";" | Where-Object { `$_ -ne `$InstallDir }) -join ";"
77+
[Environment]::SetEnvironmentVariable("Path", `$NewPath, "User")
78+
Write-Host "CapsLockX uninstalled."
79+
"@ | Out-File -Encoding utf8 "$STAGE\uninstall.ps1"
80+
Compress-Archive -Path "$STAGE\*" -DestinationPath "CapsLockX-windows-x86_64.zip"
6781
6882
# Commit binaries back to main only for stable releases
6983
- name: Commit binaries to main
@@ -73,14 +87,17 @@ jobs:
7387
git checkout main
7488
git config user.name "github-actions[bot]"
7589
git config user.email "github-actions[bot]@users.noreply.github.com"
90+
Copy-Item CapsLockX\clx.exe clx-rust.exe
91+
Copy-Item CapsLockX\clx-screen-reader.exe clx-screen-reader.exe
7692
git add clx-rust.exe clx-screen-reader.exe
7793
git diff --cached --quiet || git commit -m "chore: update prebuilt binaries for ${{ github.ref_name }}"
7894
git push origin main
95+
shell: pwsh
7996

80-
- name: Upload Windows binaries
97+
- name: Upload Windows package
8198
env:
8299
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83-
run: gh release upload "${{ github.ref_name }}" clx-rust.exe clx-screen-reader.exe --clobber
100+
run: gh release upload "${{ github.ref_name }}" CapsLockX-windows-x86_64.zip --clobber
84101

85102
build-linux:
86103
name: Build Linux (x86_64-unknown-linux-gnu)
@@ -109,13 +126,51 @@ jobs:
109126
working-directory: rs
110127
run: cargo build -p capslockx-linux --release
111128

112-
- name: Stage Linux binary
113-
run: cp rs/target/release/capslockx capslockx-linux-x86_64
114-
115-
- name: Upload Linux binary
129+
- name: Package tar.gz
130+
run: |
131+
VERSION="${{ github.ref_name }}"
132+
STAGE="CapsLockX-${VERSION}-linux-x86_64"
133+
mkdir -p "$STAGE"
134+
cp rs/target/release/capslockx "$STAGE/clx"
135+
cp scripts/install.sh "$STAGE/install.sh"
136+
chmod +x "$STAGE/clx" "$STAGE/install.sh"
137+
tar czf "CapsLockX-linux-x86_64.tar.gz" "$STAGE"
138+
139+
- name: Package .deb
140+
run: |
141+
VERSION="${{ github.ref_name }}"
142+
# Strip leading 'v' and any pre-release suffix for deb version
143+
DEB_VERSION="${VERSION#v}"
144+
PKG="capslockx_${DEB_VERSION}_amd64"
145+
mkdir -p "$PKG/DEBIAN"
146+
mkdir -p "$PKG/usr/local/bin"
147+
cp rs/target/release/capslockx "$PKG/usr/local/bin/clx"
148+
chmod 755 "$PKG/usr/local/bin/clx"
149+
cat > "$PKG/DEBIAN/control" <<EOF
150+
Package: capslockx
151+
Version: ${DEB_VERSION}
152+
Section: utils
153+
Priority: optional
154+
Architecture: amd64
155+
Maintainer: snomiao <snomiao@gmail.com>
156+
Description: CapsLockX — keyboard productivity tool + LLM agent
157+
Uses CapsLock and Space as trigger keys for hotkey combos.
158+
Vim-style cursor, mouse control, window management, voice input, and AI agent.
159+
Homepage: https://github.com/snolab/CapsLockX
160+
EOF
161+
# Remove leading spaces from control file (heredoc indentation)
162+
sed -i 's/^ //' "$PKG/DEBIAN/control"
163+
dpkg-deb --build --root-owner-group "$PKG"
164+
mv "${PKG}.deb" "CapsLockX-linux-x86_64.deb"
165+
166+
- name: Upload Linux packages
116167
env:
117168
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118-
run: gh release upload "${{ github.ref_name }}" capslockx-linux-x86_64 --clobber
169+
run: |
170+
gh release upload "${{ github.ref_name }}" \
171+
CapsLockX-linux-x86_64.tar.gz \
172+
CapsLockX-linux-x86_64.deb \
173+
--clobber
119174
120175
build-macos:
121176
name: Build macOS (aarch64-apple-darwin)
@@ -141,8 +196,18 @@ jobs:
141196
working-directory: rs
142197
run: cargo build -p capslockx-macos --release
143198

144-
- name: Stage macOS binary
145-
run: cp rs/target/release/capslockx capslockx-macos-arm64
199+
- name: Code sign binary
200+
run: codesign -s - --force --identifier "com.snomiao.capslockx" rs/target/release/capslockx
201+
202+
- name: Package tar.gz
203+
run: |
204+
VERSION="${{ github.ref_name }}"
205+
STAGE="CapsLockX-${VERSION}-macos-arm64"
206+
mkdir -p "$STAGE"
207+
cp rs/target/release/capslockx "$STAGE/clx"
208+
cp scripts/install.sh "$STAGE/install.sh"
209+
chmod +x "$STAGE/clx" "$STAGE/install.sh"
210+
tar czf "CapsLockX-macos-arm64.tar.gz" "$STAGE"
146211
147212
# Commit binary to main only for stable releases
148213
- name: Commit macOS binary to main
@@ -153,11 +218,15 @@ jobs:
153218
git config user.name "github-actions[bot]"
154219
git config user.email "github-actions[bot]@users.noreply.github.com"
155220
git pull origin main --rebase
221+
cp rs/target/release/capslockx capslockx-macos-arm64
156222
git add capslockx-macos-arm64
157223
git diff --cached --quiet || git commit -m "chore: update macOS binary for ${{ github.ref_name }}"
158224
git push origin main
159225
160-
- name: Upload macOS binary
226+
- name: Upload macOS package
161227
env:
162228
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163-
run: gh release upload "${{ github.ref_name }}" capslockx-macos-arm64 --clobber
229+
run: |
230+
gh release upload "${{ github.ref_name }}" \
231+
CapsLockX-macos-arm64.tar.gz \
232+
--clobber

scripts/install.ps1

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# CapsLockX installer for Windows
2+
# Usage: irm https://raw.githubusercontent.com/snolab/CapsLockX/beta/scripts/install.ps1 | iex
3+
$ErrorActionPreference = "Stop"
4+
5+
$Repo = "snolab/CapsLockX"
6+
$InstallDir = "$env:LOCALAPPDATA\CapsLockX"
7+
$BinName = "clx.exe"
8+
9+
# Find latest release
10+
if ($env:CLX_VERSION) { $Tag = $env:CLX_VERSION }
11+
else {
12+
$releases = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases?per_page=10"
13+
$Tag = $releases[0].tag_name
14+
}
15+
Write-Host "Installing CapsLockX $Tag for Windows..."
16+
17+
# Download and extract
18+
$ZipUrl = "https://github.com/$Repo/releases/download/$Tag/CapsLockX-windows-x86_64.zip"
19+
$TmpZip = Join-Path $env:TEMP "capslockx.zip"
20+
Invoke-WebRequest -Uri $ZipUrl -OutFile $TmpZip
21+
22+
# Extract
23+
if (Test-Path $InstallDir) { Remove-Item -Recurse -Force $InstallDir }
24+
Expand-Archive -Path $TmpZip -DestinationPath $InstallDir -Force
25+
Remove-Item $TmpZip
26+
27+
# Add to PATH if not already there
28+
$UserPath = [Environment]::GetEnvironmentVariable("Path", "User")
29+
if ($UserPath -notlike "*$InstallDir*") {
30+
[Environment]::SetEnvironmentVariable("Path", "$InstallDir;$UserPath", "User")
31+
Write-Host "Added $InstallDir to PATH (restart terminal to take effect)."
32+
}
33+
34+
# Create Start Menu shortcut
35+
$StartMenu = [Environment]::GetFolderPath("StartMenu")
36+
$ShortcutPath = Join-Path $StartMenu "CapsLockX.lnk"
37+
$WScriptShell = New-Object -ComObject WScript.Shell
38+
$Shortcut = $WScriptShell.CreateShortcut($ShortcutPath)
39+
$Shortcut.TargetPath = Join-Path $InstallDir $BinName
40+
$Shortcut.WorkingDirectory = $InstallDir
41+
$Shortcut.Description = "CapsLockX keyboard productivity tool"
42+
$Shortcut.Save()
43+
44+
Write-Host "Installed to $InstallDir ($Tag)"
45+
Write-Host "Run 'clx' to start CapsLockX."

scripts/install.sh

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
# CapsLockX installer for macOS and Linux
3+
# Usage: curl -fsSL https://raw.githubusercontent.com/snolab/CapsLockX/beta/scripts/install.sh | bash
4+
set -euo pipefail
5+
6+
REPO="snolab/CapsLockX"
7+
INSTALL_DIR="/usr/local/bin"
8+
BIN_NAME="clx"
9+
10+
# Detect platform
11+
OS="$(uname -s)"
12+
ARCH="$(uname -m)"
13+
14+
case "${OS}-${ARCH}" in
15+
Darwin-arm64) ASSET="capslockx-macos-arm64" ;;
16+
Darwin-x86_64) ASSET="capslockx-macos-arm64" ;; # Rosetta 2
17+
Linux-x86_64) ASSET="capslockx-linux-x86_64" ;;
18+
*) echo "Unsupported platform: ${OS}-${ARCH}"; exit 1 ;;
19+
esac
20+
21+
# Find latest release (or beta)
22+
TAG="${CLX_VERSION:-}"
23+
if [ -z "$TAG" ]; then
24+
TAG="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=10" \
25+
| grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)"
26+
fi
27+
echo "Installing CapsLockX ${TAG} for ${OS} ${ARCH}..."
28+
29+
# Download
30+
URL="https://github.com/${REPO}/releases/download/${TAG}/${ASSET}"
31+
TMP="$(mktemp)"
32+
curl -fsSL -o "$TMP" "$URL"
33+
chmod +x "$TMP"
34+
35+
# Install
36+
if [ -w "$INSTALL_DIR" ]; then
37+
mv "$TMP" "${INSTALL_DIR}/${BIN_NAME}"
38+
else
39+
echo "Installing to ${INSTALL_DIR} (requires sudo)..."
40+
sudo mv "$TMP" "${INSTALL_DIR}/${BIN_NAME}"
41+
fi
42+
43+
# macOS: prompt for Accessibility permission
44+
if [ "$OS" = "Darwin" ]; then
45+
echo ""
46+
echo "Grant Accessibility permission when prompted."
47+
echo " System Settings → Privacy & Security → Accessibility → enable 'clx'"
48+
fi
49+
50+
echo "Installed: $(which $BIN_NAME) (${TAG})"
51+
echo "Run 'clx' to start CapsLockX."

0 commit comments

Comments
 (0)