Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/(marketing)/docs/cli/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function CliDocsPage() {

<section className="mt-10 space-y-3">
<h2 className="text-2xl font-bold">Install</h2>
<Cmd note="Uses bun if present, otherwise npm with Node 22.6 or newer. Never asks for sudo.">
<Cmd note="Installs under ~/.local and never asks for sudo; with no Node 22+ it fetches a private official Node first. Run it again to update; remove with sh ~/.local/share/crawlproof/uninstall.sh.">
curl -fsSL https://crawlproof.com/install.sh | sh
</Cmd>
<Cmd note="Or with npm. Needs Node 22.6 or newer for the dashboard. stats and --json run anywhere.">
Expand Down
9 changes: 9 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "**" }],
},
// The curl installer: shown as text in a browser, never offered as a download.
async headers() {
return [
{
source: "/install.sh",
headers: [{ key: "Content-Type", value: "text/plain; charset=utf-8" }],
},
];
},
// Permanent redirect: www.crawlproof.com → crawlproof.com (308 preserves
// the HTTP method and body, making it the correct choice for permanent moves
// on non-GET requests as well as GET).
Expand Down
206 changes: 152 additions & 54 deletions public/install.sh
Original file line number Diff line number Diff line change
@@ -1,61 +1,159 @@
#!/bin/sh
# Install the crawlproof CLI (npm: @profullstack/crawlproof): traffic, ads and
# money for every site you own, in a terminal.
# crawlproof installer.
#
# 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.
# Installs @profullstack/crawlproof (commands: crawlproof) under your home directory. No root, no system
# package manager, no system files touched. If this machine has no Node 22+,
# an official Node build is fetched (checksum verified) into the same private directory
# and used from there. Run this again to update. Remove with:
# sh ~/.local/share/crawlproof/uninstall.sh
#
# 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 writes into the nearest package.json above its global dir when
# that dir has none (e.g. ~/package.json): give it its own first.
BUN_GLOBAL="${BUN_INSTALL:-$HOME/.bun}/install/global"
mkdir -p "$BUN_GLOBAL"
[ -f "$BUN_GLOBAL/package.json" ] || printf '{}\n' > "$BUN_GLOBAL/package.json"
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"
# sh -s -- --version X install a specific release
# sh -s -- --prefix DIR install root (default: ~/.local)
#
# Listed in the W3BS catalog (https://w3bs.org/packages): `w3bs install crawlproof` runs
# this same script and `w3bs update` re-runs it.
#
# Generated from the W3BS npm CLI installer template; keep the variables below in sync
# with package.json. Nothing runs until the last line, so a truncated download is inert.

main() {
set -eu
PKG="@profullstack/crawlproof"
TOOL="crawlproof"
URL="https://crawlproof.com/install.sh"
NODE_MAJOR="22"
PREFIX="${INSTALL_PREFIX:-$HOME/.local}"
VERSION=""
NODE_CHOICE="${INSTALL_NODE:-auto}"

while [ $# -gt 0 ]; do
case "$1" in
--version) VERSION="${2:?--version needs a value}"; shift ;;
--prefix) PREFIX="${2:?--prefix needs a value}"; shift ;;
-h | --help) echo "Usage: curl -fsSL $URL | sh [-s -- --version X --prefix DIR]"; return 0 ;;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
*) echo "$TOOL installer: unknown option $1" >&2; return 64 ;;
esac
shift
done

say() { printf '%s\n' "$*"; }
fail() { printf '%s installer: %s\n' "$TOOL" "$*" >&2; exit 1; }

