Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 93 additions & 2 deletions scripts/install-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# without waiting for CI to publish a tagged release.
#
# Usage:
# bash scripts/install-local.sh [version-label]
# bash scripts/install-local.sh [version-label] [--ahead-ok] [--skip-smoke]
#
# Version label
# -------------
Expand Down Expand Up @@ -32,6 +32,19 @@
# "you're building something CI hasn't released, the DB is now ahead of brew"
# trap. Pass `--ahead-ok` to suppress the warning when intentional.
#
# Smoke-test (pre-install verification)
# -------------------------------------
# Before stomping the Cellar binary we exercise the freshly-built binary with
# two cheap commands:
# 1. `--version` — must exit 0 with a parseable version string
# 2. `debug info` — must exit 0 with a clean stderr (no TypeError /
# ReferenceError / SyntaxError / fatal-error / U.length)
# If either fails, the install aborts and the working brew binary stays put.
# Catches the class of regression where the build succeeds at the bundler
# layer but the produced binary crashes at module-load / plugin-resolution /
# config-read — surfaces caught 2026-05-27 (PR #14-17 era) that previously
# would have gone live silently. Pass `--skip-smoke` to override.
#
# Pairs with the standard release flow: tag a release for distribution, but
# install locally immediately so Nik isn't blocked on CI for his own bin.

Expand All @@ -41,12 +54,14 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# --- parse args --------------------------------------------------------------
AHEAD_OK=0
SKIP_SMOKE=0
EXPLICIT_LABEL=""
for arg in "$@"; do
case "$arg" in
--ahead-ok) AHEAD_OK=1 ;;
--skip-smoke) SKIP_SMOKE=1 ;;
-h|--help)
sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,50p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*) EXPLICIT_LABEL="$arg" ;;
Expand Down Expand Up @@ -128,6 +143,82 @@ if [ -z "$SRC_BIN" ] || [ ! -x "$SRC_BIN" ]; then
exit 1
fi

# --- smoke-test --------------------------------------------------------------
# Verify the new binary actually loads + executes a short command path before
# we stomp the working brew Cellar binary. Catches the class of regression
# where a JS-level crash (e.g. U.length on undefined) is silently produced by
# the build but only surfaces when a NEW tab tries to mount the TUI — by which
# time the old binary is already gone and Nik has no working gruntcode.
#
# Two layers, both cheap:
# 1. --version catches module-load + top-level import crashes
# 2. debug info catches plugin-resolution + config-load regressions
# (touches the same module graph the TUI mount does)
#
# Pass --skip-smoke for emergencies (rebuild loop on a known-broken state
# where you intentionally want the new binary installed despite a smoke fail).
if [ "$SKIP_SMOKE" -eq 1 ]; then
echo "→ Smoke-test SKIPPED (--skip-smoke)"
else
echo "→ Smoke-testing new binary at $SRC_BIN..."
SMOKE_LOG=$(mktemp -t gruntcode-smoke.XXXXXX)
SMOKE_FAILED=0

# `timeout` is a GNU coreutils binary; on macOS it's `gtimeout` (via `brew
# install coreutils`) or not present at all. Both --version and `debug info`
# exit in <1s on a healthy binary, so a hang here would indicate a real
# regression worth catching — but we only insist on the wrapper if it's
# available. On macOS without coreutils we run unwrapped + rely on the
# commands being fast.
if command -v timeout >/dev/null 2>&1; then
TIMEOUT_BIN="timeout 10"
elif command -v gtimeout >/dev/null 2>&1; then
TIMEOUT_BIN="gtimeout 10"
else
TIMEOUT_BIN=""
fi

# Layer 1: --version. Exits 0 immediately if module-load succeeds.
if ! $TIMEOUT_BIN "$SRC_BIN" --version >"$SMOKE_LOG" 2>&1; then
SMOKE_FAILED=1
echo " ✗ \`--version\` failed" >&2
else
SMOKE_VERSION=$(cat "$SMOKE_LOG")
echo " ✓ --version → $SMOKE_VERSION"
fi

# Layer 2: debug info. Exercises plugin-resolution + config load.
if [ "$SMOKE_FAILED" -eq 0 ]; then
if ! $TIMEOUT_BIN "$SRC_BIN" debug info >"$SMOKE_LOG" 2>&1; then
SMOKE_FAILED=1
echo " ✗ \`debug info\` failed" >&2
elif grep -qE '(TypeError|ReferenceError|SyntaxError|fatal error|Cannot read prop|U\.length)' "$SMOKE_LOG"; then
# Catch JS-level crash markers that may print on otherwise-zero-exit
# paths — e.g. an unhandled rejection in a plugin load can still produce
# exit 0 but with `TypeError` on stderr.
SMOKE_FAILED=1
echo " ✗ \`debug info\` exited 0 but stderr looks crashy:" >&2
sed 's/^/ /' "$SMOKE_LOG" >&2
else
echo " ✓ debug info clean"
fi
fi

if [ "$SMOKE_FAILED" -ne 0 ]; then
echo >&2
echo "✗ Smoke-test FAILED. Refusing to replace the Cellar binary." >&2
echo " Build output preserved at: $SRC_BIN" >&2
echo " Last smoke log:" >&2
sed 's/^/ /' "$SMOKE_LOG" >&2
echo >&2
echo " Rebuild after fixing the regression, or pass --skip-smoke to override." >&2
exit 1
fi

rm -f "$SMOKE_LOG"
echo " Smoke OK — proceeding to install."
fi

# --- install -----------------------------------------------------------------
CELLAR_BIN=$(find /opt/homebrew/Cellar/gruntcode -name gruntcode -type f 2>/dev/null | sort -r | head -1)
if [ -z "$CELLAR_BIN" ]; then
Expand Down
Loading