Files
bare-operating-system/packages/bare-os-booter/lib/cli/repl-session.js
T
2026-08-18 18:11:34 -04:00

206 lines
5.3 KiB
JavaScript

import {
createFishReadLine,
disableFishRawMode,
releaseFishStdin
} from './fish-readline.js'
import {
bindReplDebugStream,
isReplDebug,
replDbg,
unbindReplDebugStream
} from './debug-repl.js'
import {
bareInitdShutdownActiveUnitsReverse,
runKernelShutdownHooks,
stopBareInitd
} from '../initd/bare-initd.js'
import { wrapReadLineWithReplHistoryPersist } from './cli-readline.js'
/**
* Kernel `console` must write to the same stream as the line editor so cursor stays in sync.
* @param {import('stream').Writable} stream
* @returns {import('console').Console | Record<string, (...args: unknown[]) => void>}
*/
function createFishSyncedConsole(stream) {
const Cons = globalThis.Console
if (typeof Cons === 'function') {
return new Cons(stream, stream)
}
const writeLn = (...args) => {
stream.write(
args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ') + '\n'
)
}
return {
log: writeLn,
info: writeLn,
debug: writeLn,
error: writeLn,
warn: writeLn
}
}
/**
* One TTY session for the kernel: `readLine`, `console`, and `writeScreen` share `stdout`.
* When the TTY supports raw mode, fish-style editing is the primary implementation (not a
* second layer on top of bare-readline output).
*
* @param {{
* stdin: import('stream').Readable | null,
* stdout: import('stream').Writable | null,
* rawReadLine: (p: string) => Promise<string | null>,
* interactiveAvailable: boolean,
* skipInteractive: boolean,
* ctx: Record<string, unknown>
* }} args
*/
export async function createKernelReplSession({
stdin,
stdout,
rawReadLine,
interactiveAvailable,
skipInteractive,
ctx
}) {
const baseConsole = globalThis.console
if (isReplDebug() && stdout) bindReplDebugStream(stdout)
function writeScreen(chunk) {
const s = typeof chunk === 'string' ? chunk : String(chunk)
if (isReplDebug()) {
replDbg(
'repl-writeScreen',
'write',
`len=${s.length}`,
`dest=${stdout && typeof stdout.write === 'function' ? 'sessionStdout' : 'process.stdout'}`,
JSON.stringify(s.length > 64 ? s.slice(0, 64) + '…' : s)
)
}
if (stdout && typeof stdout.write === 'function') {
stdout.write(s)
} else if (
globalThis.process?.stdout &&
typeof globalThis.process.stdout.write === 'function'
) {
globalThis.process.stdout.write(s)
}
}
const fishOff = globalThis.process?.env?.BARE_OS_FISH === '0'
let fishRead = null
if (isReplDebug()) {
replDbg(
'repl',
'createKernelReplSession',
JSON.stringify({
skipInteractive,
interactiveAvailable,
fishOff,
hasStdin: Boolean(stdin),
hasStdout: Boolean(stdout)
})
)
}
if (!skipInteractive && interactiveAvailable && !fishOff && stdin && stdout) {
fishRead = await createFishReadLine(ctx, {
stdin,
stdout,
writeScreen
})
if (isReplDebug()) {
replDbg(
'repl',
'createFishReadLine result',
fishRead ? 'attached' : 'null (fallback rawReadLine)'
)
}
if (fishRead && stdout && typeof stdout.write === 'function') {
stdout.write(
'\x1b[0m[bare-os-booter] Fish-style TTY line editor active\n'
)
}
}
let outConsole = baseConsole
if (fishRead && stdout) {
outConsole = createFishSyncedConsole(stdout)
}
if (isReplDebug()) {
replDbg(
'repl',
'console binding',
fishRead
? 'createFishSyncedConsole(session stdout)'
: 'process global console'
)
}
const coreReadLine = async (prompt) => {
if (isReplDebug()) {
replDbg(
'repl-readLine',
'invoke',
`impl=${fishRead ? 'fish' : 'raw'}`,
JSON.stringify(
typeof prompt === 'string' ? prompt.slice(0, 100) : prompt
)
)
}
const line = await (fishRead || rawReadLine)(prompt)
if (isReplDebug()) {
replDbg(
'repl-readLine',
'resolved',
line === null ? 'null' : `string len=${line.length}`,
line != null ? JSON.stringify(line.slice(0, 120)) : ''
)
}
if (line == null && !skipInteractive && !interactiveAvailable) {
baseConsole.log(
'(No interactive stdin — leaving booter and replication running. Close the app to quit.)'
)
await new Promise(() => {})
}
return line
}
const histPersistOn =
globalThis.process?.env?.BARE_OS_REPL_HISTORY === '1' ||
globalThis.process?.env?.BARE_OS_REPL_HISTORY === 'true'
const readLine =
!fishRead &&
histPersistOn &&
ctx &&
ctx.personalDrive &&
typeof ctx.personalDrive.put === 'function'
? wrapReadLineWithReplHistoryPersist(coreReadLine, ctx)
: coreReadLine
async function cleanup() {
if (isReplDebug())
replDbg('repl', 'cleanup', fishRead ? 'fish teardown' : 'noop')
await bareInitdShutdownActiveUnitsReverse(ctx)
await runKernelShutdownHooks()
stopBareInitd()
if (fishRead && stdin) {
disableFishRawMode(stdin)
releaseFishStdin(stdin)
}
unbindReplDebugStream()
}
return {
writeScreen,
readLine,
console: outConsole,
cleanup,
fishActive: Boolean(fishRead),
/** Set when fish owns the TTY; used to suspend/resume around host subprocesses. */
fishStdin: fishRead ? stdin : null
}
}