From ccf95ff38258e6de6f8c0ca971545f2edb666de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:42:16 +0100 Subject: [PATCH] feat(distribution): binary release pipeline + brew + winget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .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) --- .github/workflows/release-cli.yml | 112 ++++++++++++++++++++++ apps/web/src/app/install/route.ts | 111 ++++++++++++++------- packaging/homebrew/claudemesh.rb.template | 54 +++++++++++ packaging/winget/claudemesh.yaml.template | 31 ++++++ 4 files changed, 275 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/release-cli.yml create mode 100644 packaging/homebrew/claudemesh.rb.template create mode 100644 packaging/winget/claudemesh.yaml.template diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml new file mode 100644 index 0000000..1b61772 --- /dev/null +++ b/.github/workflows/release-cli.yml @@ -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)" diff --git a/apps/web/src/app/install/route.ts b/apps/web/src/app/install/route.ts index bc925ce..127eace 100644 --- a/apps/web/src/app/install/route.ts +++ b/apps/web/src/app/install/route.ts @@ -30,42 +30,87 @@ 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}" - exit 1 +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 + 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)" + ok "npm $(npm -v)" + say "" + say "Installing \${BOLD}claudemesh-cli\${RESET} from npm…" + if ! npm install -g claudemesh-cli; then + err "npm install failed." + say " If this is a permissions error on macOS/Linux, try:" + say " \${DIM}sudo npm install -g claudemesh-cli\${RESET}" + say " or configure npm to use a user-owned prefix:" + say " \${DIM}https://docs.npmjs.com/resolving-eacces-permissions-errors\${RESET}" + 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 -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 -fi -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 - err "npm install failed." - say " If this is a permissions error on macOS/Linux, try:" - say " \${DIM}sudo npm install -g claudemesh-cli\${RESET}" - say " or configure npm to use a user-owned prefix:" - say " \${DIM}https://docs.npmjs.com/resolving-eacces-permissions-errors\${RESET}" - exit 1 -fi -ok "claudemesh-cli installed ($(claudemesh --version))" - # --- register MCP + hooks ------------------------------------------ say "" diff --git a/packaging/homebrew/claudemesh.rb.template b/packaging/homebrew/claudemesh.rb.template new file mode 100644 index 0000000..a17edb8 --- /dev/null +++ b/packaging/homebrew/claudemesh.rb.template @@ -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 diff --git a/packaging/winget/claudemesh.yaml.template b/packaging/winget/claudemesh.yaml.template new file mode 100644 index 0000000..91adba9 --- /dev/null +++ b/packaging/winget/claudemesh.yaml.template @@ -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