Skip to content

Commit 7bb2fe3

Browse files
ralyodioclaude
andauthored
feat(install): W3BS installer at /install.sh (private Node, uninstall) (#356)
Replace the bun/npm-global install.sh with the W3BS npm CLI installer template so crawlproof can be listed at https://w3bs.org/packages. Same URL and curl line; it now installs under ~/.local/share/crawlproof, fetches a checksum-verified official Node when the machine has none, writes a manifest.json and uninstall.sh, and is served as text/plain instead of application/x-sh. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8cb8700 commit 7bb2fe3

3 files changed

Lines changed: 162 additions & 55 deletions

File tree

‎app/(marketing)/docs/cli/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function CliDocsPage() {
3838

3939
<section className="mt-10 space-y-3">
4040
<h2 className="text-2xl font-bold">Install</h2>
41-
<Cmd note="Uses bun if present, otherwise npm with Node 22.6 or newer. Never asks for sudo.">
41+
<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.">
4242
curl -fsSL https://crawlproof.com/install.sh | sh
4343
</Cmd>
4444
<Cmd note="Or with npm. Needs Node 22.6 or newer for the dashboard. stats and --json run anywhere.">

‎next.config.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ const nextConfig: NextConfig = {
4141
images: {
4242
remotePatterns: [{ protocol: "https", hostname: "**" }],
4343
},
44+
// The curl installer: shown as text in a browser, never offered as a download.
45+
async headers() {
46+
return [
47+
{
48+
source: "/install.sh",
49+
headers: [{ key: "Content-Type", value: "text/plain; charset=utf-8" }],
50+
},
51+
];
52+
},
4453
// Permanent redirect: www.crawlproof.com → crawlproof.com (308 preserves
4554
// the HTTP method and body, making it the correct choice for permanent moves
4655
// on non-GET requests as well as GET).

‎public/install.sh‎

Lines changed: 152 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,159 @@
11
#!/bin/sh
2-
# Install the crawlproof CLI (npm: @profullstack/crawlproof): traffic, ads and
3-
# money for every site you own, in a terminal.
2+
# crawlproof installer.
43
#
54
# curl -fsSL https://crawlproof.com/install.sh | sh
65
#
7-
# Uses bun if present, otherwise npm (Node >= 22.6). When the global npm
8-
# prefix is not writable it installs into ~/.local instead of asking for sudo.
9-
# Re-running it upgrades to the latest release.
6+
# Installs @profullstack/crawlproof (commands: crawlproof) under your home directory. No root, no system
7+
# package manager, no system files touched. If this machine has no Node 22+,
8+
# an official Node build is fetched (checksum verified) into the same private directory
9+
# and used from there. Run this again to update. Remove with:
10+
# sh ~/.local/share/crawlproof/uninstall.sh
1011
#
11-
# The package is scoped: the unscoped `crawlproof` on npm is someone else's.
12-
set -eu
13-
14-
PKG="@profullstack/crawlproof"
15-
BIN="crawlproof"
16-
17-
say() { printf '%s\n' "$*"; }
18-
die() { printf 'crawlproof install: %s\n' "$*" >&2; exit 1; }
19-
have() { command -v "$1" >/dev/null 2>&1; }
20-
21-
say "Installing crawlproof (CrawlProof CLI)..."
22-
23-
if have bun; then
24-
# bun add -g writes into the nearest package.json above its global dir when
25-
# that dir has none (e.g. ~/package.json): give it its own first.
26-
BUN_GLOBAL="${BUN_INSTALL:-$HOME/.bun}/install/global"
27-
mkdir -p "$BUN_GLOBAL"
28-
[ -f "$BUN_GLOBAL/package.json" ] || printf '{}\n' > "$BUN_GLOBAL/package.json"
29-
bun add -g "$PKG@latest"
30-
BIN_DIR="${BUN_INSTALL:-$HOME/.bun}/bin"
31-
elif have npm && have node; then
32-
node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=6)?0:1)' 2>/dev/null \
33-
|| die "Node >= 22.6 required (found $(node -v)). Upgrade Node or install bun: https://bun.sh"
34-
prefix=$(npm config get prefix)
35-
modules="$prefix/lib/node_modules"
36-
[ -d "$modules" ] || modules="$prefix/lib"
37-
if [ -w "$modules" ] && [ -w "$prefix/bin" ]; then
38-
npm install -g "$PKG@latest"
39-
BIN_DIR="$prefix/bin"
12+
# sh -s -- --version X install a specific release
13+
# sh -s -- --prefix DIR install root (default: ~/.local)
14+
#
15+
# Listed in the W3BS catalog (https://w3bs.org/packages): `w3bs install crawlproof` runs
16+
# this same script and `w3bs update` re-runs it.
17+
#
18+
# Generated from the W3BS npm CLI installer template; keep the variables below in sync
19+
# with package.json. Nothing runs until the last line, so a truncated download is inert.
20+
21+
main() {
22+
set -eu
23+
PKG="@profullstack/crawlproof"
24+
TOOL="crawlproof"
25+
URL="https://crawlproof.com/install.sh"
26+
NODE_MAJOR="22"
27+
PREFIX="${INSTALL_PREFIX:-$HOME/.local}"
28+
VERSION=""
29+
NODE_CHOICE="${INSTALL_NODE:-auto}"
30+
31+
while [ $# -gt 0 ]; do
32+
case "$1" in
33+
--version) VERSION="${2:?--version needs a value}"; shift ;;
34+
--prefix) PREFIX="${2:?--prefix needs a value}"; shift ;;
35+
-h | --help) echo "Usage: curl -fsSL $URL | sh [-s -- --version X --prefix DIR]"; return 0 ;;
36+
*) echo "$TOOL installer: unknown option $1" >&2; return 64 ;;
37+
esac
38+
shift
39+
done
40+
41+
say() { printf '%s\n' "$*"; }
42+
fail() { printf '%s installer: %s\n' "$TOOL" "$*" >&2; exit 1; }
43+
44+
case "$(uname -s)" in
45+
Linux) OS=linux ;;
46+
Darwin) OS=darwin ;;
47+
*) fail "unsupported operating system $(uname -s); Linux and macOS are supported" ;;
48+
esac
49+
case "$(uname -m)" in
50+
x86_64 | amd64) ARCH=x64 ;;
51+
aarch64 | arm64) ARCH=arm64 ;;
52+
*) fail "unsupported CPU $(uname -m)" ;;
53+
esac
54+
if command -v curl >/dev/null 2>&1; then
55+
fetch() { curl --proto '=https' --tlsv1.2 -fsSL "$1" -o "$2"; }
56+
elif command -v wget >/dev/null 2>&1; then
57+
fetch() { wget --https-only -q "$1" -O "$2"; }
4058
else
41-
BIN_DIR="$HOME/.local/bin"
42-
say "npm prefix $prefix is not writable; installing into $HOME/.local"
43-
npm install -g --prefix "$HOME/.local" "$PKG@latest"
59+
fail "curl or wget is required"
4460
fi
45-
else
46-
die "needs bun (https://bun.sh) or Node >= 22.6 with npm (https://nodejs.org)"
47-
fi
48-
49-
say ""
50-
if have "$BIN"; then
51-
say "crawlproof $("$BIN" --version 2>/dev/null || echo '') installed."
52-
else
53-
say "crawlproof installed to $BIN_DIR, which is not on your PATH. Add it:"
54-
say " export PATH=\"$BIN_DIR:\$PATH\""
55-
fi
56-
say ""
57-
say "Next: create an API token at https://crawlproof.com/dashboard (Social -> API tokens), then"
58-
say " export CRAWLPROOF_TOKEN=crp_... # or put { \"token\": \"crp_...\" } in ~/.crawlproof.json"
59-
say " crawlproof dashboard # ROI, Traffic, Ads, Money, Spend"
60-
say " crawlproof stats example.com # one site as text"
61-
say " crawlproof help # every command"
61+
62+
SHARE="$PREFIX/share/$TOOL"
63+
BIN="$PREFIX/bin"
64+
mkdir -p "$SHARE" "$BIN"
65+
66+
node_major() { "$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
67+
NODE=""
68+
NODE_KIND=""
69+
if [ "$NODE_CHOICE" != private ] && command -v node >/dev/null 2>&1 && [ "$(node_major node)" -ge "$NODE_MAJOR" ]; then
70+
NODE="$(command -v node)"
71+
NODE_KIND=system
72+
elif [ -x "$SHARE/node/bin/node" ] && [ "$(node_major "$SHARE/node/bin/node")" -ge "$NODE_MAJOR" ]; then
73+
NODE="$SHARE/node/bin/node"
74+
NODE_KIND=private
75+
fi
76+
if [ -z "$NODE" ]; then
77+
say "No Node $NODE_MAJOR+ here; fetching an official Node 24 into $SHARE/node ..."
78+
TMP="$(mktemp -d)"
79+
trap 'rm -rf "$TMP"' EXIT
80+
fetch "https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt" "$TMP/SHASUMS256.txt" || fail "cannot reach nodejs.org"
81+
TARBALL="$(grep -o "node-v24\.[0-9.]*-$OS-$ARCH\.tar\.gz" "$TMP/SHASUMS256.txt" | head -n 1 || true)"
82+
[ -n "$TARBALL" ] || fail "nodejs.org has no Node 24 build for $OS-$ARCH"
83+
fetch "https://nodejs.org/dist/latest-v24.x/$TARBALL" "$TMP/$TARBALL" || fail "cannot download $TARBALL"
84+
if command -v sha256sum >/dev/null 2>&1; then SUM="$(sha256sum "$TMP/$TARBALL" | cut -d ' ' -f 1)"
85+
else SUM="$(shasum -a 256 "$TMP/$TARBALL" | cut -d ' ' -f 1)"; fi
86+
grep -q "^$SUM $TARBALL\$" "$TMP/SHASUMS256.txt" || fail "checksum mismatch for $TARBALL"
87+
rm -rf "$SHARE/node" && mkdir -p "$SHARE/node"
88+
tar -xzf "$TMP/$TARBALL" -C "$SHARE/node" --strip-components=1 || fail "cannot unpack $TARBALL"
89+
NODE="$SHARE/node/bin/node"
90+
NODE_KIND=private
91+
fi
92+
if [ "$NODE_KIND" = private ]; then NPM="$SHARE/node/bin/npm"; else NPM="$(dirname "$NODE")/npm"; [ -x "$NPM" ] || NPM=npm; fi
93+
# npm runs package scripts with the Node in PATH; make that the chosen one.
94+
PATH="$(dirname "$NODE"):$PATH"
95+
export PATH
96+
97+
SPEC="$PKG"
98+
[ -n "$VERSION" ] && SPEC="$PKG@$VERSION"
99+
say "Installing $SPEC into $SHARE ..."
100+
rm -rf "$SHARE/node_modules" "$SHARE/package.json" "$SHARE/package-lock.json"
101+
cd "$SHARE"
102+
printf '{ "name": "%s-install", "private": true }\n' "$TOOL" > package.json
103+
"$NPM" install --silent --no-audit --no-fund --omit=dev "$SPEC" >/dev/null 2>&1 ||
104+
"$NPM" install --no-audit --no-fund --omit=dev "$SPEC" || fail "npm could not install $SPEC"
105+
PKG_DIR="$SHARE/node_modules/$PKG"
106+
[ -f "$PKG_DIR/package.json" ] || fail "$SPEC installed but $PKG_DIR is missing"
107+
108+
# One shim per entry in the package's own `bin` map: a script, not a symlink, so the
109+
# command always runs this copy with the chosen Node.
110+
BINS="$("$NODE" -e '
111+
const p = require(process.argv[1] + "/package.json");
112+
const b = typeof p.bin === "string" ? { [p.name.split("/").pop()]: p.bin } : (p.bin || {});
113+
for (const [k, v] of Object.entries(b)) if (/^[A-Za-z0-9._-]+$/.test(k)) console.log(k + "\t" + v);
114+
' "$PKG_DIR")"
115+
[ -n "$BINS" ] || fail "$SPEC declares no commands"
116+
PATHS="$SHARE"
117+
NAMES=""
118+
OLDIFS="$IFS"
119+
IFS='
120+
'
121+
for line in $BINS; do
122+
name="${line%% *}"
123+
rel="${line#* }"
124+
entry="$PKG_DIR/$rel"
125+
[ -f "$entry" ] || fail "$SPEC is missing $rel"
126+
printf '#!/bin/sh\n# %s, installed by the %s installer (%s).\nexec "%s" "%s" "$@"\n' "$name" "$TOOL" "$URL" "$NODE" "$entry" > "$BIN/$name"
127+
chmod 0755 "$BIN/$name"
128+
PATHS="$PATHS
129+
$BIN/$name"
130+
NAMES="$NAMES $name"
131+
done
132+
IFS="$OLDIFS"
133+
134+
INSTALLED="$("$NODE" -p "require('$PKG_DIR/package.json').version" 2>/dev/null || echo unknown)"
135+
{
136+
printf '{\n "package": "%s",\n "version": "%s",\n "installer": "%s",\n' "$PKG" "$INSTALLED" "$URL"
137+
printf ' "installedAt": "%s",\n "node": "%s",\n "paths": [\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$NODE_KIND"
138+
printf '%s\n' "$PATHS" | sed 's/.*/ "&",/' | sed '$ s/,$//'
139+
printf ' ]\n}\n'
140+
} > "$SHARE/manifest.json"
141+
{
142+
echo '#!/bin/sh'
143+
echo "# Removes $TOOL. Written by its installer."
144+
echo 'set -eu'
145+
printf '%s\n' "$PATHS" | sed 's|.*|rm -rf "&"|'
146+
echo "echo '$TOOL removed.'"
147+
} > "$SHARE/uninstall.sh"
148+
chmod 0755 "$SHARE/uninstall.sh"
149+
150+
say ""
151+
say "Installed $PKG $INSTALLED:$NAMES (in $BIN)"
152+
case ":$PATH:" in
153+
*":$BIN:"*) ;;
154+
*) say "Add $BIN to your PATH: echo 'export PATH=\"$BIN:\$PATH\"' >> ~/.profile" ;;
155+
esac
156+
say "Update by running the installer again; remove with: sh $SHARE/uninstall.sh"
157+
}
158+
159+
main "$@"

0 commit comments

Comments
 (0)