From 69c9f2e83c35c51cbcac9ff82bbc67072d87042e Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sat, 4 Jul 2026 12:33:51 +0700 Subject: [PATCH] fix(cli): print the real package version for --version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit printVersion() logged `process.env.npm_package_version ?? '0.2.0'`. npm only populates npm_package_version inside its own run-script env, so an end user running `npx pxpipe-proxy --version` or a globally-installed `pxpipe` got the stale hardcoded fallback (0.2.0) instead of the real version — the code comment even claimed an esbuild.define that was never wired up. - scripts/build.mjs reads package.json and inlines the version via esbuild `define: { __PXPIPE_VERSION__ }`, then smoke-checks the built binary (`node dist/node.js --version` must equal package.json) and fails the build on mismatch, so a broken injection can never ship. - src/node.ts reads the injected constant behind a `typeof` guard (safe under tsx, where it is undefined), falling back to npm_package_version then 'unknown' — never a stale release number. Verified: node dist/node.js --version now prints 0.8.0. --- scripts/build.mjs | 26 +++++++++++++++++++++++++- src/node.ts | 12 ++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/scripts/build.mjs b/scripts/build.mjs index dc3dc35..c41148b 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -3,10 +3,16 @@ // wrangler directly from src/worker.ts, but dist/worker.js is also emitted for // package consumers via tsc. import { build } from 'esbuild'; -import { mkdir, rm } from 'node:fs/promises'; +import { mkdir, rm, readFile } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; +// Single source of truth for the CLI version: read it here, inline it into the +// bundle via esbuild `define`. Reading npm_package_version at CLI *runtime* is +// unreliable (unset for global bins / npx, or the consumer's version), so the +// value is fixed at build time instead. +const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')); + const OUT = 'dist'; if (existsSync(OUT)) await rm(OUT, { recursive: true, force: true }); await mkdir(OUT, { recursive: true }); @@ -26,9 +32,27 @@ await build({ target: 'node18', format: 'esm', sourcemap: true, + // Inline the package version so `pxpipe --version` is correct for global/npx + // installs (see the note where `pkg` is read). esbuild replaces the bare + // identifier with the string literal at every reference. + define: { __PXPIPE_VERSION__: JSON.stringify(pkg.version) }, // Atlas is inlined as a base64 string in src/core/atlas.ts, so no external assets. external: [], banner: { js: '#!/usr/bin/env node' }, }); console.log('✓ built dist/node.js'); + +// Smoke check: the bundled CLI must report the real package version, not a +// stale fallback. Runs the shipped artifact end-to-end and fails the build on +// mismatch, so a broken version injection can never reach a release. +const smoke = spawnSync(process.execPath, ['dist/node.js', '--version'], { encoding: 'utf8' }); +const printedVersion = (smoke.stdout ?? '').trim(); +if (smoke.status !== 0 || printedVersion !== pkg.version) { + console.error( + `✗ version smoke check failed: 'node dist/node.js --version' printed ` + + `${JSON.stringify(printedVersion)} (exit ${smoke.status}), expected ${JSON.stringify(pkg.version)}`, + ); + process.exit(1); +} +console.log(`✓ version smoke check: --version prints ${pkg.version}`); diff --git a/src/node.ts b/src/node.ts index 5a24459..3ccba4c 100644 --- a/src/node.ts +++ b/src/node.ts @@ -176,9 +176,17 @@ Use with OpenAI-compatible GPT clients: `); } +// Package version, inlined at bundle time by scripts/build.mjs via esbuild +// `define`. Under a non-bundled dev runner (tsx) the identifier is not defined; +// `typeof` returns "undefined" instead of throwing (ECMA-262 §13.5.3), so the +// guard is safe. `npm_package_version` is only a dev fallback: npm sets it just +// inside its own run-script env, so for `npx pxpipe-proxy` or a global bin it is +// undefined (or reflects the *consumer's* package), never this tool's version. +declare const __PXPIPE_VERSION__: string | undefined; + function printVersion(): void { - // Filled in at bundle time by esbuild.define; falls back here. - console.log(process.env.npm_package_version ?? '0.2.0'); + const injected = typeof __PXPIPE_VERSION__ === 'string' ? __PXPIPE_VERSION__ : undefined; + console.log(injected ?? process.env.npm_package_version ?? 'unknown'); } // ---- node:http <-> Web Request/Response bridge ---------------------------