feat(web): /install shell script + real curl one-liner on landing
Landing page showed \`curl -fsSL claudemesh.sh/install | bash\` but the domain didn't resolve, so anyone copy-pasting got a DNS error. Ship: - apps/web/src/app/install/route.ts: GET returns an auditable bash installer (Node preflight, npm install -g claudemesh-cli, runs claudemesh install, prints next steps, colored output). No Node auto-install — fails clean if missing with a pointer. - apps/web/src/proxy.ts: exclude /install from the i18n matcher so Next.js returns the shell script unmangled. - hero.tsx + features.tsx: swap claudemesh.sh → claudemesh.com. Test: curl http://localhost:3000/install | bash -n → OK. Content-Type: text/x-shellscript; charset=utf-8. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
apps/web/src/app/install/route.ts
Normal file
100
apps/web/src/app/install/route.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* GET /install — serves a shell installer for claudemesh-cli.
|
||||||
|
*
|
||||||
|
* Intended to be piped into bash:
|
||||||
|
* curl -fsSL https://claudemesh.com/install | bash
|
||||||
|
*
|
||||||
|
* The script is kept short + auditable. It does not try to install
|
||||||
|
* Node for the user — it checks for a compatible Node + npm and
|
||||||
|
* directs them to install Node themselves if missing. Running `bash`
|
||||||
|
* against a domain you do not fully trust is always a risk; publishing
|
||||||
|
* the script this way (rather than obfuscating it behind a binary
|
||||||
|
* blob) lets security-conscious users inspect before executing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 function GET(): Response {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ export const Features = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
<button
|
<button
|
||||||
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
||||||
aria-label="Copy"
|
aria-label="Copy"
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const Hero = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export const proxy = (request: NextRequest) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: "/((?!api|static|.*\\..*|_next).*)",
|
matcher: "/((?!api|static|install|.*\\..*|_next).*)",
|
||||||
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user