This commit is contained in:
Raven Scott
2026-04-03 01:37:34 -04:00
parent 3b6cb97997
commit eca209b663
24 changed files with 1259 additions and 927 deletions
@@ -0,0 +1,154 @@
#!/usr/bin/env node
/**
* Standalone TTY repro for fish-style readline (lone `>`, missing `[user@host:path]`).
*
* Run in a real terminal (not piped):
* cd packages/bare-os-booter && node scripts/fish-tty-repro.js
* # or: npm run repro:fish-tty -w bare-os-booter
*
* Automated PTY (drains stdout so Node does not block; checks for green prompt):
* python3 scripts/run-fish-repro-pty.py
* # or: npm run repro:fish-tty:pty -w bare-os-booter
*
* Env:
* BARE_OS_REPRO_SKIP_STDERR=1 — omit stderr preamble (isolates stdout/stderr ordering).
* BARE_OS_REPRO_SKIP_PREAMBLE=1 — skip fake kernel lines; only fish prompt.
* BARE_OS_FISH_RESYNC_LINE=0 — disable leading newline before each prompt (default is on).
* DEBUG=1 — verbose fish/TTY logs on the same terminal as the shell (debug-repl.js).
*
* Type a few commands; use `exit` or Ctrl+D to quit. Compare first prompt vs after ^C.
*/
import b4a from 'b4a'
import { bindReplDebugStream, unbindReplDebugStream } from '../lib/debug-repl.js'
import {
createFishReadLine,
disableFishRawMode,
releaseFishStdin
} from '../lib/fish-readline.js'
function mockCtx() {
return {
vfs: {
getcwd: () => '/home/user',
resolveLogical: (p) => (p === '' ? '/' : p),
async readdir() {
return []
}
},
drive: {
readdir() {
async function* empty() {
/* no /bin entries */
}
return empty()
}
},
personalDrive: {
async get() {
return null
},
async put() {}
},
b4a,
env: {
HOME: '/home/user',
USER: 'user',
HOSTNAME: 'bare-os',
PATH: '/bin',
PWD: '/home/user'
}
}
}
/** Same idea as executeKernel writeScreen (TTY clear hook). */
function makeWriteScreen(stdout) {
return (chunk) => {
const s = typeof chunk === 'string' ? chunk : String(chunk)
stdout.write(s)
}
}
function kernelPreambleToStdout(stdout) {
const lines = [
'NAME="BareOS"',
'VERSION="0.1.0"',
'VARIANT="hyperdrive-only"',
'',
'Bare operating system — POSIX-ish shell: cd, export, exit, quit | try: help, ls /bin, pwd'
]
for (const line of lines) stdout.write(line + '\n')
}
async function main() {
const stdin = process.stdin
const stdout = process.stdout
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
process.stderr.write(
'fish-tty-repro: need a TTY with setRawMode (run in a terminal, not a pipe).\n'
)
process.exit(1)
}
const skipStderr = process.env.BARE_OS_REPRO_SKIP_STDERR === '1'
const skipPreamble = process.env.BARE_OS_REPRO_SKIP_PREAMBLE === '1'
if (!skipStderr) {
process.stderr.write(
'\x1b[0m[bare-os-booter] Fish-style TTY line editor active\n'
)
}
const ctx = mockCtx()
const writeScreen = makeWriteScreen(stdout)
if (process.env.DEBUG === '1') bindReplDebugStream(stdout)
const fishRead = await createFishReadLine(ctx, {
stdin,
stdout,
writeScreen
})
if (!fishRead) {
process.stderr.write('createFishReadLine returned null.\n')
process.exit(1)
}
if (!skipPreamble) {
kernelPreambleToStdout(stdout)
}
const readLine = (prompt) => fishRead(prompt)
process.stderr.write(
'\n--- repro ready: first prompts should show full [user@bare-os:~] > if healthy ---\n'
)
try {
while (true) {
const line = await readLine('repro> ')
if (line == null) {
stdout.write('\n')
break
}
const t = line.trim()
if (t === 'exit' || t === 'quit') break
if (t === 'cls' || t === 'clear') {
writeScreen('\x1b[H\x1b[2J\x1b[3J')
continue
}
if (t) stdout.write(`(cmd) ${t}\n`)
}
} finally {
disableFishRawMode(stdin)
releaseFishStdin(stdin)
unbindReplDebugStream()
}
}
main().catch((err) => {
process.stderr.write(String(err?.stack || err) + '\n')
process.exit(1)
})
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
Run fish-tty-repro.js under a pseudo-TTY and drain output so Node never blocks on write.
Usage (from anywhere):
python3 packages/bare-os-booter/scripts/run-fish-repro-pty.py
BARE_OS_REPRO_SKIP_STDERR=1 python3 packages/bare-os-booter/scripts/run-fish-repro-pty.py
BARE_OS_REPRO_PTY_ASSERT=1 ... # exit 2 if green prompt not detected (CI guard)
Requires: Unix (pty). Not for Windows CI.
"""
from __future__ import annotations
import os
import pty
import select
import sys
import threading
import time
HERE = os.path.dirname(os.path.abspath(__file__))
BOOTER = os.path.dirname(HERE)
def main() -> int:
chunks: list[bytes] = []
def reader(master_fd: int, done: threading.Event) -> None:
while not done.is_set():
try:
r, _, _ = select.select([master_fd], [], [], 0.2)
except (OSError, ValueError):
break
if done.is_set():
break
if not r:
continue
try:
data = os.read(master_fd, 65536)
except OSError:
break
if not data:
break
chunks.append(data)
pid, master_fd = pty.fork()
if pid == 0:
os.chdir(BOOTER)
os.execlp("node", "node", os.path.join(HERE, "fish-tty-repro.js"))
return 0 # unreachable
done = threading.Event()
t = threading.Thread(target=reader, args=(master_fd, done))
t.start()
time.sleep(0.6)
os.write(master_fd, b"hello\n")
time.sleep(0.35)
os.write(master_fd, b"exit\n")
# Wait for Node to exit before closing the PTY master (avoids exit code 1 / hang).
_, status = os.waitpid(pid, 0)
done.set()
t.join(timeout=5.0)
try:
os.close(master_fd)
except OSError:
pass
raw = b"".join(chunks)
try:
sys.stdout.buffer.write(raw)
except BrokenPipeError:
pass
text = raw.decode("utf-8", errors="replace")
ok_green = "\x1b[32m[user@bare-os" in text or "[user@bare-os" in text
sys.stderr.write(
f"\n--- pty harness: exit status {status}, bytes {len(raw)}, "
f"saw_full_prompt_hint={ok_green} ---\n"
)
if os.environ.get("BARE_OS_REPRO_PTY_ASSERT") == "1" and not ok_green:
sys.stderr.write("BARE_OS_REPRO_PTY_ASSERT: expected green [user@bare-os] prompt\n")
return 2
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
return 1
if __name__ == "__main__":
sys.exit(main())