clawhub/scripts/docs-list.ts
2026-01-07 17:58:00 +01:00

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.',
)