updates
This commit is contained in:
@@ -13,6 +13,12 @@ import {
|
||||
defaultBootCorestorePath,
|
||||
defaultLocalSeedCorestorePath
|
||||
} from './lib/paths.js'
|
||||
import {
|
||||
createBareReadlineQuestion,
|
||||
createStreamLineReader,
|
||||
looksLikeInteractiveStdin
|
||||
} from './lib/cli-readline.js'
|
||||
import { resolveStdio } from './lib/resolve-stdio.js'
|
||||
|
||||
const _pkg = packageRootDir(import.meta.url)
|
||||
|
||||
@@ -20,33 +26,85 @@ function bootStorePath() {
|
||||
return defaultBootCorestorePath(_pkg, import.meta.url)
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ readLine: (p: string) => Promise<string | null>, interactiveAvailable: boolean, skipInteractive: boolean }>} */
|
||||
async function createReadLine() {
|
||||
if (globalThis.process?.env?.BARE_OS_SKIP_REPL === '1') {
|
||||
return async () => null
|
||||
}
|
||||
try {
|
||||
const { createInterface } = await import('node:readline')
|
||||
const stdin = globalThis.process?.stdin
|
||||
const stdout = globalThis.process?.stdout
|
||||
if (!stdin || !stdout) {
|
||||
return async () => null
|
||||
const skipInteractive =
|
||||
globalThis.process?.env?.BARE_OS_SKIP_REPL === '1'
|
||||
if (skipInteractive) {
|
||||
return {
|
||||
readLine: async () => null,
|
||||
interactiveAvailable: false,
|
||||
skipInteractive: true
|
||||
}
|
||||
return (prompt) =>
|
||||
new Promise((resolve) => {
|
||||
const rl = createInterface({
|
||||
input: stdin,
|
||||
output: stdout
|
||||
})
|
||||
rl.question(prompt, (line) => {
|
||||
rl.close()
|
||||
resolve(line)
|
||||
})
|
||||
})
|
||||
} catch {
|
||||
}
|
||||
|
||||
const { stdin, stdout } = await resolveStdio()
|
||||
if (!stdin || !stdout) {
|
||||
console.warn(
|
||||
'[bare-os-booter] No node:readline; exiting REPL. Set BARE_OS_SKIP_REPL=1 for non-interactive kernel or run under Node.'
|
||||
'[bare-os-booter] No stdin/stdout (neither process nor bare-stdio). Cannot run interactive shell.'
|
||||
)
|
||||
return async () => null
|
||||
return {
|
||||
readLine: async () => null,
|
||||
interactiveAvailable: false,
|
||||
skipInteractive: false
|
||||
}
|
||||
}
|
||||
|
||||
let nodeReadline = null
|
||||
try {
|
||||
nodeReadline = await import('node:readline')
|
||||
} catch {
|
||||
/* Bare / Pear */
|
||||
}
|
||||
|
||||
if (nodeReadline?.createInterface) {
|
||||
const { createInterface } = nodeReadline
|
||||
return {
|
||||
readLine: (prompt) =>
|
||||
new Promise((resolve) => {
|
||||
const rl = createInterface({
|
||||
input: stdin,
|
||||
output: stdout
|
||||
})
|
||||
rl.question(prompt, (line) => {
|
||||
rl.close()
|
||||
resolve(line)
|
||||
})
|
||||
}),
|
||||
interactiveAvailable: true,
|
||||
skipInteractive: false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const readLine = await createBareReadlineQuestion(stdin, stdout)
|
||||
return {
|
||||
readLine,
|
||||
interactiveAvailable: true,
|
||||
skipInteractive: false
|
||||
}
|
||||
} catch {
|
||||
/* bare-readline failed to load */
|
||||
}
|
||||
|
||||
if (
|
||||
looksLikeInteractiveStdin(stdin) &&
|
||||
typeof stdout.write === 'function'
|
||||
) {
|
||||
return {
|
||||
readLine: createStreamLineReader(stdin, stdout),
|
||||
interactiveAvailable: true,
|
||||
skipInteractive: false
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'[bare-os-booter] No line input. Set BARE_OS_SKIP_REPL=1 to exit after boot.'
|
||||
)
|
||||
return {
|
||||
readLine: async () => null,
|
||||
interactiveAvailable: false,
|
||||
skipInteractive: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +115,20 @@ async function createReadLine() {
|
||||
* @param {Uint8Array} initSource
|
||||
*/
|
||||
async function executeKernel(disk, store, swarm, initSource) {
|
||||
const readLine = await createReadLine()
|
||||
const { readLine: rawReadLine, interactiveAvailable, skipInteractive } =
|
||||
await createReadLine()
|
||||
|
||||
/** Under Pear there is often no readline; never return null or the kernel exits and tears down the swarm. */
|
||||
const readLine = async (prompt) => {
|
||||
const line = await rawReadLine(prompt)
|
||||
if (line == null && !skipInteractive && !interactiveAvailable) {
|
||||
console.log(
|
||||
'(No interactive stdin — leaving booter and replication running. Close the app to quit.)'
|
||||
)
|
||||
await new Promise(() => {})
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
disk,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {import('stream').Readable} stdin
|
||||
* @param {import('stream').Writable} stdout
|
||||
* @returns {(prompt: string) => Promise<string>}
|
||||
*/
|
||||
export async function createBareReadlineQuestion(stdin, stdout) {
|
||||
const mod = await import('bare-readline')
|
||||
const createInterface =
|
||||
mod.createInterface ?? mod.default?.createInterface
|
||||
if (typeof createInterface !== 'function') {
|
||||
throw new Error('bare-readline: createInterface missing')
|
||||
}
|
||||
const rl = createInterface({
|
||||
input: stdin,
|
||||
output: stdout,
|
||||
prompt: ''
|
||||
})
|
||||
return (prompt) =>
|
||||
new Promise((resolve) => {
|
||||
rl.setPrompt(prompt)
|
||||
rl.prompt()
|
||||
rl.once('line', (line) => {
|
||||
resolve(line)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
let i
|
||||
while ((i = buf.indexOf('\n')) !== -1) {
|
||||
const raw = buf.slice(0, i)
|
||||
buf = buf.slice(i + 1)
|
||||
deliver(raw.replace(/\r$/, ''))
|
||||
}
|
||||
}
|
||||
|
||||
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') stdout.write(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'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Pear often runs without `process.stdin` / `process.stdout`; Holepunch uses `bare-stdio`
|
||||
* on fds 0–2 (same idea as pear-terminal's stdio singleton).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ stdin: import('stream').Readable | null, stdout: import('stream').Writable | null }>}
|
||||
*/
|
||||
export async function resolveStdio() {
|
||||
const p = globalThis.process
|
||||
if (p?.stdin && p?.stdout) {
|
||||
return { stdin: p.stdin, stdout: p.stdout }
|
||||
}
|
||||
try {
|
||||
const mod = await import('bare-stdio')
|
||||
const io = mod.default?.in ? mod.default : mod
|
||||
if (io?.in && io?.out) {
|
||||
return { stdin: io.in, stdout: io.out }
|
||||
}
|
||||
} catch (_) {
|
||||
/* optional under Node-only installs */
|
||||
}
|
||||
return { stdin: null, stdout: null }
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
"test": "brittle-node test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bare-readline": "^1.3.1",
|
||||
"bare-stdio": "^1.0.2",
|
||||
"bare-os": "^3.8.7",
|
||||
"bare-os-protocol": "*",
|
||||
"b4a": "^1.6.7",
|
||||
|
||||
@@ -5,7 +5,9 @@ import Corestore from 'corestore'
|
||||
import { mkdirSync, rmSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { runKernelFromSource, runBinCommand } from './lib/kernel-runner.js'
|
||||
import { createStreamLineReader } from './lib/cli-readline.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
@@ -46,6 +48,22 @@ test('runBinCommand runs /bin helper', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('createStreamLineReader yields lines after newline', async (t) => {
|
||||
const stdin = new PassThrough()
|
||||
const chunks = []
|
||||
const stdout = {
|
||||
write(s) {
|
||||
chunks.push(String(s))
|
||||
}
|
||||
}
|
||||
const readLine = createStreamLineReader(stdin, stdout)
|
||||
const p = readLine('> ')
|
||||
stdin.write('help\n')
|
||||
t.is(await p, 'help')
|
||||
t.ok(chunks.join('').includes('> '))
|
||||
stdin.end()
|
||||
})
|
||||
|
||||
test('Hyperdrive roundtrips /boot/init.js on Corestore', async (t) => {
|
||||
const dir = testCorestoreDir('seed')
|
||||
const store = new Corestore(dir)
|
||||
|
||||
Reference in New Issue
Block a user