Update Manpage

This commit is contained in:
Raven Scott
2026-04-06 07:32:05 -04:00
parent 7f0f5277a9
commit 8322f304d8
19 changed files with 655 additions and 61 deletions
@@ -5,6 +5,8 @@ import { fileURLToPath, pathToFileURL } from 'url'
import { COREUTILS_COMMANDS, MAN_EXTRA_PAGES } from '../lib/commands.mjs'
import { buildHandbookManPages } from './ingest-handbook-for-man.mjs'
import { buildDeveloperGuideManPages } from './ingest-developer-guide-for-man.mjs'
import { buildDocsManPages } from './ingest-docs-for-man.mjs'
import { buildUsersManualManPages } from './ingest-users-manual-for-man.mjs'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkgRoot = join(__dirname, '..')
@@ -134,7 +136,7 @@ function validatePage(raw, pathLabel) {
)
}
if (raw.listCategory !== undefined) {
const ok = ['coreutils', 'extra', 'handbook', 'devguide']
const ok = ['coreutils', 'extra', 'handbook', 'devguide', 'docs', 'usersmanual']
if (!ok.includes(raw.listCategory))
throw new Error(
`${pathLabel}: listCategory must be one of ${ok.join(', ')}`
@@ -337,6 +339,72 @@ export async function buildManDb() {
}
}
const docsPages = await buildDocsManPages(repoRoot)
for (let di = 0; di < docsPages.length; di++) {
const raw = docsPages[di]
raw.listCategory = 'docs'
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 usersManualPages = await buildUsersManualManPages(repoRoot)
for (let ui = 0; ui < usersManualPages.length; ui++) {
const raw = usersManualPages[ui]
raw.listCategory = 'usersmanual'
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(),
@@ -0,0 +1,146 @@
/**
* Convert repo docs/ tree (.md files, recursive) into man(7) page objects (same pipeline as handbook).
*/
import { readFile, readdir } from 'fs/promises'
import { join, relative } 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 documentation'
let t = m[1]
t = t.replace(/\*\*([^*]+)\*\*/g, '$1')
t = t.replace(/`([^`]+)`/g, '$1')
return t.trim()
}
/**
* @param {string} absDir
* @param {string} baseAbs
* @returns {Promise<string[]>}
*/
async function collectMdRelPaths(absDir, baseAbs) {
/** @type {string[]} */
const out = []
const ents = await readdir(absDir, { withFileTypes: true })
ents.sort((a, b) => a.name.localeCompare(b.name))
for (const ent of ents) {
const full = join(absDir, ent.name)
if (ent.isDirectory()) {
out.push(...(await collectMdRelPaths(full, baseAbs)))
} else if (ent.isFile() && ent.name.endsWith('.md')) {
out.push(relative(baseAbs, full).replace(/\\/g, '/'))
}
}
return out
}
/**
* @param {string} relPosix path under docs/, e.g. reference/foo.md
*/
function relPathToManMeta(relPosix) {
if (relPosix === 'README.md') {
return {
rel: relPosix,
name: 'bare-os-docs',
title:
'Bare OS documentation (docs/) — hub, maps, and where to read next',
aliases: ['docs', 'documentation', 'bare-os-documentation']
}
}
const base = relPosix.replace(/\.md$/i, '')
const slug = base.split('/').map((s) => s.toLowerCase()).join('-')
return {
rel: relPosix,
name: 'docs-' + slug,
title: '',
aliases: []
}
}
/**
* @param {string} repoRoot
* @returns {Promise<object[]>}
*/
export async function buildDocsManPages(repoRoot) {
const docsDir = join(repoRoot, 'docs')
let relPaths
try {
relPaths = await collectMdRelPaths(docsDir, docsDir)
} catch {
return []
}
relPaths.sort((a, b) => a.localeCompare(b))
/** @type {{ rel: string, name: string, title: string, aliases?: string[] }[]} */
const meta = relPaths.map(relPathToManMeta)
const seen = new Set()
for (const m of meta) {
if (seen.has(m.name))
throw new Error(`ingest-docs-for-man: duplicate man name "${m.name}"`)
seen.add(m.name)
}
const pages = []
for (let j = 0; j < meta.length; j++) {
const m = meta[j]
const filePath = join(docsDir, m.rel)
const md = await readFile(filePath, 'utf8')
const title = m.title || firstHeadingTitle(md)
const description = handbookMdToDescription(md, 'docs/' + m.rel)
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([
'docs',
'documentation',
'bare-os',
'reference',
'markdown'
])
for (const t of m.name.split(/[^a-z0-9]+/i)) {
if (t.length > 1) kw.add(t.toLowerCase())
}
for (const seg of m.rel.replace(/\.md$/i, '').split(/[/\\]+/)) {
for (const t of seg.toLowerCase().split(/[^a-z0-9]+/)) {
if (t.length > 2) kw.add(t)
}
}
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,
'Documentation page (plain text from docs/' + m.rel + ')'
],
description,
descriptionMode: 'preserve',
options: [],
keywords: Array.from(kw),
seeAlso,
bareOsNotes:
'Generated at build time from docs/' +
m.rel +
'. 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
}
@@ -0,0 +1,114 @@
/**
* Convert repo users-manual/*.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 user manual'
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 buildUsersManualManPages(repoRoot) {
const dir = join(repoRoot, 'users-manual')
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-users-manual',
title: 'Bare OS user manual — index and reading order',
aliases: ['users-manual', 'user-manual']
})
} else {
const base = file.replace(/\.md$/i, '')
meta.push({
file,
name: 'users-manual-' + 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, 'users-manual/' + 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([
'user',
'manual',
'tutorial',
'howto',
'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,
'User manual chapter (users-manual/' + m.file + ')'
],
description,
descriptionMode: 'preserve',
options: [],
keywords: Array.from(kw),
seeAlso,
bareOsNotes:
'Generated at build time from users-manual/' +
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
}
@@ -243,7 +243,7 @@ EXAMPLES.man = [
{ 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' }
{ caption: 'fixed width (overrides TTY)', code: 'MANWIDTH=64 man awk' }
]
EXAMPLES.mkdir = [
{ caption: 'one dir', code: 'mkdir proj' },
@@ -504,7 +504,7 @@ EXTRA.man = {
'man reads /share/man/man.json on the system drive.'
],
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), user manual (man users-manual), developer guide (man devguide), and docs/ (man docs), merged at build from handbook/*.md, users-manual/*.md, developer-guide/*.md, and docs/**/*.md.',
options: [
{
flag: '-k, --apropos',
@@ -514,11 +514,12 @@ EXTRA.man = {
{
flag: '-l, --list',
meaning:
'List pages grouped by category (/bin, git/shell, handbook, developer guide), then alphabetically'
'List pages grouped by category (/bin, git/shell, handbook, user manual, developer guide, docs/), then alphabetically'
}
],
environment: [
'MANWIDTH — wrap width (default 72, min 40)',
'MANWIDTH — if set, wrap width (clamped 40200); overrides auto width',
'COLUMNS — when stdout is not a TTY (or output is captured), used if MANWIDTH unset',
'NO_COLOR — disable bold headings on TTY'
],
keywords: [
@@ -534,7 +535,9 @@ EXTRA.man = {
seeAlso: [
{ name: 'help', section: 1 },
{ name: 'bare-os-handbook', section: 7 },
{ name: 'bare-os-developer-guide', section: 7 }
{ name: 'bare-os-developer-guide', section: 7 },
{ name: 'bare-os-docs', section: 7 },
{ name: 'bare-os-users-manual', section: 7 }
],
bareOsNotes: 'No troff; no embedded DB fallback in v1.'
}