From bc50e4443e43d1d83038e9d017ed4c7affd7b1af Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 12 Oct 2026 23:58:56 +0000 Subject: [PATCH] feat: serve /install.sh, a one-line curl installer for the crawlproof CLI curl -fsSL https://crawlproof.com/install.sh | sh installs the scoped @profullstack/crawlproof (the unscoped `crawlproof` on npm is not ours): bun if present, else npm with Node >= 22.6, falling back to ~/.local instead of sudo, then checks the bin is on PATH. Re-running it upgrades. public/install.sh is excluded from the proxy matcher, as llms.txt is, so the crawl gateway and the session refresh never run on it. The line is in the README, the CLI README, /docs/cli and a new CLI section in llms.txt. tests/install-sh.test.ts pins the scoped package and the matcher exclusion. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- app/(marketing)/docs/cli/page.tsx | 5 ++- app/llms.txt/route.ts | 5 +++ packages/cli/README.md | 2 +- proxy.ts | 2 +- public/install.sh | 56 +++++++++++++++++++++++++++++++ tests/install-sh.test.ts | 28 ++++++++++++++++ 7 files changed, 96 insertions(+), 4 deletions(-) create mode 100755 public/install.sh create mode 100644 tests/install-sh.test.ts diff --git a/README.md b/README.md index 0bbba213..b01d1f48 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ own, ad delivery, and the bank feed behind it. Published from [/docs/cli](https://crawlproof.com/docs/cli). ```sh -npm install -g @profullstack/crawlproof +curl -fsSL https://crawlproof.com/install.sh | sh # or: npm install -g @profullstack/crawlproof echo '{ "token": "crp_…" }' > ~/.crawlproof.json && chmod 600 ~/.crawlproof.json crawlproof dashboard # five live screens: ROI, Traffic, Ads, Money, Spend diff --git a/app/(marketing)/docs/cli/page.tsx b/app/(marketing)/docs/cli/page.tsx index 7409a8fc..ed295d72 100644 --- a/app/(marketing)/docs/cli/page.tsx +++ b/app/(marketing)/docs/cli/page.tsx @@ -38,7 +38,10 @@ export default function CliDocsPage() {

Install

- + + curl -fsSL https://crawlproof.com/install.sh | sh + + npm install -g @profullstack/crawlproof

