feat(distribution): binary release pipeline + brew + winget
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

- .github/workflows/release-cli.yml: build self-contained binaries via
  `bun build --compile` for darwin/linux/windows × x64/arm64 on every
  cli-v* tag, attach to GitHub Release with SHA256SUMS, auto-bump the
  homebrew tap on non-prerelease versions.
- packaging/homebrew/claudemesh.rb.template: formula template for the
  homebrew-claudemesh tap.
- packaging/winget/claudemesh.yaml.template: winget manifest template.
- /install script now detects absence of Node and downloads the
  platform-appropriate binary from the GitHub Release, installs to
  ~/.claudemesh/bin, and shims into ~/.local/bin — zero Node required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-15 02:42:16 +01:00
parent 43f2728283
commit ccf95ff382
4 changed files with 275 additions and 33 deletions

112
.github/workflows/release-cli.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
name: Release CLI binaries
# Fires on any push of a tag shaped like `cli-v1.2.3` (prerelease `-alpha.N` OK).
# Builds self-contained `bun build --compile` binaries for darwin/linux/win
# (x64 + arm64) and attaches them to a GitHub Release. The `install.sh`
# fallback path curls these when Node isn't available.
#
# Publishing to npm is still a manual step (pnpm publish from apps/cli-v2) —
# this workflow only handles binary distribution.
on:
push:
tags:
- "cli-v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag to build (e.g. cli-v1.0.0-alpha.28)"
required: true
permissions:
contents: write # to upload release assets
jobs:
build:
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { target: darwin-x64, bun_target: bun-darwin-x64, runner: macos-latest, ext: "" }
- { target: darwin-arm64, bun_target: bun-darwin-arm64, runner: macos-latest, ext: "" }
- { target: linux-x64, bun_target: bun-linux-x64, runner: ubuntu-latest, ext: "" }
- { target: linux-arm64, bun_target: bun-linux-arm64, runner: ubuntu-latest, ext: "" }
- { target: windows-x64, bun_target: bun-windows-x64, runner: windows-latest, ext: ".exe" }
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2"
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install workspace deps
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Compile binary
working-directory: apps/cli-v2
shell: bash
run: |
mkdir -p dist/bin
bun build --compile --minify \
--target=${{ matrix.bun_target }} \
src/entrypoints/cli.ts \
--outfile dist/bin/claudemesh-${{ matrix.target }}${{ matrix.ext }}
- name: Smoke test (non-Windows)
if: matrix.target != 'windows-x64'
working-directory: apps/cli-v2
run: |
./dist/bin/claudemesh-${{ matrix.target }} --version
./dist/bin/claudemesh-${{ matrix.target }} --help | head -5
- name: Upload artefact
uses: actions/upload-artifact@v4
with:
name: claudemesh-${{ matrix.target }}
path: apps/cli-v2/dist/bin/claudemesh-${{ matrix.target }}${{ matrix.ext }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Stage binaries
run: |
mkdir -p release
find artifacts -type f -exec cp {} release/ \;
cd release && sha256sum claudemesh-* > SHA256SUMS
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
files: |
release/claudemesh-*
release/SHA256SUMS
generate_release_notes: true
fail_on_unmatched_files: true
update-homebrew:
needs: release
runs-on: macos-latest
if: github.event_name == 'push' && !contains(github.ref_name, 'alpha')
steps:
- name: Bump Homebrew tap formula
env:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
brew tap alezmad/claudemesh || true
brew bump-formula-pr --no-browse --no-fork \
--tag "${{ github.ref_name }}" \
--revision "${{ github.sha }}" \
alezmad/claudemesh/claudemesh || echo "formula bump skipped (no tap yet)"

View File

@@ -30,30 +30,61 @@ say "\${BOLD}claudemesh-cli installer\${RESET}"
say "$(printf '%.0s─' {1..40})"
# --- preflight ------------------------------------------------------
# Prefer npm when Node 20+ is present. Otherwise fall back to a
# self-contained binary download from GitHub Releases (installs to
# ~/.claudemesh/bin and adds a shim at ~/.local/bin/claudemesh).
if ! command -v node >/dev/null 2>&1; then
err "Node.js is not installed."
say " Install Node.js 20 or newer: \${BOLD}https://nodejs.org\${RESET}"
say " Or via nvm: \${DIM}curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\${RESET}"
detect_os() {
case "$(uname -s)" in
Darwin) echo darwin ;;
Linux) echo linux ;;
*) echo "" ;;
esac
}
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo x64 ;;
arm64|aarch64) echo arm64 ;;
*) echo "" ;;
esac
}
install_via_binary() {
local os arch url target dir shim
os=$(detect_os); arch=$(detect_arch)
if [ -z "$os" ] || [ -z "$arch" ]; then
err "No precompiled binary for $(uname -s)/$(uname -m). Install Node 20+ or build from source."
exit 1
fi
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
if [ "$NODE_MAJOR" -lt 20 ]; then
err "Node.js $(node -v) is too old — claudemesh-cli needs >= 20."
say " Upgrade: \${BOLD}https://nodejs.org\${RESET}"
exit 1
dir="\${HOME}/.claudemesh/bin"
mkdir -p "$dir"
target="\${dir}/claudemesh"
url="https://github.com/alezmad/claudemesh/releases/latest/download/claudemesh-\${os}-\${arch}"
say "Downloading \${BOLD}\${url}\${RESET}"
if ! curl -fsSL "$url" -o "$target"; then
err "Download failed. Falling back to npm."
return 1
fi
chmod +x "$target"
shim="\${HOME}/.local/bin/claudemesh"
mkdir -p "\${HOME}/.local/bin"
printf '#!/bin/sh\\nexec "%s" "$@"\\n' "$target" > "$shim"
chmod +x "$shim"
ok "claudemesh binary installed → \${target}"
case ":$PATH:" in
*":\${HOME}/.local/bin:"*) : ;;
*)
say ""
say "\${BOLD}Add \${HOME}/.local/bin to PATH\${RESET} (add to your shell rc):"
say " export PATH=\\"\${HOME}/.local/bin:\$PATH\\""
;;
esac
return 0
}
install_via_npm() {
ok "Node.js $(node -v)"
if ! command -v npm >/dev/null 2>&1; then
err "npm is not installed (usually ships with Node)."
exit 1
fi
ok "npm $(npm -v)"
# --- install --------------------------------------------------------
say ""
say "Installing \${BOLD}claudemesh-cli\${RESET} from npm…"
if ! npm install -g claudemesh-cli; then
@@ -65,6 +96,20 @@ if ! npm install -g claudemesh-cli; then
exit 1
fi
ok "claudemesh-cli installed ($(claudemesh --version))"
}
if command -v node >/dev/null 2>&1; then
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)
if [ "$NODE_MAJOR" -ge 20 ] && command -v npm >/dev/null 2>&1; then
install_via_npm
else
say "Node.js < 20 or no npm — using standalone binary."
install_via_binary || install_via_npm
fi
else
say "Node.js not detected — installing standalone binary (no Node required)."
install_via_binary
fi
# --- register MCP + hooks ------------------------------------------

