206 lines
5.3 KiB
JavaScript
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
|
|
}
|
|
}
|