Update man to add the manual guide

This commit is contained in:
Raven Scott
2026-04-03 05:28:49 -04:00
parent 11cac45df6
commit 2d26a0b8d1
174 changed files with 17092 additions and 198 deletions
@@ -0,0 +1,294 @@
import { readFile, writeFile, mkdir } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
import { buildHandbookManPages } from './ingest-handbook-for-man.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = join(__dirname, '..')
const repoRoot = join(pkgRoot, '../..')
const pagesDir = join(pkgRoot, 'man', 'pages')
const kernelManDir = join(repoRoot, 'kernel', 'share', 'man')
const seederManDir = join(
repoRoot,
'packages',
'bare-os-seeder',
'kernel',
'share',
'man'
)
function isPlainObject(x) {
return x !== null && typeof x === 'object' && !Array.isArray(x)
}
function validatePage(raw, pathLabel) {
if (!isPlainObject(raw)) throw new Error(`${pathLabel}: root must be object`)
const allowed = new Set([
'name',
'section',
'title',
'synopsis',
'description',
'options',
'environment',
'files',
'exitStatus',
'diagnostics',
'seeAlso',
'bareOsNotes',
'keywords',
'aliases',
'stub',
'builtins',
'examples',
'descriptionMode'
])
for (const k of Object.keys(raw)) {
if (!allowed.has(k)) throw new Error(`${pathLabel}: unknown field "${k}"`)
}
const {
name,
section,
title,
synopsis,
description,
options,
keywords
} = raw
if (typeof name !== 'string' || !name)
throw new Error(`${pathLabel}: name must be non-empty string`)
if (typeof section !== 'number' || section < 1 || section > 8)
throw new Error(`${pathLabel}: section must be integer 18`)
if (typeof title !== 'string' || !title)
throw new Error(`${pathLabel}: title must be non-empty string`)
if (!Array.isArray(synopsis) || synopsis.length < 1)
throw new Error(`${pathLabel}: synopsis must be non-empty array`)
for (const s of synopsis) {
if (typeof s !== 'string')
throw new Error(`${pathLabel}: synopsis entries must be strings`)
}
if (typeof description !== 'string' || !description)
throw new Error(`${pathLabel}: description must be non-empty string`)
if (!Array.isArray(options))
throw new Error(`${pathLabel}: options must be array`)
for (const o of options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: each option needs { flag, meaning }`)
}
if (!Array.isArray(keywords))
throw new Error(`${pathLabel}: keywords must be array`)
for (const kw of keywords) {
if (typeof kw !== 'string')
throw new Error(`${pathLabel}: keywords entries must be strings`)
}
if (raw.environment !== undefined) {
if (!Array.isArray(raw.environment))
throw new Error(`${pathLabel}: environment must be array of strings`)
for (const e of raw.environment) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: environment must be strings`)
}
}
if (raw.files !== undefined) {
if (!Array.isArray(raw.files))
throw new Error(`${pathLabel}: files must be array of strings`)
for (const f of raw.files) {
if (typeof f !== 'string') throw new Error(`${pathLabel}: files must be strings`)
}
}
if (raw.exitStatus !== undefined) {
if (!Array.isArray(raw.exitStatus))
throw new Error(`${pathLabel}: exitStatus must be array of strings`)
for (const e of raw.exitStatus) {
if (typeof e !== 'string') throw new Error(`${pathLabel}: exitStatus must be strings`)
}
}
if (raw.diagnostics !== undefined) {
if (!Array.isArray(raw.diagnostics))
throw new Error(`${pathLabel}: diagnostics must be array of strings`)
for (const d of raw.diagnostics) {
if (typeof d !== 'string') throw new Error(`${pathLabel}: diagnostics must be strings`)
}
}
if (raw.seeAlso !== undefined) {
if (!Array.isArray(raw.seeAlso))
throw new Error(`${pathLabel}: seeAlso must be array`)
for (const ref of raw.seeAlso) {
if (
!isPlainObject(ref) ||
typeof ref.name !== 'string' ||
typeof ref.section !== 'number'
)
throw new Error(`${pathLabel}: seeAlso entries need { name, section }`)
}
}
if (raw.bareOsNotes !== undefined && typeof raw.bareOsNotes !== 'string')
throw new Error(`${pathLabel}: bareOsNotes must be string`)
if (raw.descriptionMode !== undefined) {
if (raw.descriptionMode !== 'wrap' && raw.descriptionMode !== 'preserve')
throw new Error(`${pathLabel}: descriptionMode must be "wrap" or "preserve"`)
}
if (raw.aliases !== undefined) {
if (!Array.isArray(raw.aliases))
throw new Error(`${pathLabel}: aliases must be array of strings`)
for (const a of raw.aliases) {
if (typeof a !== 'string') throw new Error(`${pathLabel}: aliases must be strings`)
}
}
if (raw.stub !== undefined && typeof raw.stub !== 'boolean')
throw new Error(`${pathLabel}: stub must be boolean`)
if (raw.examples !== undefined) {
if (!Array.isArray(raw.examples))
throw new Error(`${pathLabel}: examples must be array`)
for (const ex of raw.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: each example needs non-empty { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: example caption must be string`)
}
}
if (raw.builtins !== undefined) {
if (!Array.isArray(raw.builtins))
throw new Error(`${pathLabel}: builtins must be array`)
for (const b of raw.builtins) {
if (!isPlainObject(b) || typeof b.name !== 'string' || typeof b.description !== 'string')
throw new Error(`${pathLabel}: each builtin needs { name, description }`)
if (b.synopsis !== undefined) {
if (!Array.isArray(b.synopsis))
throw new Error(`${pathLabel}: builtin synopsis must be array`)
for (const s of b.synopsis) {
if (typeof s !== 'string') throw new Error(`${pathLabel}: builtin synopsis strings`)
}
}
if (b.options !== undefined) {
if (!Array.isArray(b.options))
throw new Error(`${pathLabel}: builtin options must be array`)
for (const o of b.options) {
if (!isPlainObject(o) || typeof o.flag !== 'string' || typeof o.meaning !== 'string')
throw new Error(`${pathLabel}: builtin option { flag, meaning }`)
}
}
if (b.examples !== undefined) {
if (!Array.isArray(b.examples))
throw new Error(`${pathLabel}: builtin examples must be array`)
for (const ex of b.examples) {
if (!isPlainObject(ex) || typeof ex.code !== 'string' || !ex.code.trim())
throw new Error(`${pathLabel}: builtin example needs { code }`)
if (ex.caption !== undefined && typeof ex.caption !== 'string')
throw new Error(`${pathLabel}: builtin example caption must be string`)
}
}
}
}
}
function tokenizeForApropos(text) {
return String(text)
.toLowerCase()
.split(/[^a-z0-9_-]+/)
.filter(Boolean)
}
export async function buildManDb() {
const required = [...COREUTILS_COMMANDS, ...MAN_EXTRA_PAGES]
const pages = []
const index = Object.create(null)
const apropos = []
for (let i = 0; i < required.length; i++) {
const cmd = required[i]
const filePath = join(pagesDir, `${cmd}.json`)
let raw
try {
raw = JSON.parse(await readFile(filePath, 'utf8'))
} catch (e) {
throw new Error(`man: missing or invalid JSON for "${cmd}": ${filePath} (${e.message})`)
}
validatePage(raw, cmd)
if (raw.name !== cmd)
throw new Error(`man/pages/${cmd}.json: "name" must be "${cmd}", got "${raw.name}"`)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const handbookPages = await buildHandbookManPages(repoRoot)
for (let hi = 0; hi < handbookPages.length; hi++) {
const raw = handbookPages[hi]
validatePage(raw, raw.name)
pages.push(raw)
const pageIdx = pages.length - 1
const addIndex = (key) => {
const k = String(key).toLowerCase()
if (index[k] !== undefined && index[k] !== pageIdx)
throw new Error(`man: duplicate index key "${k}"`)
index[k] = pageIdx
}
addIndex(raw.name)
if (Array.isArray(raw.aliases)) {
for (const a of raw.aliases) addIndex(a)
}
const kwSet = new Set()
for (const k of raw.keywords) kwSet.add(k.toLowerCase())
kwSet.add(raw.name.toLowerCase())
for (const t of tokenizeForApropos(raw.title)) kwSet.add(t)
if (Array.isArray(raw.examples)) {
for (const ex of raw.examples) {
if (ex.caption) {
for (const t of tokenizeForApropos(ex.caption)) kwSet.add(t)
}
}
}
for (const kw of kwSet) {
apropos.push({ kw, pageRef: pageIdx })
}
}
const out = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
pages,
index,
apropos
}
const json = JSON.stringify(out, null, 0) + '\n'
await mkdir(kernelManDir, { recursive: true })
await mkdir(seederManDir, { recursive: true })
await writeFile(join(kernelManDir, 'man.json'), json)
await writeFile(join(seederManDir, 'man.json'), json)
}
const isMain =
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (isMain) {
await buildManDb()
}
@@ -0,0 +1,206 @@
/**
* Convert repo handbook/*.md into man(7) page objects merged at build time.
*/
import { readFile, readdir } from 'fs/promises'
import { join } from 'path'
const SECTION = 7
function stripMdInline(s) {
let t = String(s)
t = t.replace(/\[([^\]]*)\]\(([^)]+)\)/g, '$1 <$2>')
t = t.replace(/\*\*([^*]+)\*\*/g, '$1')
t = t.replace(/__([^_]+)__/g, '$1')
t = t.replace(/\*([^*]+)\*/g, '$1')
t = t.replace(/`([^`]+)`/g, '$1')
return t.trim()
}
/**
* @param {string} md
* @param {string} sourceFile
*/
export function handbookMdToDescription(md, sourceFile) {
const lines = md.split('\n')
const out = []
let i = 0
let inFence = false
/** @type {string} */
let fenceKind = ''
while (i < lines.length) {
const line = lines[i]
const fenceM = line.match(/^(\s*)```(\w*)\s*$/)
if (fenceM) {
if (!inFence) {
inFence = true
fenceKind = fenceM[2] || ''
i++
continue
}
inFence = false
fenceKind = ''
i++
continue
}
if (inFence) {
if (fenceKind === 'mermaid') {
i++
continue
}
out.push(' ' + line)
i++
continue
}
const t = line.trim()
if (/^---+$/.test(t)) {
out.push('')
i++
continue
}
const h = line.match(/^(#{1,6})\s+(.*)$/)
if (h) {
const text = stripMdInline(h[2])
if (text) {
out.push('')
out.push(text.toUpperCase())
out.push('')
}
i++
continue
}
if (/^\s*[-*]\s+/.test(line)) {
out.push(stripMdInline(line.replace(/^\s+/, '')))
i++
continue
}
if (/^\s*\d+\.\s+/.test(line)) {
out.push(stripMdInline(line.trim()))
i++
continue
}
if (t.startsWith('|')) {
if (/^\|[\s-:|]+\|$/.test(t)) {
i++
continue
}
out.push(stripMdInline(t))
i++
continue
}
if (!t) {
out.push('')
i++
continue
}
out.push(stripMdInline(line.trim()))
i++
}
let body = out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
if (!body) body = '(empty chapter — see ' + sourceFile + ' in the repo)'
return body
}
function firstHeadingTitle(md) {
const m = md.match(/^#\s+(.+)$/m)
return m ? stripMdInline(m[1]) : 'Bare OS handbook'
}
/**
* @param {string} repoRoot
* @returns {Promise<object[]>}
*/
export async function buildHandbookManPages(repoRoot) {
const handbookDir = join(repoRoot, 'handbook')
let names
try {
names = (await readdir(handbookDir)).filter((f) => f.endsWith('.md'))
} catch {
return []
}
names.sort((a, b) => {
if (a === 'README.md') return -1
if (b === 'README.md') return 1
return a.localeCompare(b)
})
/** @type {{ file: string, name: string, title: string, aliases?: string[] }[]} */
const meta = []
for (const file of names) {
if (file === 'README.md') {
meta.push({
file,
name: 'bare-os-handbook',
title: 'Bare OS handbook — table of contents and reading order',
aliases: ['handbook', 'bare-os-handbook-index']
})
} else {
const base = file.replace(/\.md$/i, '')
meta.push({
file,
name: 'handbook-' + base,
title: '',
aliases: []
})
}
}
const pages = []
for (let j = 0; j < meta.length; j++) {
const m = meta[j]
const path = join(handbookDir, m.file)
const md = await readFile(path, 'utf8')
const title = m.title || firstHeadingTitle(md)
const description = handbookMdToDescription(md, 'handbook/' + m.file)
const next = meta[j + 1]
const prev = meta[j - 1]
/** @type {{ name: string, section: number }[]} */
const seeAlso = []
if (next) seeAlso.push({ name: next.name, section: SECTION })
if (prev) seeAlso.push({ name: prev.name, section: SECTION })
seeAlso.push({ name: 'man', section: 1 })
const kw = new Set([
'handbook',
'bare-os',
'documentation',
'narrative',
'chapter'
])
for (const t of m.name.split(/[^a-z0-9]+/i)) {
if (t.length > 1) kw.add(t.toLowerCase())
}
for (const t of title.toLowerCase().split(/[^a-z0-9]+/)) {
if (t.length > 2) kw.add(t)
}
const page = {
name: m.name,
section: SECTION,
title,
synopsis: ['man 7 ' + m.name, 'Handbook chapter (plain text from handbook/' + m.file + ')'],
description,
descriptionMode: 'preserve',
options: [],
keywords: Array.from(kw),
seeAlso,
bareOsNotes:
'Generated at build time from handbook/' +
m.file +
'. Diagrams in ```mermaid blocks are omitted; open the Markdown in the repo for figures.'
}
if (m.aliases && m.aliases.length) page.aliases = m.aliases
pages.push(page)
}
return pages
}
@@ -0,0 +1,667 @@
/**
* One-shot generator for man/pages/*.json (run after changing commands list).
* node packages/bare-os-coreutils/scripts/seed-man-pages.mjs
*/
import { mkdir, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import {
COREUTILS_COMMANDS,
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pagesDir = join(__dirname, '../man/pages')
const STUB = new Set(['chgrp', 'chown', 'xargs', 'getconf', 'mkfifo'])
const POSIX_TITLE = {
awk: 'pattern scanning and processing language',
basename: 'strip directory and suffix from pathnames',
cat: 'concatenate and print files',
chgrp: 'change file group ownership',
chmod: 'change file mode bits',
chown: 'change file owner and group',
cksum: 'write file checksums and sizes',
clear: 'clear the terminal screen',
cp: 'copy files',
crontab: 'user crontab manipulation',
cut: 'cut out selected fields of each line',
date: 'display or set date and time',
dirname: 'return directory portion of a pathname',
du: 'estimate file space usage',
echo: 'write arguments to standard output',
env: 'set the environment for command invocation',
exit: 'exit the shell or booter session',
false: 'return false value',
find: 'find files',
getconf: 'get configuration values',
grep: 'pattern matching utility',
head: 'copy the first part of files',
hdms: 'Hyperswarm distributed map store',
help: 'Bare OS help summary',
hostname: 'set or print hostname',
id: 'return user identity',
ln: 'link files',
login: 'begin a session on the system',
logout: 'end session (save vault)',
logname: "return the user's login name",
ls: 'list directory contents',
man: 'display on-line manual pages',
mkdir: 'make directories',
mkfifo: 'make FIFO special files',
mv: 'move or rename files',
nl: 'line numbering utility',
od: 'octal dump',
pathchk: 'check pathname portability',
printenv: 'print environment variables',
printf: 'format and print',
pwd: 'return working directory name',
readlink: 'print symbolic link targets',
rm: 'remove files',
rmdir: 'remove empty directories',
savevault: 'encrypt snapshot of personal drive',
sed: 'stream editor',
seq: 'print sequences of numbers',
sleep: 'suspend execution for an interval',
sort: 'sort lines',
stat: 'display file status',
tail: 'copy the last part of a file',
tee: 'duplicate standard input',
test: 'evaluate a condition',
time: 'time a simple command',
touch: 'change file timestamps or create files',
tr: 'translate or delete characters',
true: 'return true value',
tty: "return user's terminal name",
uname: 'return operating system name',
wc: 'word, line, and byte or character count',
which: 'locate a command',
whoami: 'display effective user ID',
xargs: 'construct argument lists and invoke utility'
}
/**
* cheat.sh-style snippets: { caption?, code } — shown under EXAMPLES in man output.
* @type {Record<string, Array<{ caption?: string, code: string }>>}
*/
const EXAMPLES = {}
EXAMPLES.awk = [
{ caption: 'print column 1', code: "awk '{print $1}' file.txt" },
{ caption: 'field separator', code: "awk -F: '{print $1}' /etc/passwd" },
{ caption: 'sum numbers in first column', code: "awk '{s+=$1} END{print s}' nums.txt" },
{ caption: 'lines matching /re/', code: "awk '/error/{print NR\": \"$0}' log.txt" }
]
EXAMPLES.basename = [
{ caption: 'strip directory', code: 'basename /home/user/docs/readme.md' },
{ caption: 'strip suffix', code: 'basename -s .md /path/readme.md' }
]
EXAMPLES.cat = [
{ caption: 'stdout several files', code: 'cat a.txt b.txt' },
{ caption: 'number lines (use nl)', code: 'cat -n file.txt # if supported; else nl file' },
{ caption: 'here-string via echo pipe', code: 'echo hello | cat' }
]
EXAMPLES.chgrp = [
{
caption: 'not supported — use identity model',
code: '# chgrp is a stub; group is display metadata only'
}
]
EXAMPLES.chmod = [
{ caption: 'octal', code: 'chmod 644 ~/.profile' },
{ caption: 'recursive-ish (run find + chmod per file)', code: 'find . -type f -name "*.sh" -print' },
{ caption: 'symbolic user bits', code: 'chmod u+x script.sh' },
{ caption: 'all read, owner write', code: 'chmod a+r,u+w shared.txt' }
]
EXAMPLES.chown = [
{ caption: 'not supported', code: '# chown stub — see man identity / login' }
]
EXAMPLES.cksum = [
{ caption: 'checksum file', code: 'cksum iso.img' },
{ caption: 'verify pipeline', code: 'cat f | cksum' }
]
EXAMPLES.clear = [
{ caption: 'wipe screen', code: 'clear' }
]
EXAMPLES.cp = [
{ caption: 'copy file', code: 'cp src.txt dest.txt' },
{ caption: 'into directory', code: 'cp a b c ~/backup/' },
{ caption: 'preserve implied (if implemented)', code: 'cp -R proj proj.bak' }
]
EXAMPLES.crontab = [
{ caption: 'list jobs', code: 'crontab -l' },
{ caption: 'install from file', code: 'crontab ~/.crontab' },
{ caption: 'remove all', code: 'crontab -r' }
]
EXAMPLES.cut = [
{ caption: 'fields by delimiter', code: "cut -d: -f1,3 /etc/passwd" },
{ caption: 'characters', code: 'cut -c1-16 file.txt' }
]
EXAMPLES.date = [
{ caption: 'RFC-ish output', code: 'date' },
{ caption: 'epoch seconds', code: 'date +%s' }
]
EXAMPLES.dirname = [
{ caption: 'parent path', code: 'dirname /a/b/c.txt' },
{ caption: 'compose with basename', code: 'p=/x/y/z; echo $(dirname $p)/$(basename $p)' }
]
EXAMPLES.du = [
{ caption: 'sizes under cwd', code: 'du .' },
{ caption: 'human (if supported)', code: 'du -h ~' }
]
EXAMPLES.echo = [
{ caption: 'literal', code: 'echo hello world' },
{ caption: 'no newline (if -n supported)', code: 'echo -n OK' }
]
EXAMPLES.env = [
{ caption: 'print environment', code: 'env' },
{ caption: 'run with override', code: 'env PATH=/bin:/usr/bin man ls' }
]
EXAMPLES.exit = [
{ caption: 'leave session with status', code: 'exit 0' },
{ caption: 'from script', code: '/bin/exit 42' }
]
EXAMPLES.false = [
{ caption: 'force failure in pipeline tests', code: 'false; echo $?' }
]
EXAMPLES.find = [
{ caption: 'files by name glob', code: 'find . -name "*.js"' },
{ caption: 'directories only', code: 'find . -type d' },
{ caption: 'max depth', code: 'find . -maxdepth 2 -type f' },
{ caption: 'OR names', code: 'find . \\( -name "*.c" -o -name "*.h" \\)' }
]
EXAMPLES.getconf = [
{ caption: 'stub', code: '# getconf PATH_MAX — not available on Bare OS' }
]
EXAMPLES.grep = [
{ caption: 'recursive feel (grep each file)', code: 'grep -n error *.log' },
{ caption: 'case insensitive', code: 'grep -i todo NOTES.md' },
{ caption: 'invert (lines without)', code: "grep -v '^#' config" },
{ caption: 'fixed string (no regex)', code: 'grep -F "v1.0" CHANGES' },
{ caption: 'count matches', code: 'grep -c FAIL build.log' },
{ caption: 'only filenames', code: 'grep -l main *.js' },
{ caption: 'multiple patterns', code: 'grep -e foo -e bar file.txt' }
]
EXAMPLES.head = [
{ caption: 'first 10 lines', code: 'head /etc/os-release' },
{ caption: 'first N', code: 'head -n 50 big.log' },
{ caption: 'stdin', code: 'cat long.txt | head' }
]
EXAMPLES.hdms = [
{ caption: 'when booter wires HDMS', code: 'hdms ls /mnt' },
{ caption: 'otherwise', code: '# prints unavailable without ctx.runHdms' }
]
EXAMPLES.help = [
{ caption: 'quick index', code: 'help' },
{ caption: 'then deep dive', code: 'man grep' }
]
EXAMPLES.hostname = [
{ caption: 'show host', code: 'hostname' }
]
EXAMPLES.id = [
{ caption: 'who am I numerically', code: 'id' }
]
EXAMPLES.ln = [
{ caption: 'symlink', code: 'ln -s target name' },
{ caption: 'hard link (if supported)', code: 'ln file linkname' }
]
EXAMPLES.login = [
{ caption: 'unlock existing identity', code: 'login my passphrase words here' },
{ caption: 'register new', code: 'login --new first time passphrase' }
]
EXAMPLES.logout = [
{ caption: 'end session', code: 'logout' },
{ caption: 'save vault hint', code: 'logout --save' }
]
EXAMPLES.logname = [
{ caption: 'login name', code: 'logname' }
]
EXAMPLES.ls = [
{ caption: 'long + hidden', code: 'ls -la ~' },
{ caption: 'one per line', code: 'ls -1 /bin | head' },
{ caption: 'multiple paths', code: 'ls /bin /etc' }
]
EXAMPLES.man = [
{ caption: 'open page', code: 'man sed' },
{ caption: 'handbook TOC (section 7)', code: 'man handbook' },
{ caption: 'handbook chapter by section', code: 'man 7 handbook-01-introduction' },
{ caption: 'apropos', code: 'man -k copy' },
{ caption: 'whatis', code: 'man -f grep' },
{ caption: 'all pages', code: 'man -l' },
{ caption: 'narrow terminal', code: 'MANWIDTH=64 man awk' }
]
EXAMPLES.mkdir = [
{ caption: 'one dir', code: 'mkdir proj' },
{ caption: 'parents', code: 'mkdir -p a/b/c' }
]
EXAMPLES.mkfifo = [
{ caption: 'stub', code: '# FIFOs not on Hyperdrive — use shell pipelines' }
]
EXAMPLES.mv = [
{ caption: 'rename', code: 'mv old.txt new.txt' },
{ caption: 'into dir', code: 'mv *.txt ~/inbox/' }
]
EXAMPLES.nl = [
{ caption: 'number all lines', code: 'nl README.md' }
]
EXAMPLES.od = [
{ caption: 'hex dump vibe', code: 'od -c file.bin | head' }
]
EXAMPLES.pathchk = [
{ caption: 'portable path check', code: 'pathchk -p "$HOME/file name"' }
]
EXAMPLES.printenv = [
{ caption: 'one variable', code: 'printenv HOME' },
{ caption: 'all', code: 'printenv' }
]
EXAMPLES.printf = [
{ caption: 'format', code: 'printf "hex=%x dec=%d\\n" 255 255' },
{ caption: 'no newline', code: 'printf "%s" OK' }
]
EXAMPLES.pwd = [
{ caption: 'where am I', code: 'pwd' }
]
EXAMPLES.readlink = [
{ caption: 'symlink target', code: 'readlink ~/.config' }
]
EXAMPLES.rm = [
{ caption: 'file', code: 'rm tmp.txt' },
{ caption: 'tree', code: 'rm -rf build/' }
]
EXAMPLES.rmdir = [
{ caption: 'empty dir', code: 'rmdir olddir' }
]
EXAMPLES.savevault = [
{ caption: 'snapshot encrypted vault', code: 'savevault' }
]
EXAMPLES.sed = [
{ caption: 'substitute first per line', code: "sed 's/foo/bar/' file.txt" },
{ caption: 'global per line', code: "sed 's/ //g' spaced.txt" },
{ caption: 'in-place (if supported)', code: "sed -i.bak 's/^/# /' f.cfg" },
{ caption: 'print line 5 only', code: "sed -n '5p' file" },
{ caption: 'delete blank lines', code: "sed '/^$/d' file" }
]
EXAMPLES.seq = [
{ caption: '1..10', code: 'seq 1 10' },
{ caption: 'step', code: 'seq 0 2 20' }
]
EXAMPLES.sleep = [
{ caption: 'pause seconds', code: 'sleep 2' }
]
EXAMPLES.sort = [
{ caption: 'lexicographic', code: 'sort names.txt' },
{ caption: 'numeric', code: 'sort -n scores.txt' },
{ caption: 'unique', code: 'sort -u tags.txt' }
]
EXAMPLES.stat = [
{ caption: 'metadata', code: 'stat ~/README.md' }
]
EXAMPLES.tail = [
{ caption: 'last lines', code: 'tail -n 20 app.log' },
{ caption: 'follow vibe (Bare: poll manually)', code: 'tail error.log' }
]
EXAMPLES.tee = [
{ caption: 'copy stdout to file', code: 'cat x | tee copy.txt | wc -l' }
]
EXAMPLES.test = [
{ caption: 'file exists', code: 'test -f ~/.barerc && echo yes' },
{ caption: 'directory', code: 'test -d /home/user' },
{ caption: 'string equal', code: 'test "$USER" = guest' }
]
EXAMPLES.time = [
{ caption: 'wall time a command', code: 'time sort big.txt' }
]
EXAMPLES.touch = [
{ caption: 'create empty', code: 'touch newfile' },
{ caption: 'refresh mtime', code: 'touch -c existing' }
]
EXAMPLES.tr = [
{ caption: 'uppercase', code: "echo hi | tr 'a-z' 'A-Z'" },
{ caption: 'delete chars', code: "tr -d '\\r' < win.txt" }
]
EXAMPLES.true = [
{ caption: 'always success', code: 'true && echo ok' }
]
EXAMPLES.tty = [
{ caption: 'am I a tty', code: 'tty' }
]
EXAMPLES.uname = [
{ caption: 'kernel-ish info', code: 'uname -a' }
]
EXAMPLES.wc = [
{ caption: 'lines words bytes', code: 'wc README.md' },
{ caption: 'stdin only', code: 'cat f | wc -l' }
]
EXAMPLES.which = [
{ caption: 'resolve on PATH', code: 'which ls' }
]
EXAMPLES.whoami = [
{ caption: 'effective user', code: 'whoami' }
]
EXAMPLES.xargs = [
{ caption: 'workaround: shell word split', code: '# for f in *.txt; do grep -l foo $f; done' }
]
/** @type {Record<string, Record<string, unknown>>} */
const EXTRA = {}
EXTRA.chgrp = {
description:
'Changing group ownership is not supported on Bare OS: Hyperdrive metadata is single-session oriented.',
diagnostics: ['chgrp: changing group is not supported on Bare OS'],
bareOsNotes: 'Single-user identity; gid fields exist for display only.'
}
EXTRA.chown = {
description:
'Changing file owner is not supported on Bare OS (single-user Hyperdrive metadata).',
diagnostics: ['chown: changing owner is not supported on Bare OS'],
bareOsNotes: 'Use identity login/logout instead of POSIX ownership changes.'
}
EXTRA.xargs = {
description:
'xargs does not spawn arbitrary /bin utilities on Bare OS. Use shell word splitting or pipelines.',
bareOsNotes: 'No process fork model; see handbook ch.9.'
}
EXTRA.getconf = {
description:
'Host sysconf-style values are not exposed. The command prints an error.',
bareOsNotes: 'Stub only; no kernel sysconf surface.'
}
EXTRA.mkfifo = {
description:
'FIFO special files are not implemented on Hyperdrive. The command reports failure.',
bareOsNotes: 'Documented stub; no real pipes as kernel objects.'
}
EXTRA.chmod = {
synopsis: ['chmod MODE FILE...', 'MODE is octal (e.g. 644) or symbolic (e.g. u+rw)'],
description:
'Sets file mode bits on the VFS. Supports POSIX-style symbolic modes (u/g/o/a, +/-/=, rwxX) and octal modes.',
options: [],
keywords: ['chmod', 'mode', 'permission', 'octal', 'symbolic'],
diagnostics: ['chmod: No such file', 'chmod: invalid mode'],
bareOsNotes: 'Applies to Hyperdrive metadata; not a host inode.'
}
EXTRA.grep = {
synopsis: [
'grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
],
description:
'Searches input or files for lines matching a pattern. Uses JavaScript RegExp unless -F (fixed string). Not bit-identical to GNU grep.',
options: [
{ flag: '-E', meaning: 'Extended regex (accepted; patterns use JS RegExp)' },
{ flag: '-F', meaning: 'Fixed string match' },
{ flag: '-i', meaning: 'Ignore case' },
{ flag: '-v', meaning: 'Invert match' },
{ flag: '-n', meaning: 'Prefix lines with line number' },
{ flag: '-c', meaning: 'Count matching lines only' },
{ flag: '-l', meaning: 'List files with matches' },
{ flag: '-q', meaning: 'Quiet (exit status only)' },
{ flag: '-s', meaning: 'Suppress error messages' },
{ flag: '-H / -h', meaning: 'Force / suppress filename prefix' },
{ flag: '-e pat', meaning: 'Specify pattern' },
{ flag: '-f file', meaning: 'Read patterns from file' }
],
keywords: ['grep', 'search', 'regex', 'pattern', 'filter'],
seeAlso: [
{ name: 'sed', section: 1 },
{ name: 'awk', section: 1 }
],
bareOsNotes: 'UTF-16 strings and JS regex differ from strict POSIX/GNU.'
}
EXTRA.sed = {
description:
'Stream editor with a subset of POSIX sed. Large engine is vendored in lib/sed-engine.js.',
keywords: ['sed', 'stream', 'edit', 'substitute'],
seeAlso: [
{ name: 'awk', section: 1 },
{ name: 'grep', section: 1 }
],
bareOsNotes: 'JavaScript implementation; edge cases differ from GNU sed.'
}
EXTRA.awk = {
description:
'Pattern-directed scanning and processing. Engine in lib/awk-engine.js; not full POSIX awk.',
keywords: ['awk', 'pattern', 'field', 'script'],
seeAlso: [
{ name: 'sed', section: 1 },
{ name: 'grep', section: 1 }
],
bareOsNotes: 'See handbook ch.9 for divergence from Issue 7.'
}
EXTRA.ls = {
synopsis: ['ls [-1al] [FILE...]'],
description:
'Lists directory contents. Long format shows mode, links, owner, group, size, mtime, and symlink targets.',
options: [
{ flag: '-a', meaning: 'Include names starting with .' },
{ flag: '-l', meaning: 'Long listing' },
{ flag: '-1', meaning: 'One name per line (short format)' }
],
keywords: ['ls', 'list', 'directory', 'dir'],
bareOsNotes: 'Hides .bareos_empty marker like other tools.'
}
EXTRA.man = {
synopsis: [
'man [-k keyword] [-f name] [-l] [[section] name]',
'man reads /share/man/man.json on the system drive.'
],
description:
'Displays manual pages from the merged JSON database. Section 1 only in this release.',
options: [
{ flag: '-k, --apropos', meaning: 'Search keywords and titles (substring)' },
{ flag: '-f, --whatis', meaning: 'One-line description for exact name' },
{ flag: '-l, --list', meaning: 'List all manual page names' }
],
environment: ['MANWIDTH — wrap width (default 72, min 40)', 'NO_COLOR — disable bold headings on TTY'],
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
seeAlso: [{ name: 'help', section: 1 }],
bareOsNotes: 'No troff; no embedded DB fallback in v1.'
}
EXTRA.help = {
synopsis: ['help'],
description:
'Prints a one-screen summary of shell builtins and /bin command names. Use man for long-form documentation.',
keywords: ['help', 'summary', 'builtins', 'commands'],
seeAlso: [
{ name: 'man', section: 1 },
{ name: 'bare-os-shell', section: 1 }
]
}
EXTRA.exit = {
synopsis: ['exit [status]'],
description:
'When run as /bin/exit, requests the booter to end the session via ctx.requestBooterExit. Status defaults to 0.',
bareOsNotes: 'Also available as a shell builtin with different wiring.'
}
EXTRA.hdms = {
description:
'Invokes ctx.runHdms when the booter provides HDMS integration; otherwise prints unavailable.',
keywords: ['hdms', 'hyperswarm', 'map'],
bareOsNotes: 'Optional booter capability.'
}
EXTRA.find = {
synopsis: ['find [PATH...] [EXPRESSION]'],
description:
'Walks directories and applies expressions (-name, -type, -print, -maxdepth, logical -and/-or/-not).',
keywords: ['find', 'directory', 'walk', 'search'],
bareOsNotes: 'Expression syntax is a simplified subset.'
}
EXTRA.login = {
description:
'When invoked from /bin, behavior aligns with session identity hooks (see booter). Prefer the shell builtin for passphrase entry.',
keywords: ['login', 'identity', 'passphrase'],
seeAlso: [{ name: 'logout', section: 1 }]
}
EXTRA.logout = {
description: 'Ends session; may persist vault depending on booter and flags.',
keywords: ['logout', 'session'],
seeAlso: [{ name: 'login', section: 1 }]
}
EXTRA.savevault = {
description: 'Encrypts a copy of the personal drive under /.bare/vault/ when identity services are available.',
keywords: ['savevault', 'vault', 'encrypt', 'backup'],
seeAlso: [{ name: 'login', section: 1 }]
}
function basePage(name) {
const title = POSIX_TITLE[name] || name
const p = {
name,
section: 1,
title,
synopsis: [`${name} [OPTION]... [OPERAND]...`],
description: `Bare OS implementation of ${title}. Full behavior is defined in packages/bare-os-coreutils/src/${name}.js.`,
options: [],
keywords: [name, 'bare-os', 'coreutils']
}
if (STUB.has(name)) {
p.stub = true
p.keywords.push('stub')
}
const ex = EXTRA[name]
if (ex) Object.assign(p, ex)
if (EXAMPLES[name]) p.examples = EXAMPLES[name]
return p
}
function gitPage() {
return {
name: 'git',
section: 1,
title: 'Bare OS git front-end (isomorphic-git)',
synopsis: ['git [-C dir] <subcommand> [ARGUMENTS...]'],
description:
'Runs isomorphic-git against the VFS-backed adapter. Remote HTTP(S) uses BARE_OS_GIT_HTTP when set; otherwise Pear bare module fetch.',
options: [
{ flag: '-C dir', meaning: 'Run as if git was started in dir' }
],
environment: [
'BARE_OS_GIT_HTTP — optional fetch implementation for remotes',
'GIT_* — standard hints where supported'
],
keywords: ['git', 'version control', 'repository', 'clone', 'commit', 'isomorphic-git'],
bareOsNotes: 'Not a separate /bin script; booter delegates argv[0]=git to git-cli.js.',
seeAlso: [{ name: 'bare-os-shell', section: 1 }],
examples: [
{ caption: 'new repo', code: 'git init -C ~/myrepo' },
{ caption: 'status', code: 'git -C ~/myrepo status' },
{ caption: 'clone over HTTP (needs remote + fetch)', code: 'git clone https://example.com/repo.git ~/work/repo' },
{ caption: 'config local', code: 'git -C ~/myrepo config user.email "[email protected]"' },
{ caption: 'log one line', code: 'git -C ~/myrepo log --oneline -5' }
]
}
}
function shellPage() {
return {
name: 'bare-os-shell',
section: 1,
title: 'Bare OS interactive shell builtins',
synopsis: ['# builtins only — no full POSIX sh grammar'],
description:
'The line-at-a-time shell supports aliases, simple pipelines (simulated), redirection, and the builtins below. Compound commands (if, for, while) are not available.',
options: [],
aliases: ['sh-builtins'],
keywords: [
'shell',
'builtin',
'cd',
'export',
'alias',
'bare-os-shell',
'sh-builtins'
],
builtins: [
{
name: 'alias',
synopsis: ['alias', 'alias name=value ...', 'unalias name ...'],
description: 'Define or list command aliases. unalias removes definitions.'
},
{
name: 'cd',
synopsis: ['cd [DIR]'],
description: 'Change working directory via vfs.chdir; default is HOME.'
},
{
name: 'export',
synopsis: ['export NAME=value ...'],
description: 'Set environment variables visible to child /bin invocations.'
},
{
name: 'unset',
synopsis: ['unset NAME ...'],
description: 'Remove variables; readonly names cannot be unset.'
},
{
name: 'readonly',
synopsis: ['readonly NAME[=value] ...'],
description: 'Mark variables read-only.'
},
{
name: 'umask',
synopsis: ['umask [octal]'],
description: 'Show or set shell file creation mask (stored in env UMASK).'
},
{
name: 'command',
synopsis: ['command -v|-V NAME', 'command ARGV...'],
description: 'Resolve or run a command without using shell functions (none) or aliases for -v/-V.'
},
{
name: 'type',
synopsis: ['type NAME'],
description: 'Report whether NAME is a builtin or a path under PATH.'
},
{
name: 'login / logout',
synopsis: ['login [--new] passphrase...', 'logout [--save]'],
description: 'Identity unlock/register and session teardown; require booter hooks.'
},
{
name: ':',
synopsis: [':'],
description: 'No-op builtin.'
},
{
name: 'exit',
synopsis: ['exit [n]'],
description: 'Request booter exit with status n (builtin path).'
}
],
seeAlso: [
{ name: 'help', section: 1 },
{ name: 'man', section: 1 }
],
bareOsNotes: 'Pipelines do not use OS pipes; see handbook ch.4 and ch.9.',
examples: [
{ caption: 'pipeline (simulated)', code: 'ls -1 /bin | grep man' },
{ caption: 'redirect out', code: 'echo hi > ~/hello.txt' },
{ caption: 'append', code: 'date >> ~/log.txt' },
{ caption: 'alias + use', code: "alias ll='ls -la'\nll ~" },
{ caption: 'export for children', code: 'export EDITOR=ed\nman ls' },
{ caption: 'temp var for one command', code: 'PATH=/bin man which' }
]
}
}
async function main() {
await mkdir(pagesDir, { recursive: true })
for (const name of COREUTILS_COMMANDS) {
const p = basePage(name)
await writeFile(
join(pagesDir, `${name}.json`),
JSON.stringify(p, null, 2) + '\n'
)
}
for (const name of MAN_EXTRA_PAGES) {
const p = name === 'git' ? gitPage() : shellPage()
await writeFile(
join(pagesDir, `${name}.json`),
JSON.stringify(p, null, 2) + '\n'
)
}
}
await main()