194 lines
6.1 KiB
JavaScript
194 lines
6.1 KiB
JavaScript
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, '..')
|
|
const siteRoot = path.join(repoRoot, 'site')
|
|
const outputRoot = path.join(repoRoot, 'src-tauri', 'resources', 'agent-docs')
|
|
|
|
const sectionOrder = ['start', 'concepts', 'guides', 'reference', 'troubleshooting', 'download', 'releases']
|
|
const ignoredDirs = new Set(['.vitepress', 'public', 'node_modules', '.DS_Store'])
|
|
|
|
function titleFromSlug(slug) {
|
|
return slug
|
|
.replace(/[-_]+/g, ' ')
|
|
.replace(/\b\w/g, (letter) => letter.toUpperCase())
|
|
}
|
|
|
|
function stripFrontmatter(markdown) {
|
|
return markdown.replace(/^---\n[\s\S]*?\n---\n/, '')
|
|
}
|
|
|
|
function firstHeading(markdown, fallback) {
|
|
const match = markdown.match(/^#\s+(.+)$/m)
|
|
return match?.[1]?.trim() || fallback
|
|
}
|
|
|
|
export function normalizeDocPath(relativePath) {
|
|
return relativePath.replaceAll(path.win32.sep, '/')
|
|
}
|
|
|
|
export function sectionForFile(relativePath) {
|
|
const [firstPart] = relativePath.split('/')
|
|
if (firstPart === 'index.md') return 'home'
|
|
return firstPart.replace(/\.md$/, '')
|
|
}
|
|
|
|
async function listMarkdownFiles(dir, base = dir) {
|
|
const entries = await readdir(dir, { withFileTypes: true })
|
|
const files = []
|
|
|
|
for (const entry of entries) {
|
|
if (ignoredDirs.has(entry.name)) continue
|
|
|
|
const fullPath = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
files.push(...await listMarkdownFiles(fullPath, base))
|
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
files.push(normalizeDocPath(path.relative(base, fullPath)))
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
function sortDocs(files) {
|
|
return files.sort((a, b) => {
|
|
const sectionDiff = sectionOrder.indexOf(sectionForFile(a)) - sectionOrder.indexOf(sectionForFile(b))
|
|
if (sectionDiff !== 0) return sectionDiff
|
|
return a.localeCompare(b)
|
|
})
|
|
}
|
|
|
|
function docUrl(relativePath) {
|
|
const withoutExt = relativePath.replace(/(^|\/)index\.md$/, '$1').replace(/\.md$/, '')
|
|
return `/${withoutExt}`.replace(/\/$/, '/') || '/'
|
|
}
|
|
|
|
function formatDoc(doc) {
|
|
return `# ${doc.title}\n\nSource: ${doc.path}\nURL: ${doc.url}\n\n${doc.content}`
|
|
}
|
|
|
|
function groupDocsBySection(docs) {
|
|
const bySection = new Map()
|
|
|
|
for (const doc of docs) {
|
|
const docsInSection = bySection.get(doc.section) ?? []
|
|
docsInSection.push(doc)
|
|
bySection.set(doc.section, docsInSection)
|
|
}
|
|
|
|
return bySection
|
|
}
|
|
|
|
function buildIndex(docs) {
|
|
const bySection = groupDocsBySection(docs)
|
|
const lines = [
|
|
'# Tolaria Agent Docs',
|
|
'',
|
|
'These docs are generated from the public Tolaria documentation for local AI agent lookup.',
|
|
'',
|
|
'Start here, then use `rg` over this folder for specific Tolaria concepts and workflows.',
|
|
'',
|
|
]
|
|
|
|
for (const section of ['home', ...sectionOrder]) {
|
|
const docsInSection = bySection.get(section)
|
|
if (!docsInSection?.length) continue
|
|
|
|
lines.push(`## ${titleFromSlug(section)}`, '')
|
|
for (const doc of docsInSection) {
|
|
lines.push(`- [${doc.title}](pages/${doc.path})`)
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
lines.push('## Generated Files', '')
|
|
lines.push('- `all.md`: all public docs concatenated for fast full-context reads.')
|
|
lines.push('- `search-index.json`: title, heading, section, path, and URL metadata for quick routing.')
|
|
lines.push('- `<section>.md`: one compact bundle per docs section.')
|
|
lines.push('- `pages/`: one generated Markdown file per public docs page.')
|
|
lines.push('')
|
|
|
|
return lines.join('\n')
|
|
}
|
|
|
|
function buildAgentInstructions() {
|
|
return `# AGENTS.md - Tolaria Docs Bundle
|
|
|
|
This folder contains local, generated Tolaria product docs for AI agents.
|
|
|
|
Use these docs when a user asks how Tolaria works, when you need product behavior, or before making Tolaria-specific assumptions.
|
|
|
|
Recommended lookup flow:
|
|
|
|
1. Read the active vault's AGENTS.md for vault-specific conventions.
|
|
2. Read this folder's index.md for the docs map.
|
|
3. Use \`rg\` over this folder for advanced concepts, workflows, shortcuts, Git, AutoGit, AI, types, properties, relationships, and troubleshooting.
|
|
|
|
Vault-specific AGENTS.md wins for local conventions. These bundled docs win for Tolaria product behavior.
|
|
`
|
|
}
|
|
|
|
function searchIndexFor(doc) {
|
|
const headings = [...doc.content.matchAll(/^#{2,3}\s+(.+)$/gm)].map((match) => match[1].trim())
|
|
return {
|
|
title: doc.title,
|
|
path: `pages/${doc.path}`,
|
|
url: doc.url,
|
|
section: doc.section,
|
|
headings,
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const files = sortDocs(await listMarkdownFiles(siteRoot))
|
|
const docs = []
|
|
|
|
for (const relativePath of files) {
|
|
const raw = await readFile(path.join(siteRoot, relativePath), 'utf8')
|
|
const content = stripFrontmatter(raw).trim()
|
|
const fallbackTitle = titleFromSlug(path.basename(relativePath, '.md'))
|
|
docs.push({
|
|
content,
|
|
path: relativePath,
|
|
section: sectionForFile(relativePath),
|
|
title: firstHeading(content, fallbackTitle),
|
|
url: docUrl(relativePath),
|
|
})
|
|
}
|
|
|
|
await rm(outputRoot, { force: true, recursive: true })
|
|
await mkdir(outputRoot, { recursive: true })
|
|
|
|
await writeFile(path.join(outputRoot, 'AGENTS.md'), buildAgentInstructions())
|
|
await writeFile(path.join(outputRoot, 'index.md'), buildIndex(docs))
|
|
await writeFile(path.join(outputRoot, 'all.md'), docs.map(formatDoc).join('\n\n---\n\n'))
|
|
await writeFile(path.join(outputRoot, 'search-index.json'), `${JSON.stringify(docs.map(searchIndexFor), null, 2)}\n`)
|
|
|
|
for (const doc of docs) {
|
|
const outputPath = path.join(outputRoot, 'pages', doc.path)
|
|
await mkdir(path.dirname(outputPath), { recursive: true })
|
|
await writeFile(outputPath, formatDoc(doc))
|
|
}
|
|
|
|
const bySection = groupDocsBySection(docs)
|
|
for (const [section, docsInSection] of bySection) {
|
|
await writeFile(
|
|
path.join(outputRoot, `${section}.md`),
|
|
docsInSection.map(formatDoc).join('\n\n---\n\n'),
|
|
)
|
|
}
|
|
|
|
console.log(`Generated ${docs.length} agent docs in ${path.relative(repoRoot, outputRoot)}`)
|
|
}
|
|
|
|
const entrypointUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : ''
|
|
|
|
if (import.meta.url === entrypointUrl) {
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|
|
}
|