case "$(uname -s)" in
Linux) OS=linux ;;
Darwin) OS=darwin ;;
*) fail "unsupported operating system $(uname -s); Linux and macOS are supported" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) ARCH=x64 ;;
aarch64 | arm64) ARCH=arm64 ;;
*) fail "unsupported CPU $(uname -m)" ;;
esac
if command -v curl >/dev/null 2>&1; then
fetch() { curl --proto '=https' --tlsv1.2 -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
fetch() { wget --https-only -q "$1" -O "$2"; }
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"
fail "curl or wget is required"
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"

SHARE="$PREFIX/share/$TOOL"
BIN="$PREFIX/bin"
mkdir -p "$SHARE" "$BIN"

node_major() { "$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
NODE=""
NODE_KIND=""
if [ "$NODE_CHOICE" != private ] && command -v node >/dev/null 2>&1 && [ "$(node_major node)" -ge "$NODE_MAJOR" ]; then
NODE="$(command -v node)"
NODE_KIND=system
elif [ -x "$SHARE/node/bin/node" ] && [ "$(node_major "$SHARE/node/bin/node")" -ge "$NODE_MAJOR" ]; then
NODE="$SHARE/node/bin/node"
NODE_KIND=private
fi
if [ -z "$NODE" ]; then
say "No Node $NODE_MAJOR+ here; fetching an official Node 24 into $SHARE/node ..."
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fetch "https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt" "$TMP/SHASUMS256.txt" || fail "cannot reach nodejs.org"
TARBALL="$(grep -o "node-v24\.[0-9.]*-$OS-$ARCH\.tar\.gz" "$TMP/SHASUMS256.txt" | head -n 1 || true)"
[ -n "$TARBALL" ] || fail "nodejs.org has no Node 24 build for $OS-$ARCH"
fetch "https://nodejs.org/dist/latest-v24.x/$TARBALL" "$TMP/$TARBALL" || fail "cannot download $TARBALL"
if command -v sha256sum >/dev/null 2>&1; then SUM="$(sha256sum "$TMP/$TARBALL" | cut -d ' ' -f 1)"
else SUM="$(shasum -a 256 "$TMP/$TARBALL" | cut -d ' ' -f 1)"; fi
grep -q "^$SUM $TARBALL\$" "$TMP/SHASUMS256.txt" || fail "checksum mismatch for $TARBALL"
rm -rf "$SHARE/node" && mkdir -p "$SHARE/node"
tar -xzf "$TMP/$TARBALL" -C "$SHARE/node" --strip-components=1 || fail "cannot unpack $TARBALL"
NODE="$SHARE/node/bin/node"
NODE_KIND=private
fi
if [ "$NODE_KIND" = private ]; then NPM="$SHARE/node/bin/npm"; else NPM="$(dirname "$NODE")/npm"; [ -x "$NPM" ] || NPM=npm; fi
# npm runs package scripts with the Node in PATH; make that the chosen one.
PATH="$(dirname "$NODE"):$PATH"
export PATH

SPEC="$PKG"
[ -n "$VERSION" ] && SPEC="$PKG@$VERSION"
say "Installing $SPEC into $SHARE ..."
rm -rf "$SHARE/node_modules" "$SHARE/package.json" "$SHARE/package-lock.json"
cd "$SHARE"
printf '{ "name": "%s-install", "private": true }\n' "$TOOL" > package.json
"$NPM" install --silent --no-audit --no-fund --omit=dev "$SPEC" >/dev/null 2>&1 ||
"$NPM" install --no-audit --no-fund --omit=dev "$SPEC" || fail "npm could not install $SPEC"
PKG_DIR="$SHARE/node_modules/$PKG"
[ -f "$PKG_DIR/package.json" ] || fail "$SPEC installed but $PKG_DIR is missing"

# One shim per entry in the package's own `bin` map: a script, not a symlink, so the
# command always runs this copy with the chosen Node.
BINS="$("$NODE" -e '
const p = require(process.argv[1] + "/package.json");
const b = typeof p.bin === "string" ? { [p.name.split("/").pop()]: p.bin } : (p.bin || {});
for (const [k, v] of Object.entries(b)) if (/^[A-Za-z0-9._-]+$/.test(k)) console.log(k + "\t" + v);
' "$PKG_DIR")"
[ -n "$BINS" ] || fail "$SPEC declares no commands"
PATHS="$SHARE"
NAMES=""
OLDIFS="$IFS"
IFS='
'
for line in $BINS; do
name="${line%% *}"
rel="${line#* }"
entry="$PKG_DIR/$rel"
[ -f "$entry" ] || fail "$SPEC is missing $rel"
printf '#!/bin/sh\n# %s, installed by the %s installer (%s).\nexec "%s" "%s" "$@"\n' "$name" "$TOOL" "$URL" "$NODE" "$entry" > "$BIN/$name"
chmod 0755 "$BIN/$name"
PATHS="$PATHS
$BIN/$name"
NAMES="$NAMES $name"
done
IFS="$OLDIFS"

INSTALLED="$("$NODE" -p "require('$PKG_DIR/package.json').version" 2>/dev/null || echo unknown)"
{
printf '{\n "package": "%s",\n "version": "%s",\n "installer": "%s",\n' "$PKG" "$INSTALLED" "$URL"
printf ' "installedAt": "%s",\n "node": "%s",\n "paths": [\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$NODE_KIND"
printf '%s\n' "$PATHS" | sed 's/.*/ "&",/' | sed '$ s/,$//'
printf ' ]\n}\n'
} > "$SHARE/manifest.json"
{
echo '#!/bin/sh'
echo "# Removes $TOOL. Written by its installer."
echo 'set -eu'
printf '%s\n' "$PATHS" | sed 's|.*|rm -rf "&"|'
echo "echo '$TOOL removed.'"
} > "$SHARE/uninstall.sh"
chmod 0755 "$SHARE/uninstall.sh"

say ""
say "Installed $PKG $INSTALLED:$NAMES (in $BIN)"
case ":$PATH:" in
*":$BIN:"*) ;;
*) say "Add $BIN to your PATH: echo 'export PATH=\"$BIN:\$PATH\"' >> ~/.profile" ;;
esac
say "Update by running the installer again; remove with: sh $SHARE/uninstall.sh"
}

main "$@"