Files
gstack/bin/gstack-capy-install
T
Garry Tanandcapy-ai[bot] 83f15043ac v1.91.110.0 fix(capy): install the project's pinned Node; launch Bun through a bun-only directory (#3233)
* fix(capy): install and launch the project's pinned Node; launch Bun through a bun-only directory

gbrain pins node 24.18.0 in CI while the Capy image ships 24.21.0 in
/usr/local/bin, so a full install still ended with fail pins (GBRA-117).
The installer now resolves the project's exact Node pins the way it resolves
Bun's (required non-CI pins, CI pins in different jobs as alternatives): the
node first on PATH, then gstack's store copy and nvm's copy of each wanted
version, then any other node on PATH. With no match, install mode downloads the
pinned release into <state root>/runtimes/node and checks it against
SHASUMS256.txt. The resolution is recorded in the install verdict
(node_path, node_version, node_rule), and --check reuses it.

The launch PATH no longer prepends a shared bin directory. When the resolved
Bun was /usr/local/bin/bun, the image's node there shadowed every other node
(GBRA-122). A store Bun still launches from its own version directory; any
other Bun, and a Node the launch needs, launch through a gstack-owned
directory of symlinks beside the verdict. The pregate's lanes use the
recorded launch directory.

The doctor's pins row names the node it read and, for a Node mismatch, the
installer command that installs the pinned Node (or nvm install <v> off
Capy).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: v1.91.109.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(egress): exempt the Node installer's two release downloads with their reason

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore: restamp v1.91.110.0 (1.91.109.0 is #3225's slot)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(doctor): make the Node pin case platform-neutral (an any-lane CI pin, bash's spelling of the stub path)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-17 00:41:41 +00:00

554 lines
36 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-capy-install — install gstack on a Capy cloud machine (Ubuntu) and
# certify it with one grep-able line.
#
# Usage:
# gstack-capy-install [--for <skill> | --only <skill> | --require <id,id>] [--with-browser]
# [--revision <ref>] [--project <path>] [--root <dir>]
# [--check] [--min-revision <v>|unknown]
#
# Steps: clone or reuse the checkout under a lock (never reset, never pulled
# here; the Capy skill bootstrapper fast-forwards below-minimum checkouts and
# hands its held lock over with GSTACK_CAPY_LOCK_HELD=1); preflight git, curl,
# node, jq and Bun through gstack_bun_resolve (an installed Bun that satisfies
# gstack's minimum and the project's pins is reused; otherwise the exact
# release archive, SHA-256 verified, is published in the runtime store
# <state root>/runtimes/bun/<version>/bun, GSTACK_BUN_INSTALL_VERSION
# overriding the default); ./setup
# --host claude --no-prefix; ./setup --host codex --model gpt-6-astra (both
# skipped when this HEAD was already set up and the check passes);
# gstack-doctor --check with the launch PATH; the install verdict
# (bin/gstack-install-verdict.sh), keyed by checkout and project and holding
# the resolved Bun. --check certifies only: no clone, no Bun install, no
# setup. On a Capy machine (CAPY_MACHINE_CONFIG or ~/.capy/system) install also writes the marked
# per-directory shims ~/.local/bin/bun and ~/.local/bin/bunx, which run the Bun
# recorded for the project around the working directory in any fresh shell;
# GSTACK_BUN_SHIM=0 opts out, a foreign ~/.local/bin/bun is never touched, and
# deleting the files removes them. Chromium is lazy by default (bin/gstack-browser-ensure
# installs it on first use); --with-browser installs it now. The last lines
# are the GSTACK_LAUNCH line the parent supplies to every gstack command it
# launches and the doctor's trailer (`gstack: ok ...` or `gstack: fail ...`;
# grep that, never pipe through tail).
# Exit: 0 ok, 1 a component failed, 2 usage, 3 preflight stopped before ./setup.
set -u
BASH_COMPAT=50
SELF="$(cd "$(dirname "$0")/.." && pwd -P)"
ROOT="${GSTACK_CAPY_ROOT:-$SELF}"
REPO="${GSTACK_CAPY_REPO:-https://github.com/garrytan/gstack.git}"
DOCS="https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md"
FOR="" REQUIRE="" WITH_BROWSER=0 REVISION="" PROJECT="" CHECK_ONLY=0 MIN_REVISION="${GSTACK_CAPY_REMOTE_MIN:-}"
usage() { sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; }
need() { [ -n "${2:-}" ] || { echo "gstack-capy-install: $1 needs a value" >&2; usage >&2; exit 2; }; }
while [ $# -gt 0 ]; do
case "$1" in
--for|--only) need "$1" "${2:-}"; FOR="$2"; shift ;;
--require) need "$1" "${2:-}"; REQUIRE="$2"; shift ;;
--with-browser) WITH_BROWSER=1 ;;
--revision) need "$1" "${2:-}"; REVISION="$2"; shift ;;
--project) need "$1" "${2:-}"; PROJECT="$2"; shift ;;
--root) need "$1" "${2:-}"; ROOT="$2"; shift ;;
--check) CHECK_ONLY=1 ;;
--min-revision) need "$1" "${2:-}"; MIN_REVISION="$2"; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "gstack-capy-install: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
esac
shift
done
[ -n "$FOR" ] && [ -n "$REQUIRE" ] && { echo "gstack-capy-install: --for and --require are exclusive" >&2; exit 2; }
[ -z "$FOR" ] && [ -z "$REQUIRE" ] && REQUIRE="claude,codex,patch"
say() { echo "gstack-capy-install: $*"; }
stop() { # CODE MESSAGE FIX — print and exit 3 before ./setup
echo "gstack-capy-install: $2; fix: $3 ($1; $DOCS#$(gstack_result_anchor "$1" 2>/dev/null || echo capy-install-preflight-failed))" >&2
exit 3
}
# Result-code anchors come from the generated table (lib/result-codes.ts).
. "$SELF/bin/gstack-doctor-components.sh" 2>/dev/null || gstack_result_anchor() { return 1; }
now() { date +%s; }
T0=$(now); ORIG_PWD=$(pwd)
# 1. Clone or reuse, under the same lock the user-scope skill takes.
mkdir -p "$(dirname "$ROOT")" || stop INSTALL_PREFLIGHT_FAILED "cannot create $(dirname "$ROOT")" "choose a writable --root"
command -v git >/dev/null 2>&1 || stop INSTALL_PREFLIGHT_FAILED "preflight: git not found" "apt-get install -y git"
# The bootstrapper's fast-forward, this install and its check are one
# transaction: with GSTACK_CAPY_LOCK_HELD=1 the caller already holds the lock
# (inherited fd), so taking it again would deadlock.
if [ "${GSTACK_CAPY_LOCK_HELD:-}" != 1 ]; then
exec 9>"$(dirname "$ROOT")/.gstack-capy-install.lock"
if command -v flock >/dev/null 2>&1; then flock -w 900 9 || stop INSTALL_PREFLIGHT_FAILED "another gstack-capy-install holds $(dirname "$ROOT")/.gstack-capy-install.lock" "wait for it to finish"; fi
fi
if [ "$CHECK_ONLY" -eq 1 ] && { [ ! -f "$ROOT/setup" ] || [ ! -f "$ROOT/VERSION" ]; }; then
echo "gstack: fail not-installed — no gstack checkout at $ROOT; fix: $ROOT/bin/gstack-capy-install${FOR:+ --for $FOR} (installs)"
exit 1
elif [ "$CHECK_ONLY" -eq 1 ]; then :
elif [ ! -f "$ROOT/setup" ] || [ ! -f "$ROOT/VERSION" ]; then
[ -e "$ROOT" ] && [ -n "$(ls -A "$ROOT" 2>/dev/null)" ] && stop INSTALL_PREFLIGHT_FAILED "$ROOT exists but is not a gstack checkout" "remove it or pass --root <dir>"
say "cloning $REPO into $ROOT${REVISION:+ at $REVISION}"
if [ -n "$REVISION" ] && git clone --quiet --single-branch --depth 1 --branch "$REVISION" "$REPO" "$ROOT" 2>/dev/null; then :
elif [ -n "$REVISION" ]; then
git clone --quiet "$REPO" "$ROOT" && git -C "$ROOT" checkout --quiet "$REVISION" || stop INSTALL_PREFLIGHT_FAILED "could not clone $REPO at $REVISION" "check the revision and network, then re-run"
else
git clone --quiet --single-branch --depth 1 "$REPO" "$ROOT" || stop INSTALL_PREFLIGHT_FAILED "could not clone $REPO" "check network access to github.com, then re-run"
fi
say "cloned $REPO at $(git -C "$ROOT" rev-parse --short=12 HEAD 2>/dev/null) ($(head -1 "$ROOT/VERSION"))"
elif [ -n "${GSTACK_CAPY_CLONED:-}" ]; then
say "cloned $GSTACK_CAPY_CLONED at $(git -C "$ROOT" rev-parse --short=12 HEAD 2>/dev/null) ($(head -1 "$ROOT/VERSION"))"
else
say "reusing $ROOT ($(head -1 "$ROOT/VERSION")); never reset or pulled"
fi
. "$ROOT/bin/gstack-bun-version.sh" || stop INSTALL_PREFLIGHT_FAILED "$ROOT/bin/gstack-bun-version.sh is missing" "re-clone gstack"
. "$ROOT/bin/gstack-install-verdict.sh" || stop INSTALL_PREFLIGHT_FAILED "$ROOT/bin/gstack-install-verdict.sh is missing" "re-clone gstack"
. "$ROOT/bin/gstack-node-runtime.sh" || stop INSTALL_PREFLIGHT_FAILED "$ROOT/bin/gstack-node-runtime.sh is missing" "re-clone gstack"
# 2. Preflight: tools, then Bun through the one resolution contract
# (gstack_bun_resolve: the project's exact pin and gstack's install default in
# the versioned runtime store, $BUN_INSTALL/bin, ~/.bun/bin, PATH; the first
# that satisfies gstack's minimum and every project pin wins). A verified
# archive is downloaded into the store only when no candidate qualifies; the
# store is published atomically and ~/.bun/bin is never written. --check reads
# the pins with the Bun the verdict recorded (any eligible Bun when that one is
# gone; the jq subset when nothing is recorded) and reports drift from the
# recorded resolution (BUN_RESOLUTION_DRIFT) with one converging fix.
STATE_ROOT=$("$ROOT/bin/gstack-paths" --get GSTACK_STATE_ROOT); : "${GSTACK_STATE_ROOT:=$STATE_ROOT}"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}"
ROOT_REAL=$(cd "$ROOT" && pwd -P)
STORE=$(gstack_bun_store "$GSTACK_STATE_ROOT")
[ "$CHECK_ONLY" -eq 1 ] || for tool in curl node jq; do
command -v "$tool" >/dev/null 2>&1 || stop INSTALL_PREFLIGHT_FAILED "preflight: $tool not found" "install $tool (apt-get install -y jq; Node from https://nodejs.org or nvm), then re-run"
done
INSTALL_VERSION=$(gstack_bun_install_version) || stop BUN_INSTALL_VERSION_INVALID "GSTACK_BUN_INSTALL_VERSION=$INSTALL_VERSION is not a verified Bun at or above gstack's supported minimum $GSTACK_BUN_TESTED (verified archives: $GSTACK_BUN_VERIFIED)" "unset GSTACK_BUN_INSTALL_VERSION, or set it to one of: $GSTACK_BUN_VERIFIED"
[ -n "$PROJECT" ] || { PROJECT=$(cd "$ORIG_PWD" && git rev-parse --show-toplevel 2>/dev/null || true); [ "$(cd "${PROJECT:-/nonexistent}" 2>/dev/null && pwd -P)" = "$ROOT_REAL" ] && PROJECT=""; }
PROJECT_REAL=""; [ -n "$PROJECT" ] && PROJECT_REAL=$(cd "$PROJECT" 2>/dev/null && pwd -P)
VERDICT=$(gstack_install_verdict_path "$GSTACK_STATE_ROOT" "$ROOT_REAL" "$PROJECT_REAL")
REC_BUN="" REC_VER="" REC_RULE="" REC_BUN_INSTALL=""
if [ -f "$VERDICT" ]; then
REC_BUN=$(gstack_install_verdict_field "$VERDICT" bun_path) REC_VER=$(gstack_install_verdict_field "$VERDICT" bun_version)
REC_RULE=$(gstack_install_verdict_field "$VERDICT" bun_rule) REC_BUN_INSTALL=$(gstack_install_verdict_field "$VERDICT" bun_install)
fi
LAUNCH_DIR="" RES_BIN="" RES_VER="" RES_RULE=""
PINS_OUT="" PINS_MODE=none RUNNER="" PIN="" PIN_SRC="" PIN_EXACT="" PIN_ALTS=""
# read_pins RUNNER — the project's pins through the doctor's parser
# (lib/runtime-pins.ts: engines, packageManager, .tool-versions, .bun-version,
# CI lanes) when RUNNER can run it, else the package.json / .bun-version subset
# read with jq. Sets PINS_MODE (parser|jq|none) and PIN_EXACT, the exact Bun
# to prefer: the one non-CI exact pin (two different ones conflict), else
# among this platform's CI exact pins (alternatives; any one satisfies CI)
# the install version when listed, else the highest verified one, else the
# highest one.
read_pins() {
RUNNER="$1" PINS_OUT="" PINS_MODE=none PIN="" PIN_SRC="" PIN_EXACT="" PIN_ALTS=""
[ -n "$PROJECT" ] || return 0
[ -n "$RUNNER" ] && [ -f "$ROOT/bin/gstack-runtime-pins.ts" ] && PINS_OUT=$("$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" 2>/dev/null)
if printf '%s\n' "$PINS_OUT" | grep -q '^PINS_VERDICT: '; then
PINS_MODE=parser
_exact=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_EXACT_REQUIRED: bun \([^ ]*\) \([^ ]*\)$/\1 \2/p')
if [ "$(printf '%s\n' "$_exact" | grep -c .)" -gt 1 ]; then
stop RUNTIME_PIN_CONFLICT "Bun conflict: $PROJECT pins more than one exact Bun ($(printf '%s\n' "$_exact" | sed 's/^\([^ ]*\) \(.*\)$/\2 pins \1/' | tr '\n' ';' | sed 's/;$//; s/;/; /g'))" "make the project's exact Bun pins agree"
fi
if [ -n "$_exact" ]; then
PIN_EXACT=${_exact%% *} PIN_SRC="$PROJECT/$(printf '%s' "${_exact#* }" | sed 's/:[0-9]*$//')"
return 0
fi
_alts=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_CI_ALTERNATIVES: bun //p')
[ -n "$_alts" ] || return 0
case "$_alts" in *' '*) PIN_ALTS="$_alts" ;; esac
PIN_SRC="$PROJECT/.github/workflows (CI exact pins: $_alts)"
case " $_alts " in *" $INSTALL_VERSION "*) PIN_EXACT="$INSTALL_VERSION"; return 0 ;; esac
for _v in $(printf '%s\n' $_alts | sort -t. -k1,1nr -k2,2nr -k3,3nr); do
gstack_bun_archive_sha256 linux-x64 "$_v" >/dev/null && [ "$(gstack_bun_status "$_v")" = ok ] && { PIN_EXACT="$_v"; return 0; }
done
PIN_EXACT=$(printf '%s\n' $_alts | sort -t. -k1,1nr -k2,2nr -k3,3nr | head -1)
return 0
fi
command -v jq >/dev/null 2>&1 || return 0
PINS_MODE=jq
if [ -f "$PROJECT/package.json" ]; then
PIN=$(jq -r '.engines.bun // (.packageManager // "" | select(startswith("bun@")) | ltrimstr("bun@")) // empty' "$PROJECT/package.json" 2>/dev/null | head -1)
[ -n "$PIN" ] && PIN_SRC="$PROJECT/package.json"
fi
if [ -z "$PIN" ] && [ -s "$PROJECT/.bun-version" ]; then PIN=$(head -1 "$PROJECT/.bun-version" | tr -d ' v'); PIN_SRC="$PROJECT/.bun-version"; fi
case "$PIN" in
'') PINS_MODE=none ;;
[0-9]*.[0-9]*.[0-9]*) PIN_EXACT="$PIN" ;;
'>='*) ;;
*) say "note: project pin '$PIN' ($PIN_SRC) is not evaluated here; gstack-doctor's pins row checks it after install" ;;
esac
}
# pins_accept VERSION — the resolver's eligibility hook: VERSION satisfies every project pin.
pins_accept() {
case "$PINS_MODE" in
parser) "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --bun "$1" 2>/dev/null | grep -Eq '^PINS_VERDICT: (pass|none)$' ;;
jq) case "$PIN" in
'>='*) _gstack_bun_below "$1" "${PIN#>=}"; [ $? -eq 1 ] ;;
[0-9]*.[0-9]*.[0-9]*) [ "$1" = "$PIN" ] ;;
*) return 0 ;;
esac ;;
*) return 0 ;;
esac
}
pins_text() {
case "$PINS_MODE" in
parser) printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_SUMMARY: //p' | grep '^bun' | tr '\n' ';' | sed 's/;$//; s/;/; /g' ;;
*) printf '%s' "${PIN:+$PIN ($PIN_SRC)}" ;;
esac
}
resolve() { # — RES_* from the contract under the project's pins
if gstack_bun_resolve "$STORE" "$PIN_EXACT" pins_accept; then RES_BIN="$_gb_bin" RES_VER="$_gb_ver" RES_RULE="$_gb_rule"; return 0; fi
RES_BIN="" RES_VER="" RES_RULE=""
return 1
}
install_bun() { # VERSION REASON — download, verify and publish Bun VERSION at $STORE/VERSION/bun
WANT="$1"
say "$2; installing bun $WANT from its release archive (SHA-256 verified) into $STORE/$WANT"
case "$(uname -ms)" in
'Linux x86_64') TARGET=linux-x64; grep -q avx2 /proc/cpuinfo 2>/dev/null || TARGET=linux-x64-baseline ;;
'Linux aarch64'|'Linux arm64') TARGET=linux-aarch64 ;;
*) stop INSTALL_PREFLIGHT_FAILED "no verified Bun $WANT archive for $(uname -ms) (this installer is for Capy cloud Ubuntu)" "install Bun $WANT by hand (https://bun.sh), then re-run" ;;
esac
SHA=$(gstack_bun_archive_sha256 "$TARGET" "$WANT") || stop INSTALL_PREFLIGHT_FAILED "no verified Bun $WANT archive for $TARGET" "install Bun $WANT by hand, then re-run"
mkdir -p "$STORE" || stop INSTALL_PREFLIGHT_FAILED "cannot create the runtime store $STORE" "set GSTACK_STATE_ROOT to a writable directory"
exec 8>"$HOME/.gstack-runtime.lock"
if command -v flock >/dev/null 2>&1; then flock -w 300 8 || stop INSTALL_PREFLIGHT_FAILED "another install holds $HOME/.gstack-runtime.lock" "wait for it to finish"; fi
if [ ! -x "$STORE/$WANT/bun" ] || [ "$("$STORE/$WANT/bun" --version 2>/dev/null | head -1)" != "$WANT" ]; then
TMP=$(mktemp -d "$STORE/.tmp.XXXXXX") || stop INSTALL_PREFLIGHT_FAILED "cannot create a temp dir in $STORE" "set GSTACK_STATE_ROOT to a writable directory"
curl --retry 3 -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v$WANT/bun-$TARGET.zip" -o "$TMP/bun.zip" || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "could not download bun-v$WANT/bun-$TARGET.zip" "check network access to github.com, then re-run"; }
echo "$SHA $TMP/bun.zip" | sha256sum -c - >/dev/null 2>&1 || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "bun-$TARGET.zip SHA-256 mismatch (expected $SHA); nothing installed" "update gstack_bun_archive_sha256 in bin/gstack-bun-version.sh only after checking the release's SHASUMS256.txt"; }
mkdir "$TMP/pub" && (cd "$TMP" && unzip -qo bun.zip) && install -m 0755 "$TMP/bun-$TARGET/bun" "$TMP/pub/bun" && ln -s bun "$TMP/pub/bunx" || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "could not unpack bun into $STORE" "install unzip, or set GSTACK_STATE_ROOT to a writable directory"; }
_got=$("$TMP/pub/bun" --version 2>/dev/null | head -1)
[ "$_got" = "$WANT" ] || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "the downloaded bun reports $_got, expected $WANT; nothing installed" "re-run; report it if it repeats"; }
# Publish without a window: a new version directory is renamed into
# place; in an existing one each file is renamed over its predecessor.
if [ -d "$STORE/$WANT" ] && [ ! -L "$STORE/$WANT" ]; then
mv -fT "$TMP/pub/bun" "$STORE/$WANT/bun" && mv -fT "$TMP/pub/bunx" "$STORE/$WANT/bunx"
else
rm -f "$STORE/$WANT" && mv -T "$TMP/pub" "$STORE/$WANT"
fi || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "could not publish $STORE/$WANT" "check $STORE, then re-run"; }
rm -rf "$TMP"
fi
[ -e "$STORE/$WANT/bunx" ] || [ -L "$STORE/$WANT/bunx" ] || ln -s bun "$STORE/$WANT/bunx" 2>/dev/null || true
exec 8>&-
say "bun $WANT installed at $STORE/$WANT/bun"
}
conflict() { # DETAIL
stop RUNTIME_PIN_CONFLICT "Bun conflict: $1" "raise the project's pin to a verified version ($GSTACK_BUN_VERIFIED), or install gstack on a machine the project already provisions"
}
if [ "$CHECK_ONLY" -eq 1 ]; then
_runner=""
if [ -n "$REC_BUN" ] && [ -x "$REC_BUN" ]; then _runner="$REC_BUN"
elif [ -n "$REC_BUN" ] && gstack_bun_resolve "$STORE"; then _runner="$_gb_bin"
fi
read_pins "$_runner"
resolve || true
else
# A Bun to read the pins with: the contract without project pins, else
# any Bun at or above the security floor (the parser runs there).
_runner=""
if gstack_bun_resolve "$STORE"; then _runner="$_gb_bin"
elif command -v bun >/dev/null 2>&1 && [ "$(gstack_bun_status "$(bun --version 2>/dev/null | head -1)")" = untested ]; then _runner=$(command -v bun)
fi
read_pins "$_runner"
if ! resolve; then
JUDGED="${_gb_judged:-none}"
if [ -n "$PIN_EXACT" ]; then
[ "$(gstack_bun_status "$PIN_EXACT")" = ok ] && gstack_bun_archive_sha256 linux-x64 "$PIN_EXACT" >/dev/null \
|| conflict "gstack needs >=$GSTACK_BUN_TESTED (verified archives: $GSTACK_BUN_VERIFIED), $PIN_SRC pins exactly $PIN_EXACT"
WANT="$PIN_EXACT"
else
WANT="$INSTALL_VERSION"
fi
pins_accept "$WANT" || conflict "gstack needs >=$GSTACK_BUN_TESTED (verified archives: $GSTACK_BUN_VERIFIED), $PROJECT pins $(pins_text)"
if [ -n "$PROJECT" ] && [ "$PINS_MODE" != none ]; then
install_bun "$WANT" "no installed Bun satisfies gstack's supported minimum $GSTACK_BUN_TESTED and the project pins $(pins_text) (judged: $JUDGED)"
else
install_bun "$WANT" "no installed Bun satisfies gstack's supported minimum $GSTACK_BUN_TESTED (judged: $JUDGED)"
fi
[ "$PINS_MODE" = parser ] || read_pins "$STORE/$WANT/bun"
resolve || conflict "bun $WANT in $STORE does not satisfy $PROJECT's pins $(pins_text) (judged: ${_gb_judged:-none})"
fi
[ -n "${GSTACK_BUN_INSTALL_VERSION:-}" ] && say "install version $INSTALL_VERSION from GSTACK_BUN_INSTALL_VERSION (default $GSTACK_BUN_DEFAULT)"
say "bun $RES_VER at $RES_BIN ($(gstack_bun_rule_text "$RES_RULE"); satisfies gstack's supported minimum $GSTACK_BUN_TESTED$([ -n "$PIN_ALTS" ] && printf " and the project's CI pins (alternatives: %s)" "$PIN_ALTS" || printf '%s' "${PIN_EXACT:+ and the project pin $PIN_EXACT}")$([ -z "$PIN_EXACT" ] && [ -n "$PIN" ] && printf ' and the project pin %s' "$PIN")$([ -z "$PIN_EXACT" ] && [ "$PINS_MODE" = parser ] && printf ' and the project pins'))"
fi
# Drift (--check only). The recorded Bun stays certified while it exists,
# reports the recorded version and satisfies gstack's minimum and the pins;
# a different candidate that would now resolve first is only a note. Drift
# (BUN_RESOLUTION_DRIFT) is the recorded Bun gone, changed or no longer
# satisfying the pins.
if [ "$CHECK_ONLY" -eq 1 ] && [ -n "$REC_BUN" ]; then
_why="" _wrapper="${GSTACK_CAPY_WRAPPER_PATH:+bash $GSTACK_CAPY_WRAPPER_PATH}"; _wrapper="${_wrapper:-$ROOT/bin/gstack-capy-install}${FOR:+ --for $FOR}"
_recnow=""; [ -x "$REC_BUN" ] && _recnow=$("$REC_BUN" --version 2>/dev/null | head -1)
if [ ! -x "$REC_BUN" ]; then _why="the recorded binary is gone"
elif [ "$_recnow" != "$REC_VER" ]; then _why="$REC_BUN now reports ${_recnow:-nothing} (replaced in place)"
elif [ "$(gstack_bun_status "$REC_VER")" != ok ] || ! pins_accept "$REC_VER"; then _why="bun $REC_VER no longer satisfies gstack's supported minimum $GSTACK_BUN_TESTED and the project pins $(pins_text)"
fi
if [ -z "$_why" ]; then
[ "$REC_BUN" != "$RES_BIN" ] && say "note: the recorded bun $REC_VER at $REC_BUN stays certified; ${RES_BIN:+bun $RES_VER at $RES_BIN ($RES_RULE) would now resolve first; }run $_wrapper to record the current resolution"
RES_BIN="$REC_BUN" RES_VER="$REC_VER" RES_RULE="$REC_RULE"
else
if [ -n "$RES_BIN" ]; then _fix="$_wrapper (records the current resolution: bun $RES_VER at $RES_BIN)"
else _fix="$_wrapper (reinstalls bun ${PIN_EXACT:-$INSTALL_VERSION} into $STORE)"
fi
_now="no eligible bun"; [ -n "$RES_BIN" ] && _now="bun $RES_VER at $RES_BIN ($RES_RULE)"
export GSTACK_DOCTOR_RUNTIME_DRIFT="recorded bun $REC_VER at $REC_BUN ($REC_RULE), now $_now; changed: $_why"
export GSTACK_DOCTOR_RUNTIME_DRIFT_FIX="$_fix"
fi
fi
# Node: the project's exact Node pins resolve, and install into the runtime
# store, like Bun's (bin/gstack-node-runtime.sh): non-CI exact pins are
# required, CI exact pins in different jobs are alternatives. Parser mode
# only. --check keeps the recorded Node while it still reports its version and
# satisfies the pins, and never installs.
NODE_STORE=$(gstack_node_store "$GSTACK_STATE_ROOT")
NODE_BIN="" NODE_VER="" NODE_RULE="" NODE_WANT=""
node_accept() { ! "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --node "$1" 2>/dev/null | grep -q '^PIN_FAIL: node '; }
node_pins_text() { printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_SUMMARY: //p' | grep '^node' | tr '\n' ';' | sed 's/;$//; s/;/; /g'; }
if [ "$PINS_MODE" = parser ] && printf '%s\n' "$PINS_OUT" | grep -q '^PIN: node '; then
_nreq=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_EXACT_REQUIRED: node \([^ ]*\) \([^ ]*\)$/\1 \2/p')
[ "$(printf '%s\n' "$_nreq" | grep -c .)" -gt 1 ] \
&& stop RUNTIME_PIN_CONFLICT "Node conflict: $PROJECT pins more than one exact Node ($(printf '%s\n' "$_nreq" | sed 's/^\([^ ]*\) \(.*\)$/\2 pins \1/' | tr '\n' ';' | sed 's/;$//; s/;/; /g'))" "make the project's exact Node pins agree"
if [ -n "$_nreq" ]; then NODE_WANT=${_nreq%% *}
else NODE_WANT=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_CI_ALTERNATIVES: node //p' | tr ' ' '\n' | grep . | sort -t. -k1,1nr -k2,2nr -k3,3nr | tr '\n' ' ' | sed 's/ $//')
fi
REC_NODE="" REC_NODE_VER=""
[ -f "$VERDICT" ] && REC_NODE=$(gstack_install_verdict_field "$VERDICT" node_path) REC_NODE_VER=$(gstack_install_verdict_field "$VERDICT" node_version)
if [ "$CHECK_ONLY" -eq 1 ] && [ -n "$REC_NODE" ] && [ "$(gstack_node_version "$REC_NODE")" = "$REC_NODE_VER" ] && node_accept "$REC_NODE_VER"; then
NODE_BIN="$REC_NODE" NODE_VER="$REC_NODE_VER" NODE_RULE=$(gstack_install_verdict_field "$VERDICT" node_rule)
elif gstack_node_resolve "$NODE_STORE" "$NODE_WANT" node_accept; then
NODE_BIN="$_gn_bin" NODE_VER="$_gn_ver" NODE_RULE="$_gn_rule"
elif [ "$CHECK_ONLY" -eq 0 ] && [ -n "$NODE_WANT" ]; then
_nv=${NODE_WANT%% *}
say "no installed Node satisfies the project's Node pins $(node_pins_text) (node on PATH: $(gstack_node_version "$(command -v node 2>/dev/null || echo /nonexistent)" || echo none)); installing node $_nv from its release archive (SHA-256 checked against SHASUMS256.txt) into $NODE_STORE/$_nv"
exec 8>"$HOME/.gstack-runtime.lock"
if command -v flock >/dev/null 2>&1; then flock -w 300 8 || stop INSTALL_PREFLIGHT_FAILED "another install holds $HOME/.gstack-runtime.lock" "wait for it to finish"; fi
_nerr=$(gstack_node_install "$NODE_STORE" "$_nv") || stop INSTALL_PREFLIGHT_FAILED "node $_nv: $_nerr" "check network access to nodejs.org, then re-run; or install node $_nv yourself (nvm install $_nv) and re-run"
exec 8>&-
gstack_node_resolve "$NODE_STORE" "$_nv" node_accept \
|| stop RUNTIME_PIN_CONFLICT "Node conflict: node $_nv in $NODE_STORE does not satisfy $PROJECT's Node pins $(node_pins_text)" "make the project's Node pins agree"
NODE_BIN="$_gn_bin" NODE_VER="$_gn_ver" NODE_RULE="$_gn_rule"
else
say "note: no installed Node satisfies the project's Node pins $(node_pins_text); gstack-doctor's pins row names the fix"
fi
[ -n "$NODE_BIN" ] && say "node $NODE_VER at $NODE_BIN ($NODE_RULE; satisfies the project's Node pins $(node_pins_text))"
fi
# The launch PATH. gstack's store holds one Bun per directory, so a store Bun
# launches from its own directory. Any other resolved Bun, and a resolved Node
# that PATH does not already give, launch through a gstack-owned directory of
# symlinks beside the verdict, so a shared bin directory (/usr/local/bin with
# the image's node, $BUN_INSTALL/bin) never lands in front of PATH.
if [ -n "$RES_BIN" ]; then
_launch_node=""
[ -n "$NODE_BIN" ] && [ "$(gstack_realpath "$(command -v node 2>/dev/null || echo /nonexistent)" 2>/dev/null)" != "$NODE_BIN" ] && _launch_node="$NODE_BIN"
if [ -n "$_launch_node" ] || [ "$(gstack_realpath "$(command -v bun 2>/dev/null || echo /nonexistent)" 2>/dev/null)" != "$RES_BIN" ]; then
_store_real=$(cd -P "$STORE" 2>/dev/null && pwd -P)
case "$RES_BIN" in
"${_store_real:-/nonexistent}"/*/bun) [ -z "$_launch_node" ] && LAUNCH_DIR=$(dirname "$RES_BIN") ;;
esac
if [ -z "$LAUNCH_DIR" ]; then
LAUNCH_DIR="${VERDICT%.json}.launch"
gstack_launch_dir "$LAUNCH_DIR" "$RES_BIN" "$_launch_node" \
|| stop INSTALL_PREFLIGHT_FAILED "could not publish the launch directory $LAUNCH_DIR" "make $(dirname "$LAUNCH_DIR") writable, then re-run"
fi
export PATH="$LAUNCH_DIR:$PATH"
fi
fi
# 3. Install both host profiles; Chromium lazy unless --with-browser. Skipped
# when this exact HEAD was set up with the same hosts, model and browser flag
# (recorded in the git dir after a successful setup, never keyed on VERSION)
# and the check passes; never after a fast-forward.
cd "$ROOT" || exit 1
export GSTACK_SKIP_COREUTILS="${GSTACK_SKIP_COREUTILS:-1}" GSTACK_SKIP_ASIDE="${GSTACK_SKIP_ASIDE:-1}"
BROWSER_FLAG="--no-browser"; [ "$WITH_BROWSER" -eq 1 ] && BROWSER_FLAG=""
HEAD_SHA=$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)
SETUP_STATE="$(git -C "$ROOT" rev-parse --absolute-git-dir 2>/dev/null || echo "$ROOT/.git")/gstack-capy-install.state"
SETUP_RECORD="head=$HEAD_SHA hosts=claude,codex model=gpt-6-astra browser=$WITH_BROWSER"
if [ -n "$FOR" ]; then set -- --for "$FOR"; else set -- --require "$REQUIRE"; fi
[ "$WITH_BROWSER" -eq 1 ] && [ -z "$FOR" ] && set -- --require "$REQUIRE,browse-bundle,browser"
certify() { OUT=$(cd "$ORIG_PWD" && GSTACK_EPHEMERAL="${GSTACK_EPHEMERAL:-1}" "$ROOT/bin/gstack-doctor" --check "$@" ${PROJECT:+--project "$PROJECT"} ${MIN_REVISION:+--min-revision "$MIN_REVISION"} 2>&1); RC=$?; }
record_verdict() { # TRAILER OK — --check keeps the recorded resolution (drift is reported, never re-recorded)
if [ "$CHECK_ONLY" -eq 1 ] && [ -n "$REC_BUN" ]; then set -- "$1" "$2" "$REC_BUN" "$REC_VER" "$REC_RULE" "$REC_BUN_INSTALL"
else set -- "$1" "$2" "$RES_BIN" "$RES_VER" "$RES_RULE" "${BUN_INSTALL:-}"; fi
gstack_install_verdict_write "$VERDICT" "$ROOT_REAL" "$HEAD_SHA" "${FOR:-}" "$(printf '%s\n' "${OUT:-}" | sed -n 's/^required: //p' | head -1)" \
"$(printf '%s\n' "${OUT:-}" | grep -q '^SKIP pins (planning-only' && echo 1 || echo 0)" "$2" "$1" "${MIN_REVISION:-}" "$LAUNCH_DIR" \
"$PROJECT_REAL" "$PIN_EXACT" "$3" "$4" "$5" "$6" "$NODE_BIN" "$NODE_VER" "$NODE_RULE" \
|| say "could not write the install verdict $VERDICT"
}
setup_failed() { # MESSAGE
echo "gstack-capy-install: $1" >&2
OUT="" record_verdict "gstack: fail setup" 0
echo "gstack: fail setup — $1; fix: re-run the Capy skill installer or $ROOT/bin/gstack-capy-install (setup is re-run until it succeeds)"
exit 1
}
SKIPPED=0
if [ "$CHECK_ONLY" -eq 0 ] && [ -z "${GSTACK_CAPY_UPGRADED_FROM:-}" ] && [ -n "$HEAD_SHA" ] && [ "$(head -1 "$SETUP_STATE" 2>/dev/null)" = "$SETUP_RECORD" ]; then
certify "$@"
[ "$RC" -eq 0 ] && { SKIPPED=1; say "setup skipped (HEAD ${HEAD_SHA:0:12} already set up for these hosts; the check passes)"; }
fi
if [ "$CHECK_ONLY" -eq 0 ] && [ "$SKIPPED" -eq 0 ]; then
T1=$(now)
./setup --host claude --no-prefix $BROWSER_FLAG </dev/null || setup_failed "./setup --host claude failed (exit $?)"
./setup --host codex --model gpt-6-astra $BROWSER_FLAG </dev/null || setup_failed "./setup --host codex failed (exit $?)"
echo "gstack ready (claude, codex)"
T2=$(now)
[ -n "$HEAD_SHA" ] && printf '%s\n' "$SETUP_RECORD" > "$SETUP_STATE" 2>/dev/null
say "planning-only install: $((T2 - T1))s (setup both hosts; Chromium not downloaded)"
if [ "$WITH_BROWSER" -eq 1 ]; then
T3=$(now)
"$ROOT/bin/gstack-browser-ensure" </dev/null | tail -1
say "browser bootstrap: $(( $(now) - T3 ))s"
else
echo "SKIP browser ($(. "$ROOT/bin/gstack-doctor-components.sh" && printf '%s' "$GSTACK_BROWSER_LAZY_REASON"))"
fi
certify "$@"
fi
[ "$CHECK_ONLY" -eq 1 ] && certify "$@"
# write_shim — the per-directory bun and bunx shims (Capy only, install mode).
# The store, verdict directory, checkout and install default are baked in, so
# a fresh shell needs no environment; rewritten when they change. Only a
# missing file or one carrying the marker is replaced.
shq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
write_shim() {
local dir="$HOME/.local/bin" name tmp STORE_REAL
[ "${GSTACK_BUN_SHIM:-1}" != 0 ] || return 0
[ -n "${CAPY_MACHINE_CONFIG:-}" ] || [ -d "$HOME/.capy/system" ] || return 0
mkdir -p "$dir" "$STORE" || { say "could not create $dir or $STORE; no bun shim written"; return 0; }
STORE_REAL=$(cd -P "$STORE" && pwd -P) || return 0
exec 8>"$HOME/.gstack-runtime.lock"
command -v flock >/dev/null 2>&1 && flock -w 60 8
for name in bun bunx; do
if { [ -e "$dir/$name" ] || [ -L "$dir/$name" ]; } && ! { [ -f "$dir/$name" ] && [ ! -L "$dir/$name" ] && _gstack_bun_is_shim "$dir/$name"; }; then
say "$dir/$name is not gstack's shim (no marker); left untouched, so keep the GSTACK_LAUNCH prefix"
continue
fi
tmp="$dir/.$name.gstack.$$"
if [ "$name" = bunx ]; then
printf '#!/bin/sh\n%s\n# The Capy bunx shim: bun x through the bun shim beside it (docs/capy-install.md).\nexec %s x "$@"\n' "$GSTACK_BUN_SHIM_MARKER" "$(shq "$dir/bun")" > "$tmp"
else
{
printf '#!/bin/sh\n%s\n' "$GSTACK_BUN_SHIM_MARKER"
printf '# The Capy bun shim, written by %s.\n' "$(printf '%s' "$ROOT/bin/gstack-capy-install" | tr -d '\n')"
printf 'store=%s\n' "$(shq "$STORE_REAL")"
printf 'verdicts=%s\n' "$(shq "$(dirname "$(gstack_install_verdict_path "$GSTACK_STATE_ROOT" "$ROOT_REAL" "")")")"
printf 'checkout=%s\n' "$(shq "$ROOT_REAL")"
printf 'default=%s\nminimum=%s\nfloor=%s\n' "$(shq "$INSTALL_VERSION")" "$(shq "$GSTACK_BUN_TESTED")" "$(shq "$GSTACK_BUN_FLOOR")"
printf 'marker=%s\n' "$(shq "$GSTACK_BUN_SHIM_MARKER")"
printf 'fix=%s\n' "$(shq "${GSTACK_CAPY_WRAPPER_PATH:+bash }${GSTACK_CAPY_WRAPPER_PATH:-$ROOT/bin/gstack-capy-install} --check")"
cat <<'EOF_SHIM'
# Runs the Bun recorded for the project around the working directory, or the
# 'none' record outside a project and inside the gstack checkout; else the
# resolver's order: the store copy of an exact project pin, the store default,
# $BUN_INSTALL/bin, ~/.bun/bin, then PATH, first at or above the supported
# minimum, then at or above the security floor (docs/capy-install.md).
# GSTACK_BUN_SHIM_EXPLAIN=1 prints the project root, record, choice and rule.
set -f
explain() { [ "${GSTACK_BUN_SHIM_EXPLAIN:-}" = 1 ] && echo "gstack bun shim: $*" >&2; return 0; }
sha() { if command -v sha256sum >/dev/null 2>&1; then printf '%s' "$1" | sha256sum | cut -d' ' -f1; else printf '%s' "$1" | shasum -a 256 | cut -d' ' -f1; fi; }
is_shim() { head -c 512 "$1" 2>/dev/null | grep -aqF "$marker"; }
ge() { # A B: version A is at or above B (a prerelease sorts below its release)
case "$1" in *-*) pre=1 ;; *) pre=0 ;; esac
gifs=$IFS; IFS=.; set -- ${1%%[-+]*} $2; IFS=$gifs
[ $# -eq 6 ] || return 1
for n in "$@"; do case "$n" in ''|*[!0-9]*) return 1 ;; esac; done
[ "$1" -ne "$4" ] && { [ "$1" -gt "$4" ]; return; }
[ "$2" -ne "$5" ] && { [ "$2" -gt "$5" ]; return; }
[ "$3" -ne "$6" ] && { [ "$3" -gt "$6" ]; return; }
[ "$pre" = 0 ]
}
in_store() { # VERSION: the store's VERSION/bun when VERSION is x.y.z and the file is a real binary inside the store
printf '%s\n' "$1" | grep -Eqx '[0-9]+\.[0-9]+\.[0-9]+' || return 1
d=$(cd -P "$store/$1" 2>/dev/null && pwd -P) || return 1
[ "$d" = "$store/$1" ] && [ -f "$d/bun" ] && [ ! -L "$d/bun" ] && [ -x "$d/bun" ] && ! is_shim "$d/bun" && printf '%s/bun' "$d"
}
root="" record="" chose="" rule="" pin=""
dir=$(pwd -P 2>/dev/null) || dir=""
while [ -n "$dir" ]; do
[ -e "$dir/.git" ] && { root=$dir; break; }
[ "$dir" = / ] && break
dir=${dir%/*}; [ -n "$dir" ] || dir=/
done
key=${root:-none}; [ "$root" = "$checkout" ] && key=none
record="$verdicts/$(sha "$key").json"
if [ -f "$record" ]; then
b=$(sed -n 's/^ "bun_path": "\(.*\)",$/\1/p' "$record" | head -1)
[ -n "$b" ] && [ -f "$b" ] && [ -x "$b" ] && ! is_shim "$b" && chose=$b rule=record
else
record=""
fi
if [ -z "$chose" ] && [ "$key" != none ]; then
[ -f "$root/.bun-version" ] && pin=$(head -1 "$root/.bun-version" | tr -d ' v\r')
[ -n "$pin" ] || pin=$(sed -n 's/.*"packageManager"[[:space:]]*:[[:space:]]*"bun@\([^"]*\)".*/\1/p' "$root/package.json" 2>/dev/null | head -1)
[ -n "$pin" ] || pin=$(sed -n 's/.*"bun"[[:space:]]*:[[:space:]]*"v\{0,1\}\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' "$root/package.json" 2>/dev/null | head -1)
if [ -n "$pin" ]; then
if b=$(in_store "$pin"); then chose=$b rule=pin
else echo "gstack bun shim: $root pins bun $pin, which is not in gstack's runtime store; run $fix here" >&2
fi
else
explain "$root has no recorded Bun and no exact pin; run $fix here to record one"
fi
fi
[ -n "$chose" ] || { b=$(in_store "$default") && chose=$b rule=default; }
if [ -z "$chose" ]; then
cands="BUN_INSTALL:${BUN_INSTALL:+$BUN_INSTALL/bin/bun}
home:${HOME:+$HOME/.bun/bin/bun}"
oifs=$IFS; IFS=:
for d in $PATH; do [ -n "$d" ] && cands="$cands
PATH:$d/bun"; done
IFS=$oifs
for want in "$minimum" "$floor"; do
nl='
'
oifs=$IFS; IFS=$nl
for cand in $cands; do
r=${cand%%:*} f=${cand#*:}
[ -n "$f" ] && [ -f "$f" ] && [ -x "$f" ] && ! is_shim "$f" || continue
ge "$("$f" --version 2>/dev/null | head -1)" "$want" && { chose=$f rule=$r; break 2; }
done
IFS=$oifs
done
IFS=$oifs
fi
explain "project=${root:-none} record=${record:-none} chose=${chose:-none} rule=${rule:-none}"
[ -n "$chose" ] || { echo "gstack bun shim: no bun at or above $floor found (store $store, \$BUN_INSTALL/bin, ~/.bun/bin, PATH)" >&2; exit 127; }
exec "$chose" "$@"
EOF_SHIM
} > "$tmp"
fi
chmod 0755 "$tmp" && if cmp -s "$tmp" "$dir/$name"; then rm -f "$tmp"; else mv -f "$tmp" "$dir/$name" && say "wrote $dir/$name (gstack's per-directory $name shim; GSTACK_BUN_SHIM_EXPLAIN=1 shows its choice, GSTACK_BUN_SHIM=0 opts out)"; fi
done
exec 8>&-
}
# 4. Certify. The trailer is the contract, in one grammar:
# gstack: ok <rev> for=<skill>[ (requested: …; pins: skipped (planning-only|no project); latest: <ver> available)]
# Notes, in that fixed order, ride only on an ok trailer: `requested` when the
# existing checkout is not at --revision (it is never moved here), `pins` when
# the doctor skipped the project pins, `latest` when the bootstrapper read a
# newer VERSION from origin in this run, exported as GSTACK_CAPY_REMOTE_VERSION. The
# failure head (`gstack: fail <ids>`) is unchanged.
INSTALLED=$(head -1 "$ROOT/VERSION")
TRAILER="$(printf '%s\n' "$OUT" | grep '^gstack: ' | tail -1)"
NOTES=""
note() { NOTES="${NOTES:+$NOTES; }$1"; }
if [ -n "$REVISION" ] && [ "$REVISION" != "$INSTALLED" ] && [ "$REVISION" != "v$INSTALLED" ] \
&& [ "$(git -C "$ROOT" rev-parse --verify --quiet "$REVISION^{commit}" 2>/dev/null)" != "$(git -C "$ROOT" rev-parse HEAD 2>/dev/null)" ]; then
note "requested: $REVISION (run $ROOT/bin/gstack-upgrade to move)"
fi
if printf '%s\n' "$OUT" | grep -q '^SKIP pins (planning-only'; then note "pins: skipped (planning-only)"
elif printf '%s\n' "$OUT" | grep -Eq '^SKIP pins \((no project|project directory )'; then note "pins: skipped (no project)"
fi
REMOTE_VERSION="${GSTACK_CAPY_REMOTE_VERSION:-}"
if [ -n "$REMOTE_VERSION" ] && [ "$REMOTE_VERSION" != "$INSTALLED" ] \
&& [ "$(printf '%s\n%s\n' "$INSTALLED" "$REMOTE_VERSION" | sort -V | head -1)" = "$INSTALLED" ]; then
note "latest: $REMOTE_VERSION available"
fi
case "$TRAILER" in "gstack: ok "*) [ -n "$NOTES" ] && TRAILER="$TRAILER ($NOTES)" ;; esac
record_verdict "$TRAILER" "$([ "$RC" -eq 0 ] && echo 1 || echo 0)"
[ "$CHECK_ONLY" -eq 1 ] || write_shim
printf '%s\n' "$OUT" | grep -v '^gstack: '
say "total: $(( $(now) - T0 ))s"
echo "GSTACK_LAUNCH: ${LAUNCH_DIR:+PATH=$LAUNCH_DIR:\$PATH }GSTACK_SESSION_KIND=unattended GSTACK_STATE_ROOT=$GSTACK_STATE_ROOT GSTACK_EPHEMERAL=${GSTACK_EPHEMERAL:-1} GSTACK_CAPY=1 GSTACK_INSTALL_VERDICT=$VERDICT"
[ -n "$LAUNCH_DIR" ] && echo "GSTACK_LAUNCH_PATH: $LAUNCH_DIR"
printf '%s\n' "$TRAILER"
exit "$RC"