From 42c7d2b4252f6c0b27f220d515b692e1f38d1b0e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 02:52:53 +0000 Subject: [PATCH] feat: Add the --version flag Report the package version with '--version' and the '-v' alias, using the same version injection the Seam CLI uses: 'src/lib/version.ts' ships a '0.0.0' placeholder that 'prepack.ts' replaces with the version from 'package.json' on 'npm pack', recompiling just that module with 'tsconfig.prepack.json'. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012cVGBZBJwZygoowCb9Ksp9 --- README.md | 12 ++- package-lock.json | 229 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 + prepack.ts | 56 ++++++++++ src/lib/version.ts | 4 + src/lib/wizard.test.ts | 16 +++ src/lib/wizard.ts | 13 ++- tsconfig.json | 8 +- tsconfig.prepack.json | 6 ++ 9 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 prepack.ts create mode 100644 src/lib/version.ts create mode 100644 tsconfig.prepack.json diff --git a/README.md b/README.md index 7a8d508..6c68c67 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ The `commandName` option is only used in help output so that the wizard describes itself using the command that was actually run. -The wizard handles `--help` itself and otherwise takes over the terminal +The wizard handles `--help` and `--version` itself +and otherwise takes over the terminal until the developer finishes, then restores it and prints a transcript of the run. It resolves when the wizard is done, and only rejects if the wizard could not run at all: a step that fails reports itself to the @@ -139,7 +140,8 @@ Every other argument is forwarded to the wizard untouched. The wizard is an [Ink] application under `src/lib`: - `wizard.ts` is the entrypoint the package exports. - It parses arguments, handles `--help`, and hands off to the renderer. + It parses arguments, handles `--help` and `--version`, + and hands off to the renderer. - `render.tsx` runs the app full-screen and reprints its transcript on exit. - `app.tsx` is the state machine driving the run, @@ -147,6 +149,12 @@ The wizard is an [Ink] application under `src/lib`: - `steps/` holds the logic for each step, with no UI in it. - `util/` holds the Seam API client, dotenv handling, and the subprocess runner. +- `version.ts` holds the package version reported by `--version`. + It ships a `0.0.0` placeholder that `prepack.ts` replaces with the + version from `package.json` when the package is packed, + recompiling just that module with `tsconfig.prepack.json`. + The version is only correct in a packed or published package, + not when running from a clone. Modules under `src/lib` are imported through the `lib/*` path alias rather than parent-relative specifiers. diff --git a/package-lock.json b/package-lock.json index d577d02..bed6647 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "execa": "^10.0.0", "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", "neostandard": "^0.13.0", @@ -1480,6 +1481,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -4026,6 +4034,73 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.0.tgz", + "integrity": "sha512-Cxl6MKxB1dr1H0FHmiizJ+lavKF7pV+fcDZFyqMB8d5m7qUPm/OtZYcD5vPWePKxSnTQ57KuBd9mtdZ3oNCvyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "figures": "^6.1.0", + "get-stream": "^9.0.1", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "path-key": "^4.0.0", + "pretty-ms": "^9.3.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "which-command": "^0.1.0", + "yoctocolors": "^2.1.2" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -4438,6 +4513,23 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4699,6 +4791,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -5376,6 +5478,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -5431,6 +5546,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -6457,6 +6585,36 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6743,6 +6901,19 @@ "node": ">=6" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6951,6 +7122,22 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7860,6 +8047,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8691,6 +8891,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-command": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz", + "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==", + "dev": true, + "license": "MIT", + "bin": { + "which-command": "cli.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/which-command?sponsor=1" + } + }, "node_modules/which-typed-array": { "version": "1.1.22", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", @@ -8861,6 +9077,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoga-layout": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", diff --git a/package.json b/package.json index 2c4a029..132678b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "test:debug": "vitest --inspect-brk --no-file-parallelism", "lint": "eslint .", "postlint": "prettier --check --ignore-path .gitignore .", + "prepack": "tsx ./prepack.ts", "postversion": "git push --follow-tags", "wizard": "tsx src/bin/cli.ts", "inspect": "tsx --inspect src/bin/cli.ts", @@ -94,6 +95,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.4", + "execa": "^10.0.0", "ink-testing-library": "^4.0.0", "jiti": "^2.4.2", "neostandard": "^0.13.0", diff --git a/prepack.ts b/prepack.ts new file mode 100644 index 0000000..20c422f --- /dev/null +++ b/prepack.ts @@ -0,0 +1,56 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' + +import { $ } from 'execa' + +const versionFile = './src/lib/version.ts' + +const main = async (): Promise => { + const version = await injectVersion(resolveFile(versionFile)) + // eslint-disable-next-line no-console + console.log(`✓ Version ${version} injected into ${versionFile}`) + + const { command } = await $`tsc --project tsconfig.prepack.json` + // eslint-disable-next-line no-console + console.log(`✓ Rebuilt with '${command}'`) +} + +const injectVersion = async (path: string): Promise => { + const { version } = await readPackageJson() + + if (version == null) { + throw new Error('Missing version in package.json') + } + + await replaceInFile( + path, + "const seamapiWizardVersion = '0.0.0'", + `const seamapiWizardVersion = '${version}'`, + ) + + return version +} + +const replaceInFile = async ( + path: string, + placeholder: string, + replacement: string, +): Promise => { + const source = await readFile(path, 'utf8') + + if (!source.includes(placeholder)) { + throw new Error(`Missing generated-value placeholder in ${path}`) + } + + await writeFile(path, source.replace(placeholder, replacement), 'utf8') +} + +const resolveFile = (path: string): string => + fileURLToPath(new URL(path, import.meta.url)) + +const readPackageJson = async (): Promise<{ version?: string }> => + JSON.parse(await readFile(resolveFile('package.json'), 'utf8')) as { + version?: string + } + +await main() diff --git a/src/lib/version.ts b/src/lib/version.ts new file mode 100644 index 0000000..48c3170 --- /dev/null +++ b/src/lib/version.ts @@ -0,0 +1,4 @@ +// Replaced with the package version when the package is packed. +const seamapiWizardVersion = '0.0.0' + +export default seamapiWizardVersion diff --git a/src/lib/wizard.test.ts b/src/lib/wizard.test.ts index 4ea9a89..ba8ae09 100644 --- a/src/lib/wizard.test.ts +++ b/src/lib/wizard.test.ts @@ -1,6 +1,7 @@ import { beforeEach, expect, test, vi } from 'vitest' import { renderApp } from './render.js' +import seamapiWizardVersion from './version.js' import wizard from './wizard.js' vi.mock('./render.js', () => ({ renderApp: vi.fn() })) @@ -45,6 +46,21 @@ test('wizard: does not run the app when displaying usage', async () => { expect(renderApp).not.toHaveBeenCalled() }) +test('wizard: displays the version with the --version flag', async () => { + const output = await captureOutput({ argv: ['--version'] }) + expect(output.trim()).toBe(seamapiWizardVersion) +}) + +test('wizard: displays the version with the -v alias', async () => { + const output = await captureOutput({ argv: ['-v'] }) + expect(output.trim()).toBe(seamapiWizardVersion) +}) + +test('wizard: does not run the app when displaying the version', async () => { + await captureOutput({ argv: ['--version'] }) + expect(renderApp).not.toHaveBeenCalled() +}) + test('wizard: runs the app in the working directory by default', async () => { await wizard() expect(renderApp).toHaveBeenCalledWith({ root: process.cwd() }) diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts index 8d1820f..c1914df 100644 --- a/src/lib/wizard.ts +++ b/src/lib/wizard.ts @@ -1,6 +1,7 @@ import parseArgs from 'minimist' import { renderApp } from './render.js' +import seamapiWizardVersion from './version.js' export interface WizardOptions { /** @@ -44,8 +45,8 @@ const wizard = async (options: WizardOptions = {}): Promise => { const { argv = [], commandName = 'wizard', cwd = process.cwd() } = options const args = parseArgs([...argv], { - boolean: ['help'], - alias: { h: 'help' }, + boolean: ['help', 'version'], + alias: { h: 'help', v: 'version' }, }) if (args['help'] === true) { @@ -53,6 +54,11 @@ const wizard = async (options: WizardOptions = {}): Promise => { return } + if (args['version'] === true) { + write(seamapiWizardVersion) + return + } + await renderApp({ root: cwd }) } @@ -73,7 +79,8 @@ const usage = (commandName: string): string => '', 'Options', '', - ' -h, --help Display this help guide.', + ' -h, --help Display this help guide.', + ' -v, --version Display the version.', '', ].join('\n') diff --git a/tsconfig.json b/tsconfig.json index b47625e..550188f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,11 @@ } }, "files": ["src/index.ts", "src/bin/cli.ts"], - "include": ["src/**/*", "test/**/*", "eslint.config.ts", "vitest.config.ts"] + "include": [ + "src/**/*", + "test/**/*", + "eslint.config.ts", + "prepack.ts", + "vitest.config.ts" + ] } diff --git a/tsconfig.prepack.json b/tsconfig.prepack.json new file mode 100644 index 0000000..aa6ca64 --- /dev/null +++ b/tsconfig.prepack.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.build.json", + "files": ["src/lib/version.ts"], + "exclude": ["**/*"] +}