diff --git a/lib/utils/display.js b/lib/utils/display.js index 01ad55d4ce30c..2308c0b090bd6 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -330,20 +330,37 @@ class Display { this.#progress.off() break - case input.KEYS.end: + case input.KEYS.end: { log.resume() + // For silent prompts (like password), add newline to preserve output + if (meta?.silent) { + output.standard('') + } output.flush() this.#progress.resume() break + } case input.KEYS.read: { - // The convention when calling input.read is to pass in a single fn that returns the promise to await. resolve and reject are provided by proc-log. + // The convention when calling input.read is to pass in a single fn that returns the promise to await. Resolve and reject are provided by proc-log. const [res, rej, p] = args - return input.start(() => p() - .then(res) - .catch(rej) - // Any call to procLog.input.read will render a prompt to the user, so we always add a single newline of output to stdout to move the cursor to the next line. - .finally(() => output.standard(''))) + + // Use sequential input management to avoid race condition which causes issues with spinner and adding newlines. + input.start() + + return p() + .then((result) => { + // If user hits enter, process end event and return input. + input.end({ [META]: true, silent: meta?.silent }) + res(result) + return result + }) + .catch((error) => { + // If user hits ctrl+c, add newline to preserve output. + output.standard('') + input.end() + rej(error) + }) } } }) diff --git a/lib/utils/read-user-info.js b/lib/utils/read-user-info.js index a9a50f8263ff6..31f148a02b30a 100644 --- a/lib/utils/read-user-info.js +++ b/lib/utils/read-user-info.js @@ -1,6 +1,6 @@ const { read: _read } = require('read') const userValidate = require('npm-user-validate') -const { log, input } = require('proc-log') +const { log, input, META } = require('proc-log') const otpPrompt = `This command requires a one-time password (OTP) from your authenticator app. Enter one below. You can also pass one on the command line by appending --otp=123456. @@ -11,7 +11,9 @@ const passwordPrompt = 'npm password: ' const usernamePrompt = 'npm username: ' const emailPrompt = 'email (this IS public): ' -const read = (...args) => input.read(() => _read(...args)) +// Pass options through so we can differentiate between regular and silent prompts +const read = (options) => + input.read(() => _read(options), { [META]: true, silent: options?.silent }) function readOTP (msg = otpPrompt, otp, isRetry) { if (isRetry && otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) { diff --git a/tap-snapshots/test/lib/commands/init.js.test.cjs b/tap-snapshots/test/lib/commands/init.js.test.cjs index eae04d77d2e82..821193a55e1a9 100644 --- a/tap-snapshots/test/lib/commands/init.js.test.cjs +++ b/tap-snapshots/test/lib/commands/init.js.test.cjs @@ -20,6 +20,5 @@ Press ^C at any time to quit. exports[`test/lib/commands/init.js TAP workspaces no args -- yes > should print helper info 1`] = ` - added 1 package in {TIME} ` diff --git a/tap-snapshots/test/lib/utils/open-url.js.test.cjs b/tap-snapshots/test/lib/utils/open-url.js.test.cjs index 2b3fed2b326e4..5154579f0df59 100644 --- a/tap-snapshots/test/lib/utils/open-url.js.test.cjs +++ b/tap-snapshots/test/lib/utils/open-url.js.test.cjs @@ -25,7 +25,6 @@ https://www.npmjs.com exports[`test/lib/utils/open-url.js TAP open url prompt does not error when opener cannot find command > Outputs extra Browser unavailable message and url 1`] = ` npm home: https://www.npmjs.com - Browser unavailable. Please open the URL manually: https://www.npmjs.com ` diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index bc4e23485fa3e..e04bb0b440371 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -199,3 +199,46 @@ t.test('Display.clean', async (t) => { clearOutput() } }) + +t.test('prompt functionality', async t => { + t.test('regular prompt completion works', async t => { + const { input } = await mockDisplay(t) + + const result = await input.read(() => Promise.resolve('user-input')) + + t.equal(result, 'user-input', 'should return the input result') + }) + + t.test('silent prompt completion works', async t => { + const { input } = await mockDisplay(t) + + const result = await input.read( + () => Promise.resolve('secret-password'), + { silent: true } + ) + + t.equal(result, 'secret-password', 'should return the input result for silent prompts') + }) + + t.test('metadata is correctly passed through', async t => { + const { input } = await mockDisplay(t) + + await input.read( + () => Promise.resolve('result1'), + { silent: false } + ) + t.pass('should handle silent false option') + + await input.read( + () => Promise.resolve('result2'), + {} + ) + t.pass('should handle empty options') + + await input.read( + () => Promise.resolve('result3'), + { silent: true } + ) + t.pass('should handle silent true option') + }) +}) diff --git a/test/lib/utils/read-user-info.js b/test/lib/utils/read-user-info.js index 35628f7f2faac..74463175f5e8e 100644 --- a/test/lib/utils/read-user-info.js +++ b/test/lib/utils/read-user-info.js @@ -118,3 +118,35 @@ t.test('email - invalid warns and retries', async (t) => { t.equal(result, 'foo@bar.baz', 'received the email') t.equal(logMsg, 'invalid email') }) + +t.test('read-user-info integration works', async (t) => { + t.teardown(() => { + readResult = null + readOpts = null + }) + + readResult = 'regular-input' + const username = await readUserInfo.username('Username: ') + t.equal(username, 'regular-input', 'should return username from regular prompt') + t.notOk(readOpts.silent, 'username prompt should not set silent') + + readResult = 'secret-password' + const password = await readUserInfo.password('Password: ') + t.equal(password, 'secret-password', 'should return password from silent prompt') + t.match(readOpts, { silent: true }, 'password prompt should set silent: true') +}) + +t.test('silent metadata is passed correctly by read-user-info', async (t) => { + t.teardown(() => { + readResult = null + readOpts = null + }) + + readResult = 'username' + await readUserInfo.username('Username: ') + t.notOk(readOpts?.silent, 'username prompt should not set silent') + + readResult = 'password' + await readUserInfo.password('Password: ') + t.equal(readOpts?.silent, true, 'password prompt should set silent: true') +})