mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
v1.91.227.0 fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415) (#3431)
* fix(windows): shell bins pass NUL as Bun's empty config when MSYS path conversion is off (#3415) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(windows): detect Git Bash/MSYS2/Cygwin from OSTYPE, as gstack-doctor does, so uname stubs in setup fixtures stay untouched Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (#3415) - Choose NUL only when cygpath is also on PATH, so an OSTYPE=msys inherited by a POSIX bash keeps /dev/null instead of a cwd-relative NUL file. - gstack-developer-profile --log-session reports a Bun failure instead of skipping it as invalid input, like gstack-timeline-log. - learnings-injection tests check that their anchored bun -e block exists. - CHANGELOG names the callers this fix does not cover; curation reason fixed. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes, cycle 2 (#3415) - timeline-log and developer-profile validators print their verdict; any non-zero Bun exit is a reported failure (a Bun crash on Windows exits 3). - timeline-log keeps the original event when ts stamping fails. - The definition ratchet scopes a local GSTACK_BUN_CONFIG to its function. - The shell-bins fixture clears its preload marker per test. - CHANGELOG states the callers that discard the report and the Bun 1.3.x case. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: CHANGELOG notes Bun 1.3.10 evidence and remaining literal /dev/null callers (#3415) 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>
This commit is contained in:
1 parent
8d9501373b
commit
9dc1dfb729
54 files changed
+491
-112
No files matched your search
@@ -303,4 +303,4 @@ Agents poll `eval:bg:*` logs for the `### gstack-detach EXIT=<code> ###` sentine
|
||||
- Every Codex run's verdict goes through `lib/outside-review-result.ts`; its reason codes live in `lib/gate-outcomes.ts`, each with a `docs/troubleshooting.md` anchor.
|
||||
- `test/module-size-ratchet.test.ts` keeps refactored owner modules at or under 800 lines (150 per function) and residual files from growing.
|
||||
- The `claude` CLI binary resolves via `lib/claude-bin.ts` (re-exported from `browse/src/claude-bin.ts` for browse internals; `Bun.which()` + `GSTACK_CLAUDE_BIN` override). Set `GSTACK_CLAUDE_BIN=wsl` plus `GSTACK_CLAUDE_BIN_ARGS='["claude"]'` to run Claude through WSL on Windows.
|
||||
- Every Bun gstack starts ignores the working directory's `.env` files and `bunfig.toml`: bun shebangs are `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`, shell and skill callers pass `bun --no-env-file --config=/dev/null`, TypeScript spawns use `bunIsolationArgs()` from `lib/bun-isolation.ts`, and `bun build --compile` adds `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`. `test/bun-entry-isolation.test.ts` enforces all four.
|
||||
- Every Bun gstack starts ignores the working directory's `.env` files and `bunfig.toml`: bun shebangs are `#!/usr/bin/env -S bun --no-env-file --config=/dev/null`, skill prose passes `bun --no-env-file --config=/dev/null`, shipped shell files define `GSTACK_BUN_CONFIG` with the line in `test/bun-entry-isolation.test.ts` (`NUL` when `OSTYPE` is `msys` or `cygwin` and `cygpath` is on `PATH`, where native Bun cannot open `/dev/null`, #3415) and pass `bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"`, TypeScript spawns use `bunIsolationArgs()` from `lib/bun-isolation.ts`, and `bun build --compile` adds `--no-compile-autoload-dotenv --no-compile-autoload-bunfig`. `test/bun-entry-isolation.test.ts` enforces all four.
|
||||
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.227.0] - 2026-10-26
|
||||
|
||||
**Windows: gstack's record bins stop losing review, question, learning and timeline records when Git Bash path conversion is off (#3415).** Since GSTA-56 every shell bin started Bun with `--config=/dev/null`. Native `bun.exe` cannot open `/dev/null`: it exits 1 with `EINVAL` on Bun 1.3.10, 1.4.2 and 1.4.4 (measured on windows-latest). Git Bash hides this by rewriting the argument to `nul`, but only while MSYS path conversion is on. With it off (`MSYS_NO_PATHCONV` set to any value, even empty; `MSYS2_ARG_CONV_EXCL=*`; or Cygwin), `gstack-review-log`, `gstack-question-log` and `gstack-learnings-log` exited 1 with nothing written, and `gstack-timeline-log` exited 0 with nothing written.
|
||||
|
||||
### Fixed
|
||||
- **40 shipped shell files** (the bash bins, the Claude hook wrappers, `setup`, `scripts/build-cso.sh` and one migration) define `GSTACK_BUN_CONFIG` once and start Bun with `--config="${GSTACK_BUN_CONFIG:-/dev/null}"`. The value is `NUL` only when `OSTYPE` is `msys` or `cygwin` (Git Bash, MSYS2 and Cygwin, the check `gstack-doctor` already uses) and `cygpath` is on `PATH`; everywhere else it is `/dev/null`. The `cygpath` check is a builtin `command -v`, so it forks nothing. It means an `OSTYPE=msys` inherited by a Linux or macOS bash keeps `/dev/null`, instead of passing a relative `NUL` that Bun there resolves to a file in the repository. Bun 1.4.2 and later, the version gstack tests against, read `NUL` as an empty config whether or not conversion is on. Bun 1.3.10 on Windows exits 1 on `--config=NUL` and on the lowercase `nul` that Git Bash used to produce alike (measured from PowerShell on windows-latest; the #3085 entry recorded a panic). Versions between 1.3.3 and 1.4.1, which setup and `gstack-doctor` only warn about, were not measured. The `:-/dev/null` default covers a call that loses its definition: it still passes `/dev/null`, so POSIX is unchanged and Windows fails loudly as before, and never reads the repository's `bunfig.toml`. `bin/gstack-codex-probe` is sourced into the caller's shell, so it defines the variable inside the one function that starts Bun and still runs nothing at source time.
|
||||
- **`bin/gstack-timeline-log`** and **`gstack-developer-profile --log-session`**: invalid input still exits 0 with nothing logged (the hook-path contract). Their validators now print `valid` or `invalid`, and any non-zero exit, which is Bun itself failing, is reported on stderr and returned instead of being skipped as invalid. The verdict is not an exit code because a Bun crash on Windows exits 3. When stamping a missing `ts` fails, `gstack-timeline-log` keeps the original event instead of appending an empty line. Their production callers (`gstack-skill-start`, `gstack-skill-end` and the `/office-hours` templates) still discard stderr and the exit status, so the report shows only when the bin is run directly.
|
||||
- Unchanged: bun shebangs keep `--config=/dev/null` because a shebang cannot name a per-platform path, and skill prose (templates and resolvers) keeps it because `test/bun-entry-isolation.test.ts` holds skill sources to the literal spelling. TypeScript spawns keep `bunIsolationArgs()`, and `scripts/ubicloud/` keeps `/dev/null` because it runs on Linux.
|
||||
- Not covered: callers that hand `bun.exe` a POSIX script path built from `pwd` or `$HOME` (the Claude hook wrappers, `autoplan/bin/phase-publication-hook`, `plan-ceo-review/bin/mode-handoff-hook`, and some `gstack-doctor`, `gstack-codex-probe`, `setup` and migration calls) only got the config spelling. With path conversion off they may still fail on the `/c/...` script path itself. The record bins convert their own paths with `cygpath -m`; the others were not verified on Windows with conversion off. Bun shebang entry points and Bun calls in skill prose still pass the literal `--config=/dev/null`, so with conversion off they fail with `EINVAL` as before. One example is the outside-voice probe in `scripts/resolvers/outside-voice.ts`, which then reports Claude as not installed.
|
||||
|
||||
### Tests
|
||||
- `test/bun-config-shell-bins.test.ts` (new; force-included in the Windows lane):
|
||||
- On every platform, the four record bins run through `lib/script-invocation.ts` with conversion off in a repository whose `bunfig.toml` preload and `.env` are hostile. Each writes exactly one record, and neither the preload nor the `.env` state root takes effect. Invalid timeline input still exits 0 silently.
|
||||
- On POSIX hosts a simulated Windows bash (`OSTYPE=msys`, a `cygpath`, and a `bun` stand-in that rejects `/dev/null` with EINVAL as `bun.exe` does) reproduces the bug: on main the four bins write nothing and the timeline control exits 0 (5 failures); with the fix they pass. The controls, without `OSTYPE=msys`, show each bin exits non-zero with nothing written.
|
||||
- An inherited `OSTYPE=msys` with no `cygpath`, in a repository that commits a file named `NUL` with a preload, leaves each bin on `/dev/null`: the record is written and the preload never runs. This fails on the first version of this fix (4 failures).
|
||||
- `gstack-developer-profile --log-session` reports a rejected config on stderr and exits non-zero, and still skips invalid input silently. This fails on main.
|
||||
- A `bun` that exits 3 with no output, the way a Bun crash exits on Windows, makes both bins report and exit non-zero instead of skipping the event as invalid input. This fails with an exit-code verdict.
|
||||
- `test/bun-isolation-launch.test.ts`: the shell-file spelling stays isolated with conversion off, and a win32-only control shows that the literal `--config=/dev/null` fails there with EINVAL.
|
||||
- `test/bun-entry-isolation.test.ts`: shipped shell files must use the fail-closed spelling and define `GSTACK_BUN_CONFIG` with the canonical line before first use. A `local` definition covers only the rest of its function. Skill sources keep the literal spelling.
|
||||
- `test/learnings-injection.test.ts`, `browse/test/learnings-injection.test.ts` and `test/setup-hook-canonical-paths.test.ts` now anchor on the new spelling. The two learnings-injection files also check that the anchored `bun -e` block exists. Before, a missing anchor sliced the script's last character and the negative assertions passed without checking anything.
|
||||
|
||||
### Gate integrity
|
||||
- `test/bun-entry-isolation.test.ts` is strengthened. On POSIX the required shell spelling still resolves to `/dev/null`.
|
||||
- Now offenders: the literal `--config=/dev/null` in a shipped shell file, and an undefined or redefined `GSTACK_BUN_CONFIG`.
|
||||
- The shell-caller detector also skips a `--config` value before deciding whether a call starts a program, so `bun --no-env-file --config=<anything> -e` no longer passes unchecked. The tree has no such call.
|
||||
- Negative controls, each of which fails the check: the old spelling, a variable without the default, an empty `--config=`, a missing, late or altered definition, a `local` definition used from another function, and a reverted bin.
|
||||
- No threshold, budget, timeout, trial count or case kind changed.
|
||||
|
||||
## [1.91.226.0] - 2026-10-26
|
||||
|
||||
**/review can re-review just its own fixes after a fixing pass, under the same rules as /ship (#3339).** Before, every /review fix cycle reran the whole review over the whole branch, so a large branch could hit the three-cycle stop on findings in code the earlier passes had already read. Now the pass after an edited pass starts with `gstack-review-log --start review --fix-range` and its checklist, specialists and QA read only what changed since that pass. The Step 4.8 adversarial review still reads the whole branch on every pass, because its rows carry their own freshness. A narrowed pass is never the final review: when it makes no edits, /review runs one full pass before writing the final record, and a fix-range record is never CURRENT. Garry's "do it" on 2026-10-24 for a fix-range re-review with a final full pass covers it.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.226.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.227.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Native Read barrier. A broken install must return deny JSON, never silence.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
_AUTOPLAN_DECIDED=''
|
||||
_autoplan_backstop() {
|
||||
if [ -z "$_AUTOPLAN_DECIDED" ]; then
|
||||
@@ -9,7 +10,7 @@ _autoplan_backstop() {
|
||||
}
|
||||
trap _autoplan_backstop EXIT
|
||||
_AUTOPLAN_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
_AUTOPLAN_RESULT="$(bun --no-env-file --config=/dev/null "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
|
||||
_AUTOPLAN_RESULT="$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$_AUTOPLAN_HOOK_DIR/phase-publication-hook.ts")"
|
||||
test -n "$_AUTOPLAN_RESULT"
|
||||
printf '%s\n' "$_AUTOPLAN_RESULT"
|
||||
_AUTOPLAN_DECIDED=1
|
||||
@@ -77,6 +77,7 @@
|
||||
BASH_COMPAT=50
|
||||
|
||||
set -uo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
gstack_state_root_select
|
||||
@@ -576,7 +577,7 @@ PYEOF
|
||||
CONSENT_TS="$SCRIPT_DIR/../lib/transcript-consent.ts"
|
||||
transcript_policy() {
|
||||
command -v bun >/dev/null 2>&1 || return 2
|
||||
GSTACK_STATE_ROOT="$GSTACK_HOME" bun --no-env-file --config=/dev/null run "$CONSENT_TS" "$@"
|
||||
GSTACK_STATE_ROOT="$GSTACK_HOME" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$CONSENT_TS" "$@"
|
||||
}
|
||||
|
||||
# 0 when staged transcript pages may sync at all.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
# GSTACK_CHROMIUM_NO_SANDBOX=1 for the probe. Bash 3.2 plus POSIX tools.
|
||||
BASH_COMPAT=50
|
||||
set -u
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" || { echo "gstack-browser-ensure: cannot resolve the install root" >&2; exit 1; }
|
||||
DOCS="https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#browser-ensure-failed"
|
||||
@@ -58,7 +59,7 @@ probe() {
|
||||
else PROBE_OUT=$(cd "$ROOT" && node -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?; fi
|
||||
elif command -v bun >/dev/null 2>&1; then
|
||||
ENGINE=bun
|
||||
PROBE_OUT=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
|
||||
PROBE_OUT=$(cd "$ROOT" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$PROBE" </dev/null 2>&1); PROBE_RC=$?
|
||||
else
|
||||
fail "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then re-run $0"
|
||||
fi
|
||||
|
||||
@@ -157,6 +157,7 @@ _gstack_codex_select_model() {
|
||||
# working-directory context forwarded to the shared role resolver.
|
||||
# The TOML is read by the TypeScript resolver; bash never parses it.
|
||||
local _kind="${1:-exec}" _explicit="${2:-}" _role="${3:-}" _cwd="${4:-}" _line _rc _rest
|
||||
local GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
case "$_kind" in exec|review) ;; *) echo "ERROR: _gstack_codex_select_model takes exec or review" >&2; return 2 ;; esac
|
||||
case "$_role" in ''|plan-review) ;; *) echo "ERROR: _gstack_codex_select_model role must be plan-review" >&2; return 2 ;; esac
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
@@ -168,7 +169,7 @@ _gstack_codex_select_model() {
|
||||
[ -n "$_role" ] && set -- "$@" --role "$_role"
|
||||
[ -n "$_explicit" ] && set -- "$@" --explicit "$_explicit"
|
||||
[ -n "$_cwd" ] && set -- "$@" --cwd "$_cwd"
|
||||
_line=$(bun --no-env-file --config=/dev/null run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
|
||||
_line=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$_GSTACK_CODEX_BIN/../scripts/resolve-codex-generation-model.ts" "$@")
|
||||
_rc=$?
|
||||
_GSTACK_CODEX_SEL="${_line%%$'\t'*}"
|
||||
_rest="${_line#*$'\t'}"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# Writes via bin/gstack-question-log so source tagging, dedup, and async
|
||||
# derive all apply uniformly.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
gstack_state_root_select
|
||||
@@ -101,7 +102,7 @@ IMPORTED=0
|
||||
SKIPPED_NO_ANSWER=0
|
||||
|
||||
for SESSION_FILE in "${SESSION_FILES[@]}"; do
|
||||
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun --no-env-file --config=/dev/null -e '
|
||||
COUNT_LINE=$(SESSION_FILE_PATH="$SESSION_FILE" QLOG_BIN="$SCRIPT_DIR/gstack-question-log" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
# docs/designs/PLAN_TUNING_V0.md). Event file: ~/.gstack/projects/{SLUG}/
|
||||
# question-events.jsonl.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -70,7 +71,7 @@ do_migrate() {
|
||||
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
|
||||
cat "$LEGACY_FILE" | bun --no-env-file --config=/dev/null -e "
|
||||
cat "$LEGACY_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
||||
const sessions = [];
|
||||
const signalsAcc = {};
|
||||
@@ -127,7 +128,7 @@ do_migrate() {
|
||||
mv "$LEGACY_FILE" "$LEGACY_FILE.migrated-$TS"
|
||||
|
||||
local COUNT
|
||||
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun --no-env-file --config=/dev/null -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
|
||||
COUNT=$(PROFILE_FILE_PATH="$PROFILE_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "console.log(JSON.parse(require('fs').readFileSync(process.env.PROFILE_FILE_PATH,'utf-8')).sessions.length)" 2>/dev/null || echo "?")
|
||||
echo "MIGRATE: ok — migrated $COUNT sessions from builder-profile.jsonl"
|
||||
}
|
||||
|
||||
@@ -175,7 +176,8 @@ EOF
|
||||
# and update aggregates (signals_accumulated, resources_shown, topics).
|
||||
# Fix for #1671: the writer side of the v1.0.0.0 migration. Reader and
|
||||
# writer now share the same file.
|
||||
# Silent skip on invalid input (matches gstack-timeline-log:22-26 pattern).
|
||||
# Invalid input is skipped silently; a Bun failure is reported and returned,
|
||||
# as in gstack-timeline-log (#3415).
|
||||
# -----------------------------------------------------------------------
|
||||
do_log_session() {
|
||||
local INPUT="${1:-}"
|
||||
@@ -184,12 +186,23 @@ do_log_session() {
|
||||
fi
|
||||
|
||||
# Validate: input must be parseable JSON with required fields (date, mode).
|
||||
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
|
||||
const j = JSON.parse(await Bun.stdin.text());
|
||||
if (!j.date || !j.mode) process.exit(1);
|
||||
" 2>/dev/null; then
|
||||
return 0
|
||||
# The validator prints its verdict; a non-zero exit means Bun itself failed.
|
||||
local VALIDATE_RC=0 VALIDATE_OUT
|
||||
VALIDATE_OUT=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const text = await Bun.stdin.text();
|
||||
let j;
|
||||
try { j = JSON.parse(text); } catch {}
|
||||
console.log(j && j.date && j.mode ? 'valid' : 'invalid');
|
||||
") || VALIDATE_RC=$?
|
||||
if [ "$VALIDATE_RC" -ne 0 ]; then
|
||||
echo "gstack-developer-profile: bun exited $VALIDATE_RC before the session was validated; nothing was logged" >&2
|
||||
return "$VALIDATE_RC"
|
||||
fi
|
||||
case "$VALIDATE_OUT" in
|
||||
valid) ;;
|
||||
invalid) return 0 ;;
|
||||
*) echo "gstack-developer-profile: bun printed no verdict while validating the session; nothing was logged" >&2; return 1 ;;
|
||||
esac
|
||||
|
||||
ensure_profile
|
||||
|
||||
@@ -197,7 +210,7 @@ do_log_session() {
|
||||
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" RECORD_INPUT="$INPUT" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const fs = require('fs');
|
||||
const entry = JSON.parse(process.env.RECORD_INPUT);
|
||||
if (!entry.ts) entry.ts = new Date().toISOString();
|
||||
@@ -244,7 +257,7 @@ do_reconcile() {
|
||||
TMPOUT=$(mktemp "$GSTACK_HOME/developer-profile.json.tmp.XXXXXX")
|
||||
trap 'rm -f "$TMPOUT"' EXIT
|
||||
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config=/dev/null -e "
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" PROJECTS_DIR="$GSTACK_HOME/projects" TMPOUT_PATH="$TMPOUT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const profile = JSON.parse(fs.readFileSync(process.env.PROFILE_FILE_PATH, 'utf-8'));
|
||||
@@ -314,7 +327,7 @@ do_reconcile() {
|
||||
# -----------------------------------------------------------------------
|
||||
do_read() {
|
||||
ensure_profile
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const p = JSON.parse(await Bun.stdin.text());
|
||||
const sessions = p.sessions || [];
|
||||
|
||||
@@ -388,7 +401,7 @@ do_profile() {
|
||||
# -----------------------------------------------------------------------
|
||||
do_gap() {
|
||||
ensure_profile
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const p = JSON.parse(await Bun.stdin.text());
|
||||
const declared = p.declared || {};
|
||||
const inferred = (p.inferred && p.inferred.values) || {};
|
||||
@@ -417,7 +430,7 @@ do_derive() {
|
||||
fi
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun --no-env-file --config=/dev/null -e "
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" EVENTS_PATH="$EVENTS" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
import('./scripts/question-registry.ts').then(async (regmod) => {
|
||||
const sigmod = await import('./scripts/psychographic-signals.ts');
|
||||
const fs = require('fs');
|
||||
@@ -504,7 +517,7 @@ do_trace() {
|
||||
return 0
|
||||
fi
|
||||
cd "$ROOT_DIR"
|
||||
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun --no-env-file --config=/dev/null -e "
|
||||
EVENTS_PATH="$EVENTS" TRACE_DIM="$DIM" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
import('./scripts/question-registry.ts').then(async (regmod) => {
|
||||
const sigmod = await import('./scripts/psychographic-signals.ts');
|
||||
const fs = require('fs');
|
||||
@@ -542,7 +555,7 @@ do_trace() {
|
||||
# -----------------------------------------------------------------------
|
||||
do_check_mismatch() {
|
||||
ensure_profile
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config=/dev/null -e "
|
||||
cat "$PROFILE_FILE" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const p = JSON.parse(await Bun.stdin.text());
|
||||
const declared = p.declared || {};
|
||||
const inferred = (p.inferred && p.inferred.values) || {};
|
||||
@@ -586,7 +599,7 @@ do_narrative() {
|
||||
do_vibe() {
|
||||
ensure_profile
|
||||
cd "$ROOT_DIR"
|
||||
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun --no-env-file --config=/dev/null -e "
|
||||
cat "$PROFILE_FILE" | PROFILE_DATA="$(cat "$PROFILE_FILE")" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
import('./scripts/archetypes.ts').then(async (mod) => {
|
||||
const p = JSON.parse(process.env.PROFILE_DATA);
|
||||
const dims = (p.inferred && p.inferred.values) || {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
# gstack-distill-apply --proposal <N> --gbrain-published true
|
||||
# gstack-distill-apply --list # show pending proposals
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
gstack_state_root_select
|
||||
@@ -57,7 +58,7 @@ if [ ! -f "$PROPOSAL_FILE" ]; then
|
||||
fi
|
||||
|
||||
if [ "$ACTION" = "list" ]; then
|
||||
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun --no-env-file --config=/dev/null -e '
|
||||
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const p = JSON.parse(fs.readFileSync(process.env.PROPOSAL_FILE_PATH, "utf-8"));
|
||||
const proposals = p.proposals || [];
|
||||
@@ -100,7 +101,7 @@ MEMORY_FILE_PATH="$MEMORY_FILE" \
|
||||
PROFILE_FILE_PATH="$PROFILE_FILE" \
|
||||
PREF_BIN="$SCRIPT_DIR/gstack-question-preference" \
|
||||
GBRAIN_PUBLISHED="$GBRAIN_PUBLISHED" \
|
||||
bun --no-env-file --config=/dev/null -e '
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
const idx = parseInt(process.env.PROPOSAL_IDX, 10);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
# auditability via --status when you want it.
|
||||
# Per D6: Anthropic SDK direct call, fail-loud on missing ANTHROPIC_API_KEY.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
# Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a
|
||||
# pipe in the forked child before exec, with no reader on the other end. On
|
||||
@@ -59,7 +60,7 @@ esac
|
||||
# --- Status subcommand --------------------------------------------------
|
||||
|
||||
if [ "$MODE" = "status" ]; then
|
||||
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun --no-env-file --config=/dev/null -e '
|
||||
COST_LOG_PATH="$COST_LOG" SLUG_PATH="$SLUG" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const slug = process.env.SLUG_PATH;
|
||||
const path = process.env.COST_LOG_PATH;
|
||||
@@ -98,7 +99,7 @@ if [ ! -f "$LOG_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
|
||||
EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const lines = fs.readFileSync(process.env.LOG_FILE_PATH, "utf-8").trim().split("\n").filter(Boolean);
|
||||
const out = [];
|
||||
@@ -119,7 +120,7 @@ EVENTS_JSON=$(LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
')
|
||||
|
||||
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
|
||||
EVENT_COUNT=$(printf '%s' "$EVENTS_JSON" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const a = JSON.parse(await Bun.stdin.text()); console.log(a.length);')
|
||||
if [ "$EVENT_COUNT" -eq 0 ]; then
|
||||
echo "NO_FREE_TEXT: nothing to distill"
|
||||
exit 0
|
||||
@@ -172,7 +173,7 @@ if [ "$MODE" = "dry-run" ]; then
|
||||
echo "$DISTILL_PROMPT"
|
||||
echo
|
||||
echo "=== EVENTS ($EVENT_COUNT) ==="
|
||||
echo "$EVENTS_JSON" | bun --no-env-file --config=/dev/null -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
|
||||
echo "$EVENTS_JSON" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'console.log(JSON.stringify(JSON.parse(await Bun.stdin.text()), null, 2));'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -196,7 +197,7 @@ fi
|
||||
RESULT=$(EVENTS_JSON="$EVENTS_JSON" DISTILL_PROMPT="$DISTILL_PROMPT" \
|
||||
PROPOSAL_FILE_PATH="$PROPOSAL_FILE" LOG_FILE_PATH="$LOG_FILE" \
|
||||
ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
|
||||
bun --no-env-file --config=/dev/null --cwd "$ROOT_DIR" -e '
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" --cwd "$ROOT_DIR" -e '
|
||||
const fs = require("fs");
|
||||
const Anthropic = require("@anthropic-ai/sdk").default;
|
||||
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||||
|
||||
+2
-1
@@ -40,6 +40,7 @@
|
||||
BASH_COMPAT=50
|
||||
|
||||
set -u
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
usage() {
|
||||
sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'
|
||||
@@ -512,7 +513,7 @@ check_windows_bash() {
|
||||
row warn "windows bash" "not checked: bun not found" "install Bun (see the bun row), then rerun $BIN/gstack-doctor"
|
||||
return
|
||||
fi
|
||||
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
|
||||
out=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-windows-bash.ts" ${GSTACK_DOCTOR_BASH_PROBE:+--probe "$GSTACK_DOCTOR_BASH_PROBE"} </dev/null 2>&1)
|
||||
rc=$?
|
||||
out=$(printf '%s\n' "$out" | tr -d '\000-\010\013-\037')
|
||||
bash=$(printf '%s\n' "$out" | sed -n 's/^WINDOWS_BASH: //p' | head -1)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# remote's capy/support.env, `unknown` when its fetch failed, or ""), LIVE.
|
||||
# GSTACK_CAPY_WRAPPER_PATH (optional) names the wrapper a below-minimum fix runs.
|
||||
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
. "$BIN/gstack-doctor-components.sh" || { echo "gstack-doctor: $BIN/gstack-doctor-components.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
. "$BIN/gstack-install-registry.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-install-registry.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
. "$BIN/gstack-bun-version.sh" 2>/dev/null || { echo "gstack-doctor: $BIN/gstack-bun-version.sh is missing; reinstall with cd $SRC && ./setup" >&2; exit 1; }
|
||||
@@ -213,7 +214,7 @@ _dc_check_browser() {
|
||||
out=$(cd "$ROOT" && { command -v timeout >/dev/null 2>&1 && timeout 90 node -e "$script" </dev/null 2>&1 || node -e "$script" </dev/null 2>&1; }); rc=$?
|
||||
elif command -v bun >/dev/null 2>&1; then
|
||||
engine=bun
|
||||
out=$(cd "$ROOT" && bun --no-env-file --config=/dev/null -e "$script" </dev/null 2>&1); rc=$?
|
||||
out=$(cd "$ROOT" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$script" </dev/null 2>&1); rc=$?
|
||||
else
|
||||
_dc_row FAIL browser "neither node nor bun is on PATH to launch Chromium" "install Node.js (https://nodejs.org) or Bun, then $fix" BROWSER_UNAVAILABLE
|
||||
return
|
||||
@@ -329,7 +330,7 @@ _dc_check_pins() {
|
||||
return
|
||||
fi
|
||||
node=$(node --version </dev/null 2>/dev/null | clamp 'v0-9.' 20); node="${node#v}"
|
||||
out=$(bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
|
||||
out=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-runtime-pins.ts" --project "$project" ${_dc_bun:+--bun "$_dc_bun"} ${node:+--node "$node"} </dev/null 2>&1); rc=$?
|
||||
verdict=$(printf '%s\n' "$out" | sed -n 's/^PINS_VERDICT: //p' | tail -1 | clamp 'a-z' 8)
|
||||
summary=$(printf '%s\n' "$out" | sed -n 's/^PIN_SUMMARY: //p' | tr '\n' '|' | sed 's/|$//; s/|/; /g')
|
||||
case "$verdict" in
|
||||
@@ -357,7 +358,7 @@ _dc_check_pins() {
|
||||
fi
|
||||
fi
|
||||
if [ -n "$_dc_ln_dir" ] && [ "$_dc_ln_ver" != "$_dc_bun" ] \
|
||||
&& bun --no-env-file --config=/dev/null "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
|
||||
&& bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$BIN/gstack-runtime-pins.ts" --project "$project" --bun "$_dc_ln_ver" ${node:+--node "$node"} </dev/null 2>/dev/null | grep -q '^PINS_VERDICT: pass'; then
|
||||
note="; bun $_dc_ln_ver found at $_dc_ln_dir (not on PATH) satisfies them" fix="$_dc_ln_fix"
|
||||
fi
|
||||
_dc_row FAIL pins "$failline$note" "$fix" PROJECT_PIN_MISMATCH ;;
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
# 2 — prerequisite missing or invalid argument
|
||||
# 3 — post-install validation failed (PATH shadow, broken binary, etc.)
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
# --- defaults ---
|
||||
# No version pin by default — install the latest default-branch HEAD (#1744).
|
||||
@@ -100,7 +101,7 @@ fi
|
||||
# bun, which this installer already requires. It used to need jq, which it
|
||||
# never checked for, so a successful install reported failure (#1675).
|
||||
pkg_field() {
|
||||
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun --no-env-file --config=/dev/null -e '
|
||||
GSTACK_PKG_JSON="$1" GSTACK_PKG_FIELD="$2" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
try {
|
||||
const pkg = JSON.parse(require("fs").readFileSync(process.env.GSTACK_PKG_JSON, "utf8"));
|
||||
const v = process.env.GSTACK_PKG_FIELD.split(".").reduce((o, k) => (o == null ? o : o[k]), pkg);
|
||||
@@ -198,7 +199,7 @@ _bun_exe_hint() {
|
||||
[ "$IS_WINDOWS" -eq 1 ] || return 0
|
||||
command -v bun.exe >/dev/null 2>&1 && return 0
|
||||
local real_bun
|
||||
real_bun=$(bun --no-env-file --config=/dev/null -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
|
||||
real_bun=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'console.log(process.execPath)' 2>/dev/null | tr -d '\r' || true)
|
||||
echo " detected: bun was installed via npm — bun.exe is NOT on %PATH%, and the gbrain.exe shim needs it." >&2
|
||||
if [ -n "$real_bun" ]; then
|
||||
echo " fix: add bun.exe's directory to PATH (persist it in your shell profile):" >&2
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
# Depends on: bun (already a gstack dependency), gbrain, git.
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CONFIG_BIN="$SCRIPT_DIR/gstack-config"
|
||||
@@ -222,7 +223,7 @@ ensure_worktree() {
|
||||
# is not JSON.
|
||||
source_local_path() {
|
||||
gbrain sources list --json 2>/dev/null \
|
||||
| GB_SOURCE_ID="$1" bun --no-env-file --config=/dev/null -e '
|
||||
| GB_SOURCE_ID="$1" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const raw = await Bun.stdin.text();
|
||||
try {
|
||||
const s = (JSON.parse(raw).sources || []).find((x) => x.id === process.env.GB_SOURCE_ID);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
# (needed when a hook runs TypeScript) is not found. Set GSTACK_HOOK_CHECK_BUN to
|
||||
# the bun to run (setup passes its own).
|
||||
set -u
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
LIST_ONLY=0 SETUP_FILE=""
|
||||
while [ $# -gt 1 ]; do
|
||||
@@ -173,7 +174,7 @@ if [ -n "$ENTRIES" ]; then
|
||||
# up below (lib/path-spelling.ts shellPath; test/path-spelling.test.ts keeps
|
||||
# the two equal).
|
||||
# shellcheck disable=SC2086 # newline-split list (IFS is set to a newline below)
|
||||
(cd "$ROOT" && IFS=$NL && "$BUN" --no-env-file --config=/dev/null -e '
|
||||
(cd "$ROOT" && IFS=$NL && "$BUN" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const shellPath = (value) => process.platform === "win32"
|
||||
? String(value).replace(/\\/g, "/").replace(/^([A-Za-z]):(?=\/|$)/, (_, drive) => `/${drive.toLowerCase()}`)
|
||||
: String(value);
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
# spawned sessions (ambient OpenClaw) only warn. GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1
|
||||
# admits anyway and says so.
|
||||
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
# gstack_source_root BIN_DIR — the source checkout behind an install: an
|
||||
# installed runtime root's recorded .source-path (setup _link_runtime_dists),
|
||||
# else BIN_DIR/.. resolved. Prints the realpath; returns 1 when unresolvable.
|
||||
@@ -159,7 +161,7 @@ gstack_install_admission() {
|
||||
v_bun=$(gstack_install_verdict_field "$file" bun_path)
|
||||
if [ -z "$detail" ] && [ -n "$v_bun" ]; then
|
||||
running=$(gstack_realpath "$where" 2>/dev/null)
|
||||
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config=/dev/null -p process.execPath </dev/null 2>/dev/null | head -1)
|
||||
_gstack_bun_is_shim "$where" && running=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -p process.execPath </dev/null 2>/dev/null | head -1)
|
||||
if [ -n "$running" ] && [ "$running" != "$v_bun" ]; then
|
||||
_ia_status=runtime code=BUN_RESOLUTION_DRIFT
|
||||
detail="bun $found at $running is running, but the install verdict recorded bun $(gstack_install_verdict_field "$file" bun_version) at $v_bun"
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
# session tokens are outstanding.
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
# The --help heredoc is over 512 bytes; keep it off a pipe (test/heredoc-pipe-deadlock.test.ts).
|
||||
BASH_COMPAT=50
|
||||
|
||||
@@ -67,4 +68,4 @@ if ! command -v bun >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$ENTRY" "$@"
|
||||
@@ -10,6 +10,7 @@
|
||||
# Allowlist file: ~/.gstack/ios-qa-allowlist.json (mode 0600).
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GSTACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -25,4 +26,4 @@ if ! command -v bun >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec bun --no-env-file --config=/dev/null run "$ENTRY" "$@"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$ENTRY" "$@"
|
||||
@@ -3,6 +3,7 @@
|
||||
# package and the app-owned typed state accessors.
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -144,7 +145,7 @@ do
|
||||
fi
|
||||
done
|
||||
|
||||
bun --no-env-file --config=/dev/null run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run "$GENERATOR" --input "$APP_SOURCE" --output "$GENERATED_DIR"
|
||||
|
||||
# Stamp only after successful accessor generation. ios-sync uses this marker
|
||||
# to distinguish a complete current install from an interrupted regeneration.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# Append-only storage. Duplicates (same key+type) are resolved at read time
|
||||
# by gstack-learnings-search ("latest winner" per key+type).
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Windows git-bash (#1950): pwd yields a POSIX path (/c/Users/...), which Bun
|
||||
# on Windows cannot resolve as an ES module specifier in the import below.
|
||||
@@ -27,7 +28,7 @@ INPUT="$1"
|
||||
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-learnings-log.XXXXXX") || { echo "gstack-learnings-log: could not create a temp file in ${TMPDIR:-/tmp}, so the learning was not recorded. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
|
||||
trap 'rm -f "$TMPERR"' EXIT
|
||||
set +e
|
||||
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
|
||||
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
|
||||
const raw = await Bun.stdin.text();
|
||||
let j;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# exits 127 and a failed read exits non-zero with its stderr (#2790): "could
|
||||
# not read" must never look like "nothing recorded".
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
@@ -69,7 +70,7 @@ emit_tagged_file() {
|
||||
emit_tagged_file cross "$f"
|
||||
done
|
||||
fi
|
||||
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun --no-env-file --config=/dev/null -e "
|
||||
} | GSTACK_SEARCH_TYPE="$TYPE" GSTACK_SEARCH_QUERY="$QUERY" GSTACK_SEARCH_LIMIT="$LIMIT" GSTACK_SEARCH_CROSS="$CROSS_PROJECT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
||||
const now = Date.now();
|
||||
const type = process.env.GSTACK_SEARCH_TYPE || '';
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
# Heredoc delivery guard (see bin/gstack-settings-hook for the rationale).
|
||||
BASH_COMPAT=50
|
||||
set -uo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -374,7 +375,7 @@ status_bridge() {
|
||||
# JSON; a failed query is reported as unknown, never as an empty history.
|
||||
local egress_json
|
||||
if egress_json="$("$EGRESS_BIN" list --sink "$SINK" --json 2>/dev/null)"; then
|
||||
n="$(printf '%s' "$egress_json" | bun --no-env-file --config=/dev/null -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
|
||||
n="$(printf '%s' "$egress_json" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
|
||||
case "$n" in *[!0-9]*|"") n="unknown (could not parse gstack-egress output)" ;; esac
|
||||
else
|
||||
n="unknown (gstack-egress list failed; run it yourself)"
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#
|
||||
# Append-only JSONL. Dedup is at read time in gstack-question-sensitivity --read-log.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Windows git-bash (#1950): pwd yields a POSIX path (/c/Users/...), which Bun
|
||||
# on Windows cannot resolve as an ES module specifier in bun -e imports.
|
||||
@@ -51,7 +52,7 @@ INPUT="$1"
|
||||
TMPERR=$(mktemp "${TMPDIR:-/tmp}/gstack-question-log.XXXXXX") || { echo "gstack-question-log: could not create a temp file in ${TMPDIR:-/tmp}, so the question was not logged. Fix: point TMPDIR at a writable directory." >&2; exit 1; }
|
||||
trap 'rm -f "$TMPERR"' EXIT
|
||||
set +e
|
||||
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" GSTACK_SCRIPTS_DIR="$SCRIPT_DIR/../scripts" bun --no-env-file --config=/dev/null -e "
|
||||
VALIDATED=$(printf '%s' "$INPUT" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" GSTACK_SCRIPTS_DIR="$SCRIPT_DIR/../scripts" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const { hasInjection } = await import(process.env.GSTACK_LIB_DIR + '/jsonl-store.ts');
|
||||
const { resolveOptionKey } = await import(process.env.GSTACK_SCRIPTS_DIR + '/psychographic-signals.ts');
|
||||
const path = require('path');
|
||||
@@ -241,7 +242,7 @@ LOG_FILE="$GSTACK_HOME/projects/$SLUG/question-log.jsonl"
|
||||
# Lookup is bounded so the bin stays cheap on hot paths.
|
||||
DEDUP_SKIP=""
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config=/dev/null -e '
|
||||
DEDUP_SKIP=$(VALIDATED_JSON="$VALIDATED" LOG_FILE_PATH="$LOG_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const fs = require("fs");
|
||||
const j = JSON.parse(process.env.VALIDATED_JSON);
|
||||
if (!j.tool_use_id) { console.log(""); process.exit(0); }
|
||||
@@ -261,7 +262,7 @@ if [ -f "$LOG_FILE" ]; then
|
||||
fi
|
||||
|
||||
if [ "$DEDUP_SKIP" = "dup" ]; then
|
||||
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun --no-env-file --config=/dev/null -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
|
||||
echo "DEDUP: skipped (source=$(echo "$VALIDATED" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e 'const j=JSON.parse(await Bun.stdin.text()); console.log(j.source);'), tool_use_id duplicate)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
# --check already ignores those prefs; storing them made --stats lie.
|
||||
# always-ask on a one-way id is fine (it agrees with the safety override).
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
@@ -105,7 +106,7 @@ do_check() {
|
||||
fi
|
||||
ensure_file
|
||||
cd "$ROOT_DIR"
|
||||
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun --no-env-file --config=/dev/null -e "
|
||||
PREF_FILE_PATH="$PREF_FILE" GLOBAL_PREF_FILE_PATH="$GLOBAL_PREF_FILE" QID="$QID" QSUMMARY="$QSUMMARY" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
import('./scripts/one-way-doors.ts').then((oneway) => {
|
||||
const fs = require('fs');
|
||||
const qid = process.env.QID;
|
||||
@@ -201,7 +202,7 @@ do_write() {
|
||||
|
||||
set +e
|
||||
local RESULT
|
||||
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun --no-env-file --config=/dev/null -e "
|
||||
RESULT=$(cd "$ROOT_DIR" && printf '%s' "$INPUT" | PREF_FILE_PATH="$PREF_FILE" EVENT_FILE_PATH="$EVENT_FILE" FREE_TEXT="$FREE_TEXT" HAS_FREE_TEXT="$HAS_FREE_TEXT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const fs = require('fs');
|
||||
const raw = await Bun.stdin.text();
|
||||
let j;
|
||||
@@ -320,7 +321,7 @@ do_clear() {
|
||||
echo '{}' > "$PREF_FILE"
|
||||
echo "OK: cleared all preferences"
|
||||
else
|
||||
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun --no-env-file --config=/dev/null -e "
|
||||
PREF_FILE_PATH="$PREF_FILE" QID="$QID" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const fs = require('fs');
|
||||
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
|
||||
if (prefs[process.env.QID] !== undefined) {
|
||||
@@ -339,7 +340,7 @@ do_clear() {
|
||||
# -----------------------------------------------------------------------
|
||||
do_stats() {
|
||||
ensure_file
|
||||
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun --no-env-file --config=/dev/null -e "
|
||||
(cd "$ROOT_DIR" && PREF_FILE_PATH="$PREF_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
import('./scripts/one-way-doors.ts').then((oneway) => {
|
||||
const fs = require('fs');
|
||||
const prefs = JSON.parse(fs.readFileSync(process.env.PREF_FILE_PATH, 'utf-8'));
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@
|
||||
# GSTACK_INSTALL_DIR — override gstack install directory
|
||||
# GSTACK_SKILLS_DIR — override target skills directory
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GSTACK_CONFIG="${SCRIPT_DIR}/gstack-config"
|
||||
@@ -474,7 +475,7 @@ if { [ "$DISABLED" != " " ] || grep -q '^Disabled on this install' "$_ROUTER" 2>
|
||||
_ROUTER_TMP="$(mktemp -d "${TMPDIR:-/tmp}/gstack-router.XXXXXX")" || _ROUTER_TMP=""
|
||||
_DISABLED_CSV="$(printf '%s' "$DISABLED" | xargs | tr ' ' ',')"
|
||||
if [ -n "$_ROUTER_TMP" ] && command -v bun >/dev/null 2>&1 \
|
||||
&& (cd "$_GEN_SRC" && bun --no-env-file --config=/dev/null run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
|
||||
&& (cd "$_GEN_SRC" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run scripts/gen-skill-docs.ts --host claude --out-dir "$_ROUTER_TMP" ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} ${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"} >/dev/null 2>&1) \
|
||||
&& [ -s "$_ROUTER_TMP/SKILL.md" ] && cp "$_ROUTER_TMP/SKILL.md" "$_ROUTER.tmp.$$" && mv "$_ROUTER.tmp.$$" "$_ROUTER"; then
|
||||
_link_root_skill_alias
|
||||
[ -n "$_DISABLED_CSV" ] && echo "Router: disabled skills left out of $_ROUTER"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# a model read the code. Caller-supplied binding fields are always discarded.
|
||||
# Plan-tier rows retain their legacy binding behavior.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
export GIT_OPTIONAL_LOCKS=0
|
||||
export GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'core.fsmonitor=false' 'core.untrackedCache=false'"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -77,7 +78,7 @@ SESSION_KIND=$("$SCRIPT_DIR/gstack-session-kind" 2>/dev/null || echo interactive
|
||||
export GSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" GSTACK_STAMP_TREE="$TREE" GSTACK_STAMP_WTREE="$WTREE" GSTACK_STAMP_DIRTY="$DIRTY" GSTACK_STAMP_SESSION_KIND="$SESSION_KIND"
|
||||
if [ "$INPUT" = --start ]; then
|
||||
case "${3:-}" in ''|--fix-range) ;; *) echo 'Usage: gstack-review-log --start SKILL [--fix-range]' >&2; exit 2 ;; esac
|
||||
GSTACK_REVIEW_SKILL="${2:-}" GSTACK_REVIEW_FIX_RANGE="${3:-}" bun --no-env-file --config=/dev/null -e '
|
||||
GSTACK_REVIEW_SKILL="${2:-}" GSTACK_REVIEW_FIX_RANGE="${3:-}" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const { captureReviewStart } = await import(process.env.GSTACK_REVIEW_LIB);
|
||||
try { console.log(captureReviewStart(process.env.GSTACK_REVIEW_SKILL, process.env, process.env.GSTACK_REVIEW_FIX_RANGE === "--fix-range")); }
|
||||
catch (e) { console.error(e.message); process.exit(1); }
|
||||
@@ -85,7 +86,7 @@ if [ "$INPUT" = --start ]; then
|
||||
exit $?
|
||||
fi
|
||||
if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then
|
||||
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config=/dev/null -e '
|
||||
GSTACK_REVIEW_TOKEN="$2" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const { checkSharedLibsReuse } = await import(process.env.GSTACK_REVIEW_LIB);
|
||||
console.log(JSON.stringify(checkSharedLibsReuse(JSON.parse(await Bun.stdin.text()), process.env.GSTACK_REVIEW_TOKEN)));
|
||||
'
|
||||
@@ -113,7 +114,7 @@ case "$INPUT" in --*) usage ;; esac
|
||||
# Caller values for the binding keys are dropped before stamping. A binding or
|
||||
# validation failure reaches stderr as its own line (never folded into
|
||||
# "invalid JSON"), and exits 1 with nothing written.
|
||||
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config=/dev/null -e "
|
||||
STAMPED=$(printf '%s' "$INPUT" | GSTACK_REVIEW_TOKEN="$TOKEN" GSTACK_REVIEW_FINDINGS="$FINDINGS" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const { bindReview } = await import(process.env.GSTACK_REVIEW_LIB);
|
||||
let rec;
|
||||
try { rec = JSON.parse(await Bun.stdin.text()); } catch (e) { console.error('gstack-review-log: invalid JSON, skipping (' + e.message + ')'); process.exit(1); }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# rows using review_freshness; legacy or uncaptured diff evidence cannot fall
|
||||
# back to HEAD equality. Plan rows are unchanged.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && SCRIPT_DIR="$(cygpath -m "$SCRIPT_DIR")" ;;
|
||||
@@ -19,7 +20,7 @@ gstack_state_root_select
|
||||
GSTACK_HOME="$_gstack_sr_root"
|
||||
WTREE=$("$SCRIPT_DIR/gstack-wtree" 2>/dev/null || echo "unknown")
|
||||
if [ -f "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" ]; then
|
||||
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun --no-env-file --config=/dev/null -e '
|
||||
GSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts" GSTACK_REVIEW_WTREE="$WTREE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '
|
||||
const { reviewFreshness } = await import(process.env.GSTACK_REVIEW_LIB);
|
||||
for (const line of (await Bun.stdin.text()).split("\n").filter(Boolean)) {
|
||||
try {
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
BASH_COMPAT=50
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
ACTION="${1:-}"
|
||||
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json}"
|
||||
@@ -307,7 +308,7 @@ function gsWriteIfChanged(path, beforeText, settings, existed) {
|
||||
|
||||
GSTACK_SETTINGS_INPUT="$SETTINGS_FILE"
|
||||
export GSTACK_SETTINGS_INPUT
|
||||
SETTINGS_FILE=$(bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
SETTINGS_FILE=$(bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
process.stdout.write(gsResolveSettingsPath(process.env.GSTACK_SETTINGS_INPUT));
|
||||
});') || exit $?
|
||||
|
||||
@@ -405,7 +406,7 @@ case "$ACTION" in
|
||||
fi
|
||||
_acquire_lock || exit 5
|
||||
_mutation_env
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_HOOK_CMD="$HOOK_CMD" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_HOOK_CMD="$HOOK_CMD" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const hookCmd = process.env.GSTACK_HOOK_CMD;
|
||||
const loaded = gsLoadSettings(settingsPath);
|
||||
@@ -435,7 +436,7 @@ case "$ACTION" in
|
||||
[ -f "$SETTINGS_FILE" ] || exit 1
|
||||
_acquire_lock || exit 5
|
||||
_mutation_env
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const loaded = gsLoadSettings(settingsPath);
|
||||
const settings = loaded.settings;
|
||||
@@ -505,7 +506,7 @@ case "$ACTION" in
|
||||
GSTACK_TIMEOUT="$TIMEOUT" \
|
||||
GSTACK_DIFF_ONLY="$DIFF_ONLY" \
|
||||
GSTACK_ENSURE="$ENSURE" \
|
||||
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const event = process.env.GSTACK_EVENT;
|
||||
const cmd = process.env.GSTACK_COMMAND;
|
||||
@@ -660,7 +661,7 @@ case "$ACTION" in
|
||||
[ -f "$SETTINGS_FILE" ] || exit 0
|
||||
_acquire_lock || exit 5
|
||||
_mutation_env
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_SOURCE="$SOURCE" GSTACK_RS_EVENT="$RS_EVENT" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_SOURCE="$SOURCE" GSTACK_RS_EVENT="$RS_EVENT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const source = process.env.GSTACK_SOURCE;
|
||||
const onlyEvent = process.env.GSTACK_RS_EVENT || "";
|
||||
@@ -763,7 +764,7 @@ case "$ACTION" in
|
||||
GSTACK_PRUNE_ALL="$PRUNE_ALL" \
|
||||
GSTACK_PT_OPTOUT="$GSTACK_PT_OPTOUT" \
|
||||
GSTACK_SWEEP_EXCLUDE_SOURCES="${GSTACK_SWEEP_EXCLUDE_SOURCES:-}" \
|
||||
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const root = (process.env.GSTACK_REPOINT_ROOT || "").replace(/\/+$/, "");
|
||||
const all = process.env.GSTACK_PRUNE_ALL === "1";
|
||||
@@ -902,7 +903,7 @@ case "$ACTION" in
|
||||
fi
|
||||
_mutation_env
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_RESTORE_PATH="$LATEST" \
|
||||
bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
var fs = require("fs");
|
||||
var target = process.env.GSTACK_SETTINGS_PATH;
|
||||
var backup = process.env.GSTACK_RESTORE_PATH;
|
||||
@@ -944,7 +945,7 @@ case "$ACTION" in
|
||||
exit 1
|
||||
fi
|
||||
[ -f "$SETTINGS_FILE" ] || exit 0
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_LI_EVENT="$LI_EVENT" GSTACK_LI_OWNED_BY="$LI_OWNED_BY" GSTACK_LI_CMD_RE="$LI_CMD_RE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" GSTACK_LI_EVENT="$LI_EVENT" GSTACK_LI_OWNED_BY="$LI_OWNED_BY" GSTACK_LI_CMD_RE="$LI_CMD_RE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const event = process.env.GSTACK_LI_EVENT;
|
||||
const ownedBy = process.env.GSTACK_LI_OWNED_BY || "";
|
||||
const reSrc = process.env.GSTACK_LI_CMD_RE || "";
|
||||
@@ -982,7 +983,7 @@ case "$ACTION" in
|
||||
|
||||
list-sources)
|
||||
[ -f "$SETTINGS_FILE" ] || { echo "(no settings file)"; exit 0; }
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'gsMain(function () {
|
||||
const fs = require("fs");
|
||||
let settings = {};
|
||||
try { settings = JSON.parse(fs.readFileSync(process.env.GSTACK_SETTINGS_PATH, "utf8")); }
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# Fix-range passes (scope "fix-range", #3339) are skipped: a narrowed dispatch
|
||||
# says nothing about a specialist's hit rate on a whole branch.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
@@ -33,7 +34,7 @@ if [ -z "$COMBINED" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s' "$COMBINED" | bun --no-env-file --config=/dev/null -e "
|
||||
printf '%s' "$COMBINED" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
||||
const NEVER_GATE = new Set(['security', 'data-migration', 'state-machines']);
|
||||
const stats = {};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
BASH_COMPAT=50
|
||||
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
MODE="${1:-}"
|
||||
|
||||
@@ -192,7 +193,7 @@ HOOK_EOF
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo " ! bun not found — manually add this PreToolUse hook (matcher Skill|skill, \"onFailure\": \"block\") to .claude/settings.json:"
|
||||
echo " $HOOK_COMMAND"
|
||||
elif GSTACK_SETTINGS_PATH="$SETTINGS" GSTACK_HOOK_COMMAND="$HOOK_COMMAND" bun --no-env-file --config=/dev/null -e "
|
||||
elif GSTACK_SETTINGS_PATH="$SETTINGS" GSTACK_HOOK_COMMAND="$HOOK_COMMAND" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const fs = require('fs');
|
||||
const settingsPath = process.env.GSTACK_SETTINGS_PATH;
|
||||
const handler = { type: 'command', command: process.env.GSTACK_HOOK_COMMAND, onFailure: 'block' };
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#
|
||||
# NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero
|
||||
set -uo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
SCRIPT_DIR="$GSTACK_DIR/bin"
|
||||
@@ -205,7 +206,7 @@ ERR_FIELD="null"
|
||||
# look like a JSON string, the whole message becomes null — never raw.
|
||||
ERR_MSG_FIELD="null"
|
||||
if [ -n "$ERROR_MESSAGE" ]; then
|
||||
ERR_MSG_FIELD="$(printf '%s' "$ERROR_MESSAGE" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config=/dev/null -e "
|
||||
ERR_MSG_FIELD="$(printf '%s' "$ERROR_MESSAGE" | GSTACK_LIB_DIR="$SCRIPT_DIR/../lib" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const { redactFindingSpans } = await import(process.env.GSTACK_LIB_DIR + '/redact-engine.ts');
|
||||
const input = await Bun.stdin.text();
|
||||
const out = redactFindingSpans(input, { repoVisibility: 'private' });
|
||||
|
||||
+22
-9
@@ -10,6 +10,7 @@
|
||||
# Optional: branch, outcome, duration_s, session, ts.
|
||||
# Validation failure → skip silently (non-blocking).
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
||||
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
||||
@@ -19,21 +20,33 @@ mkdir -p "$GSTACK_HOME/projects/$SLUG"
|
||||
|
||||
INPUT="$1"
|
||||
|
||||
# Validate: input must be parseable JSON with required fields
|
||||
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
|
||||
const j = JSON.parse(await Bun.stdin.text());
|
||||
if (!j.skill || !j.event) process.exit(1);
|
||||
" 2>/dev/null; then
|
||||
exit 0 # skip silently, non-blocking
|
||||
# Validate: input must be parseable JSON with required fields. The validator
|
||||
# prints its verdict; a non-zero exit means Bun itself failed (#3415). An exit
|
||||
# code cannot carry the verdict: a Bun crash on Windows exits 3.
|
||||
VALIDATE_RC=0
|
||||
VALIDATE_OUT=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const text = await Bun.stdin.text();
|
||||
let j;
|
||||
try { j = JSON.parse(text); } catch {}
|
||||
console.log(j && j.skill && j.event ? 'valid' : 'invalid');
|
||||
") || VALIDATE_RC=$?
|
||||
if [ "$VALIDATE_RC" -ne 0 ]; then
|
||||
echo "gstack-timeline-log: bun exited $VALIDATE_RC before the event was validated; nothing was logged" >&2
|
||||
exit "$VALIDATE_RC"
|
||||
fi
|
||||
case "$VALIDATE_OUT" in
|
||||
valid) ;;
|
||||
invalid) exit 0 ;; # skip silently, non-blocking
|
||||
*) echo "gstack-timeline-log: bun printed no verdict while validating the event; nothing was logged" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Inject timestamp if not present
|
||||
if ! printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "const j=JSON.parse(await Bun.stdin.text()); if(!j.ts) process.exit(1)" 2>/dev/null; then
|
||||
INPUT=$(printf '%s' "$INPUT" | bun --no-env-file --config=/dev/null -e "
|
||||
if ! printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "const j=JSON.parse(await Bun.stdin.text()); if(!j.ts) process.exit(1)" 2>/dev/null; then
|
||||
STAMPED=$(printf '%s' "$INPUT" | bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const j = JSON.parse(await Bun.stdin.text());
|
||||
j.ts = new Date().toISOString();
|
||||
console.log(JSON.stringify(j));
|
||||
" 2>/dev/null) || true
|
||||
" 2>/dev/null) && [ -n "$STAMPED" ] && INPUT=$STAMPED || true
|
||||
fi
|
||||
|
||||
echo "$INPUT" >> "$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# --branch, or an unknown argument exits 2 before any read (#1723); --help
|
||||
# prints the usage line and exits 0.
|
||||
set -euo pipefail
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
USAGE='Usage: gstack-timeline-read [--since "7 days ago"|7d|YYYY-MM-DD] [--limit N] [--branch NAME]'
|
||||
SINCE_FORMS='accepted: "<N> minute|hour|day|week|month[s] ago", "<N>h|d|w", or an ISO date YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ'
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -108,7 +109,7 @@ command -v bun >/dev/null 2>&1 || {
|
||||
exit 127
|
||||
}
|
||||
|
||||
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE_MS="$SINCE_MS" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun --no-env-file --config=/dev/null -e "
|
||||
cat "$TIMELINE_FILE" | GSTACK_TIMELINE_SINCE_MS="$SINCE_MS" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "
|
||||
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
||||
const branch = process.env.GSTACK_TIMELINE_BRANCH || '';
|
||||
const limit = Number(process.env.GSTACK_TIMELINE_LIMIT);
|
||||
|
||||
@@ -6,17 +6,22 @@ import { spawnSync } from 'child_process';
|
||||
const SCRIPT_PATH = path.join(import.meta.dir, '../../bin/gstack-learnings-search');
|
||||
const SCRIPT = fs.readFileSync(SCRIPT_PATH, 'utf-8');
|
||||
const BIN_DIR = path.join(import.meta.dir, '../../bin');
|
||||
const BUN_BLOCK_AT = SCRIPT.indexOf('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e');
|
||||
|
||||
describe('gstack-learnings-search injection safety', () => {
|
||||
it('finds the bun -e block the checks below read', () => {
|
||||
expect(BUN_BLOCK_AT).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('must not interpolate variables into JS string literals', () => {
|
||||
const jsBlock = SCRIPT.slice(SCRIPT.indexOf('bun --no-env-file --config=/dev/null -e'));
|
||||
const jsBlock = SCRIPT.slice(BUN_BLOCK_AT);
|
||||
expect(jsBlock).not.toMatch(/const \w+ = '\$\{/);
|
||||
expect(jsBlock).not.toMatch(/= \$\{[A-Z_]+\};/);
|
||||
expect(jsBlock).not.toMatch(/'\$\{CROSS_PROJECT\}'/);
|
||||
});
|
||||
|
||||
it('must use process.env for parameters', () => {
|
||||
const jsBlock = SCRIPT.slice(SCRIPT.indexOf('bun --no-env-file --config=/dev/null -e'));
|
||||
const jsBlock = SCRIPT.slice(BUN_BLOCK_AT);
|
||||
expect(jsBlock).toContain('process.env');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
# shared old renders are retired. This post-setup pass repairs missed upgrades.
|
||||
# Idempotent and non-fatal: foreign entries and failed replacements stay intact.
|
||||
set -u
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
INSTALL_DIR="${GSTACK_INSTALL_DIR:-$HOME/.claude/skills/gstack}"
|
||||
if [ -f "$INSTALL_DIR/bin/gstack-migrate-claude-code" ] && command -v bun >/dev/null 2>&1; then
|
||||
bun --no-env-file --config=/dev/null "$INSTALL_DIR/bin/gstack-migrate-claude-code" --install-dir "$INSTALL_DIR" || true
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$INSTALL_DIR/bin/gstack-migrate-claude-code" --install-dir "$INSTALL_DIR" || true
|
||||
fi
|
||||
exit 0
|
||||
@@ -3,5 +3,6 @@
|
||||
# wrapper makes the TypeScript hook executable via bun. Settings.json
|
||||
# references this file directly.
|
||||
set -e
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bun --no-env-file --config=/dev/null "$HERE/auq-error-fallback-hook.ts"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/auq-error-fallback-hook.ts"
|
||||
@@ -12,8 +12,9 @@
|
||||
# vendor process running with the prompt on its stdin. Forwarded, the signal
|
||||
# reaches the .ts, which kills the vendor's process group on its way out.
|
||||
# `<&0` keeps stdin: bash hands background jobs /dev/null otherwise.
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0
|
||||
bun --no-env-file --config=/dev/null "$HERE/memorable-user-prompt-hook.ts" <&0 &
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/memorable-user-prompt-hook.ts" <&0 &
|
||||
_child=$!
|
||||
trap 'kill -TERM "$_child" 2>/dev/null' TERM INT HUP
|
||||
wait "$_child" 2>/dev/null || wait "$_child" 2>/dev/null || true
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
# wrapper makes the TypeScript hook executable via bun. Settings.json
|
||||
# references this file directly.
|
||||
set -e
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bun --no-env-file --config=/dev/null "$HERE/question-log-hook.ts"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-log-hook.ts"
|
||||
@@ -3,5 +3,6 @@
|
||||
# wrapper makes the TypeScript hook executable via bun. Settings.json
|
||||
# references this file directly.
|
||||
set -e
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bun --no-env-file --config=/dev/null "$HERE/question-preference-hook.ts"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/question-preference-hook.ts"
|
||||
@@ -5,6 +5,7 @@
|
||||
#
|
||||
# FAIL-OPEN (F5): a Stop-event telemetry repair must never block the session.
|
||||
# Every failure path — bun missing, script crash — still exits 0.
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)" || exit 0
|
||||
bun --no-env-file --config=/dev/null "$HERE/timeline-stop-hook.ts" || true
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/timeline-stop-hook.ts" || true
|
||||
exit 0
|
||||
@@ -4,10 +4,12 @@
|
||||
* variables, and a `bunfig.toml` preload runs code before the entry point.
|
||||
* gstack runs inside the user's repository, so that directory belongs to the
|
||||
* code under review. Every Bun gstack starts passes --no-env-file and an empty
|
||||
* --config: shebangs and shell callers spell it
|
||||
* `bun --no-env-file --config=/dev/null`, TypeScript spawns use
|
||||
* bunIsolationArgs(). test/bun-entry-isolation.test.ts holds every entry point
|
||||
* to that form.
|
||||
* --config: shebangs and skill prose spell it
|
||||
* `bun --no-env-file --config=/dev/null`, shipped shell files spell
|
||||
* `--config="${GSTACK_BUN_CONFIG:-/dev/null}"` (NUL when OSTYPE is msys or
|
||||
* cygwin and cygpath is on PATH, #3415), TypeScript spawns use
|
||||
* bunIsolationArgs().
|
||||
* test/bun-entry-isolation.test.ts holds every entry point to that form.
|
||||
*
|
||||
* POSIX passes /dev/null. Native Windows has no /dev/null and `--config=NUL`
|
||||
* panicked Bun 1.3.10 while parsing the flag (#3085), so a TypeScript spawn on
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.226",
|
||||
"version": "1.91.227",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bash shim — Claude Code runs hook commands through /bin/sh; this runs the
|
||||
# TypeScript hook with bun.
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec bun --no-env-file --config=/dev/null "$HERE/mode-handoff-hook.ts"
|
||||
exec bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$HERE/mode-handoff-hook.ts"
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
CSO_BUILD_ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
cd "$CSO_BUILD_ROOT"
|
||||
@@ -227,7 +228,7 @@ cso_build_evaluation_candidate() {
|
||||
cp -R lib scripts "$source_root/"
|
||||
cp -R test/helpers "$source_root/test/"
|
||||
cp "$candidate_catalog" "$source_root/lib/cso/runtime-catalog.json"
|
||||
(cd "$source_root" && "$BUN_CMD" --no-env-file --config=/dev/null scripts/cso-runtime-promotion.ts check-evaluation lib/cso/runtime-catalog.json)
|
||||
(cd "$source_root" && "$BUN_CMD" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" scripts/cso-runtime-promotion.ts check-evaluation lib/cso/runtime-catalog.json)
|
||||
(cd "$source_root" && cso_compile_bun lib/cso/cli.ts "$CSO_EVALUATION_UNIT/gstack-cso-core")
|
||||
(cd "$source_root" && cso_compile_bun scripts/cso-eval-producer.ts "$CSO_EVALUATION_UNIT/cso-eval-producer")
|
||||
cso_compile_watchdog "$CSO_EVALUATION_UNIT/gstack-cso-watchdog"
|
||||
|
||||
@@ -314,6 +314,7 @@ export const KNOWN_WINDOWS_SAFE: Array<{ file: string; reason: string }> = [
|
||||
{ file: 'test/ship-hook-windows-paths.test.ts', reason: 'runs bin/ helpers through explicit bash and Bun argv with forward-slash paths; never executes a shebang; the path-spelling simulation is skipIf win32' },
|
||||
{ file: 'test/gstack-wtree-windows-paths.test.ts', reason: '#3101: runs bin/gstack-wtree through explicit bash in an os.tmpdir linked worktree with forward-slash paths; never executes a shebang; the drive-letter simulation is skipIf win32' },
|
||||
{ file: 'test/setup-hook-syntax-gate.test.ts', reason: '#3088/#3100: runs bin/gstack-hook-check and the setup hook sections through explicit bash with forward-slash paths; never executes a shebang. Its Bun drive-path spelling is the Windows bug it guards' },
|
||||
{ file: 'test/bun-config-shell-bins.test.ts', reason: '#3415: runs the record bins through lib/script-invocation.ts (Git Bash on Windows) with MSYS path conversion off, the case it guards; its bun and cygpath stand-ins and simulated-OSTYPE cases are skipIf win32' },
|
||||
{ file: 'test/path-spelling.test.ts', reason: 'sources bin/gstack-path-spelling.sh through explicit bash and checks lib/path-spelling.ts; on Windows it exercises the real Git Bash spellings' },
|
||||
{ file: 'test/state-root-parity.test.ts', reason: 'runs the bash twin and lib/state-root.ts over an env table with PATH empty; no shebang execution, raw-string comparison is platform-neutral' },
|
||||
{ file: 'test/generator-eexist.test.ts',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack setup — build browser binary + register skills with Claude Code / Codex
|
||||
set -e
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
umask 077 # Restrict new files to owner-only (0o600 files, 0o700 dirs)
|
||||
|
||||
# Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a
|
||||
@@ -362,7 +363,7 @@ _runtime_root_refused() {
|
||||
# --claude-model is validated before anything is written, and persisted once
|
||||
# Claude is known to be selected (below, and in the --refresh-registered parent).
|
||||
if [ "$CLAUDE_MODEL_SET" -eq 1 ]; then
|
||||
if ! _CLAUDE_MODEL_RESOLVED="$(cd "$SOURCE_GSTACK_DIR" && bun --no-env-file --config=/dev/null run scripts/models.ts claude-overlay "$CLAUDE_MODEL_ARG" 2>&1)"; then
|
||||
if ! _CLAUDE_MODEL_RESOLVED="$(cd "$SOURCE_GSTACK_DIR" && bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" run scripts/models.ts claude-overlay "$CLAUDE_MODEL_ARG" 2>&1)"; then
|
||||
printf 'Error: --claude-model %s\n' "$_CLAUDE_MODEL_RESOLVED" | sed '2,$s/^/ /' >&2
|
||||
echo " Nothing was installed or changed." >&2
|
||||
exit 1
|
||||
@@ -1317,7 +1318,7 @@ if [ "$LOCAL_INSTALL" -eq 0 ]; then
|
||||
_CODEX_PREFLIGHT_SELECTED=1
|
||||
fi
|
||||
fi
|
||||
bun --no-env-file --config=/dev/null "$SOURCE_GSTACK_DIR/scripts/preflight-codex-overlap.ts" \
|
||||
bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$SOURCE_GSTACK_DIR/scripts/preflight-codex-overlap.ts" \
|
||||
--source "$SOURCE_GSTACK_DIR" --namespace "$CODEX_SKILLS" \
|
||||
--selected "$_CODEX_PREFLIGHT_SELECTED" --local "$CODEX_REPO_LOCAL" \
|
||||
--windows "$IS_WINDOWS" --relocation "$HOME/.gstack/repos/gstack" || exit 1
|
||||
@@ -1584,7 +1585,7 @@ ensure_playwright_browser() {
|
||||
echo "gstack setup failed: Node.js is required on Windows" >&2
|
||||
return 1
|
||||
else
|
||||
probe_cmd="bun --no-env-file --config=/dev/null --eval 'import { chromium } from \"playwright\"; const browser = await chromium.launch(); await browser.close();'"
|
||||
probe_cmd="bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" --eval 'import { chromium } from \"playwright\"; const browser = await chromium.launch(); await browser.close();'"
|
||||
fi
|
||||
(
|
||||
cd "$SOURCE_GSTACK_DIR"
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* #3415: gstack's shell bins start Bun with
|
||||
* --config="${GSTACK_BUN_CONFIG:-/dev/null}", and each defines
|
||||
* GSTACK_BUN_CONFIG as NUL when OSTYPE is msys or cygwin and cygpath is on PATH
|
||||
* (Git Bash, MSYS2, Cygwin). Native bun.exe cannot
|
||||
* open /dev/null (EINVAL), so a Windows bash with MSYS path conversion off
|
||||
* (MSYS_NO_PATHCONV=1, MSYS2_ARG_CONV_EXCL=*) made the literal spelling fail:
|
||||
* the record bins wrote nothing, and gstack-timeline-log still exited 0.
|
||||
*
|
||||
* On every platform the free lanes run, windows-latest included, the record
|
||||
* bins run through lib/script-invocation.ts with conversion off in a hostile
|
||||
* repository: each writes its record, and the repository's bunfig.toml preload
|
||||
* and .env never load. On POSIX a simulated Windows bash (OSTYPE=msys, a
|
||||
* cygpath, a bun that rejects /dev/null the way bun.exe does) reproduces the
|
||||
* bug; its controls, without OSTYPE=msys, show each bin fails loudly with
|
||||
* nothing written instead of exiting 0. An OSTYPE=msys inherited by a POSIX
|
||||
* bash with no cygpath keeps /dev/null, so a file named NUL in the repository
|
||||
* is never read as Bun's config.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const IS_WINDOWS = process.platform === 'win32';
|
||||
|
||||
let tmp = '';
|
||||
let repo = '';
|
||||
let stateRoot = '';
|
||||
let evilRoot = '';
|
||||
let preloadMarker = '';
|
||||
let fakeBun = '';
|
||||
let crashBun = '';
|
||||
|
||||
const BINS: Array<{ bin: string; input: string; log: string }> = [
|
||||
{ bin: 'gstack-review-log', log: 'main-reviews.jsonl', input: JSON.stringify({ skill: 'plan-eng-review', timestamp: '2026-10-25T00:00:00Z', status: 'clean', unresolved: 0, critical_gaps: 0, issues_found: 0, mode: 'FULL_REVIEW', commit: 'abc1234' }) },
|
||||
{ bin: 'gstack-question-log', log: 'question-log.jsonl', input: JSON.stringify({ skill: 'ship', question_id: 'ship-test-failure-triage', question_summary: 'Tests failed', options_count: 3, user_choice: 'fix-now', recommended: 'fix-now', session_id: 's1' }) },
|
||||
{ bin: 'gstack-learnings-log', log: 'learnings.jsonl', input: JSON.stringify({ skill: 'review', type: 'pitfall', key: 'n-plus-one', insight: 'fixture insight', confidence: 8, source: 'observed' }) },
|
||||
{ bin: 'gstack-timeline-log', log: 'timeline.jsonl', input: JSON.stringify({ skill: 'review', event: 'started', branch: 'main' }) },
|
||||
];
|
||||
|
||||
function env(pathPrefix?: string, extra: Record<string, string> = {}): NodeJS.ProcessEnv {
|
||||
const out: NodeJS.ProcessEnv = {};
|
||||
for (const [k, v] of Object.entries(process.env)) if (!/^(GSTACK_|MSYS|OSTYPE$)/.test(k)) out[k] = v;
|
||||
const PATH = Object.keys(out).find(k => k.toUpperCase() === 'PATH') ?? 'PATH';
|
||||
if (pathPrefix) out[PATH] = pathPrefix + path.delimiter + (out[PATH] ?? '');
|
||||
return { ...out, HOME: path.join(tmp, 'home'), GSTACK_STATE_ROOT: stateRoot, MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*', ...extra };
|
||||
}
|
||||
|
||||
function run(bin: string, input: string, pathPrefix?: string, extra: Record<string, string> = {}, rest: string[] = []) {
|
||||
return runScriptSync(path.join(ROOT, 'bin', bin), [input, ...rest], { cwd: repo, env: env(pathPrefix, extra), encoding: 'utf8', timeout: 60_000 });
|
||||
}
|
||||
|
||||
function records(log: string): string[] {
|
||||
const projects = path.join(stateRoot, 'projects');
|
||||
if (!fs.existsSync(projects)) return [];
|
||||
return fs.readdirSync(projects).flatMap(slug => {
|
||||
const file = path.join(projects, slug, log);
|
||||
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean) : [];
|
||||
});
|
||||
}
|
||||
|
||||
function resetState(): void {
|
||||
fs.rmSync(stateRoot, { recursive: true, force: true });
|
||||
fs.rmSync(preloadMarker, { force: true });
|
||||
fs.mkdirSync(stateRoot, { recursive: true });
|
||||
}
|
||||
|
||||
function writeExec(file: string, text: string): void {
|
||||
fs.writeFileSync(file, text);
|
||||
fs.chmodSync(file, 0o755);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-bunconfig-')));
|
||||
repo = path.join(tmp, 'repo');
|
||||
stateRoot = path.join(tmp, 'state');
|
||||
evilRoot = path.join(tmp, 'evil');
|
||||
preloadMarker = path.join(tmp, 'preload-ran');
|
||||
for (const d of [repo, stateRoot, path.join(tmp, 'home')]) fs.mkdirSync(d, { recursive: true });
|
||||
for (const args of [['init', '-q', '-b', 'main'], ['-c', 'user.email=t@example.com', '-c', 'user.name=t', 'commit', '-q', '--allow-empty', '-m', 'init']]) {
|
||||
expect(spawnSync('git', args, { cwd: repo, timeout: 30_000 }).status).toBe(0);
|
||||
}
|
||||
fs.writeFileSync(path.join(repo, '.env'), `GSTACK_STATE_ROOT=${evilRoot}\nGSTACK_HOME=${evilRoot}\n`);
|
||||
fs.writeFileSync(path.join(repo, 'preload.ts'), `require('fs').writeFileSync(${JSON.stringify(preloadMarker)}, 'ran');\n`);
|
||||
fs.writeFileSync(path.join(repo, 'bunfig.toml'), 'preload = ["./preload.ts"]\n');
|
||||
if (IS_WINDOWS) return;
|
||||
fakeBun = path.join(tmp, 'fake-bun');
|
||||
fs.mkdirSync(fakeBun);
|
||||
writeExec(path.join(fakeBun, 'bun'), [
|
||||
'#!/usr/bin/env bash',
|
||||
'a=()',
|
||||
'for x in "$@"; do',
|
||||
' case "$x" in',
|
||||
' --config=/dev/null) echo "EINVAL: Invalid argument (open())" >&2; echo "while reading config \\"/dev/null\\"" >&2; exit 1 ;;',
|
||||
' --config=NUL) a+=(--config=/dev/null) ;;',
|
||||
' *) a+=("$x") ;;',
|
||||
' esac',
|
||||
'done',
|
||||
`exec ${JSON.stringify(process.execPath)} "\${a[@]}"`,
|
||||
'',
|
||||
].join('\n'));
|
||||
crashBun = path.join(tmp, 'crash-bun');
|
||||
fs.mkdirSync(crashBun);
|
||||
writeExec(path.join(crashBun, 'bun'), '#!/usr/bin/env bash\nexit 3\n');
|
||||
writeExec(path.join(fakeBun, 'cygpath'), '#!/usr/bin/env bash\nprintf \'%s\\n\' "${@: -1}"\n');
|
||||
});
|
||||
|
||||
afterAll(() => { if (tmp) fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
describe('#3415: record bins start Bun on a Windows bash with path conversion off', () => {
|
||||
test('fixture control: a plain bun in the repository runs its bunfig preload', () => {
|
||||
const r = spawnSync(process.execPath, ['-e', '0'], { cwd: repo, encoding: 'utf8', timeout: 60_000 });
|
||||
expect(r.status).toBe(0);
|
||||
expect(fs.existsSync(preloadMarker)).toBe(true);
|
||||
fs.rmSync(preloadMarker);
|
||||
});
|
||||
|
||||
for (const { bin, input, log } of BINS) {
|
||||
test(`${bin} writes its record and ignores the repository bunfig.toml and .env`, () => {
|
||||
resetState();
|
||||
const r = run(bin, input);
|
||||
expect(r.stderr).not.toContain('EINVAL');
|
||||
expect(r.status).toBe(0);
|
||||
expect(records(log).length).toBe(1);
|
||||
expect(fs.existsSync(preloadMarker)).toBe(false);
|
||||
expect(fs.existsSync(evilRoot)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
test('gstack-timeline-log still skips invalid input silently with exit 0', () => {
|
||||
resetState();
|
||||
for (const bad of ['not json', '{"skill":"review"}', 'null']) {
|
||||
const r = run('gstack-timeline-log', bad);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stderr).toBe('');
|
||||
}
|
||||
expect(records('timeline.jsonl')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(IS_WINDOWS)('#3415: simulated Windows bash (POSIX hosts)', () => {
|
||||
test('gstack-developer-profile --log-session reports a Bun failure instead of skipping it, and still skips invalid input silently', () => {
|
||||
resetState();
|
||||
const session = JSON.stringify({ date: '2026-10-25', mode: 'startup' });
|
||||
const failed = run('gstack-developer-profile', '--log-session', fakeBun, {}, [session]);
|
||||
expect(failed.stderr).toContain('EINVAL');
|
||||
expect(failed.stderr).toContain('nothing was logged');
|
||||
expect(failed.status).not.toBe(0);
|
||||
for (const bad of ['not json', '{"date":"2026-10-25"}', 'null']) {
|
||||
const r = run('gstack-developer-profile', '--log-session', undefined, {}, [bad]);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stderr).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
test('a Bun that dies with exit 3 (how a Bun crash exits on Windows) is reported, never read as invalid input', () => {
|
||||
resetState();
|
||||
const timeline = run('gstack-timeline-log', JSON.stringify({ skill: 'review', event: 'started' }), crashBun);
|
||||
expect(timeline.status).not.toBe(0);
|
||||
expect(timeline.stderr).toContain('bun exited 3');
|
||||
expect(records('timeline.jsonl')).toEqual([]);
|
||||
const profile = run('gstack-developer-profile', '--log-session', crashBun, {}, [JSON.stringify({ date: '2026-10-25', mode: 'startup' })]);
|
||||
expect(profile.status).not.toBe(0);
|
||||
expect(profile.stderr).toContain('bun exited 3');
|
||||
});
|
||||
|
||||
for (const { bin, input, log } of BINS) {
|
||||
test(`${bin} passes NUL and writes its record`, () => {
|
||||
resetState();
|
||||
const r = run(bin, input, fakeBun, { OSTYPE: 'msys' });
|
||||
expect(r.stderr).not.toContain('EINVAL');
|
||||
expect(r.status).toBe(0);
|
||||
expect(records(log).length).toBe(1);
|
||||
expect(fs.existsSync(preloadMarker)).toBe(false);
|
||||
});
|
||||
|
||||
test(`an inherited OSTYPE=msys without cygpath keeps /dev/null: ${bin} never reads a repository file named NUL`, () => {
|
||||
resetState();
|
||||
const nul = path.join(repo, 'NUL');
|
||||
fs.writeFileSync(nul, 'preload = ["./preload.ts"]\n');
|
||||
try {
|
||||
const r = run(bin, input, undefined, { OSTYPE: 'msys' });
|
||||
expect(r.status).toBe(0);
|
||||
expect(records(log).length).toBe(1);
|
||||
expect(fs.existsSync(preloadMarker)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(nul, { force: true });
|
||||
fs.rmSync(preloadMarker, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test(`control: when Bun rejects the config, ${bin} fails loudly and writes nothing`, () => {
|
||||
resetState();
|
||||
const r = run(bin, input, fakeBun);
|
||||
expect(r.stderr).toContain('EINVAL');
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(records(log)).toEqual([]);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -6,9 +6,11 @@
|
||||
* kinds are held to the isolated form:
|
||||
* 1. bun shebangs: exactly ISOLATED_SHEBANG (`bun run <file>` ignores the
|
||||
* shebang, so callers carry the flags too);
|
||||
* 2. shell callers in shipped shell files, skill templates, generated skill
|
||||
* docs and resolvers: `bun --no-env-file --config=/dev/null` before
|
||||
* `run`, `-e`/`-p` or a script path;
|
||||
* 2. shell callers: skill templates, generated skill docs and resolvers
|
||||
* spell `bun --no-env-file --config=/dev/null` before `run`, `-e`/`-p`
|
||||
* or a script path; shipped shell files spell SHELL_FLAGS after
|
||||
* CONFIG_DEF, so a Windows bash with MSYS path conversion off passes
|
||||
* NUL instead of a /dev/null native Bun cannot open (#3415);
|
||||
* 3. TypeScript spawns of a Bun: bunIsolationArgs() (or browse's
|
||||
* bunChildFlags()) in the argument list;
|
||||
* 4. scriptInvocation, which launches gstack's own bun bins;
|
||||
@@ -27,6 +29,8 @@ import { emptyBunConfig as browseEmptyBunConfig } from '../browse/src/bun-child-
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const FLAGS = '--no-env-file --config=/dev/null';
|
||||
const ISOLATED_SHEBANG = `#!/usr/bin/env -S bun ${FLAGS}`;
|
||||
const SHELL_FLAGS = '--no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"';
|
||||
const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac';
|
||||
|
||||
const TRACKED = spawnSync('git', ['ls-files', '-z'], { cwd: ROOT, encoding: 'utf8', timeout: 30_000 }).stdout.split('\0').filter(Boolean)
|
||||
.filter(f => !/(^|\/)test\//.test(f) && fs.existsSync(path.join(ROOT, f)) && fs.statSync(path.join(ROOT, f)).isFile());
|
||||
@@ -43,7 +47,7 @@ const BUN_WORD = /(^|[\s;&|({`"']|\$\()(bun|"\$\{?BUN(?:_[A-Z]+)?\}?"|\$\{?BUN(?
|
||||
|
||||
/** Whether the text after a `bun` word starts a program: -e/-p, a script path, `run <var>` or a bin path. A package script (`bun run build`) is not one. */
|
||||
function runsCode(rest: string): boolean {
|
||||
const r = rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+)\s+)*/, '');
|
||||
const r = rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+|config(?:=\S*|\s+\S+))\s+)*/, '');
|
||||
return /^(?:run\s+)?(?:-e|-p|--eval|--print)(?:\s|=|$)/.test(r)
|
||||
|| /^(?:run\s+)?["']?[^\s"'|;&)]*\.(?:ts|js|mjs|cjs|tsx)(?:["'\s)|;&\\`]|$)/.test(r)
|
||||
|| /^run\s+["']?\$\{?[A-Za-z_]/.test(r)
|
||||
@@ -51,14 +55,14 @@ function runsCode(rest: string): boolean {
|
||||
}
|
||||
|
||||
/** Bun invocations on one shell line that start a program without the isolation flags. */
|
||||
function unisolatedBunCalls(line: string): string[] {
|
||||
function unisolatedBunCalls(line: string, flags = FLAGS): string[] {
|
||||
if (/^\s*(#|\/\/|\*)/.test(line)) return [];
|
||||
const out: string[] = [];
|
||||
for (const m of line.matchAll(BUN_WORD)) {
|
||||
const at = m.index! + m[1]!.length;
|
||||
if (/\bln\s+-\w+\s+$/.test(line.slice(0, at))) continue;
|
||||
const rest = line.slice(at + m[2]!.length);
|
||||
if (!rest.startsWith(` ${FLAGS}`) && runsCode(rest)) out.push(line.slice(at, at + 80));
|
||||
if (!rest.startsWith(` ${flags}`) && runsCode(rest)) out.push(line.slice(at, at + 80));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -69,6 +73,26 @@ const SHELL_EXEMPT: Record<string, string> = {
|
||||
'skillify/SKILL.md': 'generated from skillify/SKILL.md.tmpl',
|
||||
};
|
||||
|
||||
/** Run only on Linux (the Ubicloud VM or its Linux-only local fallback), where /dev/null is the spelling; remote command strings expand there. */
|
||||
const SHELL_LINUX_ONLY = (f: string) => f.startsWith('scripts/ubicloud/');
|
||||
|
||||
/** Lines of shell text that use GSTACK_BUN_CONFIG with no CONFIG_DEF line before them, or define it any other way. A `local` definition covers only the rest of its function. */
|
||||
function configDefinitionGaps(text: string): string[] {
|
||||
const gaps: string[] = [];
|
||||
let defined = false;
|
||||
let definedInFunction = false;
|
||||
text.split('\n').forEach((line, i) => {
|
||||
if (/^\s*(?:local\s+)?GSTACK_BUN_CONFIG=/.test(line)) {
|
||||
const local = /^\s*local\s/.test(line);
|
||||
if (line.trim().replace(/^local\s+/, '') !== CONFIG_DEF) gaps.push(`${i + 1}: redefined: ${line.trim()}`);
|
||||
else if (local) definedInFunction = true;
|
||||
else defined = true;
|
||||
} else if (/^}/.test(line)) definedInFunction = false;
|
||||
else if (!defined && !definedInFunction && line.includes('${GSTACK_BUN_CONFIG')) gaps.push(`${i + 1}: used before CONFIG_DEF`);
|
||||
});
|
||||
return gaps;
|
||||
}
|
||||
|
||||
const isShell = (f: string) => f.endsWith('.sh') || /^#!.*\b(?:ba|z)?sh\b/.test(firstLine(f));
|
||||
const isSkillSource = (f: string) => f.endsWith('.tmpl') || /^scripts\/resolvers\/.+\.ts$/.test(f)
|
||||
|| /(^|\/)SKILL\.md$/.test(f) || /\/sections\/[^/]+\.md$/.test(f);
|
||||
@@ -116,13 +140,41 @@ describe('GSTA-56: gstack never takes configuration from the audited repository'
|
||||
]) expect(unisolatedBunCalls(ok)).toEqual([]);
|
||||
});
|
||||
|
||||
test('in shipped shell files the detector requires the Windows-safe, fail-closed config spelling (#3415)', () => {
|
||||
for (const bad of [
|
||||
`printf x | bun ${FLAGS} -e "console.log(1)"`,
|
||||
'bun --no-env-file --config="$GSTACK_BUN_CONFIG" -e 1',
|
||||
'bun --no-env-file --config= -e 1',
|
||||
'bun --no-env-file --config=NUL run "$X/bin/gstack-design-detect.ts"',
|
||||
]) expect(unisolatedBunCalls(bad, SHELL_FLAGS)).not.toEqual([]);
|
||||
for (const ok of [
|
||||
`printf x | bun ${SHELL_FLAGS} -e "console.log(1)"`,
|
||||
`exec bun ${SHELL_FLAGS} "$HERE/question-log-hook.ts"`,
|
||||
`probe_cmd="bun ${SHELL_FLAGS} --eval 'import x from \\"y\\"'"`,
|
||||
]) expect(unisolatedBunCalls(ok, SHELL_FLAGS)).toEqual([]);
|
||||
});
|
||||
|
||||
test('the config-definition check flags a missing, late or altered GSTACK_BUN_CONFIG definition', () => {
|
||||
const use = `bun ${SHELL_FLAGS} -e 1`;
|
||||
expect(configDefinitionGaps(`#!/usr/bin/env bash\n${CONFIG_DEF}\n${use}\n`)).toEqual([]);
|
||||
expect(configDefinitionGaps(`f() {\n local ${CONFIG_DEF}\n ${use}\n}\n`)).toEqual([]);
|
||||
expect(configDefinitionGaps(`f() {\n local ${CONFIG_DEF}\n}\ng() {\n ${use}\n}\n`)).toEqual(['5: used before CONFIG_DEF']);
|
||||
expect(configDefinitionGaps(`#!/usr/bin/env bash\n${use}\n`)).toEqual(['2: used before CONFIG_DEF']);
|
||||
expect(configDefinitionGaps(`${use}\n${CONFIG_DEF}\n`)).toEqual(['1: used before CONFIG_DEF']);
|
||||
expect(configDefinitionGaps(`GSTACK_BUN_CONFIG=./bunfig.toml\n${use}\n`)).toEqual(['1: redefined: GSTACK_BUN_CONFIG=./bunfig.toml', '2: used before CONFIG_DEF']);
|
||||
expect(configDefinitionGaps(`${CONFIG_DEF}\nGSTACK_BUN_CONFIG=\n${use}\n`)).toEqual(['2: redefined: GSTACK_BUN_CONFIG=']);
|
||||
});
|
||||
|
||||
test('shell callers, skill templates, generated skill docs and resolvers pass the isolation flags', () => {
|
||||
const offenders: string[] = [];
|
||||
for (const f of TRACKED) {
|
||||
if (f.startsWith('.github/') || SHELL_EXEMPT[f] || !(isSkillSource(f) || isShell(f))) continue;
|
||||
fs.readFileSync(path.join(ROOT, f), 'utf8').split('\n').forEach((line, i) => {
|
||||
for (const call of unisolatedBunCalls(line)) offenders.push(`${f}:${i + 1}: ${call}`);
|
||||
const flags = isSkillSource(f) || SHELL_LINUX_ONLY(f) ? FLAGS : SHELL_FLAGS;
|
||||
const text = fs.readFileSync(path.join(ROOT, f), 'utf8');
|
||||
text.split('\n').forEach((line, i) => {
|
||||
for (const call of unisolatedBunCalls(line, flags)) offenders.push(`${f}:${i + 1}: ${call}`);
|
||||
});
|
||||
if (flags === SHELL_FLAGS) for (const gap of configDefinitionGaps(text)) offenders.push(`${f}:${gap}`);
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
* --config=nul for the native bun.exe, which Bun 1.4.2 reads as an empty
|
||||
* config; a native Windows spawn has no /dev/null, so TypeScript callers use
|
||||
* bunIsolationArgs() (lib/bun-isolation.ts) instead of spelling the flag.
|
||||
* With conversion off (MSYS_NO_PATHCONV=1) bun.exe gets /dev/null itself and
|
||||
* fails with EINVAL, so gstack's shell files resolve GSTACK_BUN_CONFIG to NUL
|
||||
* there (#3415).
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -15,6 +18,8 @@ import { bunIsolationArgs } from '../lib/bun-isolation';
|
||||
import { bashCommandInvocation } from '../lib/script-invocation';
|
||||
|
||||
const PRINT = 'console.log("env=" + (process.env.GSTACK_LAUNCH_PROBE ?? "unset"))';
|
||||
const CONFIG_DEF = 'GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac';
|
||||
const NO_PATHCONV = { MSYS_NO_PATHCONV: '1', MSYS2_ARG_CONV_EXCL: '*' };
|
||||
let project = '';
|
||||
let preloadMarker = '';
|
||||
|
||||
@@ -61,6 +66,18 @@ describe('GSTA-56: isolated Bun launches on this platform', () => {
|
||||
expectIsolated(bash('bun --no-env-file --config=/dev/null run probe.ts'));
|
||||
});
|
||||
|
||||
test('a shipped shell file spelling, with MSYS path conversion off', () => {
|
||||
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" -e '${PRINT}'`, NO_PATHCONV));
|
||||
expectIsolated(bash(`${CONFIG_DEF}; bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" run probe.ts`, NO_PATHCONV));
|
||||
});
|
||||
|
||||
test.if(process.platform === 'win32')('control: with MSYS path conversion off, bun.exe cannot open --config=/dev/null', () => {
|
||||
const r = bash(`bun --no-env-file --config=/dev/null -e '${PRINT}'`, NO_PATHCONV);
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain('EINVAL');
|
||||
expect(fs.existsSync(preloadMarker)).toBe(false);
|
||||
});
|
||||
|
||||
test('a TypeScript spawn with bunIsolationArgs()', () => {
|
||||
expectIsolated(spawnSync(process.execPath, [...bunIsolationArgs(), 'probe.ts'], { cwd: project, encoding: 'utf8', timeout: 60_000 }));
|
||||
});
|
||||
|
||||
@@ -6,10 +6,15 @@ const SCRIPT = path.join(import.meta.dir, "..", "bin", "gstack-learnings-search"
|
||||
|
||||
describe("gstack-learnings-search injection prevention", () => {
|
||||
const script = readFileSync(SCRIPT, "utf-8");
|
||||
const bunBlockAt = script.indexOf('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e "');
|
||||
|
||||
test("the bun -e block the checks below read is present", () => {
|
||||
expect(bunBlockAt).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test("no shell interpolation inside bun -e string", () => {
|
||||
// Extract the bun -e block (everything between `bun -e "` and the closing `"`)
|
||||
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
|
||||
const bunBlock = script.slice(bunBlockAt);
|
||||
|
||||
// Should NOT contain ${VAR} patterns (shell interpolation)
|
||||
// These are RCE vectors: a malicious learnings entry with '; rm -rf / ;' in the
|
||||
@@ -27,7 +32,7 @@ describe("gstack-learnings-search injection prevention", () => {
|
||||
});
|
||||
|
||||
test("uses process.env for all user-controlled values", () => {
|
||||
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
|
||||
const bunBlock = script.slice(bunBlockAt);
|
||||
|
||||
// Must use process.env for TYPE, QUERY, LIMIT.
|
||||
// SLUG and CROSS are no longer threaded as env vars inside the bun
|
||||
@@ -52,7 +57,7 @@ describe("gstack-learnings-search injection prevention", () => {
|
||||
});
|
||||
|
||||
test("current vs cross-project rows distinguished by inline tags, not SLUG env (#1619)", () => {
|
||||
const bunBlock = script.slice(script.indexOf('bun --no-env-file --config=/dev/null -e "'));
|
||||
const bunBlock = script.slice(bunBlockAt);
|
||||
// The bun block must inspect the per-line tag to mark cross-project rows.
|
||||
// The current shape emits `current\t<json>` or `cross\t<json>` from the
|
||||
// upstream pipe (via emit_tagged_file). Inside the bun block, the script
|
||||
|
||||
@@ -16,7 +16,7 @@ import * as path from 'path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const PLAN_ARG = '${_PLAN_MODE_ARG:+"$_PLAN_MODE_ARG"}';
|
||||
const GENERATOR = /(?:\bbun(?: --no-env-file --config=\/dev\/null)?|\bbun_cmd|_gstack_render_bun) run (?:gen:skill-docs(?::user)?|scripts\/gen-skill-docs\.ts) --[a-z]/;
|
||||
const GENERATOR = /(?:\bbun(?: --no-env-file --config=(?:\/dev\/null|"\$\{GSTACK_BUN_CONFIG:-\/dev\/null\}"))?|\bbun_cmd|_gstack_render_bun) run (?:gen:skill-docs(?::user)?|scripts\/gen-skill-docs\.ts) --[a-z]/;
|
||||
const CONFIG_READ = '[ "$("$GSTACK_CONFIG" get plan_mode_preamble 2>/dev/null || true)" != off ] || _PLAN_MODE_ARG="--plan-mode=off"';
|
||||
|
||||
/** Generator invocations in a file: not comments, and not echoed or logged instructions. */
|
||||
|
||||
@@ -210,8 +210,8 @@ describe('setup: heal-first ordering + visibility', () => {
|
||||
describe('gstack-settings-hook: shared prelude (dedupe key == prune predicate)', () => {
|
||||
test('every bun script call site uses the shared JS prelude concatenation', () => {
|
||||
const codeLines = hookBinSrc.split('\n').filter((l) => !l.trim().startsWith('#'));
|
||||
const bunCalls = codeLines.filter((l) => l.includes('bun --no-env-file --config=/dev/null -e '));
|
||||
const preludeCalls = codeLines.filter((l) => l.includes(`bun --no-env-file --config=/dev/null -e "$_HOOK_JS_PRELUDE"'`));
|
||||
const bunCalls = codeLines.filter((l) => l.includes('bun --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" -e '));
|
||||
const preludeCalls = codeLines.filter((l) => l.includes(`bun --no-env-file --config="\${GSTACK_BUN_CONFIG:-/dev/null}" -e "$_HOOK_JS_PRELUDE"'`));
|
||||
expect(bunCalls.length).toBeGreaterThanOrEqual(6);
|
||||
expect(preludeCalls.length).toBe(bunCalls.length);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user