fix(install): unblock the installer on macOS — BSD sed, and a deadline on Chromium
Two independent failures, both of which stopped `make plugin` outright here.
GNU `sed -i` takes no suffix argument; BSD sed requires one and reads the
script as that suffix, so all three calls errored — and under `set -euo
pipefail` that aborts the installer. They go through a temp file now, written
back with `cat >` so the profile keeps its mode and owner.
While rewriting them: the orphan-comment cleanup `{N; /^\n$/d;}` could never
match on ANY platform — after N the pattern space starts with '#', so the
^\n$ anchor pair never applied. It was a silent no-op, now awk, and tested on
/bin/bash 3.2: both obsolete entries and their stranded headers go, the live
entry and the user's own lines stay, idempotent on re-run.
The second failure was worse because it was invisible: gstack's ./setup runs
`bunx playwright install chromium` unbounded, and Playwright 1.58.2 deadlocks
on Node 26 mid-extraction — both processes idle at 39/333 files, no error, no
progress, forever. That silently cut the 2026-09-13 run at step 2 of 10.
Chromium is now installed here instead, under a 900s deadline; on timeout the
installer bumps gstack's Playwright (its package.json declares "^1.x", so a
minor bump is in range) and retries once, then warns rather than aborts —
gstack is OFF by default and only its browser depends on this.
`timeout` is not in a stock macOS, so the deadline is pure bash. Proven by a
forced hang: rc 124 in 6s, no orphaned oopDownloadBrowserMain, stderr clean.
This commit is contained in:
+93
-5
@@ -16,6 +16,16 @@ err() { echo -e "${RED}✗${NC} $1"; }
|
||||
|
||||
REPO="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# In-place file edit that works on both GNU and BSD sed: `sed -i` needs a backup
|
||||
# suffix argument on macOS and forbids one on Linux, so go through a temp file
|
||||
# instead. Writing back with `cat >` (not `mv`) keeps the original mode/owner.
|
||||
_sed_inplace() {
|
||||
local expr="$1" file="$2" tmp
|
||||
tmp="$(mktemp)" || return 1
|
||||
sed "$expr" "$file" > "$tmp" && cat "$tmp" > "$file"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# Log to file for post-mortem debugging (terminal output unchanged)
|
||||
LOG_FILE="$REPO/install-$(date +%Y%m%d-%H%M%S).log"
|
||||
if touch "$LOG_FILE" 2>/dev/null; then
|
||||
@@ -291,6 +301,67 @@ fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Portable `timeout`: GNU coreutils' timeout is absent from a stock macOS, and
|
||||
# this has to run on a machine where nothing is installed yet. Returns 124 when
|
||||
# the deadline is hit, otherwise the command's own exit status.
|
||||
_run_with_timeout() {
|
||||
local secs="$1" waited=0 pid
|
||||
shift
|
||||
"$@" &
|
||||
pid=$!
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
if [ "$waited" -ge "$secs" ]; then
|
||||
# Park the shell's own stderr: bash announces a signal-killed job on ITS
|
||||
# stderr, so redirecting kill/wait alone does not suppress it — and that
|
||||
# line in the install log reads like a real error.
|
||||
exec 3>&2 2>/dev/null
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
# Playwright downloads in a child process that survives a TERM aimed at
|
||||
# its parent; left alone it keeps holding the browser-cache lock.
|
||||
pkill -f oopDownloadBrowserMain 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
exec 2>&3 3>&-
|
||||
return 124
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
# Populate gstack's node_modules at the locked versions so `bunx playwright`
|
||||
# resolves the LOCAL Playwright. Without it bunx pulls the latest from npm,
|
||||
# which wants a different browser revision than the one gstack imports.
|
||||
_gstack_bun_install() {
|
||||
( cd "$GSTACK_DIR" && { bun install --frozen-lockfile >/dev/null 2>&1 ||
|
||||
bun install >/dev/null 2>&1; } )
|
||||
}
|
||||
|
||||
_gstack_pw_install() { ( cd "$GSTACK_DIR" && bunx playwright install chromium ); }
|
||||
|
||||
# Install gstack's Chromium here instead of leaving it to ./setup, so the
|
||||
# download can be bounded. A Playwright older than the running Node deadlocks
|
||||
# mid-extraction — both processes idle, no error, no progress, forever — which
|
||||
# silently stalls the whole installer. On a timeout, bump Playwright (gstack
|
||||
# declares "playwright": "^1.x", so a minor bump is within its own range) and
|
||||
# retry once. A second failure warns rather than aborts: gstack is OFF by
|
||||
# default and only its browser (/browse, /qa, screenshots) depends on this.
|
||||
gstack_install_browser_guarded() {
|
||||
[ -d "$GSTACK_DIR" ] && command -v bun >/dev/null 2>&1 || return 0
|
||||
_gstack_bun_install || return 0
|
||||
if _run_with_timeout "$GSTACK_BROWSER_TIMEOUT" _gstack_pw_install; then
|
||||
ok "gstack Chromium ready"
|
||||
return 0
|
||||
fi
|
||||
warn "Chromium install stalled >${GSTACK_BROWSER_TIMEOUT}s — bumping gstack's Playwright, retrying"
|
||||
( cd "$GSTACK_DIR" && bun add playwright@latest >/dev/null 2>&1 ) || true
|
||||
if _run_with_timeout "$GSTACK_BROWSER_TIMEOUT" _gstack_pw_install; then
|
||||
ok "gstack Chromium ready after Playwright bump (./setup rebuilds browse)"
|
||||
else
|
||||
warn "gstack Chromium unavailable — /browse, /qa and screenshots stay off"
|
||||
fi
|
||||
}
|
||||
|
||||
# gstack pins Playwright (1.58.x) which only ships browser builds for
|
||||
# ubuntu<=24.04. On a newer distro the browser install fails ("does not
|
||||
# support chromium on ubuntuXX.04"). Bump gstack's Playwright to a version
|
||||
@@ -307,7 +378,7 @@ gstack_bump_playwright_if_unsupported() {
|
||||
[ -n "$ostag" ] || return 0 # only the known Ubuntu case
|
||||
pwlib="$GSTACK_DIR/node_modules/playwright-core/lib"
|
||||
# populate node_modules at the pinned version so we can read its support list
|
||||
( cd "$GSTACK_DIR" && { bun install --frozen-lockfile >/dev/null 2>&1 || bun install >/dev/null 2>&1; } ) || return 0
|
||||
_gstack_bun_install || return 0
|
||||
if grep -rqs "$ostag" "$pwlib" 2>/dev/null; then
|
||||
return 0 # pinned Playwright already supports this OS
|
||||
fi
|
||||
@@ -337,6 +408,10 @@ echo ""
|
||||
# git add skills-external/gstack && git commit -m "chore: update gstack"
|
||||
|
||||
GSTACK_DIR="$REPO/skills-external/gstack"
|
||||
# Deadline for gstack's Chromium download+extract. Generous on purpose: a real
|
||||
# install runs 1-3 min, overshooting only delays the fallback, and the failure
|
||||
# it catches would otherwise hang the installer forever.
|
||||
GSTACK_BROWSER_TIMEOUT=900
|
||||
|
||||
if [ ! -d "$GSTACK_DIR/.git" ] && [ ! -f "$GSTACK_DIR/.git" ]; then
|
||||
info "Initializing GStack submodule..."
|
||||
@@ -369,6 +444,10 @@ if [ -d "$GSTACK_DIR" ]; then
|
||||
# chromium" fail). Non-fatal if it can't — gstack is OFF by default.
|
||||
gstack_bump_playwright_if_unsupported
|
||||
|
||||
# Then fetch the browser under a deadline. ./setup would do it itself, but
|
||||
# unbounded — and this is the step that hangs when Playwright trails Node.
|
||||
gstack_install_browser_guarded
|
||||
|
||||
info "Running GStack setup..."
|
||||
_gstack_setup_ok=0
|
||||
if [ -x "$GSTACK_DIR/setup" ]; then
|
||||
@@ -993,14 +1072,23 @@ fi
|
||||
# `claude --effort max` alias (the alias would even override settings.json).
|
||||
EFFORT_CLEANED=0
|
||||
if grep -qF 'export CLAUDE_EFFORT=max' "$SHELL_PROFILE" 2>/dev/null; then
|
||||
sed -i '/export CLAUDE_EFFORT=max/d' "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
||||
_sed_inplace '/export CLAUDE_EFFORT=max/d' "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
||||
fi
|
||||
if grep -qF "alias claude='claude --effort max'" "$SHELL_PROFILE" 2>/dev/null; then
|
||||
sed -i "\#alias claude='claude --effort max'#d" "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
||||
_sed_inplace "\#alias claude='claude --effort max'#d" "$SHELL_PROFILE"; EFFORT_CLEANED=1
|
||||
fi
|
||||
if [ "$EFFORT_CLEANED" -eq 1 ]; then
|
||||
# Remove orphaned comment lines left before the deleted entries
|
||||
sed -i '/^# Claude Code — added by install-plugins.sh$/{ N; /^\n$/d; }' "$SHELL_PROFILE"
|
||||
# Drop the header comment left stranded above a deleted entry (the marker
|
||||
# followed by a blank line, or by end-of-file). awk, not sed: the previous
|
||||
# `{N; /^\n$/d;}` could never match — after N the pattern space starts with
|
||||
# '#', so the ^\n$ anchor pair never applied, and it was a silent no-op.
|
||||
_tmp_profile="$(mktemp)"
|
||||
awk -v marker='# Claude Code — added by install-plugins.sh' '
|
||||
$0 == marker { stranded = 1; next }
|
||||
stranded { stranded = 0; if ($0 == "") next; print marker }
|
||||
{ print }
|
||||
' "$SHELL_PROFILE" > "$_tmp_profile" && cat "$_tmp_profile" > "$SHELL_PROFILE"
|
||||
rm -f "$_tmp_profile"
|
||||
info "Removed obsolete effort alias/env from $SHELL_PROFILE (effort set in settings.json)"
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user