From 3c27f87515e89f8d4694023d87cd2dcef966227c Mon Sep 17 00:00:00 2001 From: chenliuyun Date: Sun, 19 Apr 2026 10:53:57 +0800 Subject: [PATCH] fix: derive CLI version from package.json (v1.3.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: src/index.ts hardcoded `.version('2.1.0')`, decoupled from package.json, so every release required two manual edits and one was bound to drift. v1.3.1 shipped with `switchbot --version` reporting `2.1.0` while package.json said `1.3.1`. Fix (three layers): - C: src/index.ts reads version via createRequire('../package.json') — single source of truth. Version strings can no longer be hardcoded. - A: tests/version.test.ts spawns `node dist/index.js --version` and compares with package.json. vitest globalSetup runs `npm run build` once per test run so the compiled CLI is always fresh. - B: ci.yml adds a post-build step that runs the CLI and diffs its output against package.json, failing the PR on drift. Side changes: - tests/commands/capabilities.test.ts fixture: '2.1.0' -> '0.0.0-test' so grep for the stale version leaves no ghosts. - package.json 1.3.1 -> 1.3.2, package-lock.json resynced. Tests: 659/659 pass (658 existing + 1 new version drift guard). --- .github/workflows/ci.yml | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/index.ts | 6 +++++- tests/commands/capabilities.test.ts | 2 +- tests/globalSetup.ts | 8 ++++++++ tests/version.test.ts | 26 ++++++++++++++++++++++++++ vitest.config.ts | 1 + 8 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 tests/globalSetup.ts create mode 100644 tests/version.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3e8abb..03b4d9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,12 @@ jobs: cache: npm - run: npm ci - run: npm run build + - name: CLI --version matches package.json + run: | + PKG=$(node -p "require('./package.json').version") + CLI=$(node dist/index.js --version) + if [ "$PKG" != "$CLI" ]; then + echo "Version drift: package.json=$PKG, CLI=$CLI" + exit 1 + fi - run: npm test diff --git a/package-lock.json b/package-lock.json index f36fc92..8160ba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@switchbot/openapi-cli", - "version": "1.3.1", + "version": "1.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@switchbot/openapi-cli", - "version": "1.3.1", + "version": "1.3.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/package.json b/package.json index 10180d1..c981272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@switchbot/openapi-cli", - "version": "1.3.1", + "version": "1.3.2", "description": "Command-line interface for SwitchBot API v1.1", "keywords": [ "switchbot", diff --git a/src/index.ts b/src/index.ts index bbb3b40..e335388 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { Command, CommanderError } from 'commander'; +import { createRequire } from 'node:module'; import { registerConfigCommand } from './commands/config.js'; import { registerDevicesCommand } from './commands/devices.js'; import { registerScenesCommand } from './commands/scenes.js'; @@ -16,12 +17,15 @@ import { registerHistoryCommand } from './commands/history.js'; import { registerPlanCommand } from './commands/plan.js'; import { registerCapabilitiesCommand } from './commands/capabilities.js'; +const require = createRequire(import.meta.url); +const { version: pkgVersion } = require('../package.json') as { version: string }; + const program = new Command(); program .name('switchbot') .description('Command-line tool for SwitchBot API v1.1') - .version('2.1.0') + .version(pkgVersion) .option('--json', 'Output raw JSON response (disables tables; useful for pipes/scripts)') .option('--format ', 'Output format: table (default), json, jsonl, tsv, yaml, id') .option('--fields ', 'Comma-separated list of columns to include (e.g. --fields=id,name,type)') diff --git a/tests/commands/capabilities.test.ts b/tests/commands/capabilities.test.ts index eb8d2f1..632d348 100644 --- a/tests/commands/capabilities.test.ts +++ b/tests/commands/capabilities.test.ts @@ -5,7 +5,7 @@ import { registerCapabilitiesCommand } from '../../src/commands/capabilities.js' /** Build a representative program that mirrors the real CLI structure. */ function makeProgram(): Command { const p = new Command(); - p.name('switchbot').version('2.1.0'); + p.name('switchbot').version('0.0.0-test'); p.option('--json', 'Output raw JSON response'); p.option('--format ', 'Output format'); p.option('--fields ', 'Column filter'); diff --git a/tests/globalSetup.ts b/tests/globalSetup.ts new file mode 100644 index 0000000..e6e744d --- /dev/null +++ b/tests/globalSetup.ts @@ -0,0 +1,8 @@ +import { execSync } from 'node:child_process'; + +export default function setup(): void { + // Build once before the test run so tests that exercise the compiled CLI + // (e.g. version.test.ts spawning `node dist/index.js --version`) see the + // latest source. Runs once per vitest invocation, not per file. + execSync('npm run build', { stdio: 'inherit' }); +} diff --git a/tests/version.test.ts b/tests/version.test.ts new file mode 100644 index 0000000..fef53f3 --- /dev/null +++ b/tests/version.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +// Read the real package.json (NOT an import — keeps this decoupled from tsc's +// JSON import assertion setup and mirrors what publish.yml does in CI). +const here = path.dirname(fileURLToPath(import.meta.url)); +const pkg = JSON.parse( + readFileSync(path.join(here, '..', 'package.json'), 'utf-8'), +) as { version: string }; + +describe('CLI --version', () => { + it('matches package.json version', () => { + // Regression guard for the v1.3.1 bug where src/index.ts hardcoded a + // stale version string. execFileSync + process.execPath avoids shell + // quoting and PATH lookup issues on Windows/macOS/Linux. + const out = execFileSync( + process.execPath, + ['dist/index.js', '--version'], + { encoding: 'utf-8' }, + ).trim(); + expect(out).toBe(pkg.version); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 89a5291..84276a4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { environment: 'node', include: ['tests/**/*.test.ts'], + globalSetup: ['./tests/globalSetup.ts'], coverage: { provider: 'v8', include: ['src/**/*.ts'],