docs-list.mjs•4.93 kB
#!/usr/bin/env node
/**
* Lists documentation summaries so agents can see what to read before coding.
* The format mirrors the helper from steipete/agent-scripts but tolerates
* legacy files that lack front matter by falling back to the first heading.
*/
import { readdirSync, readFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DOCS_DIR = join(__dirname, '..', 'docs');
const EXCLUDED_DIRS = new Set(['archive', 'research']);
function walkMarkdownFiles(dir, base = dir) {
const entries = readdirSync(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (EXCLUDED_DIRS.has(entry.name)) {
continue;
}
files.push(...walkMarkdownFiles(fullPath, base));
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
files.push(relative(base, fullPath));
}
}
return files.sort((a, b) => a.localeCompare(b));
}
function extractMetadata(fullPath) {
const content = readFileSync(fullPath, 'utf8');
const issues = [];
const readWhen = [];
if (!content.startsWith('---')) {
const summary = deriveHeadingSummary(content) ?? '(add summary front matter)';
issues.push('front matter missing');
return { summary, readWhen, issues };
}
const endIndex = content.indexOf('\n---', 3);
if (endIndex === -1) {
const summary = deriveHeadingSummary(content) ?? '(front matter incomplete)';
issues.push('unterminated front matter');
return { summary, readWhen, issues };
}
const frontMatter = content.slice(3, endIndex).trim();
const lines = frontMatter.split('\n');
let summaryLine = null;
let collectingReadWhen = false;
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith('summary:')) {
summaryLine = line.slice('summary:'.length).trim();
collectingReadWhen = false;
continue;
}
if (line.startsWith('read_when:')) {
collectingReadWhen = true;
const inline = line.slice('read_when:'.length).trim();
if (inline.startsWith('[') && inline.endsWith(']')) {
collectingReadWhen = false;
try {
const parsed = JSON.parse(inline.replace(/'/g, '"'));
if (Array.isArray(parsed)) {
for (const item of parsed) {
if (typeof item === 'string' && item.trim().length > 0) {
readWhen.push(item.trim());
}
}
}
} catch {
issues.push('read_when inline array malformed');
}
}
continue;
}
if (collectingReadWhen) {
if (line.startsWith('- ')) {
const hint = line.slice(2).trim();
if (hint.length > 0) {
readWhen.push(hint);
}
} else if (line.length === 0) {
continue;
} else {
collectingReadWhen = false;
}
}
}
if (!summaryLine) {
issues.push('summary key missing');
}
const summaryValue = normalizeSummary(summaryLine);
if (!summaryValue) {
issues.push('summary is empty');
}
const summary =
summaryValue ?? deriveHeadingSummary(content.slice(endIndex + 4)) ?? '(add summary front matter)';
return { summary, readWhen, issues };
}
function normalizeSummary(value) {
if (!value) return null;
const trimmed = value.replace(/^['"]|['"]$/g, '').replace(/\s+/g, ' ').trim();
return trimmed.length > 0 ? trimmed : null;
}
function deriveHeadingSummary(content) {
const lines = content.split('\n');
for (const rawLine of lines) {
const line = rawLine.trim();
if (line.startsWith('#')) {
const heading = line.replace(/^#+\s*/, '').trim();
if (heading.length > 0) {
return heading;
}
}
if (line.length > 0) {
// Bail once we hit real content to avoid scanning entire file.
break;
}
}
return null;
}
console.log('Listing documentation summaries (docs/):\n');
const markdownFiles = walkMarkdownFiles(DOCS_DIR);
for (const relativePath of markdownFiles) {
const fullPath = join(DOCS_DIR, relativePath);
const { summary, readWhen, issues } = extractMetadata(fullPath);
const suffix = issues.length > 0 ? ` [${issues.join(', ')}]` : '';
console.log(`${relativePath} - ${summary}${suffix}`);
if (readWhen.length > 0) {
console.log(` Read when: ${readWhen.join('; ')}`);
}
}
console.log('\nIf a doc is missing front matter, add:');
console.log('---');
console.log("summary: 'Short imperative summary'");
console.log('read_when:');
console.log(' - condition 1');
console.log(' - condition 2');
console.log('---');
console.log('before the first heading so the helper can surface it contextually.');