Dev guide

This commit is contained in:
Raven Scott
2026-04-03 05:43:13 -04:00
parent 2d26a0b8d1
commit 6259c25d4c
25 changed files with 1207 additions and 25 deletions
+11 -2
View File
@@ -796,24 +796,33 @@ test('runGitCli init and status on personal drive', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('kernel share/man/man.json page count matches coreutils + extras + handbook', async (t) => {
test('kernel share/man/man.json page count matches coreutils + extras + handbook + devguide', async (t) => {
const manPath = path.join(__dirname, '../../kernel/share/man/man.json')
const raw = JSON.parse(await readFile(manPath, 'utf8'))
const { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } = await import(
'../bare-os-coreutils/lib/commands.mjs'
)
const handbookDir = path.join(__dirname, '../../handbook')
const devguideDir = path.join(__dirname, '../../developer-guide')
const handbookMd = (await readdir(handbookDir)).filter((f) => f.endsWith('.md')).length
const devguideMd = (await readdir(devguideDir)).filter((f) => f.endsWith('.md')).length
t.is(
raw.pages.length,
COREUTILS_COMMANDS.length + MAN_EXTRA_PAGES.length + handbookMd
COREUTILS_COMMANDS.length +
MAN_EXTRA_PAGES.length +
handbookMd +
devguideMd
)
t.ok(Array.isArray(raw.apropos) && raw.apropos.length > 0)
t.is(typeof raw.index.ls, 'number')
t.is(typeof raw.index.handbook, 'number')
t.is(typeof raw.index.devguide, 'number')
const hb = raw.pages[raw.index.handbook]
t.is(hb.section, 7)
t.is(hb.name, 'bare-os-handbook')
const dg = raw.pages[raw.index.devguide]
t.is(dg.section, 7)
t.is(dg.name, 'bare-os-developer-guide')
})
test('runBinCommand man ls prints manual text', async (t) => {
@@ -61,6 +61,11 @@
"type": "string",
"enum": ["wrap", "preserve"]
},
"listCategory": {
"description": "Merged DB only: group for man -l (coreutils, extra, handbook, devguide)",
"type": "string",
"enum": ["coreutils", "extra", "handbook", "devguide"]
},
"keywords": {
"type": "array",
"items": { "type": "string" }
@@ -7,6 +7,7 @@ import {
MAN_EXTRA_PAGES
} from '../lib/commands.mjs'
import { buildHandbookManPages } from './ingest-handbook-for-man.mjs'
import { buildDeveloperGuideManPages } from './ingest-developer-guide-for-man.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = join(__dirname, '..')
@@ -46,7 +47,8 @@ function validatePage(raw, pathLabel) {
'stub',
'builtins',
'examples',
'descriptionMode'
'descriptionMode',
'listCategory'
])
for (const k of Object.keys(raw)) {
if (!allowed.has(k)) throw new Error(`${pathLabel}: unknown field "${k}"`)
@@ -132,6 +134,11 @@ function validatePage(raw, pathLabel) {
if (raw.descriptionMode !== 'wrap' && raw.descriptionMode !== 'preserve')
throw new Error(`${pathLabel}: descriptionMode must be "wrap" or "preserve"`)
}
if (raw.listCategory !== undefined) {
const ok = ['coreutils', 'extra', 'handbook', 'devguide']
if (!ok.includes(raw.listCategory))
throw new Error(`${pathLabel}: listCategory must be one of ${ok.join(', ')}`)
}
if (raw.aliases !== undefined) {
if (!Array.isArray(raw.aliases))
throw new Error(`${pathLabel}: aliases must be array of strings`)
@@ -195,6 +202,7 @@ function tokenizeForApropos(text) {
export async function buildManDb() {
const required = [...COREUTILS_COMMANDS, ...MAN_EXTRA_PAGES]
const extraSet = new Set(MAN_EXTRA_PAGES)
const pages = []
const index = Object.create(null)
const apropos = []
@@ -208,6 +216,7 @@ export async function buildManDb() {
} catch (e) {
throw new Error(`man: missing or invalid JSON for "${cmd}": ${filePath} (${e.message})`)
}
raw.listCategory = extraSet.has(cmd) ? 'extra' : 'coreutils'
validatePage(raw, cmd)
if (raw.name !== cmd)
throw new Error(`man/pages/${cmd}.json: "name" must be "${cmd}", got "${raw.name}"`)
@@ -242,6 +251,40 @@ export async function buildManDb() {
const handbookPages = await buildHandbookManPages(repoRoot)
for (let hi = 0; hi < handbookPages.length; hi++) {
const raw = handbookPages[hi]
raw.listCategory = 'handbook'
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 developerGuidePages = await buildDeveloperGuideManPages(repoRoot)
for (let di = 0; di < developerGuidePages.length; di++) {
const raw = developerGuidePages[di]
raw.listCategory = 'devguide'
validatePage(raw, raw.name)
pages.push(raw)
const pageIdx = pages.length - 1
@@ -0,0 +1,116 @@
/**
* Convert repo developer-guide/*.md into man(7) page objects (same pipeline as handbook).
*/
import { readFile, readdir } from 'fs/promises'
import { join } from 'path'
import { handbookMdToDescription } from './ingest-handbook-for-man.mjs'
const SECTION = 7
function firstHeadingTitle(md) {
const m = md.match(/^#\s+(.+)$/m)
if (!m) return 'Bare OS developer guide'
let t = m[1]
t = t.replace(/\*\*([^*]+)\*\*/g, '$1')
t = t.replace(/`([^`]+)`/g, '$1')
return t.trim()
}
/**
* @param {string} repoRoot
* @returns {Promise<object[]>}
*/
export async function buildDeveloperGuideManPages(repoRoot) {
const dir = join(repoRoot, 'developer-guide')
let names
try {
names = (await readdir(dir)).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-developer-guide',
title: 'Bare OS developer guide — index and reading order',
aliases: ['developer-guide', 'devguide']
})
} else {
const base = file.replace(/\.md$/i, '')
meta.push({
file,
name: 'devguide-' + base,
title: '',
aliases: []
})
}
}
const pages = []
for (let j = 0; j < meta.length; j++) {
const m = meta[j]
const filePath = join(dir, m.file)
const md = await readFile(filePath, 'utf8')
const title = m.title || firstHeadingTitle(md)
const description = handbookMdToDescription(md, 'developer-guide/' + 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 })
seeAlso.push({ name: 'bare-os-handbook', section: SECTION })
const kw = new Set([
'developer',
'devguide',
'develop',
'script',
'asyncfunction',
'ctx',
'bare-os',
'guide'
])
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,
'Developer guide chapter (developer-guide/' + m.file + ')'
],
description,
descriptionMode: 'preserve',
options: [],
keywords: Array.from(kw),
seeAlso,
bareOsNotes:
'Generated at build time from developer-guide/' +
m.file +
'. Mermaid diagrams omitted in terminal; see repo Markdown for figures.'
}
if (m.aliases && m.aliases.length) page.aliases = m.aliases
pages.push(page)
}
return pages
}
@@ -448,15 +448,23 @@ EXTRA.man = {
'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.',
'Displays manual pages from the merged JSON database. Section 1: /bin and git/shell pages. Section 7: handbook (man handbook) and developer guide (man devguide), merged at build from handbook/*.md and developer-guide/*.md.',
options: [
{ flag: '-k, --apropos', meaning: 'Search keywords and titles (substring)' },
{ flag: '-f, --whatis', meaning: 'One-line description for exact name' },
{ flag: '-l, --list', meaning: 'List all manual page names' }
{
flag: '-l, --list',
meaning:
'List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically'
}
],
environment: ['MANWIDTH — wrap width (default 72, min 40)', 'NO_COLOR — disable bold headings on TTY'],
keywords: ['man', 'manual', 'help', 'documentation', 'apropos', 'whatis', 'cheat', 'examples'],
seeAlso: [{ name: 'help', section: 1 }],
seeAlso: [
{ name: 'help', section: 1 },
{ name: 'bare-os-handbook', section: 7 },
{ name: 'bare-os-developer-guide', section: 7 }
],
bareOsNotes: 'No troff; no embedded DB fallback in v1.'
}
EXTRA.help = {
+43 -5
View File
@@ -6,7 +6,7 @@ async function run(ctx, argv) {
function usage() {
ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
' Section 1: /bin utilities; section 7: Bare OS handbook (man 7 bare-os-handbook).\n' +
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
' Data: /share/man/man.json on the system drive.'
)
ctx.exitCode = 2
@@ -92,10 +92,48 @@ async function run(ctx, argv) {
}
if (mode === 'list') {
const rows = db.pages
.map((p) => ({ n: p.name, s: p.section }))
.sort((a, b) => (a.n !== b.n ? (a.n < b.n ? -1 : 1) : a.s - b.s))
for (const r of rows) ctx.console.log(r.n + '(' + r.s + ')')
const CAT_ORDER = ['coreutils', 'extra', 'handbook', 'devguide']
const CAT_HEADING = {
coreutils: 'Section 1 — /bin utilities',
extra: 'Section 1 — Git and shell',
handbook: 'Section 7 — Handbook',
devguide: 'Section 7 — Developer guide'
}
function listCategoryOf(p) {
const c = p.listCategory
if (c === 'coreutils' || c === 'extra' || c === 'handbook' || c === 'devguide')
return c
if (p.section === 7) {
const n = p.name
if (n.startsWith('devguide-') || n === 'bare-os-developer-guide') return 'devguide'
return 'handbook'
}
if (p.name === 'git' || p.name === 'bare-os-shell') return 'extra'
return 'coreutils'
}
const rows = db.pages.map((p) => ({
n: p.name,
s: p.section,
cat: listCategoryOf(p)
}))
rows.sort((a, b) => {
const ia = CAT_ORDER.indexOf(a.cat)
const ib = CAT_ORDER.indexOf(b.cat)
const ca = ia === -1 ? 99 : ia
const cb = ib === -1 ? 99 : ib
if (ca !== cb) return ca - cb
if (a.n !== b.n) return a.n < b.n ? -1 : 1
return a.s - b.s
})
let prevCat = ''
for (const r of rows) {
if (r.cat !== prevCat) {
if (prevCat !== '') ctx.console.log('')
ctx.console.log(CAT_HEADING[r.cat] || r.cat)
prevCat = r.cat
}
ctx.console.log(' ' + r.n + '(' + r.s + ')')
}
return
}
+43 -5
View File
@@ -275,7 +275,7 @@ async function run(ctx, argv) {
function usage() {
ctx.console.error(
'usage: man [-k keyword] [-f name] [-l] [[section] name]\n' +
' Section 1: /bin utilities; section 7: Bare OS handbook (man 7 bare-os-handbook).\n' +
' Section 1: /bin; section 7: handbook + developer guide (man handbook, man devguide).\n' +
' Data: /share/man/man.json on the system drive.'
)
ctx.exitCode = 2
@@ -361,10 +361,48 @@ async function run(ctx, argv) {
}
if (mode === 'list') {
const rows = db.pages
.map((p) => ({ n: p.name, s: p.section }))
.sort((a, b) => (a.n !== b.n ? (a.n < b.n ? -1 : 1) : a.s - b.s))
for (const r of rows) ctx.console.log(r.n + '(' + r.s + ')')
const CAT_ORDER = ['coreutils', 'extra', 'handbook', 'devguide']
const CAT_HEADING = {
coreutils: 'Section 1 — /bin utilities',
extra: 'Section 1 — Git and shell',
handbook: 'Section 7 — Handbook',
devguide: 'Section 7 — Developer guide'
}
function listCategoryOf(p) {
const c = p.listCategory
if (c === 'coreutils' || c === 'extra' || c === 'handbook' || c === 'devguide')
return c
if (p.section === 7) {
const n = p.name
if (n.startsWith('devguide-') || n === 'bare-os-developer-guide') return 'devguide'
return 'handbook'
}
if (p.name === 'git' || p.name === 'bare-os-shell') return 'extra'
return 'coreutils'
}
const rows = db.pages.map((p) => ({
n: p.name,
s: p.section,
cat: listCategoryOf(p)
}))
rows.sort((a, b) => {
const ia = CAT_ORDER.indexOf(a.cat)
const ib = CAT_ORDER.indexOf(b.cat)
const ca = ia === -1 ? 99 : ia
const cb = ib === -1 ? 99 : ib
if (ca !== cb) return ca - cb
if (a.n !== b.n) return a.n < b.n ? -1 : 1
return a.s - b.s
})
let prevCat = ''
for (const r of rows) {
if (r.cat !== prevCat) {
if (prevCat !== '') ctx.console.log('')
ctx.console.log(CAT_HEADING[r.cat] || r.cat)
prevCat = r.cat
}
ctx.console.log(' ' + r.n + '(' + r.s + ')')
}
return
}
File diff suppressed because one or more lines are too long