View File

@@ -0,0 +1,54 @@
# Homebrew formula template — lives in the `alezmad/homebrew-claudemesh` tap.
#
# The release-cli workflow bumps `version`, `url`, and `sha256` per platform
# via `brew bump-formula-pr`. This template is the source shape — copy it
# into the tap repo as `Formula/claudemesh.rb` when bootstrapping, then let
# CI keep it up to date.
class Claudemesh < Formula
desc "Peer mesh for Claude Code sessions"
homepage "https://claudemesh.com"
version "1.0.0-alpha.28"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/alezmad/claudemesh/releases/download/cli-v#{version}/claudemesh-darwin-arm64"
sha256 "REPLACED_BY_CI"
else
url "https://github.com/alezmad/claudemesh/releases/download/cli-v#{version}/claudemesh-darwin-x64"
sha256 "REPLACED_BY_CI"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/alezmad/claudemesh/releases/download/cli-v#{version}/claudemesh-linux-arm64"
sha256 "REPLACED_BY_CI"
else
url "https://github.com/alezmad/claudemesh/releases/download/cli-v#{version}/claudemesh-linux-x64"
sha256 "REPLACED_BY_CI"
end
end
def install
bin.install Dir["*"].first => "claudemesh"
end
def caveats
<<~EOS
To enable click-to-launch from invite emails:
claudemesh url-handler install
To show live peer count in Claude Code:
claudemesh install --status-line
Shell completions:
claudemesh completions zsh > "$(brew --prefix)/share/zsh/site-functions/_claudemesh"
EOS
end
test do
assert_match "claudemesh", shell_output("#{bin}/claudemesh --version")
end
end

View File

@@ -0,0 +1,31 @@
# winget manifest template for claudemesh.
# Submit via PR to microsoft/winget-pkgs after each non-prerelease cli-v* tag.
# The release-cli workflow can automate this with a wingetcreate step.
PackageIdentifier: Claudemesh.Claudemesh
PackageVersion: 1.0.0
PackageName: Claudemesh
Publisher: Alejandro Gutierrez
License: MIT
LicenseUrl: https://github.com/alezmad/claudemesh/blob/main/LICENSE
ShortDescription: Peer mesh for Claude Code sessions
Description: |-
Claudemesh connects multiple Claude Code sessions into a peer mesh.
End-to-end encrypted, keys stay on your machine. Invite, share state,
verify safety numbers, back up encrypted configs.
Moniker: claudemesh
Tags:
- claude
- cli
- mesh
- ai
- developer-tools
Installers:
- Architecture: x64
InstallerType: portable
InstallerUrl: https://github.com/alezmad/claudemesh/releases/download/cli-v{{version}}/claudemesh-windows-x64.exe
InstallerSha256: REPLACED_BY_CI
Commands:
- claudemesh
ManifestType: singleton
ManifestVersion: 1.6.0