diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 1c2f433..0000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -tmp \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index d86882e..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "@adobe/eslint-config-aio-lib-config", - "rules": { - "jsdoc/tag-lines": [ - // The Error level should be `error`, `warn`, or `off` (or 2, 1, or 0) - "error", - "never", - { - "startLines": null - } - ] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac02c8e..fac8f55 100755 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ junit.xml /coverage oclif.manifest.json +.claude diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..89b44d6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const aioLibConfig = require('@adobe/eslint-config-aio-lib-config') +const jest = require('eslint-plugin-jest') + +module.exports = [ + { ignores: ['tmp'] }, + ...aioLibConfig, + jest.configs['flat/recommended'], + { + settings: { + n: { + allowModules: ['@oclif/core'] + } + } + } +] diff --git a/package.json b/package.json index f8ed708..3610234 100644 --- a/package.json +++ b/package.json @@ -6,31 +6,26 @@ "bugs": "https://github.com/adobe/aio-cli-plugin-config/issues", "dependencies": { "@adobe/aio-lib-core-config": "^5", - "@oclif/core": "^2.0.0", + "@inquirer/prompts": "^7.0.0", + "@oclif/core": "^4.0.0", "hjson": "^3.2.2", "js-yaml": "^4.1.0" }, "devDependencies": { - "@adobe/eslint-config-aio-lib-config": "^4.0.0", + "@adobe/eslint-config-aio-lib-config": "^5.0.0", "acorn": "^8.7.0", "chalk": "^4.0.0", - "eslint": "^8.57.1", - "eslint-config-oclif": "^5.2.2", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^27.9.0", + "eslint": "^9.0.0", + "eslint-plugin-jest": "^29.0.0", + "neostandard": "^0.13.0", "eslint-plugin-jsdoc": "^48.11.0", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.6.0", - "eslint-plugin-standard": "^5.0.0", "execa": "^4.0.2", "globby": "^11.0.0", "jest": "^29", "jest-haste-map": "^29.5.0", "jest-junit": "^16.0.0", "jest-resolve": "^29.5.0", - "oclif": "^3.2.0", + "oclif": "^4.0.0", "stdout-stderr": "^0.1.13" }, "engines": { diff --git a/src/commands/config/clear.js b/src/commands/config/clear.js index da59c8a..93f308c 100755 --- a/src/commands/config/clear.js +++ b/src/commands/config/clear.js @@ -10,15 +10,16 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') const BaseCommand = require('../../base-command') +const { prompt } = require('../../prompt') class ClearCommand extends BaseCommand { async run () { const { flags } = await this.parse(ClearCommand) if (!flags.force) { - const confirm = await ux.prompt('are you sure? [yN]', { type: 'normal' }) + const confirm = await prompt('are you sure? [yN]') if (!confirm[0] || confirm[0].toLowerCase() !== 'y') { return } diff --git a/src/commands/config/set.js b/src/commands/config/set.js index 68308f9..b3a61c0 100644 --- a/src/commands/config/set.js +++ b/src/commands/config/set.js @@ -10,13 +10,14 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { Flags, Args, ux } = require('@oclif/core') +const { Flags, Args } = require('@oclif/core') const BaseCommand = require('../../base-command') const fs = require('fs') const yaml = require('js-yaml') const hjson = require('hjson') const { getPipedData } = require('@adobe/aio-lib-core-config') const path = require('path') +const { prompt } = require('../../prompt') class SetCommand extends BaseCommand { async run () { @@ -39,7 +40,7 @@ class SetCommand extends BaseCommand { this.error(`Cannot read file: ${value}`) } } else if (flags.interactive) { - value = await ux.prompt('value', { type: 'normal' }) + value = await prompt('value') } else if (value == null) { if (args.key.indexOf('=') > 0) { const parts = args.key.split('=') diff --git a/src/prompt.js b/src/prompt.js new file mode 100644 index 0000000..3ecbe7f --- /dev/null +++ b/src/prompt.js @@ -0,0 +1,25 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { input } = require('@inquirer/prompts') + +/** + * Prompts the user for input. + * + * @param {string} message - the prompt message to display + * @returns {Promise} the user's input + */ +async function prompt (message) { + return input({ message }) +} + +module.exports = { prompt } diff --git a/test/commands/base-command.js b/test/commands/base-command.js index bfe7805..46b3ab1 100644 --- a/test/commands/base-command.js +++ b/test/commands/base-command.js @@ -15,6 +15,8 @@ const TheCommand = require('../../src/base-command') const { stdout } = require('stdout-stderr') const hjson = require('hjson') +const mockConfig = { runHook: async () => ({ successes: [], failures: [] }) } + describe('base-command', () => { test('exports', () => { expect(typeof TheCommand).toEqual('function') @@ -29,7 +31,7 @@ describe('base-command', () => { let command beforeEach(() => { - command = new TheCommand([], { }) + command = new TheCommand([], mockConfig) }) describe('printObject', () => { @@ -43,13 +45,13 @@ describe('base-command', () => { }) test('yaml', async () => { - command = new TheCommand(['--yaml'], { }) + command = new TheCommand(['--yaml'], mockConfig) await command.printObject({ foo: { bar: true } }) expect(stdout.output).toEqual('foo:\n bar: true\n\n') }) test('json', async () => { - command = new TheCommand(['--json'], { }) + command = new TheCommand(['--json'], mockConfig) await command.printObject({ foo: { bar: true } }) expect(stdout.output).toEqual('{"foo":{"bar":true}}\n') }) diff --git a/test/commands/config/clear.js b/test/commands/config/clear.js index d0e8de7..9d2a655 100755 --- a/test/commands/config/clear.js +++ b/test/commands/config/clear.js @@ -13,15 +13,10 @@ governing permissions and limitations under the License. const TheCommand = require('../../../src/commands/config/clear.js') const { mockSet } = require('@adobe/aio-lib-core-config/src/Config') -jest.mock('@oclif/core', () => { - return { - ...jest.requireActual('@oclif/core'), - ux: { - prompt: jest.fn() - } - } -}) -const { ux } = require('@oclif/core') +jest.mock('../../../src/prompt', () => ({ + prompt: jest.fn() +})) +const { prompt } = require('../../../src/prompt') describe('clear', () => { afterEach(() => { @@ -51,14 +46,14 @@ describe('clear', () => { }) test('prompt with yes', () => { - ux.prompt = jest.fn(() => 'y') + prompt.mockResolvedValue('y') return TheCommand.run([]).then(() => { expect(mockSet).toHaveBeenCalledWith(null, null, false) }) }) test('prompt with no', () => { - ux.prompt = jest.fn(() => 'n') + prompt.mockResolvedValue('n') return TheCommand.run([]).then(() => { expect(mockSet).not.toHaveBeenCalled() }) diff --git a/test/commands/config/set.js b/test/commands/config/set.js index 8713a83..0b40260 100644 --- a/test/commands/config/set.js +++ b/test/commands/config/set.js @@ -10,26 +10,18 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -const { ux } = require('@oclif/core') const TheCommand = require('../../../src/commands/config/set.js') const config = require('@adobe/aio-lib-core-config') const path = require('path') const { mockSet } = require('@adobe/aio-lib-core-config/src/Config') -jest.mock('@oclif/core', () => { - return { - ...jest.requireActual('@oclif/core'), - ux: { - prompt: jest.fn() - } - } -}) +jest.mock('../../../src/prompt', () => ({ + prompt: jest.fn() +})) +const { prompt } = require('../../../src/prompt') describe('set', () => { - let command - beforeEach(() => { - command = new TheCommand([]) mockSet.mockImplementation(() => { return { a: 12 } }) }) @@ -42,106 +34,89 @@ describe('set', () => { }) test('default', async () => { - command.argv = ['a-key', 'value'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', 'value'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'value', false) }) test('local', async () => { - command.argv = ['-l', 'a-key', 'value'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['-l', 'a-key', 'value'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'value', true) }) test('global', async () => { - command.argv = ['-g', 'a-key', 'value'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['-g', 'a-key', 'value'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'value', false) }) test('no value', async () => { - command.argv = ['a-key'] - await expect(command.run()).rejects.toEqual(new Error('Missing value')) + await expect(TheCommand.run(['a-key'])).rejects.toEqual(new Error('Missing value')) }) test('get piped data', async () => { config.getPipedData.mockResolvedValue('a file') - command.argv = ['-g', 'a-key'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['-g', 'a-key'])).resolves.not.toThrow() expect(config.getPipedData).toHaveBeenCalledWith() expect(mockSet).toHaveBeenCalledWith('a-key', 'a file', false) }) test('parse key=value', async () => { - command.argv = ['a-key=value'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key=value'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'value', false) }) test('parse json', async () => { - command.argv = ['a-key', '-j', '{a:1}'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-j', '{a:1}'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', { a: 1 }, false) }) test('throw error on bad yaml parsing', async () => { - command.argv = ['a-key', '-y', 'a:\nhy '] - await expect(command.run()).rejects.toEqual(new Error('Cannot parse yaml')) + await expect(TheCommand.run(['a-key', '-y', 'a:\nhy '])).rejects.toEqual(new Error('Cannot parse yaml')) }) test('throw error on bad json parsing', async () => { - command.argv = ['a-key', '-j', '{a:1\n'] - await expect(command.run()).rejects.toEqual(new Error('Cannot parse json')) + await expect(TheCommand.run(['a-key', '-j', '{a:1\n'])).rejects.toEqual(new Error('Cannot parse json')) }) test('parse yaml', async () => { - command.argv = ['a-key', '-y', 'a:\n b: true'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-y', 'a:\n b: true'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', { a: { b: true } }, false) }) test('json file', async () => { - command.argv = ['a-key', '-f', './test/__fixtures__/a.json'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-f', './test/__fixtures__/a.json'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', { a: 12 }, false) }) test('yaml file', async () => { - command.argv = ['a-key', '-f', './test/__fixtures__/a.yaml'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-f', './test/__fixtures__/a.yaml'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', { a: { b: 12 } }, false) }) test('yml file', async () => { - command.argv = ['a-key', '-f', './test/__fixtures__/a.yml'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-f', './test/__fixtures__/a.yml'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', { a: { b: 12 } }, false) }) test('other file', async () => { - command.argv = ['a-key', '-f', './test/__fixtures__/a.txt'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-f', './test/__fixtures__/a.txt'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'raw data', false) }) test('file but no value', async () => { - command.argv = ['a-key', '-f'] - await expect(command.run()).rejects.toEqual(new Error('Missing filename')) + await expect(TheCommand.run(['a-key', '-f'])).rejects.toEqual(new Error('Missing filename')) }) test('file but not exists', async () => { - command.argv = ['a-key', '-f', '/doesnotexist'] - await expect(command.run()).rejects.toEqual(new Error(`Cannot read file: ${path.resolve('/doesnotexist')}`)) + await expect(TheCommand.run(['a-key', '-f', '/doesnotexist'])).rejects.toEqual(new Error(`Cannot read file: ${path.resolve('/doesnotexist')}`)) }) test('prompt for value', async () => { config.getPipedData.mockResolvedValue(null) - ux.prompt = jest.fn(() => 'a value') + prompt.mockResolvedValue('a value') - command.argv = ['a-key', '-i'] - await expect(command.run()).resolves.not.toThrow() + await expect(TheCommand.run(['a-key', '-i'])).resolves.not.toThrow() expect(mockSet).toHaveBeenCalledWith('a-key', 'a value', false) }) }) diff --git a/test/commands/prompt.js b/test/commands/prompt.js new file mode 100644 index 0000000..62056de --- /dev/null +++ b/test/commands/prompt.js @@ -0,0 +1,29 @@ +/* +Copyright 2026 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const { prompt } = require('../../src/prompt') + +jest.mock('@inquirer/prompts', () => ({ + input: jest.fn() +})) +const { input } = require('@inquirer/prompts') + +describe('prompt', () => { + test('returns user input', async () => { + input.mockResolvedValue('user answer') + + const result = await prompt('enter value') + + expect(input).toHaveBeenCalledWith({ message: 'enter value' }) + expect(result).toEqual('user answer') + }) +})