This commit is contained in:
Raven Scott
2026-04-03 16:29:27 -04:00
parent 0dd01be4ab
commit 77638c4138
23 changed files with 1594 additions and 153 deletions
+36 -29
View File
@@ -20,7 +20,7 @@ bare-operating-system/
├── developer-guide/ # How-to: in-image JS, ctx, coreutils, testing
│ ├── README.md # Index + reading order
│ ├── 01-two-runtimes-host-vs-image.md … 10-glossary-and-faq.md
├── LICENSE # Apache-2.0 notice
├── LICENSE # Apache-2.0 notice (root repo)
├── .gitignore # Ignore rules
├── .prettierrc # Prettier formatting defaults
├── .github/
@@ -60,6 +60,9 @@ bare-operating-system/
├── paths.js
├── swarm-disk.js
├── kernel-runner.js
├── git-cli.js # isomorphic-git; booter delegates `git`
├── curl-cli.js # Fetch HTTP client; booter delegates `curl`
├── wget-cli.js # Fetch downloads; booter delegates `wget`
├── identity-account.js # Ed25519 account blob (bare-crypto PBKDF2 + ChaCha20-Poly1305)
├── identity-session.js # Guest vs unlocked env, vault save, ctx hooks
├── vfs.js # Two-drive path routing ($HOME → personal drive)
@@ -73,18 +76,18 @@ bare-operating-system/
## 2. Root: [package.json](package.json)
| Field | Value / meaning |
| ----------------- | -------------------------------------------------------- |
| `name` | `bare-operating-system` |
| `private` | `true` — not published as a single npm package |
| `type` | `module` — ESM |
| `workspaces` | `["packages/*"]` — npm workspaces for the three packages |
| `scripts.pretest` | Runs `npm run build -w bare-os-coreutils` before tests |
| `scripts.test` | Runs `npm run test --workspaces --if-present` |
| `scripts.format` | `prettier --write .` |
| `scripts.lint` | `prettier --check .` |
| `engines.node` | `>=20` |
| `devDependencies` | `prettier@^3.4.2` |
| Field | Value / meaning |
| ----------------- | ------------------------------------------------------------------------------------------ |
| `name` | `bare-operating-system` |
| `private` | `true` — not published as a single npm package |
| `type` | `module` — ESM |
| `workspaces` | `["packages/*"]` — npm workspaces (protocol, coreutils, seeder, booter, …) |
| `scripts.pretest` | Runs `npm run build -w bare-os-coreutils` before tests |
| `scripts.test` | Runs `npm run test --workspaces --if-present` |
| `scripts.format` | `prettier --write .` |
| `scripts.lint` | `prettier --check .` |
| `engines.node` | `>=20` |
| `devDependencies` | `prettier@^3.4.2` |
No runtime dependencies at the root; all stack deps live in workspace packages.
@@ -404,6 +407,9 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
**`runBinCommand(ctx, argv)`**
- If **`argv[0]`** is **`git`** (or a POSIX path whose basename is `git`, but not `./git` or `../git`), **delegates** to **`runGitCli`** in [`git-cli.js`](packages/bare-os-booter/lib/git-cli.js).
- If **`argv[0]`** is **`curl`** under the same basename rules, **delegates** to **`runCurlCli`** in [`curl-cli.js`](packages/bare-os-booter/lib/curl-cli.js) (HTTP client via Fetch / **bare-fetch**; manual **`curl(1)`** in the merged JSON DB).
- If **`argv[0]`** is **`wget`** under the same basename rules, **delegates** to **`runWgetCli`** in [`wget-cli.js`](packages/bare-os-booter/lib/wget-cli.js) (HTTP download via Fetch / **bare-fetch**; manual **`wget(1)`** in the merged JSON DB).
- If `argv[0]` contains `/`, resolves with `ctx.vfs.resolveLogical`, `route`, loads script bytes from the routed Hyperdrive (`get` with `follow`).
- Else walks `$PATH` (`ctx.vfs.env.PATH`, default `/bin`), joining each directory with `unix-path-resolve(dir, cmd)` (not three-argument resolve), loads from **system** `ctx.drive` only.
- Builds `AsyncFunction('ctx','argv', ...)` requiring `run`, invokes `run(ctx, argv)`.
@@ -422,6 +428,7 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
### 12.7 [packages/bare-os-booter/lib/shell.js](packages/bare-os-booter/lib/shell.js)
- **`defaultShellAliases`** — includes `ll`, `la`, `l`, `..`, `...` as the baseline merged from **`~/.barerc`**.
- **`tokenize` / `expandWord` / `parsePipeline`** — POSIX-ish words, `'...'`, `"..."`, `\`, `|`, `>`, `>>`, `<`; `$VAR` and `${VAR}`; pipelines split on `|`.
- **`execShellLine(ctx, line)`** — per-simple-command: leading `NAME=value` assignments (blocked for `ctx.shellReadonlyVars`), redirections, builtins `alias`, `unalias`, `cd`, `export`, `unset`, `readonly`, `umask`, `:`, `command`, `type`, `login`, `logout`, `exit`, else `runBinCommand`. `command -v`/`-V` and `type` use **`resolveBinInPath`**. `login`/`logout` call the same `ctx.applyRegister` / `ctx.applyUnlock` / `ctx.applyLogout` hooks as `/bin/login` and `/bin/logout`. Captures `console.log` for pipes and file redirection; `>` / `>>` target paths via `ctx.vfs.writeFile` (personal tree). Returns `'exit'` when the `exit` builtin runs.
@@ -482,17 +489,17 @@ sequenceDiagram
## 14. Environment variables (complete list)
| Variable | Used by | Meaning |
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `BARE_OS_KERNEL_ROOT` | Seeder | Absolute path to kernel tree (default: `repo/kernel`) |
| `BARE_OS_SEED_STORE` | Seeder | Corestore directory (default: `repo/data/corestore-seeder`) |
| `BARE_OS_BOOT_STORE` | Booter | Corestore for boot side (default: `repo/data/corestore-booter`) |
| `BARE_OS_BOOT_TIMEOUT_MS` | Booter | Wall-clock budget for peer wait + network boot (default `60000`) |
| `BARE_OS_NO_SPLASH` | Booter | If `1`, skip TTY splash (plain logs / non-TTY behavior unchanged) |
| `BARE_OS_LOCAL_SEED` | paths | Overrides local seed path helper (`defaultLocalSeedCorestorePath`); booter does not local-boot |
| `BARE_OS_SKIP_REPL` | Booter | If `1`, readline returns null — non-interactive exit |
| `MANWIDTH` | `/bin/man` | Wrap width for manual text (default `72`; minimum `40`) |
| `NO_COLOR` | `/bin/man` | If set, disable ANSI bold for section headings on a TTY |
| Variable | Used by | Meaning |
| ------------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `BARE_OS_KERNEL_ROOT` | Seeder | Absolute path to kernel tree (default: `repo/kernel`) |
| `BARE_OS_SEED_STORE` | Seeder | Corestore directory (default: `repo/data/corestore-seeder`) |
| `BARE_OS_BOOT_STORE` | Booter | Corestore for boot side (default: `repo/data/corestore-booter`) |
| `BARE_OS_BOOT_TIMEOUT_MS` | Booter | Wall-clock budget for peer wait + network boot (default `60000`) |
| `BARE_OS_NO_SPLASH` | Booter | If `1`, skip TTY splash (plain logs / non-TTY behavior unchanged) |
| `BARE_OS_LOCAL_SEED` | paths | Overrides local seed path helper (`defaultLocalSeedCorestorePath`); booter does not local-boot |
| `BARE_OS_SKIP_REPL` | Booter | If `1`, readline returns null — non-interactive exit |
| `MANWIDTH` | `/bin/man` | Wrap width for manual text (default `72`; minimum `40`) |
| `NO_COLOR` | `/bin/man` | If set, disable ANSI bold for section headings on a TTY |
**Session env (set by booter, not user configuration):** `USER`, `LOGNAME`, `HOME`, `PWD`, `UID`, `GID`, `GROUP`, `BARE_OS_IDENTITY` (`guest` or `unlocked`), and when unlocked `BARE_OS_PUBLIC_KEY` (hex Ed25519 public key).
@@ -500,11 +507,11 @@ sequenceDiagram
## 14a. POSIX userland appendix (implemented vs gaps)
| Area | Status |
| ------------- | ------ |
| **VFS** | Two-drive unified paths; `$HOME` → personal Hyperdrive; writable mounts under `/mnt` when HDMS allows. **`mkdir`/`rmdir`**, **`chmod`** (octal + symbolic subset), **`symlink`/`readlink`**, **`stat`/`lstat`**, **`rm`** recursive. Empty dirs use **`.bareos_empty`** (same idea as `git-fs-adapter`). |
| **Shell** | Pipelines `\|`, redirects `>`/`>>`/`<`, quoting, `$VAR`/`${VAR}`, builtins: **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`unset`**, **`readonly`**, **`umask`**, **`:`**, **`command`**, **`type`**, **`login`**, **`logout`**, **`exit`**. No **`&&`/`||`**, job control, functions, or full POSIX **`sh`** grammar. |
| **Ownership** | Display and permission checks use **`UID`/`GID`** and mode bits; **`chown`/`chgrp`** are stubs (no multi-user ownership changes). |
| Area | Status |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | -------------------------------------------------------------- |
| **VFS** | Two-drive unified paths; `$HOME` → personal Hyperdrive; writable mounts under `/mnt` when HDMS allows. **`mkdir`/`rmdir`**, **`chmod`** (octal + symbolic subset), **`symlink`/`readlink`**, **`stat`/`lstat`**, **`rm`** recursive. Empty dirs use **`.bareos_empty`** (same idea as `git-fs-adapter`). |
| **Shell** | Pipelines `\|`, redirects `>`/`>>`/`<`, quoting, `$VAR`/`${VAR}`, builtins: **`alias`**, **`unalias`**, **`cd`**, **`export`**, **`unset`**, **`readonly`**, **`umask`**, **`:`**, **`command`**, **`type`**, **`login`**, **`logout`**, **`exit`**. No \*\*`&&`/` | | `**, job control, functions, or full POSIX **`sh`\*\* grammar. |
| **Ownership** | Display and permission checks use **`UID`/`GID`** and mode bits; **`chown`/`chgrp`** are stubs (no multi-user ownership changes). |
| **Utilities** | Tier-1 JS **`/bin`** (see §12.10): includes **`man`**, **`sed`**, **`awk`**, **`cp`**, **`mv`**, **`find`**, **`cksum`**, etc. Large **`sed`/`awk`** are not byte-identical to GNU on all inputs. **`xargs`**, **`getconf`**, **`mkfifo`** are stubs. **`test`** sets **`ctx.exitCode`**; the shell does not branch on it for **`&&`**. Online help: **`/share/man/man.json`** and **`man`**. |
**Handbook:** [handbook/09-posix-utilities-shell-and-vfs.md](handbook/09-posix-utilities-shell-and-vfs.md) — narrative catalog, engine notes, and Issue 7 alignment. **Manual pages:** [handbook/10-manpages-and-online-help.md](handbook/10-manpages-and-online-help.md).
+10 -10
View File
@@ -12,16 +12,16 @@ This project is experimental research software, not a production OS.
## Repository layout
| Path | Role |
| ------------------------------------------------------ | -------------------------------------------------------------------------------- |
| [kernel/](kernel/) | Source tree mirrored into the system drive (`/boot/init.js`, `/bin/*`, `/etc/*`) |
| [packages/bare-os-protocol](packages/bare-os-protocol) | `bare-os-v1` topic, MBR layout, Protomux seed channel helper |
| [packages/bare-os-seeder](packages/bare-os-seeder) | Pear app: stage kernel → Hyperdrive, serve MBR block 0, replicate |
| [packages/bare-os-booter](packages/bare-os-booter) | Pear app: peer discovery, MBR, replicate system drive, run kernel |
| [handbook/](handbook/) | Human-readable system handbook (chapters + diagrams) |
| [developer-guide/](developer-guide/) | How to develop scripts and utilities inside Bare OS vs host Pear packages |
| [kernel/README.md](kernel/README.md) | Staged system image tree |
| [scripts/README.md](scripts/README.md) | Pear `node_modules` helper |
| Path | Role |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| [kernel/](kernel/) | Source tree mirrored into the system drive (`/boot/init.js`, `/bin/*`, `/etc/*`) |
| [packages/bare-os-protocol](packages/bare-os-protocol) | `bare-os-v1` topic, MBR layout, Protomux seed channel helper |
| [packages/bare-os-seeder](packages/bare-os-seeder) | Pear app: stage kernel → Hyperdrive, serve MBR block 0, replicate |
| [packages/bare-os-booter](packages/bare-os-booter) | Pear app: peer discovery, MBR, replicate system drive, run kernel |
| [handbook/](handbook/) | Human-readable system handbook (chapters + diagrams) |
| [developer-guide/](developer-guide/) | How to develop scripts and utilities inside Bare OS vs host Pear packages |
| [kernel/README.md](kernel/README.md) | Staged system image tree |
| [scripts/README.md](scripts/README.md) | Pear `node_modules` helper |
## Prerequisites
+1 -1
View File
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc which whoami xargs'
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc wget which whoami xargs'
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
File diff suppressed because one or more lines are too long
+14
View File
@@ -17,6 +17,10 @@ import {
} from './lib/cli-readline.js'
import { resolveStdio } from './lib/resolve-stdio.js'
import { createKernelReplSession } from './lib/repl-session.js'
import {
suspendFishStdinForSubprocess,
resumeFishStdinAfterSubprocess
} from './lib/fish-readline.js'
import { createBootSplash } from './lib/boot-splash.js'
import {
applyGuestEnv,
@@ -316,6 +320,9 @@ async function executeKernel(disk, store, swarm, initSource) {
}
}
ctx.replStdin = sessionStdin
ctx.replStdout = sessionStdout
await applyGuestEnv(ctx)
await ensureGuestHome(ctx)
@@ -328,6 +335,13 @@ async function executeKernel(disk, store, swarm, initSource) {
ctx
})
ctx.suspendReplForSubprocess = () => {
if (session.fishStdin) suspendFishStdinForSubprocess(session.fishStdin)
}
ctx.resumeReplAfterSubprocess = () => {
if (session.fishStdin) resumeFishStdinAfterSubprocess(session.fishStdin)
}
const sessionReadLine = session.readLine
ctx.execLine = async (line) => {
const t = line.trim()
+530
View File
@@ -0,0 +1,530 @@
/**
* Bare OS curl HTTP(S) client using Fetch (not Daniel Stenberg's libcurl).
* @see https://curl.se/docs/manpage.html for flag inspiration; behavior is a subset.
*/
/** @type {boolean} */
let bareFetchTried = false
async function ensureBareFetchForCurl() {
if (typeof globalThis.fetch === 'function') return
if (bareFetchTried) return
bareFetchTried = true
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 optional on Node tests */
}
}
function basicAuthHeader(user, pass) {
const s = user + ':' + pass
if (typeof btoa === 'function') return 'Basic ' + btoa(s)
if (typeof Buffer !== 'undefined')
return 'Basic ' + Buffer.from(s, 'utf8').toString('base64')
throw new Error('curl: cannot encode Basic auth (no btoa/Buffer)')
}
function looksLikeUrl(s) {
return (
/^https?:\/\//i.test(s) ||
s.startsWith('data:') ||
s.startsWith('file://')
)
}
function usage() {
return (
'usage: curl [options] URL...\n' +
'Bare OS curl is fetch-based, not libcurl. See man curl.'
)
}
function utf8Encode(str) {
return new TextEncoder().encode(str)
}
/** @param {(string | Uint8Array)[]} parts */
function concatParts(parts) {
let len = 0
for (const p of parts) {
len += typeof p === 'string' ? utf8Encode(p).length : p.length
}
const out = new Uint8Array(len)
let o = 0
for (const p of parts) {
const u8 = typeof p === 'string' ? utf8Encode(p) : p
out.set(u8, o)
o += u8.length
}
return out
}
function expandWriteOut(fmt, vars) {
return fmt.replace(/%\{([^}]+)\}/g, (_, key) => {
if (Object.prototype.hasOwnProperty.call(vars, key))
return String(vars[key])
return ''
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runCurlCli(ctx, argv) {
const vfs = ctx.vfs
const args = argv.slice(1)
let method = ''
const headers = []
/** @type {string[]} */
const dataChunks = []
let dataBinary = false
let headOnly = false
let includeHeaders = false
let silent = false
let showError = false
let failOnError = false
let location = false
let verbose = false
/** @type {string | null} */
let outputPath = null
/** @type {string | null} */
let writeOut = null
let maxTimeMs = 0
/** @type {string | null} */
let userColonPass = null
/** @type {string | null} */
let jsonBody = null
/** @type {string | null} */
let uploadPath = null
let i = 0
while (i < args.length) {
const a = args[i]
if (a === '--') {
i++
break
}
if (!a.startsWith('-') || a === '-') {
break
}
if (a === '-h' || a === '--help') {
ctx.console.log(usage())
return
}
if (a === '-V' || a === '--version') {
ctx.console.log('curl (Bare OS fetch subset) 0.1 — not libcurl')
return
}
if (a === '-X' || a === '--request') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
method = String(args[++i]).toUpperCase()
i++
continue
}
if (a.startsWith('-X') && a.length > 2) {
method = a.slice(2).toUpperCase()
i++
continue
}
if (a === '-H' || a === '--header') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
headers.push(String(args[++i]))
i++
continue
}
if (a.startsWith('-H') && a.length > 2) {
headers.push(a.slice(2))
i++
continue
}
if (
a === '-d' ||
a === '--data' ||
a === '--data-ascii' ||
a === '--data-binary' ||
a === '--data-raw'
) {
if (a === '--data-binary') dataBinary = true
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
dataChunks.push(String(args[++i]))
i++
continue
}
if (a.startsWith('-d') && a.length > 2) {
dataChunks.push(a.slice(2))
i++
continue
}
if (a.startsWith('--data=')) {
dataChunks.push(a.slice(7))
i++
continue
}
if (a === '--json') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: --json')
ctx.exitCode = 2
return
}
jsonBody = String(args[++i])
i++
continue
}
if (a === '-o' || a === '--output') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
outputPath = String(args[++i])
i++
continue
}
if (a === '-T' || a === '--upload-file') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
uploadPath = String(args[++i])
i++
continue
}
if (a === '-w' || a === '--write-out') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
writeOut = String(args[++i])
i++
continue
}
if (a === '-u' || a === '--user') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
userColonPass = String(args[++i])
i++
continue
}
if (a === '--max-time' || a === '-m') {
if (i + 1 >= args.length) {
ctx.console.error('curl: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
const sec = Number(args[++i])
if (!Number.isFinite(sec) || sec < 0) {
ctx.console.error('curl: invalid --max-time')
ctx.exitCode = 2
return
}
maxTimeMs = Math.round(sec * 1000)
i++
continue
}
if (a === '-I' || a === '--head') {
headOnly = true
i++
continue
}
if (a === '-i' || a === '--include') {
includeHeaders = true
i++
continue
}
if (a === '-s' || a === '--silent') {
silent = true
i++
continue
}
if (a === '-S' || a === '--show-error') {
showError = true
i++
continue
}
if (a === '-f' || a === '--fail') {
failOnError = true
i++
continue
}
if (a === '-L' || a === '--location') {
location = true
i++
continue
}
if (a === '-v' || a === '--verbose') {
verbose = true
i++
continue
}
if (a.startsWith('--')) {
ctx.console.error('curl: unknown option: ' + a)
ctx.exitCode = 2
return
}
const rest = a.slice(1)
for (let j = 0; j < rest.length; j++) {
const c = rest[j]
switch (c) {
case 'I':
headOnly = true
break
case 'i':
includeHeaders = true
break
case 's':
silent = true
break
case 'S':
showError = true
break
case 'f':
failOnError = true
break
case 'L':
location = true
break
case 'v':
verbose = true
break
default:
ctx.console.error('curl: invalid option -- ' + c)
ctx.exitCode = 2
return
}
}
i++
}
const urls = []
while (i < args.length) {
urls.push(String(args[i++]))
}
if (urls.length === 0) {
ctx.console.error(usage())
ctx.exitCode = 2
return
}
for (const u of urls) {
if (!looksLikeUrl(u)) {
ctx.console.error(
'curl: URL rejected (need http(s)://, data:, or file://): ' + u
)
ctx.exitCode = 2
return
}
}
let m = method
if (!m) {
if (headOnly) m = 'HEAD'
else if (uploadPath) m = 'PUT'
else if (dataChunks.length > 0 || jsonBody) m = 'POST'
else m = 'GET'
}
await ensureBareFetchForCurl()
const fetchFn =
typeof ctx.httpFetch === 'function'
? ctx.httpFetch
: typeof globalThis.fetch === 'function'
? globalThis.fetch.bind(globalThis)
: null
if (!fetchFn) {
ctx.console.error(
'curl: no HTTP client (need global fetch, e.g. Node 18+, or bare-fetch on Pear/Bare)'
)
ctx.exitCode = 1
return
}
const hdr = new Headers()
for (const line of headers) {
const colon = line.indexOf(':')
if (colon < 1) {
ctx.console.error('curl: malformed header: ' + line)
ctx.exitCode = 2
return
}
hdr.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim())
}
if (userColonPass) {
const colon = userColonPass.indexOf(':')
const user = colon >= 0 ? userColonPass.slice(0, colon) : userColonPass
const pass = colon >= 0 ? userColonPass.slice(colon + 1) : ''
hdr.set('Authorization', basicAuthHeader(user, pass))
}
/** @type {string | Uint8Array | undefined} */
let body = undefined
if (jsonBody) {
if (!hdr.has('Content-Type'))
hdr.set('Content-Type', 'application/json')
body = jsonBody
} else if (dataChunks.length > 0) {
const joined = dataChunks.join('&')
if (!hdr.has('Content-Type') && !dataBinary)
hdr.set('Content-Type', 'application/x-www-form-urlencoded')
body = joined
} else if (uploadPath) {
const buf = await vfs.readFile(uploadPath)
if (!buf) {
if (!silent || showError)
ctx.console.error('curl: cannot read upload file: ' + uploadPath)
ctx.exitCode = 26
return
}
body = new Uint8Array(buf)
if (!hdr.has('Content-Type')) hdr.set('Content-Type', 'application/octet-stream')
}
let lastSize = 0
let lastUrl = ''
for (let ui = 0; ui < urls.length; ui++) {
const url = urls[ui]
const outForUrl = urls.length > 1 && outputPath ? `${outputPath}.${ui}` : outputPath
const ac = maxTimeMs > 0 ? new AbortController() : null
const t =
maxTimeMs > 0
? setTimeout(() => {
try {
ac.abort()
} catch {
/* ignore */
}
}, maxTimeMs)
: null
let res
try {
res = await fetchFn(url, {
method: m,
headers: hdr,
body: m === 'HEAD' || m === 'GET' ? undefined : body,
redirect: location ? 'follow' : 'manual',
signal: ac ? ac.signal : undefined
})
} catch (e) {
if (t) clearTimeout(t)
const msg = e && e.message ? e.message : String(e)
if (!silent || showError) ctx.console.error('curl: (' + url + ') ' + msg)
ctx.exitCode = 7
return
}
if (t) clearTimeout(t)
lastUrl = res.url || url
if (!location && res.status >= 300 && res.status < 400) {
const loc = res.headers.get('Location')
if (loc && m === 'GET') {
if (!silent || showError)
ctx.console.error(
'curl: redirect not followed (use -L): ' + res.status + ' -> ' + loc
)
}
}
const buf =
m === 'HEAD' ? new Uint8Array(0) : new Uint8Array(await res.arrayBuffer())
lastSize = buf.length
if (failOnError && !res.ok) {
if (!silent || showError)
ctx.console.error('curl: HTTP ' + res.status + ' for ' + url)
ctx.exitCode = 22
return
}
if (verbose && !silent) {
ctx.console.error('> ' + m + ' ' + url)
res.headers.forEach((v, k) => {
ctx.console.error('< ' + k + ': ' + v)
})
}
const chunks = []
if (m === 'HEAD' || (includeHeaders && m !== 'HEAD')) {
const statusLine =
'HTTP/1.1 ' + res.status + ' ' + (res.statusText || '')
chunks.push(statusLine + '\r\n')
res.headers.forEach((v, k) => {
chunks.push(k + ': ' + v + '\r\n')
})
chunks.push('\r\n')
}
if (m !== 'HEAD') {
chunks.push(buf)
}
const outBytes = concatParts(chunks)
if (outForUrl) {
await vfs.writeFile(outForUrl, outBytes)
} else {
ctx.console.log(ctx.b4a.toString(outBytes))
}
if (writeOut) {
const line = expandWriteOut(writeOut, {
http_code: res.status,
url_effective: lastUrl,
size_download: lastSize,
num_redirects: 0
})
ctx.console.log(line)
}
}
ctx.exitCode = 0
}
@@ -172,6 +172,42 @@ export function releaseFishStdin(stdin) {
}
}
/**
* Temporarily release the TTY from fish (e.g. before spawning a fullscreen subprocess).
* Keeps the data handler registered in {@link fishStdinDataHandler} so
* {@link resumeFishStdinAfterSubprocess} can re-attach it.
* @param {import('stream').Readable | null | undefined} stdin
* @returns {boolean} true if fish had this stdin attached
*/
export function suspendFishStdinForSubprocess(stdin) {
if (!stdin || !fishStdinDataHandler.has(stdin)) return false
disableFishRawMode(stdin)
const h = fishStdinDataHandler.get(stdin)
stdin.removeListener('data', h)
return true
}
/**
* Restore fish raw mode and stdin listener after a subprocess exits.
* @param {import('stream').Readable | null | undefined} stdin
*/
export function resumeFishStdinAfterSubprocess(stdin) {
if (!stdin || !fishStdinDataHandler.has(stdin)) return false
const h = fishStdinDataHandler.get(stdin)
stdin.on('data', h)
try {
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
} catch {
/* ignore */
}
try {
if (typeof stdin.resume === 'function') stdin.resume()
} catch {
/* ignore */
}
return true
}
/** @type {WeakMap<import('stream').Readable, (chunk: Buffer | string) => void>} */
const fishStdinDataHandler = new WeakMap()
@@ -17,6 +17,22 @@ function shouldDelegateGit(cmd) {
return true
}
function shouldDelegateCurl(cmd) {
if (cmd === 'curl') return true
if (!cmd.includes('/')) return false
if (path.posix.basename(cmd) !== 'curl') return false
if (cmd.startsWith('./') || cmd.startsWith('../')) return false
return true
}
function shouldDelegateWget(cmd) {
if (cmd === 'wget') return true
if (!cmd.includes('/')) return false
if (path.posix.basename(cmd) !== 'wget') 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
@@ -83,6 +99,16 @@ export async function runBinCommand(ctx, argv) {
return runGitCli(ctx, argv)
}
if (shouldDelegateCurl(cmd)) {
const { runCurlCli } = await import('./curl-cli.js')
return runCurlCli(ctx, argv)
}
if (shouldDelegateWget(cmd)) {
const { runWgetCli } = await import('./wget-cli.js')
return runWgetCli(ctx, argv)
}
if (cmd.includes('/')) {
const abs = vfs.resolveLogical(cmd)
const { drive, path } = vfs.route(abs)
+3 -1
View File
@@ -177,6 +177,8 @@ export async function createKernelReplSession({
readLine,
console: outConsole,
cleanup,
fishActive: Boolean(fishRead)
fishActive: Boolean(fishRead),
/** Set when fish owns the TTY; used to suspend/resume around host subprocesses. */
fishStdin: fishRead ? stdin : null
}
}
+9 -6
View File
@@ -185,10 +185,7 @@ export async function loadBarerc(ctx, opts = {}) {
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))
) {
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)
@@ -472,7 +469,10 @@ export async function execShellLine(ctx, line) {
const origErr = ctx.console.error
for (const [k, val] of Object.entries(cmd.assign)) {
if (ctx.shellReadonlyVars instanceof Set && ctx.shellReadonlyVars.has(k)) {
if (
ctx.shellReadonlyVars instanceof Set &&
ctx.shellReadonlyVars.has(k)
) {
origErr.call(ctx.console, k + ': readonly variable')
continue
}
@@ -521,7 +521,10 @@ export async function execShellLine(ctx, line) {
const eq = a.indexOf('=')
if (eq > 0) {
const k = a.slice(0, eq)
if (ctx.shellReadonlyVars instanceof Set && ctx.shellReadonlyVars.has(k)) {
if (
ctx.shellReadonlyVars instanceof Set &&
ctx.shellReadonlyVars.has(k)
) {
origErr.call(ctx.console, k + ': readonly variable')
continue
}
+374
View File
@@ -0,0 +1,374 @@
/**
* Bare OS wget HTTP(S) download via Fetch (not GNU wget2 / libwget).
* @see https://www.gnu.org/software/wget/manual/ for UX inspiration; behavior is a small subset.
*/
import path from 'path'
/** @type {boolean} */
let bareFetchTried = false
async function ensureBareFetchForWget() {
if (typeof globalThis.fetch === 'function') return
if (bareFetchTried) return
bareFetchTried = true
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 optional on Node tests */
}
}
function looksLikeUrl(s) {
return (
/^https?:\/\//i.test(s) ||
s.startsWith('data:') ||
s.startsWith('file://')
)
}
function defaultLocalName(urlString) {
try {
const u = new URL(urlString)
let p = u.pathname || '/'
if (p.endsWith('/') || p === '/' || p === '') return 'index.html'
const parts = p.split('/').filter(Boolean)
const base = parts[parts.length - 1]
return base && base.length > 0 ? base : 'index.html'
} catch {
return 'index.html'
}
}
function usage() {
return (
'usage: wget [options] URL...\n' +
'Bare OS wget is fetch-based, not GNU wget2. See man wget.'
)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runWgetCli(ctx, argv) {
const vfs = ctx.vfs
const args = argv.slice(1)
let quiet = false
/** @type {string | null} */
let outputDocument = null
/** @type {string | null} */
let directoryPrefix = null
/** @type {string | null} */
let userAgent = null
let timeoutMs = 0
const extraHeaders = []
/** @type {string | null} */
let postData = null
/** @type {string | null} */
let postFile = null
let i = 0
while (i < args.length) {
const a = args[i]
if (a === '--') {
i++
break
}
if (!a.startsWith('-') || a === '-') break
if (a === '-h' || a === '--help') {
ctx.console.log(usage())
return
}
if (a === '-V' || a === '--version') {
ctx.console.log('wget (Bare OS fetch subset) 0.1 — not GNU wget2')
return
}
if (a === '-q' || a === '--quiet') {
quiet = true
i++
continue
}
if (a === '-O' || a === '--output-document') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
outputDocument = String(args[++i])
i++
continue
}
if (a.startsWith('--output-document=')) {
outputDocument = a.slice('--output-document='.length)
i++
continue
}
if (a === '-P' || a === '--directory-prefix') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
directoryPrefix = String(args[++i])
i++
continue
}
if (a.startsWith('--directory-prefix=')) {
directoryPrefix = a.slice('--directory-prefix='.length)
i++
continue
}
if (a === '-U' || a === '--user-agent') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
userAgent = String(args[++i])
i++
continue
}
if (a.startsWith('--user-agent=')) {
userAgent = a.slice('--user-agent='.length)
i++
continue
}
if (a === '-T' || a === '--timeout') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: ' + a)
ctx.exitCode = 2
return
}
const sec = Number(args[++i])
if (!Number.isFinite(sec) || sec < 0) {
ctx.console.error('wget: invalid timeout')
ctx.exitCode = 2
return
}
timeoutMs = Math.round(sec * 1000)
i++
continue
}
if (a.startsWith('--timeout=')) {
const sec = Number(a.slice('--timeout='.length))
if (!Number.isFinite(sec) || sec < 0) {
ctx.console.error('wget: invalid timeout')
ctx.exitCode = 2
return
}
timeoutMs = Math.round(sec * 1000)
i++
continue
}
if (a === '--header') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: --header')
ctx.exitCode = 2
return
}
extraHeaders.push(String(args[++i]))
i++
continue
}
if (a.startsWith('--header=')) {
extraHeaders.push(a.slice('--header='.length))
i++
continue
}
if (a === '--post-data') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: --post-data')
ctx.exitCode = 2
return
}
postData = String(args[++i])
i++
continue
}
if (a.startsWith('--post-data=')) {
postData = a.slice('--post-data='.length)
i++
continue
}
if (a === '--post-file') {
if (i + 1 >= args.length) {
ctx.console.error('wget: option requires an argument: --post-file')
ctx.exitCode = 2
return
}
postFile = String(args[++i])
i++
continue
}
if (a.startsWith('--')) {
ctx.console.error('wget: unknown option: ' + a)
ctx.exitCode = 2
return
}
ctx.console.error('wget: invalid option: ' + a)
ctx.exitCode = 2
return
}
const urls = []
while (i < args.length) {
urls.push(String(args[i++]))
}
if (urls.length === 0) {
ctx.console.error(usage())
ctx.exitCode = 2
return
}
for (const u of urls) {
if (!looksLikeUrl(u)) {
ctx.console.error(
'wget: URL rejected (need http(s)://, data:, or file://): ' + u
)
ctx.exitCode = 2
return
}
}
if (urls.length > 1 && outputDocument != null) {
ctx.console.error(
'wget: cannot use -O/--output-document with multiple URLs'
)
ctx.exitCode = 2
return
}
if (outputDocument && directoryPrefix) {
ctx.console.error('wget: cannot combine -O and -P')
ctx.exitCode = 2
return
}
await ensureBareFetchForWget()
const fetchFn =
typeof ctx.httpFetch === 'function'
? ctx.httpFetch
: typeof globalThis.fetch === 'function'
? globalThis.fetch.bind(globalThis)
: null
if (!fetchFn) {
ctx.console.error(
'wget: no HTTP client (need global fetch or bare-fetch on Pear/Bare)'
)
ctx.exitCode = 4
return
}
let body = undefined
if (postFile) {
const buf = await vfs.readFile(postFile)
if (!buf) {
if (!quiet) ctx.console.error('wget: cannot read --post-file: ' + postFile)
ctx.exitCode = 3
return
}
body = new Uint8Array(buf)
} else if (postData != null) {
body = postData
}
const method = body !== undefined ? 'POST' : 'GET'
const hdr = new Headers()
for (const line of extraHeaders) {
const colon = line.indexOf(':')
if (colon < 1) {
ctx.console.error('wget: malformed header: ' + line)
ctx.exitCode = 2
return
}
hdr.set(line.slice(0, colon).trim(), line.slice(colon + 1).trim())
}
if (userAgent) hdr.set('User-Agent', userAgent)
if (body !== undefined && !hdr.has('Content-Type'))
hdr.set('Content-Type', 'application/x-www-form-urlencoded')
for (let ui = 0; ui < urls.length; ui++) {
const url = urls[ui]
const ac = timeoutMs > 0 ? new AbortController() : null
const t =
timeoutMs > 0
? setTimeout(() => {
try {
ac.abort()
} catch {
/* ignore */
}
}, timeoutMs)
: null
let res
try {
res = await fetchFn(url, {
method,
headers: hdr,
body: method === 'GET' ? undefined : body,
signal: ac ? ac.signal : undefined
})
} catch (e) {
if (t) clearTimeout(t)
const msg = e && e.message ? e.message : String(e)
if (!quiet) ctx.console.error('wget: ' + msg)
ctx.exitCode = 4
return
}
if (t) clearTimeout(t)
if (!res.ok) {
if (!quiet)
ctx.console.error('wget: HTTP error ' + res.status + ' for ' + url)
ctx.exitCode = 8
return
}
const buf = new Uint8Array(await res.arrayBuffer())
if (outputDocument === '-') {
ctx.console.log(ctx.b4a.toString(buf))
} else if (outputDocument != null) {
await vfs.writeFile(outputDocument, buf)
if (!quiet) ctx.console.error(outputDocument)
} else {
const name = defaultLocalName(url)
const dir = directoryPrefix || '.'
const outPath = path.posix.join(dir.replace(/\/$/, '') || '.', name)
await vfs.writeFile(outPath, buf)
if (!quiet) ctx.console.error(outPath)
}
}
ctx.exitCode = 0
}
+1
View File
@@ -0,0 +1 @@
t.sNNMMNN
+122 -12
View File
@@ -271,7 +271,11 @@ test('ls -l long listing uses session user and regular file mode', async (t) =>
await personal.ready()
await drive.put('/bin/ls', b4a.from(lsSrc))
const lines = []
const ctx = testCtx(drive, personal, { USER: 'carol', UID: '9001', GID: '9001' })
const ctx = testCtx(drive, personal, {
USER: 'carol',
UID: '9001',
GID: '9001'
})
ctx.console = {
log: (s) => lines.push(String(s)),
error: (...a) => lines.push(a.join(' '))
@@ -441,6 +445,18 @@ test('expandArgvAliases expands first word and keeps trailing argv', async (t) =
])
})
test('expandArgvAliases leaves sed unchanged', async (t) => {
t.alike(expandArgvAliases(['sed', 's/a/b/', 'x.txt'], defaultShellAliases()), [
'sed',
's/a/b/',
'x.txt'
])
})
test('defaultShellAliases does not remap sed', async (t) => {
t.is(defaultShellAliases().sed, undefined)
})
test('expandArgvAliases throws on cyclic alias chain', async (t) => {
const cyclic = { a: 'b', b: 'a' }
t.exception(
@@ -724,6 +740,95 @@ test('tier-1 jq from system drive', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('curl delegated from booter with stub fetch', async (t) => {
const dir = testCorestoreDir('curl')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pcurl'))
await drive.ready()
await personal.ready()
const lines = []
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error(s) {
lines.push('e:' + String(s))
}
}
ctx.httpFetch = async () =>
new Response('hello', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
await runBinCommand(ctx, ['curl', 'https://stub.example/x'])
t.is(ctx.exitCode, 0)
t.is(lines[0], 'hello')
lines.length = 0
ctx.exitCode = 0
ctx.httpFetch = async () => new Response('', { status: 404 })
await runBinCommand(ctx, ['curl', '-f', '-s', 'https://stub.example/missing'])
t.is(ctx.exitCode, 22)
await store.close()
rmSync(dir, { recursive: true, force: true })
})
test('wget delegated from booter with stub fetch', async (t) => {
const dir = testCorestoreDir('wget')
const store = new Corestore(dir)
const drive = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pwget'))
await drive.ready()
await personal.ready()
const lines = []
const ctx = testCtx(drive, personal)
ctx.exitCode = 0
ctx.console = {
log(s) {
lines.push(String(s))
},
error(s) {
lines.push('e:' + String(s))
}
}
ctx.httpFetch = async () =>
new Response('payload', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
await runBinCommand(ctx, [
'wget',
'-O',
'saved.txt',
'-q',
'https://stub.example/a'
])
t.is(ctx.exitCode, 0)
const out = await ctx.vfs.readFile('saved.txt')
t.ok(out)
t.is(ctx.b4a.toString(out), 'payload')
lines.length = 0
ctx.exitCode = 0
ctx.httpFetch = async () => new Response('', { status: 500 })
await runBinCommand(ctx, ['wget', '-q', 'https://stub.example/err'])
t.is(ctx.exitCode, 8)
lines.length = 0
ctx.exitCode = 0
ctx.httpFetch = async () => new Response('stdout-body', { status: 200 })
await runBinCommand(ctx, ['wget', '-O', '-', '-q', 'https://stub.example/b'])
t.is(ctx.exitCode, 0)
t.is(lines[0], 'stdout-body')
await store.close()
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)
@@ -775,7 +880,9 @@ test('runBinCommand delegates git to booter (ignores /bin/git script body)', asy
await personal.ready()
await drive.put(
'/bin/git',
b4a.from(`async function run() { throw new Error('eval git should not run') }`)
b4a.from(
`async function run() { throw new Error('eval git should not run') }`
)
)
const logs = []
const ctx = testCtx(drive, personal)
@@ -838,19 +945,19 @@ test('runGitCli init and status on personal drive', async (t) => {
test('kernel share/man/man.json page count matches coreutils + extras + handbook + devguide', async (t) => {
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
const raw = JSON.parse(await readFile(manPath, 'utf8'))
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } = await import(
'../bare-os-coreutils/lib/commands.mjs'
)
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } =
await import('../bare-os-coreutils/lib/commands.mjs')
const handbookDir = path.join(__dirname, '../../handbook')
const devguideDir = path.join(__dirname, '../../developer-guide')
const handbookMd = (await readdir(handbookDir)).filter((f) => f.endsWith('.md')).length
const devguideMd = (await readdir(devguideDir)).filter((f) => f.endsWith('.md')).length
const handbookMd = (await readdir(handbookDir)).filter((f) =>
f.endsWith('.md')
).length
const devguideMd = (await readdir(devguideDir)).filter((f) =>
f.endsWith('.md')
).length
t.is(
raw.pages.length,
COREUTILS_COMMANDS.length +
MAN_EXTRA_PAGES.length +
handbookMd +
devguideMd
COREUTILS_COMMANDS.length + MAN_EXTRA_PAGES.length + handbookMd + devguideMd
)
t.ok(Array.isArray(raw.apropos) && raw.apropos.length > 0)
t.is(typeof raw.index.ls, 'number')
@@ -872,7 +979,10 @@ test('runBinCommand man ls prints manual text', async (t) => {
await drive.ready()
await personal.ready()
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
await drive.put('/share/man/man.json', b4a.from(await readFile(manPath, 'utf8')))
await drive.put(
'/share/man/man.json',
b4a.from(await readFile(manPath, 'utf8'))
)
await drive.put('/bin/man', b4a.from(await readBuiltBin('man')))
const logs = []
const ctx = testCtx(drive, personal)
+15
View File
@@ -0,0 +1,15 @@
h
pppThis is a test
+1
View File
@@ -0,0 +1 @@
o
+3
View File
@@ -0,0 +1,3 @@
dddNNNNMMM
+6 -1
View File
@@ -70,4 +70,9 @@ export const COREUTILS_COMMANDS = [
]
/** Extra manual pages not built as /bin scripts on the system drive. */
export const MAN_EXTRA_PAGES = ['git', 'bare-os-shell']
export const MAN_EXTRA_PAGES = [
'bare-os-shell',
'curl',
'git',
'wget'
]
@@ -0,0 +1,54 @@
{
"name": "curl",
"section": 1,
"title": "transfer a URL (Fetch-based client, not libcurl)",
"synopsis": [
"curl [options] URL...",
"curl uses Fetch in the booter (Node fetch or bare-fetch), not libcurl."
],
"description": "HTTP/HTTPS client delegated from the booter. Subset of curl(1) flags; see man page JSON for full options and exit codes.",
"options": [
{
"flag": "-X, --request METHOD",
"meaning": "HTTP method"
},
{
"flag": "-H, --header LINE",
"meaning": "Request header (repeatable)"
},
{
"flag": "-d, --data / --json",
"meaning": "Request body"
},
{
"flag": "-o, --output FILE",
"meaning": "Write response to VFS path"
},
{
"flag": "-T, --upload-file PATH",
"meaning": "PUT file from VFS"
},
{
"flag": "-I / -i / -L / -f / -s / -S / -v / -u / -m / -w",
"meaning": "See full man curl.json"
}
],
"keywords": [
"curl",
"http",
"https",
"fetch",
"download"
],
"bareOsNotes": "Not https://curl.se libcurl; booter curl-cli.js. URLs: http(s), data:, file://.",
"seeAlso": [
{
"name": "git",
"section": 1
},
{
"name": "jq",
"section": 1
}
]
}
@@ -0,0 +1,98 @@
{
"name": "wget",
"section": 1,
"title": "non-interactive network download (Fetch-based, not GNU wget2)",
"synopsis": [
"wget [options] URL...",
"wget is implemented in the booter with the Fetch API (Node fetch or bare-fetch), not the C GNU wget2 tree."
],
"description": "Downloads resources over HTTP or HTTPS into the VFS. Bare OS does not ship GNU wget or wget2 (C); this command is a small compatibility-oriented subset built on JavaScript fetch. It is delegated from the booter (like curl and git), not loaded from a /bin script on the system drive.",
"options": [
{
"flag": "-O, --output-document FILE",
"meaning": "Write the body to FILE; use - for stdout. Only one URL allowed."
},
{
"flag": "-P, --directory-prefix DIR",
"meaning": "Save under DIR using a name derived from the URL (last path segment, or index.html if the path ends with /)"
},
{
"flag": "-q, --quiet",
"meaning": "Suppress non-error messages on stderr (saved path lines)"
},
{
"flag": "-U, --user-agent STRING",
"meaning": "Set User-Agent request header"
},
{
"flag": "-T, --timeout SECONDS",
"meaning": "Abort the request after SECONDS (AbortController)"
},
{
"flag": "--header LINE",
"meaning": "Extra header Name: value (repeatable)"
},
{
"flag": "--post-data STRING",
"meaning": "POST body (sets method POST; default Content-Type application/x-www-form-urlencoded)"
},
{
"flag": "--post-file PATH",
"meaning": "POST body read from a file on the VFS"
},
{
"flag": "-V, --version",
"meaning": "Print Bare OS wget version string"
},
{
"flag": "-h, --help",
"meaning": "Short usage"
}
],
"environment": [],
"keywords": [
"wget",
"download",
"http",
"https",
"fetch",
"mirror"
],
"bareOsNotes": "Not GNU wget2 (https://gitlab.com/gnuwget/wget2). URLs must start with http://, https://, data:, or file://. No recursive retrieval, FTP, or WARC. Cannot combine -O and -P. On Pear/Bare, use bare-fetch for global fetch. Tests may set ctx.httpFetch.",
"seeAlso": [
{
"name": "curl",
"section": 1
},
{
"name": "git",
"section": 1
}
],
"examples": [
{
"caption": "save with default name in cwd",
"code": "wget https://example.com/README"
},
{
"caption": "choose output path",
"code": "wget -O ~/page.html https://example.com/"
},
{
"caption": "directory prefix",
"code": "wget -P ~/dl https://example.com/a/b.bin"
},
{
"caption": "stdout",
"code": "wget -O - -q https://example.com/robots.txt"
}
],
"exitStatus": [
"0 — success",
"1 — generic error (reserved)",
"2 — bad usage or options",
"3 — file I/O error (e.g. --post-file unreadable)",
"4 — network failure or no fetch implementation",
"8 — HTTP 4xx/5xx response"
]
}
@@ -6,10 +6,7 @@ import { mkdir, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
import { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } from '../lib/commands.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pagesDir = join(__dirname, '../man/pages')
@@ -92,8 +89,14 @@ const EXAMPLES = {}
EXAMPLES.awk = [
{ caption: 'print column 1', code: "awk '{print $1}' file.txt" },
{ caption: 'field separator', code: "awk -F: '{print $1}' /etc/passwd" },
{ caption: 'sum numbers in first column', code: "awk '{s+=$1} END{print s}' nums.txt" },
{ caption: 'lines matching /re/', code: "awk '/error/{print NR\": \"$0}' log.txt" }
{
caption: 'sum numbers in first column',
code: "awk '{s+=$1} END{print s}' nums.txt"
},
{
caption: 'lines matching /re/',
code: 'awk \'/error/{print NR": "$0}\' log.txt'
}
]
EXAMPLES.basename = [
{ caption: 'strip directory', code: 'basename /home/user/docs/readme.md' },
@@ -101,7 +104,10 @@ EXAMPLES.basename = [
]
EXAMPLES.cat = [
{ caption: 'stdout several files', code: 'cat a.txt b.txt' },
{ caption: 'number lines (use nl)', code: 'cat -n file.txt # if supported; else nl file' },
{
caption: 'number lines (use nl)',
code: 'cat -n file.txt # if supported; else nl file'
},
{ caption: 'here-string via echo pipe', code: 'echo hello | cat' }
]
EXAMPLES.chgrp = [
@@ -112,7 +118,10 @@ EXAMPLES.chgrp = [
]
EXAMPLES.chmod = [
{ caption: 'octal', code: 'chmod 644 ~/.profile' },
{ caption: 'recursive-ish (run find + chmod per file)', code: 'find . -type f -name "*.sh" -print' },
{
caption: 'recursive-ish (run find + chmod per file)',
code: 'find . -type f -name "*.sh" -print'
},
{ caption: 'symbolic user bits', code: 'chmod u+x script.sh' },
{ caption: 'all read, owner write', code: 'chmod a+r,u+w shared.txt' }
]
@@ -123,9 +132,7 @@ EXAMPLES.cksum = [
{ caption: 'checksum file', code: 'cksum iso.img' },
{ caption: 'verify pipeline', code: 'cat f | cksum' }
]
EXAMPLES.clear = [
{ caption: 'wipe screen', code: 'clear' }
]
EXAMPLES.clear = [{ caption: 'wipe screen', code: 'clear' }]
EXAMPLES.cp = [
{ caption: 'copy file', code: 'cp src.txt dest.txt' },
{ caption: 'into directory', code: 'cp a b c ~/backup/' },
@@ -137,7 +144,7 @@ EXAMPLES.crontab = [
{ caption: 'remove all', code: 'crontab -r' }
]
EXAMPLES.cut = [
{ caption: 'fields by delimiter', code: "cut -d: -f1,3 /etc/passwd" },
{ caption: 'fields by delimiter', code: 'cut -d: -f1,3 /etc/passwd' },
{ caption: 'characters', code: 'cut -c1-16 file.txt' }
]
EXAMPLES.date = [
@@ -146,7 +153,10 @@ EXAMPLES.date = [
]
EXAMPLES.dirname = [
{ caption: 'parent path', code: 'dirname /a/b/c.txt' },
{ caption: 'compose with basename', code: 'p=/x/y/z; echo $(dirname $p)/$(basename $p)' }
{
caption: 'compose with basename',
code: 'p=/x/y/z; echo $(dirname $p)/$(basename $p)'
}
]
EXAMPLES.du = [
{ caption: 'sizes under cwd', code: 'du .' },
@@ -198,27 +208,24 @@ EXAMPLES.help = [
{ caption: 'quick index', code: 'help' },
{ caption: 'then deep dive', code: 'man grep' }
]
EXAMPLES.hostname = [
{ caption: 'show host', code: 'hostname' }
]
EXAMPLES.id = [
{ caption: 'who am I numerically', code: 'id' }
]
EXAMPLES.hostname = [{ caption: 'show host', code: 'hostname' }]
EXAMPLES.id = [{ caption: 'who am I numerically', code: 'id' }]
EXAMPLES.ln = [
{ caption: 'symlink', code: 'ln -s target name' },
{ caption: 'hard link (if supported)', code: 'ln file linkname' }
]
EXAMPLES.login = [
{ caption: 'unlock existing identity', code: 'login my passphrase words here' },
{
caption: 'unlock existing identity',
code: 'login my passphrase words here'
},
{ caption: 'register new', code: 'login --new first time passphrase' }
]
EXAMPLES.logout = [
{ caption: 'end session', code: 'logout' },
{ caption: 'save vault hint', code: 'logout --save' }
]
EXAMPLES.logname = [
{ caption: 'login name', code: 'logname' }
]
EXAMPLES.logname = [{ caption: 'login name', code: 'logname' }]
EXAMPLES.ls = [
{ caption: 'long + hidden', code: 'ls -la ~' },
{ caption: 'one per line', code: 'ls -1 /bin | head' },
@@ -227,7 +234,10 @@ EXAMPLES.ls = [
EXAMPLES.man = [
{ caption: 'open page', code: 'man sed' },
{ caption: 'handbook TOC (section 7)', code: 'man handbook' },
{ caption: 'handbook chapter by section', code: 'man 7 handbook-01-introduction' },
{
caption: 'handbook chapter by section',
code: 'man 7 handbook-01-introduction'
},
{ caption: 'apropos', code: 'man -k copy' },
{ caption: 'whatis', code: 'man -f grep' },
{ caption: 'all pages', code: 'man -l' },
@@ -244,12 +254,8 @@ EXAMPLES.mv = [
{ caption: 'rename', code: 'mv old.txt new.txt' },
{ caption: 'into dir', code: 'mv *.txt ~/inbox/' }
]
EXAMPLES.nl = [
{ caption: 'number all lines', code: 'nl README.md' }
]
EXAMPLES.od = [
{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }
]
EXAMPLES.nl = [{ caption: 'number all lines', code: 'nl README.md' }]
EXAMPLES.od = [{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }]
EXAMPLES.pathchk = [
{ caption: 'portable path check', code: 'pathchk -p "$HOME/file name"' }
]
@@ -261,19 +267,13 @@ EXAMPLES.printf = [
{ caption: 'format', code: 'printf "hex=%x dec=%d\\n" 255 255' },
{ caption: 'no newline', code: 'printf "%s" OK' }
]
EXAMPLES.pwd = [
{ caption: 'where am I', code: 'pwd' }
]
EXAMPLES.readlink = [
{ caption: 'symlink target', code: 'readlink ~/.config' }
]
EXAMPLES.pwd = [{ caption: 'where am I', code: 'pwd' }]
EXAMPLES.readlink = [{ caption: 'symlink target', code: 'readlink ~/.config' }]
EXAMPLES.rm = [
{ caption: 'file', code: 'rm tmp.txt' },
{ caption: 'tree', code: 'rm -rf build/' }
]
EXAMPLES.rmdir = [
{ caption: 'empty dir', code: 'rmdir olddir' }
]
EXAMPLES.rmdir = [{ caption: 'empty dir', code: 'rmdir olddir' }]
EXAMPLES.savevault = [
{ caption: 'snapshot encrypted vault', code: 'savevault' }
]
@@ -288,17 +288,13 @@ EXAMPLES.seq = [
{ caption: '1..10', code: 'seq 1 10' },
{ caption: 'step', code: 'seq 0 2 20' }
]
EXAMPLES.sleep = [
{ caption: 'pause seconds', code: 'sleep 2' }
]
EXAMPLES.sleep = [{ caption: 'pause seconds', code: 'sleep 2' }]
EXAMPLES.sort = [
{ caption: 'lexicographic', code: 'sort names.txt' },
{ caption: 'numeric', code: 'sort -n scores.txt' },
{ caption: 'unique', code: 'sort -u tags.txt' }
]
EXAMPLES.stat = [
{ caption: 'metadata', code: 'stat ~/README.md' }
]
EXAMPLES.stat = [{ caption: 'metadata', code: 'stat ~/README.md' }]
EXAMPLES.tail = [
{ caption: 'last lines', code: 'tail -n 20 app.log' },
{ caption: 'follow vibe (Bare: poll manually)', code: 'tail error.log' }
@@ -311,9 +307,7 @@ EXAMPLES.test = [
{ caption: 'directory', code: 'test -d /home/user' },
{ caption: 'string equal', code: 'test "$USER" = guest' }
]
EXAMPLES.time = [
{ caption: 'wall time a command', code: 'time sort big.txt' }
]
EXAMPLES.time = [{ caption: 'wall time a command', code: 'time sort big.txt' }]
EXAMPLES.touch = [
{ caption: 'create empty', code: 'touch newfile' },
{ caption: 'refresh mtime', code: 'touch -c existing' }
@@ -322,27 +316,20 @@ EXAMPLES.tr = [
{ caption: 'uppercase', code: "echo hi | tr 'a-z' 'A-Z'" },
{ caption: 'delete chars', code: "tr -d '\\r' < win.txt" }
]
EXAMPLES.true = [
{ caption: 'always success', code: 'true && echo ok' }
]
EXAMPLES.tty = [
{ caption: 'am I a tty', code: 'tty' }
]
EXAMPLES.uname = [
{ caption: 'kernel-ish info', code: 'uname -a' }
]
EXAMPLES.true = [{ caption: 'always success', code: 'true && echo ok' }]
EXAMPLES.tty = [{ caption: 'am I a tty', code: 'tty' }]
EXAMPLES.uname = [{ caption: 'kernel-ish info', code: 'uname -a' }]
EXAMPLES.wc = [
{ caption: 'lines words bytes', code: 'wc README.md' },
{ caption: 'stdin only', code: 'cat f | wc -l' }
]
EXAMPLES.which = [
{ caption: 'resolve on PATH', code: 'which ls' }
]
EXAMPLES.whoami = [
{ caption: 'effective user', code: 'whoami' }
]
EXAMPLES.which = [{ caption: 'resolve on PATH', code: 'which ls' }]
EXAMPLES.whoami = [{ caption: 'effective user', code: 'whoami' }]
EXAMPLES.xargs = [
{ caption: 'workaround: shell word split', code: '# for f in *.txt; do grep -l foo $f; done' }
{
caption: 'workaround: shell word split',
code: '# for f in *.txt; do grep -l foo $f; done'
}
]
/** @type {Record<string, Record<string, unknown>>} */
@@ -376,7 +363,10 @@ EXTRA.mkfifo = {
bareOsNotes: 'Documented stub; no real pipes as kernel objects.'
}
EXTRA.chmod = {
synopsis: ['chmod MODE FILE...', 'MODE is octal (e.g. 644) or symbolic (e.g. u+rw)'],
synopsis: [
'chmod MODE FILE...',
'MODE is octal (e.g. 644) or symbolic (e.g. u+rw)'
],
description:
'Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.',
options: [],
@@ -391,7 +381,10 @@ EXTRA.grep = {
description:
'Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.',
options: [
{ flag: '-E', meaning: 'Extended regex (accepted; patterns use JS RegExp)' },
{
flag: '-E',
meaning: 'Extended regex (accepted; patterns use JS RegExp)'
},
{ flag: '-F', meaning: 'Fixed string match' },
{ flag: '-i', meaning: 'Ignore case' },
{ flag: '-v', meaning: 'Invert match' },
@@ -451,7 +444,10 @@ EXTRA.man = {
description:
'Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.',
options: [
{ flag: '-k, --apropos', meaning: 'Search keywords and titles (substring)' },
{
flag: '-k, --apropos',
meaning: 'Search keywords and titles (substring)'
},
{ flag: '-f, --whatis', meaning: 'One-line description for exact name' },
{
flag: '-l, --list',
@@ -459,8 +455,20 @@ EXTRA.man = {
'List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically'
}
],
environment: ['MANWIDTH — wrap width (default 72, min 40)', 'NO_COLOR — disable bold headings on TTY'],
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
environment: [
'MANWIDTH — wrap width (default 72, min 40)',
'NO_COLOR — disable bold headings on TTY'
],
keywords: [
'man',
'manual',
'help',
'documentation',
'apropos',
'whatis',
'cheat',
'examples'
],
seeAlso: [
{ name: 'help', section: 1 },
{ name: 'bare-os-handbook', section: 7 },
@@ -505,12 +513,25 @@ EXTRA.jq = {
description:
'Runs a jq filter program against JSON values. The engine is vendored jqjs (pure JavaScript), not the C implementation at https://github.com/jqlang/jq — language coverage and edge cases differ.',
options: [
{ flag: '-n, --null-input', meaning: 'Use null as the sole input (ignore file/stdin for input)' },
{ flag: '-R, --raw-input', meaning: 'Treat each line as a string instead of JSON' },
{ flag: '-s, --slurp', meaning: 'Read all inputs into one array; run the filter once' },
{
flag: '-n, --null-input',
meaning: 'Use null as the sole input (ignore file/stdin for input)'
},
{
flag: '-R, --raw-input',
meaning: 'Treat each line as a string instead of JSON'
},
{
flag: '-s, --slurp',
meaning: 'Read all inputs into one array; run the filter once'
},
{ flag: '-c, --compact-output', meaning: 'Compact JSON on output' },
{ flag: '-r, --raw-output', meaning: 'Print strings without JSON quotes' },
{ flag: '-e, --exit-status', meaning: 'Set exit status from outputs (no output → 4; last false/null → 1)' },
{
flag: '-e, --exit-status',
meaning:
'Set exit status from outputs (no output → 4; last false/null → 1)'
},
{ flag: '-f, --from-file', meaning: 'Read filter program from file' }
],
keywords: ['jq', 'json', 'query', 'filter', 'jqjs'],
@@ -523,8 +544,8 @@ EXTRA.jq = {
examples: [
{ caption: 'pretty-print', code: 'jq . data.json' },
{ caption: 'field', code: 'jq .version package.json' },
{ caption: 'slurp array', code: 'jq -s \'map(.x) | add\' parts.jsonl' },
{ caption: 'compact', code: 'jq -c \'.[] | select(.ok)\' items.json' }
{ caption: 'slurp array', code: "jq -s 'map(.x) | add' parts.jsonl" },
{ caption: 'compact', code: "jq -c '.[] | select(.ok)' items.json" }
]
}
EXTRA.login = {
@@ -539,7 +560,8 @@ EXTRA.logout = {
seeAlso: [{ name: 'login', section: 1 }]
}
EXTRA.savevault = {
description: 'Encrypts a copy of the personal drive under /.bare/vault/ when identity services are available.',
description:
'Encrypts a copy of the personal drive under /.bare/vault/ when identity services are available.',
keywords: ['savevault', 'vault', 'encrypt', 'backup'],
seeAlso: [{ name: 'login', section: 1 }]
}
@@ -573,26 +595,154 @@ function gitPage() {
synopsis: ['git [-C dir] <subcommand> [ARGUMENTS...]'],
description:
'Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.',
options: [
{ flag: '-C dir', meaning: 'Run as if git was started in dir' }
],
options: [{ flag: '-C dir', meaning: 'Run as if git was started in dir' }],
environment: [
'BARE_OS_GIT_HTTP — optional fetch implementation for remotes',
'GIT_* — standard hints where supported'
],
keywords: ['git', 'version control', 'repository', 'clone', 'commit', 'isomorphic-git'],
bareOsNotes: 'Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.',
keywords: [
'git',
'version control',
'repository',
'clone',
'commit',
'isomorphic-git'
],
bareOsNotes:
'Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.',
seeAlso: [{ name: 'bare-os-shell', section: 1 }],
examples: [
{ caption: 'new repo', code: 'git init -C ~/myrepo' },
{ caption: 'status', code: 'git -C ~/myrepo status' },
{ caption: 'clone over HTTP (needs remote + fetch)', code: 'git clone https://example.com/repo.git ~/work/repo' },
{ caption: 'config local', code: 'git -C ~/myrepo config user.email "[email protected]"' },
{
caption: 'clone over HTTP (needs remote + fetch)',
code: 'git clone https://example.com/repo.git ~/work/repo'
},
{
caption: 'config local',
code: 'git -C ~/myrepo config user.email "[email protected]"'
},
{ caption: 'log one line', code: 'git -C ~/myrepo log --oneline -5' }
]
}
}
function curlPage() {
return {
name: 'curl',
section: 1,
title: 'transfer a URL (Fetch-based client, not libcurl)',
synopsis: [
'curl [options] URL...',
'curl uses Fetch in the booter (Node fetch or bare-fetch), not libcurl.'
],
description:
'HTTP/HTTPS client delegated from the booter. Subset of curl(1) flags; see man page JSON for full options and exit codes.',
options: [
{ flag: '-X, --request METHOD', meaning: 'HTTP method' },
{ flag: '-H, --header LINE', meaning: 'Request header (repeatable)' },
{ flag: '-d, --data / --json', meaning: 'Request body' },
{ flag: '-o, --output FILE', meaning: 'Write response to VFS path' },
{ flag: '-T, --upload-file PATH', meaning: 'PUT file from VFS' },
{
flag: '-I / -i / -L / -f / -s / -S / -v / -u / -m / -w',
meaning: 'See full man curl.json'
}
],
keywords: ['curl', 'http', 'https', 'fetch', 'download'],
bareOsNotes:
'Not https://curl.se libcurl; booter curl-cli.js. URLs: http(s), data:, file://.',
seeAlso: [
{ name: 'git', section: 1 },
{ name: 'jq', section: 1 }
]
}
}
function wgetPage() {
return {
name: 'wget',
section: 1,
title: 'non-interactive network download (Fetch-based, not GNU wget2)',
synopsis: [
'wget [options] URL...',
'wget is implemented in the booter with the Fetch API (Node fetch or bare-fetch), not the C GNU wget2 tree.'
],
description:
'Downloads resources over HTTP or HTTPS into the VFS. Bare OS does not ship GNU wget or wget2 (C); this command is a small compatibility-oriented subset built on JavaScript fetch. It is delegated from the booter (like curl and git), not loaded from a /bin script on the system drive.',
options: [
{
flag: '-O, --output-document FILE',
meaning:
'Write the body to FILE; use - for stdout. Only one URL allowed.'
},
{
flag: '-P, --directory-prefix DIR',
meaning:
'Save under DIR using a name derived from the URL (last path segment, or index.html if the path ends with /)'
},
{
flag: '-q, --quiet',
meaning: 'Suppress non-error messages on stderr (saved path lines)'
},
{
flag: '-U, --user-agent STRING',
meaning: 'Set User-Agent request header'
},
{
flag: '-T, --timeout SECONDS',
meaning: 'Abort the request after SECONDS (AbortController)'
},
{
flag: '--header LINE',
meaning: 'Extra header Name: value (repeatable)'
},
{
flag: '--post-data STRING',
meaning:
'POST body (sets method POST; default Content-Type application/x-www-form-urlencoded)'
},
{
flag: '--post-file PATH',
meaning: 'POST body read from a file on the VFS'
},
{ flag: '-V, --version', meaning: 'Print Bare OS wget version string' },
{ flag: '-h, --help', meaning: 'Short usage' }
],
environment: [],
keywords: ['wget', 'download', 'http', 'https', 'fetch', 'mirror'],
bareOsNotes:
'Not GNU wget2 (https://gitlab.com/gnuwget/wget2). URLs must start with http://, https://, data:, or file://. No recursive retrieval, FTP, or WARC. Cannot combine -O and -P. On Pear/Bare, use bare-fetch for global fetch. Tests may set ctx.httpFetch.',
seeAlso: [
{ name: 'curl', section: 1 },
{ name: 'git', section: 1 }
],
examples: [
{
caption: 'save with default name in cwd',
code: 'wget https://example.com/README'
},
{
caption: 'choose output path',
code: 'wget -O ~/page.html https://example.com/'
},
{
caption: 'directory prefix',
code: 'wget -P ~/dl https://example.com/a/b.bin'
},
{ caption: 'stdout', code: 'wget -O - -q https://example.com/robots.txt' }
],
exitStatus: [
'0 — success',
'1 — generic error (reserved)',
'2 — bad usage or options',
'3 — file I/O error (e.g. --post-file unreadable)',
'4 — network failure or no fetch implementation',
'8 — HTTP 4xx/5xx response'
]
}
}
function shellPage() {
return {
name: 'bare-os-shell',
@@ -616,7 +766,8 @@ function shellPage() {
{
name: 'alias',
synopsis: ['alias', 'alias name=value ...', 'unalias name ...'],
description: 'Define or list command aliases. unalias removes definitions.'
description:
'Define or list command aliases. unalias removes definitions.'
},
{
name: 'cd',
@@ -626,7 +777,8 @@ function shellPage() {
{
name: 'export',
synopsis: ['export NAME=value ...'],
description: 'Set environment variables visible to child /bin invocations.'
description:
'Set environment variables visible to child /bin invocations.'
},
{
name: 'unset',
@@ -641,12 +793,14 @@ function shellPage() {
{
name: 'umask',
synopsis: ['umask [octal]'],
description: 'Show or set shell file creation mask (stored in env UMASK).'
description:
'Show or set shell file creation mask (stored in env UMASK).'
},
{
name: 'command',
synopsis: ['command -v|-V NAME', 'command ARGV...'],
description: 'Resolve or run a command without using shell functions (none) or aliases for -v/-V.'
description:
'Resolve or run a command without using shell functions (none) or aliases for -v/-V.'
},
{
name: 'type',
@@ -656,7 +810,8 @@ function shellPage() {
{
name: 'login / logout',
synopsis: ['login [--new] passphrase...', 'logout [--save]'],
description: 'Identity unlock/register and session teardown; require booter hooks.'
description:
'Identity unlock/register and session teardown; require booter hooks.'
},
{
name: ':',
@@ -695,7 +850,14 @@ async function main() {
)
}
for (const name of MAN_EXTRA_PAGES) {
const p = name === 'git' ? gitPage() : shellPage()
const p =
name === 'git'
? gitPage()
: name === 'curl'
? curlPage()
: name === 'wget'
? wgetPage()
: shellPage()
await writeFile(
join(pagesDir, `${name}.json`),
JSON.stringify(p, null, 2) + '\n'
+1 -1
View File
@@ -1,6 +1,6 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc which whoami xargs'
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc wget which whoami xargs'
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
+1 -1
View File
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc which whoami xargs'
'Bare OS — default user: guest | shell builtins: alias, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: awk basename cat chgrp chmod chown cksum clear cp crontab curl cut date dirname du echo env exit false find getconf grep head hdms help hostname id jq ln login logout logname ls man mkdir mkfifo mv nl od pathchk printenv printf pwd readlink rm rmdir savevault sed seq sleep sort stat tail tee test time touch tr true tty uname wc wget which whoami xargs'
)
ctx.console.log(
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
File diff suppressed because one or more lines are too long