Updates
This commit is contained in:
@@ -27,7 +27,7 @@ HTTP in Pear/Bare uses **WHATWG Fetch** (Node `fetch` or **`bare-fetch`** on `ba
|
||||
| `--connect-timeout`, `-Y`, `-y` | out of scope | Fetch exposes one abort timer |
|
||||
| TLS client certs, `--cacert`, `-k` | out of scope | Stack / trust store |
|
||||
| HTTP/2, HTTP/3, SOCKS, FTP, SCP | out of scope | |
|
||||
| `-J` content-disposition filename | stub / future | Not implemented |
|
||||
| `-J` content-disposition filename | supported | With **`-O`**; uses **`Content-Disposition`** basename only; rejects **`..`** and absolute paths in the server-provided name |
|
||||
|
||||
## wget ([`lib/wget-cli.js`](lib/wget-cli.js))
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
import path from 'path'
|
||||
import {
|
||||
DEFAULT_CURL_USER_AGENT,
|
||||
defaultFetchSaveName,
|
||||
@@ -45,6 +46,49 @@ function basicAuthHeader(user, pass) {
|
||||
throw new Error('curl: cannot encode Basic auth (no btoa/Buffer)')
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 2183 / 5987 style Content-Disposition filename (best-effort).
|
||||
* @param {string | null} headerVal
|
||||
* @returns {string | null} basename-safe segment or null
|
||||
*/
|
||||
export function filenameFromContentDisposition(headerVal) {
|
||||
if (!headerVal || typeof headerVal !== 'string') return null
|
||||
const h = headerVal.trim()
|
||||
const star = h.match(/filename\*\s*=\s*(?:UTF-8''|utf-8'')?([^;\s]+)/i)
|
||||
if (star) {
|
||||
try {
|
||||
const raw = star[1].replace(/^["']|["']$/g, '')
|
||||
const decoded = decodeURIComponent(raw)
|
||||
const base = decoded.split(/[/\\]/).pop()
|
||||
return base || null
|
||||
} catch {
|
||||
const base = star[1].split(/[/\\]/).pop()
|
||||
return base || null
|
||||
}
|
||||
}
|
||||
const q = h.match(/filename\s*=\s*"((?:\\.|[^"\\])*)"/i)
|
||||
if (q) {
|
||||
const inner = q[1].replace(/\\(.)/g, '$1')
|
||||
const base = inner.split(/[/\\]/).pop()
|
||||
return base || null
|
||||
}
|
||||
const plain = h.match(/filename\s*=\s*([^;\s]+)/i)
|
||||
if (plain) {
|
||||
const base = plain[1].replace(/^["']|["']$/g, '').split(/[/\\]/).pop()
|
||||
return base || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Reject absolute paths and `..` segments from server-provided names (-J). */
|
||||
function safeRemoteFilename(name) {
|
||||
if (!name || typeof name !== 'string') return null
|
||||
const t = name.trim()
|
||||
if (!t || t.includes('..') || path.posix.isAbsolute(t)) return null
|
||||
const base = path.posix.basename(t)
|
||||
return base || null
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return (
|
||||
'usage: curl [options] URL...\n' +
|
||||
@@ -170,6 +214,8 @@ export async function runCurlCli(ctx, argv) {
|
||||
/** @type {string | null} */
|
||||
let outputPath = null
|
||||
let remoteName = false
|
||||
/** Use Content-Disposition filename with -O (curl -J); server-controlled path — only save under trusted cwd. */
|
||||
let remoteHeaderName = false
|
||||
/** @type {string | null} */
|
||||
let writeOut = null
|
||||
let maxTimeMs = 0
|
||||
@@ -327,6 +373,12 @@ export async function runCurlCli(ctx, argv) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (a === '-J' || a === '--remote-header-name') {
|
||||
remoteHeaderName = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (a === '-T' || a === '--upload-file') {
|
||||
if (i + 1 >= args.length) {
|
||||
ctx.console.error('curl: option requires an argument: ' + a)
|
||||
@@ -447,6 +499,9 @@ export async function runCurlCli(ctx, argv) {
|
||||
case 'O':
|
||||
remoteName = true
|
||||
break
|
||||
case 'J':
|
||||
remoteHeaderName = true
|
||||
break
|
||||
default:
|
||||
ctx.console.error('curl: invalid option -- ' + c)
|
||||
ctx.exitCode = 2
|
||||
@@ -480,6 +535,14 @@ export async function runCurlCli(ctx, argv) {
|
||||
return
|
||||
}
|
||||
|
||||
if (remoteHeaderName && !remoteName) {
|
||||
ctx.console.error(
|
||||
'curl: -J / --remote-header-name requires -O / --remote-name'
|
||||
)
|
||||
ctx.exitCode = 2
|
||||
return
|
||||
}
|
||||
|
||||
let m = method
|
||||
if (!m) {
|
||||
if (headOnly) m = 'HEAD'
|
||||
@@ -608,6 +671,14 @@ export async function runCurlCli(ctx, argv) {
|
||||
|
||||
lastUrl = res.url || url
|
||||
|
||||
if (remoteName && remoteHeaderName && res.ok && m !== 'HEAD') {
|
||||
const raw = filenameFromContentDisposition(
|
||||
res.headers.get('Content-Disposition')
|
||||
)
|
||||
const safe = raw ? safeRemoteFilename(raw) : null
|
||||
if (safe) outForUrl = safe
|
||||
}
|
||||
|
||||
if (!location && res.status >= 300 && res.status < 400) {
|
||||
const loc = res.headers.get('Location')
|
||||
if (loc && (m === 'GET' || m === 'HEAD')) {
|
||||
|
||||
@@ -101,12 +101,27 @@ Commands:
|
||||
stash (push|pop|list|apply|drop)
|
||||
reset [--hard] [commit]
|
||||
cherry-pick <commit>
|
||||
clean -fd Remove untracked (not implemented)
|
||||
clean -fd Remove untracked files (-f required); -d prunes empty dirs
|
||||
hash-object|rev-parse|ls-files|diff (limited)
|
||||
|
||||
See https://isomorphic-git.org for API behavior.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* True if argv contains `-x` or a bundled short-flag group like `-fd` (git-style).
|
||||
* @param {string[]} argv
|
||||
* @param {string} letter
|
||||
*/
|
||||
function argvHasShortFlag(argv, letter) {
|
||||
for (const a of argv) {
|
||||
if (a === '-' + letter) return true
|
||||
if (a.startsWith('-') && !a.startsWith('--') && a.length > 1) {
|
||||
if (a.slice(1).includes(letter)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} args
|
||||
* @returns {{ rest: string[], dir: string, gitdir?: string }}
|
||||
@@ -474,7 +489,70 @@ export async function runGitCli(ctx, argv) {
|
||||
}
|
||||
|
||||
if (sub === 'clean') {
|
||||
ctx.console.error('git clean: not implemented; remove files via shell rm')
|
||||
const force =
|
||||
tail.includes('-f') ||
|
||||
tail.includes('--force') ||
|
||||
argvHasShortFlag(tail, 'f')
|
||||
const dirs = tail.includes('-d') || argvHasShortFlag(tail, 'd')
|
||||
if (!force) {
|
||||
ctx.console.error(
|
||||
'git clean: refusing without -f (would remove untracked files)'
|
||||
)
|
||||
return
|
||||
}
|
||||
const matrix = await git.statusMatrix({ ...baseOpts() })
|
||||
/** Untracked `??` per isomorphic-git statusMatrix (HEAD absent, WORKDIR present, stage absent). */
|
||||
const untracked = matrix.filter(
|
||||
([_fp, head, workdir, stage]) =>
|
||||
head === 0 && workdir === 2 && stage === 0
|
||||
)
|
||||
if (untracked.length === 0) {
|
||||
ctx.console.log('Nothing to clean')
|
||||
return
|
||||
}
|
||||
const rels = untracked.map(([fp]) => fp).sort((a, b) => b.length - a.length)
|
||||
/** @type {Set<string>} */
|
||||
const parents = new Set()
|
||||
for (const rel of rels) {
|
||||
const full = path.posix.join(dir, rel)
|
||||
try {
|
||||
const st = await fs.promises.stat(full)
|
||||
if (st.isDirectory()) {
|
||||
if (dirs) {
|
||||
await fs.promises.rm(full, { recursive: true, force: true })
|
||||
let parent = path.posix.dirname(rel)
|
||||
while (parent && parent !== '.' && parent !== '/') {
|
||||
parents.add(parent)
|
||||
parent = path.posix.dirname(parent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await fs.promises.unlink(full)
|
||||
let parent = path.posix.dirname(rel)
|
||||
while (parent && parent !== '.' && parent !== '/') {
|
||||
parents.add(parent)
|
||||
parent = path.posix.dirname(parent)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.console.error('git clean: ' + rel + ': ' + (e?.message || e))
|
||||
}
|
||||
}
|
||||
if (dirs && parents.size) {
|
||||
const ordered = [...parents].sort(
|
||||
(a, b) => b.split('/').length - a.split('/').length
|
||||
)
|
||||
for (const p of ordered) {
|
||||
const full = path.posix.join(dir, p)
|
||||
try {
|
||||
const names = await fs.promises.readdir(full)
|
||||
if (names && names.length === 0) await fs.promises.rmdir(full)
|
||||
} catch {
|
||||
/* not empty or gone */
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.console.log(`Removed ${rels.length} untracked path(s)`)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -830,6 +830,176 @@ async function execParsedPipeline(ctx, pipeline) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* Split on `;` only at depth 0 (`if` / `fi` nesting).
|
||||
* @param {Token[]} tokens
|
||||
* @returns {Token[][]}
|
||||
*/
|
||||
function splitTopLevelStatements(tokens) {
|
||||
/** @type {Token[][]} */
|
||||
const out = []
|
||||
/** @type {Token[]} */
|
||||
let cur = []
|
||||
let depth = 0
|
||||
for (const t of tokens) {
|
||||
if (t.type === 'word') {
|
||||
if (t.value === 'if') depth++
|
||||
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
|
||||
}
|
||||
if (t.type === 'op' && t.value === ';' && depth === 0) {
|
||||
if (cur.length) out.push(cur)
|
||||
cur = []
|
||||
} else {
|
||||
cur.push(t)
|
||||
}
|
||||
}
|
||||
if (cur.length) out.push(cur)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {number} start index after `if`
|
||||
*/
|
||||
function findThenIndex(tokens, start) {
|
||||
let d = 1
|
||||
for (let j = start; j < tokens.length; j++) {
|
||||
const t = tokens[j]
|
||||
if (t.type !== 'word') continue
|
||||
if (t.value === 'if') d++
|
||||
else if (t.value === 'fi') d--
|
||||
else if (t.value === 'then' && d === 1) return j
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* After `then`, find `else` at depth 1 or closing `fi` at depth 0.
|
||||
* @param {Token[]} tokens
|
||||
* @param {number} start index after `then` token
|
||||
* @returns {{ kind: 'else' | 'fi', idx: number } | null}
|
||||
*/
|
||||
function findElseOrFiAfterThen(tokens, start) {
|
||||
let d = 1
|
||||
for (let j = start; j < tokens.length; j++) {
|
||||
const t = tokens[j]
|
||||
if (t.type !== 'word') continue
|
||||
if (t.value === 'if') d++
|
||||
else if (t.value === 'fi') {
|
||||
d--
|
||||
if (d === 0) return { kind: 'fi', idx: j }
|
||||
} else if (t.value === 'else' && d === 1) return { kind: 'else', idx: j }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @param {number} start index after `else`
|
||||
*/
|
||||
function findFiAfterElse(tokens, start) {
|
||||
let d = 1
|
||||
for (let j = start; j < tokens.length; j++) {
|
||||
const t = tokens[j]
|
||||
if (t.type !== 'word') continue
|
||||
if (t.value === 'if') d++
|
||||
else if (t.value === 'fi') {
|
||||
d--
|
||||
if (d === 0) return j
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* `if` COMPOUND `then` COMPOUND [ `else` COMPOUND ] `fi`
|
||||
* Condition / branch bodies use the same `&&` / `||` / `|` rules as a normal line.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} tokens
|
||||
* @returns {Promise<'exit' | 'ok'>}
|
||||
*/
|
||||
/**
|
||||
* Run `;`-separated lists (same as outside `if`); last command sets exit status.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} toks
|
||||
*/
|
||||
async function execSemicolonLists(ctx, toks) {
|
||||
const lists = splitTokensBySemicolon(toks)
|
||||
for (const list of lists) {
|
||||
if (!list.length) continue
|
||||
const r = await execAndOrList(ctx, list)
|
||||
if (r === 'exit') return 'exit'
|
||||
}
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
async function execIfConstruct(ctx, tokens) {
|
||||
const thenIdx = findThenIndex(tokens, 1)
|
||||
if (thenIdx < 0) {
|
||||
ctx.console.error('shell: syntax error: if without matching then')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
const cond = tokens.slice(1, thenIdx)
|
||||
const tail = findElseOrFiAfterThen(tokens, thenIdx + 1)
|
||||
if (!tail) {
|
||||
ctx.console.error('shell: syntax error: if without fi')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
let closingFiIdx
|
||||
if (tail.kind === 'fi') {
|
||||
closingFiIdx = tail.idx
|
||||
} else {
|
||||
closingFiIdx = findFiAfterElse(tokens, tail.idx + 1)
|
||||
if (closingFiIdx < 0) {
|
||||
ctx.console.error('shell: syntax error: else without fi')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
}
|
||||
if (closingFiIdx !== tokens.length - 1) {
|
||||
ctx.console.error('shell: syntax error: unexpected tokens after fi')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
const condLists = splitTokensBySemicolon(cond).filter((c) => c.length)
|
||||
if (!condLists.length) {
|
||||
ctx.console.error('shell: invalid null command')
|
||||
ctx.exitCode = 2
|
||||
return 'ok'
|
||||
}
|
||||
for (const c of condLists) {
|
||||
const r = await execAndOrList(ctx, c)
|
||||
if (r === 'exit') return 'exit'
|
||||
}
|
||||
const condOk = (Number(ctx.exitCode) || 0) === 0
|
||||
|
||||
if (tail.kind === 'fi') {
|
||||
const thenToks = tokens.slice(thenIdx + 1, tail.idx)
|
||||
if (condOk) {
|
||||
const r = await execSemicolonLists(ctx, thenToks)
|
||||
if (r === 'exit') return 'exit'
|
||||
} else {
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
const elseIdx = tail.idx
|
||||
const thenToks = tokens.slice(thenIdx + 1, elseIdx)
|
||||
const elseToks = tokens.slice(elseIdx + 1, closingFiIdx)
|
||||
if (condOk) {
|
||||
const r = await execSemicolonLists(ctx, thenToks)
|
||||
if (r === 'exit') return 'exit'
|
||||
} else {
|
||||
const r = await execSemicolonLists(ctx, elseToks)
|
||||
if (r === 'exit') return 'exit'
|
||||
}
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Token[]} tokens
|
||||
@@ -880,10 +1050,13 @@ export async function execShellLine(ctx, line) {
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
const lists = splitTokensBySemicolon(tokens)
|
||||
for (const listTok of lists) {
|
||||
if (!listTok.length) continue
|
||||
const r = await execAndOrList(ctx, listTok)
|
||||
const statements = splitTopLevelStatements(tokens)
|
||||
for (const stmt of statements) {
|
||||
if (!stmt.length) continue
|
||||
const r =
|
||||
stmt[0]?.type === 'word' && stmt[0].value === 'if'
|
||||
? await execIfConstruct(ctx, stmt)
|
||||
: await execAndOrList(ctx, stmt)
|
||||
if (r === 'exit') {
|
||||
syncBareOsExitStatusEnv(ctx)
|
||||
return 'exit'
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
DEFAULT_CURL_USER_AGENT,
|
||||
DEFAULT_WGET_USER_AGENT
|
||||
} from './lib/http-fetch-url.js'
|
||||
import { filenameFromContentDisposition } from './lib/curl-cli.js'
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function headerFromInit(init, name) {
|
||||
@@ -808,6 +809,67 @@ async function run(ctx, argv) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('execShellLine if then else fi', async (t) => {
|
||||
const dir = testCorestoreDir('shif')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('sif'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put(
|
||||
'/bin/echo',
|
||||
b4a.from(`
|
||||
async function run(ctx, argv) {
|
||||
ctx.console.log(argv.slice(1).join(' '))
|
||||
}
|
||||
`)
|
||||
)
|
||||
await drive.put(
|
||||
'/bin/true',
|
||||
b4a.from(`
|
||||
async function run(ctx) {
|
||||
ctx.exitCode = 0
|
||||
}
|
||||
`)
|
||||
)
|
||||
await drive.put(
|
||||
'/bin/false',
|
||||
b4a.from(`
|
||||
async function run(ctx) {
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
`)
|
||||
)
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
log: (s) => lines.push(String(s)),
|
||||
error: (...a) => lines.push(a.join(' '))
|
||||
}
|
||||
await execShellLine(ctx, 'if false; then echo no; else echo yes; fi')
|
||||
t.ok(lines.some((l) => l.includes('yes')))
|
||||
lines.length = 0
|
||||
await execShellLine(ctx, 'if true; then echo ok; fi')
|
||||
t.ok(lines.some((l) => l.includes('ok')))
|
||||
lines.length = 0
|
||||
await execShellLine(ctx, 'if false; then echo x; fi')
|
||||
t.ok(!lines.some((l) => l.includes('x')))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('filenameFromContentDisposition parses attachment names', async (t) => {
|
||||
t.is(
|
||||
filenameFromContentDisposition('attachment; filename="a b.txt"'),
|
||||
'a b.txt'
|
||||
)
|
||||
t.is(
|
||||
filenameFromContentDisposition("attachment; filename*=UTF-8''x%20y.bin"),
|
||||
'x y.bin'
|
||||
)
|
||||
t.is(filenameFromContentDisposition(null), null)
|
||||
})
|
||||
|
||||
test('cron fieldMatches and dowFieldMatches', async (t) => {
|
||||
t.ok(fieldMatches('*', 0, 0, 59))
|
||||
t.ok(fieldMatches('*/5', 10, 0, 59))
|
||||
@@ -1592,6 +1654,58 @@ test('runGitCli init and status on personal drive', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('git clean -fd removes untracked files only', async (t) => {
|
||||
const dir = testCorestoreDir('gitclean')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pgcl'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = { log() {}, error() {} }
|
||||
await runGitCli(ctx, ['git', 'init', '-C', '/home/user/clrepo'])
|
||||
await ctx.vfs.writeFile('/home/user/clrepo/a.txt', b4a.from('a'))
|
||||
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'add', 'a.txt'])
|
||||
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'commit', '-m', 'init'])
|
||||
await ctx.vfs.writeFile('/home/user/clrepo/junk.txt', b4a.from('j'))
|
||||
await runGitCli(ctx, ['git', '-C', '/home/user/clrepo', 'clean', '-fd'])
|
||||
t.is(await ctx.vfs.readFile('/home/user/clrepo/junk.txt'), null)
|
||||
t.ok(await ctx.vfs.readFile('/home/user/clrepo/a.txt'))
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('curl -O -J saves Content-Disposition filename', async (t) => {
|
||||
const dir = testCorestoreDir('curlj')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pcj'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.exitCode = 0
|
||||
ctx.httpFetch = async () =>
|
||||
new Response(b4a.from('payload'), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Disposition': 'attachment; filename="from-server.bin"'
|
||||
}
|
||||
})
|
||||
await runBinCommand(ctx, [
|
||||
'curl',
|
||||
'-s',
|
||||
'-O',
|
||||
'-J',
|
||||
'https://stub.example/blob'
|
||||
])
|
||||
t.is(ctx.exitCode, 0)
|
||||
const out = await ctx.vfs.readFile('/home/user/from-server.bin')
|
||||
t.ok(out)
|
||||
t.is(ctx.b4a.toString(out), 'payload')
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('kernel share/man/man.json page count matches coreutils + extras + handbook + devguide', async (t) => {
|
||||
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
|
||||
const raw = JSON.parse(await readFile(manPath, 'utf8'))
|
||||
|
||||
@@ -46,6 +46,18 @@ export function setupSeedChannel(mux, localRAM, replicateDrive) {
|
||||
chan.addMessage({
|
||||
encoding: msgRpcReqEncoding,
|
||||
onmessage(m) {
|
||||
if (m.module === 'bare_os' && m.method === 'version') {
|
||||
chan.messages[6].send({
|
||||
id: m.id,
|
||||
success: true,
|
||||
result: JSON.stringify({
|
||||
protocol: PROTOCOL_NAME,
|
||||
seederRpc: 'bare_os.version'
|
||||
}),
|
||||
error: ''
|
||||
})
|
||||
return
|
||||
}
|
||||
chan.messages[6].send({
|
||||
id: m.id,
|
||||
success: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Optional boot-time shell lines (one command per non-comment line).
|
||||
# Executed by /boot/init.js after /etc/os-release and this motd, before the main banner.
|
||||
# Executed by /boot/init.js after /etc/os-release and /etc/motd, then /etc/bare-os/rc.d/*, before the main banner.
|
||||
# Example (uncomment to use):
|
||||
# export BARE_OS_SHOW_RC=1
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Optional boot snippets (trusted)
|
||||
|
||||
Files in this directory are executed after `/etc/bare-os/rc`, in **lexicographic
|
||||
order** by filename. Use numeric prefixes (e.g. `10-local`, `20-proxy`) to
|
||||
control order. Each file is treated like `rc`: one shell command per non-empty,
|
||||
non-comment line.
|
||||
|
||||
Lines starting with `#` and blank lines are ignored. Dotfiles and names ending
|
||||
in `~` are skipped.
|
||||
|
||||
Only ship snippets you trust — they run with full `execLine` power.
|
||||
@@ -1,31 +1,96 @@
|
||||
/**
|
||||
* Hyperdrive-resident kernel (staged as /boot/init.js).
|
||||
* Loaded by the booter with an injected ctx object (trusted replication source).
|
||||
*
|
||||
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
|
||||
* (sorted by filename) → session banner → interactive loop.
|
||||
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Print /etc/motd and run /etc/bare-os/rc lines (comments and blank lines skipped).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} text
|
||||
*/
|
||||
async function runRcLines(ctx, text) {
|
||||
const { execLine, console } = ctx
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) continue
|
||||
try {
|
||||
await execLine(t)
|
||||
} catch (e) {
|
||||
console.error((e && e.message) || String(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function runEtcSnippets(ctx) {
|
||||
const { drive, execLine, b4a, console } = ctx
|
||||
async function printOsRelease(ctx) {
|
||||
const { drive, b4a, console } = ctx
|
||||
try {
|
||||
const rel = await drive.get('/etc/os-release')
|
||||
if (rel) console.log(b4a.toString(rel))
|
||||
} catch (e) {
|
||||
console.error((e && e.message) || String(e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function printMotd(ctx) {
|
||||
const { drive, b4a, console } = ctx
|
||||
try {
|
||||
const motd = await drive.get('/etc/motd')
|
||||
if (motd) console.log(b4a.toString(motd).trimEnd())
|
||||
} catch (e) {
|
||||
console.error((e && e.message) || String(e))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} drivePath absolute path on system drive
|
||||
* @param {string} label for errors
|
||||
*/
|
||||
async function runRcFileAt(ctx, drivePath, label) {
|
||||
const { drive, b4a, console } = ctx
|
||||
try {
|
||||
const rc = await drive.get('/etc/bare-os/rc')
|
||||
if (!rc) return
|
||||
const text = b4a.toString(rc)
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) continue
|
||||
const buf = await drive.get(drivePath)
|
||||
if (!buf) return
|
||||
await runRcLines(ctx, b4a.toString(buf))
|
||||
} catch (e) {
|
||||
console.error(`${label}: ` + ((e && e.message) || String(e)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional snippets under /etc/bare-os/rc.d/ — executed in lexicographic order.
|
||||
* Skips dotfiles and names ending in ~.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function runBareOsRcDir(ctx) {
|
||||
const { drive, b4a, console } = ctx
|
||||
try {
|
||||
/** @type {string[]} */
|
||||
const names = []
|
||||
try {
|
||||
for await (const n of drive.readdir('/etc/bare-os/rc.d')) names.push(n)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
names.sort()
|
||||
for (const name of names) {
|
||||
if (!name || name.startsWith('.') || name.endsWith('~')) continue
|
||||
const p = `/etc/bare-os/rc.d/${name}`
|
||||
try {
|
||||
await execLine(t)
|
||||
const buf = await drive.get(p)
|
||||
if (!buf) continue
|
||||
await runRcLines(ctx, b4a.toString(buf))
|
||||
} catch (e) {
|
||||
console.error((e && e.message) || String(e))
|
||||
console.error(`rc.d/${name}: ` + ((e && e.message) || String(e)))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -33,14 +98,22 @@ async function runEtcSnippets(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
await runEtcSnippets(ctx)
|
||||
console.log(
|
||||
'Bare operating system — session: guest (login [--new] <passphrase> to unlock identity) | shell: cd, export, && || ;, exit, login, logout | try: help, getconf PATH_MAX, ls /bin, pwd, crontab -l'
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function printSessionBanner(ctx) {
|
||||
ctx.console.log(
|
||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock) | shell: cd, export, if/fi, && || ;, |, exit | services: systemctl list-units, journalctl -u UNIT | try: help, ls /bin, crontab -l'
|
||||
)
|
||||
}
|
||||
|
||||
async function start(ctx) {
|
||||
const { readLine, execLine, console } = ctx
|
||||
await printOsRelease(ctx)
|
||||
await printMotd(ctx)
|
||||
await runRcFileAt(ctx, '/etc/bare-os/rc', 'rc')
|
||||
await runBareOsRcDir(ctx)
|
||||
printSessionBanner(ctx)
|
||||
while (true) {
|
||||
const line = await readLine('')
|
||||
if (line == null) break
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user