From 3e62aceae59fa61392c242ddd788c7a6b4ac2462 Mon Sep 17 00:00:00 2001 From: garrytan Date: Thu, 8 Oct 2026 06:10:56 +0000 Subject: [PATCH] fix(windows): every compiled binary answers --version before any side effect browse --version started a server and exited 1, pdf printed usage and exited 1, gstack-global-discover rejected the flag, and find-browse ran discovery. A launch probe for Smart App Control (#2595) needs one side-effect-free command that exits 0 on a healthy build, so browse, find-browse, make-pdf and gstack-global-discover now answer --version first (design already did). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-global-discover.ts | 18 ++++++++- browse/src/cli.ts | 6 +++ browse/src/find-browse.ts | 11 +++++- make-pdf/src/cli.ts | 2 + test/binary-version-probe.test.ts | 61 +++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 test/binary-version-probe.test.ts diff --git a/bin/gstack-global-discover.ts b/bin/gstack-global-discover.ts index 8e66abbde..03cc1a572 100644 --- a/bin/gstack-global-discover.ts +++ b/bin/gstack-global-discover.ts @@ -10,7 +10,7 @@ */ import { existsSync, readdirSync, statSync, readFileSync, openSync, readSync, closeSync } from "fs"; -import { join, basename } from "path"; +import { join, basename, dirname } from "path"; import { execSync } from "child_process"; import { homedir } from "os"; import { canonicalRemote } from "../lib/remote-identity"; @@ -50,12 +50,24 @@ function printUsage(): void { --since Time window: e.g. 7d, 14d, 30d, 24h --format Output format: json (default) or summary --help Show this help + --version Print the gstack version and exit (no scan) Examples: gstack-global-discover --since 7d gstack-global-discover --since 14d --format summary`); } +function readGstackVersion(): string { + // The compiled binary and this source file both live in bin/, one level below VERSION. + for (const dir of [dirname(process.execPath), import.meta.dir]) { + try { + const version = readFileSync(join(dir, "..", "VERSION"), "utf8").trim(); + if (version) return version; + } catch {} + } + return "unknown"; +} + function parseArgs(): { since: string; format: "json" | "summary" } { const args = process.argv.slice(2); let since = ""; @@ -65,6 +77,10 @@ function parseArgs(): { since: string; format: "json" | "summary" } { if (args[i] === "--help" || args[i] === "-h") { printUsage(); process.exit(0); + } else if (args[i] === "--version") { + // setup's and gstack-doctor's launch probe (#2595): exits before any scan. + console.log(readGstackVersion()); + process.exit(0); } else if (args[i] === "--since" && args[i + 1]) { since = args[++i]; } else if (args[i] === "--format" && args[i + 1]) { diff --git a/browse/src/cli.ts b/browse/src/cli.ts index d4ddee1d8..cbb55ad47 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -1715,6 +1715,12 @@ export function withLoopbackNoProxy(env: Record): st } async function main() { + // setup's and gstack-doctor's launch probe: proves the binary starts, with no + // server, flags or network (Smart App Control detection, #2595). + if (process.argv[2] === '--version') { + console.log(readVersionHash() ?? 'unknown'); + process.exit(0); + } process.env.NO_PROXY = process.env.no_proxy = withLoopbackNoProxy(process.env); const rawArgs = process.argv.slice(2); diff --git a/browse/src/find-browse.ts b/browse/src/find-browse.ts index 77f7526ec..cdfcaef92 100644 --- a/browse/src/find-browse.ts +++ b/browse/src/find-browse.ts @@ -5,8 +5,8 @@ * Outputs the absolute path to the browse binary on stdout, or exits 1 if not found. */ -import { accessSync, constants } from 'fs'; -import { join } from 'path'; +import { accessSync, constants, readFileSync } from 'fs'; +import { dirname, join } from 'path'; import { homedir } from 'os'; // ─── Binary Discovery ─────────────────────────────────────────── @@ -91,6 +91,13 @@ export function locateBinary(): string | null { // ─── Main ─────────────────────────────────────────────────────── function main() { + // setup's and gstack-doctor's launch probe (#2595): no git, no discovery. + if (process.argv[2] === '--version') { + let version = 'unknown'; + try { version = readFileSync(join(dirname(process.execPath), '.version'), 'utf8').trim() || version; } catch {} + console.log(version); + process.exit(0); + } const bin = locateBinary(); if (!bin) { process.stderr.write('ERROR: browse binary not found. Run: cd && ./setup\n'); diff --git a/make-pdf/src/cli.ts b/make-pdf/src/cli.ts index ec6d3b7d2..ab7fdbc84 100644 --- a/make-pdf/src/cli.ts +++ b/make-pdf/src/cli.ts @@ -40,6 +40,8 @@ export function parseArgs(argv: string[]): ParsedArgs { printUsage(); process.exit(ExitCode.Success); } + // setup's and gstack-doctor's launch probe (#2595): the version command needs no browser. + if (args[0] === "--version") return { command: "version", positional: [], flags: {} }; // First non-flag arg is the command. let command = ""; diff --git a/test/binary-version-probe.test.ts b/test/binary-version-probe.test.ts new file mode 100644 index 000000000..62947bc31 --- /dev/null +++ b/test/binary-version-probe.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, test } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +// #2595: setup and gstack-doctor detect Smart App Control by launching each +// compiled binary with --version. Each entry must answer before any server +// start, discovery scan or network call, or the probe would misclassify a +// healthy build. Runs the source entries; the compiled binaries embed them. +const ROOT = path.resolve(import.meta.dir, '..'); +const tmps: string[] = []; +afterEach(() => { for (const t of tmps.splice(0)) fs.rmSync(t, { recursive: true, force: true }); }); + +function runVersion(entry: string) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-version-probe-')); + tmps.push(tmp); + const home = path.join(tmp, 'home'); + fs.mkdirSync(home); + const r = spawnSync(process.execPath, ['run', path.join(ROOT, entry), '--version'], { + cwd: tmp, + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + BROWSE_STATE_FILE: path.join(tmp, 'state', '.gstack', 'browse.json'), + GSTACK_STATE_ROOT: path.join(tmp, 'gstack-state'), + HTTPS_PROXY: 'http://127.0.0.1:9', + HTTP_PROXY: 'http://127.0.0.1:9', + }, + encoding: 'utf8', + timeout: 30_000, + }); + return { ...r, tmp, home }; +} + +describe('compiled-binary entries answer --version early (#2595)', () => { + for (const entry of ['browse/src/cli.ts', 'browse/src/find-browse.ts', 'design/src/cli.ts', 'make-pdf/src/cli.ts', 'bin/gstack-global-discover.ts']) { + test(`${entry} --version exits 0 with one line and no side effects`, () => { + const r = runVersion(entry); + expect(r.status).toBe(0); + expect(r.stdout.trim().split('\n')).toHaveLength(1); + expect(r.stdout.trim()).not.toBe(''); + expect(r.stderr).not.toContain('Starting server'); + expect(r.stderr).not.toMatch(/Unknown|Usage/); + expect(fs.existsSync(path.join(r.tmp, 'state'))).toBe(false); + expect(fs.existsSync(path.join(r.tmp, 'gstack-state'))).toBe(false); + // `bun run` itself may create its runtime cache under the home; the entry writes nothing. + expect(fs.readdirSync(r.home).filter(name => name !== '.bun' && name !== 'AppData')).toEqual([]); + }); + } + + test('find-browse --version does not search for browse', () => { + expect(runVersion('browse/src/find-browse.ts').stdout).not.toContain('browse/dist/browse'); + }); + + test('gstack-global-discover --version prints the checkout VERSION', () => { + const version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf8').trim(); + expect(runVersion('bin/gstack-global-discover.ts').stdout.trim()).toBe(version); + }); +});