New tooling

Entry: scripts/run-bin.mjs
Preload shim (must load first): scripts/run-bin-prelude.mjs
Harness: scripts/lib/run-bin-harness.mjs

Usage:

node scripts/run-bin.mjs ls -la /bin
node scripts/run-bin.mjs cat /etc/passwd
node scripts/run-bin.mjs --cmd sed -n '1,5p' /home/user/note.txt
node scripts/run-bin.mjs --json echo "hello"
printf 'a\nb\n' | node scripts/run-bin.mjs cat
npm run test:coreutils -- --suite basic
npm run test:coreutils -- --build --watch -- echo test   # rebuild + watch src/<cmd>.js (use `--buil
This commit is contained in:
Raven Scott
2026-04-27 21:32:50 -04:00
parent 93742db1cc
commit ff9ab499a2
15 changed files with 926 additions and 6 deletions
+516
View File
@@ -0,0 +1,516 @@
#!/usr/bin/env node
/**
* Fast isolated /bin runner: temp Hyperdrives + createVfs + runBinCommand (no Pear / swarm).
* Usage: node scripts/run-bin.mjs [--json] [--build] [--cmd NAME] [--suite NAME] [--interactive] [--watch] [--no-vfs-snapshot] -- [args…]
*/
import './run-bin-prelude.mjs'
import { spawn } from 'node:child_process'
import { watch } from 'node:fs'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import readline from 'node:readline/promises'
import { fileURLToPath } from 'node:url'
import {
stdin as stdinStream,
stdout as stdoutStream,
stderr as stderrStream
} from 'node:process'
import { createRunBinHarness } from './lib/run-bin-harness.mjs'
/** Same mapping as bare-os-coreutils/build.mjs `commandSourceFile`. */
function commandSourceFile(binName) {
if (binName === 'nano') return 'edit'
if (binName === 'btop') return 'baretop'
if (binName === 'say') return 'baresay'
return binName
}
const G = '\x1b[32m'
const Y = '\x1b[33m'
const Dim = '\x1b[2m'
const R = '\x1b[31m'
const Reset = '\x1b[0m'
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'..'
)
function printHelp() {
stdoutStream.write(`Bare OS run-bin — execute built kernel/bin utilities in an isolated VFS (no Pear).
${Dim}Usage:${Reset}
node scripts/run-bin.mjs [options] <command> [args…]
node scripts/run-bin.mjs [options] --cmd <name> [args…]
printf 'a\\nb' | node scripts/run-bin.mjs cat
${Dim}Options:${Reset}
--json Machine-readable JSON on stdout (for agents / Cursor)
--build Run npm run build -w bare-os-coreutils first
--cmd <name> Command name when argv would be ambiguous
--suite <name> Run test/fixtures/coreutils/<name>.suite.json
--interactive REPL (reuse one harness)
--watch Watch coreutils src/<cmd>.js and rebuild + rerun last invocation
--no-vfs-snapshot Omit vfsSnapshot from --json (faster)
-h, --help This help
${Dim}Examples:${Reset}
node scripts/run-bin.mjs ls -la /bin
node scripts/run-bin.mjs --json echo hello
node scripts/run-bin.mjs --cmd sed -n '1,2p' /home/user/note.txt
npm run test:coreutils -- --suite basic
`)
}
function parseArgs(argv) {
/** @type {{ json: boolean, build: boolean, interactive: boolean, watch: boolean, vfsSnapshot: boolean, suite: string | null, cmdExplicit: string | null, help: boolean, positional: string[] }} */
const out = {
json: false,
build: false,
interactive: false,
watch: false,
vfsSnapshot: true,
suite: null,
cmdExplicit: null,
help: false,
positional: []
}
let i = 0
while (i < argv.length) {
const a = argv[i]
if (a === '--') {
out.positional.push(...argv.slice(i + 1))
break
}
if (a === '-h' || a === '--help') {
out.help = true
i++
continue
}
if (a === '--json') {
out.json = true
i++
continue
}
if (a === '--build') {
out.build = true
i++
continue
}
if (a === '--interactive') {
out.interactive = true
i++
continue
}
if (a === '--watch') {
out.watch = true
i++
continue
}
if (a === '--no-vfs-snapshot') {
out.vfsSnapshot = false
i++
continue
}
if (a === '--suite') {
out.suite = argv[i + 1] || ''
i += 2
continue
}
if (a.startsWith('--suite=')) {
out.suite = a.slice('--suite='.length)
i++
continue
}
if (a === '--cmd') {
out.cmdExplicit = argv[i + 1] || ''
i += 2
out.positional.push(...argv.slice(i))
break
}
if (a.startsWith('-')) {
stderrStream.write(`${R}run-bin:${Reset} unknown flag ${a}\n`)
process.exitCode = 2
return null
}
out.positional.push(...argv.slice(i))
break
}
return out
}
function runNpmBuild() {
return new Promise((resolve, reject) => {
const p = spawn('npm', ['run', 'build', '-w', 'bare-os-coreutils'], {
cwd: repoRoot,
stdio: 'inherit',
env: process.env
})
p.on('error', reject)
p.on('close', (code) => {
if (code === 0) resolve()
else
reject(new Error(`npm run build -w bare-os-coreutils exited ${code}`))
})
})
}
async function readStdinIfPiped() {
if (stdinStream.isTTY) return ''
const chunks = []
return await new Promise((resolve, reject) => {
let settled = false
let gotData = false
function finish() {
if (settled) return
settled = true
resolve(Buffer.concat(chunks).toString('utf8'))
}
stdinStream.on('data', (c) => {
gotData = true
chunks.push(c)
})
stdinStream.once('end', finish)
stdinStream.once('error', reject)
stdinStream.resume()
setTimeout(() => {
if (!gotData) finish()
}, 100)
})
}
async function executeOnce(harness, positional, opts) {
let argv
if (opts.cmdExplicit) {
argv = [opts.cmdExplicit, ...positional]
} else if (positional.length === 0) {
throw new Error(
'run-bin: missing command (pass a utility name or --suite / --interactive)'
)
} else {
argv = positional
}
const shellStdin = opts.stdinOverride ?? (await readStdinIfPiped())
const r = await harness.run(argv, {
shellStdin,
timeoutMs: harness.timeoutMs
})
const snap = await harness.vfsSnapshot({
skip: opts.vfsSnapshot === false
})
let errStr = null
if (r.thrown) errStr = r.thrown.stack || r.thrown.message
const command = argv[0] || ''
const payload = {
command,
argv,
exitCode: r.exitCode,
stdout: r.stdout,
stderr: r.stderr,
durationMs: Math.round(r.durationMs * 1000) / 1000,
error: errStr,
vfsSnapshot: snap ?? undefined
}
if (opts.json) {
stdoutStream.write(JSON.stringify(payload) + '\n')
} else {
if (r.stdout)
stdoutStream.write(r.stdout.endsWith('\n') ? r.stdout : r.stdout + '\n')
if (r.stderr)
stderrStream.write(
Y + r.stderr + Reset + (r.stderr.endsWith('\n') ? '' : '\n')
)
if (r.thrown) {
stderrStream.write(
R + (r.thrown.stack || r.thrown.message) + Reset + '\n'
)
}
const ec = r.exitCode
const ecColor = ec === 0 ? G : R
stderrStream.write(
`${Dim}(${ecColor}exit ${ec}${Dim} · ${Math.round(r.durationMs)}ms${Dim})${Reset}\n`
)
}
process.exitCode =
r.exitCode === 0 && !r.thrown ? 0 : r.thrown ? 1 : r.exitCode
return payload
}
/**
* @param {Awaited<ReturnType<typeof createRunBinHarness>} harness
* @param {*} parsed
*/
async function runInteractive(harness, parsed) {
const rl = readline.createInterface({
input: stdinStream,
output: stdoutStream,
terminal: stdinStream.isTTY
})
stderrStream.write(
`${G}run-bin REPL${Reset} — type a command line (empty to exit). cwd PWD=/\n`
)
try {
while (true) {
const line = await rl.question(`${Y}run-bin>${Reset} `)
const t = line.trim()
if (!t) break
const parts = tokenizeLine(t)
if (parts.length === 0) continue
parsed.cmdExplicit = null
parsed.positional = parts
parsed.stdinOverride = ''
await executeOnce(harness, parts, {
json: parsed.json,
vfsSnapshot: parsed.vfsSnapshot,
cmdExplicit: null,
stdinOverride: ''
})
}
} finally {
rl.close()
}
}
/** Minimal quoting: split on spaces, respect single-quoted segments */
function tokenizeLine(line) {
const out = []
let cur = ''
let quote = null
for (let i = 0; i < line.length; i++) {
const c = line[i]
if (quote === "'") {
if (c === "'") quote = null
else cur += c
continue
}
if (c === "'") {
quote = "'"
continue
}
if (/\s/.test(c)) {
if (cur.length) {
out.push(cur)
cur = ''
}
continue
}
cur += c
}
if (cur.length) out.push(cur)
return out
}
/**
* @param {Awaited<ReturnType<typeof createRunBinHarness>} harness
*/
async function runSuiteFile(harness, suiteName, parsed) {
const suitePath = path.join(
repoRoot,
'test/fixtures/coreutils',
`${suiteName}.suite.json`
)
let raw
try {
raw = JSON.parse(await readFile(suitePath, 'utf8'))
} catch (e) {
throw new Error(`run-bin: cannot read suite ${suitePath}: ${e}`)
}
const cases = raw.cases
if (!Array.isArray(cases)) {
throw new Error(`run-bin: suite ${suitePath} missing cases[]`)
}
/** @type {unknown[]} */
const results = []
let failed = 0
for (const c of cases) {
const id = String(c.id || '?')
const argv = c.argv
if (!Array.isArray(argv) || argv.length === 0) {
stderrStream.write(`${R}SKIP ${id}: invalid argv${Reset}\n`)
failed++
continue
}
if (c.fixture) await harness.applyFixture(String(c.fixture))
harness.ctx.shellStdin = typeof c.stdin === 'string' ? c.stdin : ''
harness.resetBuffers()
harness.ctx.exitCode = 0
const r = await harness.run(argv, { shellStdin: harness.ctx.shellStdin })
const exp = c.expect || {}
const wantEc = exp.exitCode
let ok = true
/** @type {string[]} */
const reasons = []
if (r.thrown) {
ok = false
reasons.push(String(r.thrown.message || r.thrown))
}
if (wantEc !== undefined && r.exitCode !== wantEc) {
ok = false
reasons.push(`exitCode want ${wantEc} got ${r.exitCode}`)
}
if (typeof exp.stdoutIncludes === 'string') {
if (!String(r.stdout).includes(exp.stdoutIncludes)) {
ok = false
reasons.push(`stdout missing substring`)
}
}
if (typeof exp.stderrIncludes === 'string') {
if (!String(r.stderr).includes(exp.stderrIncludes)) {
ok = false
reasons.push(`stderr missing substring`)
}
}
const row = {
id,
argv,
ok,
exitCode: r.exitCode,
stdout: r.stdout,
stderr: r.stderr,
durationMs: Math.round(r.durationMs * 1000) / 1000,
reasons: ok ? undefined : reasons,
error: r.thrown ? r.thrown.message : undefined
}
results.push(row)
if (!ok) failed++
if (parsed.json) continue
const tag = ok ? `${G}OK${Reset}` : `${R}FAIL${Reset}`
stderrStream.write(`${tag} ${id} (${Math.round(r.durationMs)}ms)\n`)
if (!ok) {
stderrStream.write(` ${reasons.join('; ')}\n`)
stderrStream.write(` stdout: ${JSON.stringify(r.stdout)}\n`)
}
}
if (parsed.json) {
stdoutStream.write(
JSON.stringify({ suite: suiteName, failed, cases: results }, null, 2) +
'\n'
)
} else if (failed === 0) {
stderrStream.write(`${G}All ${cases.length} case(s) passed.${Reset}\n`)
} else {
stderrStream.write(`${R}${failed} case(s) failed.${Reset}\n`)
}
process.exitCode = failed > 0 ? 1 : 0
}
async function prepareHarness() {
const harness = await createRunBinHarness(repoRoot)
try {
await harness.seedKernelBins()
} catch (e) {
await harness.cleanup()
throw new Error(
`run-bin: kernel/bin missing or unreadable — run: npm run build -w bare-os-coreutils (${e})`
)
}
await harness.seedLayout()
return harness
}
async function main() {
const parsed = parseArgs(process.argv.slice(2))
if (!parsed) return
if (parsed.help) {
printHelp()
return
}
if (parsed.build) await runNpmBuild()
if (parsed.suite) {
const harness = await prepareHarness()
try {
await runSuiteFile(harness, parsed.suite, parsed)
} finally {
await harness.cleanup()
}
return
}
const harness = await prepareHarness()
try {
if (parsed.interactive) {
await runInteractive(harness, parsed)
return
}
const positional = parsed.positional
if (positional.length === 0 && !parsed.cmdExplicit && !parsed.suite) {
stderrStream.write(
`${Y}run-bin:${Reset} missing arguments (try --help)\n`
)
process.exitCode = 2
return
}
await executeOnce(harness, positional, {
json: parsed.json,
vfsSnapshot: parsed.vfsSnapshot,
cmdExplicit: parsed.cmdExplicit
})
if (parsed.watch && !parsed.json) {
const buildOnWatch = parsed.build
const cmd = parsed.cmdExplicit || positional[0] || ''
const srcBase = commandSourceFile(cmd)
const srcFile = path.join(
repoRoot,
'packages/bare-os-coreutils/src',
`${srcBase}.js`
)
stderrStream.write(
`${Dim}Watching ${srcFile} · re-run uses ${buildOnWatch ? '`npm run build -w bare-os-coreutils`' : 'seed from existing kernel/bin only'}${Reset}\n`
)
let timer = null
watch(srcFile, { persistent: true }, () => {
if (timer) clearTimeout(timer)
timer = setTimeout(async () => {
stderrStream.write(`${Dim}Change detected…${Reset}\n`)
try {
if (buildOnWatch) {
stderrStream.write(
`${Dim}npm run build -w bare-os-coreutils…${Reset}\n`
)
await runNpmBuild()
}
await harness.seedKernelBins()
stderrStream.write(`${Dim}Re-seeded /bin — re-running…${Reset}\n`)
await executeOnce(harness, positional, {
json: false,
vfsSnapshot: parsed.vfsSnapshot,
cmdExplicit: parsed.cmdExplicit,
stdinOverride: ''
})
} catch (e) {
stderrStream.write(`${R}${e}${Reset}\n`)
}
}, 180)
})
await new Promise(() => {})
}
} finally {
if (!parsed.watch) await harness.cleanup()
}
}
main().catch((err) => {
stderrStream.write(`${R}${err.stack || err.message}${Reset}\n`)
process.exitCode = 1
})