diff --git a/app/llms.txt/route.ts b/app/llms.txt/route.ts index 43a6aea7..5ad59b07 100644 --- a/app/llms.txt/route.ts +++ b/app/llms.txt/route.ts @@ -27,6 +27,11 @@ const body = `# CrawlProof - AI-bot rules for GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, Applebot-Extended, CCBot - Positioning clarity: who, what, audience, pricing, CTA +## CLI +- Install: curl -fsSL ${env.siteUrl}/install.sh | sh (or npm install -g @profullstack/crawlproof; the unscoped "crawlproof" package is not ours) +- crawlproof dashboard, crawlproof stats , crawlproof ads; authenticates with an API token (CRAWLPROOF_TOKEN or ~/.crawlproof.json) +- Docs: ${env.siteUrl}/docs/cli + ## Contact - General: hello@crawlproof.com - Sales: sales@crawlproof.com diff --git a/packages/cli/README.md b/packages/cli/README.md index 351bee72..84a35f1a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -3,7 +3,7 @@ What the fleet costs and what it returns, in a terminal. ``` -npm install -g @profullstack/crawlproof +curl -fsSL https://crawlproof.com/install.sh | sh # or: npm install -g @profullstack/crawlproof crawlproof dashboard ``` diff --git a/proxy.ts b/proxy.ts index e38566e5..2446ba80 100644 --- a/proxy.ts +++ b/proxy.ts @@ -102,6 +102,6 @@ export async function proxy(request: NextRequest) { export const config = { matcher: [ - "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|llms.txt|skill.md|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", + "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|llms.txt|skill.md|install.sh|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], }; diff --git a/public/install.sh b/public/install.sh new file mode 100755 index 00000000..caf9ec51 --- /dev/null +++ b/public/install.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Install the crawlproof CLI (npm: @profullstack/crawlproof): traffic, ads and +# money for every site you own, in a terminal. +# +# curl -fsSL https://crawlproof.com/install.sh | sh +# +# Uses bun if present, otherwise npm (Node >= 22.6). When the global npm +# prefix is not writable it installs into ~/.local instead of asking for sudo. +# Re-running it upgrades to the latest release. +# +# The package is scoped: the unscoped `crawlproof` on npm is someone else's. +set -eu + +PKG="@profullstack/crawlproof" +BIN="crawlproof" + +say() { printf '%s\n' "$*"; } +die() { printf 'crawlproof install: %s\n' "$*" >&2; exit 1; } +have() { command -v "$1" >/dev/null 2>&1; } + +say "Installing crawlproof (CrawlProof CLI)..." + +if have bun; then + bun add -g "$PKG@latest" + BIN_DIR="${BUN_INSTALL:-$HOME/.bun}/bin" +elif have npm && have node; then + node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=6)?0:1)' 2>/dev/null \ + || die "Node >= 22.6 required (found $(node -v)). Upgrade Node or install bun: https://bun.sh" + prefix=$(npm config get prefix) + modules="$prefix/lib/node_modules" + [ -d "$modules" ] || modules="$prefix/lib" + if [ -w "$modules" ] && [ -w "$prefix/bin" ]; then + npm install -g "$PKG@latest" + BIN_DIR="$prefix/bin" + else + BIN_DIR="$HOME/.local/bin" + say "npm prefix $prefix is not writable; installing into $HOME/.local" + npm install -g --prefix "$HOME/.local" "$PKG@latest" + fi +else + die "needs bun (https://bun.sh) or Node >= 22.6 with npm (https://nodejs.org)" +fi + +say "" +if have "$BIN"; then + say "crawlproof $("$BIN" --version 2>/dev/null || echo '') installed." +else + say "crawlproof installed to $BIN_DIR, which is not on your PATH. Add it:" + say " export PATH=\"$BIN_DIR:\$PATH\"" +fi +say "" +say "Next: create an API token at https://crawlproof.com/dashboard (Social -> API tokens), then" +say " export CRAWLPROOF_TOKEN=crp_... # or put { \"token\": \"crp_...\" } in ~/.crawlproof.json" +say " crawlproof dashboard # ROI, Traffic, Ads, Money, Spend" +say " crawlproof stats example.com # one site as text" +say " crawlproof help # every command" diff --git a/tests/install-sh.test.ts b/tests/install-sh.test.ts new file mode 100644 index 00000000..e4c33196 --- /dev/null +++ b/tests/install-sh.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// `curl -fsSL https://crawlproof.com/install.sh | sh` is the CLI's one-line +// install. It is a static file in public/, and the proxy must not run on it: +// the crawl gateway would answer curl's 402 or HTML instead of the script. +const root = join(__dirname, ".."); + +describe("/install.sh", () => { + const script = readFileSync(join(root, "public/install.sh"), "utf8"); + + it("is a POSIX shell script that installs the scoped package", () => { + expect(script.startsWith("#!/bin/sh\n")).toBe(true); + expect(script).toContain('PKG="@profullstack/crawlproof"'); + // The unscoped `crawlproof` on npm is not ours. + expect(script).not.toMatch(/(?:npm install -g|bun add -g) "?crawlproof\b/); + }); + + it("is excluded from the proxy matcher", () => { + const source = readFileSync(join(root, "proxy.ts"), "utf8"); + const literal = /matcher:\s*\[\s*"([^"]+)"/.exec(source)?.[1]; + expect(literal).toBeDefined(); + const matcher = new RegExp(`^${literal!.replace(/\\\\/g, "\\")}$`); + expect(matcher.test("/install.sh")).toBe(false); + expect(matcher.test("/docs/cli")).toBe(true); + }); +});