From d008757e8703012196e97e69ed85287eaba61470 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 15 Oct 2026 00:35:25 +0000 Subject: [PATCH] fix(install): fresh-install log lines name the clone and every host installed (A10) gstack-capy-install prints `cloned at ()` for a checkout it cloned, or one the bootstrapper (revision 2, GSTACK_CAPY_CLONED) cloned in this run, instead of `reusing`. setup's final line is `gstack ready ()` from the same rows as its summary table, including the --refresh-registered upgrade summary. Files: bin/gstack-capy-install, bin/gstack-capy-session, capy/skill/gstack/scripts/install.sh, setup, test/gstack-capy-install.test.ts, test/host-conformance.test.ts Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-capy-install | 3 +++ bin/gstack-capy-session | 1 + capy/skill/gstack/scripts/install.sh | 5 ++++- setup | 13 +++++++++++++ test/gstack-capy-install.test.ts | 4 ++++ test/host-conformance.test.ts | 2 ++ 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/bin/gstack-capy-install b/bin/gstack-capy-install index 13fd12104..0ec777cd9 100755 --- a/bin/gstack-capy-install +++ b/bin/gstack-capy-install @@ -91,6 +91,9 @@ elif [ ! -f "$ROOT/setup" ] || [ ! -f "$ROOT/VERSION" ]; then else git clone --quiet --single-branch --depth 1 "$REPO" "$ROOT" || stop INSTALL_PREFLIGHT_FAILED "could not clone $REPO" "check network access to github.com, then re-run" fi + say "cloned $REPO at $(git -C "$ROOT" rev-parse --short=12 HEAD 2>/dev/null) ($(head -1 "$ROOT/VERSION"))" +elif [ -n "${GSTACK_CAPY_CLONED:-}" ]; then + say "cloned $GSTACK_CAPY_CLONED at $(git -C "$ROOT" rev-parse --short=12 HEAD 2>/dev/null) ($(head -1 "$ROOT/VERSION"))" else say "reusing $ROOT ($(head -1 "$ROOT/VERSION")); never reset or pulled" fi diff --git a/bin/gstack-capy-session b/bin/gstack-capy-session index 815919eb7..ac04de7ac 100755 --- a/bin/gstack-capy-session +++ b/bin/gstack-capy-session @@ -8,6 +8,7 @@ # GSTACK_CAPY_REMOTE_VERSION origin's VERSION read in this run (wrapper 2+; the # installer's `latest: available` trailer note) # GSTACK_CAPY_UPGRADED_FROM the version it fast-forwarded from (setup is never skipped then) +# GSTACK_CAPY_CLONED the repo it just cloned (wrapper 2+; the installer's `cloned` line) # GSTACK_CAPY_LOCK_HELD=1 install mode: .gstack-capy-install.lock is held on an # inherited descriptor; nothing here takes it again # Because this file moves with the checkout, session-start behavior can change diff --git a/capy/skill/gstack/scripts/install.sh b/capy/skill/gstack/scripts/install.sh index 6f81370ef..4d5b021f8 100755 --- a/capy/skill/gstack/scripts/install.sh +++ b/capy/skill/gstack/scripts/install.sh @@ -10,7 +10,8 @@ # 4. exec the checkout's full wrapper, bin/gstack-capy-session, with the same # arguments, handing the held lock over (GSTACK_CAPY_LOCK_HELD=1) and, # from revision 2, origin's VERSION read in this run -# (GSTACK_CAPY_REMOTE_VERSION, the trailer's `latest:` note). +# (GSTACK_CAPY_REMOTE_VERSION, the trailer's `latest:` note) and the repo +# it just cloned (GSTACK_CAPY_CLONED, the installer's `cloned` line). # Everything else (Bun, setup, the doctor, the verdict, the launch line) is # the checkout's. The last line is the `gstack:` trailer; exit 0 only on # `gstack: ok`. Contract: docs/capy-install.md; tests: @@ -30,6 +31,7 @@ repo="${GSTACK_CAPY_REPO:-https://github.com/garrytan/gstack.git}" echo "gstack-capy-skill: wrapper $GSTACK_CAPY_WRAPPER ($self)" check=0 skill="" revision="" min_note="" remote_min="" remote_version="" +unset GSTACK_CAPY_CLONED args=() while [ $# -gt 0 ]; do case "$1" in @@ -83,6 +85,7 @@ if [ ! -f "$root/setup" ]; then fi remote_min=$(support_get "$(cat "$root/capy/support.env" 2>/dev/null)" GSTACK_MIN_REVISION) remote_version=$(head -1 "$root/VERSION" 2>/dev/null) + export GSTACK_CAPY_CLONED="$repo" else # The remote's rules: fetch the default branch into FETCH_HEAD (never --depth # into an existing checkout), read capy/support.env from it. diff --git a/setup b/setup index ceb9a0849..1153f3fc6 100755 --- a/setup +++ b/setup @@ -440,6 +440,15 @@ _repair_crlf_checkout() { } [ -n "${GSTACK_SETUP_REFRESH_CHILD:-}" ] || _repair_crlf_checkout +# _setup_ready_line — the final `gstack ready (, ...)` line from summary +# rows on stdin (host...result in field 7): every host installed, +# updated or unchanged, in table order; nothing when no host row exists. +_setup_ready_line() { + local hosts + hosts=$(awk -F'\t' '$1 != "state" && $7 ~ /^(installed|updated|unchanged)$/ && !seen[$1]++ { printf "%s%s", (n++ ? ", " : ""), $1 }') + [ -z "$hosts" ] || log "gstack ready ($hosts)" +} + # ─── --refresh-registered: refresh exactly the registered installs (#1925) ─── # What /gstack-upgrade and the team-mode auto-update run. Each install this # checkout registered is refreshed by running setup for that one host, in its @@ -553,6 +562,7 @@ EOF if [ "$_rf_disabled" != " " ]; then echo " disabled skills (not registered on any host):$_rf_disabled" fi + _setup_ready_line < "$_RF_SUMMARY" rm -f "$_RF_SUMMARY" exit "$_RF_FAILED" fi @@ -4766,3 +4776,6 @@ if [ -n "${_HOOK_REFUSED:-}" ]; then echo "gstack setup: everything else is installed, but these hooks were not registered because they do not parse: $_HOOK_REFUSED(first: $_HOOK_REFUSED_AT). This is a gstack bug; report $_HOOK_REFUSED_AT. https://github.com/garrytan/gstack/blob/main/docs/troubleshooting.md#setup-hook-does-not-parse" >&2 exit 1 fi +# The final line names every host this run installed, from the same rows as +# the summary table; instruction-only hosts write no row and print none. +printf '%s' "$_SETUP_ROWS" | _setup_ready_line diff --git a/test/gstack-capy-install.test.ts b/test/gstack-capy-install.test.ts index 075f086a2..759822c6c 100644 --- a/test/gstack-capy-install.test.ts +++ b/test/gstack-capy-install.test.ts @@ -264,6 +264,10 @@ describe('gstack-capy-install', () => { const r = run(f, ['--root', clone], { GSTACK_CAPY_REPO: f.root }); expect(r.status, r.stdout + r.stderr).toBe(0); expect(r.stdout).toContain(`cloning ${f.root} into ${clone}`); + expect(r.stdout).toMatch(new RegExp(`^gstack-capy-install: cloned ${f.root} at [0-9a-f]{12} \\(1\\.2\\.3\\)$`, 'm')); + const handed = run(f, [], { GSTACK_CAPY_CLONED: 'https://github.com/garrytan/gstack.git' }); + expect(handed.stdout).toMatch(/^gstack-capy-install: cloned https:\/\/github\.com\/garrytan\/gstack\.git at [0-9a-f]{12} \(1\.2\.3\)$/m); + expect(handed.stdout).not.toContain('reusing'); expect(fs.existsSync(path.join(clone, 'setup'))).toBe(true); const occupied = path.join(f.base, 'occupied'); write(path.join(occupied, 'README.md'), 'x'); diff --git a/test/host-conformance.test.ts b/test/host-conformance.test.ts index a3b21f474..6e7ea0f42 100644 --- a/test/host-conformance.test.ts +++ b/test/host-conformance.test.ts @@ -129,6 +129,7 @@ describe.skipIf(process.platform === 'win32')('host conformance kit', () => { const rows = registryRows(f); expect(rows.map(row => [row[0], row[1], row[3], row[5], row[6]])).toEqual([[host, 'global', dest, realpathSync(src), readFileSync(join(src, 'VERSION'), 'utf8').trim()]]); expect(r.stdout + r.stderr).toMatch(new RegExp(`Install summary:[\\s\\S]*\\b${host}\\s+\\S+\\s+global\\s+installed`)); + expect(r.stdout.trim().split('\n').at(-1)).toBe(`gstack ready (${host})`); const status = runSetup(f, join(src, 'setup'), ['--status']); expect(status.status).toBe(0); @@ -231,6 +232,7 @@ describe.skipIf(process.platform === 'win32')('host conformance kit', () => { expect(upgrade.status, upgrade.stdout + upgrade.stderr).toBe(0); expect(upgrade.stdout).toContain(`Refreshing registered gstack installs from source: ${realpathSync(src)}`); expect(upgrade.stdout).toMatch(/Upgrade summary:\n[\s\S]*claude\s+full\s+global\s+unchanged\s+1\.91\.14\.0[\s\S]*codex\s+experimental\s+global\s+installed\s+1\.91\.14\.0/); + expect(upgrade.stdout.trim().split('\n').at(-1)).toBe('gstack ready (claude, codex)'); expect(registryRows(f).map(r => [r[0], r[6]]).sort()).toEqual([['claude', '1.91.14.0'], ['codex', '1.91.14.0']]); // The refreshed Codex install runs the new checkout's render. expect(realpathSync(join(f.home, '.codex/skills/gstack-review/SKILL.md'))).toBe(join(realpathSync(src), '.agents/skills/gstack-review/SKILL.md'));