From d3640f28f81c73df18271014e6c5f03cc7637263 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:46:16 +0000 Subject: [PATCH] B4: design --version prints the version, and the probe uses it The design CLI treated --version as an unknown command (exit 1), so the probe launched the binary with no arguments instead. Add --version (prints the gstack version, exit 0, before auth or network) and probe with it, so messages read ' --version exited ' as planned. --- design/src/cli.ts | 6 ++++++ design/test/cli-version.test.ts | 21 +++++++++++++++++++++ scripts/resolvers/design.ts | 14 +++++++------- test/design-setup-probe.test.ts | 6 +++--- 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 design/test/cli-version.test.ts diff --git a/design/src/cli.ts b/design/src/cli.ts index b266c6f38..d6d8bcc47 100644 --- a/design/src/cli.ts +++ b/design/src/cli.ts @@ -26,6 +26,7 @@ import { generateDesignToCodePrompt } from "./design-to-code"; import { serve } from "./serve"; import { gallery } from "./gallery"; import { normalizeIntFlag } from "./flag-utils"; +import { readVersionString } from "./daemon-state"; import { daemonStatus as daemonStatusClient, ensureDaemon, @@ -44,6 +45,11 @@ function parseArgs(argv: string[]): { printUsage(); process.exit(0); } + // DESIGN SETUP's launch probe (B4): proves the binary starts, without auth or network. + if (args[0] === "--version") { + console.log(readVersionString()); + process.exit(0); + } const command = args[0]; const flags: Record = {}; diff --git a/design/test/cli-version.test.ts b/design/test/cli-version.test.ts new file mode 100644 index 000000000..962dd1d83 --- /dev/null +++ b/design/test/cli-version.test.ts @@ -0,0 +1,21 @@ +/** + * B4: DESIGN SETUP probes `"$D" --version` to prove the binary starts. The CLI + * treated `--version` as an unknown command (exit 1), so the probe could never + * pass. It prints the gstack version and exits 0, before auth or network. + */ +import { describe, expect, test } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..', '..'); + +describe('design --version', () => { + test('prints the gstack version and exits 0 without credentials', () => { + const r = spawnSync(process.execPath, [path.join(ROOT, 'design/src/cli.ts'), '--version'], { + encoding: 'utf8', timeout: 30_000, env: { PATH: process.env.PATH ?? '', HOME: '/nonexistent' }, + }); + expect(r.status, r.stderr).toBe(0); + expect(r.stdout.trim()).toBe(fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf8').trim()); + }); +}); diff --git a/scripts/resolvers/design.ts b/scripts/resolvers/design.ts index 08e57dd6f..3dc9def81 100644 --- a/scripts/resolvers/design.ts +++ b/scripts/resolvers/design.ts @@ -1118,8 +1118,8 @@ Source: [OpenAI "Designing Delightful Frontends with GPT-5.4"](https://developer /** * B4 (#1076, #1254): DESIGN_READY only when the binary actually starts. A * binary that is merely executable can still be SIGKILLed at launch (an - * invalid macOS code signature), so launch it (no arguments prints usage and - * exits 0) under the portable deadline aside.ts uses. A success is cached + * invalid macOS code signature), so run `--version` under the portable + * deadline aside.ts uses. A success is cached * (inode + path, valid while newer than the binary) so Gatekeeper's * first-launch scan is paid once. */ @@ -1129,15 +1129,15 @@ _dt() { if command -v gtimeout >/dev/null; then gtimeout 10 "$@"; elif command - elif command -v perl >/dev/null; then perl -e 'alarm(shift);exec(@ARGV)' 10 "$@"; else return 125; fi; } _RC=0; _F="Fix: cd \${D%/design/dist/design} && ./setup" if [ ! -x "$D" ]; then _RC=missing -elif [ ! "$_DC" -nt "$D" ] || [ "$(cat "$_DC")" != "$_DK" ]; then _dt "$D" >/dev/null 2>&1 /dev/null 2>&1 "$_DC" 2>/dev/null ;; missing) echo "DESIGN_NOT_AVAILABLE: $D is not installed. $_F" ;; - 124|142) echo "DESIGN_NOT_AVAILABLE: $D timed out after 10s" ;; + 124|142) echo "DESIGN_NOT_AVAILABLE: $D --version timed out after 10s" ;; 125) echo "DESIGN_NOT_AVAILABLE: no timeout/gtimeout/perl to bound $D" ;; - 137) echo "DESIGN_NOT_AVAILABLE: $D exited 137 (killed at launch; on macOS usually an invalid code signature). $_F" ;; - *) echo "DESIGN_NOT_AVAILABLE: $D exited $_RC" ;; + 137) echo "DESIGN_NOT_AVAILABLE: $D --version exited 137 (killed at launch; on macOS usually an invalid code signature). $_F" ;; + *) echo "DESIGN_NOT_AVAILABLE: $D --version exited $_RC" ;; esac`; } @@ -1421,7 +1421,7 @@ else fi \`\`\` -**If TASTE_PROFILE_UNAVAILABLE:** say that line once, then continue without a taste profile (do not treat it as "no history"). +**If TASTE_PROFILE_UNAVAILABLE:** say so once; continue without a taste profile. **If TASTE_PROFILE_FOUND:** Parse the full JSON; malformed/unreadable uses the legacy fallback. After decay, rank each dimension by confidence * approved_count (or rejected_count); take three per kind. Count retained sessions (at most 50, not lifetime). Include in ${ctx.skillName === 'design-consultation' ? 'the Phase 1 product brief (later shared unchanged with both independent voices)' : 'the brief'}: diff --git a/test/design-setup-probe.test.ts b/test/design-setup-probe.test.ts index 5697a1d0b..2f94aa304 100644 --- a/test/design-setup-probe.test.ts +++ b/test/design-setup-probe.test.ts @@ -61,13 +61,13 @@ describe('B4: DESIGN_READY requires a binary that starts', () => { expect(w.launches()).toBe(1); fs.writeFileSync(path.join(w.install, 'design/dist/design'), `#!/bin/sh\necho launched >> "${w.home}/launches"\nexit 3\n`); fs.utimesSync(path.join(w.install, 'design/dist/design'), new Date(), new Date(Date.now() + 5_000)); - expect(run(setup(), w).stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design exited 3`); + expect(run(setup(), w).stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design --version exited 3`); }); test(`${host.name}: a binary killed at launch is not ready and names the fix`, () => { const w = world(host.root, 'kill -9 $$'); const r = run(setup(), w); - expect(r.stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design exited 137 (killed at launch; on macOS usually an invalid code signature). Fix: cd ${w.install} && ./setup`); + expect(r.stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design --version exited 137 (killed at launch; on macOS usually an invalid code signature). Fix: cd ${w.install} && ./setup`); expect(fs.existsSync(path.join(w.state, 'design-ready'))).toBe(false); }); @@ -100,7 +100,7 @@ describe('B4: DESIGN_READY requires a binary that starts', () => { child.on('error', reject); child.on('close', () => resolve(text)); }); - expect(stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design timed out after 10s`); + expect(stdout.trim()).toBe(`DESIGN_NOT_AVAILABLE: ${w.install}/design/dist/design --version timed out after 10s`); }, 30_000); test('office-hours visual exploration uses the same probe', () => {