From 28211a78eaa6f089ea47f1657d7e13f2089c07f8 Mon Sep 17 00:00:00 2001 From: bmottin Date: Sun, 13 Sep 2026 17:21:23 -0400 Subject: [PATCH] =?UTF-8?q?fix(install):=20unblock=20the=20installer=20on?= =?UTF-8?q?=20macOS=20=E2=80=94=20BSD=20sed,=20and=20a=20deadline=20on=20C?= =?UTF-8?q?hromium?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- install-plugins.sh | 98 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/install-plugins.sh b/install-plugins.sh index 4610ef4..d1e560e 100644 --- a/install-plugins.sh +++ b/install-plugins.sh @@ -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