@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.
@@ -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+"$@"}