docutrack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -0
- package/bin/docutrack.js +67 -0
- package/package.json +38 -0
- package/src/analyzer/complexity.js +145 -0
- package/src/analyzer/detect.js +124 -0
- package/src/analyzer/index.js +121 -0
- package/src/analyzer/parsers/express.js +110 -0
- package/src/analyzer/parsers/fastapi.js +89 -0
- package/src/commands/analyze.js +47 -0
- package/src/commands/badge.js +79 -0
- package/src/commands/check.js +187 -0
- package/src/commands/clear.js +17 -0
- package/src/commands/export.js +182 -0
- package/src/commands/init.js +182 -0
- package/src/commands/onboard.js +288 -0
- package/src/commands/scan.js +121 -0
- package/src/commands/serve.js +48 -0
- package/src/commands/status.js +94 -0
- package/src/utils/drift.js +167 -0
- package/src/utils/queue.js +62 -0
- package/src/utils/settings.js +69 -0
- package/src/utils/stale.js +80 -0
- package/src/viewer/index.html +1411 -0
- package/src/viewer/server.js +652 -0
- package/templates/ARCHITECTURE.md +51 -0
- package/templates/agents/documentalista.md +113 -0
- package/templates/claude-snippet.md +39 -0
- package/templates/commands/adr-new.md +58 -0
- package/templates/commands/arch-review.md +59 -0
- package/templates/commands/ask-docs.md +26 -0
- package/templates/commands/doc-map.md +50 -0
- package/templates/docs/api/.gitkeep +0 -0
- package/templates/docs/decisions/.gitkeep +0 -0
- package/templates/docs/modules/.gitkeep +0 -0
- package/templates/docutrack.config.json +13 -0
- package/templates/github/workflows/docutrack-docs.yml +42 -0
- package/templates/github/workflows/docutrack-gate.yml +31 -0
- package/templates/github/workflows/docutrack-pr.yml +93 -0
- package/templates/hooks/on-stop.js +39 -0
- package/templates/hooks/post-tool-use.js +52 -0
- package/templates/stacks/express/ARCHITECTURE.md +67 -0
- package/templates/stacks/express/documentalista.md +63 -0
- package/templates/stacks/fastapi/ARCHITECTURE.md +68 -0
- package/templates/stacks/fastapi/documentalista.md +88 -0
- package/templates/stacks/go/ARCHITECTURE.md +68 -0
- package/templates/stacks/go/documentalista.md +89 -0
- package/templates/stacks/monorepo/ARCHITECTURE.md +60 -0
- package/templates/stacks/monorepo/documentalista.md +59 -0
- package/templates/stacks/nextjs/ARCHITECTURE.md +76 -0
- package/templates/stacks/nextjs/documentalista.md +93 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const FORMATS = ['mintlify', 'docusaurus']
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
if (!fs.existsSync('.docutrack')) {
|
|
10
|
+
console.log('\nDocuTrack is not initialized. Run "npx docutrack init" first.\n')
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formatArg = args.find(a => a.startsWith('--format='))?.split('=')[1]
|
|
15
|
+
|| (args.indexOf('--format') !== -1 ? args[args.indexOf('--format') + 1] : null)
|
|
16
|
+
|
|
17
|
+
const outArg = args.find(a => a.startsWith('--out='))?.split('=')[1]
|
|
18
|
+
|| (args.indexOf('--out') !== -1 ? args[args.indexOf('--out') + 1] : null)
|
|
19
|
+
|
|
20
|
+
if (!formatArg || !FORMATS.includes(formatArg)) {
|
|
21
|
+
console.log(`\nUsage: docutrack export --format <${FORMATS.join('|')}> [--out <dir>]\n`)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const outDir = outArg || `docutrack-${formatArg}`
|
|
26
|
+
|
|
27
|
+
if (formatArg === 'mintlify') await exportMintlify(outDir)
|
|
28
|
+
else if (formatArg === 'docusaurus') await exportDocusaurus(outDir)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Mintlify ──────────────────────────────────────────────────────────────
|
|
32
|
+
async function exportMintlify(outDir) {
|
|
33
|
+
console.log(`\nExporting to Mintlify format → ${outDir}/\n`)
|
|
34
|
+
|
|
35
|
+
const nav = []
|
|
36
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
37
|
+
|
|
38
|
+
// ARCHITECTURE.md → overview.mdx
|
|
39
|
+
if (fs.existsSync('ARCHITECTURE.md')) {
|
|
40
|
+
const content = fs.readFileSync('ARCHITECTURE.md', 'utf8')
|
|
41
|
+
fs.writeFileSync(path.join(outDir, 'overview.mdx'), `---\ntitle: "Architecture"\ndescription: "System architecture overview"\n---\n\n${content}`)
|
|
42
|
+
nav.push('overview')
|
|
43
|
+
console.log(' ✓ overview.mdx')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// docs/modules/ → modules/
|
|
47
|
+
const modulesNav = copySection('docs/modules', path.join(outDir, 'modules'), 'mdx')
|
|
48
|
+
if (modulesNav.length) {
|
|
49
|
+
nav.push({ group: 'Modules', pages: modulesNav.map(f => `modules/${f}`) })
|
|
50
|
+
console.log(` ✓ modules/ (${modulesNav.length} files)`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// docs/decisions/ → decisions/
|
|
54
|
+
const decisionsNav = copySection('docs/decisions', path.join(outDir, 'decisions'), 'mdx')
|
|
55
|
+
if (decisionsNav.length) {
|
|
56
|
+
nav.push({ group: 'Decisions', pages: decisionsNav.map(f => `decisions/${f}`) })
|
|
57
|
+
console.log(` ✓ decisions/ (${decisionsNav.length} files)`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// docs/api/ → api-reference/
|
|
61
|
+
const apiNav = copySection('docs/api', path.join(outDir, 'api-reference'), 'mdx')
|
|
62
|
+
if (apiNav.length) {
|
|
63
|
+
nav.push({ group: 'API Reference', pages: apiNav.map(f => `api-reference/${f}`) })
|
|
64
|
+
console.log(` ✓ api-reference/ (${apiNav.length} files)`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Copy openapi.json if exists
|
|
68
|
+
if (fs.existsSync('docs/api/openapi.json')) {
|
|
69
|
+
fs.copyFileSync('docs/api/openapi.json', path.join(outDir, 'openapi.json'))
|
|
70
|
+
console.log(' ✓ openapi.json')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generate mint.json
|
|
74
|
+
const mintJson = {
|
|
75
|
+
$schema: 'https://mintlify.com/schema.json',
|
|
76
|
+
name: getProjectName(),
|
|
77
|
+
logo: { light: '/logo/light.svg', dark: '/logo/dark.svg' },
|
|
78
|
+
favicon: '/favicon.svg',
|
|
79
|
+
colors: { primary: '#6366f1', light: '#818cf8', dark: '#4f46e5' },
|
|
80
|
+
topbarLinks: [],
|
|
81
|
+
topbarCtaButton: { name: 'GitHub', url: 'https://github.com/your-org/your-repo' },
|
|
82
|
+
tabs: [],
|
|
83
|
+
anchors: [],
|
|
84
|
+
navigation: nav,
|
|
85
|
+
footerSocials: {},
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(path.join(outDir, 'mint.json'), JSON.stringify(mintJson, null, 2))
|
|
88
|
+
console.log(' ✓ mint.json')
|
|
89
|
+
console.log(`\nDone. To preview:\n cd ${outDir} && npx mintlify dev\n`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Docusaurus ─────────────────────────────────────────────────────────────
|
|
93
|
+
async function exportDocusaurus(outDir) {
|
|
94
|
+
console.log(`\nExporting to Docusaurus format → ${outDir}/\n`)
|
|
95
|
+
|
|
96
|
+
const docsDir = path.join(outDir, 'docs')
|
|
97
|
+
fs.mkdirSync(docsDir, { recursive: true })
|
|
98
|
+
|
|
99
|
+
const sidebar = {}
|
|
100
|
+
|
|
101
|
+
// ARCHITECTURE.md → docs/overview.md
|
|
102
|
+
if (fs.existsSync('ARCHITECTURE.md')) {
|
|
103
|
+
const content = fs.readFileSync('ARCHITECTURE.md', 'utf8')
|
|
104
|
+
fs.writeFileSync(path.join(docsDir, 'overview.md'), `---\nsidebar_position: 1\n---\n\n${content}`)
|
|
105
|
+
console.log(' ✓ docs/overview.md')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// docs/modules/ → docs/modules/
|
|
109
|
+
const modulesItems = copySection('docs/modules', path.join(docsDir, 'modules'), 'md')
|
|
110
|
+
if (modulesItems.length) {
|
|
111
|
+
sidebar.modules = { label: 'Modules', items: modulesItems }
|
|
112
|
+
console.log(` ✓ docs/modules/ (${modulesItems.length} files)`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// docs/decisions/ → docs/decisions/
|
|
116
|
+
const decisionsItems = copySection('docs/decisions', path.join(docsDir, 'decisions'), 'md')
|
|
117
|
+
if (decisionsItems.length) {
|
|
118
|
+
sidebar.decisions = { label: 'Decisions', items: decisionsItems }
|
|
119
|
+
console.log(` ✓ docs/decisions/ (${decisionsItems.length} files)`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// docs/api/ → docs/api/
|
|
123
|
+
const apiItems = copySection('docs/api', path.join(docsDir, 'api'), 'md')
|
|
124
|
+
if (apiItems.length) {
|
|
125
|
+
sidebar.api = { label: 'API', items: apiItems }
|
|
126
|
+
console.log(` ✓ docs/api/ (${apiItems.length} files)`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// sidebars.js
|
|
130
|
+
const sidebarJs = `/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
|
131
|
+
const sidebars = {
|
|
132
|
+
docs: [
|
|
133
|
+
'overview',
|
|
134
|
+
${sidebar.modules ? `{ type: 'category', label: 'Modules', items: ${JSON.stringify(sidebar.modules.items.map(f => `modules/${f}`))} },` : ''}
|
|
135
|
+
${sidebar.decisions ? `{ type: 'category', label: 'Decisions', items: ${JSON.stringify(sidebar.decisions.items.map(f => `decisions/${f}`))} },` : ''}
|
|
136
|
+
${sidebar.api ? `{ type: 'category', label: 'API', items: ${JSON.stringify(sidebar.api.items.map(f => `api/${f}`))} },` : ''}
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
module.exports = sidebars`
|
|
140
|
+
fs.writeFileSync(path.join(outDir, 'sidebars.js'), sidebarJs)
|
|
141
|
+
|
|
142
|
+
// Minimal docusaurus.config.js
|
|
143
|
+
const name = getProjectName()
|
|
144
|
+
const config = `/** @type {import('@docusaurus/types').Config} */
|
|
145
|
+
const config = {
|
|
146
|
+
title: '${name}',
|
|
147
|
+
tagline: 'Generated by DocuTrack',
|
|
148
|
+
url: 'https://your-domain.com',
|
|
149
|
+
baseUrl: '/',
|
|
150
|
+
onBrokenLinks: 'warn',
|
|
151
|
+
onBrokenMarkdownLinks: 'warn',
|
|
152
|
+
i18n: { defaultLocale: 'en', locales: ['en'] },
|
|
153
|
+
presets: [['classic', { docs: { sidebarPath: require.resolve('./sidebars.js'), routeBasePath: '/' }, blog: false, theme: { customCss: [] } }]],
|
|
154
|
+
themeConfig: { navbar: { title: '${name}', items: [] }, footer: { style: 'dark', copyright: 'Generated by DocuTrack' } },
|
|
155
|
+
}
|
|
156
|
+
module.exports = config`
|
|
157
|
+
fs.writeFileSync(path.join(outDir, 'docusaurus.config.js'), config)
|
|
158
|
+
console.log(' ✓ docusaurus.config.js + sidebars.js')
|
|
159
|
+
console.log(`\nDone. To preview:\n cd ${outDir} && npx create-docusaurus@latest . classic --skip-install && npm install && npm start\n`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
163
|
+
function copySection(srcDir, destDir, ext) {
|
|
164
|
+
const files = []
|
|
165
|
+
if (!fs.existsSync(srcDir)) return files
|
|
166
|
+
fs.mkdirSync(destDir, { recursive: true })
|
|
167
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
168
|
+
if (!entry.endsWith('.md') || entry === '.gitkeep') continue
|
|
169
|
+
const base = entry.replace('.md', '')
|
|
170
|
+
const content = fs.readFileSync(path.join(srcDir, entry), 'utf8')
|
|
171
|
+
const destFile = `${base}.${ext}`
|
|
172
|
+
fs.writeFileSync(path.join(destDir, destFile), content)
|
|
173
|
+
files.push(base)
|
|
174
|
+
}
|
|
175
|
+
return files
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getProjectName() {
|
|
179
|
+
try { return JSON.parse(fs.readFileSync('package.json', 'utf8')).name || 'Project' } catch { return 'Project' }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { run }
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { installHooks, SETTINGS_PATH } = require('../utils/settings')
|
|
6
|
+
|
|
7
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates')
|
|
8
|
+
const VALID_TEMPLATES = ['nextjs', 'fastapi', 'express', 'monorepo', 'go']
|
|
9
|
+
|
|
10
|
+
function copyFile(src, dest) {
|
|
11
|
+
const dir = path.dirname(dest)
|
|
12
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
13
|
+
fs.copyFileSync(src, dest)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function copyDir(src, dest) {
|
|
17
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true })
|
|
18
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
19
|
+
const srcPath = path.join(src, entry.name)
|
|
20
|
+
const destPath = path.join(dest, entry.name)
|
|
21
|
+
if (entry.isDirectory()) copyDir(srcPath, destPath)
|
|
22
|
+
else copyFile(srcPath, destPath)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function step(msg) { process.stdout.write(` ${msg}\n`) }
|
|
27
|
+
|
|
28
|
+
function autoDetectTemplate() {
|
|
29
|
+
// Node.js: check package.json
|
|
30
|
+
try {
|
|
31
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
32
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
33
|
+
if (deps.next) return 'nextjs'
|
|
34
|
+
if (pkg.workspaces || fs.existsSync('pnpm-workspace.yaml') || fs.existsSync('turbo.json')) return 'monorepo'
|
|
35
|
+
if (deps.express || deps['@types/express'] || deps.fastify || deps.koa) return 'express'
|
|
36
|
+
} catch { /* not a node project */ }
|
|
37
|
+
|
|
38
|
+
// Python: check for FastAPI
|
|
39
|
+
for (const f of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
|
|
40
|
+
if (!fs.existsSync(f)) continue
|
|
41
|
+
const content = fs.readFileSync(f, 'utf8').toLowerCase()
|
|
42
|
+
if (content.includes('fastapi')) return 'fastapi'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Go
|
|
46
|
+
if (fs.existsSync('go.mod')) return 'go'
|
|
47
|
+
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function run(args) {
|
|
52
|
+
console.log('\nDocuTrack — initializing in current project\n')
|
|
53
|
+
|
|
54
|
+
// Guard: already initialized
|
|
55
|
+
if (fs.existsSync('.docutrack')) {
|
|
56
|
+
console.log('DocuTrack is already initialized in this project.')
|
|
57
|
+
console.log('Run "docutrack status" to see the current queue.\n')
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Resolve template
|
|
62
|
+
const templateFlag = args?.find(a => a.startsWith('--template='))?.split('=')[1]
|
|
63
|
+
|| (args?.indexOf('--template') !== -1 ? args[args.indexOf('--template') + 1] : null)
|
|
64
|
+
const template = (templateFlag && VALID_TEMPLATES.includes(templateFlag))
|
|
65
|
+
? templateFlag
|
|
66
|
+
: autoDetectTemplate()
|
|
67
|
+
|
|
68
|
+
if (templateFlag && !VALID_TEMPLATES.includes(templateFlag)) {
|
|
69
|
+
console.log(`Unknown template: "${templateFlag}". Valid options: ${VALID_TEMPLATES.join(', ')}\n`)
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 1. .docutrack/ structure
|
|
74
|
+
fs.mkdirSync('.docutrack/hooks', { recursive: true })
|
|
75
|
+
step('Created .docutrack/')
|
|
76
|
+
|
|
77
|
+
// 2. Queue
|
|
78
|
+
fs.writeFileSync('.docutrack/queue.json', JSON.stringify({ pending: [], lastClear: null }, null, 2))
|
|
79
|
+
step('Created .docutrack/queue.json')
|
|
80
|
+
|
|
81
|
+
// 3. Hook scripts
|
|
82
|
+
copyFile(path.join(TEMPLATES_DIR, 'hooks', 'post-tool-use.js'), '.docutrack/hooks/post-tool-use.js')
|
|
83
|
+
copyFile(path.join(TEMPLATES_DIR, 'hooks', 'on-stop.js'), '.docutrack/hooks/on-stop.js')
|
|
84
|
+
step('Installed hooks → .docutrack/hooks/')
|
|
85
|
+
|
|
86
|
+
// 4. /docs structure
|
|
87
|
+
copyDir(path.join(TEMPLATES_DIR, 'docs'), 'docs')
|
|
88
|
+
step('Created docs/ (modules/, decisions/, api/)')
|
|
89
|
+
|
|
90
|
+
// 5. ARCHITECTURE.md — use stack template if available, else base
|
|
91
|
+
if (!fs.existsSync('ARCHITECTURE.md')) {
|
|
92
|
+
const stackArch = template && path.join(TEMPLATES_DIR, 'stacks', template, 'ARCHITECTURE.md')
|
|
93
|
+
const archSrc = (stackArch && fs.existsSync(stackArch))
|
|
94
|
+
? stackArch
|
|
95
|
+
: path.join(TEMPLATES_DIR, 'ARCHITECTURE.md')
|
|
96
|
+
copyFile(archSrc, 'ARCHITECTURE.md')
|
|
97
|
+
step('Created ARCHITECTURE.md' + (template ? ` (${template} template)` : ''))
|
|
98
|
+
} else {
|
|
99
|
+
step('ARCHITECTURE.md already exists — skipped')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 6. Slash commands
|
|
103
|
+
const commandsDir = path.join(TEMPLATES_DIR, 'commands')
|
|
104
|
+
for (const name of fs.readdirSync(commandsDir)) {
|
|
105
|
+
copyFile(path.join(commandsDir, name), path.join('.claude', 'commands', name))
|
|
106
|
+
}
|
|
107
|
+
step('Installed slash commands → .claude/commands/ (doc-map, arch-review, adr-new, ask-docs)')
|
|
108
|
+
|
|
109
|
+
// 7. Documentalista — use stack-specific version if available
|
|
110
|
+
const stackAgent = template && path.join(TEMPLATES_DIR, 'stacks', template, 'documentalista.md')
|
|
111
|
+
const agentSrc = (stackAgent && fs.existsSync(stackAgent))
|
|
112
|
+
? stackAgent
|
|
113
|
+
: path.join(TEMPLATES_DIR, 'agents', 'documentalista.md')
|
|
114
|
+
copyFile(agentSrc, '.claude/agents/documentalista.md')
|
|
115
|
+
step('Installed documentalista subagent → .claude/agents/documentalista.md' + (template ? ` (${template})` : ''))
|
|
116
|
+
|
|
117
|
+
// 8. docutrack.config.json
|
|
118
|
+
if (!fs.existsSync('docutrack.config.json')) {
|
|
119
|
+
const cfgSrc = path.join(TEMPLATES_DIR, 'docutrack.config.json')
|
|
120
|
+
let cfg = JSON.parse(fs.readFileSync(cfgSrc, 'utf8'))
|
|
121
|
+
if (template) cfg.template = template
|
|
122
|
+
else delete cfg.template
|
|
123
|
+
fs.writeFileSync('docutrack.config.json', JSON.stringify(cfg, null, 2))
|
|
124
|
+
step('Created docutrack.config.json')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 9. Hooks in .claude/settings.json
|
|
128
|
+
const installed = installHooks()
|
|
129
|
+
step(installed
|
|
130
|
+
? `Registered hooks in ${SETTINGS_PATH}`
|
|
131
|
+
: `Hooks already registered in ${SETTINGS_PATH} — skipped`)
|
|
132
|
+
|
|
133
|
+
// 10. Detect if this is an existing project with source files
|
|
134
|
+
const hasExistingCode = detectExistingCode()
|
|
135
|
+
|
|
136
|
+
// 11. Print next steps
|
|
137
|
+
const snippetPath = path.join(TEMPLATES_DIR, 'claude-snippet.md')
|
|
138
|
+
const snippet = fs.readFileSync(snippetPath, 'utf8')
|
|
139
|
+
copyFile(snippetPath, '.docutrack/claude-snippet.md')
|
|
140
|
+
|
|
141
|
+
const templateLine = template
|
|
142
|
+
? `\n Stack template : ${template}`
|
|
143
|
+
: ''
|
|
144
|
+
|
|
145
|
+
const existingProjectTip = hasExistingCode ? `
|
|
146
|
+
Existing project detected — bootstrap your docs:
|
|
147
|
+
1. Run: docutrack scan (queues all source files at once)
|
|
148
|
+
2. In Claude Code, say: "Run the documentalista to document all pending files"
|
|
149
|
+
3. Run: docutrack serve (view the populated docs)
|
|
150
|
+
` : `
|
|
151
|
+
What DocuTrack does from here:
|
|
152
|
+
• After every file edit → logs the file to .docutrack/queue.json
|
|
153
|
+
• When the session ends → the documentalista subagent updates the docs
|
|
154
|
+
• Run "docutrack serve" to open the documentation web viewer
|
|
155
|
+
• Run "docutrack status" to see coverage, pending, and stale docs
|
|
156
|
+
• Use /doc-map, /arch-review, /adr-new inside Claude Code sessions
|
|
157
|
+
`
|
|
158
|
+
|
|
159
|
+
console.log(`
|
|
160
|
+
Done. DocuTrack is active.${templateLine}
|
|
161
|
+
${existingProjectTip}
|
|
162
|
+
Add this to your CLAUDE.md:
|
|
163
|
+
${'─'.repeat(52)}
|
|
164
|
+
${snippet}${'─'.repeat(52)}
|
|
165
|
+
|
|
166
|
+
(The snippet is also saved at .docutrack/claude-snippet.md)
|
|
167
|
+
`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function detectExistingCode() {
|
|
171
|
+
const SOURCE_DIRS = ['src', 'lib', 'app', 'routes', 'controllers', 'handlers', 'api', 'pkg', 'internal']
|
|
172
|
+
const SOURCE_EXTS = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.mjs']
|
|
173
|
+
for (const dir of SOURCE_DIRS) {
|
|
174
|
+
if (!fs.existsSync(dir)) continue
|
|
175
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
+
if (entry.isFile() && SOURCE_EXTS.includes(path.extname(entry.name))) return true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { run }
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { findStale } = require('../utils/stale')
|
|
6
|
+
|
|
7
|
+
async function run(args) {
|
|
8
|
+
if (!fs.existsSync('.docutrack')) {
|
|
9
|
+
console.log('\nDocuTrack is not initialized. Run "npx docutrack init" first.\n')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const root = process.cwd()
|
|
14
|
+
const force = args.includes('--force')
|
|
15
|
+
const outPath = path.join(root, 'docs', 'ONBOARDING.md')
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(outPath) && !force) {
|
|
18
|
+
console.log(`\nONBOARDING.md already exists. Use --force to regenerate.\n`)
|
|
19
|
+
console.log(` Location: ${outPath}\n`)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('\nDocuTrack — generating ONBOARDING.md\n')
|
|
24
|
+
|
|
25
|
+
const sections = []
|
|
26
|
+
|
|
27
|
+
// ── Project name + description ──────────────────────────
|
|
28
|
+
const meta = getProjectMeta(root)
|
|
29
|
+
sections.push(`# ${meta.name} — Onboarding Guide\n\n> Auto-generated by DocuTrack on ${new Date().toISOString().slice(0, 10)}.`)
|
|
30
|
+
|
|
31
|
+
// ── What is this project? ───────────────────────────────
|
|
32
|
+
const arch = readFile(root, 'ARCHITECTURE.md')
|
|
33
|
+
if (arch) {
|
|
34
|
+
// Extract intro paragraph (content before first ## header)
|
|
35
|
+
const intro = arch.split(/^##\s/m)[0].replace(/^#[^\n]*\n/, '').trim()
|
|
36
|
+
if (intro) {
|
|
37
|
+
sections.push(`## What Is This?\n\n${intro}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Stack + framework ───────────────────────────────────
|
|
42
|
+
const stackInfo = getStackInfo(root)
|
|
43
|
+
if (stackInfo) sections.push(`## Stack\n\n${stackInfo}`)
|
|
44
|
+
|
|
45
|
+
// ── Setup ───────────────────────────────────────────────
|
|
46
|
+
const setup = getSetupInstructions(root, meta)
|
|
47
|
+
sections.push(`## Getting Started\n\n${setup}`)
|
|
48
|
+
|
|
49
|
+
// ── Module map ──────────────────────────────────────────
|
|
50
|
+
const moduleDocs = loadModuleDocs(root)
|
|
51
|
+
if (moduleDocs.length > 0) {
|
|
52
|
+
const table = buildModuleTable(moduleDocs)
|
|
53
|
+
sections.push(`## Module Map\n\n${table}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── API surface ─────────────────────────────────────────
|
|
57
|
+
const apiInfo = getApiSummary(root)
|
|
58
|
+
if (apiInfo) sections.push(`## API Surface\n\n${apiInfo}`)
|
|
59
|
+
|
|
60
|
+
// ── Architecture highlights ─────────────────────────────
|
|
61
|
+
if (arch) {
|
|
62
|
+
const highlights = extractArchSection(arch, ['## Architecture', '## System Design', '## Design'])
|
|
63
|
+
if (highlights) sections.push(`## Architecture Overview\n\n${highlights}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── ADRs ────────────────────────────────────────────────
|
|
67
|
+
const adrs = loadAdrs(root)
|
|
68
|
+
if (adrs.length > 0) {
|
|
69
|
+
const adrList = adrs.map(a => `- **${a.title}** — ${a.status} · \`${a.file}\``).join('\n')
|
|
70
|
+
sections.push(`## Key Decisions (ADRs)\n\n${adrList}\n\nRead the full ADRs in \`docs/decisions/\`.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Documentation health ────────────────────────────────
|
|
74
|
+
const healthSection = getHealthSummary(root)
|
|
75
|
+
if (healthSection) sections.push(`## Documentation Health\n\n${healthSection}`)
|
|
76
|
+
|
|
77
|
+
// ── Workflow ────────────────────────────────────────────
|
|
78
|
+
sections.push(`## Development Workflow
|
|
79
|
+
|
|
80
|
+
1. Make your changes in a feature branch
|
|
81
|
+
2. Run \`docutrack status\` to see what needs documenting
|
|
82
|
+
3. Use the \`/arch-review\` slash command to check coverage
|
|
83
|
+
4. The documentalista subagent will update docs when the session ends
|
|
84
|
+
5. Run \`docutrack check\` to validate doc health before opening a PR
|
|
85
|
+
|
|
86
|
+
### Useful Commands
|
|
87
|
+
|
|
88
|
+
\`\`\`bash
|
|
89
|
+
docutrack status # Coverage, pending files, stale docs
|
|
90
|
+
docutrack check # Full health check: drift, complexity, stale
|
|
91
|
+
docutrack serve # Open docs in browser at localhost:4242
|
|
92
|
+
docutrack analyze # Re-scan API endpoints
|
|
93
|
+
docutrack badge # Regenerate README badge
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
### Slash Commands (in Claude Code)
|
|
97
|
+
|
|
98
|
+
| Command | Purpose |
|
|
99
|
+
|---------|---------|
|
|
100
|
+
| \`/doc-map\` | Visual map of all modules and endpoints |
|
|
101
|
+
| \`/arch-review\` | Coverage audit — which files lack docs |
|
|
102
|
+
| \`/adr-new\` | Guided ADR creation wizard |
|
|
103
|
+
| \`/ask-docs\` | Ask a question about the codebase using docs |
|
|
104
|
+
`)
|
|
105
|
+
|
|
106
|
+
// ── Write output ────────────────────────────────────────
|
|
107
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true })
|
|
108
|
+
fs.writeFileSync(outPath, sections.join('\n\n---\n\n'))
|
|
109
|
+
|
|
110
|
+
console.log(` ✓ Generated: ${path.relative(root, outPath)}`)
|
|
111
|
+
console.log(` ${moduleDocs.length} modules documented`)
|
|
112
|
+
if (adrs.length > 0) console.log(` ${adrs.length} ADRs referenced`)
|
|
113
|
+
console.log(`\nShare this with new contributors or serve it with "docutrack serve".\n`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function getProjectMeta(root) {
|
|
119
|
+
try {
|
|
120
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
121
|
+
return { name: pkg.name || path.basename(root), version: pkg.version, description: pkg.description }
|
|
122
|
+
} catch { /* ok */ }
|
|
123
|
+
try {
|
|
124
|
+
const mod = fs.readFileSync(path.join(root, 'go.mod'), 'utf8')
|
|
125
|
+
const m = mod.match(/^module\s+(\S+)/m)
|
|
126
|
+
return { name: m?.[1]?.split('/').pop() || path.basename(root), version: null }
|
|
127
|
+
} catch { /* ok */ }
|
|
128
|
+
return { name: path.basename(root), version: null }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getStackInfo(root) {
|
|
132
|
+
const lines = []
|
|
133
|
+
try {
|
|
134
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
135
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
136
|
+
const runtime = `Node.js ${pkg.engines?.node || '18+'}`
|
|
137
|
+
lines.push(`| Runtime | ${runtime} |`)
|
|
138
|
+
const fw = deps.next ? 'Next.js' : deps.express ? 'Express' : deps.fastify ? 'Fastify' : deps.koa ? 'Koa' : 'Node.js'
|
|
139
|
+
lines.push(`| Framework | ${fw} |`)
|
|
140
|
+
if (pkg.scripts) {
|
|
141
|
+
const scripts = Object.entries(pkg.scripts).slice(0, 5).map(([k, v]) => `\`npm run ${k}\` — ${v}`).join('<br>')
|
|
142
|
+
lines.push(`| Scripts | ${scripts} |`)
|
|
143
|
+
}
|
|
144
|
+
} catch { /* ok */ }
|
|
145
|
+
try {
|
|
146
|
+
const req = fs.readFileSync(path.join(root, 'requirements.txt'), 'utf8')
|
|
147
|
+
if (req.toLowerCase().includes('fastapi')) {
|
|
148
|
+
lines.push('| Runtime | Python 3.10+ |')
|
|
149
|
+
lines.push('| Framework | FastAPI |')
|
|
150
|
+
}
|
|
151
|
+
} catch { /* ok */ }
|
|
152
|
+
try {
|
|
153
|
+
const mod = fs.readFileSync(path.join(root, 'go.mod'), 'utf8')
|
|
154
|
+
const ver = mod.match(/^go\s+([\d.]+)/m)?.[1]
|
|
155
|
+
if (ver) lines.push(`| Runtime | Go ${ver} |`)
|
|
156
|
+
} catch { /* ok */ }
|
|
157
|
+
|
|
158
|
+
if (!lines.length) return null
|
|
159
|
+
return `| | |\n|---|---|\n${lines.join('\n')}`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getSetupInstructions(root, meta) {
|
|
163
|
+
const lines = []
|
|
164
|
+
const hasPkg = fs.existsSync(path.join(root, 'package.json'))
|
|
165
|
+
const hasPy = fs.existsSync(path.join(root, 'requirements.txt')) || fs.existsSync(path.join(root, 'pyproject.toml'))
|
|
166
|
+
const hasGo = fs.existsSync(path.join(root, 'go.mod'))
|
|
167
|
+
|
|
168
|
+
lines.push('```bash')
|
|
169
|
+
lines.push('# Clone the repo')
|
|
170
|
+
lines.push(`git clone <repo-url> && cd ${meta.name}`)
|
|
171
|
+
|
|
172
|
+
if (hasPkg) {
|
|
173
|
+
lines.push('')
|
|
174
|
+
lines.push('# Install dependencies')
|
|
175
|
+
const hasPnpm = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
|
|
176
|
+
const hasYarn = fs.existsSync(path.join(root, 'yarn.lock'))
|
|
177
|
+
lines.push(hasPnpm ? 'pnpm install' : hasYarn ? 'yarn install' : 'npm install')
|
|
178
|
+
}
|
|
179
|
+
if (hasPy) {
|
|
180
|
+
lines.push('')
|
|
181
|
+
lines.push('# Python setup')
|
|
182
|
+
lines.push('python -m venv .venv && source .venv/bin/activate')
|
|
183
|
+
lines.push(fs.existsSync(path.join(root, 'pyproject.toml')) ? 'pip install -e ".[dev]"' : 'pip install -r requirements.txt')
|
|
184
|
+
}
|
|
185
|
+
if (hasGo) {
|
|
186
|
+
lines.push('')
|
|
187
|
+
lines.push('go mod download')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push('```')
|
|
191
|
+
|
|
192
|
+
// Check for .env.example
|
|
193
|
+
if (fs.existsSync(path.join(root, '.env.example'))) {
|
|
194
|
+
lines.push('\n**Environment variables:**\n```bash\ncp .env.example .env\n# Edit .env with your values\n```')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return lines.join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function loadModuleDocs(root) {
|
|
201
|
+
const docsDir = path.join(root, 'docs', 'modules')
|
|
202
|
+
if (!fs.existsSync(docsDir)) return []
|
|
203
|
+
const modules = []
|
|
204
|
+
for (const file of fs.readdirSync(docsDir)) {
|
|
205
|
+
if (!file.endsWith('.md')) continue
|
|
206
|
+
const content = fs.readFileSync(path.join(docsDir, file), 'utf8')
|
|
207
|
+
// Extract responsibility line
|
|
208
|
+
const resp = content.match(/\*\*Responsibility\*\*:?\s*(.+)/)?.[1]?.trim()
|
|
209
|
+
|| content.match(/^##?\s*Responsibility\s*\n+(.+)/m)?.[1]?.trim()
|
|
210
|
+
|| content.split('\n').filter(l => l.trim() && !l.startsWith('#'))[0]?.slice(0, 100)
|
|
211
|
+
modules.push({ name: file.replace('.md', ''), responsibility: resp || '—', file: `docs/modules/${file}` })
|
|
212
|
+
}
|
|
213
|
+
return modules
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildModuleTable(modules) {
|
|
217
|
+
const rows = modules.map(m => `| \`${m.name}\` | ${m.responsibility} |`)
|
|
218
|
+
return `| Module | Responsibility |\n|--------|----------------|\n${rows.join('\n')}`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getApiSummary(root) {
|
|
222
|
+
const specPath = path.join(root, 'docs', 'api', 'openapi.json')
|
|
223
|
+
if (!fs.existsSync(specPath)) return null
|
|
224
|
+
try {
|
|
225
|
+
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'))
|
|
226
|
+
const pathCount = Object.keys(spec.paths || {}).length
|
|
227
|
+
if (pathCount === 0) return null
|
|
228
|
+
const methods = {}
|
|
229
|
+
for (const p of Object.values(spec.paths)) {
|
|
230
|
+
for (const m of Object.keys(p)) methods[m] = (methods[m] || 0) + 1
|
|
231
|
+
}
|
|
232
|
+
const summary = Object.entries(methods).map(([m, n]) => `${n} ${m.toUpperCase()}`).join(', ')
|
|
233
|
+
const tags = [...new Set(Object.values(spec.paths).flatMap(p => Object.values(p).flatMap(op => op.tags || [])))]
|
|
234
|
+
return `**${pathCount} endpoints** (${summary})\\\nGroups: ${tags.map(t => `\`${t}\``).join(', ')}\n\nRun \`docutrack serve\` and open **API Explorer** to try endpoints interactively.`
|
|
235
|
+
} catch { return null }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function extractArchSection(content, headerNames) {
|
|
239
|
+
for (const header of headerNames) {
|
|
240
|
+
const re = new RegExp(`${header.replace('## ', '^## ')}[\\s\\S]*?(?=^## |$)`, 'm')
|
|
241
|
+
const match = content.match(re)
|
|
242
|
+
if (match) return match[0].replace(/^## [^\n]*\n/, '').trim().slice(0, 800)
|
|
243
|
+
}
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function loadAdrs(root) {
|
|
248
|
+
const dir = path.join(root, 'docs', 'decisions')
|
|
249
|
+
if (!fs.existsSync(dir)) return []
|
|
250
|
+
const adrs = []
|
|
251
|
+
for (const file of fs.readdirSync(dir)) {
|
|
252
|
+
if (!file.endsWith('.md') || file === '.gitkeep') continue
|
|
253
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf8')
|
|
254
|
+
const title = content.match(/^#\s+(.+)/m)?.[1] || file.replace('.md', '')
|
|
255
|
+
const status = content.match(/##?\s*Status\s*:?\s*(.+)/i)?.[1]?.trim() || 'Unknown'
|
|
256
|
+
adrs.push({ file: `docs/decisions/${file}`, title, status })
|
|
257
|
+
}
|
|
258
|
+
return adrs.slice(0, 10)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getHealthSummary(root) {
|
|
262
|
+
try {
|
|
263
|
+
const queue = JSON.parse(fs.readFileSync(path.join(root, '.docutrack', 'queue.json'), 'utf8'))
|
|
264
|
+
const stale = findStale(root)
|
|
265
|
+
const docCount = countDocs(root)
|
|
266
|
+
const pending = queue.pending?.length || 0
|
|
267
|
+
const lines = [
|
|
268
|
+
`| Metric | Value |`,
|
|
269
|
+
`|--------|-------|`,
|
|
270
|
+
`| Documented modules | ${docCount} |`,
|
|
271
|
+
`| Pending queue | ${pending} files |`,
|
|
272
|
+
`| Stale docs | ${stale.length} |`,
|
|
273
|
+
]
|
|
274
|
+
return lines.join('\n')
|
|
275
|
+
} catch { return null }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function countDocs(root) {
|
|
279
|
+
const d = path.join(root, 'docs', 'modules')
|
|
280
|
+
if (!fs.existsSync(d)) return 0
|
|
281
|
+
return fs.readdirSync(d).filter(f => f.endsWith('.md')).length
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function readFile(root, file) {
|
|
285
|
+
try { return fs.readFileSync(path.join(root, file), 'utf8') } catch { return null }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { run }
|