agentos / docs /generate-docs.mjs
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/agentos
c5f93fc verified
Raw
History Blame Contribute Delete
7.07 kB
#!/usr/bin/env node
/**
* SNAPKITTY DOCS GENERATOR
* Continuously generates documentation from code
* Run: node generate-docs.mjs
* Or: node generate-docs.mjs --watch
*/
import { readdirSync, readFileSync, writeFileSync, statSync, existsSync, mkdirSync } from 'fs'
import { join, basename, extname } from 'path'
import { spawn } from 'child_process'
const CONFIG = {
inputDirs: ['src', 'lib', 'proofs', 'docs'],
outputDir: 'docs/generated',
extensions: ['.rs', '.lean', '.py', '.apl', '.hs', '.mjs', '.js', '.pl', '.md'],
ignore: ['node_modules', 'target', 'build', '.git'],
watch: process.argv.includes('--watch'),
interval: 5000 // 5 seconds
}
function log(msg, level = 'info') {
const colors = { error: '\x1b[31m', warn: '\x1b[33m', info: '\x1b[36m', success: '\x1b[32m' }
const color = colors[level] || ''
const reset = '\x1b[0m'
console.log(`[${new Date().toISOString()}] ${color}${level.toUpperCase()}: ${msg}${reset}`)
}
function isIgnored(dir) {
return CONFIG.ignore.includes(basename(dir))
}
function isValidFile(file) {
return CONFIG.extensions.includes(extname(file).toLowerCase())
}
function extractDocComment(content, ext) {
const patterns = {
'.rs': /\/\*\*(.*?)\*\/|\/\/\!\s*(.*)/gs,
'.lean': /\/\-\!(.*?)$/gm,
'.py': /"""(.*?)"""|#\s*(.*)/gs,
'.apl': /\⍝\s*(.*)/g,
'.hs': /\-\-\!(.*?)$/gm,
'.mjs': /\/\*\*(.*?)\*\/|\/\/\s*(.*)/gs,
'.js': /\/\*\*(.*?)\*\/|\/\/\s*(.*)/gs,
'.pl': /#\s*(.*)/g,
'.md': /(.*?)/gs
}
const pattern = patterns[ext] || /\/\*(.*?)\*\//gs
const matches = [...content.matchAll(pattern)]
return matches.map(m => {
const text = (m[1] || m[2] || m[0] || '').trim()
return text ? text.replace(/^\s*[\/*#⍝\-]+\s*/gm, '').trim() : null
}).filter(Boolean).join('\n\n')
}
function parseFunction(content, ext) {
// Extract function signatures
const signatures = []
if (['.rs', '.mjs', '.js'].includes(ext)) {
const funcPattern = /fn\s+(\w+)\s*\(([^)]*)\)\s*[^{]*{/g
for (const match of content.matchAll(funcPattern)) {
signatures.push(`fn ${match[1]}(${match[2]})`)
}
}
if (['.lean'].includes(ext)) {
const funcPattern = /def\s+(\w+)\s*\(([^)]*)\)\s*:/g
for (const match of content.matchAll(funcPattern)) {
signatures.push(`def ${match[1]}(${match[2]})`)
}
}
if (['.py'].includes(ext)) {
const funcPattern = /def\s+(\w+)\s*\(([^)]*)\)\s*:/g
for (const match of content.matchAll(funcPattern)) {
signatures.push(`def ${match[1]}(${match[2]})`)
}
}
return signatures
}
function generateFileDoc(filePath) {
try {
const content = readFileSync(filePath, 'utf8')
const ext = extname(filePath)
const relativePath = filePath.replace(process.cwd() + '/', '')
const docComment = extractDocComment(content, ext)
const functions = parseFunction(content, ext)
let doc = `# ${relativePath}\n\n`
if (docComment) {
doc += `## Description\n\n${docComment}\n\n`
}
if (functions.length > 0) {
doc += `## Functions/Definitions\n\n`
doc += functions.map(f => `- \`${f}\``).join('\n')
doc += '\n\n'
}
doc += `---\n\n`
doc += `[Back to index](./index.md)\n`
return doc
} catch (e) {
log(`Error processing ${filePath}: ${e.message}`, 'warn')
return null
}
}
function scanFiles(dir = '.') {
const files = []
try {
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = join(dir, entry)
const stat = statSync(fullPath)
if (isIgnored(fullPath)) continue
if (stat.isDirectory()) {
files.push(...scanFiles(fullPath))
} else if (stat.isFile() && isValidFile(fullPath)) {
files.push(fullPath)
}
}
} catch (e) {
log(`Error scanning ${dir}: ${e.message}`, 'warn')
}
return files
}
function generateIndex(files) {
const grouped = {}
for (const file of files) {
const ext = extname(file)
const dir = basename(join(file, '..'))
if (!grouped[ext]) grouped[ext] = []
if (!grouped[dir]) grouped[dir] = []
const relative = file.replace(process.cwd() + '/', '')
grouped[ext].push(relative)
grouped[dir].push(relative)
}
let index = '# Documentation Index\n\n'
index += 'Automatically generated from source code\n\n'
index += `---\n\n`
// By directory
index += '## By Directory\n\n'
for (const [dir, files] of Object.entries(grouped)) {
if (dir.startsWith('.')) continue
index += `\n### ${dir}/\n\n`
for (const file of files.sort()) {
const link = file.replace(/\\/g, '/').replace(/ /g, '%20')
index += `- [${file}](${link}.md)\n`
}
}
// By type
index += '\n\n## By Type\n\n'
for (const [ext, files] of Object.entries(grouped)) {
if (!ext.startsWith('.')) continue
const type = ext.substring(1).toUpperCase()
index += `\n### ${type}\n\n`
for (const file of files.sort()) {
const link = file.replace(/\\/g, '/').replace(/ /g, '%20')
index += `- [${file}](${link}.md)\n`
}
}
return index
}
function generateAll() {
log('Starting documentation generation', 'info')
// Ensure output directory exists
if (!existsSync(CONFIG.outputDir)) {
mkdirSync(CONFIG.outputDir, { recursive: true })
}
// Scan files
const files = scanFiles()
log(`Found ${files.length} source files`, 'info')
// Generate index
const index = generateIndex(files)
writeFileSync(join(CONFIG.outputDir, 'index.md'), index)
log('Generated index.md', 'info')
// Generate individual file docs
for (const file of files) {
const doc = generateFileDoc(file)
if (doc) {
const relativePath = file.replace(process.cwd() + '/', '')
const outputPath = join(CONFIG.outputDir, relativePath + '.md')
// Ensure directory exists
const outputDir = join(CONFIG.outputDir, join(file, '..').replace(process.cwd() + '/', ''))
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true })
}
writeFileSync(outputPath, doc)
}
}
log(`Generated documentation for ${files.length} files`, 'success')
// Commit and push if in a git repo
try {
spawn('git', ['add', CONFIG.outputDir], { stdio: 'inherit' })
spawn('git', ['commit', '-m', 'Update generated docs'], { stdio: 'inherit' })
spawn('git', ['push'], { stdio: 'inherit' })
log('Committed and pushed documentation', 'success')
} catch (e) {
log(`Could not commit docs: ${e.message}`, 'warn')
}
}
function watch() {
log('Watching for changes...', 'info')
generateAll()
setInterval(() => {
generateAll()
}, CONFIG.interval)
}
// Main
if (CONFIG.watch) {
watch()
} else {
generateAll()
}