chore: wrap up the gap-closing session
- info/inbox commands → unified render.ts - install route: drop in-memory counter, rely on PostHog + structured logs - docs: roadmap, CLAUDE.md reflect alpha.31 state - tests workflow now also builds + smoke-tests the CLI bundle - homebrew tap bootstrap kit in packaging/homebrew-tap-bootstrap/ (README + copy of the formula template for dropping into the tap repo) - upstream Claude Code issue draft for rich <channel> UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Feature request draft: rich `<channel>` notification UI
|
||||
|
||||
**Target:** `anthropics/claude-code` GitHub issues / feedback channel.
|
||||
**Drafted:** 2026-04-15.
|
||||
|
||||
Paste the section below once the issue template is ready. Adjust tone
|
||||
to match Claude Code's issue style.
|
||||
|
||||
---
|
||||
|
||||
### Title
|
||||
|
||||
Rich UI for `notifications/claude/channel` messages (first-class chat, not just reminders)
|
||||
|
||||
### Body
|
||||
|
||||
**Summary**
|
||||
|
||||
MCP servers can emit `notifications/claude/channel` notifications which
|
||||
Claude Code renders inside the current turn as a `<channel>` reminder.
|
||||
For MCP servers that are conversational in nature (peer messaging,
|
||||
collaborative sessions, delegated agents), rendering these inline as
|
||||
plain-text reminders misses the UX affordances users expect from chat:
|
||||
|
||||
- sender avatar / identity
|
||||
- timestamp
|
||||
- priority badge (urgent / normal / low)
|
||||
- expandable quote from the original thread
|
||||
- optional inline reply action that calls a specific MCP tool
|
||||
|
||||
**Concrete use case**
|
||||
|
||||
[claudemesh](https://claudemesh.com) is a peer mesh for Claude Code
|
||||
sessions. When a peer sends a message it arrives as
|
||||
`notifications/claude/channel` with structured metadata in `meta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "notifications/claude/channel",
|
||||
"params": {
|
||||
"content": "alice: can you rebase main before deploy?",
|
||||
"meta": {
|
||||
"from_id": "<ed25519 hex>",
|
||||
"from_name": "alice",
|
||||
"priority": "now",
|
||||
"sent_at": "2026-04-15T00:00:00Z",
|
||||
"mesh_slug": "team-platform",
|
||||
"kind": "direct"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Today this renders as a `<channel>` text block — useful, but the user
|
||||
can't tell at a glance that it's from another human.
|
||||
|
||||
**What we'd like**
|
||||
|
||||
A hint on the notification (e.g. `meta.display: "chat"`) that lets
|
||||
Claude Code render it as a chat bubble with the `from_name` as the
|
||||
speaker, priority visualised, and an optional "Reply" action bound to
|
||||
a declared MCP tool (`reply_tool_name`).
|
||||
|
||||
**Why users would benefit beyond claudemesh**
|
||||
|
||||
- Delegated agent frameworks can render sub-agent responses as chat
|
||||
- Live-pairing MCP servers get a proper UI without inventing their own
|
||||
- The existing `<channel>` fallback means older clients still see
|
||||
the same text — additive, not breaking
|
||||
|
||||
**Willing to contribute a PR** if the feature is on-roadmap.
|
||||
84
.artifacts/specs/2026-04-15-invite-v2-cli-migration.md
Normal file
84
.artifacts/specs/2026-04-15-invite-v2-cli-migration.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Invite v2 — CLI migration (server-side already shipped)
|
||||
|
||||
## Current state
|
||||
|
||||
**Server-side (broker) — DEPLOYED**
|
||||
- `canonicalInviteV2` bytes format (crypto.ts)
|
||||
- `verifyInviteV2` signature check
|
||||
- `claimInviteV2Core` at `POST /invites/:code/claim`
|
||||
- `sealRootKeyToRecipient` using crypto_box_seal
|
||||
- Every v1 invite also stores `capability_v2` for cross-compat
|
||||
- Web route `/api/public/invites/:code/claim` proxies to broker
|
||||
|
||||
**Client-side (CLI) — NOT MIGRATED**
|
||||
The CLI still uses the v1 flow (`enrollWithBroker`) which reads
|
||||
`mesh_root_key` from the invite token's base64 payload. This means:
|
||||
- Long URL `/join/<token>` contains the root key
|
||||
- Short URL `/i/<code>` resolves to the long URL → still contains root key
|
||||
- Anyone who can read the URL (history, screenshot, mail archive) has the key
|
||||
|
||||
## The v2 CLI flow
|
||||
|
||||
```
|
||||
parseInviteLinkV2(url)
|
||||
→ short URL /i/<code>? GET /api/public/invite-code/:code
|
||||
→ returns `{ found, code, mesh_slug, broker_url, owner_pubkey,
|
||||
canonical_v2, expires_at, role }` (NO root_key)
|
||||
→ generate local x25519 keypair (curve25519)
|
||||
→ POST /invites/<code>/claim { recipient_x25519_pubkey, display_name }
|
||||
→ broker verifies capability_v2 signature
|
||||
→ broker seals mesh.root_key with crypto_box_seal(root_key, our_pubkey)
|
||||
→ returns { sealed_root_key, mesh_id, member_id, owner_pubkey, canonical_v2 }
|
||||
→ open sealed_root_key with our x25519 secret key
|
||||
→ store root_key in ~/.claudemesh/config.json.meshes[].rootKey
|
||||
(NOT in the invite link — it was never transmitted unsealed)
|
||||
→ upgrade enroll to use claim response instead of the /join endpoint
|
||||
```
|
||||
|
||||
## What needs to change in the CLI
|
||||
|
||||
1. **New file** `apps/cli/src/services/invite/parse-v2.ts`
|
||||
- Detect short URL, resolve via `/api/public/invite-code/:code`
|
||||
- Expect the API returns v2 shape (server already has this route; verify field names)
|
||||
- Generate x25519 keypair via libsodium
|
||||
- POST to claim endpoint
|
||||
- Unseal root_key
|
||||
|
||||
2. **Conditional in `parseInviteLink`**
|
||||
- If URL is short-form and broker supports v2, use the new path
|
||||
- Fall back to v1 for legacy long-form URLs in transit
|
||||
|
||||
3. **Config schema** already has `rootKey` per mesh — just write from
|
||||
unsealed bytes instead of from the token payload.
|
||||
|
||||
4. **Spec test** `tests/golden/invite-v2.test.ts`
|
||||
- Broker already has `claimInviteV2Core` tests; add a CLI-side
|
||||
end-to-end that hits a local broker and verifies the sealed key
|
||||
round-trips.
|
||||
|
||||
## Why it wasn't rushed in this session
|
||||
|
||||
Crypto code deserves review. The server-side v2 shipped weeks ago
|
||||
with its own testing and audit; the CLI migration needs the same
|
||||
rigor — at minimum, a test that proves the sealed key we unseal
|
||||
matches the root_key the broker had in its DB, verified against
|
||||
`canonical_v2` signature.
|
||||
|
||||
The current v1 flow is a known quantity (the root_key-in-URL risk
|
||||
is documented in the spec). Broker is already v2-ready so when the
|
||||
CLI migration lands, emails / links can immediately start using the
|
||||
claim-only short URL without a server deploy.
|
||||
|
||||
## Rollout plan
|
||||
|
||||
1. Ship CLI v2 path behind `CLAUDEMESH_INVITE_V2=1` env.
|
||||
2. Dogfood: new invites generated by `claudemesh share` use `/api/public/invite-code/:code` with v2-shape response that omits token; CLI resolves via claim.
|
||||
3. Verify with `claudemesh verify` safety numbers cross-check.
|
||||
4. After 2 weeks uneventful, flip default to v2.
|
||||
5. After 60 days, stop embedding root_key in long URLs entirely.
|
||||
6. v3 (future): short URL becomes the only form.
|
||||
|
||||
## Effort
|
||||
|
||||
~1 day of focused crypto + testing. Broker work is done; API work is
|
||||
done; CLI work is a new parse path + a new enroll path + a few tests.
|
||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -45,3 +45,12 @@ jobs:
|
||||
|
||||
- name: 🧪 Test
|
||||
run: pnpm run test
|
||||
|
||||
- name: 📦 Build CLI bundle (check size budget)
|
||||
working-directory: apps/cli
|
||||
run: pnpm run build
|
||||
|
||||
- name: 🔧 CLI smoke — --version + --help
|
||||
run: |
|
||||
node apps/cli/dist/entrypoints/cli.js --version
|
||||
node apps/cli/dist/entrypoints/cli.js --help | head -5
|
||||
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -4,10 +4,12 @@ Peer mesh for Claude Code sessions. Broker + CLI + MCP server.
|
||||
|
||||
## Structure
|
||||
|
||||
- `apps/broker/` — WebSocket broker (Bun + Drizzle + PostgreSQL), deployed at `wss://ic.claudemesh.com/ws`
|
||||
- `apps/cli/` — `claudemesh-cli` npm package (CLI + MCP server)
|
||||
- `apps/broker/` — WebSocket broker (Bun + Drizzle + PostgreSQL), deployed at `wss://ic.claudemesh.com/ws`. Runs drizzle migrations on startup under pg_advisory_lock.
|
||||
- `apps/cli/` — `claudemesh-cli` npm package (CLI + MCP server). Was `apps/cli-v2/` until 2026-04-15; legacy v0 at branch `legacy-cli-archive` + tag `cli-v0-legacy-final`.
|
||||
- `apps/web/` — Marketing site + dashboard at claudemesh.com
|
||||
- `docs/` — Protocol spec, quickstart, FAQ, roadmap
|
||||
- `packaging/` — Homebrew formula + winget manifest templates
|
||||
- `.github/workflows/release-cli.yml` — tag `cli-v*` → 5 platform binaries → GitHub Release with SHA256SUMS
|
||||
|
||||
## Key docs
|
||||
|
||||
@@ -18,8 +20,10 @@ Peer mesh for Claude Code sessions. Broker + CLI + MCP server.
|
||||
|
||||
## Deploy
|
||||
|
||||
- **Broker:** `git push gitea-vps main` triggers Coolify auto-deploy. Manual: `curl -s -X GET "http://100.122.34.28:8000/api/v1/deploy?uuid=mcn8m74tbxfxbplmyb40b2ia" -H "Authorization: Bearer 3|K2vkSJzdUA69rj22CKZc5z0YB6pkY43GLEonti3UzcnqVJj6WhrqqYTAng6DzMUi"`
|
||||
- **CLI:** `cd apps/cli && pnpm publish --access public --no-git-checks`
|
||||
- **Broker:** `git push gitea-vps main` triggers Coolify auto-deploy. Manual: `curl -s -X GET "http://100.122.34.28:8000/api/v1/deploy?uuid=mcn8m74tbxfxbplmyb40b2ia" -H "Authorization: Bearer 3|K2vkSJzdUA69rj22CKZc5z0YB6pkY43GLEonti3UzcnqVJj6WhrqqYTAng6DzMUi"`. Pending migrations apply automatically on startup.
|
||||
- **CLI:**
|
||||
- npm: `cd apps/cli && npm publish --tag alpha --access public --no-git-checks --ignore-scripts`
|
||||
- Binaries: `git tag cli-v<version> && git push github cli-v<version>` — workflow builds 5 platforms.
|
||||
- **Web:** Vercel auto-deploy on push to GitHub
|
||||
|
||||
## Dev
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import { withMesh } from "./connect.js";
|
||||
import type { InboundPush } from "~/services/broker/facade.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
import { bold, dim } from "~/ui/styles.js";
|
||||
|
||||
export interface InboxFlags {
|
||||
mesh?: string;
|
||||
@@ -14,47 +16,34 @@ export interface InboxFlags {
|
||||
wait?: number;
|
||||
}
|
||||
|
||||
function formatMessage(msg: InboundPush, useColor: boolean): string {
|
||||
const dim = (s: string) => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const bold = (s: string) => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||
|
||||
function formatMessage(msg: InboundPush): string {
|
||||
const text = msg.plaintext ?? `[encrypted: ${msg.ciphertext.slice(0, 32)}…]`;
|
||||
const from = msg.senderPubkey.slice(0, 8);
|
||||
const time = new Date(msg.createdAt).toLocaleTimeString();
|
||||
const kindTag = msg.kind === "direct" ? "→ direct" : msg.kind;
|
||||
|
||||
return ` ${bold(from)} ${dim(`[${kindTag}] ${time}`)}\n ${text}`;
|
||||
}
|
||||
|
||||
export async function runInbox(flags: InboxFlags): Promise<void> {
|
||||
const useColor =
|
||||
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||
const dim = (s: string) => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const bold = (s: string) => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||
|
||||
const waitMs = (flags.wait ?? 1) * 1000;
|
||||
|
||||
await withMesh({ meshSlug: flags.mesh ?? null }, async (client, mesh) => {
|
||||
// Wait briefly for broker to push any held messages.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
|
||||
|
||||
const messages = client.drainPushBuffer();
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(messages, null, 2));
|
||||
process.stdout.write(JSON.stringify(messages, null, 2) + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log(dim(`No messages on mesh "${mesh.slug}".`));
|
||||
render.info(dim(`No messages on mesh "${mesh.slug}".`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(bold(`Inbox — ${mesh.slug}`) + dim(` (${messages.length} message${messages.length === 1 ? "" : "s"})`));
|
||||
console.log("");
|
||||
render.section(`inbox — ${mesh.slug} (${messages.length} message${messages.length === 1 ? "" : "s"})`);
|
||||
for (const msg of messages) {
|
||||
console.log(formatMessage(msg, useColor));
|
||||
console.log("");
|
||||
process.stdout.write(formatMessage(msg) + "\n\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { withMesh } from "./connect.js";
|
||||
import { readConfig } from "~/services/config/facade.js";
|
||||
import { render } from "~/ui/render.js";
|
||||
|
||||
export interface InfoFlags {
|
||||
mesh?: string;
|
||||
@@ -13,11 +14,6 @@ export interface InfoFlags {
|
||||
}
|
||||
|
||||
export async function runInfo(flags: InfoFlags): Promise<void> {
|
||||
const useColor =
|
||||
!process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY;
|
||||
const dim = (s: string) => (useColor ? `\x1b[2m${s}\x1b[22m` : s);
|
||||
const bold = (s: string) => (useColor ? `\x1b[1m${s}\x1b[22m` : s);
|
||||
|
||||
const config = readConfig();
|
||||
|
||||
await withMesh({ meshSlug: flags.mesh ?? null }, async (client, mesh) => {
|
||||
@@ -39,20 +35,24 @@ export async function runInfo(flags: InfoFlags): Promise<void> {
|
||||
};
|
||||
|
||||
if (flags.json) {
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(bold(mesh.slug) + dim(` · ${mesh.brokerUrl}`));
|
||||
console.log(dim(` mesh: ${mesh.meshId}`));
|
||||
console.log(dim(` member: ${mesh.memberId}`));
|
||||
console.log(` peers: ${peers.length} connected`);
|
||||
console.log(` state: ${state.length} keys`);
|
||||
render.section(`${mesh.slug} · ${mesh.brokerUrl}`);
|
||||
render.kv([
|
||||
["mesh", mesh.meshId],
|
||||
["member", mesh.memberId],
|
||||
["peers", `${peers.length} connected`],
|
||||
["state", `${state.length} keys`],
|
||||
]);
|
||||
if (brokerInfo && typeof brokerInfo === "object") {
|
||||
const extras: Array<[string, string]> = [];
|
||||
for (const [k, v] of Object.entries(brokerInfo)) {
|
||||
if (["slug", "meshId", "brokerUrl"].includes(k)) continue;
|
||||
console.log(dim(` ${k}: ${JSON.stringify(v)}`));
|
||||
extras.push([k, JSON.stringify(v)]);
|
||||
}
|
||||
if (extras.length) render.kv(extras);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
|
||||
import { headers } from "next/headers";
|
||||
|
||||
// In-memory counter (resets on deploy — good enough for a signal).
|
||||
// For persistent tracking, write to DB or use PostHog server SDK.
|
||||
let installFetches = 0;
|
||||
// Persistent counts live in PostHog (event: install_script_fetched).
|
||||
// Stdout logs are aggregated by the Coolify log collector. No in-memory
|
||||
// counter — it reset on every deploy and misled operators into thinking
|
||||
// the signal was fresh.
|
||||
|
||||
const SCRIPT = `#!/usr/bin/env bash
|
||||
# claudemesh-cli installer
|
||||
@@ -140,19 +141,24 @@ say ""
|
||||
`;
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
installFetches++;
|
||||
|
||||
// Log server-side for monitoring
|
||||
// Log server-side for Coolify log aggregation.
|
||||
const h = await headers();
|
||||
const ua = h.get("user-agent") ?? "unknown";
|
||||
const ip = h.get("x-forwarded-for") ?? h.get("x-real-ip") ?? "unknown";
|
||||
const referer = h.get("referer") ?? "direct";
|
||||
|
||||
const ts = new Date().toISOString();
|
||||
console.log(
|
||||
`[install] #${installFetches} | ip=${ip} | ua=${ua.slice(0, 80)} | ref=${referer}`,
|
||||
JSON.stringify({
|
||||
level: "info",
|
||||
event: "install_script_fetched",
|
||||
ts,
|
||||
ip,
|
||||
ua: ua.slice(0, 120),
|
||||
referer,
|
||||
}),
|
||||
);
|
||||
|
||||
// PostHog server-side event (if configured)
|
||||
// PostHog server-side event — source of truth for counts.
|
||||
try {
|
||||
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
|
||||
@@ -164,10 +170,10 @@ export async function GET(): Promise<Response> {
|
||||
api_key: posthogKey,
|
||||
event: "install_script_fetched",
|
||||
distinct_id: ip,
|
||||
timestamp: ts,
|
||||
properties: {
|
||||
user_agent: ua,
|
||||
referer,
|
||||
install_count: installFetches,
|
||||
},
|
||||
}),
|
||||
}).catch(() => {}); // fire-and-forget
|
||||
|
||||
@@ -17,18 +17,42 @@ broker, ready for real teams.
|
||||
|
||||
---
|
||||
|
||||
## In progress — *v0.1.x*
|
||||
## v1.0.0-alpha — *shipping now*
|
||||
|
||||
Security and onboarding work landing inside the v0.1 line, before
|
||||
v0.2.0 cuts.
|
||||
The ship-all push — Claude Code-grade CLI, zero-Node binary distribution,
|
||||
end-to-end crypto backup, per-peer capability grants, self-update.
|
||||
|
||||
- **v2 invite protocol** — short opaque codes (`claudemesh.com/i/{code}`)
|
||||
replace base64url URLs that embedded the mesh root key. The key is
|
||||
now sealed to a recipient-controlled x25519 pubkey on claim, never in
|
||||
a URL. v1 invites keep working through v0.1.x; removed at v0.2.0.
|
||||
- **Email invites** — admins invite by email. A new `pending_invite`
|
||||
table tracks `{email, code, sentAt, acceptedAt, revokedAt}`;
|
||||
delivery goes through Postmark.
|
||||
- **Single-binary distribution** — `curl -fsSL claudemesh.com/install | sh`
|
||||
downloads the right binary (darwin/linux/windows × x64/arm64) when
|
||||
Node isn't present. GitHub Releases auto-publishes on each `cli-v*` tag.
|
||||
- **`claudemesh://` URL scheme** — invite emails become one-click.
|
||||
`claudemesh url-handler install` registers the scheme per-OS.
|
||||
- **`claudemesh <url>`** — join + launch in one command. `-y` makes it
|
||||
fully non-interactive for CI.
|
||||
- **Live status line in Claude Code** — `◇ <mesh> · N/M online` polled
|
||||
from the MCP server's peer cache. Enable with
|
||||
`claudemesh install --status-line`.
|
||||
- **Per-peer capability grants** — `claudemesh grant/revoke/block/grants`.
|
||||
Enforced server-side in the broker (silent drop) and client-side in
|
||||
the MCP server.
|
||||
- **Encrypted backup / restore** — `claudemesh backup` / `restore` with
|
||||
Argon2id + XChaCha20-Poly1305. Portable `.cmb` recovery file.
|
||||
- **Safety numbers** — `claudemesh verify <peer>` shows a 30-digit SAS
|
||||
derived from both ed25519 pubkeys, for out-of-band verification.
|
||||
- **Shell completions** — `claudemesh completions zsh|bash|fish`.
|
||||
- **QR on share** — `claudemesh share` prints a terminal QR for
|
||||
phone-to-laptop pairing.
|
||||
- **Self-update** — `claudemesh upgrade` reinstalls the latest alpha
|
||||
via the npm that installed the running binary.
|
||||
- **Auto-migrate on broker startup** — pending drizzle migrations apply
|
||||
under `pg_advisory_lock` before the HTTP server binds. Exits non-zero
|
||||
on failure so Coolify fails the healthcheck closed.
|
||||
- **v2 invite protocol (broker + API)** — short opaque codes
|
||||
(`/i/{code}`); broker seals `mesh_root_key` to a recipient x25519
|
||||
pubkey via `crypto_box_seal`. CLI migration tracked at
|
||||
`.artifacts/specs/2026-04-15-invite-v2-cli-migration.md`.
|
||||
- **Email invites** — admins invite by email via Postmark with a
|
||||
branded react-email template.
|
||||
|
||||
---
|
||||
|
||||
|
||||
54
packaging/homebrew-tap-bootstrap/Formula/claudemesh.rb
Normal file
54
packaging/homebrew-tap-bootstrap/Formula/claudemesh.rb
Normal 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
|
||||
38
packaging/homebrew-tap-bootstrap/README.md
Normal file
38
packaging/homebrew-tap-bootstrap/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Bootstrapping the `homebrew-claudemesh` tap
|
||||
|
||||
A Homebrew tap is just a GitHub repo named `homebrew-<anything>` in the
|
||||
organization whose formulas you want to expose. Users add it with:
|
||||
|
||||
```
|
||||
brew tap alezmad/claudemesh
|
||||
brew install claudemesh
|
||||
```
|
||||
|
||||
## One-time setup
|
||||
|
||||
1. Create a public GitHub repo called **`homebrew-claudemesh`** under the
|
||||
`alezmad` account (or any organization Homebrew can resolve — the name
|
||||
after `homebrew-` is the "tap" users type, and the owner is the namespace).
|
||||
2. Copy `packaging/homebrew-tap-bootstrap/Formula/claudemesh.rb` from THIS
|
||||
repo into the tap's `Formula/claudemesh.rb`.
|
||||
3. Fill in the `sha256` placeholders. For each platform, run:
|
||||
```
|
||||
curl -sL https://github.com/alezmad/claudemesh/releases/download/cli-v1.0.0/claudemesh-darwin-arm64 | sha256sum
|
||||
```
|
||||
(And so on for the three other platforms.)
|
||||
4. Commit + push. Users can now `brew tap alezmad/claudemesh && brew install claudemesh`.
|
||||
|
||||
## Keeping it up to date
|
||||
|
||||
The release workflow (`.github/workflows/release-cli.yml`) has an
|
||||
`update-homebrew` job that fires on non-prerelease tags. It calls
|
||||
`brew bump-formula-pr` against the tap, which opens a PR with updated
|
||||
`url`, `version`, and `sha256` entries.
|
||||
|
||||
Requires a `HOMEBREW_TAP_TOKEN` secret on the repo — a PAT scoped to the
|
||||
tap repo with `contents:write`.
|
||||
|
||||
## Why not auto-create the repo from this workflow?
|
||||
|
||||
Repo creation needs org-level permissions that shouldn't live in CI. One
|
||||
manual step, then everything after is automatic.
|
||||
Reference in New Issue
Block a user