From c8e17701e4cb56756552bee38b0d3c49bc15f2a8 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Tue, 13 Oct 2026 15:28:41 +0100 Subject: [PATCH 1/4] Add pretty-cli TypeScript declarations Give TypeScript consumers typed CLI registration and output APIs without changing the CommonJS runtime. Check both module formats as part of package tests and ship the declarations with the package. --- packages/pretty-cli/README.md | 15 +- packages/pretty-cli/index.d.ts | 3 + packages/pretty-cli/lib/pretty-cli.d.ts | 196 +++++++++++++++++++ packages/pretty-cli/lib/styles.d.ts | 13 ++ packages/pretty-cli/lib/ui.d.ts | 12 ++ packages/pretty-cli/package.json | 10 +- packages/pretty-cli/test/types/consumer.cts | 35 ++++ packages/pretty-cli/test/types/consumer.mts | 40 ++++ packages/pretty-cli/test/types/tsconfig.json | 12 ++ pnpm-lock.yaml | 3 + vitest.config.ts | 2 +- 11 files changed, 335 insertions(+), 6 deletions(-) create mode 100644 packages/pretty-cli/index.d.ts create mode 100644 packages/pretty-cli/lib/pretty-cli.d.ts create mode 100644 packages/pretty-cli/lib/styles.d.ts create mode 100644 packages/pretty-cli/lib/ui.d.ts create mode 100644 packages/pretty-cli/test/types/consumer.cts create mode 100644 packages/pretty-cli/test/types/consumer.mts create mode 100644 packages/pretty-cli/test/types/tsconfig.json diff --git a/packages/pretty-cli/README.md b/packages/pretty-cli/README.md index 396787510..7a748bd95 100644 --- a/packages/pretty-cli/README.md +++ b/packages/pretty-cli/README.md @@ -39,7 +39,7 @@ prettyCLI Pretty CLI also provides a common UI interface, providing log functions to output coloured messages to the UI: ``` -const ui = require('@tryghost/pretty-cli/ui');` +const ui = require('@tryghost/pretty-cli/lib/ui');` ui.log.info('Done'); ui.log.warn('Uh Oh', 'Something went wrong'); @@ -49,10 +49,21 @@ You can also grab a fresh instance of the api with `prettyCLI.Api.get()`. The style rules used are available at `prettyCLI.styles`. +## TypeScript + +The package includes declarations for its standard command, option, help and +output APIs, including the existing `lib/` entry points. Both ESM imports and +CommonJS `import = require()` are supported. Runtime code remains CommonJS. + +Parsed argument values are `unknown` because sywac's option configuration is +dynamic. Narrow them before use; command `setup` callbacks receive the typed API +and `parse()` returns a typed result. These declarations do not model sywac's +internal type factories. + ## Test - `pnpm lint` runs oxlint -- `pnpm test` runs lint and tests +- `pnpm test` runs type checks, runtime tests and lint # Copyright & License diff --git a/packages/pretty-cli/index.d.ts b/packages/pretty-cli/index.d.ts new file mode 100644 index 000000000..a334dd2fc --- /dev/null +++ b/packages/pretty-cli/index.d.ts @@ -0,0 +1,3 @@ +import prettyCli = require('./lib/pretty-cli'); + +export = prettyCli; diff --git a/packages/pretty-cli/lib/pretty-cli.d.ts b/packages/pretty-cli/lib/pretty-cli.d.ts new file mode 100644 index 000000000..5fb6681e5 --- /dev/null +++ b/packages/pretty-cli/lib/pretty-cli.d.ts @@ -0,0 +1,196 @@ +import ui = require('./ui'); +import styles = require('./styles'); + +declare namespace prettyCli { + /** Parsed arguments are dynamic; narrow values before using them. */ + type Args = Record; + type Flags = string | string[]; + type Style = (text: string, details?: unknown) => string; + type Styles = Partial< + Record< + | 'group' + | 'groupError' + | 'flags' + | 'flagsError' + | 'desc' + | 'descError' + | 'hints' + | 'hintsError' + | 'messages' + | 'usagePrefix' + | 'usagePositionals' + | 'usageCommandPlaceholder' + | 'usageArgsPlaceholder' + | 'usageOptionsPlaceholder' + | 'example' + | 'all', + Style + > + >; + type OutputSettings = { + lineSep?: string; + sectionSep?: string; + pad?: string; + indent?: string; + split?: string | RegExp; + maxWidth?: number; + examplePrefix?: string; + showHelpOnError?: boolean; + }; + type ApiOptions = { + helpOpts?: OutputSettings; + name?: string; + parentName?: string; + strictMode?: boolean; + showHelpByDefault?: boolean; + }; + type OptionOptions = { + flags?: string; + aliases?: string[]; + type?: string; + desc?: string; + description?: string; + defaultValue?: unknown; + required?: boolean; + strict?: boolean; + hidden?: boolean; + hints?: string; + group?: string; + choices?: unknown[]; + version?: string; + mustExist?: boolean; + dirAllowed?: boolean; + fileAllowed?: boolean; + normalize?: boolean; + asObject?: boolean; + asPosix?: boolean; + asWin32?: boolean; + coerce?: (value: unknown) => unknown; + }; + type PositionalOptions = OptionOptions & { + params?: Array | Record; + paramsDescription?: string | string[]; + paramsDesc?: string | string[]; + paramsGroup?: string; + ignore?: string[]; + }; + type CommandHandler = (argv: Args, context: Context) => unknown; + type CommandOptions = PositionalOptions & { + setup?: (api: Api) => void; + run?: CommandHandler; + }; + type CommandDefinition = CommandOptions & ({ flags: string } | { aliases: string[] }); + type ExampleOptions = { flags?: string; desc?: string; description?: string; group?: string }; + type UsageOptions = { + usage?: string; + prefix?: string; + commandPlaceholder?: string; + argsPlaceholder?: string; + optionsPlaceholder?: string; + }; + type Details = { args: string[]; types: unknown[] }; + type ParseResult = { + code: number; + output: string; + errors: unknown[]; + argv: Args; + details: Details; + }; + type Context = ParseResult & { + commandHandlerRun: boolean; + helpRequested: boolean | OutputSettings; + versionRequested: boolean | { version?: string }; + cliMessage(message: unknown, ...args: unknown[]): void; + deferHelp(options?: OutputSettings): Context; + }; + type Api = CliApi; +} + +declare class CliApi { + constructor(options?: prettyCli.ApiOptions); + static get(options?: prettyCli.ApiOptions): CliApi; + static readonly DEFAULT_COMMAND_INDICATOR: string; + readonly name: string; + readonly parentName: string; + readonly types: unknown[]; + + configure(options?: prettyCli.ApiOptions): this; + newChild(name: string, options?: prettyCli.ApiOptions): CliApi; + strict(enabled?: boolean): this; + showHelpByDefault(enabled?: boolean): this; + preface(icon: string, slogan?: string): this; + usage(usage: string | prettyCli.UsageOptions): this; + groupOrder(groups?: string[]): this; + example(example: string | prettyCli.ExampleOptions, options?: prettyCli.ExampleOptions): this; + exampleOrder(groups?: string[]): this; + epilogue(text: string): this; + outputSettings(settings: prettyCli.OutputSettings): this; + style(hooks: prettyCli.Styles): this; + + command(options: prettyCli.CommandDefinition): this; + command(flags: string, options?: prettyCli.CommandOptions | prettyCli.CommandHandler): this; + commandDirectory( + directory: string, + options?: prettyCli.CommandOptions & { extensions?: string[] }, + ): this; + positional( + flags: string | string[] | prettyCli.PositionalOptions, + options?: prettyCli.PositionalOptions, + ): this; + option( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + boolean( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + string( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + number( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + path(flags: prettyCli.Flags | prettyCli.OptionOptions, options?: prettyCli.OptionOptions): this; + file(flags: prettyCli.Flags | prettyCli.OptionOptions, options?: prettyCli.OptionOptions): this; + dir(flags: prettyCli.Flags | prettyCli.OptionOptions, options?: prettyCli.OptionOptions): this; + enumeration( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + help( + flags?: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + version( + flags?: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + array( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + stringArray( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + numberArray( + flags: prettyCli.Flags | prettyCli.OptionOptions, + options?: prettyCli.OptionOptions, + ): this; + check(handler: prettyCli.CommandHandler): this; + parse(args?: string | string[]): Promise; + parseAndExit(args?: string | string[]): Promise; + getHelp(options?: prettyCli.OutputSettings): string; +} + +/** The styled process-wide instance; Api.get() creates an unstyled fresh instance. */ +declare const prettyCli: CliApi & { + Api: typeof CliApi; + styles: typeof styles; + ui: typeof ui; +}; + +export = prettyCli; diff --git a/packages/pretty-cli/lib/styles.d.ts b/packages/pretty-cli/lib/styles.d.ts new file mode 100644 index 000000000..f22c2d080 --- /dev/null +++ b/packages/pretty-cli/lib/styles.d.ts @@ -0,0 +1,13 @@ +declare const styles: { + usagePrefix(text: string): string; + group(text: string): string; + flags(text: string): string; + hints(text: string): string; + groupError(text: string): string; + flagsError(text: string): string; + descError(text: string): string; + hintsError(text: string): string; + messages(text: string): string; +}; + +export = styles; diff --git a/packages/pretty-cli/lib/ui.d.ts b/packages/pretty-cli/lib/ui.d.ts new file mode 100644 index 000000000..d959cb3d0 --- /dev/null +++ b/packages/pretty-cli/lib/ui.d.ts @@ -0,0 +1,12 @@ +type Log = ((...args: unknown[]) => void) & { + ok(...args: unknown[]): void; + trace(...args: unknown[]): void; + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; + fatal(...args: unknown[]): void; +}; + +declare const ui: { log: Log }; +export = ui; diff --git a/packages/pretty-cli/package.json b/packages/pretty-cli/package.json index 3367b448f..76d54f5fb 100644 --- a/packages/pretty-cli/package.json +++ b/packages/pretty-cli/package.json @@ -11,23 +11,27 @@ }, "files": [ "index.js", + "index.d.ts", "lib" ], "main": "index.js", + "types": "index.d.ts", "publishConfig": { "access": "public" }, "scripts": { "dev": "echo \"Implement me!\"", - "test": "NODE_ENV=testing vitest run --config ../../vitest.config.ts --root . --coverage", + "test": "pnpm test:types && NODE_ENV=testing vitest run --config ../../vitest.config.ts --root . --coverage", "lint": "oxlint -c ../../.oxlintrc.json .", - "posttest": "pnpm run lint" + "posttest": "pnpm run lint", + "test:types": "tsc --noEmit -p test/types/tsconfig.json" }, "dependencies": { "chalk": "^6.0.0", "sywac": "^1.3.0" }, "devDependencies": { - "sinon": "catalog:" + "sinon": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/pretty-cli/test/types/consumer.cts b/packages/pretty-cli/test/types/consumer.cts new file mode 100644 index 000000000..9a1a5959b --- /dev/null +++ b/packages/pretty-cli/test/types/consumer.cts @@ -0,0 +1,35 @@ +import cli = require('../../index.js'); +import ui = require('../../lib/ui'); +import styles = require('../../lib/styles'); + +const api: cli.Api = new cli.Api({ name: 'test', strictMode: true }); +api.positional('', { hidden: true }) + .boolean('--force', { defaultValue: false }) + .string(['--name', '-n']) + .number('--count', { defaultValue: 1 }) + .option('--custom', { type: 'string' }) + .enumeration('--mode', { choices: ['a', 'b'] }) + .path('--path').file('--file').dir('--dir') + .array('--values', { type: 'string' }) + .stringArray('--names').numberArray('--counts') + .version('-v, --version', { version: '1.0.0' }) + .command('run', async (argv, context) => { + if (typeof argv.count === 'number') { + ui.log.info(argv.count); + } + context.deferHelp(); + }) + .check((argv, context) => { + if (argv.force === false) { + context.cliMessage('Confirmation required'); + } + }) + .usage({ prefix: 'Usage:', commandPlaceholder: '' }) + .groupOrder(['Commands:', 'Options:']) + .example('test run', { desc: 'Run a command' }) + .exampleOrder(['Examples:']) + .outputSettings({ maxWidth: 80 }).showHelpByDefault().epilogue('Done'); + +const help: string = api.getHelp(); +const label: string = styles.group(help); +cli.ui.log(label); diff --git a/packages/pretty-cli/test/types/consumer.mts b/packages/pretty-cli/test/types/consumer.mts new file mode 100644 index 000000000..173a38eb2 --- /dev/null +++ b/packages/pretty-cli/test/types/consumer.mts @@ -0,0 +1,40 @@ +import cli from '../../index.js'; +import ui from '../../lib/ui.js'; +import styles from '../../lib/styles.js'; +import implementation from '../../lib/pretty-cli.js'; + +const fresh: cli.Api = cli.Api.get({ name: 'gx' }); +fresh.help('-h, --help').style(cli.styles).preface('Ghost Dev Tools').strict(); +fresh.command({ + flags: 'generate', + setup(group) { + group.command({ + flags: 'package ', + setup(command) { + command.string('--description', { defaultValue: 'Example' }); + }, + async run(argv, context) { + if (typeof argv.name === 'string') { + cli.ui.log.info(argv.name); + } else { + context.cliMessage('Expected a package name'); + } + } + }); + } +}); +const result: cli.ParseResult = await fresh.parse(['--help']); +const code: number = result.code; +const output: string = result.output; +const args: cli.Args = await fresh.parseAndExit([]); +const styled: string = styles.flags(output); +ui.log('Result', code, styled, args); +implementation.ui.log.ok('Done'); + +// Catch accidental erosion of the declaration boundary to untyped results. +type Assert = T; +export type Checks = [ + Assert extends Promise ? true : false>, + Assert, + Assert +]; diff --git a/packages/pretty-cli/test/types/tsconfig.json b/packages/pretty-cli/test/types/tsconfig.json new file mode 100644 index 000000000..c3e00508f --- /dev/null +++ b/packages/pretty-cli/test/types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "types": [] + }, + "include": ["*.mts", "*.cts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bc673eec..d2697a499 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -661,6 +661,9 @@ importers: sinon: specifier: 'catalog:' version: 22.1.0 + typescript: + specifier: 'catalog:' + version: 7.0.2 packages/pretty-stream: dependencies: diff --git a/vitest.config.ts b/vitest.config.ts index 096dc4bfa..69fc14baf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ coverage: { provider: 'v8', include: ['**/lib/**'], - exclude: ['**/build/**', '**/test/**'], + exclude: ['**/build/**', '**/test/**', '**/*.d.ts'], reporter: ['text', 'cobertura'], thresholds: { lines: 90, From f9b8bc5d98a6b7db90dd85faf53d5578c152654d Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Tue, 13 Oct 2026 15:30:38 +0100 Subject: [PATCH 2/4] Fix UI example syntax --- packages/pretty-cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pretty-cli/README.md b/packages/pretty-cli/README.md index 7a748bd95..91d9ab51a 100644 --- a/packages/pretty-cli/README.md +++ b/packages/pretty-cli/README.md @@ -39,7 +39,7 @@ prettyCLI Pretty CLI also provides a common UI interface, providing log functions to output coloured messages to the UI: ``` -const ui = require('@tryghost/pretty-cli/lib/ui');` +const ui = require('@tryghost/pretty-cli/lib/ui'); ui.log.info('Done'); ui.log.warn('Uh Oh', 'Something went wrong'); From b193b498a7a934ca12c83b7b30c2438013666c37 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Tue, 13 Oct 2026 16:25:45 +0100 Subject: [PATCH 3/4] Match sywac alias and version types Accept string aliases and version callbacks already supported by sywac. Verify both declarations and runtime behavior. --- packages/pretty-cli/lib/pretty-cli.d.ts | 9 +++--- packages/pretty-cli/test/pretty-cli.test.js | 31 +++++++++++++++++++++ packages/pretty-cli/test/types/consumer.mts | 5 ++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/pretty-cli/lib/pretty-cli.d.ts b/packages/pretty-cli/lib/pretty-cli.d.ts index 5fb6681e5..23f815b58 100644 --- a/packages/pretty-cli/lib/pretty-cli.d.ts +++ b/packages/pretty-cli/lib/pretty-cli.d.ts @@ -5,6 +5,7 @@ declare namespace prettyCli { /** Parsed arguments are dynamic; narrow values before using them. */ type Args = Record; type Flags = string | string[]; + type Version = string | (() => string); type Style = (text: string, details?: unknown) => string; type Styles = Partial< Record< @@ -46,7 +47,7 @@ declare namespace prettyCli { }; type OptionOptions = { flags?: string; - aliases?: string[]; + aliases?: Flags; type?: string; desc?: string; description?: string; @@ -57,7 +58,7 @@ declare namespace prettyCli { hints?: string; group?: string; choices?: unknown[]; - version?: string; + version?: Version; mustExist?: boolean; dirAllowed?: boolean; fileAllowed?: boolean; @@ -79,7 +80,7 @@ declare namespace prettyCli { setup?: (api: Api) => void; run?: CommandHandler; }; - type CommandDefinition = CommandOptions & ({ flags: string } | { aliases: string[] }); + type CommandDefinition = CommandOptions & ({ flags: string } | { aliases: Flags }); type ExampleOptions = { flags?: string; desc?: string; description?: string; group?: string }; type UsageOptions = { usage?: string; @@ -99,7 +100,7 @@ declare namespace prettyCli { type Context = ParseResult & { commandHandlerRun: boolean; helpRequested: boolean | OutputSettings; - versionRequested: boolean | { version?: string }; + versionRequested: boolean | { version?: Version }; cliMessage(message: unknown, ...args: unknown[]): void; deferHelp(options?: OutputSettings): Context; }; diff --git a/packages/pretty-cli/test/pretty-cli.test.js b/packages/pretty-cli/test/pretty-cli.test.js index f489b255f..c90ad1f30 100644 --- a/packages/pretty-cli/test/pretty-cli.test.js +++ b/packages/pretty-cli/test/pretty-cli.test.js @@ -38,6 +38,37 @@ describe('API', function () { }); }); +describe('sywac compatibility', function () { + it('accepts string option and command aliases', async function () { + let received; + const api = prettyCLI.Api.get({ name: 'test' }); + api.string({ aliases: 'name' }).command({ + aliases: 'run', + run(argv) { + received = argv.name; + }, + }); + const result = await api.parse(['run', '--name', 'example']); + assert.equal(result.code, 0, result.output); + assert.equal(received, 'example'); + }); + + it('calls a function-valued version', async function () { + let called = 0; + const api = prettyCLI.Api.get({ name: 'test' }); + api.version('--version', { + version() { + called += 1; + return '1.0.0'; + }, + }); + const result = await api.parse(['--version']); + assert.equal(result.code, 0, result.output); + assert.equal(result.output, '1.0.0'); + assert.equal(called, 1); + }); +}); + describe('styles', function () { it('usagePrefix styles first word and indents the remainder', function () { const out = prettyCLI.styles.usagePrefix('Usage: app --help'); diff --git a/packages/pretty-cli/test/types/consumer.mts b/packages/pretty-cli/test/types/consumer.mts index 173a38eb2..c773f2cf3 100644 --- a/packages/pretty-cli/test/types/consumer.mts +++ b/packages/pretty-cli/test/types/consumer.mts @@ -23,6 +23,11 @@ fresh.command({ }); } }); +fresh.string({ aliases: 'name' }); +fresh.command({ aliases: 'run', run() {} }); +fresh.version('--version', { version: () => '1.0.0' }); +const deferredVersion: cli.Context['versionRequested'] = { version: () => '1.0.0' }; +cli.ui.log(deferredVersion); const result: cli.ParseResult = await fresh.parse(['--help']); const code: number = result.code; const output: string = result.output; From 110d85d63da8cf27a9694680162406e616f7cb68 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 19 Oct 2026 14:37:27 +0100 Subject: [PATCH 4/4] Fix pretty-cli positional and directory overloads Accept the positional maps, mixed parameter arrays and optional directory arguments supported by sywac. Check both module formats reject invalid options, and verify parsing and caller-relative command discovery against the runtime. --- packages/pretty-cli/lib/pretty-cli.d.ts | 12 ++-- packages/pretty-cli/test/pretty-cli.test.js | 76 +++++++++++++++++++++ packages/pretty-cli/test/types/consumer.cts | 15 ++++ packages/pretty-cli/test/types/consumer.mts | 15 ++++ 4 files changed, 112 insertions(+), 6 deletions(-) diff --git a/packages/pretty-cli/lib/pretty-cli.d.ts b/packages/pretty-cli/lib/pretty-cli.d.ts index 23f815b58..9b7498a92 100644 --- a/packages/pretty-cli/lib/pretty-cli.d.ts +++ b/packages/pretty-cli/lib/pretty-cli.d.ts @@ -68,8 +68,9 @@ declare namespace prettyCli { asWin32?: boolean; coerce?: (value: unknown) => unknown; }; + type PositionalParameters = Array | Record; type PositionalOptions = OptionOptions & { - params?: Array | Record; + params?: PositionalParameters; paramsDescription?: string | string[]; paramsDesc?: string | string[]; paramsGroup?: string; @@ -80,6 +81,7 @@ declare namespace prettyCli { setup?: (api: Api) => void; run?: CommandHandler; }; + type CommandDirectoryOptions = CommandOptions & { extensions?: string[] }; type CommandDefinition = CommandOptions & ({ flags: string } | { aliases: Flags }); type ExampleOptions = { flags?: string; desc?: string; description?: string; group?: string }; type UsageOptions = { @@ -130,12 +132,10 @@ declare class CliApi { command(options: prettyCli.CommandDefinition): this; command(flags: string, options?: prettyCli.CommandOptions | prettyCli.CommandHandler): this; - commandDirectory( - directory: string, - options?: prettyCli.CommandOptions & { extensions?: string[] }, - ): this; + commandDirectory(directory: string, options?: prettyCli.CommandDirectoryOptions): this; + commandDirectory(options?: prettyCli.CommandDirectoryOptions): this; positional( - flags: string | string[] | prettyCli.PositionalOptions, + flags: string | prettyCli.PositionalParameters | prettyCli.PositionalOptions, options?: prettyCli.PositionalOptions, ): this; option( diff --git a/packages/pretty-cli/test/pretty-cli.test.js b/packages/pretty-cli/test/pretty-cli.test.js index c90ad1f30..5f822d392 100644 --- a/packages/pretty-cli/test/pretty-cli.test.js +++ b/packages/pretty-cli/test/pretty-cli.test.js @@ -1,5 +1,8 @@ const assert = require('assert/strict'); const sinon = require('sinon'); +const { mkdtempSync, writeFileSync, rmSync } = require('node:fs'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); const prettyCLI = require('../'); describe('API', function () { @@ -53,6 +56,79 @@ describe('sywac compatibility', function () { assert.equal(received, 'example'); }); + for (const [name, definition] of [ + [ + 'a positional map', + { '': { desc: 'Name' }, '[destination]': { desc: 'Destination' } }, + ], + ['positional objects and strings', [{ flags: '', desc: 'Name' }, '[destination]']], + [ + 'positional options', + { params: { '': { desc: 'Name' }, '[destination]': { desc: 'Destination' } } }, + ], + ]) { + it(`parses ${name}`, async function () { + const api = prettyCLI.Api.get({ name: 'test' }); + assert.equal(api.positional(definition), api); + const result = await api.parse(['example', 'output']); + assert.equal(result.code, 0, result.output); + assert.equal(result.argv.name, 'example'); + assert.equal(result.argv.destination, 'output'); + }); + } + + for (const mode of ['options only', 'no arguments', 'directory and options']) { + it(`loads commands with ${mode}`, async function () { + const directory = mkdtempSync(join(tmpdir(), 'pretty-cli-commands-')); + const loaderPath = join(directory, 'load.cjs'); + const defaultPath = join(directory, 'default.js'); + const customPath = join(directory, 'custom.cjs'); + try { + writeFileSync( + loaderPath, + `module.exports = (api, options) => options + ? api.commandDirectory(options) : api.commandDirectory();`, + ); + for (const [path, command] of [ + [defaultPath, 'default'], + [customPath, 'custom'], + ]) { + writeFileSync( + path, + `module.exports = { + flags: '${command} ', + run(argv) { module.exports.received = argv.name; } + };`, + ); + } + const api = prettyCLI.Api.get({ name: 'test', strictMode: true }); + const load = require(loaderPath); + const returned = + mode === 'directory and options' + ? api.commandDirectory(directory, { extensions: ['.cjs'] }) + : load(api, mode === 'options only' ? { extensions: ['.cjs'] } : undefined); + assert.equal(returned, api); + const command = mode === 'no arguments' ? 'default' : 'custom'; + const result = await api.parse([command, 'example']); + assert.equal(result.code, 0, result.output); + assert.equal( + require(command === 'default' ? defaultPath : customPath).received, + 'example', + ); + const excluded = await api.parse([ + command === 'default' ? 'custom' : 'default', + 'example', + ]); + assert.notEqual(excluded.code, 0); + } finally { + for (const path of [loaderPath, defaultPath, customPath]) { + delete require.cache[path]; + } + rmSync(directory, { recursive: true, force: true }); + } + }); + } + it('calls a function-valued version', async function () { let called = 0; const api = prettyCLI.Api.get({ name: 'test' }); diff --git a/packages/pretty-cli/test/types/consumer.cts b/packages/pretty-cli/test/types/consumer.cts index 9a1a5959b..6215ab1e9 100644 --- a/packages/pretty-cli/test/types/consumer.cts +++ b/packages/pretty-cli/test/types/consumer.cts @@ -33,3 +33,18 @@ api.positional('', { hidden: true }) const help: string = api.getHelp(); const label: string = styles.group(help); cli.ui.log(label); + +// These sywac forms must work for both ESM and CommonJS consumers. +api.positional({'': {desc: 'Name', required: true}}).help(); +api.positional([{flags: '', desc: 'Name'}, '[destination]']).help(); +api.positional({params: {'': {desc: 'Name'}}}).help(); +api.commandDirectory('commands', {extensions: ['.cjs']}).help(); +api.commandDirectory({extensions: ['.cjs']}).help(); +api.commandDirectory().help(); + +// @ts-expect-error Positional map entries must use typed option values. +api.positional({'': {required: 'yes'}}); +// @ts-expect-error Positional array entries must use typed option values. +api.positional([{flags: '', required: 'yes'}]); +// @ts-expect-error Directory extensions must be an array of strings. +api.commandDirectory({extensions: '.cjs'}); diff --git a/packages/pretty-cli/test/types/consumer.mts b/packages/pretty-cli/test/types/consumer.mts index c773f2cf3..2a0652053 100644 --- a/packages/pretty-cli/test/types/consumer.mts +++ b/packages/pretty-cli/test/types/consumer.mts @@ -43,3 +43,18 @@ export type Checks = [ Assert, Assert ]; + +// These sywac forms must work for both ESM and CommonJS consumers. +fresh.positional({'': {desc: 'Name', required: true}}).help(); +fresh.positional([{flags: '', desc: 'Name'}, '[destination]']).help(); +fresh.positional({params: {'': {desc: 'Name'}}}).help(); +fresh.commandDirectory('commands', {extensions: ['.cjs']}).help(); +fresh.commandDirectory({extensions: ['.cjs']}).help(); +fresh.commandDirectory().help(); + +// @ts-expect-error Positional map entries must use typed option values. +fresh.positional({'': {required: 'yes'}}); +// @ts-expect-error Positional array entries must use typed option values. +fresh.positional([{flags: '', required: 'yes'}]); +// @ts-expect-error Directory extensions must be an array of strings. +fresh.commandDirectory({extensions: '.cjs'});