Files
msd-core/hooks/gsd-node-runner.sh
Carlos Cativo 9b77320580 fix(#4076): add missing gsd-hook-version header to gsd-node-runner.sh (#4092)
* fix(#4076): add missing gsd-hook-version header to gsd-node-runner.sh

gsd-node-runner.sh was registered in MANAGED_HOOKS but shipped without a
gsd-hook-version header, so gsd-check-update-worker.js always classified it
as 'definitely stale' (a missing header is indistinguishable from a
pre-version-tracking file). Every install on an otherwise up-to-date
version showed a permanent, unclearable '⚠ stale hooks — run /gsd-update'
warning naming this one file.

Root cause: the build-hooks.js comment claimed the file is 'not a
registered hook' and 'staged verbatim — no templating', but it IS in
MANAGED_HOOKS (managed-hooks-registry.cjs:34) and install.js already
stamps {{GSD_VERSION}} into every .sh hook unconditionally, gsd-node-runner.sh
included. The comment contradicted both the registry and the installer's
actual behavior, and the header line itself was simply never added.

Fix: add the header (matching every other managed .sh hook's format) and
correct the comment so it no longer asserts the opposite of what the
registry and installer actually do.

Adds a regression test that iterates every MANAGED_HOOKS entry and asserts
it carries a header matching the worker's own detection regex, so a future
hook added to the registry without one fails CI instead of shipping
silently.

Fixes #4076

* chore(#4076): add changeset fragment for PR #4092

* fix(#4076): address review nits — drop unneeded exemption, fix blank line

Per @trek-e's review on #4092:
- tests/managed-hooks.test.cjs:96: the readFileSync call uses a loop
  variable (entry-derived hookPath), not a literal path, so
  local/no-source-grep's static literal-path detector never flags it —
  the allow-test-rule exemption comment was unnecessary. Replaced with a
  plain note explaining the source-read rationale.
- tests/managed-hooks.test.cjs:121-122: dropped a stray extra blank line
  before the bug #2136 section divider.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-09-02 08:04:37 -04:00

78 lines
2.7 KiB
Bash
Executable File

#!/bin/sh
# gsd-hook-version: {{GSD_VERSION}}
# gsd-node-runner.sh — GSD portable node resolver (#3662).
#
# Managed JS hook commands under --portable-hooks route through this script:
#
# bash "<hooks>/gsd-node-runner.sh" "<baked-node-path>" "<script.js>" [args...]
#
# so a config root shared across environments (mounted ~/.claude, shared
# containers) resolves node at hook-fire time instead of depending on the
# absolute path of whichever environment ran the installer. Candidates, in
# order — the first executable one wins:
#
# 1. the first argument — the install-time node path, tried FIRST and by
# absolute path so the #2979/#3002/#3017/#3022 minimal-PATH guarantee
# holds (GUI launches with a stripped PATH still resolve where the
# baked path exists);
# 2. `command -v node`, accepted only when it yields an absolute path;
# 3. the well-known stable layouts: $HOME-derived mise/volta shims, the
# Homebrew prefixes, /usr/local/bin/node, /usr/bin/node.
#
# No bare `node` lookup is ever depended on: a candidate is used only after
# an explicit executable check, and when nothing resolves this script fails
# visibly (stderr diagnostic + exit 127) rather than emitting a half-resolved
# invocation.
#
# The candidate list below is a SUPERSET of the inline chain token emitted by
# buildNodeRunnerChainToken (src/runtime-hooks-surface.cts, #3662) — keep the
# two lists consistent.
#
# Diagnostic escape: GSD_NODE_RUNNER_NO_FALLBACKS=1 disables candidates 2-3
# (first-argument-only resolution) — used by the test suite and useful to
# pin down which node a given environment picks.
set -u
preferred=${1:-}
script=${2:-}
if [ -n "$script" ]; then
shift 2
elif [ -n "$preferred" ]; then
shift 1
preferred=
fi
found=''
# check <path> — record <path> if it is an absolute, executable file.
# Absolute = POSIX root (/*) or a win32 drive-letter path (C:/…), which is
# what the installer bakes on Windows; anything else (a relative `command -v`
# hit under a relative PATH entry, a bare name) is rejected so repo-cwd
# content can never reach the runner slot.
check() {
case "$1" in
/*|[A-Za-z]:/*) if [ -x "$1" ]; then found=$1; fi ;;
esac
[ -n "$found" ]
}
check "$preferred" || {
if [ "${GSD_NODE_RUNNER_NO_FALLBACKS:-0}" != "1" ]; then
path_node=$(command -v node 2>/dev/null || true)
check "$path_node" ||
check "${HOME:-}/.local/share/mise/shims/node" ||
check "${HOME:-}/.volta/bin/node" ||
check /opt/homebrew/bin/node ||
check /usr/local/bin/node ||
check /usr/bin/node ||
true
fi
}
if [ -z "$found" ]; then
echo "gsd-node-runner: no usable node found (preferred: ${preferred:-<none>})" >&2
exit 127
fi
exec "$found" "$script" "$@"