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' 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, '..') 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', 'listCategory' ]) 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 1–8`) 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.listCategory !== undefined) { const ok = ['coreutils', 'extra', 'handbook', 'devguide', 'docs', 'usersmanual'] 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`) 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 extraSet = new Set(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})` ) } 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}"` ) 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] 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 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 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(), 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() }