Repository: oclif/core (ts)

Issue:
CliUx.ux.prompt text truncation
**Describe the bug**
When pasting text into `CliUx.ux.prompt()` method that is a very long single line, the text gets trunctated.

**To Reproduce**
Steps to reproduce the behavior:
```
      const value = await CliUx.ux.prompt(`Enter long text`)
```

paste in text below:
```
0101000000000000000200000000000000000000000000000000000000000000000100000000000000000000006b64d25e2d4a21a38cce4e7e9bc8fa9063b64214ffc2d7d7ca8a804a93677759e7a38f90688ae21d5c955b212685e2ad1740262ad12095a225be4df5f260a003e7a38f90688ae21d5c955b212685e2ad1740262ad12095a225be4df5f260a003b35bbd18661b1ba073651e33f91dcb8c6df44a13054f2edc95acc458405070954bd151f19b097173d8075f25597661b7b71451f4c489f8a2dda9d5c1010233ed2978a506d7dfcf0734e79cfd8c3fc283e096f7414f952d83423e0d9bf2531dc211360fbaffda214c9e55e65ca7566072c69a2ef6caea084f5fd01d7b0417949a940f131d4b6129203bbcbe5bbcc660f4b66b6ca60c09ad82d738725fc9d1174edc29828529b9d2c5a891fedbba2a54b1925386a6780ffbccdf63308c25a15bea347c365569246524d779e84527241e780e665a146e815515a1bc0886e10fb2151153a1ae1e5e4da133118be9f1d5ddb68c09aea19304e684d25460b027c2804801340600b299c8b0dbc35d0f466b37b06f601c54acd5d08f1924160d15995249a4904d6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b918c74c6b5ec5204074f20aa6191241feec6372b24c7dff99ebc84427944e14702b1c2fcb0315ca29224f0e7348b62ea6b0aee012f377403a43ebeafdad169c4e079cd47d50f24a1ca9e368316ac192368e5ad0d331567500a6e41d7d89ece50cdca4dee063b77ff0936298c4e7c8d3afb248b7ee074d6c3257280e1f94035d19394155b31c1395c1a0b68ea11e17688c62a8e710e2715bafedd67e473225e34a7bf7e29f83f934d22cf07de033f133ff423bfcfb3bdf5b90997529e76151ad7314420553b2d21bbc10b83549bf6861f1de13593bc8dea0183718f3720d6586a2e33095806a87435424f1db51329b40de9d21d0c1a2765143b748509bd99f664d832cced33abc24e67d1afa518786ef0b95741c98e19608a68b0d9bb542fb880d40118f21302f07335c35d06875fdd9f5955b282ecfbd6cefb4e9ca41e93c63e14a610b268ee3e49b0b7e61a0ca444aec823121481f0de1e5c1f872533386808206d0bce2d1e2389529d9694cfe22385f83983ef10a591e0d81830d236ada1bc009bf08400963a87d393b08962aad659bf46d86fa793d1c650133d7f676b97d8c065814255b96e3f1c82d91eeb71c161be1fff8cf876be4058992b851973b97dc717f3865e3b8450eb17490f907725f6fbf0056aff3ffd2fdfe4a9ce89144f0a914047ee2032698c001462ee9606474393ca270d09c5c1b7fe16a133952d5b3e85ef5d18c2c427f8aaae414f3e0e9fbb1aee4f2ad72b361be44b53e78b7f1d8e8b941954f8fbfae21357ac326357231acac24a98102ea96aebf3b1fa8036aa47413954146b0d1157523028273991739a6c30eec9a6fbd3a02c960612c452bd7cf7f983ee2a800c50e4805b726e25cc7d58aeb4651cca183034f58c2ff7e202e04bdce1a860c4ea956c9107aa9821e6257414d6fee802da6a366008f0cd9c627fb40444c5821a14c7e01bf5168f3f448edab8ac6e50746bf355ed5a48a964a7e2f55708c7b01aee12803c3a3c73936cf5d20b445aad622791c3d65761803175b3218eee732ffbd13535f1de6ab3ac451b960681d166c4b9b18ed186df20c9545218790f0a47c990ee8160bc1ce74905d9afa65b87f1856e36e1320ab651c4a12ec68650cd63cc44c2137a5fe4f3620a0852034b87a791f5b951e0feb68d89cf78f485c1163103142b96260992c56081d27545dcd02d59fb749847ae1430aedb7d0cc451e5b1fc8efe31531114a4fcea633bb25a0ba51be4c3f623d0a9b8b494d1db7f994411d4092a03dc807b1fd3c78d58212c8956c6be668edaa1321bb2a8e630f2cd72df9da99c4fe6040581ee96e4dc17a6a62fac34e0283633b1b603c95bae50b123e6750a26bf14b68c5f6ee1b4e3facb7353be847ce324642df6dd8762693bac50a8f6739fd879eb14add3653c8b9e2adee8ac0b3e30700da6b1351260dac2d18f5e2fc6a4d232ab42a3501b9ba42cefb375a2bd1ca5f87885209144bf4af076f328986cc71fb67052c9aa7977918060e33da58ead8130ccb5b2a02ebc1448a0c87195e86bcbf533bf80a1803
```

