Updates
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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/)'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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):'
|
||||
|
||||
@@ -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/)'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user