feat(web): /install route with server-side tracking
Some checks failed
CI / Docker build (linux/amd64) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled

Restores the curl installer script at /install. Adds:
- In-memory fetch counter (visible in container logs)
- Server-side PostHog event 'install_script_fetched' with
  IP, user-agent, and referer (fire-and-forget)
- Console log per fetch for monitoring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-09 20:57:37 +01:00
parent 1d506f3ea5
commit e6962f1454

View File

@@ -0,0 +1,135 @@
/**
* GET /install — shell installer for claudemesh-cli.
*
* curl -fsSL https://claudemesh.com/install | bash
*
* Tracks each fetch server-side (PostHog server event + console log).
* curl doesn't execute JS, so client-side analytics can't track this.
*/
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;
const SCRIPT = `#!/usr/bin/env bash
# claudemesh-cli installer
# Source: https://claudemesh.com/install
# Audit: curl -fsSL https://claudemesh.com/install | less
set -euo pipefail
RED=$'\\033[31m'; GREEN=$'\\033[32m'; DIM=$'\\033[2m'; BOLD=$'\\033[1m'; RESET=$'\\033[0m'
say() { printf "%s\\n" "$*"; }
ok() { printf "%s✓%s %s\\n" "\${GREEN}" "\${RESET}" "$*"; }
err() { printf "%s✗%s %s\\n" "\${RED}" "\${RESET}" "$*" >&2; }
say ""
say "\${BOLD}claudemesh-cli installer\${RESET}"
say "$(printf '%.0s─' {1..40})"
# --- preflight ------------------------------------------------------
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
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 ""
say "Registering Claude Code MCP server + status hooks…"
if ! claudemesh install; then
err "claudemesh install failed — run it manually to see the error."
exit 1
fi
# --- done -----------------------------------------------------------
say ""
say "\${GREEN}\${BOLD}Done.\${RESET}"
say ""
say "Next steps:"
say " 1. Restart Claude Code so the MCP tools appear."
say " 2. Join a mesh: \${BOLD}claudemesh join <invite-url>\${RESET}"
say " 3. Launch with push: \${BOLD}claudemesh launch\${RESET}"
say ""
say "Need an invite? Visit \${BOLD}https://claudemesh.com\${RESET}"
say ""
`;
export async function GET(): Promise<Response> {
installFetches++;
// Log server-side for monitoring
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";
console.log(
`[install] #${installFetches} | ip=${ip} | ua=${ua.slice(0, 80)} | ref=${referer}`,
);
// PostHog server-side event (if configured)
try {
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
if (posthogKey && posthogHost) {
fetch(`${posthogHost}/capture/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: posthogKey,
event: "install_script_fetched",
distinct_id: ip,
properties: {
user_agent: ua,
referer,
install_count: installFetches,
},
}),
}).catch(() => {}); // fire-and-forget
}
} catch {}
return new Response(SCRIPT, {
status: 200,
headers: {
"Content-Type": "text/x-shellscript; charset=utf-8",
"Cache-Control": "public, max-age=300, s-maxage=600",
"X-Content-Type-Options": "nosniff",
},
});
}