@basein/runner 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -21
- package/dist/auth/client.d.ts +40 -1
- package/dist/auth/client.js +77 -9
- package/dist/bin/bir-hooks.d.ts +18 -3
- package/dist/bin/bir-hooks.js +124 -38
- package/dist/bin/bir.d.ts +2 -0
- package/dist/bin/bir.js +362 -39
- package/dist/bin/setup.d.ts +72 -0
- package/dist/bin/setup.js +286 -0
- package/dist/config/adapters/claude-code.d.ts +90 -4
- package/dist/config/adapters/claude-code.js +164 -16
- package/dist/config/generate.d.ts +93 -1
- package/dist/config/generate.js +90 -3
- package/dist/control/client.d.ts +5 -0
- package/dist/control/client.js +8 -0
- package/dist/control/daemon.d.ts +116 -0
- package/dist/control/daemon.js +339 -0
- package/dist/control/discovery.d.ts +26 -0
- package/dist/control/discovery.js +41 -9
- package/dist/control/ensure-hook.d.ts +39 -0
- package/dist/control/ensure-hook.js +98 -0
- package/dist/control/paths.d.ts +14 -0
- package/dist/control/paths.js +20 -0
- package/dist/control/server.d.ts +28 -0
- package/dist/control/server.js +15 -2
- package/dist/proxy/session.d.ts +8 -1
- package/dist/proxy/session.js +28 -6
- package/docs/calculatedReplayGuide.md +157 -70
- package/docs/installRun.md +457 -111
- package/docs/loginWeb.md +1 -1
- package/docs/quickstart.md +193 -158
- package/package.json +2 -1
- package/scripts/install.ps1 +669 -0
- package/scripts/install.sh +586 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# BaseIn runner — one-line setup for macOS and Linux.
|
|
4
|
+
#
|
|
5
|
+
# curl -fsSL https://api.bi2202.com/install.sh | BIR_SETUP_TOKEN="<token>" sh
|
|
6
|
+
#
|
|
7
|
+
# Paste that in a terminal opened in the project you start Claude Code in (the
|
|
8
|
+
# console's "Set up the runner" page mints the token). When it finishes, `claude`
|
|
9
|
+
# in that folder is recorded and replayed, and nothing has to stay open.
|
|
10
|
+
#
|
|
11
|
+
# What it does, in order — every step prints a "==> …" line:
|
|
12
|
+
#
|
|
13
|
+
# 0. refuses the home folder and the root of the disk, before anything is
|
|
14
|
+
# downloaded (BIR_PROJECT=<dir> points it somewhere else);
|
|
15
|
+
# 1. Node: the machine's `node` when it is >= 20, has npm next to it and is not
|
|
16
|
+
# managed by a version manager (nvm, fnm, Volta, asdf, mise, nodenv — their
|
|
17
|
+
# paths change per shell or per version, and the recorder's SessionStart hook
|
|
18
|
+
# pins an absolute path); otherwise a private Node 24.21.0 is downloaded into
|
|
19
|
+
# ~/.baseinstrunner/node/v<V>/, checked against the digests embedded below.
|
|
20
|
+
# The private Node is never put on PATH;
|
|
21
|
+
# 2. the runner: @basein/runner into ~/.baseinstrunner/npm, always through
|
|
22
|
+
# npm-cli.js and never into the machine's npm prefix (no sudo, ever); then
|
|
23
|
+
# launchers `bir` and `bir-hooks` in ~/.baseinstrunner/bin and one PATH line
|
|
24
|
+
# in the shell profile;
|
|
25
|
+
# 3. Claude Code, through its official installer, when `claude` is missing;
|
|
26
|
+
# 4. `bir setup`: signs in with the token (or the cached session, or the
|
|
27
|
+
# browser), wires the project and starts the recorder in the background.
|
|
28
|
+
#
|
|
29
|
+
# Safe to re-run; re-running is also the upgrade path. To record another project
|
|
30
|
+
# later: cd there and run `bir setup` (no token needed, the sign-in is kept).
|
|
31
|
+
#
|
|
32
|
+
# Environment:
|
|
33
|
+
# BIR_SETUP_TOKEN one-time setup token from the console. Copied and removed
|
|
34
|
+
# from the environment as the first thing this script does;
|
|
35
|
+
# only the `bir setup` child ever sees it.
|
|
36
|
+
# BIR_AUTH_URL the API address. Needed only when this is the raw file from
|
|
37
|
+
# the repository — the copy the API serves has it filled in.
|
|
38
|
+
# BIR_VERSION runner version to install (default: the served one, else latest).
|
|
39
|
+
# BIR_PROJECT the project directory (default: the current one).
|
|
40
|
+
# BIR_NODE_VERSION private Node version (default 24.21.0). Any other version
|
|
41
|
+
# is verified against nodejs.org's SHASUMS256.txt instead of
|
|
42
|
+
# the embedded digests.
|
|
43
|
+
# BIR_NODE_ARCHIVE a node-v<V>-<os>-<arch>.tar.gz already on disk, used instead
|
|
44
|
+
# of the download (air-gapped machines). Still digest-checked.
|
|
45
|
+
# BIR_TARBALL a local @basein/runner .tgz instead of the registry (air-gapped).
|
|
46
|
+
# BIR_NO_CLAUDE=1 do not install Claude Code.
|
|
47
|
+
# BIR_DRY_RUN=1 print every action; download, install and write nothing.
|
|
48
|
+
# HTTPS_PROXY, HTTP_PROXY, NO_PROXY, NODE_EXTRA_CA_CERTS pass through to node and npm.
|
|
49
|
+
#
|
|
50
|
+
# Portability: POSIX sh only (dash, and macOS's bash 3.2 as /bin/sh). Everything
|
|
51
|
+
# happens inside main(), invoked on the last line, so a truncated download runs
|
|
52
|
+
# nothing. It never prompts: under `curl | sh`, stdin is the script itself.
|
|
53
|
+
#
|
|
54
|
+
# Canonical source: scripts/install.sh in the @basein/runner repository. The API
|
|
55
|
+
# serves a copy (runner-install/install.sh) with the address and version filled in.
|
|
56
|
+
|
|
57
|
+
set -eu
|
|
58
|
+
|
|
59
|
+
# ── output ───────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
step() { printf '%s==> %s%s\n' "$C_STEP" "$1" "$C_OFF"; }
|
|
62
|
+
note() { printf ' %s%s%s\n' "$C_NOTE" "$1" "$C_OFF"; }
|
|
63
|
+
warn() { printf ' %swarning: %s%s\n' "$C_WARN" "$1" "$C_OFF" >&2; }
|
|
64
|
+
die() { printf '%serror: %s%s\n' "$C_ERR" "$1" "$C_OFF" >&2; exit 1; }
|
|
65
|
+
dry() { note "dry-run: $*"; }
|
|
66
|
+
|
|
67
|
+
# ── small helpers ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
# ver_major "v22.15.1" → 22; ver_minor → 15. Anything unparseable counts as 0.
|
|
70
|
+
ver_major() {
|
|
71
|
+
_v=${1#v}; _v=${_v%%.*}
|
|
72
|
+
case "$_v" in ''|*[!0-9]*) _v=0 ;; esac
|
|
73
|
+
printf '%s\n' "$_v"
|
|
74
|
+
}
|
|
75
|
+
ver_minor() {
|
|
76
|
+
_v=${1#v}
|
|
77
|
+
case "$_v" in *.*) _v=${_v#*.}; _v=${_v%%.*} ;; *) _v=0 ;; esac
|
|
78
|
+
case "$_v" in ''|*[!0-9]*) _v=0 ;; esac
|
|
79
|
+
printf '%s\n' "$_v"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# True when a Node of this version understands --use-system-ca: 22.15+, 23.8+, 24+.
|
|
83
|
+
node_has_system_ca() {
|
|
84
|
+
_maj=$(ver_major "$1"); _min=$(ver_minor "$1")
|
|
85
|
+
if [ "$_maj" -ge 24 ]; then return 0; fi
|
|
86
|
+
if [ "$_maj" -eq 22 ] && [ "$_min" -ge 15 ]; then return 0; fi
|
|
87
|
+
if [ "$_maj" -eq 23 ] && [ "$_min" -ge 8 ]; then return 0; fi
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# True when a node binary lives where a version manager, a per-version package
|
|
92
|
+
# directory or a per-shell shim keeps it. The SessionStart hook pins the absolute
|
|
93
|
+
# path of the node it was installed with; one that moves on the next `nvm use`,
|
|
94
|
+
# `brew upgrade` or shell start would leave the hook pointing at nothing.
|
|
95
|
+
managed_node_path() {
|
|
96
|
+
case "$1" in
|
|
97
|
+
*fnm_multishells*|*/fnm/*|*/.fnm/*) return 0 ;;
|
|
98
|
+
*/.nvm/*|*/nvm/versions/*) return 0 ;;
|
|
99
|
+
*/.volta/*|*/volta/*) return 0 ;;
|
|
100
|
+
*/nodenv/*|*/.nodenv/*) return 0 ;;
|
|
101
|
+
*/asdf/*|*/.asdf/*) return 0 ;;
|
|
102
|
+
*/mise/*|*/.mise/*) return 0 ;;
|
|
103
|
+
*/scoop/*) return 0 ;;
|
|
104
|
+
*/Cellar/*|*/linuxbrew/*|/snap/*) return 0 ;;
|
|
105
|
+
esac
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# The two ways the script can download. Both are checked before anything
|
|
110
|
+
# depends on them; the person got here through one of them.
|
|
111
|
+
have_fetcher() { command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1; }
|
|
112
|
+
|
|
113
|
+
# fetch <url> → stdout. curl first, wget as the fallback.
|
|
114
|
+
fetch() {
|
|
115
|
+
if command -v curl >/dev/null 2>&1; then
|
|
116
|
+
curl -fsSL --proto '=https' "$1"
|
|
117
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
118
|
+
wget -qO- --https-only "$1"
|
|
119
|
+
else
|
|
120
|
+
die "neither curl nor wget is installed; install one and paste the line again."
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# sha256_of <file> → the hex digest, from whichever tool the machine has.
|
|
125
|
+
sha256_of() {
|
|
126
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
127
|
+
sha256sum "$1" | awk '{ print $1 }'
|
|
128
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
129
|
+
shasum -a 256 "$1" | awk '{ print $1 }'
|
|
130
|
+
else
|
|
131
|
+
die "neither sha256sum nor shasum is installed; the Node download cannot be verified."
|
|
132
|
+
fi
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# is_digest <string>: exactly 64 lowercase hex characters.
|
|
136
|
+
is_digest() {
|
|
137
|
+
[ ${#1} -eq 64 ] || return 1
|
|
138
|
+
case "$1" in *[!0-9a-f]*) return 1 ;; esac
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# pinned_digest <archive file name> → the digest embedded for the pinned Node
|
|
143
|
+
# version (nothing for any other file). Kept identical to install.ps1's table.
|
|
144
|
+
pinned_digest() {
|
|
145
|
+
case "$1" in
|
|
146
|
+
node-v24.21.0-win-x64.zip) printf '%s\n' 158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541 ;;
|
|
147
|
+
node-v24.21.0-win-arm64.zip) printf '%s\n' 8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921 ;;
|
|
148
|
+
node-v24.21.0-darwin-arm64.tar.gz) printf '%s\n' bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057 ;;
|
|
149
|
+
node-v24.21.0-darwin-x64.tar.gz) printf '%s\n' 1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097 ;;
|
|
150
|
+
node-v24.21.0-linux-x64.tar.gz) printf '%s\n' 6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff ;;
|
|
151
|
+
node-v24.21.0-linux-arm64.tar.gz) printf '%s\n' 724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5 ;;
|
|
152
|
+
esac
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# shasums_digest <version> <archive file name> → the digest from nodejs.org's
|
|
156
|
+
# SHASUMS256.txt for that version. The file name is matched whole, against the
|
|
157
|
+
# second column only; zero or several matches abort — a guess is worse than none.
|
|
158
|
+
shasums_digest() {
|
|
159
|
+
_url="https://nodejs.org/dist/v$1/SHASUMS256.txt"
|
|
160
|
+
_list=$(fetch "$_url") || die "could not download $_url"
|
|
161
|
+
_hits=$(printf '%s\n' "$_list" | awk -v f="$2" '$2 == f { print $1 }')
|
|
162
|
+
_n=$(printf '%s\n' "$_hits" | awk 'NF { n++ } END { print n + 0 }')
|
|
163
|
+
case "$_n" in
|
|
164
|
+
1) printf '%s\n' "$_hits" ;;
|
|
165
|
+
0) die "$_url has no entry for $2" ;;
|
|
166
|
+
*) die "$_url has $_n entries for $2; refusing to pick one" ;;
|
|
167
|
+
esac
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Sets plat and arch to the names nodejs.org uses in its archive names.
|
|
171
|
+
detect_platform() {
|
|
172
|
+
_os=$(uname -s 2>/dev/null || printf 'unknown')
|
|
173
|
+
_machine=$(uname -m 2>/dev/null || printf 'unknown')
|
|
174
|
+
case "$_os" in
|
|
175
|
+
Darwin)
|
|
176
|
+
plat=darwin
|
|
177
|
+
# A shell running under Rosetta reports x86_64; the machine, not the shell,
|
|
178
|
+
# decides which Node to fetch.
|
|
179
|
+
if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = "1" ]; then
|
|
180
|
+
arch=arm64
|
|
181
|
+
else
|
|
182
|
+
arch=x64
|
|
183
|
+
fi
|
|
184
|
+
;;
|
|
185
|
+
Linux)
|
|
186
|
+
plat=linux
|
|
187
|
+
case "$_machine" in
|
|
188
|
+
x86_64|amd64) arch=x64 ;;
|
|
189
|
+
aarch64|arm64) arch=arm64 ;;
|
|
190
|
+
*) die "nodejs.org has no Node build for Linux/$_machine. Install Node 20 or newer yourself and paste the line again." ;;
|
|
191
|
+
esac
|
|
192
|
+
;;
|
|
193
|
+
*)
|
|
194
|
+
die "$_os is not supported by this script. On Windows, use the PowerShell line from the console."
|
|
195
|
+
;;
|
|
196
|
+
esac
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Removes the work directory a private-Node install left behind, if any.
|
|
200
|
+
cleanup() {
|
|
201
|
+
if [ -n "${work:-}" ] && [ -d "$work" ]; then rm -rf "$work"; fi
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── the private Node ─────────────────────────────────────────────────────────
|
|
205
|
+
#
|
|
206
|
+
# Into $node_dir (versioned, so a recorder that is running keeps its files and
|
|
207
|
+
# nothing is ever extracted over a binary in use). Skipped when the binary is
|
|
208
|
+
# already there and answers with the right version. Download to a work
|
|
209
|
+
# directory, verify, extract there, then rename into place.
|
|
210
|
+
|
|
211
|
+
ensure_private_node() {
|
|
212
|
+
if [ -x "$node_dir/bin/node" ] && [ "$("$node_dir/bin/node" -v 2>/dev/null || true)" = "v$node_version" ]; then
|
|
213
|
+
note "private Node v$node_version is already in $node_dir"
|
|
214
|
+
return 0
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
detect_platform
|
|
218
|
+
archive="node-v$node_version-$plat-$arch.tar.gz"
|
|
219
|
+
url="https://nodejs.org/dist/v$node_version/$archive"
|
|
220
|
+
|
|
221
|
+
if [ "$node_version" = "$NODE_PINNED" ]; then
|
|
222
|
+
expected=$(pinned_digest "$archive")
|
|
223
|
+
[ -n "$expected" ] || die "no digest is embedded for $archive"
|
|
224
|
+
else
|
|
225
|
+
note "BIR_NODE_VERSION=$node_version is not the pinned $NODE_PINNED — its digest comes from nodejs.org's SHASUMS256.txt"
|
|
226
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
227
|
+
expected="<from https://nodejs.org/dist/v$node_version/SHASUMS256.txt>"
|
|
228
|
+
else
|
|
229
|
+
expected=$(shasums_digest "$node_version" "$archive")
|
|
230
|
+
is_digest "$expected" || die "the SHASUMS256.txt entry for $archive is not a sha256 digest: $expected"
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
235
|
+
if [ -n "${BIR_NODE_ARCHIVE:-}" ]; then
|
|
236
|
+
dry "use $BIR_NODE_ARCHIVE instead of downloading $url"
|
|
237
|
+
else
|
|
238
|
+
dry "download $url"
|
|
239
|
+
fi
|
|
240
|
+
dry "verify sha256 = $expected"
|
|
241
|
+
dry "tar -xzf $archive --strip-components=1 into $node_dir"
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
command -v tar >/dev/null 2>&1 || die "tar is not installed; it is needed to unpack Node."
|
|
246
|
+
|
|
247
|
+
mkdir -p "$bir_home/node"
|
|
248
|
+
work="$bir_home/node/.install-$$"
|
|
249
|
+
rm -rf "$work"
|
|
250
|
+
mkdir "$work"
|
|
251
|
+
trap cleanup EXIT
|
|
252
|
+
trap 'cleanup; exit 130' INT TERM HUP
|
|
253
|
+
|
|
254
|
+
if [ -n "${BIR_NODE_ARCHIVE:-}" ]; then
|
|
255
|
+
[ -f "$BIR_NODE_ARCHIVE" ] || die "BIR_NODE_ARCHIVE not found: $BIR_NODE_ARCHIVE"
|
|
256
|
+
note "using $BIR_NODE_ARCHIVE (BIR_NODE_ARCHIVE) instead of downloading"
|
|
257
|
+
cp "$BIR_NODE_ARCHIVE" "$work/$archive"
|
|
258
|
+
else
|
|
259
|
+
note "downloading $url"
|
|
260
|
+
have_fetcher || die "neither curl nor wget is installed; install one and paste the line again."
|
|
261
|
+
fetch "$url" > "$work/$archive" || die "download failed: $url"
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
got=$(sha256_of "$work/$archive")
|
|
265
|
+
if [ "$got" != "$expected" ]; then
|
|
266
|
+
rm -f "$work/$archive"
|
|
267
|
+
die "sha256 mismatch for $archive: expected $expected, got ${got:-nothing}. The file was deleted; paste the line again."
|
|
268
|
+
fi
|
|
269
|
+
note "sha256 verified"
|
|
270
|
+
|
|
271
|
+
mkdir "$work/node"
|
|
272
|
+
tar -xzf "$work/$archive" -C "$work/node" --strip-components=1
|
|
273
|
+
[ -x "$work/node/bin/node" ] || die "$archive did not contain bin/node"
|
|
274
|
+
# Run it before it is moved into place: a build that does not start here (an
|
|
275
|
+
# older macOS, a glibc older than 2.28, musl) must say so now, not as an
|
|
276
|
+
# opaque npm failure a step later.
|
|
277
|
+
if [ "$("$work/node/bin/node" -v 2>/dev/null || true)" != "v$node_version" ]; then
|
|
278
|
+
die "the Node $node_version build does not run on this machine (it needs macOS 13.5+ or glibc 2.28+). Install Node 20 or newer yourself and paste the line again."
|
|
279
|
+
fi
|
|
280
|
+
if [ -d "$node_dir" ]; then rm -rf "$node_dir"; fi
|
|
281
|
+
mv "$work/node" "$node_dir"
|
|
282
|
+
cleanup
|
|
283
|
+
note "Node v$node_version installed in $node_dir (not on PATH; only the runner uses it)"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# ── the profile line ─────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
# persist <file>: append the PATH line to that profile file once.
|
|
289
|
+
persist() {
|
|
290
|
+
if [ -f "$1" ] && grep -qxF "$path_line" "$1"; then
|
|
291
|
+
note "already in $1"
|
|
292
|
+
return 0
|
|
293
|
+
fi
|
|
294
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
295
|
+
dry "append to $1: $path_line"
|
|
296
|
+
return 0
|
|
297
|
+
fi
|
|
298
|
+
touch "$1"
|
|
299
|
+
# A file whose last line has no newline would swallow ours.
|
|
300
|
+
if [ -s "$1" ] && [ -n "$(tail -c 1 "$1")" ]; then printf '\n' >> "$1"; fi
|
|
301
|
+
printf '%s\n' "$path_line" >> "$1"
|
|
302
|
+
note "added to $1"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# write_launcher <name> <script.js>: ~/.baseinstrunner/bin/<name> that execs
|
|
306
|
+
# the chosen node on the installed runner's script.
|
|
307
|
+
write_launcher() {
|
|
308
|
+
_launcher="$bin_dir/$1"
|
|
309
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
310
|
+
dry "write $_launcher: exec \"$node_bin\" \"$runner_dir/dist/bin/$2\" \"\$@\""
|
|
311
|
+
return 0
|
|
312
|
+
fi
|
|
313
|
+
mkdir -p "$bin_dir"
|
|
314
|
+
# The launcher carries the node flag this run used (--use-system-ca when the
|
|
315
|
+
# Node knows it), so a `bir setup` typed later trusts the same certificates.
|
|
316
|
+
printf '#!/bin/sh\nexec "%s"%s "%s/dist/bin/%s" "$@"\n' "$node_bin" "${system_ca:+ --use-system-ca}" "$runner_dir" "$2" > "$_launcher.tmp"
|
|
317
|
+
chmod +x "$_launcher.tmp"
|
|
318
|
+
mv -f "$_launcher.tmp" "$_launcher"
|
|
319
|
+
note "$_launcher"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# find_claude → the path of `claude`, from PATH or the native installer's
|
|
323
|
+
# ~/.local/bin. Fails when there is none.
|
|
324
|
+
find_claude() {
|
|
325
|
+
if command -v claude >/dev/null 2>&1; then
|
|
326
|
+
command -v claude
|
|
327
|
+
return 0
|
|
328
|
+
fi
|
|
329
|
+
if [ -x "$HOME/.local/bin/claude" ]; then
|
|
330
|
+
printf '%s\n' "$HOME/.local/bin/claude"
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
return 1
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# ── main ─────────────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
main() {
|
|
339
|
+
# 0. The token. Copied and removed from the environment before anything else
|
|
340
|
+
# runs, so npm, the Claude Code installer and the `claude` started next
|
|
341
|
+
# never see it. It reaches `bir setup` through that child's environment only.
|
|
342
|
+
setup_token=${BIR_SETUP_TOKEN:-}
|
|
343
|
+
unset BIR_SETUP_TOKEN
|
|
344
|
+
|
|
345
|
+
PACKAGE='@basein/runner'
|
|
346
|
+
NODE_PINNED='24.21.0'
|
|
347
|
+
|
|
348
|
+
# The API fills these two in when it serves the file. Raw from the repository
|
|
349
|
+
# they are still placeholders, which count as unset.
|
|
350
|
+
served_url='__BIR_AUTH_URL__'
|
|
351
|
+
served_version='__BIR_VERSION__'
|
|
352
|
+
case "$served_url" in __BIR_*) served_url='' ;; esac
|
|
353
|
+
case "$served_version" in __BIR_*) served_version='' ;; esac
|
|
354
|
+
|
|
355
|
+
DRY_RUN=0
|
|
356
|
+
if [ "${BIR_DRY_RUN:-}" = 1 ]; then DRY_RUN=1; fi
|
|
357
|
+
|
|
358
|
+
if [ -t 1 ]; then
|
|
359
|
+
C_STEP=$(printf '\033[36m'); C_NOTE=$(printf '\033[90m'); C_WARN=$(printf '\033[33m')
|
|
360
|
+
C_ERR=$(printf '\033[31m'); C_OFF=$(printf '\033[0m')
|
|
361
|
+
else
|
|
362
|
+
C_STEP=''; C_NOTE=''; C_WARN=''; C_ERR=''; C_OFF=''
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
[ -n "${HOME:-}" ] || die "HOME is not set."
|
|
366
|
+
bir_home="$HOME/.baseinstrunner"
|
|
367
|
+
bin_dir="$bir_home/bin"
|
|
368
|
+
node_version=${BIR_NODE_VERSION:-$NODE_PINNED}
|
|
369
|
+
node_version=${node_version#v}
|
|
370
|
+
node_dir="$bir_home/node/v$node_version"
|
|
371
|
+
version=${BIR_VERSION:-${served_version:-latest}}
|
|
372
|
+
auth_url=${served_url:-${BIR_AUTH_URL:-}}
|
|
373
|
+
# shellcheck disable=SC2016
|
|
374
|
+
path_line='export PATH="$HOME/.baseinstrunner/bin:$PATH"'
|
|
375
|
+
|
|
376
|
+
# 00. The folder — before anything is downloaded. A terminal opened from the
|
|
377
|
+
# Dock or the applications menu starts in the home directory, and that is
|
|
378
|
+
# where a first paste tends to land.
|
|
379
|
+
step "Checking the folder"
|
|
380
|
+
# Files named relative to where the line was pasted stay valid after a cd.
|
|
381
|
+
case "${BIR_TARBALL:-}" in ''|/*) ;; *) BIR_TARBALL="$PWD/$BIR_TARBALL" ;; esac
|
|
382
|
+
case "${BIR_NODE_ARCHIVE:-}" in ''|/*) ;; *) BIR_NODE_ARCHIVE="$PWD/$BIR_NODE_ARCHIVE" ;; esac
|
|
383
|
+
if [ -n "${BIR_PROJECT:-}" ]; then
|
|
384
|
+
cd "$BIR_PROJECT" 2>/dev/null || die "BIR_PROJECT is not a directory: $BIR_PROJECT"
|
|
385
|
+
fi
|
|
386
|
+
project=$(pwd)
|
|
387
|
+
project_phys=$(pwd -P)
|
|
388
|
+
home_dir=$HOME
|
|
389
|
+
while [ "$home_dir" != "/" ] && [ "${home_dir%/}" != "$home_dir" ]; do home_dir=${home_dir%/}; done
|
|
390
|
+
home_phys=$(cd "$HOME" 2>/dev/null && pwd -P) || home_phys=$home_dir
|
|
391
|
+
what=''
|
|
392
|
+
if [ "$project" = "$home_dir" ] || [ "$project_phys" = "$home_phys" ]; then
|
|
393
|
+
what='your home folder'
|
|
394
|
+
elif [ "$project" = "/" ] || [ "$project_phys" = "/" ]; then
|
|
395
|
+
what='the root of the disk'
|
|
396
|
+
fi
|
|
397
|
+
if [ -n "$what" ]; then
|
|
398
|
+
printf '%s\n' "This is $what. Open a terminal in the project you start Claude Code in (cd there) and paste the line again." >&2
|
|
399
|
+
printf '%s\n' "(Or point the line at the project: BIR_PROJECT=/path/to/project.)" >&2
|
|
400
|
+
exit 1
|
|
401
|
+
fi
|
|
402
|
+
note "project: $project"
|
|
403
|
+
if [ "$DRY_RUN" = 1 ]; then note "dry run (BIR_DRY_RUN=1): nothing is downloaded, installed or written"; fi
|
|
404
|
+
if [ -z "$auth_url" ]; then
|
|
405
|
+
die "no service address. This is the raw script; set BIR_AUTH_URL=https://api.your-service and paste the line again (the copy the API serves has it filled in)."
|
|
406
|
+
fi
|
|
407
|
+
note "service: $auth_url"
|
|
408
|
+
if [ -n "$setup_token" ]; then
|
|
409
|
+
note "setup token: present (handed to bir setup only)"
|
|
410
|
+
else
|
|
411
|
+
note "no setup token: bir setup uses the cached sign-in, or opens the browser"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
# 1. Node.
|
|
415
|
+
step "Node"
|
|
416
|
+
node_bin=''
|
|
417
|
+
npm_cli=''
|
|
418
|
+
node_ver=''
|
|
419
|
+
if command -v node >/dev/null 2>&1; then
|
|
420
|
+
sys_ver=$(node -v 2>/dev/null || true)
|
|
421
|
+
if [ "$(ver_major "$sys_ver")" -ge 20 ]; then
|
|
422
|
+
# The shim (Volta, asdf, mise, …) is not the binary; the binary is where
|
|
423
|
+
# node_modules/npm lives and what the hook must point at.
|
|
424
|
+
sys_real=$(node -p process.execPath 2>/dev/null || true)
|
|
425
|
+
if [ -z "$sys_real" ] || [ ! -x "$sys_real" ]; then
|
|
426
|
+
note "node $sys_ver does not report its binary — using a private Node"
|
|
427
|
+
elif managed_node_path "$sys_real"; then
|
|
428
|
+
note "node $sys_ver lives in a per-version directory ($sys_real) that moves on the next upgrade or shell, and the recorder pins an absolute path — installing a private Node for it"
|
|
429
|
+
else
|
|
430
|
+
sys_root=$(cd "$(dirname "$sys_real")/.." 2>/dev/null && pwd) || sys_root=''
|
|
431
|
+
sys_npm="$sys_root/lib/node_modules/npm/bin/npm-cli.js"
|
|
432
|
+
if [ -n "$sys_root" ] && [ -f "$sys_npm" ]; then
|
|
433
|
+
node_bin=$sys_real
|
|
434
|
+
npm_cli=$sys_npm
|
|
435
|
+
node_ver=$sys_ver
|
|
436
|
+
note "using node $sys_ver at $sys_real"
|
|
437
|
+
else
|
|
438
|
+
note "node $sys_ver at $sys_real has no npm beside it — using a private Node"
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
else
|
|
442
|
+
note "node ${sys_ver:-(unknown version)} is older than 20 — using a private Node"
|
|
443
|
+
fi
|
|
444
|
+
else
|
|
445
|
+
note "node is not installed — using a private Node"
|
|
446
|
+
fi
|
|
447
|
+
if [ -z "$node_bin" ]; then
|
|
448
|
+
ensure_private_node
|
|
449
|
+
node_bin="$node_dir/bin/node"
|
|
450
|
+
npm_cli="$node_dir/lib/node_modules/npm/bin/npm-cli.js"
|
|
451
|
+
node_ver="v$node_version"
|
|
452
|
+
fi
|
|
453
|
+
# Node 22.15+ can trust the operating system's certificate store, which is
|
|
454
|
+
# where a company CA usually is. Older Nodes need NODE_EXTRA_CA_CERTS.
|
|
455
|
+
system_ca=''
|
|
456
|
+
if node_has_system_ca "$node_ver"; then system_ca=1; fi
|
|
457
|
+
|
|
458
|
+
# 2. The runner — one code path for both Nodes: always the private prefix,
|
|
459
|
+
# never the machine's (root-owned after a .pkg or apt install; per-version
|
|
460
|
+
# under nvm, Volta, asdf). Always npm-cli.js, never an `npm` shim.
|
|
461
|
+
step "Installing the runner"
|
|
462
|
+
prefix="$bir_home/npm"
|
|
463
|
+
if [ -n "${BIR_TARBALL:-}" ]; then
|
|
464
|
+
[ -f "$BIR_TARBALL" ] || die "BIR_TARBALL not found: $BIR_TARBALL"
|
|
465
|
+
spec=$BIR_TARBALL
|
|
466
|
+
note "from $BIR_TARBALL (BIR_TARBALL)"
|
|
467
|
+
else
|
|
468
|
+
spec="$PACKAGE@$version"
|
|
469
|
+
note "$spec from the npm registry"
|
|
470
|
+
fi
|
|
471
|
+
note "into $prefix (the machine's npm prefix is left alone; no sudo)"
|
|
472
|
+
runner_dir="$prefix/lib/node_modules/$PACKAGE"
|
|
473
|
+
runner_js="$runner_dir/dist/bin/bir.js"
|
|
474
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
475
|
+
dry "\"$node_bin\"${system_ca:+ --use-system-ca} \"$npm_cli\" install -g --prefix \"$prefix\" --no-fund --no-audit $spec"
|
|
476
|
+
else
|
|
477
|
+
mkdir -p "$prefix"
|
|
478
|
+
npm_log="$bir_home/npm-install.log"
|
|
479
|
+
if ! "$node_bin" ${system_ca:+--use-system-ca} "$npm_cli" install -g --prefix "$prefix" \
|
|
480
|
+
--no-fund --no-audit --loglevel=error "$spec" > "$npm_log" 2>&1; then
|
|
481
|
+
cat "$npm_log" >&2
|
|
482
|
+
if grep -qE 'CERT|ISSUER|SELF_SIGNED' "$npm_log"; then
|
|
483
|
+
printf '\n%s\n' "Your network inspects TLS. Set NODE_EXTRA_CA_CERTS to your company's CA bundle (ask IT) and run this again. Offline: BIR_TARBALL=<file.tgz> and BIR_NODE_ARCHIVE=<archive>." >&2
|
|
484
|
+
fi
|
|
485
|
+
die "npm could not install $spec (the log is in $npm_log)"
|
|
486
|
+
fi
|
|
487
|
+
rm -f "$npm_log"
|
|
488
|
+
[ -f "$runner_js" ] || die "$runner_js is missing after the install"
|
|
489
|
+
note "bir $("$node_bin" "$runner_js" --version 2>/dev/null || printf '(version unknown)') in $runner_dir"
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
step "Putting bir on PATH"
|
|
493
|
+
write_launcher bir bir.js
|
|
494
|
+
write_launcher bir-hooks bir-hooks.js
|
|
495
|
+
# Which profile files a terminal reads depends on the shell and the OS: zsh
|
|
496
|
+
# reads .zprofile at login and .zshrc for every interactive shell; bash reads
|
|
497
|
+
# the first of .bash_profile / .bash_login / .profile at login (creating
|
|
498
|
+
# .bash_profile would make it stop reading .profile), and on Linux a terminal
|
|
499
|
+
# window is not a login shell, so .bashrc as well.
|
|
500
|
+
shell_name=${SHELL:-}
|
|
501
|
+
shell_name=${shell_name##*/}
|
|
502
|
+
case "$shell_name" in
|
|
503
|
+
zsh)
|
|
504
|
+
persist "$HOME/.zprofile"
|
|
505
|
+
persist "$HOME/.zshrc"
|
|
506
|
+
;;
|
|
507
|
+
bash)
|
|
508
|
+
if [ -f "$HOME/.bash_profile" ]; then
|
|
509
|
+
persist "$HOME/.bash_profile"
|
|
510
|
+
elif [ -f "$HOME/.bash_login" ]; then
|
|
511
|
+
persist "$HOME/.bash_login"
|
|
512
|
+
else
|
|
513
|
+
persist "$HOME/.profile"
|
|
514
|
+
fi
|
|
515
|
+
if [ "$(uname -s 2>/dev/null || true)" = "Linux" ]; then persist "$HOME/.bashrc"; fi
|
|
516
|
+
;;
|
|
517
|
+
*)
|
|
518
|
+
persist "$HOME/.profile"
|
|
519
|
+
;;
|
|
520
|
+
esac
|
|
521
|
+
PATH="$bin_dir:$PATH"
|
|
522
|
+
export PATH
|
|
523
|
+
|
|
524
|
+
# 3. Claude Code.
|
|
525
|
+
step "Claude Code"
|
|
526
|
+
if [ "${BIR_NO_CLAUDE:-}" = 1 ]; then
|
|
527
|
+
note "skipped (BIR_NO_CLAUDE=1)"
|
|
528
|
+
elif claude_bin=$(find_claude); then
|
|
529
|
+
note "claude is installed ($claude_bin)"
|
|
530
|
+
else
|
|
531
|
+
note "installing Claude Code with its official installer — this can take a minute"
|
|
532
|
+
note "Claude Code needs a Claude subscription; it asks you to sign in the first time it starts"
|
|
533
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
534
|
+
dry "curl -fsSL https://claude.ai/install.sh | bash"
|
|
535
|
+
elif ! command -v bash >/dev/null 2>&1; then
|
|
536
|
+
warn "bash is not installed, and Claude Code's installer needs it. Install Claude Code by hand: https://docs.claude.com/en/docs/claude-code/setup"
|
|
537
|
+
elif ! have_fetcher; then
|
|
538
|
+
warn "neither curl nor wget is installed; install Claude Code by hand: https://docs.claude.com/en/docs/claude-code/setup"
|
|
539
|
+
else
|
|
540
|
+
if ! fetch https://claude.ai/install.sh | bash; then
|
|
541
|
+
warn "Claude Code's installer failed. Run it again later: curl -fsSL https://claude.ai/install.sh | bash"
|
|
542
|
+
elif claude_bin=$(find_claude); then
|
|
543
|
+
note "claude installed ($claude_bin)"
|
|
544
|
+
else
|
|
545
|
+
warn "Claude Code did not install. Run it again later: curl -fsSL https://claude.ai/install.sh | bash"
|
|
546
|
+
fi
|
|
547
|
+
fi
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
# 4. Setup: sign in, wire the project, start the recorder. The token goes into
|
|
551
|
+
# this child's environment and nowhere else.
|
|
552
|
+
step "Setting up the runner in $project"
|
|
553
|
+
setup_rc=0
|
|
554
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
555
|
+
token_note=''
|
|
556
|
+
if [ -n "$setup_token" ]; then token_note=' (with BIR_SETUP_TOKEN in its environment)'; fi
|
|
557
|
+
dry "\"$node_bin\"${system_ca:+ --use-system-ca} \"$runner_js\" setup --auth-url $auth_url --project \"$project\"$token_note"
|
|
558
|
+
elif [ -n "$setup_token" ]; then
|
|
559
|
+
BIR_SETUP_TOKEN=$setup_token "$node_bin" ${system_ca:+--use-system-ca} "$runner_js" setup \
|
|
560
|
+
--auth-url "$auth_url" --project "$project" || setup_rc=$?
|
|
561
|
+
else
|
|
562
|
+
"$node_bin" ${system_ca:+--use-system-ca} "$runner_js" setup \
|
|
563
|
+
--auth-url "$auth_url" --project "$project" || setup_rc=$?
|
|
564
|
+
fi
|
|
565
|
+
|
|
566
|
+
printf '\n'
|
|
567
|
+
if [ "$setup_rc" -ne 0 ]; then
|
|
568
|
+
step "bir setup exited with $setup_rc — the lines above say why. Everything it needs is installed."
|
|
569
|
+
else
|
|
570
|
+
step "Done"
|
|
571
|
+
fi
|
|
572
|
+
# A Claude Code this run installed lives in ~/.local/bin, which this terminal
|
|
573
|
+
# may not have on PATH either; the one line covers both.
|
|
574
|
+
if [ -x "$HOME/.local/bin/claude" ] && ! command -v claude >/dev/null 2>&1; then
|
|
575
|
+
# shellcheck disable=SC2016
|
|
576
|
+
path_line='export PATH="$HOME/.baseinstrunner/bin:$HOME/.local/bin:$PATH"'
|
|
577
|
+
fi
|
|
578
|
+
printf ' To use bir (and claude) in this terminal: %s (or open a new terminal)\n' "$path_line"
|
|
579
|
+
# shellcheck disable=SC2016
|
|
580
|
+
printf ' Check it any time: %s\n' '"$HOME/.baseinstrunner/bin/bir" doctor'
|
|
581
|
+
exit "$setup_rc"
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# ${1+"$@"} is "$@" on every shell, including the bash 3.2 macOS ships as
|
|
585
|
+
# /bin/sh under set -u with no arguments.
|
|
586
|
+
main ${1+"$@"}
|