This commit is contained in:
Raven Scott
2026-04-03 04:01:24 -04:00
parent 8505077f8d
commit 98259a7b4a
23 changed files with 2371 additions and 66 deletions
+3 -1
View File
@@ -304,7 +304,9 @@ async function executeKernel(disk, store, swarm, initSource) {
},
async saveVault() {
await saveVaultToDrive(this)
}
},
/** Populated by `loadBarerc` / first `execShellLine` (default alias table). */
shellAliases: undefined
}
await applyGuestEnv(ctx)
+524
View File
@@ -0,0 +1,524 @@
/**
* git dispatcher backed by isomorphic-git (JavaScript implementation, not GNU git).
* Requires Node-style fs from createGitFsFromVfs(ctx.vfs).
*/
import path from 'path'
import * as git from 'isomorphic-git'
import { createGitFsFromVfs } from './git-fs-adapter.js'
/** @type {import('isomorphic-git').HttpClient | null} */
let httpNode = null
/**
* isomorphic-git/http/web calls bare `fetch`. Node 18+ and browsers have it;
* Pear/Bare does not unless we install holepunch's fetch (bare-http1/https).
*/
async function ensureFetchForIsomorphicGit() {
if (typeof globalThis.fetch === 'function') return
try {
const m = await import('bare-fetch')
const f = m.default
if (typeof f !== 'function') return
globalThis.fetch = f
if (f.Request) globalThis.Request = f.Request
if (f.Response) globalThis.Response = f.Response
if (f.Headers) globalThis.Headers = f.Headers
if (typeof global !== 'undefined') {
global.fetch = f
if (f.Request) global.Request = f.Request
if (f.Response) global.Response = f.Response
if (f.Headers) global.Headers = f.Headers
}
} catch {
/* bare-fetch is Bare-only; Node tests already have native fetch */
}
}
async function getHttp() {
if (httpNode) return httpNode
const mode = globalThis.process?.env?.BARE_OS_GIT_HTTP
if (mode === 'web') {
await ensureFetchForIsomorphicGit()
const m = await import('isomorphic-git/http/web')
httpNode = m.default
return httpNode
}
try {
const m = await import('isomorphic-git/http/node')
httpNode = m.default
return httpNode
} catch {
await ensureFetchForIsomorphicGit()
if (typeof globalThis.fetch !== 'function') {
throw new Error(
'git over HTTP needs fetch (Node http client failed and global fetch is missing). On Pear/Bare, bare-fetch should be installed.'
)
}
const m = await import('isomorphic-git/http/web')
httpNode = m.default
return httpNode
}
}
/**
* @param {Record<string, unknown>} ctx
*/
function gitAuthor(ctx) {
const env = /** @type {Record<string, string>} */ (ctx.env || {})
return {
name: env.GIT_AUTHOR_NAME || env.USER || 'bare-os',
email: env.GIT_AUTHOR_EMAIL || 'nobody@localhost',
timestamp: Math.floor(Date.now() / 1000),
timezoneOffset: new Date().getTimezoneOffset()
}
}
function printHelp(console) {
console.log(`bare-os git (isomorphic-git) — not full GNU git.
Usage: git [--git-dir <path>] [-C <path>] <command> [args]
Commands:
version, --version Show isomorphic-git version
init [--bare] Create a repository
clone <url> [dir] Clone (needs network)
status Working tree status
add <pathspec...> Stage files (use "." for all)
rm <pathspec...> Remove from index and working tree
commit [-m msg] Create commit
log [--oneline] [n] History
branch [-a] [name] List or create branch
checkout <ref> Switch branch or restore files
fetch [remote] Fetch from remote
pull [remote] Fetch + merge
push [remote] [ref] Push to remote
remote [-v] | add | rm | rename
tag [list | -a name -m msg]
merge [--ff-only] <branch>
config [--get] key [value]
show [ref] Show commit or object
stash (push|pop|list|apply|drop)
reset [--hard] [commit]
cherry-pick <commit>
clean -fd Remove untracked (not implemented)
hash-object|rev-parse|ls-files|diff (limited)
See https://isomorphic-git.org for API behavior.`)
}
/**
* @param {string[]} args
* @returns {{ rest: string[], dir: string, gitdir?: string }}
*/
function parseGlobalArgs(args) {
let dir = null
let gitdir
const rest = []
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '-C' && args[i + 1]) {
dir = args[++i]
continue
}
if (a.startsWith('-C') && a.length > 2) {
dir = a.slice(2)
continue
}
if (a === '--git-dir' && args[i + 1]) {
gitdir = args[++i]
continue
}
rest.push(a)
}
return { rest, dir: dir || '', gitdir }
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runGitCli(ctx, argv) {
const vfs = ctx.vfs
if (!vfs) {
ctx.console.error('git: no vfs')
return
}
const fs = createGitFsFromVfs(vfs)
const args = argv.slice(1)
if (args.length === 0) {
printHelp(ctx.console)
return
}
const { rest, dir: cDir, gitdir: optGitdir } = parseGlobalArgs(args)
const baseDir = cDir ? vfs.resolveLogical(cDir) : vfs.getcwd()
const dir = baseDir
const baseOpts = () => ({
fs,
dir,
...(optGitdir ? { gitdir: optGitdir } : {})
})
const sub = rest[0]
const tail = rest.slice(1)
try {
if (sub === '--version' || sub === '-v' || sub === 'version') {
const v = await git.version()
ctx.console.log(`isomorphic-git ${v}`)
return
}
if (sub === 'help' || sub === '--help' || sub === '-h') {
if (tail[0]) {
ctx.console.log(`git help ${tail[0]}: see isomorphic-git docs`)
} else {
printHelp(ctx.console)
}
return
}
if (sub === 'init') {
const bare = tail.includes('--bare')
await git.init({ ...baseOpts(), bare, defaultBranch: 'main' })
ctx.console.log(
bare ? 'Initialized empty Git repository' : 'Initialized empty Git repository in ' + dir
)
return
}
if (sub === 'clone') {
const http = await getHttp()
const url = tail[0]
if (!url) {
ctx.console.error('git clone: missing url')
return
}
let outDir = tail[1] ? vfs.resolveLogical(tail[1]) : null
if (!outDir) {
try {
const u = new URL(url.replace(/\.git\/?$/, ''))
const seg = u.pathname.split('/').filter(Boolean).pop() || 'repo'
outDir = path.posix.join(dir, seg.replace(/\.git$/, ''))
} catch {
const m = url.match(/[/:]([^/]+?)(?:\.git)?\/?$/)
const seg = (m && m[1]) || 'repo'
outDir = path.posix.join(dir, seg.replace(/\.git$/, ''))
}
}
await git.clone({
fs,
http,
dir: outDir,
url,
singleBranch: true,
depth: 1
})
ctx.console.log('Cloned into ' + outDir)
return
}
if (sub === 'status') {
const matrix = await git.statusMatrix({ ...baseOpts() })
if (matrix.length === 0) {
ctx.console.log('nothing to show (empty repository)')
return
}
for (const row of matrix) {
const [filepath, head, workdir, stage] = row
const h = head === 1 ? 'H' : ' '
const w = workdir === 2 ? 'M' : workdir === 1 ? ' ' : '?'
const s = stage === 2 ? 'S' : stage === 1 ? ' ' : '?'
ctx.console.log(`${h}${w}${s} ${filepath}`)
}
return
}
if (sub === 'add') {
const paths = tail.filter((t) => !t.startsWith('-'))
if (paths.length === 0) paths.push('.')
await git.add({ ...baseOpts(), filepath: paths.length === 1 ? paths[0] : paths })
return
}
if (sub === 'rm') {
const paths = tail.filter((t) => !t.startsWith('-'))
for (const p of paths) {
await git.remove({ ...baseOpts(), filepath: p })
}
return
}
if (sub === 'commit') {
let message = ''
const t = [...tail]
for (let i = 0; i < t.length; i++) {
if (t[i] === '-m' && t[i + 1]) {
message = t[++i]
} else if (t[i].startsWith('-m') && t[i].length > 2) {
message = t[i].slice(2)
}
}
if (!message) message = 'commit'
const sha = await git.commit({
...baseOpts(),
message,
author: gitAuthor(ctx)
})
ctx.console.log(`[${dir}] ${sha}`)
return
}
if (sub === 'log') {
const oneline = tail.includes('--oneline')
let depth
const num = tail.find((x) => /^\d+$/.test(x))
if (num) depth = Number.parseInt(num, 10)
const commits = await git.log({ ...baseOpts(), depth: depth || undefined })
for (const c of commits) {
if (oneline) ctx.console.log(`${c.oid.slice(0, 7)} ${c.commit.message.split('\n')[0]}`)
else {
ctx.console.log(`commit ${c.oid}`)
ctx.console.log(c.commit.message)
}
}
return
}
if (sub === 'branch') {
if (tail.includes('-a') || tail.includes('--all')) {
const local = await git.listBranches({ ...baseOpts() })
for (const b of local) ctx.console.log(b)
return
}
if (tail[0] && !tail[0].startsWith('-')) {
await git.branch({ ...baseOpts(), ref: tail[0], checkout: false })
ctx.console.log('Branch ' + tail[0])
return
}
const branches = await git.listBranches({ ...baseOpts() })
const cur = await git.currentBranch({ ...baseOpts() })
for (const b of branches) {
ctx.console.log((b === cur ? '* ' : ' ') + b)
}
return
}
if (sub === 'checkout') {
const ref = tail.filter((x) => !x.startsWith('-'))[0]
if (!ref) {
ctx.console.error('git checkout: missing ref')
return
}
await git.checkout({ ...baseOpts(), ref })
return
}
if (sub === 'fetch') {
const http = await getHttp()
const remote = tail[0] || 'origin'
await git.fetch({ ...baseOpts(), http, remote })
ctx.console.log('fetch: done')
return
}
if (sub === 'pull') {
const http = await getHttp()
const remote = tail[0] || 'origin'
await git.pull({
...baseOpts(),
http,
remote,
author: gitAuthor(ctx)
})
ctx.console.log('pull: done')
return
}
if (sub === 'push') {
const http = await getHttp()
const remote = tail[0] || 'origin'
const ref = tail[1]
await git.push({ ...baseOpts(), http, remote, ref })
ctx.console.log('push: done')
return
}
if (sub === 'remote') {
if (tail[0] === 'add' && tail[1] && tail[2]) {
await git.addRemote({ ...baseOpts(), remote: tail[1], url: tail[2] })
return
}
if (tail[0] === 'remove' || tail[0] === 'rm') {
await git.deleteRemote({ ...baseOpts(), remote: tail[1] })
return
}
if (tail[0] === 'rename' && tail[1] && tail[2]) {
ctx.console.error('git remote rename: use deleteRemote + addRemote')
return
}
const list = await git.listRemotes({ ...baseOpts() })
const verbose = tail.includes('-v') || tail.includes('-vv')
for (const r of list) {
ctx.console.log(verbose ? `${r.remote}\t${r.url}` : r.remote)
}
return
}
if (sub === 'tag') {
if (tail[0] === '-a' && tail[1]) {
let msg = ''
const i = tail.indexOf('-m')
if (i !== -1 && tail[i + 1]) msg = tail[i + 1]
await git.tag({ ...baseOpts(), ref: tail[1], message: msg })
return
}
const tags = await git.listTags({ ...baseOpts() })
for (const t of tags) ctx.console.log(t)
return
}
if (sub === 'merge') {
const ffOnly = tail.includes('--ff-only')
const branch = tail.filter((x) => !x.startsWith('-'))[0]
if (!branch) {
ctx.console.error('git merge: missing branch/ref')
return
}
const r = await git.merge({
...baseOpts(),
ours: 'HEAD',
theirs: branch,
fastForwardOnly: ffOnly,
author: gitAuthor(ctx)
})
ctx.console.log(JSON.stringify(r))
return
}
if (sub === 'config') {
const positional = tail.filter((x) => !x.startsWith('-'))
const cfgPath = positional[0]
const val = positional[1]
if (!cfgPath) {
ctx.console.error('git config: missing key')
return
}
if (val === undefined || tail.includes('--get')) {
const v = await git.getConfig({ ...baseOpts(), path: cfgPath })
ctx.console.log(v != null ? String(v) : '')
return
}
await git.setConfig({ ...baseOpts(), path: cfgPath, value: val })
return
}
if (sub === 'show') {
const ref = tail[0] || 'HEAD'
const oid = await git.resolveRef({ ...baseOpts(), ref })
const { commit } = await git.readCommit({ ...baseOpts(), oid })
ctx.console.log(commit.message)
return
}
if (sub === 'stash') {
const raw = tail[0]
const op =
raw === 'push' || raw === 'pop' || raw === 'list' || raw === 'drop' || raw === 'apply'
? raw
: 'push'
const msg =
(raw === 'push' && tail[1] && !tail[1].startsWith('-') ? tail[1] : null) ||
(raw === 'save' && tail[1] && !tail[1].startsWith('-') ? tail[1] : null) ||
'WIP'
if (op === 'push' || raw === 'save') {
await git.stash({ ...baseOpts(), op: 'push', message: msg })
return
}
if (op === 'pop') {
await git.stash({ ...baseOpts(), op: 'pop' })
return
}
if (op === 'list') {
const s = await git.stash({ ...baseOpts(), op: 'list' })
ctx.console.log(JSON.stringify(s))
return
}
if (op === 'drop' || op === 'apply') {
await git.stash({ ...baseOpts(), op })
return
}
ctx.console.error('git stash: use push | pop | list | apply | drop')
return
}
if (sub === 'reset') {
const hard = tail.includes('--hard')
const ref = tail.filter((x) => !x.startsWith('-')).pop() || 'HEAD'
await git.resetIndex({ ...baseOpts(), filepath: '.', ref: hard ? ref : undefined })
ctx.console.log('reset (limited): see isomorphic-git resetIndex')
return
}
if (sub === 'cherry-pick') {
const c = tail.filter((x) => !x.startsWith('-'))[0]
if (!c) {
ctx.console.error('git cherry-pick: missing commit oid')
return
}
const oid = await git.expandOid({ ...baseOpts(), oid: c })
await git.cherryPick({ ...baseOpts(), oid })
return
}
if (sub === 'clean') {
ctx.console.error('git clean: not implemented; remove files via shell rm')
return
}
if (sub === 'rev-parse') {
const ref = tail.filter((x) => !x.startsWith('-'))[0] || 'HEAD'
const oid = await git.resolveRef({ ...baseOpts(), ref })
ctx.console.log(oid)
return
}
if (sub === 'hash-object') {
const w = tail.includes('-w')
const file = tail.filter((x) => !x.startsWith('-')).pop()
if (!file) {
ctx.console.error('git hash-object: missing file')
return
}
const full = path.posix.isAbsolute(file) ? file : path.posix.join(dir, file)
const buf = await fs.promises.readFile(full)
const u8 = buf instanceof Buffer ? new Uint8Array(buf) : buf
const { oid } = await git.hashBlob({ object: u8 })
if (w) await git.writeBlob({ ...baseOpts(), blob: u8 })
ctx.console.log(oid)
return
}
if (sub === 'ls-files') {
const files = await git.listFiles({ ...baseOpts() })
for (const f of files) ctx.console.log(f)
return
}
if (sub === 'diff') {
ctx.console.error(
'git diff: use statusMatrix or external tools; full diff not wired in bare-os git'
)
return
}
ctx.console.error(
`git: '${sub}' is not supported by bare-os (isomorphic-git wrapper). Try: git help`
)
} catch (e) {
const msg = e?.message || String(e)
ctx.console.error('git: ' + msg)
}
}
@@ -0,0 +1,258 @@
/**
* Node fs.promises-shaped adapter over ctx.vfs for isomorphic-git.
* Empty directories are represented by a hidden marker file (filtered from readdir).
* Stat/lstat return Node-like objects (mtime/ctime, mtimeMs/ctimeMs, mode, dev, ino, uid, gid)
* because isomorphic-git normalizeStats() requires them for the index.
*/
import path from 'path'
const MARKER = '.bareos_empty'
function err(code, message) {
const e = new Error(message || code)
// @ts-ignore
e.code = code
return e
}
/** Default Unix mode bits isomorphic-git expects for Git trees (see normalizeMode in upstream). */
const MODE_FILE = 0o100644
const MODE_DIR = 0o040755
const MODE_SYMLINK = 0o120000
/**
* Node fs.Stats-shaped object for isomorphic-git (normalizeStats reads mtime/ctime and numeric ids).
*/
export class VfsGitStats {
/**
* @param {'file' | 'directory' | 'symlink'} type
* @param {number} [size]
* @param {{ mtimeMs?: number, ctimeMs?: number, mode?: number, dev?: number, ino?: number, uid?: number, gid?: number }} [opts]
*/
constructor(type, size = 0, opts = {}) {
this.type = type
this.size = size
const ms = typeof opts.mtimeMs === 'number' ? opts.mtimeMs : Date.now()
this.mtimeMs = ms
this.ctimeMs = typeof opts.ctimeMs === 'number' ? opts.ctimeMs : ms
this.atimeMs = ms
this.birthtimeMs = this.ctimeMs
this.mtime = new Date(this.mtimeMs)
this.ctime = new Date(this.ctimeMs)
this.atime = this.mtime
this.birthtime = this.ctime
this.dev = opts.dev ?? 0
this.ino = opts.ino ?? 0
this.mode = opts.mode ?? (type === 'directory' ? MODE_DIR : type === 'symlink' ? MODE_SYMLINK : MODE_FILE)
this.uid = opts.uid ?? 0
this.gid = opts.gid ?? 0
this.blocks = 0
this.blksize = 4096
this.nlink = 1
this.rdev = 0
}
isFile() {
return this.type === 'file'
}
isDirectory() {
return this.type === 'directory'
}
isSymbolicLink() {
return this.type === 'symlink'
}
}
/**
* Map vfs lstat/stat result to Stats or throw ENOENT.
* @param {Record<string, unknown> | null} st
*/
function toStats(st) {
if (!st) throw err('ENOENT', 'ENOENT')
const ts = {
mtimeMs: typeof st.mtimeMs === 'number' ? st.mtimeMs : undefined,
ctimeMs: typeof st.ctimeMs === 'number' ? st.ctimeMs : undefined,
mode: typeof st.mode === 'number' ? st.mode : undefined
}
if (st.type === 'file') return new VfsGitStats('file', Number(st.size) || 0, ts)
if (st.type === 'directory') return new VfsGitStats('directory', 0, ts)
if (st.type === 'symlink') return new VfsGitStats('symlink', 0, ts)
throw err('ENOENT', 'ENOENT')
}
/**
* @param {{ readFile: Function, writeFile: Function, unlink: Function, readdir: Function, stat: Function, lstat: Function, readlink: Function, symlink: Function }} vfs
*/
export function createGitFsFromVfs(vfs) {
async function rawReaddir(p) {
return vfs.readdir(p)
}
async function readDirFiltered(p) {
const names = await rawReaddir(p)
return names.filter((n) => n !== MARKER)
}
async function stat(p) {
const st = await vfs.stat(p)
return toStats(st)
}
async function lstat(p) {
const st = await vfs.lstat(p)
return toStats(st)
}
async function readFile(p, opts = {}) {
const buf = await vfs.readFile(p)
if (buf == null) throw err('ENOENT', 'ENOENT')
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
if (opts.encoding === 'utf8' || opts.encoding === 'utf-8') {
return Buffer.from(u8).toString('utf8')
}
return Buffer.from(u8)
}
async function writeFile(p, data, _opts = {}) {
const body =
typeof data === 'string'
? Buffer.from(data, 'utf8')
: data instanceof Buffer
? data
: Buffer.from(data)
await vfs.writeFile(p, body)
}
async function unlink(p) {
try {
await vfs.unlink(p)
} catch (e) {
if (e && e.code === 'ENOENT') throw e
throw e
}
}
async function mkdir(p, options = {}) {
const recursive = Boolean(options.recursive)
const norm = path.posix.normalize(p)
if (norm === '/' || norm === '') return
try {
const s = await stat(norm)
if (s.isDirectory()) return
if (s.isFile()) throw err('EEXIST', 'EEXIST')
} catch (e) {
if (e.code !== 'ENOENT') throw e
const parent = path.posix.dirname(norm)
if (parent !== norm && parent !== '/' && parent !== '') {
try {
const ps = await stat(parent)
if (!ps.isDirectory()) throw err('ENOTDIR', 'ENOTDIR')
} catch (pe) {
if (pe.code === 'ENOENT') {
if (recursive) await mkdir(parent, { recursive: true })
else throw err('ENOENT', 'ENOENT')
} else throw pe
}
}
const markerPath = path.posix.join(norm, MARKER)
await vfs.writeFile(markerPath, Buffer.alloc(0))
}
}
async function rmdir(p, opts = {}) {
if (opts.recursive) {
await rm(p, { recursive: true, force: true })
return
}
const names = await readDirFiltered(p)
if (names.length) throw err('ENOTEMPTY', 'ENOTEMPTY')
const markerPath = path.posix.join(p, MARKER)
try {
await vfs.unlink(markerPath)
} catch (e) {
if (e && e.code !== 'ENOENT') throw e
}
}
async function rm(p, opts = {}) {
const recursive = Boolean(opts.recursive)
const force = Boolean(opts.force)
let st
try {
st = await lstat(p)
} catch (e) {
if (e.code === 'ENOENT' && force) return
throw e
}
if (st.isSymbolicLink() || st.isFile()) {
await unlink(p)
return
}
if (!st.isDirectory()) {
await unlink(p)
return
}
if (!recursive) {
await rmdir(p, {})
return
}
const children = await rawReaddir(p)
for (const name of children) {
const full = path.posix.join(p, name)
await rm(full, { recursive: true, force: true })
}
try {
await vfs.unlink(path.posix.join(p, MARKER))
} catch {
/* */
}
try {
await rmdir(p, {})
} catch (e) {
if (e.code !== 'ENOENT' && e.code !== 'ENOTEMPTY') throw e
}
}
async function readdir(p) {
try {
await stat(p)
} catch (e) {
if (e.code === 'ENOENT') return null
throw e
}
return readDirFiltered(p)
}
async function readlink(p) {
return vfs.readlink(p)
}
async function symlink(target, p) {
return vfs.symlink(target, p)
}
async function chmod(_p, _mode) {
/* no-op: hyperdrive mode not exposed */
}
const promises = {
readFile,
writeFile,
unlink,
mkdir,
rmdir,
rm,
stat,
lstat,
readdir,
readlink,
symlink,
chmod
}
return { promises }
}
@@ -3,6 +3,7 @@
*/
import b4a from 'b4a'
import { loadBarerc } from './shell.js'
import {
ACCOUNT_PATH,
decodeAccount,
@@ -59,6 +60,11 @@ export async function applyGuestEnv(ctx) {
} catch (e) {
ctx.console?.error?.('[bare-os] onIdentityGuest: ' + (e?.message || e))
}
try {
await loadBarerc(ctx)
} catch (e) {
ctx.console?.error?.('[bare-os] loadBarerc: ' + (e?.message || e))
}
}
/**
@@ -95,6 +101,11 @@ export async function applyUnlockedEnv(ctx, publicKey, secretKey) {
} catch (e) {
ctx.console?.error?.('[bare-os] onIdentityUnlocked: ' + (e?.message || e))
}
try {
await loadBarerc(ctx, { createSkeletonIfMissing: true })
} catch (e) {
ctx.console?.error?.('[bare-os] loadBarerc: ' + (e?.message || e))
}
}
/**
@@ -1,8 +1,22 @@
import b4a from 'b4a'
import path from 'path'
import unixPathResolve from 'unix-path-resolve'
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
/**
* Run booter-hosted `git` (isomorphic-git) instead of eval'd /bin scripts.
* Skip for explicit relative paths like `./git` so a workspace script can win.
* @param {string} cmd
*/
function shouldDelegateGit(cmd) {
if (cmd === 'git') return true
if (!cmd.includes('/')) return false
if (path.posix.basename(cmd) !== 'git') return false
if (cmd.startsWith('./') || cmd.startsWith('../')) return false
return true
}
/** Strip one leading Unix shebang so AsyncFunction does not see `#!` as invalid syntax. */
function stripShebang(source) {
if (typeof source !== 'string' || !source.startsWith('#!')) return source
@@ -64,6 +78,11 @@ export async function runBinCommand(ctx, argv) {
const vfs = ctx.vfs
const pathEnv = (vfs && vfs.env && vfs.env.PATH) || '/bin'
if (shouldDelegateGit(cmd)) {
const { runGitCli } = await import('./git-cli.js')
return runGitCli(ctx, argv)
}
if (cmd.includes('/')) {
const abs = vfs.resolveLogical(cmd)
const { drive, path } = vfs.route(abs)
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path'
import { statSync } from 'fs'
import { fileURLToPath } from 'node:url'
import { fileURLToPath } from 'url'
import os from 'bare-os'
function cwd() {
+218 -2
View File
@@ -1,5 +1,193 @@
import { runBinCommand } from './kernel-runner.js'
/** Max alias indirections (prevents cycles). */
const MAX_ALIAS_DEPTH = 16
/**
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
* @returns {Record<string, string>}
*/
export function defaultShellAliases() {
return {
ll: 'ls -la',
la: 'ls -A',
l: 'ls',
'..': 'cd ..',
'...': 'cd ../..'
}
}
/**
* Expand first argv[0] through alias chain; append original argv.slice(1).
* @param {string[]} argv
* @param {Record<string, string> | null | undefined} aliases
* @returns {string[]}
*/
export function expandArgvAliases(argv, aliases) {
if (!argv.length) return argv
const map = aliases && typeof aliases === 'object' ? aliases : {}
const out = [...argv]
let depth = 0
while (depth < MAX_ALIAS_DEPTH) {
const first = out[0]
const repl = map[first]
if (repl == null || repl === '') break
const words = tokenize(repl)
.filter((t) => t.type === 'word')
.map((t) => t.value)
if (!words.length) break
out.splice(0, 1, ...words)
depth++
}
if (depth >= MAX_ALIAS_DEPTH && map[out[0]]) {
throw new Error('alias: expansion nested too deeply')
}
return out
}
/**
* Strip one layer of matching single/double quotes from alias value.
* @param {string} val
*/
function stripAliasQuotes(val) {
const v = val.trim()
if (v.length >= 2) {
const q = v[0]
if ((q === "'" || q === '"') && v[v.length - 1] === q) {
return v.slice(1, -1)
}
}
return v
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} rest content after `alias ` (name=value...)
*/
export function applyAliasDefinition(ctx, rest) {
const eq = rest.indexOf('=')
if (eq <= 0) return false
const aname = rest.slice(0, eq).trim()
if (!aname) return false
let val = rest.slice(eq + 1).trim()
val = stripAliasQuotes(val)
if (!ctx.shellAliases) ctx.shellAliases = {}
ctx.shellAliases[aname] = val
return true
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv argv for unalias builtin (includes 'unalias')
*/
export function runUnaliasBuiltin(ctx, argv, logError) {
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
const args = argv.slice(1)
if (args.length === 0) {
logError('unalias: missing name')
return
}
if (args.includes('-a')) {
ctx.shellAliases = { ...defaultShellAliases() }
return
}
for (const name of args) {
if (name === '-a') continue
delete ctx.shellAliases[name]
}
}
/**
* Parse one line from ~/.barerc (without leading `unalias`).
*/
function applyBarercUnalias(ctx, rest) {
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
const parts = rest.split(/\s+/).filter(Boolean)
if (parts.length === 1 && parts[0] === '-a') {
ctx.shellAliases = { ...defaultShellAliases() }
return
}
for (const name of parts) {
if (name === '-a') ctx.shellAliases = { ...defaultShellAliases() }
else delete ctx.shellAliases[name]
}
}
/**
* Comment-only template written on first login when `~/.barerc` is absent
* (`loadBarerc(ctx, { createSkeletonIfMissing: true })`).
*/
export const BARERC_SKELETON = `# Bare OS — ~/.barerc (not full sh; only export, alias, unalias, # comments).
#
# export MY_VAR=value
# alias gst='git status'
# unalias ll
`
/**
* Load `~/.barerc`: only `export`, `alias`, `unalias`, comments, blank lines.
* Resets aliases to defaults first, then applies file.
* @param {Record<string, unknown>} ctx
* @param {{ createSkeletonIfMissing?: boolean }} [opts] If true and the file is missing, write {@link BARERC_SKELETON} (login / unlock only).
*/
export async function loadBarerc(ctx, opts = {}) {
const { createSkeletonIfMissing = false } = opts
const strict = globalThis.process?.env?.BARE_OS_STRICT_BARC === '1'
ctx.shellAliases = { ...defaultShellAliases() }
const vfs = ctx.vfs
const env = vfs.env
let buf = null
try {
buf = await vfs.readFile('~/.barerc')
} catch {
buf = null
}
let text = null
if (!buf && createSkeletonIfMissing) {
try {
await vfs.writeFile('~/.barerc', ctx.b4a.from(BARERC_SKELETON))
text = BARERC_SKELETON
} catch (e) {
ctx.console?.error?.(
'[bare-os] could not create ~/.barerc: ' + ((e && e.message) || e)
)
}
}
if (!text) {
if (!buf) return
text = ctx.b4a.toString(buf)
}
for (const line of text.split(/\r?\n/)) {
const t = line.trim()
if (!t || t.startsWith('#')) continue
if (t.startsWith('export ')) {
const rest = t.slice(7).trim()
const eq = rest.indexOf('=')
if (
eq > 0 &&
/^[A-Za-z_][A-Za-z0-9_]*$/.test(rest.slice(0, eq))
) {
env[rest.slice(0, eq)] = expandWord(rest.slice(eq + 1), env)
} else if (strict) {
ctx.console?.error?.('barerc: ignored: ' + t)
}
continue
}
if (t.startsWith('alias ')) {
const ok = applyAliasDefinition(ctx, t.slice(6).trim())
if (!ok && strict) ctx.console?.error?.('barerc: ignored: ' + t)
continue
}
if (t.startsWith('unalias ')) {
applyBarercUnalias(ctx, t.slice(8).trim())
continue
}
if (strict) ctx.console?.error?.('barerc: ignored: ' + t)
}
}
/**
* @typedef {{ type: 'word', value: string } | { type: 'op', value: string }} Token
*/
@@ -246,7 +434,15 @@ export async function execShellLine(ctx, line) {
if (!cmd.argv.length) continue
const argv = cmd.argv.map((w) => expandWord(w, env))
if (!ctx.shellAliases) ctx.shellAliases = { ...defaultShellAliases() }
let argv = cmd.argv.map((w) => expandWord(w, env))
try {
argv = expandArgvAliases(argv, ctx.shellAliases)
} catch (e) {
ctx.console.error((e && e.message) || String(e))
continue
}
const name = argv[0]
if (cmd.redirIn) {
@@ -267,7 +463,27 @@ export async function execShellLine(ctx, line) {
let code = 'ok'
try {
if (name === 'cd') {
if (name === 'alias') {
if (argv.length === 1) {
const al = ctx.shellAliases || {}
for (const k of Object.keys(al).sort()) {
origLog.call(ctx.console, `${k}='${al[k]}'`)
}
} else {
let okCount = 0
for (const part of argv.slice(1)) {
if (applyAliasDefinition(ctx, part)) okCount++
}
if (okCount === 0) {
origErr.call(
ctx.console,
'alias: usage: alias name=value [name=value ...]'
)
}
}
} else if (name === 'unalias') {
runUnaliasBuiltin(ctx, argv, (m) => origErr.call(ctx.console, m))
} else if (name === 'cd') {
try {
await vfs.chdir(argv[1] || vfs.home)
} catch (e) {
+198 -26
View File
@@ -134,6 +134,138 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
return !!(e && e.value && e.value.blob)
}
function joinLogical(base, name) {
if (base === '/') return '/' + name.replace(/^\/+/, '')
return unixPathResolve(base, name)
}
async function lstatFromAbs(abs) {
if (abs === '/mnt' || abs === '/mnt/') {
return { type: 'directory', path: abs }
}
const activeSeg = activeHomeBasename()
if (activeSeg && abs === '/home') {
return { type: 'directory', path: abs }
}
const r = route(abs)
if (r.virtualMntRoot) {
return { type: 'directory', path: abs }
}
const { drive, path: p } = r
if (abs === '/' || isHyperdriveRootPath(p)) {
return { type: 'directory', path: abs }
}
const e = await entryOn(drive, p, { follow: false })
if (e && e.value && e.value.linkname) {
return { type: 'symlink', path: abs, linkname: e.value.linkname }
}
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 }
}
const names = await (async () => {
const out = []
try {
const stream = drive.readdir(p === '/' ? '/' : p)
for await (const n of stream) out.push(n)
} catch {
/* missing */
}
return out
})()
if (names.length) return { type: 'directory', path: abs }
if (e) return { type: 'directory', path: abs }
return null
}
async function readdirFromAbs(abs) {
if (abs === '/mnt' || abs === '/mnt/') {
return [...getMntMap().keys()].sort()
}
const activeSeg = activeHomeBasename()
if (activeSeg && abs === '/home') {
return [activeSeg].sort()
}
const r = route(abs)
if (r.virtualMntRoot) {
return [...getMntMap().keys()].sort()
}
const { drive, path: p } = r
const folder = p === '/' ? '/' : p
const names = []
const stream = drive.readdir(folder)
for await (const name of stream) {
names.push(name)
}
if (activeSeg && abs === '/' && !names.includes('home')) {
names.push('home')
}
if (abs === '/' && !names.includes('mnt')) {
names.push('mnt')
}
return names.sort()
}
async function delFromAbs(abs) {
const r = route(abs)
if (r.virtualHomeDir || r.virtualMntRoot) {
throw new Error('Read-only path (not under $HOME): ' + abs)
}
if (r.mntReadOnly === true) {
throw new Error('Read-only mount: ' + abs)
}
const { drive, path: p } = r
if (r.mntReadOnly === false) {
if (isHyperdriveRootPath(p)) {
throw new Error('Cannot unlink directory root')
}
return drive.del(p)
}
if (drive !== personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + abs)
}
if (isHyperdriveRootPath(p)) {
throw new Error('Cannot unlink directory root')
}
return drive.del(p)
}
/**
* Recursive remove: Hyperdrive `del` is one entry; directories need a tree walk.
*/
async function rmFromAbs(abs, { recursive = false, force = false } = {}) {
const r = route(abs)
if (r.virtualHomeDir || r.virtualMntRoot) {
if (force) return
throw new Error('Read-only path (not under $HOME): ' + abs)
}
if (r.mntReadOnly === true) {
if (force) return
throw new Error('Read-only mount: ' + abs)
}
const st = await lstatFromAbs(abs)
if (!st) {
if (force) return
throw new Error('ENOENT: no such file or directory')
}
if (st.type === 'symlink' || st.type === 'file') {
return delFromAbs(abs)
}
if (st.type === 'directory') {
if (!recursive) {
throw new Error('Is a directory')
}
const names = await readdirFromAbs(abs)
for (const n of names) {
await rmFromAbs(joinLogical(abs, n), { recursive: true, force: true })
}
}
}
return {
get home() {
return normalizeHome()
@@ -217,6 +349,19 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
return drive.del(p)
},
/**
* Remove file, symlink, or directory tree (with recursive). Single-key `del` is not enough for dirs.
* @param {string} userPath
* @param {{ recursive?: boolean, force?: boolean }} [opts]
*/
async rm(userPath, opts = {}) {
const abs = resolveLogical(userPath)
return rmFromAbs(abs, {
recursive: Boolean(opts.recursive),
force: Boolean(opts.force)
})
},
async exists(userPath) {
const abs = resolveLogical(userPath)
const r = route(abs)
@@ -228,32 +373,7 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
/** @returns {Promise<string[]>} */
async readdir(userPath) {
const abs = resolveLogical(userPath)
if (abs === '/mnt' || abs === '/mnt/') {
return [...getMntMap().keys()].sort()
}
const activeSeg = activeHomeBasename()
if (activeSeg && abs === '/home') {
return [activeSeg].sort()
}
const r = route(abs)
if (r.virtualMntRoot) {
return [...getMntMap().keys()].sort()
}
const { drive, path: p } = r
const folder = p === '/' ? '/' : p
const names = []
const stream = drive.readdir(folder)
for await (const name of stream) {
names.push(name)
}
if (activeSeg && abs === '/' && !names.includes('home')) {
names.push('home')
}
if (abs === '/' && !names.includes('mnt')) {
names.push('mnt')
}
return names.sort()
return readdirFromAbs(resolveLogical(userPath))
},
async stat(userPath) {
@@ -298,6 +418,58 @@ export function createVfs(systemDrive, personalDrive, env, mntRef = null) {
if (names.length) return { type: 'directory', path: abs }
if (e) return { type: 'directory', path: abs }
return null
},
/**
* Like stat but do not follow symlinks at the final path (for isomorphic-git lstat).
*/
async lstat(userPath) {
return lstatFromAbs(resolveLogical(userPath))
},
async readlink(userPath) {
const abs = resolveLogical(userPath)
const r = route(abs)
if (r.virtualHomeDir || r.virtualMntRoot) {
throw new Error('EINVAL readlink')
}
if (r.mntReadOnly === true) {
throw new Error('Read-only mount: ' + userPath)
}
const { drive, path: p } = r
if (isHyperdriveRootPath(p)) throw new Error('EINVAL readlink')
const e = await entryOn(drive, p, { follow: false })
if (e && e.value && e.value.linkname) return e.value.linkname
throw new Error('EINVAL not a symlink')
},
/**
* @param {string} target link text (stored as-is)
* @param {string} userPath new symlink path
*/
async symlink(target, userPath) {
const abs = resolveLogical(userPath)
const r = route(abs)
if (r.virtualHomeDir || r.virtualMntRoot) {
throw new Error('Read-only path (not under $HOME): ' + userPath)
}
if (r.mntReadOnly === true) {
throw new Error('Read-only mount: ' + userPath)
}
const { drive, path: p } = r
if (r.mntReadOnly === false) {
if (isHyperdriveRootPath(p)) {
throw new Error('Cannot symlink at directory root')
}
return drive.symlink(p, target)
}
if (drive !== personalDrive) {
throw new Error('Read-only path (not under $HOME): ' + userPath)
}
if (isHyperdriveRootPath(p)) {
throw new Error('Cannot symlink at directory root')
}
return drive.symlink(p, target)
}
}
}
+2
View File
@@ -14,6 +14,7 @@
"autopass": "^3.4.0",
"b4a": "^1.6.7",
"bare-crypto": "^1.13.4",
"bare-fetch": "^2.8.1",
"bare-os": "^3.8.7",
"bare-os-protocol": "*",
"bare-readline": "^1.3.1",
@@ -23,6 +24,7 @@
"hypercore-id-encoding": "^1.3.0",
"hyperdrive": "^13.3.2",
"hyperswarm": "^4.16.0",
"isomorphic-git": "^1.37.4",
"protomux": "^3.10.1",
"safety-catch": "^1.0.2",
"unix-path-resolve": "^1.0.2"
+199 -1
View File
@@ -8,9 +8,19 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { PassThrough } from 'node:stream'
import { runKernelFromSource, runBinCommand } from './lib/kernel-runner.js'
import { createGitFsFromVfs } from './lib/git-fs-adapter.js'
import { runGitCli } from './lib/git-cli.js'
import { createStreamLineReader } from './lib/cli-readline.js'
import { createVfs } from './lib/vfs.js'
import { tokenize, expandWord, execShellLine } from './lib/shell.js'
import {
tokenize,
expandWord,
execShellLine,
expandArgvAliases,
defaultShellAliases,
loadBarerc,
BARERC_SKELETON
} from './lib/shell.js'
import {
fuzzyMatch,
stripAnsi,
@@ -344,6 +354,59 @@ test('expandWord reads env', async (t) => {
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
})
test('expandArgvAliases expands first word and keeps trailing argv', async (t) => {
t.alike(expandArgvAliases(['ll', 'z'], defaultShellAliases()), [
'ls',
'-la',
'z'
])
})
test('expandArgvAliases throws on cyclic alias chain', async (t) => {
const cyclic = { a: 'b', b: 'a' }
t.exception(
() => expandArgvAliases(['a'], cyclic),
/alias: expansion nested too deeply/
)
})
test('loadBarerc applies export and alias from personal ~/.barerc', async (t) => {
const dir = testCorestoreDir('barerc')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('brc'))
await drive.ready()
await personal.ready()
await personal.put(
'/.barerc',
b4a.from('export MYRC=1\nalias dog=echo woof\n')
)
const ctx = testCtx(drive, personal)
await loadBarerc(ctx)
t.is(ctx.vfs.env.MYRC, '1')
t.is(ctx.shellAliases.dog, 'echo woof')
t.is(ctx.shellAliases.ll, 'ls -la')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('loadBarerc createSkeletonIfMissing writes ~/.barerc when absent', async (t) => {
const dir = testCorestoreDir('barercskel')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('brcsk'))
await drive.ready()
await personal.ready()
const ctx = testCtx(drive, personal)
await loadBarerc(ctx, { createSkeletonIfMissing: true })
const back = await ctx.vfs.readFile('~/.barerc')
t.ok(back)
t.is(ctx.b4a.toString(back), BARERC_SKELETON)
t.is(ctx.shellAliases.ll, 'ls -la')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine runs cd and external', async (t) => {
const dir = testCorestoreDir('sh')
const store = new Corestore(dir)
@@ -370,6 +433,30 @@ async function run(ctx, argv) {
rmSync(dir, { recursive: true, force: true })
})
test('execShellLine expands default ll to ls -la', async (t) => {
const dir = testCorestoreDir('llalias')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('lla'))
await drive.ready()
await personal.ready()
await drive.put(
'/bin/ls',
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, 'll one')
t.is(got[0], 'ls -la one')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('cron fieldMatches and dowFieldMatches', async (t) => {
t.ok(fieldMatches('*', 0, 0, 59))
t.ok(fieldMatches('*/5', 10, 0, 59))
@@ -519,6 +606,117 @@ test('tier-1 grep from system drive', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('tier-1 rm -rf removes directory tree on personal drive', async (t) => {
const dir = testCorestoreDir('rmrf')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('prm'))
await drive.ready()
await personal.ready()
await drive.put('/bin/rm', b4a.from(await readBuiltBin('rm')))
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = { log() {}, error() {} }
await personal.put('/nest/leaf/x.txt', b4a.from('x'))
await personal.put('/nest/other/y.txt', b4a.from('y'))
t.ok((await ctx.vfs.readdir('nest')).includes('leaf'))
await runBinCommand(ctx, ['rm', '-rf', 'nest'])
t.is(ctx.exitCode, 0)
const after = await ctx.vfs.readdir('.')
t.ok(!after.includes('nest'))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('git-fs-adapter mkdir recursive and readdir hides .bareos_empty', async (t) => {
const dir = testCorestoreDir('gitfs')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pgf'))
await drive.ready()
await personal.ready()
const ctx = testCtx(drive, personal)
const fs = createGitFsFromVfs(ctx.vfs)
const base = '/home/user/gittest'
await fs.promises.mkdir(path.posix.join(base, 'a', 'b'), { recursive: true })
const inA = await fs.promises.readdir(path.posix.join(base, 'a'))
t.is(inA.indexOf('.bareos_empty'), -1)
t.ok(inA.includes('b'))
const top = await fs.promises.readdir(base)
t.ok(top.includes('a'))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('runBinCommand delegates git to booter (ignores /bin/git script body)', async (t) => {
const dir = testCorestoreDir('gitdel')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pgd'))
await drive.ready()
await personal.ready()
await drive.put(
'/bin/git',
b4a.from(`async function run() { throw new Error('eval git should not run') }`)
)
const logs = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => logs.push(a.join(' '))
}
await runBinCommand(ctx, ['git', 'version'])
t.ok(logs.some((l) => /isomorphic-git/i.test(l)))
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('runBinCommand runs ./git from cwd instead of booter delegate', async (t) => {
const dir = testCorestoreDir('gitlocal')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pgl'))
await drive.ready()
await personal.ready()
await personal.put(
'/git',
b4a.from(`
async function run(ctx) {
ctx.out.push('local-git-script')
}
`)
)
const out = []
const ctx = testCtx(drive, personal)
ctx.out = out
await runBinCommand(ctx, ['./git'])
t.is(out[0], 'local-git-script')
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('runGitCli init and status on personal drive', async (t) => {
const dir = testCorestoreDir('gitcmd')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pgc'))
await drive.ready()
await personal.ready()
const logs = []
const ctx = testCtx(drive, personal)
ctx.console = {
log: (...a) => logs.push(a.join(' ')),
error: (...a) => logs.push(a.join(' '))
}
await runGitCli(ctx, ['git', 'init', '-C', '/home/user/myrepo'])
t.ok(logs.some((l) => /initialized|git repository/i.test(l)))
logs.length = 0
await runGitCli(ctx, ['git', '-C', '/home/user/myrepo', 'status'])
t.ok(logs.length > 0)
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)
+1 -1
View File
@@ -1,6 +1,6 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | builtins: cd, export, exit, login, logout | /bin: basename cat clear crontab date dirname echo env exit false grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
'Bare OS — default user: guest | builtins: alias, cd, export, exit, login, logout, unalias | /bin: basename cat clear crontab date dirname echo env exit false git grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
)
ctx.console.log(
'Identity: login [--new] <passphrase> | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'
+32 -2
View File
@@ -1,4 +1,11 @@
/**
* rm — remove files or directories.
* Flags: -r -R --recursive, -f --force, -- ; bundled e.g. -rf
*/
async function run(ctx, argv) {
const vfs = ctx.vfs
let recursive = false
let force = false
const files = []
let dash = false
for (let i = 1; i < argv.length; i++) {
@@ -11,7 +18,22 @@ async function run(ctx, argv) {
dash = true
continue
}
if (a.startsWith('-')) continue
if (a === '--recursive' || a === '-r' || a === '-R') {
recursive = true
continue
}
if (a === '--force' || a === '-f') {
force = true
continue
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
if (c === 'r' || c === 'R') recursive = true
else if (c === 'f') force = true
}
continue
}
files.push(a)
}
if (!files.length) {
@@ -19,10 +41,18 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
const doRm =
vfs && typeof vfs.rm === 'function'
? (p) => vfs.rm(p, { recursive, force })
: async (p) => {
if (recursive) throw new Error('recursive rm not supported')
return vfs.unlink(p)
}
for (const f of files) {
try {
await ctx.vfs.unlink(f)
await doRm(f)
} catch (e) {
if (force) continue
ctx.console.error('rm: ' + f + ': ' + (e.message || e))
ctx.exitCode = 1
}
+13 -8
View File
@@ -136,7 +136,8 @@ async function run(ctx, argv) {
patterns.push(s)
}
} catch (e) {
if (!suppressErrors) ctx.console.error('grep: ' + pf + ': ' + (e.message || e))
if (!suppressErrors)
ctx.console.error('grep: ' + pf + ': ' + (e.message || e))
ctx.exitCode = 2
return
}
@@ -181,13 +182,15 @@ async function run(ctx, argv) {
try {
const buf = await vfs.readFile(path)
if (!buf) {
if (!suppressErrors) ctx.console.error('grep: ' + path + ': No such file')
if (!suppressErrors)
ctx.console.error('grep: ' + path + ': No such file')
fatal = true
continue
}
text = ctx.b4a.toString(buf)
} catch (e) {
if (!suppressErrors) ctx.console.error('grep: ' + path + ': ' + (e.message || e))
if (!suppressErrors)
ctx.console.error('grep: ' + path + ': ' + (e.message || e))
fatal = true
continue
}
@@ -249,9 +252,12 @@ async function run(ctx, argv) {
function buildMatchers(patterns, o) {
if (patterns.length === 0) throw new Error('no pattern')
if (o.fixed) {
const pats = o.icase ? patterns.map((p) => p.toLowerCase()) : patterns.slice()
return pats.map((p) => (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
const pats = o.icase
? patterns.map((p) => p.toLowerCase())
: patterns.slice()
return pats.map(
(p) => (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
)
}
const flags = o.icase ? 'i' : ''
@@ -283,8 +289,7 @@ function stripCr(line) {
* @param {boolean} multiFile
*/
function namePrefixForCount(showName, label, useStdin, multiFile) {
const need =
multiFile || (showName && (label != null || useStdin))
const need = multiFile || (showName && (label != null || useStdin))
if (!need) return ''
if (label != null) return label + ':'
return '(standard input):'
+1 -1
View File
@@ -5,7 +5,7 @@ function bareStdin(ctx) {
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | builtins: cd, export, exit, login, logout | /bin: basename cat clear crontab date dirname echo env exit false grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
'Bare OS — default user: guest | builtins: alias, cd, export, exit, login, logout, unalias | /bin: basename cat clear crontab date dirname echo env exit false git grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
)
ctx.console.log(
'Identity: login [--new] <passphrase> | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'
+32 -2
View File
@@ -3,7 +3,14 @@ function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/**
* rm — remove files or directories.
* Flags: -r -R --recursive, -f --force, -- ; bundled e.g. -rf
*/
async function run(ctx, argv) {
const vfs = ctx.vfs
let recursive = false
let force = false
const files = []
let dash = false
for (let i = 1; i < argv.length; i++) {
@@ -16,7 +23,22 @@ async function run(ctx, argv) {
dash = true
continue
}
if (a.startsWith('-')) continue
if (a === '--recursive' || a === '-r' || a === '-R') {
recursive = true
continue
}
if (a === '--force' || a === '-f') {
force = true
continue
}
if (a.startsWith('-') && a.length > 1) {
for (let j = 1; j < a.length; j++) {
const c = a[j]
if (c === 'r' || c === 'R') recursive = true
else if (c === 'f') force = true
}
continue
}
files.push(a)
}
if (!files.length) {
@@ -24,10 +46,18 @@ async function run(ctx, argv) {
ctx.exitCode = 1
return
}
const doRm =
vfs && typeof vfs.rm === 'function'
? (p) => vfs.rm(p, { recursive, force })
: async (p) => {
if (recursive) throw new Error('recursive rm not supported')
return vfs.unlink(p)
}
for (const f of files) {
try {
await ctx.vfs.unlink(f)
await doRm(f)
} catch (e) {
if (force) continue
ctx.console.error('rm: ' + f + ': ' + (e.message || e))
ctx.exitCode = 1
}