166 lines
4.8 KiB
JavaScript
166 lines
4.8 KiB
JavaScript
#!/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.');
|