#!/usr/bin/env node // okf-index.mjs // Genererer OKF-kompatible `index.md` per nivaa i en bundle-rot (prosjekt // `.claude/okr/` eller home `~/.claude/okr/org/`). Verbatim OKF-«Documents/kb // Layout»-index-form: // # Overskrift // // okf_version: (KUN rot-index) // // * [title](relativ.md) - description // Ingen frontmatter paa index.md (OKF-reservert). Konsept-filers `title`/ // `description` leses via lib/frontmatter.mjs. Underkataloger faar en peker til // sin egen index.md. Kjoeres PER ROT (de to bundlene har ulik livssyklus). // // Idempotens / vedlikehold (NFR): en eksisterende index.md sin `# overskrift`, // rotens `okf_version`-verdi, og menneske-skrevne beskrivelser for underkatalog- // pekere bevares; konsept-entries regenereres alltid fra frontmatter (autoritativ // kilde). Skriving er atomisk (temp + renameSync), jf. write-org-profile.mjs. // // Zero npm dependencies (node:-builtins). import { readdirSync, readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs'; import { join, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFrontmatter } from '../lib/frontmatter.mjs'; export const OKF_VERSION = 'kb-layout-2026-06'; // "strategisk-kontekst" -> "Strategisk kontekst" function titleFromName(name) { const spaced = name.replace(/[-_]+/g, ' ').trim(); return spaced.charAt(0).toUpperCase() + spaced.slice(1); } // Parse en eksisterende index.md for bevaring: overskrift, okf_version, og // beskrivelser pr. lenke (for underkatalog-pekere). Kaster aldri. function parseExistingIndex(path) { const result = { heading: null, okfVersion: null, descByLink: {} }; if (!existsSync(path)) return result; for (const line of readFileSync(path, 'utf8').split('\n')) { if (result.heading === null && line.startsWith('# ')) { result.heading = line.slice(2).trim(); } const ver = line.match(/^okf_version:\s*(.+)$/); if (ver) result.okfVersion = ver[1].trim(); const entry = line.match(/^\*\s*\[([^\]]*)\]\(([^)]+)\)(?:\s*-\s*(.*))?$/); if (entry) result.descByLink[entry[2]] = { title: entry[1], desc: (entry[3] || '').trim() }; } return result; } // Bygg en enkelt entry-linje paa OKF-form. Tom beskrivelse -> dropp ` - d`. function entryLine(title, link, desc) { return desc ? `* [${title}](${link}) - ${desc}` : `* [${title}](${link})`; } // Generer og skriv index.md for EN katalog (ikke rekursivt). isRoot styrer okf_version. function writeIndexFor(dir, isRoot, okfVersion) { const existing = parseExistingIndex(join(dir, 'index.md')); const dirents = readdirSync(dir, { withFileTypes: true }); const subdirs = dirents.filter((e) => e.isDirectory()).map((e) => e.name).sort(); const concepts = dirents .filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'index.md') .map((e) => e.name) .sort(); const heading = existing.heading || (isRoot ? 'OKF second brain' : titleFromName(basename(dir))); const lines = [`# ${heading}`, '']; if (isRoot) { lines.push(`okf_version: ${existing.okfVersion || okfVersion}`, ''); } for (const sd of subdirs) { const link = `${sd}/index.md`; const prev = existing.descByLink[link]; lines.push(entryLine(prev?.title || titleFromName(sd), link, prev?.desc || 'Underkatalog.')); } for (const c of concepts) { const { get } = parseFrontmatter(readFileSync(join(dir, c), 'utf8')); const title = get('title') || titleFromName(basename(c, '.md')); lines.push(entryLine(title, c, get('description') || '')); } const content = `${lines.join('\n')}\n`; const tmp = join(dir, `.index.md.${process.pid}.tmp`); writeFileSync(tmp, content); renameSync(tmp, join(dir, 'index.md')); } // Generer index.md for rot + alle underkataloger, rekursivt. export function generateIndexes(root, opts = {}) { const okfVersion = opts.okfVersion || OKF_VERSION; const walk = (dir, isRoot) => { writeIndexFor(dir, isRoot, okfVersion); for (const e of readdirSync(dir, { withFileTypes: true })) { if (e.isDirectory()) walk(join(dir, e.name), false); } }; if (!existsSync(root)) throw new Error(`Bundle-rot finnes ikke: ${root}`); walk(root, true); } // --- CLI --- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isMain) { const root = process.argv[2]; if (!root) { process.stderr.write('Bruk: node okf-index.mjs [--okf-version ]\n'); process.exit(2); } const vi = process.argv.indexOf('--okf-version'); const okfVersion = vi !== -1 ? process.argv[vi + 1] : undefined; generateIndexes(root, { okfVersion }); process.stdout.write(`OKF-index generert for ${root}\n`); }