updates
CI / test (push) Failing after 4m56s

This commit is contained in:
Raven Scott
2026-04-02 22:35:09 -04:00
parent efeee0088a
commit 2e26b14250
56 changed files with 1838 additions and 75 deletions
+27 -14
View File
@@ -4,10 +4,11 @@ import b4a from 'b4a'
import safetyCatch from 'safety-catch'
import Hyperdrive from 'hyperdrive'
import Corestore from 'corestore'
import path from 'path'
import { topicKey, parseMbr } from 'bare-os-protocol'
import { SwarmDisk } from './lib/swarm-disk.js'
import { runKernelFromSource, runBinCommand } from './lib/kernel-runner.js'
import { runKernelFromSource } from './lib/kernel-runner.js'
import { createVfs } from './lib/vfs.js'
import { execShellLine } from './lib/shell.js'
import {
packageRootDir,
defaultBootCorestorePath,
@@ -28,8 +29,7 @@ function bootStorePath() {
/** @returns {Promise<{ readLine: (p: string) => Promise<string | null>, interactiveAvailable: boolean, skipInteractive: boolean }>} */
async function createReadLine() {
const skipInteractive =
globalThis.process?.env?.BARE_OS_SKIP_REPL === '1'
const skipInteractive = globalThis.process?.env?.BARE_OS_SKIP_REPL === '1'
if (skipInteractive) {
return {
readLine: async () => null,
@@ -87,10 +87,7 @@ async function createReadLine() {
/* bare-readline failed to load */
}
if (
looksLikeInteractiveStdin(stdin) &&
typeof stdout.write === 'function'
) {
if (looksLikeInteractiveStdin(stdin) && typeof stdout.write === 'function') {
return {
readLine: createStreamLineReader(stdin, stdout),
interactiveAvailable: true,
@@ -115,8 +112,11 @@ async function createReadLine() {
* @param {Uint8Array} initSource
*/
async function executeKernel(disk, store, swarm, initSource) {
const { readLine: rawReadLine, interactiveAvailable, skipInteractive } =
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) => {
@@ -130,18 +130,28 @@ async function executeKernel(disk, store, swarm, initSource) {
return line
}
const shellEnv = {
HOME: '/home/user',
PATH: '/bin',
USER: 'user',
PWD: '/home/user',
SHELL: 'bare-sh',
0: 'bare-os'
}
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv)
const ctx = {
disk,
drive: disk.drive,
personalDrive: disk.personalDrive,
vfs,
env: shellEnv,
console,
b4a,
topic: topicKey(),
readLine,
async execLine(line) {
const parts = line.trim().split(/\s+/)
if (!parts[0]) return
await runBinCommand(disk.drive, parts, ctx)
return await execShellLine(ctx, line)
}
}
@@ -166,7 +176,10 @@ async function bootFromPeers(disk, store, swarm) {
let initSource = null
for (const driveKey of keys) {
try {
console.log('Mounting drive', b4a.toString(driveKey, 'hex').slice(0, 16) + '...')
console.log(
'Mounting drive',
b4a.toString(driveKey, 'hex').slice(0, 16) + '...'
)
disk.drive = new Hyperdrive(store, driveKey)
await disk.drive.ready()
+3 -4
View File
@@ -10,8 +10,7 @@
*/
export async function createBareReadlineQuestion(stdin, stdout) {
const mod = await import('bare-readline')
const createInterface =
mod.createInterface ?? mod.default?.createInterface
const createInterface = mod.createInterface ?? mod.default?.createInterface
if (typeof createInterface !== 'function') {
throw new Error('bare-readline: createInterface missing')
}
@@ -90,7 +89,7 @@ export function createStreamLineReader(stdin, stdout) {
export function looksLikeInteractiveStdin(stdin) {
return Boolean(
stdin &&
typeof stdin.on === 'function' &&
typeof stdin.resume === 'function'
typeof stdin.on === 'function' &&
typeof stdin.resume === 'function'
)
}
+42 -12
View File
@@ -1,4 +1,5 @@
import b4a from 'b4a'
import unixPathResolve from 'unix-path-resolve'
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
@@ -16,23 +17,52 @@ export async function runKernelFromSource(source, ctx) {
}
/**
* Run /bin/<cmd> script with `async function run(ctx, argv)`.
* @param {import('hyperdrive').default} drive
* @param {string[]} argv
* @param {Record<string, unknown>} ctx
* @param {string} src
* @param {string[]} argv
* @param {string} [label]
*/
export async function runBinCommand(drive, argv, ctx) {
const cmd = argv[0]
const buf = await drive.get('/bin/' + cmd)
if (!buf) {
ctx.console.log('unknown command: ' + cmd)
return
}
const src = b4a.toString(buf)
async function runScriptFromSource(ctx, src, argv, label = argv[0]) {
const fn = new AsyncFunction(
'ctx',
'argv',
`${src}\nif (typeof run !== 'function') throw new Error('missing run() in /bin/${cmd}')\nreturn run(ctx, argv)\n`
`${src}\nif (typeof run !== 'function') throw new Error('missing run() in ${label}')\nreturn run(ctx, argv)\n`
)
return fn(ctx, argv)
}
/**
* Run a command: PATH on system drive, or path script on routed drive.
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runBinCommand(ctx, argv) {
const cmd = argv[0]
const systemDrive = ctx.drive
const vfs = ctx.vfs
const pathEnv = (vfs && vfs.env && vfs.env.PATH) || '/bin'
if (cmd.includes('/')) {
const abs = vfs.resolveLogical(cmd)
const { drive, path } = vfs.route(abs)
const buf = await drive.get(path, { follow: true })
if (!buf) {
ctx.console.log('not found: ' + cmd)
return
}
const source = b4a.toString(buf)
return runScriptFromSource(ctx, source, argv, cmd)
}
const dirs = pathEnv.split(':').filter(Boolean)
for (const dir of dirs) {
const p = unixPathResolve(dir, cmd)
const buf = await systemDrive.get(p, { follow: true })
if (buf) {
const source = b4a.toString(buf)
return runScriptFromSource(ctx, source, argv, p)
}
}
ctx.console.log('unknown command: ' + cmd)
}
+318
View File
@@ -0,0 +1,318 @@
import { runBinCommand } from './kernel-runner.js'
/**
* @typedef {{ type: 'word', value: string } | { type: 'op', value: string }} Token
*/
/** @param {string} line */
export function tokenize(line) {
/** @type {Token[]} */
const tokens = []
let i = 0
const skipWs = () => {
while (i < line.length && /\s/.test(line[i])) i++
}
while (i < line.length) {
skipWs()
if (i >= line.length) break
const c = line[i]
if (c === '|') {
tokens.push({ type: 'op', value: '|' })
i++
continue
}
if (c === '>') {
if (line[i + 1] === '>') {
tokens.push({ type: 'op', value: '>>' })
i += 2
} else {
tokens.push({ type: 'op', value: '>' })
i++
}
continue
}
if (c === '<') {
tokens.push({ type: 'op', value: '<' })
i++
continue
}
let word = ''
while (i < line.length) {
const ch = line[i]
if (ch === '\\') {
i++
if (i < line.length) word += line[i++]
continue
}
if (ch === "'") {
i++
while (i < line.length && line[i] !== "'") {
word += line[i++]
}
if (i < line.length) i++
continue
}
if (ch === '"') {
i++
while (i < line.length && line[i] !== '"') {
if (line[i] === '\\' && i + 1 < line.length) {
i++
word += line[i++]
continue
}
word += line[i++]
}
if (i < line.length) i++
continue
}
if (/\s/.test(ch) || ch === '|' || ch === '>' || ch === '<') break
word += ch
i++
}
if (word.length) tokens.push({ type: 'word', value: word })
}
return tokens
}
/**
* @param {string} s
* @param {Record<string, string>} env
*/
export function expandWord(s, env) {
let out = ''
let j = 0
while (j < s.length) {
if (s[j] === '$') {
if (s[j + 1] === '{') {
const end = s.indexOf('}', j + 2)
if (end === -1) {
out += s.slice(j)
break
}
const name = s.slice(j + 2, end)
out += env[name] ?? ''
j = end + 1
continue
}
if (/[0-9]/.test(s[j + 1] ?? '')) {
out += env[s[j + 1]] ?? ''
j += 2
continue
}
let k = j + 1
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
const name = s.slice(j + 1, k)
if (name) {
out += env[name] ?? ''
j = k
} else {
out += '$'
j++
}
continue
}
out += s[j++]
}
return out
}
/**
* @typedef {{ argv: string[], assign: Record<string, string>, redirIn: string | null, redirOut: string | null, redirAppend: boolean }} SimpleCmd
*/
/**
* @param {Token[]} tokens
* @returns {SimpleCmd[][]}
*/
export function parsePipeline(tokens) {
/** @type {Token[][]} */
const pipes = [[]]
for (const t of tokens) {
if (t.type === 'op' && t.value === '|') {
pipes.push([])
} else {
pipes[pipes.length - 1].push(t)
}
}
return pipes.map((seg) => parseSimpleCommand(seg))
}
/** @param {Token[]} seg */
function parseSimpleCommand(seg) {
/** @type {Record<string, string>} */
const assign = {}
let redirIn = null
let redirOut = null
let redirAppend = false
/** @type {string[]} */
const argvWords = []
let seenCommand = false
let w = 0
while (w < seg.length) {
const t = seg[w]
if (t.type === 'op') {
if (t.value === '>' || t.value === '>>') {
const n = seg[w + 1]
if (n && n.type === 'word') {
redirAppend = t.value === '>>'
redirOut = n.value
w += 2
continue
}
}
if (t.value === '<') {
const n = seg[w + 1]
if (n && n.type === 'word') {
redirIn = n.value
w += 2
continue
}
}
w++
continue
}
const v = t.value
if (!seenCommand) {
const eq = v.indexOf('=')
if (eq > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(v.slice(0, eq))) {
assign[v.slice(0, eq)] = v.slice(eq + 1)
w++
continue
}
}
seenCommand = true
argvWords.push(v)
w++
}
let i = 0
while (i < argvWords.length) {
if (argvWords[i] === '>') {
redirOut = argvWords[i + 1] ?? ''
redirAppend = false
argvWords.splice(i, 2)
continue
}
if (argvWords[i] === '>>') {
redirOut = argvWords[i + 1] ?? ''
redirAppend = true
argvWords.splice(i, 2)
continue
}
if (argvWords[i] === '<') {
redirIn = argvWords[i + 1] ?? ''
argvWords.splice(i, 2)
continue
}
i++
}
return { argv: argvWords, assign, redirIn, redirOut, redirAppend }
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} line
* @returns {Promise<'exit' | 'ok'>}
*/
export async function execShellLine(ctx, line) {
const raw = line.trim()
if (!raw) return 'ok'
const tokens = tokenize(raw)
if (!tokens.length) return 'ok'
const pipeline = parsePipeline(tokens)
const vfs = ctx.vfs
const env = vfs.env
let stdinText = typeof ctx.shellStdin === 'string' ? ctx.shellStdin : null
for (let pi = 0; pi < pipeline.length; pi++) {
const cmd = pipeline[pi]
const isLast = pi === pipeline.length - 1
for (const [k, val] of Object.entries(cmd.assign)) {
env[k] = expandWord(val, env)
}
if (!cmd.argv.length) continue
const argv = cmd.argv.map((w) => expandWord(w, env))
const name = argv[0]
if (cmd.redirIn) {
const p = expandWord(cmd.redirIn, env)
const buf = await vfs.readFile(p)
stdinText = buf ? ctx.b4a.toString(buf) : ''
}
const outChunks = []
const origLog = ctx.console.log
const origErr = ctx.console.error
if (!isLast || cmd.redirOut) {
ctx.console.log = (...args) => {
outChunks.push(args.map(String).join(' ') + '\n')
}
ctx.console.error = ctx.console.log
}
let code = 'ok'
try {
if (name === 'cd') {
try {
await vfs.chdir(argv[1] || vfs.home)
} catch (e) {
origErr.call(ctx.console, (e && e.message) || String(e))
}
} else if (name === 'export') {
for (const a of argv.slice(1)) {
const eq = a.indexOf('=')
if (eq > 0) env[a.slice(0, eq)] = expandWord(a.slice(eq + 1), env)
}
} else if (name === 'exit') {
code = 'exit'
} else {
const childCtx =
stdinText != null
? Object.assign({}, ctx, { shellStdin: stdinText, env })
: Object.assign({}, ctx, { env })
await runBinCommand(childCtx, argv)
}
} finally {
if (!isLast || cmd.redirOut) {
ctx.console.log = origLog
ctx.console.error = origErr
}
}
if (code === 'exit') return 'exit'
let pipeOut = outChunks.join('')
if (cmd.redirOut) {
const path = expandWord(cmd.redirOut, env)
const data = ctx.b4a.from(pipeOut)
if (cmd.redirAppend) {
const prev = await vfs.readFile(path)
const merged = prev ? ctx.b4a.concat([prev, data]) : data
await vfs.writeFile(path, merged)
} else {
await vfs.writeFile(path, data)
}
pipeOut = ''
}
stdinText = isLast ? null : pipeOut
ctx.shellStdin = stdinText ?? undefined
}
return 'ok'
}
+10 -3
View File
@@ -151,7 +151,10 @@ export class SwarmDisk {
c.array(c.string).encode(state, m.matches)
},
decode(state) {
return { id: c.uint32.decode(state), matches: c.array(c.string).decode(state) }
return {
id: c.uint32.decode(state),
matches: c.array(c.string).decode(state)
}
}
},
onmessage: (m, ch) => ch.userData.onsearchres(m)
@@ -242,14 +245,18 @@ export class SwarmDisk {
this.peers.delete(peer)
})
if (this.drive) this.drive.replicate(mux.stream, { live: true, download: true })
if (this.drive)
this.drive.replicate(mux.stream, { live: true, download: true })
if (this.personalDrive) this.personalDrive.replicate(mux.stream)
}
async read(index) {
if (this.localRAM.has(index)) return this.localRAM.get(index)
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('MBR read timeout')), 10000)
const timeout = setTimeout(
() => reject(new Error('MBR read timeout')),
10000
)
this.pendingReads.set(index, (data) => {
clearTimeout(timeout)
resolve(data)
+144
View File
@@ -0,0 +1,144 @@
import unixPathResolve from 'unix-path-resolve'
/**
* Unified path view: system Hyperdrive for OS paths, personal Hyperdrive under $HOME.
* @param {import('hyperdrive').default} systemDrive
* @param {import('hyperdrive').default} personalDrive
* @param {Record<string, string>} env
*/
export function createVfs(systemDrive, personalDrive, env) {
const HOME = () => env.HOME || '/home/user'
let cwd = env.PWD || HOME()
function normalizeHome() {
const h = HOME()
return h.length > 1 && h.endsWith('/') ? h.slice(0, -1) : h
}
/** Logical absolute path from cwd + user path */
function resolveLogical(userPath) {
return unixPathResolve(cwd, userPath)
}
/** Map logical absolute path to { drive, path } for Hyperdrive ops */
function route(absPath) {
const h = normalizeHome()
if (absPath === h || absPath.startsWith(h + '/')) {
const sub =
absPath === h
? '/'
: '/' + absPath.slice(h.length + 1).replace(/^\//, '')
const p = unixPathResolve('/', sub)
return { drive: personalDrive, path: p }
}
return { drive: systemDrive, path: absPath }
}
async function entryOn(drive, p, opts) {
return drive.entry(p, opts)
}
async function isRegularFile(absPath) {
const { drive, path: p } = route(absPath)
const e = await entryOn(drive, p, { follow: true })
return !!(e && e.value && e.value.blob)
}
return {
get home() {
return normalizeHome()
},
getcwd() {
return cwd
},
resolveLogical,
route,
env,
async chdir(userPath) {
const abs = resolveLogical(userPath)
if (await isRegularFile(abs)) {
throw new Error('Not a directory: ' + userPath)
}
cwd = abs
env.PWD = cwd
},
async readFile(userPath) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
return drive.get(p, { follow: true })
},
async writeFile(userPath, buf, opts = {}) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
if (drive !== personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + userPath)
}
return drive.put(p, buf, opts)
},
async unlink(userPath) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
if (drive !== personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + userPath)
}
return drive.del(p)
},
async exists(userPath) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
return drive.exists(p)
},
/** @returns {Promise<string[]>} */
async readdir(userPath) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
const folder = p === '/' ? '/' : p
const names = []
const stream = drive.readdir(folder)
for await (const name of stream) {
names.push(name)
}
return names.sort()
},
async stat(userPath) {
const abs = resolveLogical(userPath)
const { drive, path: p } = route(abs)
const e = await entryOn(drive, p, { follow: true })
if (e && e.value && e.value.blob) {
const bl = e.value.blob
const len =
typeof bl.byteLength === 'number'
? bl.byteLength
: (bl.blockLength ?? 0)
return { type: 'file', size: len, path: abs }
}
if (e && e.value && e.value.linkname) {
return { type: 'symlink', path: abs }
}
const names = await (async () => {
const out = []
try {
const stream = drive.readdir(p === '/' ? '/' : p)
for await (const n of stream) out.push(n)
} catch {
/* treat as missing */
}
return out
})()
if (names.length) return { type: 'directory', path: abs }
if (e) return { type: 'directory', path: abs }
return null
}
}
}
+2 -1
View File
@@ -20,7 +20,8 @@
"hyperdrive": "^13.3.2",
"hyperswarm": "^4.16.0",
"protomux": "^3.10.1",
"safety-catch": "^1.0.2"
"safety-catch": "^1.0.2",
"unix-path-resolve": "^1.0.2"
},
"devDependencies": {
"brittle": "^3.1.0"
+118 -2
View File
@@ -8,15 +8,40 @@ 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'
import { createVfs } from './lib/vfs.js'
import { tokenize, expandWord, execShellLine } from './lib/shell.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
function testCorestoreDir(name) {
const dir = path.join(__dirname, '.test-data', name + '-' + process.pid + '-' + Math.random().toString(36).slice(2))
const dir = path.join(
__dirname,
'.test-data',
name + '-' + process.pid + '-' + Math.random().toString(36).slice(2)
)
mkdirSync(path.dirname(dir), { recursive: true })
return dir
}
function testCtx(drive, personal, env) {
const shellEnv = {
HOME: '/home/user',
PATH: '/bin',
USER: 'user',
PWD: '/home/user',
...env
}
const vfs = createVfs(drive, personal, shellEnv)
return {
drive,
personalDrive: personal,
vfs,
env: shellEnv,
console,
b4a
}
}
test('runKernelFromSource invokes start(ctx)', async (t) => {
const calls = []
const source = `
@@ -32,7 +57,9 @@ test('runBinCommand runs /bin helper', async (t) => {
const dir = testCorestoreDir('bin')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('p'))
await drive.ready()
await personal.ready()
await drive.put(
'/bin/hello',
b4a.from(`
@@ -42,7 +69,9 @@ test('runBinCommand runs /bin helper', async (t) => {
`)
)
const out = []
await runBinCommand(drive, ['hello', 'a', 'b'], { out, console })
const ctx = testCtx(drive, personal)
ctx.out = out
await runBinCommand(ctx, ['hello', 'a', 'b'])
t.is(out[0], 'hello a b')
await store.close()
rmSync(dir, { recursive: true, force: true })
@@ -76,3 +105,90 @@ test('Hyperdrive roundtrips /boot/init.js on Corestore', async (t) => {
await drive.close()
rmSync(dir, { recursive: true, force: true })
})
test('vfs routes HOME to personal drive', async (t) => {
const dir = testCorestoreDir('vfs')
const store = new Corestore(dir)
const sys = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pv'))
await sys.ready()
await personal.ready()
const env = { HOME: '/home/user', PWD: '/home/user', PATH: '/bin' }
const vfs = createVfs(sys, personal, env)
await vfs.writeFile('f.txt', b4a.from('hi'))
const buf = await personal.get('/f.txt')
t.ok(buf)
t.is(b4a.toString(buf), 'hi')
const sysTry = await sys.get('/home/user/f.txt')
t.is(sysTry, null)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('tokenize handles quotes and ops', async (t) => {
const tok = tokenize('ls -la | cat > out')
t.ok(tok.some((x) => x.type === 'op' && x.value === '|'))
t.ok(tok.some((x) => x.type === 'op' && x.value === '>'))
const w = tokenize("echo 'a b'")
const words = w.filter((x) => x.type === 'word').map((x) => x.value)
t.is(words.join(','), 'echo,a b')
})
test('expandWord reads env', async (t) => {
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
})
test('execShellLine runs cd and external', async (t) => {
const dir = testCorestoreDir('sh')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('psh'))
await drive.ready()
await personal.ready()
await drive.put(
'/bin/xy',
b4a.from(`
async function run(ctx, argv) {
ctx.got.push(argv.join(' '))
}
`)
)
const got = []
const ctx = testCtx(drive, personal)
ctx.got = got
await execShellLine(ctx, 'cd /bin')
t.is(ctx.vfs.getcwd(), '/bin')
await execShellLine(ctx, 'xy one two')
t.is(got[0], 'xy one two')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 cat from system drive', async (t) => {
const dir = testCorestoreDir('cat')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pc'))
await drive.ready()
await personal.ready()
await drive.put('/bin/cat', b4a.from(await readBuiltBin('cat')))
await drive.put('/etc/x', b4a.from('hello'))
const lines = []
const ctx = testCtx(drive, personal)
ctx.console = {
log(s) {
lines.push(String(s))
},
error() {}
}
await runBinCommand(ctx, ['cat', '/etc/x'])
t.is(lines.join('\n'), 'hello')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
async function readBuiltBin(name) {
const fs = await import('node:fs/promises')
const p = path.join(__dirname, '../../kernel/bin', name)
return fs.readFile(p, 'utf8')
}
+44
View File
@@ -0,0 +1,44 @@
import { readFile, writeFile, mkdir } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '../..')
const kernelBin = join(repoRoot, 'kernel/bin')
const seederKernelBin = join(repoRoot, 'packages/bare-os-seeder/kernel/bin')
const commands = [
'ls',
'pwd',
'cat',
'test',
'basename',
'dirname',
'wc',
'head',
'tail',
'uname',
'pathchk',
'echo',
'help'
]
export async function build() {
const runtime = await readFile(join(__dirname, 'lib/runtime.js'), 'utf8')
await mkdir(kernelBin, { recursive: true })
await mkdir(seederKernelBin, { recursive: true })
for (const name of commands) {
const body = await readFile(join(__dirname, 'src', `${name}.js`), 'utf8')
const out = runtime + '\n' + body
await writeFile(join(kernelBin, name), out)
await writeFile(join(seederKernelBin, name), out)
}
}
const isMain =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (isMain) {
await build()
}
@@ -0,0 +1,4 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "bare-os-coreutils",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./build.mjs"
}
}
@@ -0,0 +1,17 @@
async function run(ctx, argv) {
const parts = argv.slice(1).filter((a) => a !== '--')
if (!parts.length) {
ctx.console.error('basename: missing operand')
return
}
const path = parts[0]
const suffix = parts[1] || ''
let base = path.replace(/\/+$/, '')
const slash = base.lastIndexOf('/')
base = slash === -1 ? base : base.slice(slash + 1)
if (!base) base = path
if (suffix && base.endsWith(suffix) && base.length > suffix.length) {
base = base.slice(0, -suffix.length)
}
ctx.console.log(base)
}
+17
View File
@@ -0,0 +1,17 @@
async function run(ctx, argv) {
const vfs = ctx.vfs
const files = argv.slice(1)
if (!files.length) {
const s = bareStdin(ctx)
ctx.console.log(s)
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('cat: ' + f + ': No such file or directory')
continue
}
ctx.console.log(ctx.b4a.toString(buf))
}
}
+14
View File
@@ -0,0 +1,14 @@
async function run(ctx, argv) {
const parts = argv.slice(1).filter((a) => a !== '--')
if (!parts.length) {
ctx.console.error('dirname: missing operand')
return
}
for (const path of parts) {
const cleaned = path.replace(/\/+$/, '') || '/'
const i = cleaned.lastIndexOf('/')
const out =
i <= 0 ? (cleaned[0] === '/' ? '/' : '.') : cleaned.slice(0, i) || '/'
ctx.console.log(out)
}
}
+15
View File
@@ -0,0 +1,15 @@
async function run(ctx, argv) {
const parts = argv.slice(1)
let n = false
if (parts[0] === '-n') {
n = true
parts.shift()
}
const s = parts.join(' ')
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, s + (n ? '' : '\n'))
} else {
ctx.console.log(n ? s : s)
}
}
+33
View File
@@ -0,0 +1,33 @@
async function run(ctx, argv) {
const vfs = ctx.vfs
let n = 10
let start = 1
if (argv[1] === '-n' && argv[2]) {
n = parseInt(argv[2], 10) || 10
start = 3
} else if (argv[1] && /^-\d+$/.test(argv[1])) {
n = parseInt(argv[1].slice(1), 10) || 10
start = 2
}
const files = argv.slice(start).filter((a) => a !== '--')
const take = (s) => {
const lines = s.split('\n')
const out = lines.slice(0, n).join('\n')
ctx.console.log(
out + (out && !out.endsWith('\n') && lines.length > n ? '\n' : '')
)
}
if (!files.length) {
take(bareStdin(ctx))
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('head: ' + f + ': No such file')
continue
}
if (files.length > 1) ctx.console.log('==> ' + f + ' <==')
take(ctx.b4a.toString(buf))
}
}
+5
View File
@@ -0,0 +1,5 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS userland — builtins: cd, export, exit | /bin: ls pwd cat echo test basename dirname wc head tail uname pathchk help'
)
}
+47
View File
@@ -0,0 +1,47 @@
async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
let longFmt = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
if (c === 'a') showAll = true
else if (c === 'l') longFmt = true
else if (c === '1') longFmt = false
}
continue
}
paths.push(a)
}
const targets = paths.length ? paths : ['.']
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
let names
try {
names = await vfs.readdir(t)
} catch (e) {
ctx.console.error('ls: cannot access ' + t + ': ' + (e.message || e))
continue
}
if (!showAll) names = names.filter((n) => n !== '.' && n !== '..')
if (!longFmt) {
ctx.console.log(names.join(' '))
} else {
for (const n of names) {
const sub = t === '.' || t === './' ? n : t.replace(/\/$/, '') + '/' + n
const st = await vfs.stat(sub)
const tag = st ? (st.type === 'directory' ? 'd' : '-') : '?'
const sz = st && st.size != null ? String(st.size) : '0'
ctx.console.log(tag + 'rwxr-xr-x 1 user user ' + sz + ' ' + n)
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => !a.startsWith('-'))
if (!paths.length) {
ctx.console.error('pathchk: missing operand')
return
}
for (const p of paths) {
if (!p.length) {
ctx.console.error('pathchk: empty path name')
continue
}
if (p.length > 4096) {
ctx.console.error('pathchk: path too long')
continue
}
if (p.includes('\0')) {
ctx.console.error('pathchk: NUL in path')
continue
}
}
}
+3
View File
@@ -0,0 +1,3 @@
async function run(ctx, argv) {
ctx.console.log(ctx.vfs.getcwd())
}
+29
View File
@@ -0,0 +1,29 @@
async function run(ctx, argv) {
const vfs = ctx.vfs
let n = 10
let start = 1
if (argv[1] === '-n' && argv[2]) {
n = parseInt(argv[2], 10) || 10
start = 3
}
const files = argv.slice(start).filter((a) => a !== '--')
const take = (s) => {
let lines = s.split('\n')
if (s.endsWith('\n') && lines[lines.length - 1] === '') lines.pop()
const slice = lines.length <= n ? lines : lines.slice(-n)
ctx.console.log(slice.join('\n'))
}
if (!files.length) {
take(bareStdin(ctx))
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('tail: ' + f + ': No such file')
continue
}
if (files.length > 1) ctx.console.log('==> ' + f + ' <==')
take(ctx.b4a.toString(buf))
}
}
+30
View File
@@ -0,0 +1,30 @@
async function evalTest(ctx, args) {
if (!args.length) return false
if (args[0] === '!') {
const inner = await evalTest(ctx, args.slice(1))
return !inner
}
if (args.length === 3) {
const [a, op, b] = args
if (op === '=') return a === b
if (op === '!=') return a !== b
return false
}
if (args.length === 2) {
const op = args[0]
const p = args[1]
const st = await ctx.vfs.stat(p)
if (op === '-e' || op === '-a') return st != null
if (op === '-f') return st != null && st.type === 'file'
if (op === '-d') return st != null && st.type === 'directory'
if (op === '-z') return p.length === 0
if (op === '-n') return p.length > 0
return false
}
if (args.length === 1) return args[0] !== ''
return false
}
async function run(ctx, argv) {
ctx.exitCode = (await evalTest(ctx, argv.slice(1))) ? 0 : 1
}
+32
View File
@@ -0,0 +1,32 @@
async function run(ctx, argv) {
const flagArgs = argv.slice(1).filter((a) => a.startsWith('-') && a !== '--')
const all = argv.includes('-a')
const noFlag = flagArgs.length === 0
const wantS = all || argv.includes('-s') || noFlag
const wantN = all || argv.includes('-n')
const wantR = all || argv.includes('-r')
const wantM = all || argv.includes('-m')
const wantV = all || argv.includes('-v')
let name = 'BareOS'
let version = '0.1'
const buf = await ctx.vfs.readFile('/etc/os-release')
if (buf) {
const t = ctx.b4a.toString(buf)
for (const line of t.split('\n')) {
const id = line.match(/^NAME=(.*)$/)
if (id) name = id[1].replace(/^"|"$/g, '')
const ver = line.match(/^VERSION=(.*)$/)
if (ver) version = ver[1].replace(/^"|"$/g, '')
}
}
const parts = []
if (wantS) parts.push(name)
if (wantN) parts.push('bare-os')
if (wantR) parts.push(version)
if (wantV) parts.push('bare-userland')
if (wantM) parts.push('unknown')
ctx.console.log(parts.join(' '))
}
+36
View File
@@ -0,0 +1,36 @@
function count(s) {
const lines = (s.match(/\n/g) || []).length
const words = s.trim() ? s.trim().split(/\s+/).length : 0
const bytes = new TextEncoder().encode(s).length
return { lines, words, bytes }
}
async function run(ctx, argv) {
const vfs = ctx.vfs
const files = argv.slice(1).filter((a) => !a.startsWith('-'))
if (!files.length) {
const s = bareStdin(ctx)
const c = count(s)
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes)
return
}
let tLines = 0
let tWords = 0
let tBytes = 0
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('wc: ' + f + ': No such file')
continue
}
const s = ctx.b4a.toString(buf)
const c = count(s)
tLines += c.lines
tWords += c.words
tBytes += c.bytes
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes + ' ' + f)
}
if (files.length > 1) {
ctx.console.log(' ' + tLines + ' ' + tWords + ' ' + tBytes + ' total')
}
}
+6
View File
@@ -65,6 +65,12 @@ async function main() {
console.clear?.()
console.log('--- bare-os-seeder (Hyperdrive + MBR) ---')
const { build } = await import(
new URL('../bare-os-coreutils/build.mjs', import.meta.url).href
)
await build()
console.log('Coreutils emitted to kernel/bin')
const kernelRoot = defaultKernelRoot(_pkg, import.meta.url)
const store = new Corestore(corestorePath())
@@ -0,0 +1,22 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const parts = argv.slice(1).filter((a) => a !== '--')
if (!parts.length) {
ctx.console.error('basename: missing operand')
return
}
const path = parts[0]
const suffix = parts[1] || ''
let base = path.replace(/\/+$/, '')
const slash = base.lastIndexOf('/')
base = slash === -1 ? base : base.slice(slash + 1)
if (!base) base = path
if (suffix && base.endsWith(suffix) && base.length > suffix.length) {
base = base.slice(0, -suffix.length)
}
ctx.console.log(base)
}
+22
View File
@@ -0,0 +1,22 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const vfs = ctx.vfs
const files = argv.slice(1)
if (!files.length) {
const s = bareStdin(ctx)
ctx.console.log(s)
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('cat: ' + f + ': No such file or directory')
continue
}
ctx.console.log(ctx.b4a.toString(buf))
}
}
@@ -0,0 +1,19 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const parts = argv.slice(1).filter((a) => a !== '--')
if (!parts.length) {
ctx.console.error('dirname: missing operand')
return
}
for (const path of parts) {
const cleaned = path.replace(/\/+$/, '') || '/'
const i = cleaned.lastIndexOf('/')
const out =
i <= 0 ? (cleaned[0] === '/' ? '/' : '.') : cleaned.slice(0, i) || '/'
ctx.console.log(out)
}
}
+19 -3
View File
@@ -1,4 +1,20 @@
// Drive path: /bin/echo — print arguments (kernel invokes run(argv))
async function run(ctx, argv) {
ctx.console.log(argv.slice(1).join(' '))
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const parts = argv.slice(1)
let n = false
if (parts[0] === '-n') {
n = true
parts.shift()
}
const s = parts.join(' ')
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, s + (n ? '' : '\n'))
} else {
ctx.console.log(n ? s : s)
}
}
+38
View File
@@ -0,0 +1,38 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let n = 10
let start = 1
if (argv[1] === '-n' && argv[2]) {
n = parseInt(argv[2], 10) || 10
start = 3
} else if (argv[1] && /^-\d+$/.test(argv[1])) {
n = parseInt(argv[1].slice(1), 10) || 10
start = 2
}
const files = argv.slice(start).filter((a) => a !== '--')
const take = (s) => {
const lines = s.split('\n')
const out = lines.slice(0, n).join('\n')
ctx.console.log(
out + (out && !out.endsWith('\n') && lines.length > n ? '\n' : '')
)
}
if (!files.length) {
take(bareStdin(ctx))
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('head: ' + f + ': No such file')
continue
}
if (files.length > 1) ctx.console.log('==> ' + f + ' <==')
take(ctx.b4a.toString(buf))
}
}
+9 -2
View File
@@ -1,3 +1,10 @@
async function run(ctx, _argv) {
ctx.console.log('Commands: help, echo <text>, exit')
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
ctx.console.log(
'Bare OS userland — builtins: cd, export, exit | /bin: ls pwd cat echo test basename dirname wc head tail uname pathchk help'
)
}
+52
View File
@@ -0,0 +1,52 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let showAll = false
let longFmt = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
if (c === 'a') showAll = true
else if (c === 'l') longFmt = true
else if (c === '1') longFmt = false
}
continue
}
paths.push(a)
}
const targets = paths.length ? paths : ['.']
for (const t of targets) {
if (targets.length > 1) ctx.console.log(t + ':')
let names
try {
names = await vfs.readdir(t)
} catch (e) {
ctx.console.error('ls: cannot access ' + t + ': ' + (e.message || e))
continue
}
if (!showAll) names = names.filter((n) => n !== '.' && n !== '..')
if (!longFmt) {
ctx.console.log(names.join(' '))
} else {
for (const n of names) {
const sub = t === '.' || t === './' ? n : t.replace(/\/$/, '') + '/' + n
const st = await vfs.stat(sub)
const tag = st ? (st.type === 'directory' ? 'd' : '-') : '?'
const sz = st && st.size != null ? String(st.size) : '0'
ctx.console.log(tag + 'rwxr-xr-x 1 user user ' + sz + ' ' + n)
}
}
}
}
@@ -0,0 +1,26 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const paths = argv.slice(1).filter((a) => !a.startsWith('-'))
if (!paths.length) {
ctx.console.error('pathchk: missing operand')
return
}
for (const p of paths) {
if (!p.length) {
ctx.console.error('pathchk: empty path name')
continue
}
if (p.length > 4096) {
ctx.console.error('pathchk: path too long')
continue
}
if (p.includes('\0')) {
ctx.console.error('pathchk: NUL in path')
continue
}
}
}
+8
View File
@@ -0,0 +1,8 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
ctx.console.log(ctx.vfs.getcwd())
}
+34
View File
@@ -0,0 +1,34 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const vfs = ctx.vfs
let n = 10
let start = 1
if (argv[1] === '-n' && argv[2]) {
n = parseInt(argv[2], 10) || 10
start = 3
}
const files = argv.slice(start).filter((a) => a !== '--')
const take = (s) => {
let lines = s.split('\n')
if (s.endsWith('\n') && lines[lines.length - 1] === '') lines.pop()
const slice = lines.length <= n ? lines : lines.slice(-n)
ctx.console.log(slice.join('\n'))
}
if (!files.length) {
take(bareStdin(ctx))
return
}
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('tail: ' + f + ': No such file')
continue
}
if (files.length > 1) ctx.console.log('==> ' + f + ' <==')
take(ctx.b4a.toString(buf))
}
}
+35
View File
@@ -0,0 +1,35 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function evalTest(ctx, args) {
if (!args.length) return false
if (args[0] === '!') {
const inner = await evalTest(ctx, args.slice(1))
return !inner
}
if (args.length === 3) {
const [a, op, b] = args
if (op === '=') return a === b
if (op === '!=') return a !== b
return false
}
if (args.length === 2) {
const op = args[0]
const p = args[1]
const st = await ctx.vfs.stat(p)
if (op === '-e' || op === '-a') return st != null
if (op === '-f') return st != null && st.type === 'file'
if (op === '-d') return st != null && st.type === 'directory'
if (op === '-z') return p.length === 0
if (op === '-n') return p.length > 0
return false
}
if (args.length === 1) return args[0] !== ''
return false
}
async function run(ctx, argv) {
ctx.exitCode = (await evalTest(ctx, argv.slice(1))) ? 0 : 1
}
+37
View File
@@ -0,0 +1,37 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
async function run(ctx, argv) {
const flagArgs = argv.slice(1).filter((a) => a.startsWith('-') && a !== '--')
const all = argv.includes('-a')
const noFlag = flagArgs.length === 0
const wantS = all || argv.includes('-s') || noFlag
const wantN = all || argv.includes('-n')
const wantR = all || argv.includes('-r')
const wantM = all || argv.includes('-m')
const wantV = all || argv.includes('-v')
let name = 'BareOS'
let version = '0.1'
const buf = await ctx.vfs.readFile('/etc/os-release')
if (buf) {
const t = ctx.b4a.toString(buf)
for (const line of t.split('\n')) {
const id = line.match(/^NAME=(.*)$/)
if (id) name = id[1].replace(/^"|"$/g, '')
const ver = line.match(/^VERSION=(.*)$/)
if (ver) version = ver[1].replace(/^"|"$/g, '')
}
}
const parts = []
if (wantS) parts.push(name)
if (wantN) parts.push('bare-os')
if (wantR) parts.push(version)
if (wantV) parts.push('bare-userland')
if (wantM) parts.push('unknown')
ctx.console.log(parts.join(' '))
}
+41
View File
@@ -0,0 +1,41 @@
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
function count(s) {
const lines = (s.match(/\n/g) || []).length
const words = s.trim() ? s.trim().split(/\s+/).length : 0
const bytes = new TextEncoder().encode(s).length
return { lines, words, bytes }
}
async function run(ctx, argv) {
const vfs = ctx.vfs
const files = argv.slice(1).filter((a) => !a.startsWith('-'))
if (!files.length) {
const s = bareStdin(ctx)
const c = count(s)
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes)
return
}
let tLines = 0
let tWords = 0
let tBytes = 0
for (const f of files) {
const buf = await vfs.readFile(f)
if (!buf) {
ctx.console.error('wc: ' + f + ': No such file')
continue
}
const s = ctx.b4a.toString(buf)
const c = count(s)
tLines += c.lines
tWords += c.words
tBytes += c.bytes
ctx.console.log(' ' + c.lines + ' ' + c.words + ' ' + c.bytes + ' ' + f)
}
if (files.length > 1) {
ctx.console.log(' ' + tLines + ' ' + tWords + ' ' + tBytes + ' total')
}
}
+6 -6
View File
@@ -6,15 +6,15 @@ async function start(ctx) {
const { console, drive, readLine, execLine, b4a } = ctx
const rel = await drive.get('/etc/os-release')
if (rel) console.log(b4a.toString(rel))
console.log('Bare operating system — commands: help, echo, exit')
console.log(
'Bare operating system — POSIX-ish shell: cd, export, exit | try: help, ls /bin, pwd'
)
while (true) {
const line = await readLine('bare-os> ')
if (line == null) break
const t = line.trim()
if (t === '' || t === 'exit') {
if (t === 'exit') break
continue
}
await execLine(t)
if (t === '') continue
const status = await execLine(t)
if (status === 'exit') break
}
}