Files
bare-operating-system/packages/bare-os-booter/lib/cli-readline.js
T
Raven Scott a08e1891d3 fix(bare-openssh): drop stray ">" after commands (readline PS1 on recycle)
bare-readline treats empty prompt as falsy and defaults to ">"; each SSH
readline recycle painted that before setPrompt. Pass initialPrompt from
the real PS1, defer first attach until after profile/barerc, and skip the
extra \\r\\n after recycled bare-readline.
2026-04-08 23:59:39 -04:00

165 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Line-oriented stdin for Bare/Pear (no node:readline) and Node fallback.
* Pear-terminal uses bare-readline + bare-stdio; we mirror that after Node readline.
*/
import { createInterface as bareReadlineCreateInterface } from 'bare-readline'
/**
* Strip C0 controls; apply BS (0x08) and DEL (0x7F) for raw SSH line fallback.
* @param {string} s
*/
export function sanitizeInteractiveShellLine(s) {
let out = ''
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i)
if (c === 8 || c === 127) {
out = out.slice(0, -1)
continue
}
if (c < 32 && c !== 9) continue
out += s[i]
}
return out
}
/**
* Interactive line editor (cursor, echo, history) — same engine as the Pear console.
* Caller should call **`close()`** when the session ends to detach input listeners.
*
* bare-readline treats **`prompt: ''` as falsy** and falls back to `'> '`, which flashes
* after each `new Readline` (e.g. SSH recycle). Use a non-empty placeholder or pass
* **`initialPrompt`** so the constructors first `prompt()` matches the real PS1.
*
* @param {import('stream').Readable} stdin
* @param {import('stream').Writable} stdout
* @param {{ initialPrompt?: string }} [opts]
* @returns {Promise<{ readLine: (prompt: string) => Promise<string | null>, close: () => void }>}
*/
export async function createBareReadlineSession(stdin, stdout, opts = {}) {
if (typeof bareReadlineCreateInterface !== 'function') {
throw new Error('bare-readline: createInterface missing')
}
const initial =
typeof opts.initialPrompt === 'string' && opts.initialPrompt.length > 0
? opts.initialPrompt
: ' '
const rl = bareReadlineCreateInterface({
input: stdin,
output: stdout,
prompt: initial
})
return {
readLine(prompt) {
return new Promise((resolve) => {
rl.setPrompt(prompt)
rl.prompt()
rl.once('line', (line) => {
resolve(line)
})
})
},
close() {
try {
rl.close()
} catch {
/* ignore */
}
}
}
}
/**
* @param {import('stream').Readable} stdin
* @param {import('stream').Writable} stdout
* @returns {(prompt: string) => Promise<string>}
*/
export async function createBareReadlineQuestion(stdin, stdout) {
const { readLine } = await createBareReadlineSession(stdin, stdout)
return readLine
}
/**
* @param {import('stream').Readable} stdin
* @param {import('stream').Writable} stdout
* @returns {(prompt: string) => Promise<string | null>}
*/
export function createStreamLineReader(stdin, stdout) {
let buf = ''
/** @type {string[]} */
const queue = []
/** @type {((line: string | null) => void)[]} */
const waiters = []
function deliver(line) {
if (waiters.length) waiters.shift()(line)
else queue.push(line)
}
function onData(chunk) {
const s =
typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')
buf += s
while (buf.length) {
const n = buf.indexOf('\n')
const r = buf.indexOf('\r')
if (n === -1 && r === -1) break
let cut
let ateCr = false
if (n === -1) {
cut = r
ateCr = true
} else if (r === -1) {
cut = n
} else if (r < n) {
cut = r
ateCr = true
} else {
cut = n
}
const raw = buf.slice(0, cut)
buf = buf.slice(cut + 1)
if (ateCr && buf.length && buf[0] === '\n') buf = buf.slice(1)
deliver(String(raw).replace(/\r/g, ''))
}
}
function flushRemainder() {
if (buf.length) {
deliver(buf.replace(/\r$/, ''))
buf = ''
}
}
function onEnd() {
flushRemainder()
while (waiters.length) waiters.shift()(null)
}
if (typeof stdin.setEncoding === 'function') stdin.setEncoding('utf8')
stdin.on('data', onData)
stdin.on('end', onEnd)
stdin.on('error', onEnd)
if (typeof stdin.resume === 'function') stdin.resume()
return function readLine(prompt) {
if (typeof stdout.write === 'function') {
// DECTCEM: show cursor (boot splash / curses often hide it; dumb fallback has no redraw)
stdout.write('\x1b[?25h\x1b[0m' + prompt)
}
if (queue.length) return Promise.resolve(queue.shift() ?? null)
return new Promise((resolve) => waiters.push(resolve))
}
}
/**
* @returns {boolean}
*/
export function looksLikeInteractiveStdin(stdin) {
return Boolean(
stdin &&
typeof stdin.on === 'function' &&
typeof stdin.resume === 'function'
)
}