From 35f82f0223639fa4ceb6ef1326316632ce06bd10 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 18:06:44 +0000 Subject: [PATCH] feat: add wizard entrypoint and local development CLI Adopt the CLI patterns from seamapi/cli so the wizard can be mounted as a subcommand of the Seam CLI while remaining a library: - Add the wizard entrypoint as the package default export, which the Seam CLI mounts under `seam wizard`. It parses its own arguments so the consumer only forwards argv, and takes a `commandName` used in help output. - Add a dummy `src/bin/cli.ts` for local development, runnable with `npm run wizard` (and `npm run inspect`). - Keep the package free of a published binary: no `bin` field, and `src/bin` is excluded from both the build and the package files. - Fix the stale vitest alias for the package name, which broke `test/todo.test.ts`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JyzztAG3bm4SHVkGvrdAj4 --- README.md | 35 ++++++++++++++++++++ package-lock.json | 42 +++++++---------------- package.json | 7 ++++ src/bin/cli.ts | 15 +++++++++ src/index.ts | 1 + src/lib/index.ts | 1 + src/lib/wizard.test.ts | 44 +++++++++++++++++++++++++ src/lib/wizard.ts | 75 ++++++++++++++++++++++++++++++++++++++++++ tsconfig.build.json | 2 +- tsconfig.json | 2 +- vitest.config.ts | 4 +-- 11 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 src/bin/cli.ts create mode 100644 src/lib/wizard.test.ts create mode 100644 src/lib/wizard.ts diff --git a/README.md b/README.md index 5cdf03d..bc496a5 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ The AI powered Seam setup wizard. TODO +This package is not a standalone command line program: +it deliberately publishes no `bin`. +The wizard is distributed as a library and mounted by the +[Seam CLI](https://github.com/seamapi/cli) under `seam wizard`. + ## Installation Add this as a dependency to your project using [npm] with @@ -19,6 +24,25 @@ $ npm install @seamapi/wizard [npm]: https://www.npmjs.com/ +## Usage + +Mount the entire wizard as a subcommand by forwarding the arguments +that belong to the wizard to the default export: + +```ts +import wizard from '@seamapi/wizard' + +// e.g., for `seam wizard --help`, argv is `['--help']`. +await wizard({ + argv: process.argv.slice(3), + commandName: 'seam wizard', +}) +``` + +The `commandName` option is only used in help output +so that the wizard describes itself using the command +that was actually run. + ## Development and Testing ### Quickstart @@ -31,6 +55,17 @@ $ npm install $ npm run test:watch ``` +Run the wizard locally with + +``` +$ npm run wizard +``` + +This runs the development CLI in `src/bin/cli.ts`, +which simply calls the wizard with the arguments given. +That file exists for local development only: +it is excluded from the build and from the published package. + Primary development tasks are defined under `scripts` in `package.json` and available via `npm run`. View them with diff --git a/package-lock.json b/package-lock.json index fed16df..6e4d64d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "@seamapi/wizard", "version": "0.0.1", "license": "MIT", + "dependencies": { + "minimist": "^1.2.8" + }, "devDependencies": { + "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", @@ -981,9 +985,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1001,9 +1002,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1021,9 +1019,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1041,9 +1036,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1061,9 +1053,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1081,9 +1070,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1314,6 +1300,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -4903,9 +4896,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4927,9 +4917,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4951,9 +4938,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4975,9 +4959,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5191,7 +5172,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/package.json b/package.json index 93420da..38ded7b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "index.d.ts", "lib", "src", + "!src/bin", "!test", "!**/*.test.ts" ], @@ -52,6 +53,8 @@ "lint": "eslint .", "postlint": "prettier --check --ignore-path .gitignore .", "postversion": "git push --follow-tags", + "wizard": "tsx src/bin/cli.ts", + "inspect": "tsx --inspect src/bin/cli.ts", "example": "tsx examples", "example:inspect": "tsx --inspect examples", "format": "prettier --write --ignore-path .gitignore .", @@ -72,7 +75,11 @@ "version": "^11.0.0 || ^10.0.0" } }, + "dependencies": { + "minimist": "^1.2.8" + }, "devDependencies": { + "@types/minimist": "^1.2.5", "@types/node": "^24.10.9", "@vitest/coverage-v8": "^4.1.10", "del-cli": "^7.0.0", diff --git a/src/bin/cli.ts b/src/bin/cli.ts new file mode 100644 index 0000000..f6af9e1 --- /dev/null +++ b/src/bin/cli.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +// This CLI exists for local development of the wizard only: +// it is not compiled or published, and the package exposes no bin. +// Consumers, e.g., the Seam CLI, mount the wizard with the default export. +// Run it with 'npm run wizard'. + +import wizard from 'lib/wizard.js' + +wizard({ argv: process.argv.slice(2) }).catch((err: unknown) => { + const { message, stack } = err instanceof Error ? err : new Error(String(err)) + // eslint-disable-next-line no-console + console.error(`Wizard Error: ${message}\n${stack ?? ''}`) + process.exitCode = 1 +}) diff --git a/src/index.ts b/src/index.ts index ea9e4aa..88790f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ export * from 'lib/index.js' +export { default } from 'lib/index.js' diff --git a/src/lib/index.ts b/src/lib/index.ts index 0cbac41..f3b05e0 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,2 @@ export { todo } from './todo.js' +export { default, type WizardOptions } from './wizard.js' diff --git a/src/lib/wizard.test.ts b/src/lib/wizard.test.ts new file mode 100644 index 0000000..5b083fe --- /dev/null +++ b/src/lib/wizard.test.ts @@ -0,0 +1,44 @@ +import { expect, test, vi } from 'vitest' + +import wizard from './wizard.js' + +const captureOutput = async ( + options: Parameters[0], +): Promise => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}) + try { + await wizard(options) + return log.mock.calls.map(([message]) => String(message)).join('\n') + } finally { + log.mockRestore() + } +} + +test('wizard: displays usage with the --help flag', async () => { + const output = await captureOutput({ argv: ['--help'] }) + expect(output).toContain('Seam Wizard') + expect(output).toContain('$ wizard [options]') +}) + +test('wizard: displays usage with the -h alias', async () => { + const output = await captureOutput({ argv: ['-h'] }) + expect(output).toContain('Seam Wizard') +}) + +test('wizard: uses the given command name in usage', async () => { + const output = await captureOutput({ + argv: ['--help'], + commandName: 'seam wizard', + }) + expect(output).toContain('$ seam wizard [options]') +}) + +test('wizard: runs with no arguments', async () => { + const output = await captureOutput({}) + expect(output).toContain('not implemented yet') +}) + +test('wizard: reports forwarded arguments', async () => { + const output = await captureOutput({ argv: ['setup', 'devices'] }) + expect(output).toContain('setup devices') +}) diff --git a/src/lib/wizard.ts b/src/lib/wizard.ts new file mode 100644 index 0000000..f461577 --- /dev/null +++ b/src/lib/wizard.ts @@ -0,0 +1,75 @@ +import parseArgs from 'minimist' + +export interface WizardOptions { + /** + * Command line arguments for the wizard, e.g., `process.argv.slice(2)`. + * + * These are the arguments _after_ the command used to invoke the wizard, + * so a consumer mounting the wizard as a subcommand should forward only + * the arguments belonging to the wizard. + */ + argv?: readonly string[] + + /** + * The command used to invoke the wizard, shown in help output. + * + * Defaults to `wizard`. + * The Seam CLI mounts this wizard and passes `seam wizard`. + */ + commandName?: string +} + +/** + * Run the Seam setup wizard. + * + * This is the entrypoint used by the Seam CLI to mount the entire wizard + * as a subcommand. It is also used by the development CLI in `src/bin/cli.ts`. + */ +const wizard = async (options: WizardOptions = {}): Promise => { + const { argv = [], commandName = 'wizard' } = options + + const args = parseArgs([...argv], { + boolean: ['help'], + alias: { h: 'help' }, + }) + + if (args['help'] === true) { + write(usage(commandName)) + return + } + + // TODO: Implement the wizard. + await Promise.resolve() + + write( + [ + `The ${commandName} is not implemented yet.`, + `Run '${commandName} --help' for usage.`, + ...(args._.length > 0 ? [`Received arguments: ${args._.join(' ')}`] : []), + ].join('\n'), + ) +} + +export default wizard + +const usage = (commandName: string): string => + [ + 'Seam Wizard', + '', + ' The AI powered Seam setup wizard.', + '', + 'Usage', + '', + ` $ ${commandName} [options]`, + '', + 'Options', + '', + ' -h, --help Display this help guide.', + '', + ].join('\n') + +// TODO: Replace this with a logger wrapper. +const write = (message: string): void => { + // eslint-disable-next-line no-console + console.log(message) +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 2a62903..01d2f6d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,5 +11,5 @@ }, "files": ["src/index.ts"], "include": ["src/**/*"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts", "src/bin/**/*"] } diff --git a/tsconfig.json b/tsconfig.json index 498dcee..cdfc697 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,7 @@ "lib/*": ["./src/lib/*"] } }, - "files": ["src/index.ts"], + "files": ["src/index.ts", "src/bin/cli.ts"], "include": [ "src/**/*", "test/**/*", diff --git a/vitest.config.ts b/vitest.config.ts index 1d5a679..01a4447 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,8 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: { - '@seamapi/makenew-tsmodule': new URL('./src/index.ts', import.meta.url) - .pathname, + '@seamapi/wizard': new URL('./src/index.ts', import.meta.url).pathname, lib: new URL('./src/lib', import.meta.url).pathname, }, }, @@ -12,6 +11,7 @@ export default defineConfig({ coverage: { exclude: [ '**/index.ts', + 'src/bin/cli.ts', 'package/**/*.ts', 'examples/**/*.ts', '**/*.test.ts',