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 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, * interactiveAvailable: boolean, * skipInteractive: boolean, * ctx: Record * }} 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 } }