返回 CodeWhale
install.sh
根目录 / web / public / install.sh
1 #!/bin/sh
2 set -eu
3
4 repo="codewhale-hq/CodeWhale"
5 version="${CODEWHALE_VERSION:-latest}"
6 release_base="${CODEWHALE_RELEASE_BASE_URL:-${DEEPSEEK_TUI_RELEASE_BASE_URL:-}}"
7
8 usage() {
9 cat <<'USAGE'
10 Codewhale GitHub release installer for new macOS and Linux installations.
11 For an existing direct install, run its codewhale update command.
12
13 Usage:
14 curl -fsSL https://codewhale.net/install.sh | sh
15
16 Environment:
17 CODEWHALE_INSTALL_DIR Install directory. Default: $HOME/.local/bin
18 CODEWHALE_VERSION Release tag for a fresh directory. Default: latest
19 CODEWHALE_RELEASE_BASE_URL
20 Custom release asset base URL ending in /download
21 CODEWHALE_INSTALL_COMPILED_HOST=1
22 Also install a qualified optional Bun image + notices/source
23 Node remains the default; missing eligibility fails loudly
24 CODEWHALE_SKIP_GLIBC_CHECK=1
25 Skip Linux arm64 glibc compatibility preflight
26
27 Examples:
28 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR="$HOME/.local/codewhale/bin" sh
29 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=vX.Y.Z sh
30 USAGE
31 }
32
33 case "${1:-}" in
34 -h|--help)
35 usage
36 exit 0
37 ;;
38 esac
39
40 say() {
41 printf '%s\n' "$*"
42 }
43
44 fail() {
45 printf 'codewhale install: %s\n' "$*" >&2
46 exit 1
47 }
48
49 if [ -n "${CODEWHALE_INSTALL_DIR:-}" ]; then
50 install_dir="$CODEWHALE_INSTALL_DIR"
51 else
52 [ -n "${HOME:-}" ] || fail "HOME is not set; set CODEWHALE_INSTALL_DIR"
53 install_dir="$HOME/.local/bin"
54 fi
55
56 need_cmd() {
57 command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
58 }
59
60 download() {
61 url="$1"
62 out="$2"
63 if command -v curl >/dev/null 2>&1; then
64 curl -fsSL "$url" -o "$out"
65 elif command -v wget >/dev/null 2>&1; then
66 wget -q "$url" -O "$out"
67 else
68 fail "curl or wget is required"
69 fi
70 }
71
72 sha256_file() {
73 file="$1"
74 if command -v sha256sum >/dev/null 2>&1; then
75 sha256sum "$file" | awk '{print $1}'
76 elif command -v shasum >/dev/null 2>&1; then
77 shasum -a 256 "$file" | awk '{print $1}'
78 else
79 fail "sha256sum or shasum is required to verify downloads"
80 fi
81 }
82
83 verify_asset() {
84 asset="$1"
85 file="$2"
86 manifest="$3"
87 expected="$(
88 awk -v name="$asset" '
89 {
90 digest = tolower($1)
91 file = $2
92 sub(/^\*/, "", file)
93 if (file == name && digest ~ /^[0-9a-f]{64}$/) {
94 print digest
95 exit
96 }
97 }
98 ' "$manifest"
99 )"
100 [ -n "$expected" ] || fail "checksum not found for $asset"
101 actual="$(sha256_file "$file" | tr '[:upper:]' '[:lower:]')"
102 [ "$actual" = "$expected" ] || fail "checksum mismatch for $asset"
103 }
104
105 glibc_version() {
106 if command -v getconf >/dev/null 2>&1; then
107 getconf GNU_LIBC_VERSION 2>/dev/null | awk '{ print $NF; exit }'
108 return
109 fi
110 if command -v ldd >/dev/null 2>&1; then
111 ldd --version 2>/dev/null | awk 'NR == 1 {
112 for (i = 1; i <= NF; i++) {
113 if ($i ~ /^[0-9]+\.[0-9]+/) {
114 print $i
115 exit
116 }
117 }
118 }'
119 fi
120 }
121
122 version_at_least() {
123 have="$1"
124 need="$2"
125 awk -v have="$have" -v need="$need" '
126 BEGIN {
127 split(have, h, ".")
128 split(need, n, ".")
129 for (i = 1; i <= 3; i++) {
130 hv = h[i] + 0
131 nv = n[i] + 0
132 if (hv > nv) exit 0
133 if (hv < nv) exit 1
134 }
135 exit 0
136 }
137 '
138 }
139
140 check_glibc() {
141 case "$target" in
142 linux-arm64) ;;
143 *) return ;;
144 esac
145
146 # Linux arm64 assets became static musl builds in v0.9.6. `latest` and
147 # explicit v0.9.6+ installs therefore have no glibc floor. Keep the
148 # preflight only for explicitly requested older releases, whose arm64
149 # assets were linked against GNU libc on Ubuntu 24.04.
150 if [ "$version" = "latest" ]; then
151 return
152 fi
153 numeric_version="${version#v}"
154 if awk -v have="$numeric_version" '
155 BEGIN {
156 if (have !~ /^[0-9]+\.[0-9]+\.[0-9]+$/) exit 1
157 split(have, h, ".")
158 if (h[1] > 0) exit 0
159 if (h[1] < 0) exit 1
160 if (h[2] > 9) exit 0
161 if (h[2] < 9) exit 1
162 exit !(h[3] >= 6)
163 }
164 '; then
165 return
166 fi
167
168 [ "${CODEWHALE_SKIP_GLIBC_CHECK:-}" = "1" ] && return
169 [ "${DEEPSEEK_TUI_SKIP_GLIBC_CHECK:-}" = "1" ] && return
170 [ "${DEEPSEEK_SKIP_GLIBC_CHECK:-}" = "1" ] && return
171
172 required="2.39"
173 host="$(glibc_version || true)"
174 if [ -z "$host" ] || ! version_at_least "$host" "$required"; then
175 cat >&2 <<EOF
176 codewhale install: Codewhale $version $target assets require glibc $required or newer.
177 This system reports glibc ${host:-unavailable}.
178
179 Linux arm64 assets before v0.9.6 were GNU libc builds from Ubuntu 24.04.
180 Current v0.9.6+ assets are static musl builds. Build this older release from
181 source with Cargo or set
182 CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.
183 EOF
184 exit 1
185 fi
186 }
187
188 detect_platform() {
189 os="$(uname -s)"
190 arch="$(uname -m)"
191
192 if [ -n "${TERMUX_VERSION:-}" ] || [ "$(uname -o 2>/dev/null || true)" = "Android" ]; then
193 fail "Android/Termux needs the Android arm64 preview archive, not a Linux binary. See https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md"
194 fi
195
196 case "$os" in
197 Darwin) platform="macos" ;;
198 Linux) platform="linux" ;;
199 *) fail "unsupported OS: $os. Use the matching asset at https://github.com/codewhale-hq/CodeWhale/releases/latest; npm and Cargo are secondary options." ;;
200 esac
201
202 case "$arch" in
203 x86_64|amd64) cpu="x64" ;;
204 arm64|aarch64) cpu="arm64" ;;
205 riscv64) fail "Linux riscv64 prebuilt assets are temporarily unavailable because the locked rquickjs-sys dependency does not ship riscv64gc bindings." ;;
206 *) fail "unsupported CPU architecture: $arch. Use Cargo or build from source." ;;
207 esac
208
209 printf '%s-%s' "$platform" "$cpu"
210 }
211
212 if [ -z "$release_base" ]; then
213 if [ "$version" = "latest" ]; then
214 release_base="https://github.com/$repo/releases/latest/download"
215 else
216 release_base="https://github.com/$repo/releases/download/$version"
217 fi
218 fi
219
220 target="$(detect_platform)"
221 check_glibc
222 cli_asset="codewhale-$target"
223 shim_asset="codew-$target"
224 manifest_asset="codewhale-artifacts-sha256.txt"
225
226 tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t codewhale-install)"
227 trap 'rm -rf "$tmpdir"' EXIT INT TERM
228
229 say "Installing Codewhale for $target"
230 say "Release assets: $release_base"
231 say "Install dir: $install_dir"
232
233 download "$release_base/$manifest_asset" "$tmpdir/$manifest_asset"
234 download "$release_base/$cli_asset" "$tmpdir/codewhale"
235 download "$release_base/$shim_asset" "$tmpdir/codew"
236
237 verify_asset "$cli_asset" "$tmpdir/codewhale" "$tmpdir/$manifest_asset"
238 verify_asset "$shim_asset" "$tmpdir/codew" "$tmpdir/$manifest_asset"
239 say "Checksums verified"
240
241 chmod 755 "$tmpdir/codewhale" "$tmpdir/codew"
242 if command -v xattr >/dev/null 2>&1; then
243 xattr -d com.apple.quarantine "$tmpdir/codewhale" "$tmpdir/codew" 2>/dev/null || true
244 fi
245
246 # Resolve the real directory before applying managed-prefix checks. Never use
247 # sudo or allow an install directory symlink to obscure which files will change.
248 case "$install_dir" in
249 /*) ;;
250 *) fail "CODEWHALE_INSTALL_DIR must be an absolute path" ;;
251 esac
252 [ ! -L "$install_dir" ] || fail "install directory is a symlink: $install_dir; choose a fresh user directory"
253 mkdir -p "$install_dir" || fail "cannot create $install_dir; choose a writable user directory (no sudo is used)"
254 install_dir="$(cd -P "$install_dir" && pwd)"
255 case "$install_dir/" in
256 /bin/*|/sbin/*|/usr/bin/*|/usr/sbin/*|/nix/store/*|/gnu/store/*|*/node_modules/*|*/Cellar/*|*/.linuxbrew/*|*/linuxbrew/*|*/.cargo/bin/*)
257 fail "refusing managed/system directory $install_dir; use a fresh user directory"
258 ;;
259 esac
260 [ -w "$install_dir" ] || fail "$install_dir is not writable; choose a user directory (no sudo is used)"
261
262 check_destination() {
263 destination="$1"
264 source="$2"
265 mode="${3:-755}"
266 destination_exists=0
267 if [ -e "$destination" ] || [ -L "$destination" ]; then
268 if [ ! -L "$destination" ] && [ -f "$destination" ] && { [ "$mode" != 755 ] || [ -x "$destination" ]; } && cmp -s "$source" "$destination"; then
269 destination_exists=1
270 return
271 fi
272 cat >&2 <<EOF
273 codewhale install: refusing to replace existing $destination.
274 It may be a newer build, another installation, or a symlink. No existing file was changed.
275 For an existing direct Codewhale install, run its full path with 'update'.
276 To migrate safely from a package manager or mixed installation, create a fresh directory:
277 mkdir -p "\$HOME/.local"
278 codewhale_install_dir="\$(mktemp -d "\$HOME/.local/codewhale-release.XXXXXX")"
279 curl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR="\$codewhale_install_dir" sh
280 "\$codewhale_install_dir/codewhale" --version
281 export PATH="\$codewhale_install_dir:\$PATH"
282 hash -r
283 command -v codewhale codew
284 EOF
285 exit 1
286 fi
287 }
288
289 # Check every command before publishing any of them. Existing identical release
290 # files are an idempotent install; anything different uses the canonical updater.
291 # An enabled companion must exist in this same checksummed release. No
292 # installer fetches a Bun runtime or invents Android/musl/cross-arch support.
293 if [ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" = 1 ]; then
294 host_asset="codewhale-extension-host-$target"
295 for companion in "$host_asset" "$host_asset-LICENSES.txt" "$host_asset-relink-source.tar.gz" codewhale-extension-hosts.json; do
296 download "$release_base/$companion" "$tmpdir/$companion"
297 verify_asset "$companion" "$tmpdir/$companion" "$tmpdir/codewhale-artifacts-sha256.txt"
298 done
299 mv "$tmpdir/$host_asset" "$tmpdir/codewhale-extension-host"
300 mv "$tmpdir/$host_asset-LICENSES.txt" "$tmpdir/codewhale-extension-host.LICENSES.txt"
301 mv "$tmpdir/$host_asset-relink-source.tar.gz" "$tmpdir/codewhale-extension-host.relink-source.tar.gz"
302 mv "$tmpdir/codewhale-extension-hosts.json" "$tmpdir/codewhale-extension-host.release.json"
303 if [ "$target" = linux-x64 ] || [ "$target" = linux-arm64 ]; then
304 required="$(grep -aoE 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' "$tmpdir/codewhale-extension-host" 2>/dev/null | sed 's/GLIBC_//' | awk -F. '{ code=$1*1000000+$2*1000+$3; if(code>best){best=code; value=$0} } END {print value}' || true)"
305 if [ -n "$required" ]; then
306 available="$(glibc_version || true)"
307 [ -n "$available" ] && version_at_least "$available" "$required" || fail "optional Bun host requires GLIBC_$required; CLI remains static musl. Use Node on this installation."
308 fi
309 fi
310 for companion in codewhale-extension-host codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
311 mode=644
312 [ "$companion" != codewhale-extension-host ] || mode=755
313 check_destination "$install_dir/$companion" "$tmpdir/$companion" "$mode"
314 done
315 fi
316
317 check_destination "$install_dir/codewhale" "$tmpdir/codewhale"
318 check_destination "$install_dir/codew" "$tmpdir/codew"
319 legacy_tui="$install_dir/codewhale-tui"
320 if [ -e "$legacy_tui" ] || [ -L "$legacy_tui" ]; then
321 check_destination "$legacy_tui" "$tmpdir/codewhale"
322 fi
323
324 stage=""
325 stage_dir=""
326 # Commands this run published. If a later publication fails they are removed
327 # again, but only while each is still the exact file this run wrote, so a
328 # failed install leaves no half-installed pair and never touches a file that
329 # was already installed.
330 published="$tmpdir/.published"
331 : > "$published"
332 rollback_published() {
333 while IFS= read -r name; do
334 destination="$install_dir/$name"
335 if [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$tmpdir/$name" "$destination"; then
336 rm -f "$destination"
337 say "Removed $destination: this install did not complete." >&2
338 fi
339 done < "$published"
340 }
341 on_exit() {
342 status=$?
343 if [ -n "$stage" ]; then rm -f "$stage"; fi
344 if [ -n "$stage_dir" ]; then rmdir "$stage_dir"; fi
345 if [ "$status" -ne 0 ]; then rollback_published; fi
346 rm -rf "$tmpdir"
347 }
348 trap on_exit EXIT
349 trap 'exit 130' INT
350 trap 'exit 143' TERM
351 install_binary() {
352 source="$1"
353 destination="$2"
354 # Recheck immediately before publication. Never replace a file another
355 # process created since preflight: linking the staged inode is no-clobber.
356 check_destination "$destination" "$source" "${3:-755}"
357 if [ "$destination_exists" -eq 1 ]; then
358 say "Already installed: $destination"
359 return
360 fi
361 stage_dir="$(mktemp -d "$install_dir/.codewhale-install.XXXXXX")"
362 stage="$stage_dir/$(basename "$destination")"
363 cp "$source" "$stage"
364 chmod "${3:-755}" "$stage"
365 # Pass the intended parent as the directory operand. Passing destination
366 # itself would make ln treat a raced-in directory/symlink as a container.
367 ln "$stage" "$install_dir/" || fail "destination appeared during install: $destination; it was not replaced"
368 [ ! -L "$destination" ] && [ -f "$destination" ] && cmp -s "$stage" "$destination" || fail "installed path changed during publication: $destination"
369 basename "$destination" >> "$published"
370 rm -f "$stage"
371 rmdir "$stage_dir"
372 stage=""
373 stage_dir=""
374 }
375
376 install_binary "$tmpdir/codewhale" "$install_dir/codewhale"
377 install_binary "$tmpdir/codew" "$install_dir/codew"
378 if [ "${CODEWHALE_INSTALL_COMPILED_HOST:-}" = 1 ]; then
379 install_binary "$tmpdir/codewhale-extension-host" "$install_dir/codewhale-extension-host"
380 for companion in codewhale-extension-host.LICENSES.txt codewhale-extension-host.relink-source.tar.gz codewhale-extension-host.release.json; do
381 install_binary "$tmpdir/$companion" "$install_dir/$companion" 644
382 done
383 say "Installed qualified opt-in image and its notice/relink-source closure; Node remains default."
384 fi
385
386 say "Installed checksummed release commands:"
387 say " $install_dir/codewhale"
388 say " $install_dir/codew"
389
390 say ""
391 say "Use this installation: \"$install_dir/codewhale\""
392 say "Future updates: \"$install_dir/codewhale\" update"
393 path_selected=1
394 for command_name in codewhale codew; do
395 resolved="$(command -v "$command_name" 2>/dev/null || true)"
396 if [ "$resolved" != "$install_dir/$command_name" ]; then
397 say "PATH selects ${resolved:-no $command_name command}; this install is $install_dir/$command_name"
398 path_selected=0
399 fi
400 done
401 if [ "$path_selected" -eq 0 ]; then
402 # Print the persistent line for the user's login shell. The installer never
403 # edits shell profiles itself; the user runs the line once.
404 path_dir="$install_dir"
405 if [ -n "${HOME:-}" ] && [ "$install_dir" = "$(cd -P "$HOME/.local/bin" 2>/dev/null && pwd)" ]; then
406 path_dir="\$HOME/.local/bin"
407 fi
408 shell_name="${SHELL:-}"
409 shell_name="${shell_name##*/}"
410 say ""
411 case "$path_dir" in
412 *[\'\"\`\\\$]*|*"
413 "*)
414 # Only the literal $HOME form may carry a shell-special character. Any
415 # other one would break the printed quoting, or run as a command on
416 # every shell start once the line is in a profile.
417 if [ "$path_dir" != "\$HOME/.local/bin" ]; then
418 shell_name="unsafe-path"
419 fi
420 ;;
421 esac
422 case "$shell_name" in
423 fish)
424 say "Put $install_dir first on PATH in future shells (run once; this installer does not edit shell profiles):"
425 say " fish_add_path \"$path_dir\""
426 say "It takes effect in this fish shell and in new ones (fish 3.2 or newer)."
427 ;;
428 zsh|bash|sh|dash|ksh|mksh|ash|"")
429 case "$shell_name" in
430 zsh) profile=".zshrc" ;;
431 bash)
432 case "$target" in
433 # Login bash reads the first of these that exists; creating
434 # ~/.bash_profile would stop an existing ~/.profile from loading.
435 macos-*)
436 profile=".bash_profile"
437 for candidate in .bash_profile .bash_login .profile; do
438 if [ -n "${HOME:-}" ] && [ -e "$HOME/$candidate" ]; then
439 profile="$candidate"
440 break
441 fi
442 done
443 ;;
444 *) profile=".bashrc" ;;
445 esac
446 ;;
447 *) profile=".profile" ;;
448 esac
449 say "Put $install_dir first on PATH in future shells (run once; this installer does not edit shell profiles):"
450 say " echo 'export PATH=\"$path_dir:\$PATH\"' >> ~/$profile"
451 say "Then run: . ~/$profile (or open a new terminal)"
452 say "Or for this shell only:"
453 say " export PATH=\"$path_dir:\$PATH\"; hash -r"
454 ;;
455 unsafe-path)
456 say "Add $install_dir first to PATH in your shell's startup file; its name contains shell-special characters, so no command line is printed for it."
457 ;;
458 *)
459 say "Add $install_dir first to PATH in your shell's startup file; this installer has no PATH line for $shell_name."
460 ;;
461 esac
462 say "Verify: command -v codewhale codew"
463 say "PATH help: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md#put-it-on-your-path"
464 fi
465 if ! command -v node >/dev/null 2>&1; then
466 say "Computer Use is included and needs Node.js 20 or newer on PATH."
467 say "Install Node.js from https://nodejs.org/, then restart Codewhale to enable Computer Use."
468 fi
469
469 lines BASH