Updates
This commit is contained in:
+13
-6
@@ -20,7 +20,7 @@ bare-operating-system/
|
|||||||
├── developer-guide/ # How-to: in-image JS, ctx, coreutils, testing
|
├── developer-guide/ # How-to: in-image JS, ctx, coreutils, testing
|
||||||
│ ├── README.md # Index + reading order
|
│ ├── README.md # Index + reading order
|
||||||
│ ├── 01-two-runtimes-host-vs-image.md … 10-glossary-and-faq.md
|
│ ├── 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
|
├── .gitignore # Ignore rules
|
||||||
├── .prettierrc # Prettier formatting defaults
|
├── .prettierrc # Prettier formatting defaults
|
||||||
├── .github/
|
├── .github/
|
||||||
@@ -60,6 +60,9 @@ bare-operating-system/
|
|||||||
├── paths.js
|
├── paths.js
|
||||||
├── swarm-disk.js
|
├── swarm-disk.js
|
||||||
├── kernel-runner.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-account.js # Ed25519 account blob (bare-crypto PBKDF2 + ChaCha20-Poly1305)
|
||||||
├── identity-session.js # Guest vs unlocked env, vault save, ctx hooks
|
├── identity-session.js # Guest vs unlocked env, vault save, ctx hooks
|
||||||
├── vfs.js # Two-drive path routing ($HOME → personal drive)
|
├── vfs.js # Two-drive path routing ($HOME → personal drive)
|
||||||
@@ -74,11 +77,11 @@ bare-operating-system/
|
|||||||
## 2. Root: [package.json](package.json)
|
## 2. Root: [package.json](package.json)
|
||||||
|
|
||||||
| Field | Value / meaning |
|
| Field | Value / meaning |
|
||||||
| ----------------- | -------------------------------------------------------- |
|
| ----------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| `name` | `bare-operating-system` |
|
| `name` | `bare-operating-system` |
|
||||||
| `private` | `true` — not published as a single npm package |
|
| `private` | `true` — not published as a single npm package |
|
||||||
| `type` | `module` — ESM |
|
| `type` | `module` — ESM |
|
||||||
| `workspaces` | `["packages/*"]` — npm workspaces for the three packages |
|
| `workspaces` | `["packages/*"]` — npm workspaces (protocol, coreutils, seeder, booter, …) |
|
||||||
| `scripts.pretest` | Runs `npm run build -w bare-os-coreutils` before tests |
|
| `scripts.pretest` | Runs `npm run build -w bare-os-coreutils` before tests |
|
||||||
| `scripts.test` | Runs `npm run test --workspaces --if-present` |
|
| `scripts.test` | Runs `npm run test --workspaces --if-present` |
|
||||||
| `scripts.format` | `prettier --write .` |
|
| `scripts.format` | `prettier --write .` |
|
||||||
@@ -404,6 +407,9 @@ Pear-safe path resolution (same idea as Holepunch [pear-rti](https://github.com/
|
|||||||
|
|
||||||
**`runBinCommand(ctx, argv)`**
|
**`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`).
|
- 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.
|
- 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)`.
|
- 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)
|
### 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 `|`.
|
- **`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.
|
- **`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.
|
||||||
|
|
||||||
@@ -483,7 +490,7 @@ sequenceDiagram
|
|||||||
## 14. Environment variables (complete list)
|
## 14. Environment variables (complete list)
|
||||||
|
|
||||||
| Variable | Used by | Meaning |
|
| Variable | Used by | Meaning |
|
||||||
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
| ------------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| `BARE_OS_KERNEL_ROOT` | Seeder | Absolute path to kernel tree (default: `repo/kernel`) |
|
| `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_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_STORE` | Booter | Corestore for boot side (default: `repo/data/corestore-booter`) |
|
||||||
@@ -501,9 +508,9 @@ sequenceDiagram
|
|||||||
## 14a. POSIX userland appendix (implemented vs gaps)
|
## 14a. POSIX userland appendix (implemented vs gaps)
|
||||||
|
|
||||||
| Area | Status |
|
| 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`). |
|
| **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. |
|
| **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). |
|
| **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`**. |
|
| **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`**. |
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ This project is experimental research software, not a production OS.
|
|||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
| Path | Role |
|
| Path | Role |
|
||||||
| ------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| [kernel/](kernel/) | Source tree mirrored into the system drive (`/boot/init.js`, `/bin/*`, `/etc/*`) |
|
| [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-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-seeder](packages/bare-os-seeder) | Pear app: stage kernel → Hyperdrive, serve MBR block 0, replicate |
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
ctx.console.log(
|
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(
|
ctx.console.log(
|
||||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
'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
@@ -17,6 +17,10 @@ import {
|
|||||||
} from './lib/cli-readline.js'
|
} from './lib/cli-readline.js'
|
||||||
import { resolveStdio } from './lib/resolve-stdio.js'
|
import { resolveStdio } from './lib/resolve-stdio.js'
|
||||||
import { createKernelReplSession } from './lib/repl-session.js'
|
import { createKernelReplSession } from './lib/repl-session.js'
|
||||||
|
import {
|
||||||
|
suspendFishStdinForSubprocess,
|
||||||
|
resumeFishStdinAfterSubprocess
|
||||||
|
} from './lib/fish-readline.js'
|
||||||
import { createBootSplash } from './lib/boot-splash.js'
|
import { createBootSplash } from './lib/boot-splash.js'
|
||||||
import {
|
import {
|
||||||
applyGuestEnv,
|
applyGuestEnv,
|
||||||
@@ -316,6 +320,9 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.replStdin = sessionStdin
|
||||||
|
ctx.replStdout = sessionStdout
|
||||||
|
|
||||||
await applyGuestEnv(ctx)
|
await applyGuestEnv(ctx)
|
||||||
await ensureGuestHome(ctx)
|
await ensureGuestHome(ctx)
|
||||||
|
|
||||||
@@ -328,6 +335,13 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
ctx
|
ctx
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ctx.suspendReplForSubprocess = () => {
|
||||||
|
if (session.fishStdin) suspendFishStdinForSubprocess(session.fishStdin)
|
||||||
|
}
|
||||||
|
ctx.resumeReplAfterSubprocess = () => {
|
||||||
|
if (session.fishStdin) resumeFishStdinAfterSubprocess(session.fishStdin)
|
||||||
|
}
|
||||||
|
|
||||||
const sessionReadLine = session.readLine
|
const sessionReadLine = session.readLine
|
||||||
ctx.execLine = async (line) => {
|
ctx.execLine = async (line) => {
|
||||||
const t = line.trim()
|
const t = line.trim()
|
||||||
|
|||||||
@@ -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>} */
|
/** @type {WeakMap<import('stream').Readable, (chunk: Buffer | string) => void>} */
|
||||||
const fishStdinDataHandler = new WeakMap()
|
const fishStdinDataHandler = new WeakMap()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,22 @@ function shouldDelegateGit(cmd) {
|
|||||||
return true
|
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. */
|
/** Strip one leading Unix shebang so AsyncFunction does not see `#!` as invalid syntax. */
|
||||||
function stripShebang(source) {
|
function stripShebang(source) {
|
||||||
if (typeof source !== 'string' || !source.startsWith('#!')) return source
|
if (typeof source !== 'string' || !source.startsWith('#!')) return source
|
||||||
@@ -83,6 +99,16 @@ export async function runBinCommand(ctx, argv) {
|
|||||||
return runGitCli(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('/')) {
|
if (cmd.includes('/')) {
|
||||||
const abs = vfs.resolveLogical(cmd)
|
const abs = vfs.resolveLogical(cmd)
|
||||||
const { drive, path } = vfs.route(abs)
|
const { drive, path } = vfs.route(abs)
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ export async function createKernelReplSession({
|
|||||||
readLine,
|
readLine,
|
||||||
console: outConsole,
|
console: outConsole,
|
||||||
cleanup,
|
cleanup,
|
||||||
fishActive: Boolean(fishRead)
|
fishActive: Boolean(fishRead),
|
||||||
|
/** Set when fish owns the TTY; used to suspend/resume around host subprocesses. */
|
||||||
|
fishStdin: fishRead ? stdin : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,10 +185,7 @@ export async function loadBarerc(ctx, opts = {}) {
|
|||||||
if (t.startsWith('export ')) {
|
if (t.startsWith('export ')) {
|
||||||
const rest = t.slice(7).trim()
|
const rest = t.slice(7).trim()
|
||||||
const eq = rest.indexOf('=')
|
const eq = rest.indexOf('=')
|
||||||
if (
|
if (eq > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(rest.slice(0, eq))) {
|
||||||
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)
|
env[rest.slice(0, eq)] = expandWord(rest.slice(eq + 1), env)
|
||||||
} else if (strict) {
|
} else if (strict) {
|
||||||
ctx.console?.error?.('barerc: ignored: ' + t)
|
ctx.console?.error?.('barerc: ignored: ' + t)
|
||||||
@@ -472,7 +469,10 @@ export async function execShellLine(ctx, line) {
|
|||||||
const origErr = ctx.console.error
|
const origErr = ctx.console.error
|
||||||
|
|
||||||
for (const [k, val] of Object.entries(cmd.assign)) {
|
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')
|
origErr.call(ctx.console, k + ': readonly variable')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -521,7 +521,10 @@ export async function execShellLine(ctx, line) {
|
|||||||
const eq = a.indexOf('=')
|
const eq = a.indexOf('=')
|
||||||
if (eq > 0) {
|
if (eq > 0) {
|
||||||
const k = a.slice(0, eq)
|
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')
|
origErr.call(ctx.console, k + ': readonly variable')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
t.sNNMMNN
|
||||||
+122
-12
@@ -271,7 +271,11 @@ test('ls -l long listing uses session user and regular file mode', async (t) =>
|
|||||||
await personal.ready()
|
await personal.ready()
|
||||||
await drive.put('/bin/ls', b4a.from(lsSrc))
|
await drive.put('/bin/ls', b4a.from(lsSrc))
|
||||||
const lines = []
|
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 = {
|
ctx.console = {
|
||||||
log: (s) => lines.push(String(s)),
|
log: (s) => lines.push(String(s)),
|
||||||
error: (...a) => lines.push(a.join(' '))
|
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) => {
|
test('expandArgvAliases throws on cyclic alias chain', async (t) => {
|
||||||
const cyclic = { a: 'b', b: 'a' }
|
const cyclic = { a: 'b', b: 'a' }
|
||||||
t.exception(
|
t.exception(
|
||||||
@@ -724,6 +740,95 @@ test('tier-1 jq from system drive', async (t) => {
|
|||||||
rmSync(dir, { recursive: true, force: true })
|
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) => {
|
test('tier-1 rm -rf removes directory tree on personal drive', async (t) => {
|
||||||
const dir = testCorestoreDir('rmrf')
|
const dir = testCorestoreDir('rmrf')
|
||||||
const store = new Corestore(dir)
|
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 personal.ready()
|
||||||
await drive.put(
|
await drive.put(
|
||||||
'/bin/git',
|
'/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 logs = []
|
||||||
const ctx = testCtx(drive, personal)
|
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) => {
|
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 manPath = path.join(__dirname, '../../kernel/share/man/man.json')
|
||||||
const raw = JSON.parse(await readFile(manPath, 'utf8'))
|
const raw = JSON.parse(await readFile(manPath, 'utf8'))
|
||||||
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } = await import(
|
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } =
|
||||||
'../bare-os-coreutils/lib/commands.mjs'
|
await import('../bare-os-coreutils/lib/commands.mjs')
|
||||||
)
|
|
||||||
const handbookDir = path.join(__dirname, '../../handbook')
|
const handbookDir = path.join(__dirname, '../../handbook')
|
||||||
const devguideDir = path.join(__dirname, '../../developer-guide')
|
const devguideDir = path.join(__dirname, '../../developer-guide')
|
||||||
const handbookMd = (await readdir(handbookDir)).filter((f) => f.endsWith('.md')).length
|
const handbookMd = (await readdir(handbookDir)).filter((f) =>
|
||||||
const devguideMd = (await readdir(devguideDir)).filter((f) => f.endsWith('.md')).length
|
f.endsWith('.md')
|
||||||
|
).length
|
||||||
|
const devguideMd = (await readdir(devguideDir)).filter((f) =>
|
||||||
|
f.endsWith('.md')
|
||||||
|
).length
|
||||||
t.is(
|
t.is(
|
||||||
raw.pages.length,
|
raw.pages.length,
|
||||||
COREUTILS_COMMANDS.length +
|
COREUTILS_COMMANDS.length + MAN_EXTRA_PAGES.length + handbookMd + devguideMd
|
||||||
MAN_EXTRA_PAGES.length +
|
|
||||||
handbookMd +
|
|
||||||
devguideMd
|
|
||||||
)
|
)
|
||||||
t.ok(Array.isArray(raw.apropos) && raw.apropos.length > 0)
|
t.ok(Array.isArray(raw.apropos) && raw.apropos.length > 0)
|
||||||
t.is(typeof raw.index.ls, 'number')
|
t.is(typeof raw.index.ls, 'number')
|
||||||
@@ -872,7 +979,10 @@ test('runBinCommand man ls prints manual text', async (t) => {
|
|||||||
await drive.ready()
|
await drive.ready()
|
||||||
await personal.ready()
|
await personal.ready()
|
||||||
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
|
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')))
|
await drive.put('/bin/man', b4a.from(await readBuiltBin('man')))
|
||||||
const logs = []
|
const logs = []
|
||||||
const ctx = testCtx(drive, personal)
|
const ctx = testCtx(drive, personal)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
h
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pppThis is a test
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
o
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
dddNNNNMMM
|
||||||
@@ -70,4 +70,9 @@ export const COREUTILS_COMMANDS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
/** Extra manual pages not built as /bin scripts on the system drive. */
|
/** 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 { dirname, join } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import {
|
import { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } from '../lib/commands.mjs'
|
||||||
COREUTILS_COMMANDS,
|
|
||||||
MAN_EXTRA_PAGES
|
|
||||||
} from '../lib/commands.mjs'
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
const pagesDir = join(__dirname, '../man/pages')
|
const pagesDir = join(__dirname, '../man/pages')
|
||||||
@@ -92,8 +89,14 @@ const EXAMPLES = {}
|
|||||||
EXAMPLES.awk = [
|
EXAMPLES.awk = [
|
||||||
{ caption: 'print column 1', code: "awk '{print $1}' file.txt" },
|
{ caption: 'print column 1', code: "awk '{print $1}' file.txt" },
|
||||||
{ caption: 'field separator', code: "awk -F: '{print $1}' /etc/passwd" },
|
{ 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 = [
|
EXAMPLES.basename = [
|
||||||
{ caption: 'strip directory', code: 'basename /home/user/docs/readme.md' },
|
{ caption: 'strip directory', code: 'basename /home/user/docs/readme.md' },
|
||||||
@@ -101,7 +104,10 @@ EXAMPLES.basename = [
|
|||||||
]
|
]
|
||||||
EXAMPLES.cat = [
|
EXAMPLES.cat = [
|
||||||
{ caption: 'stdout several files', code: 'cat a.txt b.txt' },
|
{ 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' }
|
{ caption: 'here-string via echo pipe', code: 'echo hello | cat' }
|
||||||
]
|
]
|
||||||
EXAMPLES.chgrp = [
|
EXAMPLES.chgrp = [
|
||||||
@@ -112,7 +118,10 @@ EXAMPLES.chgrp = [
|
|||||||
]
|
]
|
||||||
EXAMPLES.chmod = [
|
EXAMPLES.chmod = [
|
||||||
{ caption: 'octal', code: 'chmod 644 ~/.profile' },
|
{ 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: 'symbolic user bits', code: 'chmod u+x script.sh' },
|
||||||
{ caption: 'all read, owner write', code: 'chmod a+r,u+w shared.txt' }
|
{ 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: 'checksum file', code: 'cksum iso.img' },
|
||||||
{ caption: 'verify pipeline', code: 'cat f | cksum' }
|
{ caption: 'verify pipeline', code: 'cat f | cksum' }
|
||||||
]
|
]
|
||||||
EXAMPLES.clear = [
|
EXAMPLES.clear = [{ caption: 'wipe screen', code: 'clear' }]
|
||||||
{ caption: 'wipe screen', code: 'clear' }
|
|
||||||
]
|
|
||||||
EXAMPLES.cp = [
|
EXAMPLES.cp = [
|
||||||
{ caption: 'copy file', code: 'cp src.txt dest.txt' },
|
{ caption: 'copy file', code: 'cp src.txt dest.txt' },
|
||||||
{ caption: 'into directory', code: 'cp a b c ~/backup/' },
|
{ caption: 'into directory', code: 'cp a b c ~/backup/' },
|
||||||
@@ -137,7 +144,7 @@ EXAMPLES.crontab = [
|
|||||||
{ caption: 'remove all', code: 'crontab -r' }
|
{ caption: 'remove all', code: 'crontab -r' }
|
||||||
]
|
]
|
||||||
EXAMPLES.cut = [
|
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' }
|
{ caption: 'characters', code: 'cut -c1-16 file.txt' }
|
||||||
]
|
]
|
||||||
EXAMPLES.date = [
|
EXAMPLES.date = [
|
||||||
@@ -146,7 +153,10 @@ EXAMPLES.date = [
|
|||||||
]
|
]
|
||||||
EXAMPLES.dirname = [
|
EXAMPLES.dirname = [
|
||||||
{ caption: 'parent path', code: 'dirname /a/b/c.txt' },
|
{ 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 = [
|
EXAMPLES.du = [
|
||||||
{ caption: 'sizes under cwd', code: 'du .' },
|
{ caption: 'sizes under cwd', code: 'du .' },
|
||||||
@@ -198,27 +208,24 @@ EXAMPLES.help = [
|
|||||||
{ caption: 'quick index', code: 'help' },
|
{ caption: 'quick index', code: 'help' },
|
||||||
{ caption: 'then deep dive', code: 'man grep' }
|
{ caption: 'then deep dive', code: 'man grep' }
|
||||||
]
|
]
|
||||||
EXAMPLES.hostname = [
|
EXAMPLES.hostname = [{ caption: 'show host', code: 'hostname' }]
|
||||||
{ caption: 'show host', code: 'hostname' }
|
EXAMPLES.id = [{ caption: 'who am I numerically', code: 'id' }]
|
||||||
]
|
|
||||||
EXAMPLES.id = [
|
|
||||||
{ caption: 'who am I numerically', code: 'id' }
|
|
||||||
]
|
|
||||||
EXAMPLES.ln = [
|
EXAMPLES.ln = [
|
||||||
{ caption: 'symlink', code: 'ln -s target name' },
|
{ caption: 'symlink', code: 'ln -s target name' },
|
||||||
{ caption: 'hard link (if supported)', code: 'ln file linkname' }
|
{ caption: 'hard link (if supported)', code: 'ln file linkname' }
|
||||||
]
|
]
|
||||||
EXAMPLES.login = [
|
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' }
|
{ caption: 'register new', code: 'login --new first time passphrase' }
|
||||||
]
|
]
|
||||||
EXAMPLES.logout = [
|
EXAMPLES.logout = [
|
||||||
{ caption: 'end session', code: 'logout' },
|
{ caption: 'end session', code: 'logout' },
|
||||||
{ caption: 'save vault hint', code: 'logout --save' }
|
{ caption: 'save vault hint', code: 'logout --save' }
|
||||||
]
|
]
|
||||||
EXAMPLES.logname = [
|
EXAMPLES.logname = [{ caption: 'login name', code: 'logname' }]
|
||||||
{ caption: 'login name', code: 'logname' }
|
|
||||||
]
|
|
||||||
EXAMPLES.ls = [
|
EXAMPLES.ls = [
|
||||||
{ caption: 'long + hidden', code: 'ls -la ~' },
|
{ caption: 'long + hidden', code: 'ls -la ~' },
|
||||||
{ caption: 'one per line', code: 'ls -1 /bin | head' },
|
{ caption: 'one per line', code: 'ls -1 /bin | head' },
|
||||||
@@ -227,7 +234,10 @@ EXAMPLES.ls = [
|
|||||||
EXAMPLES.man = [
|
EXAMPLES.man = [
|
||||||
{ caption: 'open page', code: 'man sed' },
|
{ caption: 'open page', code: 'man sed' },
|
||||||
{ caption: 'handbook TOC (section 7)', code: 'man handbook' },
|
{ 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: 'apropos', code: 'man -k copy' },
|
||||||
{ caption: 'whatis', code: 'man -f grep' },
|
{ caption: 'whatis', code: 'man -f grep' },
|
||||||
{ caption: 'all pages', code: 'man -l' },
|
{ caption: 'all pages', code: 'man -l' },
|
||||||
@@ -244,12 +254,8 @@ EXAMPLES.mv = [
|
|||||||
{ caption: 'rename', code: 'mv old.txt new.txt' },
|
{ caption: 'rename', code: 'mv old.txt new.txt' },
|
||||||
{ caption: 'into dir', code: 'mv *.txt ~/inbox/' }
|
{ caption: 'into dir', code: 'mv *.txt ~/inbox/' }
|
||||||
]
|
]
|
||||||
EXAMPLES.nl = [
|
EXAMPLES.nl = [{ caption: 'number all lines', code: 'nl README.md' }]
|
||||||
{ caption: 'number all lines', code: 'nl README.md' }
|
EXAMPLES.od = [{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }]
|
||||||
]
|
|
||||||
EXAMPLES.od = [
|
|
||||||
{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }
|
|
||||||
]
|
|
||||||
EXAMPLES.pathchk = [
|
EXAMPLES.pathchk = [
|
||||||
{ caption: 'portable path check', code: 'pathchk -p "$HOME/file name"' }
|
{ 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: 'format', code: 'printf "hex=%x dec=%d\\n" 255 255' },
|
||||||
{ caption: 'no newline', code: 'printf "%s" OK' }
|
{ caption: 'no newline', code: 'printf "%s" OK' }
|
||||||
]
|
]
|
||||||
EXAMPLES.pwd = [
|
EXAMPLES.pwd = [{ caption: 'where am I', code: 'pwd' }]
|
||||||
{ caption: 'where am I', code: 'pwd' }
|
EXAMPLES.readlink = [{ caption: 'symlink target', code: 'readlink ~/.config' }]
|
||||||
]
|
|
||||||
EXAMPLES.readlink = [
|
|
||||||
{ caption: 'symlink target', code: 'readlink ~/.config' }
|
|
||||||
]
|
|
||||||
EXAMPLES.rm = [
|
EXAMPLES.rm = [
|
||||||
{ caption: 'file', code: 'rm tmp.txt' },
|
{ caption: 'file', code: 'rm tmp.txt' },
|
||||||
{ caption: 'tree', code: 'rm -rf build/' }
|
{ caption: 'tree', code: 'rm -rf build/' }
|
||||||
]
|
]
|
||||||
EXAMPLES.rmdir = [
|
EXAMPLES.rmdir = [{ caption: 'empty dir', code: 'rmdir olddir' }]
|
||||||
{ caption: 'empty dir', code: 'rmdir olddir' }
|
|
||||||
]
|
|
||||||
EXAMPLES.savevault = [
|
EXAMPLES.savevault = [
|
||||||
{ caption: 'snapshot encrypted vault', code: 'savevault' }
|
{ caption: 'snapshot encrypted vault', code: 'savevault' }
|
||||||
]
|
]
|
||||||
@@ -288,17 +288,13 @@ EXAMPLES.seq = [
|
|||||||
{ caption: '1..10', code: 'seq 1 10' },
|
{ caption: '1..10', code: 'seq 1 10' },
|
||||||
{ caption: 'step', code: 'seq 0 2 20' }
|
{ caption: 'step', code: 'seq 0 2 20' }
|
||||||
]
|
]
|
||||||
EXAMPLES.sleep = [
|
EXAMPLES.sleep = [{ caption: 'pause seconds', code: 'sleep 2' }]
|
||||||
{ caption: 'pause seconds', code: 'sleep 2' }
|
|
||||||
]
|
|
||||||
EXAMPLES.sort = [
|
EXAMPLES.sort = [
|
||||||
{ caption: 'lexicographic', code: 'sort names.txt' },
|
{ caption: 'lexicographic', code: 'sort names.txt' },
|
||||||
{ caption: 'numeric', code: 'sort -n scores.txt' },
|
{ caption: 'numeric', code: 'sort -n scores.txt' },
|
||||||
{ caption: 'unique', code: 'sort -u tags.txt' }
|
{ caption: 'unique', code: 'sort -u tags.txt' }
|
||||||
]
|
]
|
||||||
EXAMPLES.stat = [
|
EXAMPLES.stat = [{ caption: 'metadata', code: 'stat ~/README.md' }]
|
||||||
{ caption: 'metadata', code: 'stat ~/README.md' }
|
|
||||||
]
|
|
||||||
EXAMPLES.tail = [
|
EXAMPLES.tail = [
|
||||||
{ caption: 'last lines', code: 'tail -n 20 app.log' },
|
{ caption: 'last lines', code: 'tail -n 20 app.log' },
|
||||||
{ caption: 'follow vibe (Bare: poll manually)', code: 'tail error.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: 'directory', code: 'test -d /home/user' },
|
||||||
{ caption: 'string equal', code: 'test "$USER" = guest' }
|
{ caption: 'string equal', code: 'test "$USER" = guest' }
|
||||||
]
|
]
|
||||||
EXAMPLES.time = [
|
EXAMPLES.time = [{ caption: 'wall time a command', code: 'time sort big.txt' }]
|
||||||
{ caption: 'wall time a command', code: 'time sort big.txt' }
|
|
||||||
]
|
|
||||||
EXAMPLES.touch = [
|
EXAMPLES.touch = [
|
||||||
{ caption: 'create empty', code: 'touch newfile' },
|
{ caption: 'create empty', code: 'touch newfile' },
|
||||||
{ caption: 'refresh mtime', code: 'touch -c existing' }
|
{ caption: 'refresh mtime', code: 'touch -c existing' }
|
||||||
@@ -322,27 +316,20 @@ EXAMPLES.tr = [
|
|||||||
{ caption: 'uppercase', code: "echo hi | tr 'a-z' 'A-Z'" },
|
{ caption: 'uppercase', code: "echo hi | tr 'a-z' 'A-Z'" },
|
||||||
{ caption: 'delete chars', code: "tr -d '\\r' < win.txt" }
|
{ caption: 'delete chars', code: "tr -d '\\r' < win.txt" }
|
||||||
]
|
]
|
||||||
EXAMPLES.true = [
|
EXAMPLES.true = [{ caption: 'always success', code: 'true && echo ok' }]
|
||||||
{ 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.tty = [
|
|
||||||
{ caption: 'am I a tty', code: 'tty' }
|
|
||||||
]
|
|
||||||
EXAMPLES.uname = [
|
|
||||||
{ caption: 'kernel-ish info', code: 'uname -a' }
|
|
||||||
]
|
|
||||||
EXAMPLES.wc = [
|
EXAMPLES.wc = [
|
||||||
{ caption: 'lines words bytes', code: 'wc README.md' },
|
{ caption: 'lines words bytes', code: 'wc README.md' },
|
||||||
{ caption: 'stdin only', code: 'cat f | wc -l' }
|
{ caption: 'stdin only', code: 'cat f | wc -l' }
|
||||||
]
|
]
|
||||||
EXAMPLES.which = [
|
EXAMPLES.which = [{ caption: 'resolve on PATH', code: 'which ls' }]
|
||||||
{ caption: 'resolve on PATH', code: 'which ls' }
|
EXAMPLES.whoami = [{ caption: 'effective user', code: 'whoami' }]
|
||||||
]
|
|
||||||
EXAMPLES.whoami = [
|
|
||||||
{ caption: 'effective user', code: 'whoami' }
|
|
||||||
]
|
|
||||||
EXAMPLES.xargs = [
|
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>>} */
|
/** @type {Record<string, Record<string, unknown>>} */
|
||||||
@@ -376,7 +363,10 @@ EXTRA.mkfifo = {
|
|||||||
bareOsNotes: 'Documented stub; no real pipes as kernel objects.'
|
bareOsNotes: 'Documented stub; no real pipes as kernel objects.'
|
||||||
}
|
}
|
||||||
EXTRA.chmod = {
|
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:
|
description:
|
||||||
'Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.',
|
'Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.',
|
||||||
options: [],
|
options: [],
|
||||||
@@ -391,7 +381,10 @@ EXTRA.grep = {
|
|||||||
description:
|
description:
|
||||||
'Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.',
|
'Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.',
|
||||||
options: [
|
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: '-F', meaning: 'Fixed string match' },
|
||||||
{ flag: '-i', meaning: 'Ignore case' },
|
{ flag: '-i', meaning: 'Ignore case' },
|
||||||
{ flag: '-v', meaning: 'Invert match' },
|
{ flag: '-v', meaning: 'Invert match' },
|
||||||
@@ -451,7 +444,10 @@ EXTRA.man = {
|
|||||||
description:
|
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.',
|
'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: [
|
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: '-f, --whatis', meaning: 'One-line description for exact name' },
|
||||||
{
|
{
|
||||||
flag: '-l, --list',
|
flag: '-l, --list',
|
||||||
@@ -459,8 +455,20 @@ EXTRA.man = {
|
|||||||
'List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically'
|
'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'],
|
environment: [
|
||||||
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
|
'MANWIDTH — wrap width (default 72, min 40)',
|
||||||
|
'NO_COLOR — disable bold headings on TTY'
|
||||||
|
],
|
||||||
|
keywords: [
|
||||||
|
'man',
|
||||||
|
'manual',
|
||||||
|
'help',
|
||||||
|
'documentation',
|
||||||
|
'apropos',
|
||||||
|
'whatis',
|
||||||
|
'cheat',
|
||||||
|
'examples'
|
||||||
|
],
|
||||||
seeAlso: [
|
seeAlso: [
|
||||||
{ name: 'help', section: 1 },
|
{ name: 'help', section: 1 },
|
||||||
{ name: 'bare-os-handbook', section: 7 },
|
{ name: 'bare-os-handbook', section: 7 },
|
||||||
@@ -505,12 +513,25 @@ EXTRA.jq = {
|
|||||||
description:
|
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.',
|
'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: [
|
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: '-n, --null-input',
|
||||||
{ flag: '-s, --slurp', meaning: 'Read all inputs into one array; run the filter once' },
|
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: '-c, --compact-output', meaning: 'Compact JSON on output' },
|
||||||
{ flag: '-r, --raw-output', meaning: 'Print strings without JSON quotes' },
|
{ 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' }
|
{ flag: '-f, --from-file', meaning: 'Read filter program from file' }
|
||||||
],
|
],
|
||||||
keywords: ['jq', 'json', 'query', 'filter', 'jqjs'],
|
keywords: ['jq', 'json', 'query', 'filter', 'jqjs'],
|
||||||
@@ -523,8 +544,8 @@ EXTRA.jq = {
|
|||||||
examples: [
|
examples: [
|
||||||
{ caption: 'pretty-print', code: 'jq . data.json' },
|
{ caption: 'pretty-print', code: 'jq . data.json' },
|
||||||
{ caption: 'field', code: 'jq .version package.json' },
|
{ caption: 'field', code: 'jq .version package.json' },
|
||||||
{ caption: 'slurp array', code: 'jq -s \'map(.x) | add\' parts.jsonl' },
|
{ caption: 'slurp array', code: "jq -s 'map(.x) | add' parts.jsonl" },
|
||||||
{ caption: 'compact', code: 'jq -c \'.[] | select(.ok)\' items.json' }
|
{ caption: 'compact', code: "jq -c '.[] | select(.ok)' items.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
EXTRA.login = {
|
EXTRA.login = {
|
||||||
@@ -539,7 +560,8 @@ EXTRA.logout = {
|
|||||||
seeAlso: [{ name: 'login', section: 1 }]
|
seeAlso: [{ name: 'login', section: 1 }]
|
||||||
}
|
}
|
||||||
EXTRA.savevault = {
|
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'],
|
keywords: ['savevault', 'vault', 'encrypt', 'backup'],
|
||||||
seeAlso: [{ name: 'login', section: 1 }]
|
seeAlso: [{ name: 'login', section: 1 }]
|
||||||
}
|
}
|
||||||
@@ -573,26 +595,154 @@ function gitPage() {
|
|||||||
synopsis: ['git [-C dir] <subcommand> [ARGUMENTS...]'],
|
synopsis: ['git [-C dir] <subcommand> [ARGUMENTS...]'],
|
||||||
description:
|
description:
|
||||||
'Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.',
|
'Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.',
|
||||||
options: [
|
options: [{ flag: '-C dir', meaning: 'Run as if git was started in dir' }],
|
||||||
{ flag: '-C dir', meaning: 'Run as if git was started in dir' }
|
|
||||||
],
|
|
||||||
environment: [
|
environment: [
|
||||||
'BARE_OS_GIT_HTTP — optional fetch implementation for remotes',
|
'BARE_OS_GIT_HTTP — optional fetch implementation for remotes',
|
||||||
'GIT_* — standard hints where supported'
|
'GIT_* — standard hints where supported'
|
||||||
],
|
],
|
||||||
keywords: ['git', 'version control', 'repository', 'clone', 'commit', 'isomorphic-git'],
|
keywords: [
|
||||||
bareOsNotes: 'Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.',
|
'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 }],
|
seeAlso: [{ name: 'bare-os-shell', section: 1 }],
|
||||||
examples: [
|
examples: [
|
||||||
{ caption: 'new repo', code: 'git init -C ~/myrepo' },
|
{ caption: 'new repo', code: 'git init -C ~/myrepo' },
|
||||||
{ caption: 'status', code: 'git -C ~/myrepo status' },
|
{ 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' }
|
{ 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() {
|
function shellPage() {
|
||||||
return {
|
return {
|
||||||
name: 'bare-os-shell',
|
name: 'bare-os-shell',
|
||||||
@@ -616,7 +766,8 @@ function shellPage() {
|
|||||||
{
|
{
|
||||||
name: 'alias',
|
name: 'alias',
|
||||||
synopsis: ['alias', 'alias name=value ...', 'unalias name ...'],
|
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',
|
name: 'cd',
|
||||||
@@ -626,7 +777,8 @@ function shellPage() {
|
|||||||
{
|
{
|
||||||
name: 'export',
|
name: 'export',
|
||||||
synopsis: ['export NAME=value ...'],
|
synopsis: ['export NAME=value ...'],
|
||||||
description: 'Set environment variables visible to child /bin invocations.'
|
description:
|
||||||
|
'Set environment variables visible to child /bin invocations.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'unset',
|
name: 'unset',
|
||||||
@@ -641,12 +793,14 @@ function shellPage() {
|
|||||||
{
|
{
|
||||||
name: 'umask',
|
name: 'umask',
|
||||||
synopsis: ['umask [octal]'],
|
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',
|
name: 'command',
|
||||||
synopsis: ['command -v|-V NAME', 'command ARGV...'],
|
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',
|
name: 'type',
|
||||||
@@ -656,7 +810,8 @@ function shellPage() {
|
|||||||
{
|
{
|
||||||
name: 'login / logout',
|
name: 'login / logout',
|
||||||
synopsis: ['login [--new] passphrase...', 'logout [--save]'],
|
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: ':',
|
name: ':',
|
||||||
@@ -695,7 +850,14 @@ async function main() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (const name of MAN_EXTRA_PAGES) {
|
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(
|
await writeFile(
|
||||||
join(pagesDir, `${name}.json`),
|
join(pagesDir, `${name}.json`),
|
||||||
JSON.stringify(p, null, 2) + '\n'
|
JSON.stringify(p, null, 2) + '\n'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
ctx.console.log(
|
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(
|
ctx.console.log(
|
||||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function barePosixBlocks(size) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
ctx.console.log(
|
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(
|
ctx.console.log(
|
||||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
'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
Reference in New Issue
Block a user