From e429636e1e9011f13e8d05fec2ca2568d0318619 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 23 Mar 2026 22:48:14 -0500 Subject: [PATCH 01/13] chore(deps): abbrev@4.0.0 update abbrev to the most recent versions to pulling recent features and bugfixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b0b472..e866ad3 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ }, "homepage": "https://github.com/esatterwhite/node-seeli", "dependencies": { - "abbrev": "^1.1.1", + "abbrev": "^4.0.0", "chalk": "^4.1.2", "cliui": "^7.0.4", "clone": "^2.1.2", From f183318241c122a7b1c40e433d4e98a87064b498 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 23 Mar 2026 22:52:26 -0500 Subject: [PATCH 02/13] chore(deps): chalk@5.6.2 pull in most recent chalk package for features and bugfixes --- lib/colorize.js | 2 +- lib/command/index.js | 2 +- lib/index.js | 2 +- lib/seeli.js | 2 +- lib/usage/from.js | 2 +- lib/usage/list.js | 2 +- package.json | 2 +- test/seeli.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/colorize.js b/lib/colorize.js index 4acbe6f..9d106d9 100644 --- a/lib/colorize.js +++ b/lib/colorize.js @@ -7,7 +7,7 @@ * @requires chalk * @requires seeli/lib/conf **/ -const chalk = require('chalk') +const {default: chalk} = require('chalk') const conf = require('./conf') const {InvalidColorModeException} = require('./exceptions') diff --git a/lib/command/index.js b/lib/command/index.js index 1231d08..100693b 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -24,7 +24,7 @@ const tty = require('tty') const inquirer = require('inquirer') const nopt = require('nopt') -const chalk = require('chalk') +const {default: chalk} = require('chalk') const strip = require('strip-ansi') const debug = require('debug') const toArray = require('mout/lang/toArray') diff --git a/lib/index.js b/lib/index.js index 0c6b117..817c440 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,7 +11,7 @@ * @requires seeli/lib/commands * @requires seeli/lib/conf **/ -const chalk = require('chalk') +const {default: chalk} = require('chalk') const Command = require('./command') const Seeli = require('./seeli') const commands = require('./commands') diff --git a/lib/seeli.js b/lib/seeli.js index 3bdc93f..9f844b6 100644 --- a/lib/seeli.js +++ b/lib/seeli.js @@ -6,7 +6,7 @@ * @requires seeli/lib/command * @requires seeli/lib/conf **/ -const chalk = require('chalk') +const {default: chalk} = require('chalk') const toArray = require('mout/lang/toArray') const kindOf = require('mout/lang/kindOf') const Command = require('./command') diff --git a/lib/usage/from.js b/lib/usage/from.js index a43ce08..6d62e84 100644 --- a/lib/usage/from.js +++ b/lib/usage/from.js @@ -17,7 +17,7 @@ const os = require('os') const cliui = require('cliui') const util = require('util') -const chalk = require('chalk') +const {default: chalk} = require('chalk') const width = require('string-width') const toArray = require('mout/lang/toArray') const isUndef = require('mout/lang/isUndefined') diff --git a/lib/usage/list.js b/lib/usage/list.js index eab3b70..6266df3 100644 --- a/lib/usage/list.js +++ b/lib/usage/list.js @@ -11,7 +11,7 @@ * @requires seeli/lib/conf **/ const os = require('os') -const chalk = require('chalk') +const {default: chalk} = require('chalk') const conf = require('../conf') const colorize = require('../colorize') const usage = chalk.white.bold diff --git a/package.json b/package.json index e866ad3..dfe03ec 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "homepage": "https://github.com/esatterwhite/node-seeli", "dependencies": { "abbrev": "^4.0.0", - "chalk": "^4.1.2", + "chalk": "^5.6.2", "cliui": "^7.0.4", "clone": "^2.1.2", "debug": "^4.3.2", diff --git a/test/seeli.js b/test/seeli.js index e0de7e1..e960eae 100644 --- a/test/seeli.js +++ b/test/seeli.js @@ -3,7 +3,7 @@ const os = require('os') const path = require('path') const {test} = require('tap') -const chalk = require('chalk') +const {default: chalk} = require('chalk') const cli = require('../') const config = require('../lib/conf.js') const Seeli = require('../lib/seeli.js') From 0a722bfb309592734c6a17b7b2756f67876ef001 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 23 Mar 2026 22:57:07 -0500 Subject: [PATCH 03/13] chore(deps): cliui@9.0.1 pickup most recent version of cliui for bugfixes and features --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfe03ec..a1b4ec2 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "dependencies": { "abbrev": "^4.0.0", "chalk": "^5.6.2", - "cliui": "^7.0.4", + "cliui": "^9.0.1", "clone": "^2.1.2", "debug": "^4.3.2", "inquirer": "^7.1.0", From 74a843e91f8a64cb5ae716707c1282786ee258b4 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 23 Mar 2026 22:59:43 -0500 Subject: [PATCH 04/13] chore(deps): nopt@9.0.0 updates nopt to pull in most recent features and bugfixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1b4ec2..7153202 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "debug": "^4.3.2", "inquirer": "^7.1.0", "mout": "^1.2.2", - "nopt": "^7.0.0", + "nopt": "^9.0.0", "ora": "^5.4.1", "pkg-up": "^3.1.0", "string-width": "^4.2.0", From e963b2a5e7b101ed45bd56c0fe53550ac4556da4 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Mon, 23 Mar 2026 23:02:28 -0500 Subject: [PATCH 05/13] chore(deps): string-width@8.2.0 update string-with from most recent features and bugfixes --- lib/usage/from.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/usage/from.js b/lib/usage/from.js index 6d62e84..a8bd69e 100644 --- a/lib/usage/from.js +++ b/lib/usage/from.js @@ -18,7 +18,7 @@ const os = require('os') const cliui = require('cliui') const util = require('util') const {default: chalk} = require('chalk') -const width = require('string-width') +const {default: width} = require('string-width') const toArray = require('mout/lang/toArray') const isUndef = require('mout/lang/isUndefined') const typeOf = require('./type-of') diff --git a/package.json b/package.json index 7153202..394c516 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "nopt": "^9.0.0", "ora": "^5.4.1", "pkg-up": "^3.1.0", - "string-width": "^4.2.0", + "string-width": "^8.2.0", "strip-ansi": "^6.0.0" }, "devDependencies": { From 6817c7e82d0974e4b0eec0b4c8d415532a49ea17 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Tue, 24 Mar 2026 00:10:27 -0500 Subject: [PATCH 06/13] chore(deps): ora@9.3.1 update ora and cli spinners, removing the local ora override. The newer versions are a pain to extend or override. This just inlines the spinner colorization bypassing the in build orz colorizer --- lib/command/index.js | 14 +++++++++++--- lib/ora.js | 28 ---------------------------- package.json | 6 +++--- 3 files changed, 14 insertions(+), 34 deletions(-) delete mode 100644 lib/ora.js diff --git a/lib/command/index.js b/lib/command/index.js index 100693b..2f10dc7 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -35,7 +35,8 @@ const toPrompt = require('./flag-to-prompt') const flagType = require('./flag-type') const Registry = require('../registry') const conf = require('../conf') -const ora = require('../ora') +const {default: ora} = require('ora') +const {default: spinners} = require('cli-spinners') const usage = require('../usage') const object = require('../lang/object') const typeOf = require('../usage/type-of') @@ -167,9 +168,16 @@ class Command extends Registry { this.setOptions(defaults, ...options) this._shcache = this.shorthands + const spinner = spinners[this.options.ui] + this.ui = ora({ - color: conf.get('color') - , spinner: this.options.ui + color: false + , spinner: { + interval: spinner.interval + , frames: spinner.frames.map((frame) => { + return colorize(frame) + }) + } , text: 'loading' , stream: process.stdout }) diff --git a/lib/ora.js b/lib/ora.js deleted file mode 100644 index aca8341..0000000 --- a/lib/ora.js +++ /dev/null @@ -1,28 +0,0 @@ -/* istanbul ignore file */ -'use strict' -const ora = require('ora') -const colorize = require('./colorize') - -const instance = ora() - -instance.constructor.prototype.frame = frame - -module.exports = ora - -function frame() { - const {frames} = this.spinner - let frame = frames[this.frameIndex] - - if (this.color) { - frame = colorize(frame, this.color) - } - - this.frameIndex = ++this.frameIndex % frames.length - const fullText = typeof this.text === 'string' ? ' ' + this.text : '' - const fullPrefixText = (typeof this.prefixText === 'string' && this.prefixText !== '') - ? this.prefixText + ' ' - : '' - - return fullPrefixText + frame + fullText -} - diff --git a/package.json b/package.json index 394c516..454d768 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "no-cond-assign": 0 }, "parserOptions": { - "ecmaVersion": 2020, + "ecmaVersion": 2022, "sourceType": "script" }, "ignorePatterns": [ @@ -88,13 +88,14 @@ "dependencies": { "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": "^5.4.1", + "ora": "^9.3.0", "pkg-up": "^3.1.0", "string-width": "^8.2.0", "strip-ansi": "^6.0.0" @@ -103,7 +104,6 @@ "@codedependant/release-config-npm": "^1.0.3", "@semantic-release/exec": "^5.0.0", "@vuepress/plugin-back-to-top": "^1.5.4", - "cli-spinners": "^2.6.0", "eslint": "^7.31.0", "eslint-config-codedependant": "^2.1.6", "semantic-release": "^17.4.2", From ece3179c75774867178e43a3deae3620c4bcb3d4 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Tue, 24 Mar 2026 00:21:19 -0500 Subject: [PATCH 07/13] chore(deps): package-up@5.0.0 package for finding the package json file of the host application when installed as a dependecy --- lib/conf.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/conf.js b/lib/conf.js index b6aa042..818bfd3 100644 --- a/lib/conf.js +++ b/lib/conf.js @@ -1,7 +1,7 @@ 'use strict' const path = require('path') -const pkgup = require('pkg-up') +const {packageUpSync} = require('package-up') const set = require('mout/object/set') const get = require('mout/object/get') const isObject = require('mout/lang/isPlainObject') @@ -23,7 +23,7 @@ let config = { try { const cwd = get(require, 'main.path') || CWD - const pkgjson = pkgup.sync({cwd}) + const pkgjson = packageUpSync({cwd}) debug('loading configuration from %s', pkgjson) const pkg = require(pkgjson) const override = pkg.seeli || {} diff --git a/package.json b/package.json index 454d768..5ea12e4 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "mout": "^1.2.2", "nopt": "^9.0.0", "ora": "^9.3.0", - "pkg-up": "^3.1.0", + "package-up": "^5.0.0", "string-width": "^8.2.0", "strip-ansi": "^6.0.0" }, From d54dc988b60ea80d30697fcac43f3323bc119565 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 06:37:38 -0500 Subject: [PATCH 08/13] fix(registry): make sure registry sets name if provided in the case the register function is passed a name, the command name should be set to that if it doesn't have one. --- lib/registry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/registry.js b/lib/registry.js index 055f4ef..5eae899 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -83,6 +83,8 @@ class Registry extends mix(Map, EventEmitter) { _command = _name _name = _command.options.name } + + if (!_command.options.name) _command.options.name = _name const abbrevs = abbrev(_name) const alias = _command.options.alias From 804350eb1195b239c9684b3f6949bf3a6db6de32 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 07:28:00 -0500 Subject: [PATCH 09/13] chore(deps)!: enquirer@2.4.1 Replaces the old inquirer package with enquirer. Inquirer has been abandonded and replaced with a meta package of sigurlar prompts that doesn't entirely do the same sort of thing. This replaces it with its spiratural successor equirer. There are some subtle differences but things remain functionally similar There is a known Problem with node 24 that Ctrl+C will throw an error BREAKING CHANGE: replaces inquirer with enquirer. See: https://github.com/enquirer/enquirer/issues/483 --- examples/commands/hello.js | 11 ++++- examples/commands/sub/hub.js | 4 +- lib/command/flag-to-prompt.js | 43 +++++++++++++++---- lib/command/flag-type.js | 11 +++-- lib/command/index.js | 16 ++++---- lib/command/invert-when.js | 29 +++++++++++++ lib/conf.js | 1 + package.json | 2 +- test/invert-when.js | 77 +++++++++++++++++++++++++++++++++++ 9 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 lib/command/invert-when.js create mode 100644 test/invert-when.js diff --git a/examples/commands/hello.js b/examples/commands/hello.js index 6279311..6dad62b 100644 --- a/examples/commands/hello.js +++ b/examples/commands/hello.js @@ -27,7 +27,9 @@ module.exports = new cli.Command({ 'type': Boolean , 'shorthand': 'e' , 'description': 'Say hello in a very excited manner' - , 'default': false + , 'default': true + , 'affirmative': 'Yes' + , 'negative': 'Nope' } , volume: { @@ -37,6 +39,13 @@ 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 eaf3086..3595542 100644 --- a/examples/commands/sub/hub.js +++ b/examples/commands/sub/hub.js @@ -13,10 +13,12 @@ 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 'hub' + return data.ask ? 'hub' : 'no hub' } }) diff --git a/lib/command/flag-to-prompt.js b/lib/command/flag-to-prompt.js index db922d3..df0c91b 100644 --- a/lib/command/flag-to-prompt.js +++ b/lib/command/flag-to-prompt.js @@ -1,19 +1,48 @@ 'use strict' const flagType = require('./flag-type') - +const kindOf = require('mout/lang/kindOf') module.exports = flagToPrompt -function flagToPrompt(name, opt) { +/** + * 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 + **/ +function invertWhen(fn) { + if (!fn) 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 + } + } + } +} + +function flagToPrompt(name, opt = {}) { const display = name.replace(':', ' ') + const t = flagType(opt) + + return { - 'type': flagType(opt) + 'type': t , 'name': name , 'message': display + ': ' + (opt.description || '(no description)') , 'choices': opt.choices - , 'default': opt.default || null - , 'when': opt.when - , 'filter': opt.filter - , 'transformer': opt.transformer + , 'initial': opt.default || null + , 'skip': invertWhen(opt.when) + , 'result': opt.filter + , 'format': opt.transformer + , 'affirmative': opt.affirmative + , 'negative': opt.negative } } diff --git a/lib/command/flag-type.js b/lib/command/flag-type.js index d11f445..4363cf3 100644 --- a/lib/command/flag-type.js +++ b/lib/command/flag-type.js @@ -10,12 +10,17 @@ function flagType(flag) { , multi: true }) } - if (flag.type === Boolean) return 'confirm' + if (flag.type === Boolean) { + if (flag.affirmative && flag.negative) { + return 'toggle' + } + return 'confirm' + } if (flag.type === Number) return 'number' if (flag.mask) return 'password' if (flag.choices) { - if (flag.multi) return 'checkbox' - return 'list' + if (flag.multi) return 'multiselect' + return 'select' } return 'input' diff --git a/lib/command/index.js b/lib/command/index.js index 2f10dc7..c61e9ae 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -21,8 +21,8 @@ * @requires seeli/lib/lang/object **/ -const tty = require('tty') -const inquirer = require('inquirer') +const tty = require('node:tty') +const Equirer = require('enquirer') const nopt = require('nopt') const {default: chalk} = require('chalk') const strip = require('strip-ansi') @@ -153,7 +153,7 @@ class Command extends Registry { this._optcache = null this.parsed = null this.options = Object.create(null) - this[kPrompt] = inquirer.createPromptModule() + this[kPrompt] = new Equirer() this.reset() const subcommands = options.reduce((acc, opts) => { @@ -372,14 +372,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) - Object.assign(answers, res) + answers[flag] = res[flag] // 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') { @@ -638,8 +638,8 @@ class Command extends Registry { * @returns {Promise} Promise object representing the end user input from the question **/ prompt(opts) { - const prompt = this[kPrompt] - return prompt(opts) + const enquirer = this[kPrompt] + return enquirer.prompt(opts) } /** @@ -680,7 +680,7 @@ class Command extends Registry { const question = toQuestion(name, opts) while (true) { - const answer = await this[kPrompt](question) + const answer = await this[kPrompt].prompt(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 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/conf.js b/lib/conf.js index 818bfd3..301b4f7 100644 --- a/lib/conf.js +++ b/lib/conf.js @@ -33,6 +33,7 @@ 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 5ea12e4..ff8f7b2 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "cliui": "^9.0.1", "clone": "^2.1.2", "debug": "^4.3.2", - "inquirer": "^7.1.0", + "enquirer": "^2.4.1", "mout": "^1.2.2", "nopt": "^9.0.0", "ora": "^9.3.0", 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 498d0e99876d5b2dadbe230e586428698800daaa Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 15:01:50 -0500 Subject: [PATCH 10/13] chore(dep-dev): tap@21.6.2 updates tap to the most recent version --- .gitignore | 2 + lib/command/index.js | 5 ++ package.json | 18 ++---- test/command.js | 141 +++++++++++++++++++++++++---------------- test/flag-to-prompt.js | 8 +-- test/flag-type.js | 4 +- test/lang.js | 10 +-- test/registry.js | 6 +- test/seeli.js | 30 ++++----- 9 files changed, 130 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index de03390..ad2d935 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ cli.js *.vim pnpm-lock.yaml +.tap +.tap-output/ diff --git a/lib/command/index.js b/lib/command/index.js index c61e9ae..359d825 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -167,6 +167,11 @@ 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] diff --git a/package.json b/package.json index ff8f7b2..33827a1 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ ], "license": "MIT", "engines": { - "node": ">=16.0.0" + "node": ">=22.0.0" }, "bugs": { "url": "https://github.com/esatterwhite/node-seeli/issues" @@ -107,13 +107,12 @@ "eslint": "^7.31.0", "eslint-config-codedependant": "^2.1.6", "semantic-release": "^17.4.2", - "tap": "^16.3.9", + "tap": "^21.6.2", "vuepress": "^1.8.2" }, "tap": { - "jsx": false, - "ts": false, "browser": false, + "show-full-coverage": true, "functions": 97, "lines": 97, "branches": 84, @@ -121,19 +120,14 @@ "coverage-report": [ "text", "text-summary", + "lcov", "json", + "json-summary", "html" ], + "output-file": ".tap-out", "files": [ "test/**/*.js" - ], - "nyc-arg": [ - "--exclude=test/", - "--exclude=examples/", - "--exclude=gh-pages/", - "--exclude=docs/", - "--exclude=release.config.js", - "--all" ] } } diff --git a/test/command.js b/test/command.js index 15939d1..86b0d2f 100644 --- a/test/command.js +++ b/test/command.js @@ -583,16 +583,23 @@ test('command', async (t) => { }) t.test('manual prompt', async (t) => { + const mockEnquirer = { + prompt: async (questions) => { + // Simulate user input - return the expected values + return { option: 'yes' } + } + } + const cmd = new Command({ + enquirer: mockEnquirer, run: async function() { - const promise = this.prompt({ + const value = await this.prompt({ type: 'input' , name: 'option' , message: 'do you want this option' }) - promise.ui.rl.emit('line', 'yes') - return promise + return value } }) @@ -612,8 +619,23 @@ 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'] @@ -629,6 +651,7 @@ test('command', async (t) => { } } , run: async function(_, data) { + debugger; return data } }) @@ -637,14 +660,8 @@ 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 - } + debugger; const answers = await cmd.run() t.match(answers, { fake: ['yes'] @@ -653,8 +670,24 @@ 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: { @@ -673,15 +706,7 @@ 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 - } - + debugger; const answers = await cmd.run() t.match(answers, { fake: 'yes' @@ -690,8 +715,23 @@ 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: { @@ -710,15 +750,6 @@ 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' @@ -726,8 +757,23 @@ 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: { @@ -745,20 +791,6 @@ 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' @@ -768,9 +800,20 @@ 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: { @@ -797,14 +840,6 @@ 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') @@ -871,15 +906,15 @@ test('command', async (t) => { t.match(out, [{ type: 'confirm' , message: 'one: boolean flag' - , when: Function + , skip: Function , validate: undefined - , filter: undefined + , result: undefined }, { type: 'number' , message: /no description/ig , when: undefined , validate: undefined - , filter: Function + , result: Function }]) }) diff --git a/test/flag-to-prompt.js b/test/flag-to-prompt.js index 09880ce..bdb35af 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: 'checkbox' + , type: 'multiselect' , message: 'foobar: hello world' , choices: ['one'] - , when: Function - , filter: Function - , transformer: Function + , skip: Function + , result: Function + , format: Function }) }) }).catch(threw) diff --git a/test/flag-type.js b/test/flag-type.js index d2a2f06..6d66429 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: []}, 'list', 'choices === list'] - , [{type: String, choices: [], multi: true}, 'checkbox', 'choices + multi === checkbox'] + , [{type: String, choices: []}, 'select', 'choices === select'] + , [{type: String, choices: [], multi: true}, 'multiselect', 'choices + multi === multiselect'] , [{type: Function}, 'input', 'unexpected type === input'] ] diff --git a/test/lang.js b/test/lang.js index 21bb0c6..079a965 100644 --- a/test/lang.js +++ b/test/lang.js @@ -10,7 +10,7 @@ const test = tap.test test('object', async (t) => { t.test('#set', async (tt) => { const a = object.set({}, 'a:b:c:d', 1) - tt.deepEqual(a, { + tt.same(a, { a: { b: { c: { @@ -21,7 +21,7 @@ test('object', async (t) => { }) const b = object.set({}, 'a,b,c,d', 2, ',') - tt.deepEqual(b, { + tt.same(b, { a: { b: { c: { @@ -36,9 +36,9 @@ test('object', async (t) => { const one = {a: {b: [1]}, c: path, d: url, x: {y: {z: 1}}} const two = {x: {y: {f: 3}}} const out = object.merge(one, two) - t.deepEqual(out.c, path, 'path module intact') - t.deepEqual(out.d, url, 'url module intact') - t.deepEqual(out, { + t.same(out.c, path, 'path module intact') + t.same(out.d, url, 'url module intact') + t.same(out, { a: {b: [1]} , c: path , d: url diff --git a/test/registry.js b/test/registry.js index 5b415e0..c12aee0 100644 --- a/test/registry.js +++ b/test/registry.js @@ -24,7 +24,7 @@ test('registry', async (t) => { registry.register(two) t.equal(registry.get('two'), two, 'registered property') - t.deepEqual(registry.list(), ['two'], 'registered commands') + t.same(registry.list(), ['two'], 'registered commands') t.equal(registry.get('tow'), two, 'registered alias') }) @@ -34,7 +34,7 @@ test('registry', async (t) => { registry.register('one', two) t.equal(registry.get('one'), two, 'registered property') - t.deepEqual(registry.list(), ['one'], 'registered commands') + t.same(registry.list(), ['one'], 'registered commands') t.equal(registry.get('tow'), two, 'registered alias') }) @@ -44,7 +44,7 @@ test('registry', async (t) => { t.equal(registry.get('one'), one, 'registered property') registry.unregister(one.options.name) - t.deepEqual(registry.list(), [], 'registered commands') + t.same(registry.list(), [], 'registered commands') t.notOk(registry.get('one')) t.doesNotThrow(() => { registry.unregister() diff --git a/test/seeli.js b/test/seeli.js index e960eae..57ba13b 100644 --- a/test/seeli.js +++ b/test/seeli.js @@ -15,7 +15,7 @@ test('cli', async (t) => { const seeli = new Seeli() t.notOk(seeli.config('foobar'), 'initial value not set') seeli.config('foobar', 1) - t.strictEqual(seeli.config('foobar'), 1, 'config value set') + t.equal(seeli.config('foobar'), 1, 'config value set') }) t.test('statics', async (t) => { @@ -23,26 +23,26 @@ test('cli', async (t) => { Seeli.set('foo', 'bar') Seeli.set({one: true}) const result = Seeli.get('one') - t.strictEqual(result, true, 'config value set') + t.equal(result, true, 'config value set') }) t.test('static get Command', async (t) => { - t.strictEqual(Seeli.Command, Command, 'returns base command class') + t.equal(Seeli.Command, Command, 'returns base command class') }) t.test('instance get Command', async (t) => { - t.strictEqual(new Seeli().Command, Command, 'returns base command class') + t.equal(new Seeli().Command, Command, 'returns base command class') }) t.test('colorize', async (t) => { const configured = config.get('color') - t.strictEqual( + t.equal( Seeli.colorize('hello') , chalk[configured]('hello') , 'no arguments returns configured color' ) - t.strictEqual( + t.equal( Seeli.colorize('hello', 'bold') , chalk.bold('hello') , 'second argument controls color output' @@ -123,7 +123,7 @@ test('cli', async (t) => { t.test('should allow commands to be registered by name', async (t) => { cli.use('test', TestCommand) - t.notEqual(cli.list.indexOf('test'), -1) + t.notSame(cli.list.indexOf('test'), -1) }) t.test('#list', async (t) => { @@ -132,8 +132,8 @@ test('cli', async (t) => { }) t.test('should only list top level commands', async (t) => { - t.notEqual(cli.list.indexOf('test'), -1) - t.notEqual(cli.list.indexOf('help'), -1) + t.notSame(cli.list.indexOf('test'), -1) + t.notSame(cli.list.indexOf('help'), -1) t.equal(cli.list.indexOf('h'), -1) t.equal(cli.list.indexOf('he'), -1) t.equal(cli.list.indexOf('hel'), -1) @@ -186,7 +186,7 @@ test('cli', async (t) => { path.join(FIXTURE_DIR, 'plugin-a.fixture') ]) const seeli = new Seeli() - t.strictEqual(seeli.config('plugin_a_fixture'), true, 'plugin path loaded') + t.equal(seeli.config('plugin_a_fixture'), true, 'plugin path loaded') }) t.test('require path loader', async (t) => { @@ -196,7 +196,7 @@ test('cli', async (t) => { ] }) - t.strictEqual(seeli.config('plugin_b_fixture'), false, 'plugin path loaded') + t.equal(seeli.config('plugin_b_fixture'), false, 'plugin path loaded') }) t.test('inline function loader', async (t) => { @@ -213,8 +213,8 @@ test('cli', async (t) => { plugins: [inline] }) - t.strictEqual(seeli.config('inline_plugin'), 1, 'inline plugin loaded') - t.deepEqual(seeli.list(), ['manual'], 'registered command list') + t.equal(seeli.config('inline_plugin'), 1, 'inline plugin loaded') + t.same(seeli.list(), ['manual'], 'registered command list') }) t.test('explicit call', async (t) => { @@ -230,8 +230,8 @@ test('cli', async (t) => { const seeli = new Seeli() seeli.plugin(outline) - t.strictEqual(seeli.config('outline_plugin'), true, 'inline plugin loaded') - t.deepEqual(seeli.list(), ['explicit'], 'registered command list') + t.equal(seeli.config('outline_plugin'), true, 'inline plugin loaded') + t.same(seeli.list(), ['explicit'], 'registered command list') }) t.test('invalid plugin type', async (t) => { From 3a0c2fb095f014983c2c21989119f769dfd2b668 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 15:14:48 -0500 Subject: [PATCH 11/13] chore(dep-dev): eslint-config-logdna@8.0.1 update for eslint 10+ and updated style rules applied --- eslint.config.js | 21 +++++++++++++++ lib/command/flag-to-prompt.js | 48 +++++++++-------------------------- lib/command/index.js | 13 ++++++---- lib/commands/help.js | 2 +- lib/lang/object/set.js | 1 - lib/registry.js | 1 - lib/seeli.js | 7 +++-- lib/usage/from.js | 4 +-- package.json | 4 +-- test/command.js | 44 ++++++++++++++------------------ 10 files changed, 70 insertions(+), 75 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6adce93 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,21 @@ +'use strict' + +const {defineConfig} = require('eslint/config') +const logdna = require('eslint-config-logdna') + +module.exports = defineConfig([ + { + 'extends': [logdna] + , 'files': ['lib/**/*.js', '*.js', 'test/**/*.js'] + , 'languageOptions': { + ecmaVersion: 2022 + , sourceType: 'commonjs' + } + , 'rules': { + 'sensible/check-require': [2, 'always', { + root: __dirname + }] + , 'logdna/require-file-extension': ['off', false] + } + } +]) diff --git a/lib/command/flag-to-prompt.js b/lib/command/flag-to-prompt.js index df0c91b..03e5c90 100644 --- a/lib/command/flag-to-prompt.js +++ b/lib/command/flag-to-prompt.js @@ -1,48 +1,24 @@ 'use strict' const flagType = require('./flag-type') -const kindOf = require('mout/lang/kindOf') -module.exports = flagToPrompt - -/** - * 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 - **/ -function invertWhen(fn) { - if (!fn) return undefined - return (...args) => { - switch(kindOf(fn)) { - case 'Boolean': { - return !fn - } - case 'AsyncFunction': { - return fn(...args).then((value) => {return !value}) - } +const invertWhen = require('./invert-when') - case 'Function': { - const value = fn(...args) - return !value - } - } - } -} +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: 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 } } diff --git a/lib/command/index.js b/lib/command/index.js index 359d825..5c1a560 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -23,6 +23,8 @@ const tty = require('node:tty') const Equirer = require('enquirer') +const {default: spinners} = require('cli-spinners') +const {default: ora} = require('ora') const nopt = require('nopt') const {default: chalk} = require('chalk') const strip = require('strip-ansi') @@ -35,8 +37,6 @@ const toPrompt = require('./flag-to-prompt') const flagType = require('./flag-type') const Registry = require('../registry') const conf = require('../conf') -const {default: ora} = require('ora') -const {default: spinners} = require('cli-spinners') const usage = require('../usage') const object = require('../lang/object') const typeOf = require('../usage/type-of') @@ -83,7 +83,6 @@ const defaults = { , run: noop } - /** * Base command class for creating re-usable commands * @constructor @@ -679,7 +678,7 @@ class Command extends Registry { * @property {?Function} filter **/ - /* istanbul ignore next */ + /* c8 ignore start */ async ask(name, opts) { const results = [] const question = toQuestion(name, opts) @@ -694,6 +693,8 @@ class Command extends Registry { } return results } + /* c8 ignore stop */ + /** * Convert all registered flags to inquierer compatible prompt objects * @method module:seeli/lib/command#toPrompt @@ -742,6 +743,7 @@ class Command extends Registry { module.exports = Command +/* c8 ignore start */ function transform(input, answers, status) { if (!status.isFinal) return input if (this.type === 'number' && isNaN(input)) return '' @@ -753,6 +755,7 @@ function isRepeatable(flag) { if (flag.multi) return false return true } +/* c8 ignore stop */ function toQuestion(flag, opts, answers) { const arg = toPrompt(flag, opts) @@ -767,7 +770,7 @@ function toQuestion(flag, opts, answers) { return arg } -function getChoices(cfg, answers) { +function getChoices(cfg) { return new Set( toArray(cfg.choices) .filter((choice) => { diff --git a/lib/commands/help.js b/lib/commands/help.js index 8ac265c..1108e93 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -1,4 +1,4 @@ -/* eslint-disable consistent-this */ + 'use strict' /** * Built in command for constructing help for seeli and all registered commands diff --git a/lib/lang/object/set.js b/lib/lang/object/set.js index 260d1bc..2f0c145 100644 --- a/lib/lang/object/set.js +++ b/lib/lang/object/set.js @@ -16,7 +16,6 @@ * { foo: { bar : { baz: 12 } } } **/ - module.exports = setProperty function setProperty(obj, key, value, sep = ':') { diff --git a/lib/registry.js b/lib/registry.js index 5eae899..913a1f1 100644 --- a/lib/registry.js +++ b/lib/registry.js @@ -17,7 +17,6 @@ const toArray = require('mout/lang/toArray') const fill = require('mout/object/fillIn') const typeOf = require('./usage/type-of') - function copyProperties(target, source) { for (const key of Reflect.ownKeys(source)) { if (key !== 'constructor' && key !== 'prototype' && key !== 'name') { diff --git a/lib/seeli.js b/lib/seeli.js index 9f844b6..bfe056c 100644 --- a/lib/seeli.js +++ b/lib/seeli.js @@ -77,7 +77,7 @@ class Seeli extends Command { let cmd = parsed.argv.remain.shift() // did the try to use the help command directly? const help = !!parsed.help - if (help || cmd === 'help' || cmd == null) { + if (help || cmd === 'help' || cmd === null || cmd === undefined) { if (!this.has('help')) { console.error('unknown command %s', cmd) console.error('know commands: %s ', Array.from(this.names).join(', ')) @@ -85,7 +85,10 @@ class Seeli extends Command { return } // allow for abbreviated commands - cmd = (cmd === 'help' || cmd == null) ? parsed.argv.remain.shift() : cmd + cmd = (cmd === 'help' || cmd === null || cmd === undefined) + ? parsed.argv.remain.shift() + : cmd + return this.get('help') .run(cmd) .then(onComplete.bind(this)) diff --git a/lib/usage/from.js b/lib/usage/from.js index a8bd69e..dfaeec9 100644 --- a/lib/usage/from.js +++ b/lib/usage/from.js @@ -15,8 +15,8 @@ **/ const os = require('os') -const cliui = require('cliui') const util = require('util') +const cliui = require('cliui') const {default: chalk} = require('chalk') const {default: width} = require('string-width') const toArray = require('mout/lang/toArray') @@ -61,7 +61,7 @@ function actions(command) { return acts } -function options(command, plain) { +function options(command) { const ui = cliui() const required_with_ui = cliui() const required_without_ui = cliui() diff --git a/package.json b/package.json index 33827a1..d803a4f 100644 --- a/package.json +++ b/package.json @@ -104,8 +104,8 @@ "@codedependant/release-config-npm": "^1.0.3", "@semantic-release/exec": "^5.0.0", "@vuepress/plugin-back-to-top": "^1.5.4", - "eslint": "^7.31.0", - "eslint-config-codedependant": "^2.1.6", + "eslint": "^10.1.0", + "eslint-config-logdna": "^8.0.1", "semantic-release": "^17.4.2", "tap": "^21.6.2", "vuepress": "^1.8.2" diff --git a/test/command.js b/test/command.js index 86b0d2f..9b7fed5 100644 --- a/test/command.js +++ b/test/command.js @@ -237,8 +237,8 @@ test('command', async (t) => { }) // internal argv parsing - t.test('~argv', async (tt) => { - test('should accept an array of arguments', async (t) => { + t.test('~argv', async (t) => { + t.test('should accept an array of arguments', async (t) => { const ArgCommand = new Command({ args: ['--no-color'] }) @@ -322,8 +322,6 @@ test('command', async (t) => { t.equal(NumberCommand.argv.num, 1) }) - - t.test('should accept multiple value flags', async (t) => { const MultiCommand = new Command({ flags: { @@ -414,8 +412,8 @@ test('command', async (t) => { }) }) - t.test('#run', async (tt) => { - test('should emit events for marked flags', (t) => { + t.test('#run', async (t) => { + t.test('should emit events for marked flags', (t) => { t.plan(3) const EventCommand = new Command({ args: ['--one', '--no-two'] @@ -463,8 +461,8 @@ test('command', async (t) => { t.equal(out, 'static call', 'static run function output') }) - t.test('Subclassing', async (tt) => { - test('should allow for subclassing', async (t) => { + t.test('Subclassing', async (t) => { + t.test('should allow for subclassing', async (t) => { const defaults = { description: 'This is a subclass' } @@ -493,8 +491,8 @@ test('command', async (t) => { }) }) - t.test('Aliasing', async (tt) => { - test('from string', async (t) => { + t.test('Aliasing', async (t) => { + t.test('from string', async (t) => { t.afterEach((cb) => { cli.reset() cb() @@ -526,8 +524,8 @@ test('command', async (t) => { }) }) - t.test('Directive parsing', async (tt) => { - test('should pass the first non-flag argument to run', async (t) => { + t.test('Directive parsing', async (t) => { + t.test('should pass the first non-flag argument to run', async (t) => { const DirectiveCommand = new Command({ flags: { test: { @@ -566,7 +564,7 @@ test('command', async (t) => { , 'default': true } } - , run: async (cmd, data) => { + , run: async () => { return '' } }) @@ -584,22 +582,22 @@ test('command', async (t) => { t.test('manual prompt', async (t) => { const mockEnquirer = { - prompt: async (questions) => { + prompt: async () => { // Simulate user input - return the expected values - return { option: 'yes' } + return {option: 'yes'} } } const cmd = new Command({ - enquirer: mockEnquirer, - run: async function() { + enquirer: mockEnquirer + , run: async function() { const value = await this.prompt({ type: 'input' , name: 'option' , message: 'do you want this option' }) - return value + return value } }) @@ -628,7 +626,7 @@ test('command', async (t) => { case 'other': { return {other: ''} } - + } return {} } @@ -651,17 +649,15 @@ test('command', async (t) => { } } , run: async function(_, data) { - debugger; return data } }) - cmd.ask = async function(flag_name) { + cmd.ask = async function() { // only used for `fake` because it's an array value return ['yes'] } - debugger; const answers = await cmd.run() t.match(answers, { fake: ['yes'] @@ -679,7 +675,7 @@ test('command', async (t) => { case 'other': { return {other: 'yes'} } - + } return {} } @@ -706,7 +702,6 @@ test('command', async (t) => { } }) - debugger; const answers = await cmd.run() t.match(answers, { fake: 'yes' @@ -797,7 +792,6 @@ test('command', async (t) => { }, 'correct error message') }) - t.test('interactive command with custom flag `validate`', async (t) => { let validate_call_count = 0 const mockEnquirer = { From 5d3fb104fee110bbd26b0a898005ae8f9da685d6 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 15:18:51 -0500 Subject: [PATCH 12/13] chore(ci): update testable node versions updates the tested node versions to 20, 22, and 24 removing support for node 16, 18 --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e2d7ba..85bdee1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x, 18.x, 20.x] + node-version: [20.x, 22.x, 24.x] steps: - name: Checkout @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14 + node-version: 22 - run: npm install - name: Publish From 250486befdd25ad725a13a0200b7152cedcf3a91 Mon Sep 17 00:00:00 2001 From: Eric Satterwhite Date: Fri, 27 Mar 2026 22:26:44 -0500 Subject: [PATCH 13/13] chore(doc): update docs to reflect new flag options updates the readme and doc pages to reflect the new flag options add for recent updates --- .ai/context.md | 30 +++++++++++++++++ README.md | 65 +++++++++++++++++++------------------ gh-pages/guides/commands.md | 5 +++ 3 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 .ai/context.md diff --git a/.ai/context.md b/.ai/context.md new file mode 100644 index 0000000..ed2c2da --- /dev/null +++ b/.ai/context.md @@ -0,0 +1,30 @@ + +### your role + +You are an expert in building node.js api applications focused on command line interfaces and termainal based applications. +You have a deep knowledge of the npm packages ora, enquirer, , as well as tap for developing the best in breed node applications. + +### your mission + +Your role is to help build and maintain a node.js application seeli which focuses on flexibile and composible command line applications. + +### technology stack +- backend: node.js, enquirer, ora, debug +- testing: tap + +### coding standards +- commas should be placed at the beginning of a line rather than the end +- add jsdoc comments for modules, exported functions and classes +- follow the existing folder structure and naming conventions +- function names should be camel cased +- variable names should be snake cased +- prefer composition over inheritence +- avoid using classes unless complex state management is required +- linting rules found in the shared eslint configuration eslint-config-logdna should be followed and applied. The command 'npm run lint' should pass with an exit code of 0 +- many linting errors can be auto fixed by running `npm run lint:fix` which lints and applies fixes where possible. +- tap executes our tests. running `npm test` will run all tests. npm test wil run a specific file. all tests should exit with a code of 0 +- class names should be upper camel case, FooBar +- function names should be standard camel case, fooBar +- all other variable names should be snake case, foo_bar +- When testing, mocking should be avoided unless strictly necessary. Interfacing with a live data store, or external service is preferable + diff --git a/README.md b/README.md index a9629b7..2eeace6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Test + Release](https://github.com/esatterwhite/node-seeli/actions/workflows/release.yml/badge.svg)](https://github.com/esatterwhite/node-seeli/actions/workflows/release.yml) ![package dependancies](https://david-dm.org/esatterwhite/node-seeli.png) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47a935a723c94c73bc97d749836ee489)](https://www.codacy.com/app/esatterwhite/node-seeli?utm_source=github.com&utm_medium=referral&utm_content=esatterwhite/node-seeli&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/0df91a318dc444e7aedcdd4c77fda673)](https://app.codacy.com/gh/esatterwhite/node-seeli/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) # seeli ( C. L. I. ) @@ -110,37 +110,36 @@ node ./cli world --name=Mark --name=Sally --no-excited -- [seeli ( C. L. I. )](#seeli--c-l-i-) -- [API](#api) - - [Seeli.Command(``)](#seelicommandcommand) - - [Command Options](#command-options) - - [Flag Options](#flag-options) - - [Nested Flags](#nested-flags) - - [`cmd` Shape](#cmd-shape) - - [Seeli.run( )](#seelirun-) - - [Seeli.list``](#seelilistarray) - - [Seeli.use( \[name ``,\] cmd `` )](#seeliuse-name-string-cmd-command-) - - [Seeli.bold( text ``)](#seelibold-text-string) - - [Seeli.green( text ``)](#seeligreen-text-string) - - [Seeli.blue( text ``)](#seeliblue-text-string) - - [Seeli.red( text ``)](#seelired-text-string) - - [Seeli.yellow( text ``)](#seeliyellow-text-string) - - [Seeli.cyan( text ``)](#seelicyan-text-string) - - [Seeli.magenta( text ``)](#seelimagenta-text-string) - - [Seeli.redBright( text ``)](#seeliredbright-text-string) - - [Seeli.blueBright( text ``)](#seelibluebright-text-string) - - [Seeli.greenBright( text ``)](#seeligreenbright-text-string) - - [Seeli.yellowBright( text ``)](#seeliyellowbright-text-string) - - [Seeli.cyanBright( text ``)](#seelicyanbright-text-string) - - [Seeli.config( key ``, value `` )](#seeliconfig-key-string-value-object-) - - [Seeli.config( opts `` )](#seeliconfig-opts-object-) - - [Config Options](#config-options) - - [Seeli.config( key `` )](#seeliconfig-key-string-) -- [Package Configuration](#package-configuration) -- [Auto Help](#auto-help) -- [Asyncronous](#asyncronous) -- [Showing Progress](#showing-progress) -- [Events](#events) +* [API](#api) + * [Seeli.Command(``)](#seelicommandcommand) + * [Command Options](#command-options) + * [Flag Options](#flag-options) + * [Nested Flags](#nested-flags) + * [`cmd` Shape](#cmd-shape) + * [Seeli.run( )](#seelirun-) + * [Seeli.list``](#seelilistarray) + * [Seeli.use( [name ``,] cmd `` )](#seeliuse-name-string-cmd-command-) + * [Seeli.bold( text ``)](#seelibold-text-string) + * [Seeli.green( text ``)](#seeligreen-text-string) + * [Seeli.blue( text ``)](#seeliblue-text-string) + * [Seeli.red( text ``)](#seelired-text-string) + * [Seeli.yellow( text ``)](#seeliyellow-text-string) + * [Seeli.cyan( text ``)](#seelicyan-text-string) + * [Seeli.magenta( text ``)](#seelimagenta-text-string) + * [Seeli.redBright( text ``)](#seeliredbright-text-string) + * [Seeli.blueBright( text ``)](#seelibluebright-text-string) + * [Seeli.greenBright( text ``)](#seeligreenbright-text-string) + * [Seeli.yellowBright( text ``)](#seeliyellowbright-text-string) + * [Seeli.cyanBright( text ``)](#seelicyanbright-text-string) + * [Seeli.config( key ``, value `` )](#seeliconfig-key-string-value-object-) + * [Seeli.config( opts `` )](#seeliconfig-opts-object-) + * [Config Options](#config-options) + * [Seeli.config( key `` )](#seeliconfig-key-string-) +* [Package Configuration](#package-configuration) +* [Auto Help](#auto-help) +* [Asyncronous](#asyncronous) +* [Showing Progress](#showing-progress) +* [Events](#events) # API @@ -183,6 +182,8 @@ 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/gh-pages/guides/commands.md b/gh-pages/guides/commands.md index 62c985f..50c5203 100644 --- a/gh-pages/guides/commands.md +++ b/gh-pages/guides/commands.md @@ -166,6 +166,11 @@ name | required | type | description **when** | `false` | `function` | **interactive mode only** Receives the current user answers hash and should return true or **false** depending on whether or not this question should be asked. **validate** | `false` | `function` | receives user input and should return true if the value is **valid**, and an error message (String) otherwise. If false is returned, a default error message is provided. **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