@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 : ''
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(ctx.vfs.getcwd())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(' '))
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(' '))
|
||||
}
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user