149 lines
4.1 KiB
TypeScript
149 lines
4.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
import { dirname, join, relative } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const DOCS_DIR = resolveDocsDir()
|
|
|
|
const EXCLUDED_DIRS = new Set(['archive', 'research'])
|
|
|
|
function resolveDocsDir() {
|
|
const env = process.env.DOCS_DIR?.trim()
|
|
if (env) return env
|
|
|
|
const fromCwd = join(process.cwd(), 'docs')
|
|
if (existsSync(fromCwd)) return fromCwd
|
|
|
|
const docsListFile = fileURLToPath(import.meta.url)
|
|
const docsListDir = dirname(docsListFile)
|
|
return join(docsListDir, '..', 'docs')
|
|
}
|
|
|
|
function compactStrings(values: unknown[]): string[] {
|
|
const result: string[] = []
|
|
for (const value of values) {
|
|
if (value === null || value === undefined) continue
|
|
const normalized = String(value).trim()
|
|
if (normalized.length > 0) result.push(normalized)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function walkMarkdownFiles(dir: string, base: string = dir): string[] {
|
|
const entries = readdirSync(dir, { withFileTypes: true })
|
|
const files: string[] = []
|
|
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))
|
|
continue
|
|
}
|
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
files.push(relative(base, fullPath))
|
|
}
|
|
}
|
|
return files.sort((a, b) => a.localeCompare(b))
|
|
}
|
|
|
|
function extractMetadata(fullPath: string): {
|
|
summary: string | null
|
|
readWhen: string[]
|
|
error?: string
|
|
} {
|
|
const content = readFileSync(fullPath, 'utf8')
|
|
|
|
if (!content.startsWith('---')) {
|
|
return { summary: null, readWhen: [], error: 'missing front matter' }
|
|
}
|
|
|
|
const endIndex = content.indexOf('\n---', 3)
|
|
if (endIndex === -1) {
|
|
return { summary: null, readWhen: [], error: 'unterminated front matter' }
|
|
}
|
|
|
|
const frontMatter = content.slice(3, endIndex).trim()
|
|
const lines = frontMatter.split('\n')
|
|
|
|
let summaryLine: string | null = null
|
|
const readWhen: string[] = []
|
|
let collectingField: 'read_when' | null = null
|
|
|
|
for (const rawLine of lines) {
|
|
const line = rawLine.trim()
|
|
|
|
if (line.startsWith('summary:')) {
|
|
summaryLine = line
|
|
collectingField = null
|
|
continue
|
|
}
|
|
|
|
if (line.startsWith('read_when:')) {
|
|
collectingField = 'read_when'
|
|
const inline = line.slice('read_when:'.length).trim()
|
|
if (inline.startsWith('[') && inline.endsWith(']')) {
|
|
try {
|
|
const parsed = JSON.parse(inline.replace(/'/g, '"')) as unknown
|
|
if (Array.isArray(parsed)) {
|
|
readWhen.push(...compactStrings(parsed))
|
|
}
|
|
} catch {
|
|
// ignore malformed inline arrays
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (collectingField === 'read_when') {
|
|
if (line.startsWith('- ')) {
|
|
const hint = line.slice(2).trim()
|
|
if (hint) readWhen.push(hint)
|
|
} else if (line === '') {
|
|
// ignore
|
|
} else {
|
|
collectingField = null
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!summaryLine) {
|
|
return { summary: null, readWhen, error: 'summary key missing' }
|
|
}
|
|
|
|
const summaryValue = summaryLine.slice('summary:'.length).trim()
|
|
const normalized = summaryValue
|
|
.replace(/^['"]|['"]$/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
|
|
if (!normalized) {
|
|
return { summary: null, readWhen, error: 'summary is empty' }
|
|
}
|
|
|
|
return { summary: normalized, readWhen }
|
|
}
|
|
|
|
console.log('Listing all markdown files in docs folder:')
|
|
|
|
const markdownFiles = walkMarkdownFiles(DOCS_DIR)
|
|
|
|
for (const relativePath of markdownFiles) {
|
|
const fullPath = join(DOCS_DIR, relativePath)
|
|
const { summary, readWhen, error } = extractMetadata(fullPath)
|
|
if (summary) {
|
|
console.log(`${relativePath} - ${summary}`)
|
|
if (readWhen.length > 0) {
|
|
console.log(` Read when: ${readWhen.join('; ')}`)
|
|
}
|
|
} else {
|
|
const reason = error ? ` - [${error}]` : ''
|
|
console.log(`${relativePath}${reason}`)
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
'\nReminder: keep docs up to date as behavior changes. When your task matches any "Read when" hint above, read that doc before coding, and suggest new coverage when it is missing.',
|
|
)
|