178 lines
5.0 KiB
JavaScript
178 lines
5.0 KiB
JavaScript
/**
|
|
* Plain-stream masked line input (same pattern as agent --config / bareAgentPromptSetupLine).
|
|
* Used by /bin/login so the Fish REPL is suspended and stdin/stdout are the TTY streams.
|
|
*/
|
|
|
|
/**
|
|
* @param {Record<string, unknown> | undefined} ctx
|
|
* @param {import('stream').Writable | undefined} out
|
|
* @param {string} s
|
|
*/
|
|
function bareLoginInteractiveWrite(ctx, out, s) {
|
|
const text =
|
|
ctx && ctx.bareOsPtyStdoutCrlf
|
|
? String(s).replace(/\r?\n/g, '\r\n')
|
|
: String(s)
|
|
if (out && typeof out.write === 'function') {
|
|
try {
|
|
out.write(text)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {import('stream').Readable} stdin
|
|
* @returns {Promise<string>}
|
|
*/
|
|
function bareLoginReadStreamLineOnce(stdin) {
|
|
return new Promise((resolve) => {
|
|
let acc = ''
|
|
/** @param {string | Uint8Array | Buffer} chunk */
|
|
function onData(chunk) {
|
|
let s = ''
|
|
if (typeof chunk === 'string') s = chunk
|
|
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
|
else if (
|
|
typeof Buffer !== 'undefined' &&
|
|
typeof Buffer.isBuffer === 'function' &&
|
|
Buffer.isBuffer(chunk)
|
|
)
|
|
s = chunk.toString('utf8')
|
|
else s = String(chunk)
|
|
acc += s
|
|
const n = acc.indexOf('\n')
|
|
if (n >= 0) {
|
|
cleanup()
|
|
resolve(acc.slice(0, n).replace(/\r$/, ''))
|
|
}
|
|
}
|
|
function onEnd() {
|
|
cleanup()
|
|
resolve(acc.replace(/\r$/, ''))
|
|
}
|
|
function cleanup() {
|
|
stdin.removeListener('data', onData)
|
|
stdin.removeListener('end', onEnd)
|
|
stdin.removeListener('error', onEnd)
|
|
}
|
|
stdin.on('data', onData)
|
|
stdin.once('end', onEnd)
|
|
stdin.once('error', onEnd)
|
|
if (typeof stdin.resume === 'function') stdin.resume()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {import('stream').Readable} stdin
|
|
* @param {import('stream').Writable | undefined} stdout
|
|
* @returns {Promise<string>}
|
|
*/
|
|
function bareLoginReadMaskedLineOnce(ctx, stdin, stdout) {
|
|
const ttyIn = /** @type {{ setRawMode?: (v: boolean) => void, isTTY?: boolean }} */ (
|
|
stdin
|
|
)
|
|
if (!ttyIn || typeof ttyIn.setRawMode !== 'function' || !ttyIn.isTTY) {
|
|
return bareLoginReadStreamLineOnce(stdin)
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
/** @type {string[]} */
|
|
const chars = []
|
|
let done = false
|
|
/** @param {string | Uint8Array | Buffer} chunk */
|
|
function onData(chunk) {
|
|
if (done) return
|
|
let s = ''
|
|
if (typeof chunk === 'string') s = chunk
|
|
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
|
else if (
|
|
typeof Buffer !== 'undefined' &&
|
|
typeof Buffer.isBuffer === 'function' &&
|
|
Buffer.isBuffer(chunk)
|
|
) {
|
|
s = chunk.toString('utf8')
|
|
} else s = String(chunk)
|
|
for (const ch of s) {
|
|
const code = ch.charCodeAt(0)
|
|
if (ch === '\r' || ch === '\n') {
|
|
finish(true)
|
|
return
|
|
}
|
|
if (ch === '\u0003') {
|
|
finish(false, new Error('interrupted'))
|
|
return
|
|
}
|
|
if (ch === '\u007f' || ch === '\b') {
|
|
if (chars.length) {
|
|
chars.pop()
|
|
bareLoginInteractiveWrite(ctx, stdout, '\b \b')
|
|
}
|
|
continue
|
|
}
|
|
if (code >= 32 && code !== 127) {
|
|
chars.push(ch)
|
|
bareLoginInteractiveWrite(ctx, stdout, '*')
|
|
}
|
|
}
|
|
}
|
|
/** @param {boolean} ok @param {Error} [err] */
|
|
function finish(ok, err) {
|
|
if (done) return
|
|
done = true
|
|
cleanup()
|
|
if (ok) resolve(chars.join(''))
|
|
else reject(err || new Error('masked_input_failed'))
|
|
}
|
|
function cleanup() {
|
|
stdin.removeListener('data', onData)
|
|
stdin.removeListener('end', onEnd)
|
|
stdin.removeListener('error', onErr)
|
|
try {
|
|
ttyIn.setRawMode(false)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
bareLoginInteractiveWrite(ctx, stdout, '\n')
|
|
}
|
|
function onEnd() {
|
|
finish(true)
|
|
}
|
|
/** @param {unknown} e */
|
|
function onErr(e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
finish(false, new Error(msg))
|
|
}
|
|
try {
|
|
ttyIn.setRawMode(true)
|
|
} catch {
|
|
return resolve('')
|
|
}
|
|
stdin.on('data', onData)
|
|
stdin.once('end', onEnd)
|
|
stdin.once('error', onErr)
|
|
if (typeof stdin.resume === 'function') stdin.resume()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} prompt
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function bareLoginPromptMaskedLine(ctx, prompt) {
|
|
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
|
ctx.replStdin || ctx.stdin
|
|
)
|
|
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
|
ctx.replStdout || ctx.stdout
|
|
)
|
|
if (!stdin || typeof stdin.on !== 'function') {
|
|
throw new Error('login: stdin stream unavailable (need an interactive TTY)')
|
|
}
|
|
bareLoginInteractiveWrite(ctx, stdout, '\x1b[?25h\x1b[0m' + prompt)
|
|
return bareLoginReadMaskedLineOnce(ctx, stdin, stdout)
|
|
}
|