**Expected behavior**
Entire text blob should be pasted in.

**Screenshots**
When pasting into prompt:
<img width="1109" alt="Screenshot 2024-02-12 at 12 04 43 PM" src="https://github.com/oclif/core/assets/26990067/1cd7eed6-0365-43c7-8aba-35e815f7eaf5">


**Environment (please complete the following information):**
 - Behavior verified on MacOS and Ubuntu
 - zsh, terminal, bash



Pull request description:
fix: allow long text in ux.prompt
Use `readline.createInterface` for `ux.prompt`

Fixes #952 
@W-15023621@

Patch:
diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts
index 84f25dc..4955935 100644
--- a/src/cli-ux/prompt.ts
+++ b/src/cli-ux/prompt.ts
@@ -1,4 +1,5 @@
 import chalk from 'chalk'
+import readline from 'node:readline'
 
 import * as Errors from '../errors'
 import {config} from './config'
@@ -26,29 +27,36 @@ interface IPromptConfig {
 
 function normal(options: IPromptConfig, retries = 100): Promise<string> {
   if (retries < 0) throw new Error('no input')
+  const ac = new AbortController()
+  const {signal} = ac
+
   return new Promise((resolve, reject) => {
-    let timer: NodeJS.Timeout
-    if (options.timeout) {
-      timer = setTimeout(() => {
-        process.stdin.pause()
-        reject(new Error('Prompt timeout'))
-      }, options.timeout)
-      timer.unref()
-    }
+    const rl = readline.createInterface({
+      input: process.stdin,
+      output: process.stdout,
+    })
 
-    process.stdin.setEncoding('utf8')
-    process.stderr.write(options.prompt)
-    process.stdin.resume()
-    process.stdin.once('data', (b) => {
-      if (timer) clearTimeout(timer)
-      process.stdin.pause()
-      const data: string = (typeof b === 'string' ? b : b.toString()).trim()
+    rl.question(options.prompt, {signal}, (answer) => {
+      rl.close()
+      const data = answer.trim()
       if (!options.default && options.required && data === '') {
         resolve(normal(options, retries - 1))
       } else {
         resolve(data || (options.default as string))
       }
     })
+
+    if (options.timeout) {
+      signal.addEventListener(
+        'abort',
+        () => {
+          reject(new Error('Prompt timeout'))
+        },
+        {once: true},
+      )
+
+      setTimeout(() => ac.abort(), options.timeout)
+    }
   })
 }
 


Test patch:
diff --git a/test/cli-ux/prompt.test.ts b/test/cli-ux/prompt.test.ts
index 7f34e83..387be7d 100644
--- a/test/cli-ux/prompt.test.ts
+++ b/test/cli-ux/prompt.test.ts
@@ -1,73 +1,65 @@
-import * as chai from 'chai'
-
-const {expect} = chai
+import {expect} from 'chai'
+import readline from 'node:readline'
+import {SinonSandbox, createSandbox} from 'sinon'
 
 import {ux} from '../../src/cli-ux'
-import {fancy} from './fancy'
 
 describe('prompt', () => {
-  fancy
-    .stdout()
-    .stderr()
-    .end('requires input', async () => {
-      const promptPromise = ux.prompt('Require input?')
-      process.stdin.emit('data', '')
-      process.stdin.emit('data', 'answer')
-      const answer = await promptPromise
-      await ux.done()
-      expect(answer).to.equal('answer')
-    })
+  let sandbox: SinonSandbox
 
-  fancy
-    .stdout()
-    .stderr()
-    .stdin('y')
-    .end('confirm', async () => {
-      const promptPromise = ux.confirm('yes/no?')
-      const answer = await promptPromise
-      await ux.done()
-      expect(answer).to.equal(true)
+  function stubReadline(answers: string[]) {
+    let callCount = 0
+    sandbox.stub(readline, 'createInterface').returns({
+      // @ts-expect-error because we're stubbing
+      async question(_message, opts, cb) {
+        callCount += 1
+        cb(answers[callCount - 1])
+      },
+      close() {},
     })
+  }
 
-  fancy
-    .stdout()
-    .stderr()
-    .stdin('n')
-    .end('confirm', async () => {
-      const promptPromise = ux.confirm('yes/no?')
-      const answer = await promptPromise
-      await ux.done()
-      expect(answer).to.equal(false)
-    })
+  beforeEach(() => {
+    sandbox = createSandbox()
+  })
 
-  fancy
-    .stdout()
-    .stderr()
-    .stdin('x')
-    .end('gets anykey', async () => {
-      const promptPromise = ux.anykey()
-      const answer = await promptPromise
-      await ux.done()
-      expect(answer).to.equal('x')
-    })
+  afterEach(() => {
+    sandbox.restore()
+  })
 
-  fancy
-    .stdout()
-    .stderr()
-    .end('does not require input', async () => {
-      const promptPromise = ux.prompt('Require input?', {
-        required: false,
-      })
-      process.stdin.emit('data', '')
-      const answer = await promptPromise
-      await ux.done()
-      expect(answer).to.equal('')
-    })
+  it('should require input', async () => {
+    stubReadline(['', '', 'answer'])
+    const answer = await ux.prompt('Require input?')
+    expect(answer).to.equal('answer')
+  })
 
-  fancy
-    .stdout()
-    .stderr()
-    .it('timeouts with no input', async () => {
-      await expect(ux.prompt('Require input?', {timeout: 1})).to.eventually.be.rejectedWith('Prompt timeout')
-    })
+  it('should not require input', async () => {
+    stubReadline([''])
+    const answer = await ux.prompt('Require input?', {required: false})
+    expect(answer).to.equal('')
+  })
+
+  it('should use default input', async () => {
+    stubReadline([''])
+    const answer = await ux.prompt('Require input?', {default: 'default'})
+    expect(answer).to.equal('default')
+  })
+
+  it('should confirm with y', async () => {
+    stubReadline(['y'])
+    const answer = await ux.confirm('yes/no?')
+    expect(answer).to.equal(true)
+  })
+
+  it('should confirm with n', async () => {
+    stubReadline(['n'])
+    const answer = await ux.confirm('yes/no?')
+    expect(answer).to.equal(false)
+  })
+
+  it('should get anykey', async () => {
+    stubReadline(['x'])
+    const answer = await ux.anykey()
+    expect(answer).to.equal('x')
+  })
 })