From c4953b5834308ad9a806c12d483214de0ccc885d Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Sun, 29 Mar 2026 22:47:37 -0500 Subject: [PATCH 1/6] Revert "chore(deps)!: enquirer@2.4.1" This reverts commit 17f15aa5fd3659e8e257182085b90f1f1d0a8eb1. --- eslint.config.js | 2 +- examples/commands/hello.js | 11 +-- examples/commands/sub/hub.js | 4 +- lib/command/flag-to-prompt.js | 20 ++--- lib/command/flag-type.js | 11 +-- lib/command/index.js | 19 ++--- lib/command/invert-when.js | 29 ------- lib/conf.js | 1 - package.json | 2 +- test/command.js | 140 +++++++++++++--------------------- test/flag-to-prompt.js | 8 +- test/flag-type.js | 4 +- test/invert-when.js | 77 ------------------- 13 files changed, 82 insertions(+), 246 deletions(-) delete mode 100644 lib/command/invert-when.js delete mode 100644 test/invert-when.js diff --git a/eslint.config.js b/eslint.config.js index 6adce93..cde2123 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ const logdna = require('eslint-config-logdna') module.exports = defineConfig([ { 'extends': [logdna] - , 'files': ['lib/**/*.js', '*.js', 'test/**/*.js'] + , 'files': ['lib/**/*.js', 'test/**/*.js', 'index.js'] , 'languageOptions': { ecmaVersion: 2022 , sourceType: 'commonjs' diff --git a/examples/commands/hello.js b/examples/commands/hello.js index 6dad62b..6279311 100644 --- a/examples/commands/hello.js +++ b/examples/commands/hello.js @@ -27,9 +27,7 @@ module.exports = new cli.Command({ 'type': Boolean , 'shorthand': 'e' , 'description': 'Say hello in a very excited manner' - , 'default': true - , 'affirmative': 'Yes' - , 'negative': 'Nope' + , 'default': false } , volume: { @@ -39,13 +37,6 @@ module.exports = new cli.Command({ , 'default': 'normal' , 'shorthand': 'v' } - , things: { - type: String - , description: 'which thing' - , choices: ['one', 'two', 'three'] - , multi: true - , shorthand: 't' - } } , onContent: (content) => { // command success diff --git a/examples/commands/sub/hub.js b/examples/commands/sub/hub.js index 3595542..eaf3086 100644 --- a/examples/commands/sub/hub.js +++ b/examples/commands/sub/hub.js @@ -13,12 +13,10 @@ module.exports = new cli.Command({ 'type': Boolean , 'default': true , 'description': 'do you like hub' - , affirmative: 'I think so' - , negative: 'i do not think so' } } , async run(cmd, data) { - return data.ask ? 'hub' : 'no hub' + return 'hub' } }) diff --git a/lib/command/flag-to-prompt.js b/lib/command/flag-to-prompt.js index 03e5c90..5cd47fe 100644 --- a/lib/command/flag-to-prompt.js +++ b/lib/command/flag-to-prompt.js @@ -1,24 +1,20 @@ 'use strict' const flagType = require('./flag-type') -const invertWhen = require('./invert-when') module.exports = flagToPrompt function flagToPrompt(name, opt = {}) { const display = name.replace(':', ' ') - const t = flagType(opt) return { - type: t - , name: name - , message: display + ': ' + (opt.description || '(no description)') - , choices: opt.choices - , initial: opt.default || null - , skip: invertWhen(opt.when) - , result: opt.filter - , format: opt.transformer - , affirmative: opt.affirmative - , negative: opt.negative + 'type': flagType(opt) + , 'name': name + , 'message': display + ': ' + (opt.description || '(no description)') + , 'choices': opt.choices + , 'default': opt.default || null + , 'when': opt.when + , 'filter': opt.filter + , 'transformer': opt.transformer } } diff --git a/lib/command/flag-type.js b/lib/command/flag-type.js index 4363cf3..d11f445 100644 --- a/lib/command/flag-type.js +++ b/lib/command/flag-type.js @@ -10,17 +10,12 @@ function flagType(flag) { , multi: true }) } - if (flag.type === Boolean) { - if (flag.affirmative && flag.negative) { - return 'toggle' - } - return 'confirm' - } + if (flag.type === Boolean) return 'confirm' if (flag.type === Number) return 'number' if (flag.mask) return 'password' if (flag.choices) { - if (flag.multi) return 'multiselect' - return 'select' + if (flag.multi) return 'checkbox' + return 'list' } return 'input' diff --git a/lib/command/index.js b/lib/command/index.js index 5c1a560..0dd86cb 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -22,7 +22,7 @@ **/ const tty = require('node:tty') -const Equirer = require('enquirer') +const inquirer = require('inquirer') const {default: spinners} = require('cli-spinners') const {default: ora} = require('ora') const nopt = require('nopt') @@ -152,7 +152,7 @@ class Command extends Registry { this._optcache = null this.parsed = null this.options = Object.create(null) - this[kPrompt] = new Equirer() + this[kPrompt] = inquirer.createPromptModule() this.reset() const subcommands = options.reduce((acc, opts) => { @@ -166,11 +166,6 @@ class Command extends Registry { this.setOptions(defaults, ...options) - // Allow injecting a custom enquirer instance for testing - if (this.options.enquirer) { - this[kPrompt] = this.options.enquirer - } - this._shcache = this.shorthands const spinner = spinners[this.options.ui] @@ -376,14 +371,14 @@ class Command extends Registry { if (current.interactive === false) continue if (Array.isArray(current.type)) { const previous = toArray(answers[flag]) - const answer = (await this.ask(flag, current)) + const [answer] = (await this.ask(flag, current)) previous.push(...toArray(answer)) answers[flag] = previous continue } const arg = toQuestion(flag, current, answers) const res = await this.prompt(arg) - answers[flag] = res[flag] + Object.assign(answers, res) // If the flag has a validation function, call it now to possibly terminate early. // Pass the shape of `this.parsed` to match the parameter from non-interactive mode. if (typeof current.validate === 'function') { @@ -642,8 +637,8 @@ class Command extends Registry { * @returns {Promise} Promise object representing the end user input from the question **/ prompt(opts) { - const enquirer = this[kPrompt] - return enquirer.prompt(opts) + const prompt = this[kPrompt] + return prompt(opts) } /** @@ -684,7 +679,7 @@ class Command extends Registry { const question = toQuestion(name, opts) while (true) { - const answer = await this[kPrompt].prompt(question) + const answer = await this[kPrompt](question) const value = typecast(answer[name]) if (value === '') break if (question.type === 'number' && isNaN(value)) break diff --git a/lib/command/invert-when.js b/lib/command/invert-when.js deleted file mode 100644 index bf846f4..0000000 --- a/lib/command/invert-when.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' -const kindOf = require('mout/lang/kindOf') -/** - * In switching from inquierer to enquierer - * the way to skip a question was inverted from the positive - * to the negetive (skip vs when). This maintains the behavior of when - **/ - -module.exports = invertWhen - -function invertWhen(fn) { - if (fn === undefined || fn === null) return undefined - - return (...args) => { - switch (kindOf(fn)) { - case 'Boolean': { - return !fn - } - case 'AsyncFunction': { - return fn(...args).then((value) => { return !value }) - } - - case 'Function': { - const value = fn(...args) - return !value - } - } - } -} diff --git a/lib/conf.js b/lib/conf.js index 301b4f7..818bfd3 100644 --- a/lib/conf.js +++ b/lib/conf.js @@ -33,7 +33,6 @@ try { , ...override , help: help } - debug(config) } catch (e) { debug('unable to load configuration. using config', e) } diff --git a/package.json b/package.json index 5588cb3..0ee2012 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "cliui": "^9.0.1", "clone": "^2.1.2", "debug": "^4.3.2", - "enquirer": "^2.4.1", + "inquirer": "^7.1.0", "mout": "^1.2.2", "nopt": "^9.0.0", "ora": "^9.3.0", diff --git a/test/command.js b/test/command.js index 9b7fed5..387d66a 100644 --- a/test/command.js +++ b/test/command.js @@ -581,23 +581,16 @@ test('command', async (t) => { }) t.test('manual prompt', async (t) => { - const mockEnquirer = { - prompt: async () => { - // Simulate user input - return the expected values - return {option: 'yes'} - } - } - const cmd = new Command({ - enquirer: mockEnquirer - , run: async function() { - const value = await this.prompt({ + run: async function() { + const promise = this.prompt({ type: 'input' , name: 'option' , message: 'do you want this option' }) - return value + promise.ui.rl.emit('line', 'yes') + return promise } }) @@ -617,23 +610,8 @@ test('command', async (t) => { }) t.test('interactive command success', async (t) => { - const mockEnquirer = { - prompt: async (question) => { - switch (question.name) { - case 'fake': { - return {fake: ['yes']} - } - case 'other': { - return {other: ''} - } - - } - return {} - } - } const cmd = new Command({ interactive: true - , enquirer: mockEnquirer , strict: true , args: ['--interactive'] , requires_one: ['fake', 'other'] @@ -657,7 +635,14 @@ test('command', async (t) => { // only used for `fake` because it's an array value return ['yes'] } + const prompt = cmd.prompt + cmd.prompt = function(arg) { + const promise = prompt.call(cmd, arg) + promise.ui.activePrompt.done('') + promise.ui.activePrompt.close() + return promise + } const answers = await cmd.run() t.match(answers, { fake: ['yes'] @@ -666,24 +651,8 @@ test('command', async (t) => { }) t.test('interactive command success with `required_with`', async (t) => { - const mockEnquirer = { - prompt: async (question) => { - switch (question.name) { - case 'fake': { - return {fake: 'yes'} - } - case 'other': { - return {other: 'yes'} - } - - } - return {} - } - } - const cmd = new Command({ interactive: true - , enquirer: mockEnquirer , strict: true , args: ['--interactive'] , flags: { @@ -702,6 +671,15 @@ test('command', async (t) => { } }) + const prompt = cmd.prompt + + cmd.prompt = function(arg) { + const promise = prompt.call(cmd, arg) + promise.ui.activePrompt.done('yes') + promise.ui.activePrompt.close() + return promise + } + const answers = await cmd.run() t.match(answers, { fake: 'yes' @@ -710,23 +688,8 @@ test('command', async (t) => { }) t.test('interactive command that fails with required_without', async (t) => { - const mockEnquirer = { - prompt: async (question) => { - switch (question.name) { - case 'fake': { - return {fake: 'yes'} - } - case 'other': { - return {other: 'yes'} - } - - } - return {} - } - } const cmd = new Command({ interactive: true - , enquirer: mockEnquirer , strict: true , args: ['--interactive'] , flags: { @@ -745,6 +708,15 @@ test('command', async (t) => { } }) + const prompt = cmd.prompt + + cmd.prompt = function(arg) { + const promise = prompt.call(cmd, arg) + promise.ui.activePrompt.done('yes') + promise.ui.activePrompt.close() + return promise + } + t.rejects(cmd.run(), { code: 'EMUTEXCLUSIVE' , message: '`other` is mutually exclusive with fake. Erroneously set: fake' @@ -752,23 +724,8 @@ test('command', async (t) => { }) t.test('interactive command that fails with required_with', async (t) => { - const mockEnquirer = { - prompt: async (question) => { - switch (question.name) { - case 'fake': { - return {} - } - case 'other': { - return {other: 'yes'} - } - - } - return {} - } - } const cmd = new Command({ interactive: true - , enquirer: mockEnquirer , strict: true , args: ['--interactive'] , flags: { @@ -786,6 +743,20 @@ test('command', async (t) => { return data } }) + + const prompt = cmd.prompt + + cmd.prompt = function(arg) { + const promise = prompt.call(cmd, arg) + if (arg.name === 'fake') { + promise.ui.activePrompt.done('') + } else { + promise.ui.activePrompt.done('yes') + } + promise.ui.activePrompt.close() + return promise + } + t.rejects(cmd.run(), { code: 'EMUTINCLUSIVE' , message: '`other` is mutually inclusive with fake. Not set: fake' @@ -794,20 +765,9 @@ test('command', async (t) => { t.test('interactive command with custom flag `validate`', async (t) => { let validate_call_count = 0 - const mockEnquirer = { - prompt: async (question) => { - switch (question.name) { - case 'other': { - return {other: 'my_string'} - } - } - return {} - } - } const cmd = new Command({ interactive: true - , enquirer: mockEnquirer , strict: true , args: ['--interactive'] , flags: { @@ -834,6 +794,14 @@ test('command', async (t) => { } }) + const prompt = cmd.prompt + + cmd.prompt = function(arg) { + const promise = prompt.call(cmd, arg) + promise.ui.activePrompt.done('my_string') + promise.ui.activePrompt.close() + return promise + } const answer = await cmd.run() t.equal(answer, 'my_string', 'run function produced the right result') t.equal(validate_call_count, 1, 'validate function was only called once') @@ -900,15 +868,15 @@ test('command', async (t) => { t.match(out, [{ type: 'confirm' , message: 'one: boolean flag' - , skip: Function + , when: Function , validate: undefined - , result: undefined + , filter: undefined }, { type: 'number' , message: /no description/ig , when: undefined , validate: undefined - , result: Function + , filter: Function }]) }) diff --git a/test/flag-to-prompt.js b/test/flag-to-prompt.js index bdb35af..09880ce 100644 --- a/test/flag-to-prompt.js +++ b/test/flag-to-prompt.js @@ -33,12 +33,12 @@ test('flagToPrompt', async (t) => { t.match(out, { name: 'foobar' - , type: 'multiselect' + , type: 'checkbox' , message: 'foobar: hello world' , choices: ['one'] - , skip: Function - , result: Function - , format: Function + , when: Function + , filter: Function + , transformer: Function }) }) }).catch(threw) diff --git a/test/flag-type.js b/test/flag-type.js index 6d66429..7112ee0 100644 --- a/test/flag-type.js +++ b/test/flag-type.js @@ -15,8 +15,8 @@ test('flagType', async (t) => { , [{type: url}, 'input', 'url === input'] , [{type: [Number, Array]}, 'number', '[Number, Array] === number'] , [{type: String, mask: true}, 'password', 'mask=true === password'] - , [{type: String, choices: []}, 'select', 'choices === select'] - , [{type: String, choices: [], multi: true}, 'multiselect', 'choices + multi === multiselect'] + , [{type: String, choices: []}, 'list', 'choices === list'] + , [{type: String, choices: [], multi: true}, 'checkbox', 'choices + multi === checkbox'] // eslint-disable-line max-len , [{type: Function}, 'input', 'unexpected type === input'] ] diff --git a/test/invert-when.js b/test/invert-when.js deleted file mode 100644 index 79f841a..0000000 --- a/test/invert-when.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict' - -const test = require('tap').test -const invertWhen = require('../lib/command/invert-when') - -function trueFn() { - return true -} - -function falseFn() { - return false -} - -async function asyncTrueFn() { - return true -} - -async function asyncFalseFn() { - return false -} - -function fnWithArgs(arg1, arg2) { - return arg1 && arg2 -} - -async function asyncFnWithArgs(arg1, arg2) { - return arg1 && arg2 -} - -test('invertWhen', async (t) => { - t.test('should return undefined when fn is explicitly undefined or null', async (t) => { - t.equal(invertWhen(undefined), undefined) - t.equal(invertWhen(null), undefined) - }) - - t.test('should return a function that inverts boolean values', async (t) => { - const invertedTrue = invertWhen(true) - const invertedFalse = invertWhen(false) - - // The function should return a function that when called, returns the inverted value - t.equal(invertedTrue(), false) - t.equal(invertedFalse(), true) - }) - - t.test('should invert function results', async (t) => { - const invertedTrue = invertWhen(trueFn) - const invertedFalse = invertWhen(falseFn) - - t.equal(invertedTrue(), false) - t.equal(invertedFalse(), true) - }) - - t.test('should invert async function results', async (t) => { - const invertedAsyncTrue = invertWhen(asyncTrueFn) - const invertedAsyncFalse = invertWhen(asyncFalseFn) - - t.equal(await invertedAsyncTrue(), false) - t.equal(await invertedAsyncFalse(), true) - }) - - t.test('should handle function with arguments', async (t) => { - const invertedFn = invertWhen(fnWithArgs) - - t.equal(invertedFn(true, true), false) - t.equal(invertedFn(true, false), true) - t.equal(invertedFn(false, false), true) - }) - - t.test('should handle async function with arguments', async (t) => { - const invertedAsyncFn = invertWhen(asyncFnWithArgs) - - t.equal(await invertedAsyncFn(true, true), false) - t.equal(await invertedAsyncFn(true, false), true) - t.equal(await invertedAsyncFn(false, false), true) - }) -}) - From 38d9a2a02c803a01d5665108f392a9f336d1da2f Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Sun, 29 Mar 2026 22:52:38 -0500 Subject: [PATCH 2/6] chore(dep-dev): semantic-release@25 update semantic release to the latest to support trusted publishing --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0ee2012..20ef7fd 100644 --- a/package.json +++ b/package.json @@ -102,11 +102,13 @@ }, "devDependencies": { "@codedependant/release-config-npm": "^1.0.3", - "@semantic-release/exec": "^5.0.0", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^7.1.0", + "@semantic-release/git": "^10.0.1", "@vuepress/plugin-back-to-top": "^1.5.4", "eslint": "^10.1.0", "eslint-config-logdna": "^8.0.1", - "semantic-release": "^17.4.2", + "semantic-release": "^25.0.3", "tap": "^21.6.2", "vuepress": "^1.8.2" }, From 0e359037b10dad7d1df9e132bbad30b09243ff2a Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Sun, 29 Mar 2026 23:22:50 -0500 Subject: [PATCH 3/6] chore(ci): update release for npm trusted publisher Adds the setup required for semantic release to work with npm trusted publisheers --- .github/workflows/release.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85bdee1..1fcc847 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,20 +32,24 @@ jobs: name: release needs: test runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + issues: write + pull-requests: write steps: - name: Checkout uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 22 + node-version: 24 - run: npm install - name: Publish run: npm run release env: - GITHUB_TOKEN: ${{ secrets.PACKAGES_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_AUTHOR_NAME: 'Dependant Bot' GIT_AUTHOR_EMAIL: 'release-bot@codedependant.net' GIT_COMMITTER_NAME: 'Dependant Bot' GIT_COMMITTER_EMAIL: 'release-bot@codedependant.net' - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} From cf09b2b10a4ed7896163dcfc574abca53d4007de Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 30 Mar 2026 23:00:27 -0500 Subject: [PATCH 4/6] chore(deps): @inquirer/prompts@8.3.2 replaces the inquirer package for it successor @insquirer/prompts BREAKING CHANGE: The filter function is handled by seeli, not inquirer --- lib/command/flag-to-prompt.js | 13 ++- lib/command/flag-type.js | 2 +- lib/command/index.js | 45 +++++++---- lib/command/invert-when.js | 29 +++++++ lib/command/prompt-map.js | 14 ++++ lib/conf.js | 2 + package.json | 3 +- test/command.js | 146 +++++++++++++++++++++++++--------- test/flag-to-prompt.js | 4 +- test/flag-type.js | 2 +- test/invert-when.js | 77 ++++++++++++++++++ 11 files changed, 275 insertions(+), 62 deletions(-) create mode 100644 lib/command/invert-when.js create mode 100644 lib/command/prompt-map.js create mode 100644 test/invert-when.js diff --git a/lib/command/flag-to-prompt.js b/lib/command/flag-to-prompt.js index 5cd47fe..8aa099a 100644 --- a/lib/command/flag-to-prompt.js +++ b/lib/command/flag-to-prompt.js @@ -1,20 +1,27 @@ 'use strict' const flagType = require('./flag-type') +const invertWhen = require('./invert-when') module.exports = flagToPrompt +function toChoice(choice) { + return typeof choice === 'string' ? {name: choice, value: choice} : choice +} + function flagToPrompt(name, opt = {}) { const display = name.replace(':', ' ') + const flag_type = flagType(opt) return { - 'type': flagType(opt) + 'type': flag_type , 'name': name , 'message': display + ': ' + (opt.description || '(no description)') - , 'choices': opt.choices + , 'choices': Array.isArray(opt.choices) ? opt.choices.map(toChoice) : undefined , 'default': opt.default || null - , 'when': opt.when + , 'skip': invertWhen(opt.when) , 'filter': opt.filter , 'transformer': opt.transformer + , 'mask': flag_type === 'password' ? true : undefined } } diff --git a/lib/command/flag-type.js b/lib/command/flag-type.js index d11f445..4088522 100644 --- a/lib/command/flag-type.js +++ b/lib/command/flag-type.js @@ -15,7 +15,7 @@ function flagType(flag) { if (flag.mask) return 'password' if (flag.choices) { if (flag.multi) return 'checkbox' - return 'list' + return 'select' } return 'input' diff --git a/lib/command/index.js b/lib/command/index.js index 0dd86cb..d8f5c09 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -7,7 +7,7 @@ * @requires options * @requires events * @requires os - * @requires inquirer + * @requires @inquirer/prompts * @requires nopt * @requires chalk * @requires debug @@ -22,7 +22,6 @@ **/ const tty = require('node:tty') -const inquirer = require('inquirer') const {default: spinners} = require('cli-spinners') const {default: ora} = require('ora') const nopt = require('nopt') @@ -45,7 +44,7 @@ const colorize = require('../colorize') const stop_flags = new Set(['help', 'interactive', 'skip', 'color']) const ARGV = 'argv' const on_exp = /^on([A-z])/ -const kPrompt = Symbol.for('kPrompt') +const PROMPTS = require('./prompt-map') function noop() { return Promise.resolve(null) @@ -152,7 +151,6 @@ class Command extends Registry { this._optcache = null this.parsed = null this.options = Object.create(null) - this[kPrompt] = inquirer.createPromptModule() this.reset() const subcommands = options.reduce((acc, opts) => { @@ -382,7 +380,7 @@ class Command extends Registry { // If the flag has a validation function, call it now to possibly terminate early. // Pass the shape of `this.parsed` to match the parameter from non-interactive mode. if (typeof current.validate === 'function') { - const is_valid = current.validate.call(this, {...this.parsed, ...answers}) + const is_valid = await current.validate.call(this, {...this.parsed, ...answers}) throwIfFlagFailedValidation(flag, is_valid) } } @@ -636,9 +634,26 @@ class Command extends Registry { * @param {Object} options Inquirer prompt options * @returns {Promise} Promise object representing the end user input from the question **/ - prompt(opts) { - const prompt = this[kPrompt] - return prompt(opts) + async prompt(opts) { + const answers = {} + const theme = conf.get('theme') || {} + for (const question of toArray(opts)) { + const activePrompt = PROMPTS.get(question.type) + try { + const value = await activePrompt({theme, ...question}) + answers[question.name] = isFunction(question.filter) + ? question.filter(value) + : value + } catch (err) { + if (err?.name === 'ExitPromptError') { + const exit = conf.get('exitOnCancel') + if (exit) return process.exit(0) + } + throw err + } + } + + return answers } /** @@ -679,7 +694,7 @@ class Command extends Registry { const question = toQuestion(name, opts) while (true) { - const answer = await this[kPrompt](question) + const answer = await this.prompt(question) const value = typecast(answer[name]) if (value === '') break if (question.type === 'number' && isNaN(value)) break @@ -739,8 +754,7 @@ class Command extends Registry { module.exports = Command /* c8 ignore start */ -function transform(input, answers, status) { - if (!status.isFinal) return input +function transform(input) { if (this.type === 'number' && isNaN(input)) return '' return chalk.cyan(input) } @@ -758,10 +772,11 @@ function toQuestion(flag, opts, answers) { // TODO(esatterwhite) // wrap validate to throw returned errors so `ask` // can return them - arg.when = opts.when ? opts.when.bind(null, answers) : undefined - arg.validate = opts.validate ? opts.validate.bind(null, answers) : undefined - arg.filter = opts.filter ? opts.filter.bind(null) : undefined - arg.transformer = opts.transformer ? opts.transformer : transform.bind(arg) + arg.when = isFunction(opts.when) ? opts.when.bind(null, answers) : undefined + arg.validate = isFunction(opts.validate) ? opts.validate.bind(null, answers) : undefined + arg.filter = isFunction(opts.filter) ? opts.filter.bind(null) : undefined + arg.transformer = isFunction(opts.transformer) ? opts.transformer : transform.bind(arg) + return arg } diff --git a/lib/command/invert-when.js b/lib/command/invert-when.js new file mode 100644 index 0000000..bf846f4 --- /dev/null +++ b/lib/command/invert-when.js @@ -0,0 +1,29 @@ +'use strict' +const kindOf = require('mout/lang/kindOf') +/** + * In switching from inquierer to enquierer + * the way to skip a question was inverted from the positive + * to the negetive (skip vs when). This maintains the behavior of when + **/ + +module.exports = invertWhen + +function invertWhen(fn) { + if (fn === undefined || fn === null) return undefined + + return (...args) => { + switch (kindOf(fn)) { + case 'Boolean': { + return !fn + } + case 'AsyncFunction': { + return fn(...args).then((value) => { return !value }) + } + + case 'Function': { + const value = fn(...args) + return !value + } + } + } +} diff --git a/lib/command/prompt-map.js b/lib/command/prompt-map.js new file mode 100644 index 0000000..43ad6eb --- /dev/null +++ b/lib/command/prompt-map.js @@ -0,0 +1,14 @@ +'use strict' + +const {checkbox, confirm, input, number, password, select} = require('@inquirer/prompts') + +module.exports = new Map([ + ['checkbox', select] +, ['confirm', confirm] +, ['input', input] +, ['number', number] +, ['password', password] +, ['select', checkbox] +, ['multiselect', checkbox] +, ['text', input] +]) diff --git a/lib/conf.js b/lib/conf.js index 818bfd3..22e739b 100644 --- a/lib/conf.js +++ b/lib/conf.js @@ -19,6 +19,8 @@ let config = { , help: path.resolve(path.join(__dirname, 'commands', 'help')) , exitOnError: false , exitOnContent: false +, exitOnCancel: true +, theme: {} } try { diff --git a/package.json b/package.json index 20ef7fd..c10a498 100644 --- a/package.json +++ b/package.json @@ -86,13 +86,13 @@ }, "homepage": "https://github.com/esatterwhite/node-seeli", "dependencies": { + "@inquirer/prompts": "^8.3.2", "abbrev": "^4.0.0", "chalk": "^5.6.2", "cli-spinners": "^3.4.0", "cliui": "^9.0.1", "clone": "^2.1.2", "debug": "^4.3.2", - "inquirer": "^7.1.0", "mout": "^1.2.2", "nopt": "^9.0.0", "ora": "^9.3.0", @@ -102,6 +102,7 @@ }, "devDependencies": { "@codedependant/release-config-npm": "^1.0.3", + "@inquirer/testing": "^3.3.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", diff --git a/test/command.js b/test/command.js index 387d66a..dfa7898 100644 --- a/test/command.js +++ b/test/command.js @@ -4,12 +4,14 @@ const fs = require('fs') const path = require('path') const assert = require('assert') const strip = require('strip-ansi') +const {input} = require('@inquirer/prompts') +const {render} = require('@inquirer/testing') const cli = require('../') const Command = require('../lib/command') const commands = require('../lib/commands') +const PROMPTS = require('../lib/command/prompt-map') const Help = require('../lib/commands/help') const test = require('tap').test - test('command', async (t) => { // description parsing @@ -583,17 +585,30 @@ test('command', async (t) => { t.test('manual prompt', async (t) => { const cmd = new Command({ run: async function() { - const promise = this.prompt({ + const answer = await this.prompt({ type: 'input' , name: 'option' , message: 'do you want this option' }) - promise.ui.rl.emit('line', 'yes') - return promise + return answer } }) + const prompt = cmd.prompt + + t.teardown(() => { + cmd.prompt = prompt + }) + + cmd.prompt = async function(options) { + const {answer, events} = await render(input, { + message: options.message + }) + events.type('yes') + events.keypress('enter') + return {[options.name]: await answer} + } const output = await cmd.run() t.match(output, {option: 'yes'}) }) @@ -635,13 +650,11 @@ test('command', async (t) => { // only used for `fake` because it's an array value return ['yes'] } - const prompt = cmd.prompt cmd.prompt = function(arg) { - const promise = prompt.call(cmd, arg) - promise.ui.activePrompt.done('') - promise.ui.activePrompt.close() - return promise + // Mock the prompt to return predefined answers + const answer = arg.name === 'fake' ? 'yes' : undefined + return Promise.resolve({[arg.name]: answer}) } const answers = await cmd.run() t.match(answers, { @@ -672,12 +685,28 @@ test('command', async (t) => { }) const prompt = cmd.prompt + const input = PROMPTS.get('input') - cmd.prompt = function(arg) { - const promise = prompt.call(cmd, arg) - promise.ui.activePrompt.done('yes') - promise.ui.activePrompt.close() + // shim the input prompt + PROMPTS.set('input', function(options) { + const promise = render(input, options) return promise + }) + + t.teardown(() => { + cmd.prompt = prompt + PROMPTS.set('input', input) + }) + + cmd.prompt = async function(arg) { + // returns the wrapped prompts for progmattic control + const result = await prompt.call(cmd, arg) + + // Mock the prompt to return predefined answers + result[arg.name].events.type('yes') + result[arg.name].events.keypress('enter') + const ans = await result[arg.name].answer + return {[arg.name]: ans} } const answers = await cmd.run() @@ -709,12 +738,26 @@ test('command', async (t) => { }) const prompt = cmd.prompt + const input = PROMPTS.get('input') - cmd.prompt = function(arg) { - const promise = prompt.call(cmd, arg) - promise.ui.activePrompt.done('yes') - promise.ui.activePrompt.close() - return promise + // shim the input prompt + PROMPTS.set('input', function(options) { + return render(input, options) + }) + + t.teardown(() => { + cmd.prompt = prompt + PROMPTS.set('input', input) + }) + + cmd.prompt = async function(arg) { + // returns the wrapped prompts for progmattic control + const result = await prompt.call(cmd, arg) + + // Mock the prompt to return predefined answers + result[arg.name].events.type('yes') + result[arg.name].events.keypress('enter') + return {[arg.name]: await result[arg.name].answer} } t.rejects(cmd.run(), { @@ -745,16 +788,28 @@ test('command', async (t) => { }) const prompt = cmd.prompt + const input = PROMPTS.get('input') - cmd.prompt = function(arg) { - const promise = prompt.call(cmd, arg) - if (arg.name === 'fake') { - promise.ui.activePrompt.done('') - } else { - promise.ui.activePrompt.done('yes') - } - promise.ui.activePrompt.close() - return promise + // shim the input prompt + PROMPTS.set('input', function(options) { + return render(input, options) + }) + + t.teardown(() => { + cmd.prompt = prompt + PROMPTS.set('input', input) + }) + + cmd.prompt = async function(arg) { + const input = arg.name === 'fake' ? '' : 'yes' + + // returns the wrapped prompts for progmattic control + const result = await prompt.call(cmd, arg) + + result[arg.name].events.type(input) + result[arg.name].events.keypress('enter') + // Mock the prompt to return predefined answers + return {[arg.name]: await result[arg.name].answer} } t.rejects(cmd.run(), { @@ -774,16 +829,16 @@ test('command', async (t) => { other: { type: String , description: 'other flag' - , validate: (cmd) => { - t.match(cmd, { - interactive: true + , validate: async (value) => { + t.match(value, { + other: 'my_string' , argv: { remain: [] , cooked: ['--interactive'] , original: ['--interactive'] } + , interactive: true , color: true - , other: 'my_string' }, 'cmd appears to be `this.parsed`') validate_call_count += 1 } @@ -795,13 +850,27 @@ test('command', async (t) => { }) const prompt = cmd.prompt + const input = PROMPTS.get('input') - cmd.prompt = function(arg) { - const promise = prompt.call(cmd, arg) - promise.ui.activePrompt.done('my_string') - promise.ui.activePrompt.close() - return promise + // shim the input prompt + PROMPTS.set('input', function(options) { + return render(input, options) + }) + + t.teardown(() => { + cmd.prompt = prompt + PROMPTS.set('input', input) + }) + + cmd.prompt = async function(arg) { + const answer = 'my_string' + const result = await prompt.call(cmd, arg) + + result[arg.name].events.type(answer) + result[arg.name].events.keypress('enter') + return {[arg.name]: await result[arg.name].answer} } + const answer = await cmd.run() t.equal(answer, 'my_string', 'run function produced the right result') t.equal(validate_call_count, 1, 'validate function was only called once') @@ -864,17 +933,16 @@ test('command', async (t) => { const out = cmd.toPrompt() t.ok(Array.isArray(out), 'output is an array') t.equal(out.length, 2, 'expected prompt count') - t.match(out, [{ type: 'confirm' , message: 'one: boolean flag' - , when: Function + , skip: Function , validate: undefined , filter: undefined }, { type: 'number' , message: /no description/ig - , when: undefined + , skip: undefined , validate: undefined , filter: Function }]) diff --git a/test/flag-to-prompt.js b/test/flag-to-prompt.js index 09880ce..d605c42 100644 --- a/test/flag-to-prompt.js +++ b/test/flag-to-prompt.js @@ -35,8 +35,8 @@ test('flagToPrompt', async (t) => { name: 'foobar' , type: 'checkbox' , message: 'foobar: hello world' - , choices: ['one'] - , when: Function + , choices: [{name: 'one', value: 'one'}] + , skip: Function , filter: Function , transformer: Function }) diff --git a/test/flag-type.js b/test/flag-type.js index 7112ee0..1a0879e 100644 --- a/test/flag-type.js +++ b/test/flag-type.js @@ -15,7 +15,7 @@ test('flagType', async (t) => { , [{type: url}, 'input', 'url === input'] , [{type: [Number, Array]}, 'number', '[Number, Array] === number'] , [{type: String, mask: true}, 'password', 'mask=true === password'] - , [{type: String, choices: []}, 'list', 'choices === list'] + , [{type: String, choices: []}, 'select', 'choices === select'] , [{type: String, choices: [], multi: true}, 'checkbox', 'choices + multi === checkbox'] // eslint-disable-line max-len , [{type: Function}, 'input', 'unexpected type === input'] ] diff --git a/test/invert-when.js b/test/invert-when.js new file mode 100644 index 0000000..79f841a --- /dev/null +++ b/test/invert-when.js @@ -0,0 +1,77 @@ +'use strict' + +const test = require('tap').test +const invertWhen = require('../lib/command/invert-when') + +function trueFn() { + return true +} + +function falseFn() { + return false +} + +async function asyncTrueFn() { + return true +} + +async function asyncFalseFn() { + return false +} + +function fnWithArgs(arg1, arg2) { + return arg1 && arg2 +} + +async function asyncFnWithArgs(arg1, arg2) { + return arg1 && arg2 +} + +test('invertWhen', async (t) => { + t.test('should return undefined when fn is explicitly undefined or null', async (t) => { + t.equal(invertWhen(undefined), undefined) + t.equal(invertWhen(null), undefined) + }) + + t.test('should return a function that inverts boolean values', async (t) => { + const invertedTrue = invertWhen(true) + const invertedFalse = invertWhen(false) + + // The function should return a function that when called, returns the inverted value + t.equal(invertedTrue(), false) + t.equal(invertedFalse(), true) + }) + + t.test('should invert function results', async (t) => { + const invertedTrue = invertWhen(trueFn) + const invertedFalse = invertWhen(falseFn) + + t.equal(invertedTrue(), false) + t.equal(invertedFalse(), true) + }) + + t.test('should invert async function results', async (t) => { + const invertedAsyncTrue = invertWhen(asyncTrueFn) + const invertedAsyncFalse = invertWhen(asyncFalseFn) + + t.equal(await invertedAsyncTrue(), false) + t.equal(await invertedAsyncFalse(), true) + }) + + t.test('should handle function with arguments', async (t) => { + const invertedFn = invertWhen(fnWithArgs) + + t.equal(invertedFn(true, true), false) + t.equal(invertedFn(true, false), true) + t.equal(invertedFn(false, false), true) + }) + + t.test('should handle async function with arguments', async (t) => { + const invertedAsyncFn = invertWhen(asyncFnWithArgs) + + t.equal(await invertedAsyncFn(true, true), false) + t.equal(await invertedAsyncFn(true, false), true) + t.equal(await invertedAsyncFn(false, false), true) + }) +}) + From 51fb0da53366799a91da68357a05de42950345fb Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 30 Mar 2026 23:01:55 -0500 Subject: [PATCH 5/6] chore(pkg): include basic type definitions includes type definitions for the registry, command and seeli classes --- .gitignore | 3 +- jsconfig.json | 19 +++++++ lib/command/index.d.ts | 123 +++++++++++++++++++++++++++++++++++++++++ lib/command/index.js | 7 +++ lib/registry.d.ts | 60 ++++++++++++++++++++ lib/seeli.d.ts | 73 ++++++++++++++++++++++++ 6 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 jsconfig.json create mode 100644 lib/command/index.d.ts create mode 100644 lib/registry.d.ts create mode 100644 lib/seeli.d.ts diff --git a/.gitignore b/.gitignore index ad2d935..744354a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ cli.js pnpm-lock.yaml .tap -.tap-output/ +.tap-out +.vimspector.json diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..6415750 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,19 @@ +{ + "exclude": [ + "deployment", + "compose" + ], + "include": [ + "lib/**/*.js", + "*.js" + ], + "compilerOptions": { + "baseUrl": ".", + "moduleResolution": "Node", + "checkJs": false, + "target": "ES2025", + "module": "CommonJS", + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + } +} diff --git a/lib/command/index.d.ts b/lib/command/index.d.ts new file mode 100644 index 0000000..30bfd7b --- /dev/null +++ b/lib/command/index.d.ts @@ -0,0 +1,123 @@ +/** + * Base command class for creating interactive cli programs + * @module module:seeli/lib/command + * @author Eric Satterwhite + */ +declare class Command extends Map { + /** + * Constructs and returns the final command usage + */ + get usage(): string; + + /** + * The description of the command + */ + get description(): string; + + /** + * The final parsed out command line input as key/value pairs + */ + get argv(): any; + + /** + * Constructs and returns an object of flags and their types for consumption by the command + */ + get conf(): any; + + /** + * Maps and returns any shorthand switches to their parent flags for consumption by the command + */ + get shorthands(): any; + + /** + * Merges passing in object as configuration overrides + * @param options Configuration overrides to set + */ + setOptions(...opts: any[]): Command; + + /** + * Dispatches an event for each flag that has the event flag enabled + */ + dispatch(): Command; + + /** + * Method used to setup and execute the commands interactive mode + * @param cmd Optional argument for your command specific usage + * @param callback An optional callback to be executed when the command is complete. + */ + interactive(cmd?: any): Promise; + + /** + * Resets the internal command cache to its internal state + * @chainable + * @return Command + */ + reset(): Command; + + /** + * Executes the command as defined + * @param cmd Optional argument for your command specific usage + * @param callback An optional callback to be executed when the command is complete. + * Will be passed the contents return by the command + * @return String|undefined Will return the result from the command specific run directive if there is any. + */ + run(cmd?: any, depth?: number): Promise; + + /** + * Validates the current data set before running the command + * @param command The name of the command being executed + */ + validate(command?: string): void; + + /** + * Pass through function to inquirer for prompting input at the terminal + * @param options Inquirer prompt options + * @returns Promise object representing the end user input from the question + */ + prompt(options: any): Promise; + + /** + * Colorizes a text blob + * @param color The color to use. can be one of `red`, `blue`,`green`, `yellow`,`bold`, `grey`, `dim`, `black`, `magenta`, `cyan` + * @param text text to colorize + * @returns colorized version of the text + */ + colorize(color: string, text: string): string; + + /** + * Registers a new sub command + * @param command The command to register + */ + use(command: Command): Command; + + /** + * Convert all registered flags to inquierer compatible prompt objects + * @returns array of inquirer prompt objects + */ + toPrompt(): any[]; + + /** + * Get all available flags for this command + */ + get flags(): string[]; + + /** + * Get the command tree structure + */ + get tree(): any; + + /** + * Run a command directly + * @param args Arguments to pass to the command + */ + static run(...args: any[]): Promise; + + /** + * Constructor for Command class + * @param options instance configuration + */ + constructor(...options: any[]); +} + +export = Command; + diff --git a/lib/command/index.js b/lib/command/index.js index d8f5c09..f5f38ad 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -92,6 +92,10 @@ const defaults = { * @param {String} [options.ui="dots"] ui progress indicator * @param {Object} [options.flags] cli flags. top level keys will be used as the long hand flag * @param {Function} [options.run] A function that will be used as the primary drive of the command. It should perform what ever action the command was intended to do + * @param {Boolean} [options.strict=false] Enable strict mode for flag validation + * @param {Boolean} [options.interactive=true] Enable interactive mode by default + * @param {String} [options.name=null] The name of the command + * @param {Array} [options.commands=[]] Subcommands to register * @example var hello = new Command({ options:{ description:"diaplays a simple hello world command" @@ -192,6 +196,7 @@ class Command extends Registry { * constructs and retuns the final command usage * @property usage * @type string + * @returns {string} The formatted usage string **/ get usage() { return usage.from(this) @@ -201,6 +206,7 @@ class Command extends Registry { * The description of the command * @property description * @type String + * @returns {string} The command description **/ get description() { return this.options.description @@ -211,6 +217,7 @@ class Command extends Registry { * @property module:lib/command.Command.argv * @type object * @throws module:seeli/lib/exceptions/UnknownFlagException + * @returns {object} The parsed command line arguments **/ get argv() { if (this.parsed) return this.parsed diff --git a/lib/registry.d.ts b/lib/registry.d.ts new file mode 100644 index 0000000..bd74106 --- /dev/null +++ b/lib/registry.d.ts @@ -0,0 +1,60 @@ +/** + * Command registry for seeli + * @module index.js + * @author Eric Satterwhite + * @since 9.0.0 + */ +declare class Registry extends Map { + /** + * Constructor for Registry class + */ + constructor(); + + /** + * Resolve a command from a shallow key lookup + * @param key The key to resolve + * @returns The resolved command or undefined + */ + resolveShallow(key: any): any; + + /** + * Resolve a command from a full key lookup + * @param key The key to resolve + * @returns The resolved command or undefined + */ + resolve(key: any): any; + + /** + * Register a command + * @param name The name of the command + * @param cmd The command to register + */ + register(name: string, cmd: any): void; + + /** + * Unregister a command + * @param name The name of the command to unregister + * @returns The registry instance + */ + unregister(name: string): Registry; + + /** + * Get a list of all registered commands + * @returns Array of command names + */ + list(): string[]; + + /** + * Clear all registered commands + * @returns The registry instance + */ + clear(): Registry; + + /** + * Get the set of all command names + */ + get names(): Set; +} + +export = Registry; + diff --git a/lib/seeli.d.ts b/lib/seeli.d.ts new file mode 100644 index 0000000..f726b13 --- /dev/null +++ b/lib/seeli.d.ts @@ -0,0 +1,73 @@ +/** + * Seeli Entrypoint used for registering and managing commands + * @module module:seeli/lib/seeli + * @author Eric Satterwhite + */ +declare class Seeli extends Command { + /** + * Get configuration value + * @param args Arguments to pass to config.get + */ + static get(...args: any[]): any; + + /** + * Set configuration value + * @param args Arguments to pass to config.set + */ + static set(...args: any[]): any; + + /** + * Colorize text with a specific color + * @param txt Text to colorize + * @param color Color to use + */ + static colorize(txt: string, color: string): string; + + /** + * Get the Command class + */ + static get Command(): typeof Command; + + /** + * Get the Command class + */ + get Command(): typeof Command; + + /** + * Constructor for Seeli class + * @param args Arguments to pass to the constructor + */ + constructor(...args: any[]); + + /** + * Colorize text with a specific color + * @param txt Text to colorize + * @param color Color to use + */ + colorize(txt: string, color: string): string; + + /** + * Get or set configuration values + * @param args Arguments to pass to config + */ + config(...args: any[]): any; + + /** + * Run the Seeli application + */ + run(): void; + + /** + * Reset the Seeli instance + */ + reset(): Seeli; + + /** + * Load plugins + * @param args Plugin arguments + */ + plugin(...args: any[]): Seeli; +} + +export = Seeli; + From 3e3aa08f3cf99de8e1145d07b246f9723900d06b Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Tue, 31 Mar 2026 06:44:02 -0500 Subject: [PATCH 6/6] chore(doc): update documentation after inquire refactor There were references to affirmative / negative which have been removed --- README.md | 2 -- examples/commands/sub/hub.js | 2 +- gh-pages/guides/commands.md | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 2eeace6..234632b 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,6 @@ name | required | type | description **filter** | `false` | `function` | Receives the user input and return the filtered value to be used **inside** the program. The value returned will be added to the Answers hash. **required_with** | `false` | `Array` | A non-empty array which says that if the flag is set, then the specified other flags must also be set, i.e. "mutual inclusion." **required_without** | `false` | `Array` | A non-empty array which says that if the flag is set, then none of the other specified flags may also be set, i.e. "mutual exclusion." -**affirmative** | `false` | `String` | **interactive mode only** For `Boolean` flags, this is the value to display when the flag is true (default is `yes`) -**negative** | `false` | `String` | **interactive mode only** For `Boolean` flags, this is the value to display when the flag is true (default is `no`) ### Nested Flags diff --git a/examples/commands/sub/hub.js b/examples/commands/sub/hub.js index eaf3086..55afc6d 100644 --- a/examples/commands/sub/hub.js +++ b/examples/commands/sub/hub.js @@ -16,7 +16,7 @@ module.exports = new cli.Command({ } } , async run(cmd, data) { - return 'hub' + return data.ask ? 'hub' : 'no hub' } }) diff --git a/gh-pages/guides/commands.md b/gh-pages/guides/commands.md index 50c5203..9a9aec6 100644 --- a/gh-pages/guides/commands.md +++ b/gh-pages/guides/commands.md @@ -168,8 +168,6 @@ name | required | type | description **filter** | `false` | `function` | **interactive mode only** Receives the user input and return the filtered value to be used **inside** the program. The value returned will be added to the Answers hash. **required_with** | `false` | `Array` | A non-empty array which says that if the flag is set, then the specified other flags must also be set, i.e. "mutual inclusion." **required_without** | `false` | `Array` | A non-empty array which says that if the flag is set, then none of the other specified flags may also be set, i.e. "mutual exclusion." -**affirmative** | `false` | `String` | **interactive mode only** For `Boolean` flags, this is the value to display when the flag is true (default is `yes`) -**negative** | `false` | `String` | **interactive mode only** For `Boolean` flags, this is the value to display when the flag is true (default is `no`) ### Types