docutrack 0.1.1 → 0.1.6
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 +86 -81
- package/bin/docutrack.js +73 -67
- package/package.json +5 -3
- package/src/commands/init.js +245 -80
- package/src/commands/install-global.js +93 -0
- package/src/commands/scan.js +3 -15
- package/src/commands/setup.js +126 -0
- package/src/hooks/global-on-stop.js +18 -0
- package/src/hooks/global-post-tool-use.js +25 -0
- package/src/utils/daemon.js +48 -0
- package/src/viewer/index.html +1545 -1641
- package/src/viewer/server.js +383 -694
- package/templates/agents/documentalista.md +47 -28
- package/templates/claude-snippet.md +53 -39
- package/templates/hooks/on-stop.js +59 -11
- package/templates/hooks/post-tool-use.js +12 -8
package/src/commands/init.js
CHANGED
|
@@ -2,10 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const readline = require('readline')
|
|
6
|
+
const { exec } = require('child_process')
|
|
5
7
|
const { installHooks, SETTINGS_PATH } = require('../utils/settings')
|
|
8
|
+
const { isPortInUse, startServerDaemon, isServerRunning } = require('../utils/daemon')
|
|
9
|
+
const { write: writeQueue } = require('../utils/queue')
|
|
10
|
+
|
|
11
|
+
// ── Questionnaire strings (bilingual) ─────────────────────────
|
|
12
|
+
const Q = {
|
|
13
|
+
es: {
|
|
14
|
+
q2: ' Describe el proyecto en una oración:\n > ',
|
|
15
|
+
q3: '\n ¿Quién lee estos docs?\n [1] Solo yo / equipo pequeño (técnico, conciso)\n [2] Incorporamos devs nuevos (más contexto y explicación)\n [3] Mixto\n > ',
|
|
16
|
+
q4: '\n ¿Profundidad de los docs?\n [1] Conciso — resumen + API pública\n [2] Estándar — + decisiones de diseño y gotchas (recomendado)\n [3] Detallado — + ejemplos y contexto completo\n > ',
|
|
17
|
+
saved: '✓ Preferencias guardadas',
|
|
18
|
+
},
|
|
19
|
+
en: {
|
|
20
|
+
q2: ' Describe this project in one sentence:\n > ',
|
|
21
|
+
q3: '\n Who reads these docs?\n [1] Just me / small team (technical, concise)\n [2] Onboarding new devs (more context and explanation)\n [3] Mixed\n > ',
|
|
22
|
+
q4: '\n Documentation depth?\n [1] Concise — summary + public API\n [2] Standard — + design decisions and gotchas (recommended)\n [3] Detailed — + examples and full context\n > ',
|
|
23
|
+
saved: '✓ Preferences saved',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseFlags(args) {
|
|
28
|
+
const get = (name) => {
|
|
29
|
+
const eq = args?.find(a => a.startsWith(`--${name}=`))
|
|
30
|
+
if (eq) return eq.slice(`--${name}=`.length)
|
|
31
|
+
const idx = args?.indexOf(`--${name}`)
|
|
32
|
+
if (idx !== undefined && idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--')) return args[idx + 1]
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
return { lang: get('lang'), description: get('description'), audience: get('audience'), depth: get('depth') }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function runQuestionnaire(args) {
|
|
39
|
+
// Flags override everything — Claude (or CI) passes these directly
|
|
40
|
+
const flags = parseFlags(args)
|
|
41
|
+
if (flags.lang) {
|
|
42
|
+
const audience = ['onboarding', 'mixed', 'team'].includes(flags.audience) ? flags.audience : 'team'
|
|
43
|
+
const docDepth = ['concise', 'detailed', 'standard'].includes(flags.depth) ? flags.depth : 'standard'
|
|
44
|
+
return { lang: flags.lang, projectDescription: flags.description || '', audience, docDepth }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// In non-interactive mode (CI, pipes) without flags: silent defaults
|
|
48
|
+
if (!process.stdin.isTTY) {
|
|
49
|
+
return { lang: 'en', projectDescription: '', audience: 'team', docDepth: 'standard' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Single readline instance for the whole questionnaire
|
|
53
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
54
|
+
const ask = (prompt) => new Promise(resolve => rl.question(prompt, a => resolve(a.trim())))
|
|
55
|
+
|
|
56
|
+
console.log('')
|
|
57
|
+
|
|
58
|
+
// Q1: Language — always bilingual so any developer understands it
|
|
59
|
+
const langRaw = await ask(
|
|
60
|
+
' Documentation language? / ¿Idioma de la documentación?\n' +
|
|
61
|
+
' [1] Español [2] English [3] Otro/Other: ___\n' +
|
|
62
|
+
' > '
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
let lang
|
|
66
|
+
const l = langRaw.toLowerCase()
|
|
67
|
+
if (l === '1' || l.startsWith('es') || l.startsWith('sp')) lang = 'es'
|
|
68
|
+
else if (l === '2' || l.startsWith('en')) lang = 'en'
|
|
69
|
+
else lang = langRaw || 'en'
|
|
70
|
+
|
|
71
|
+
const s = Q[lang] || Q.en
|
|
72
|
+
|
|
73
|
+
// Q2–Q4 in the chosen language
|
|
74
|
+
const description = await ask('\n' + s.q2)
|
|
75
|
+
const audRaw = await ask(s.q3)
|
|
76
|
+
const depthRaw = await ask(s.q4)
|
|
77
|
+
|
|
78
|
+
rl.close()
|
|
79
|
+
|
|
80
|
+
const audience = audRaw === '2' ? 'onboarding' : audRaw === '3' ? 'mixed' : 'team'
|
|
81
|
+
const docDepth = depthRaw === '1' ? 'concise' : depthRaw === '3' ? 'detailed' : 'standard'
|
|
82
|
+
|
|
83
|
+
console.log(`\n ${s.saved}\n`)
|
|
84
|
+
return { lang, projectDescription: description, audience, docDepth }
|
|
85
|
+
}
|
|
6
86
|
|
|
7
87
|
const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates')
|
|
8
88
|
const VALID_TEMPLATES = ['nextjs', 'fastapi', 'express', 'monorepo', 'go']
|
|
89
|
+
const PORT = 4242
|
|
90
|
+
|
|
91
|
+
const SOURCE_DIRS = ['src', 'lib', 'app', 'pkg', 'internal', 'api', 'routes', 'controllers', 'handlers', 'packages']
|
|
92
|
+
const SOURCE_EXTS = new Set(['.js', '.ts', '.mjs', '.jsx', '.tsx', '.py', '.go'])
|
|
93
|
+
const IGNORE_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '__pycache__', '.docutrack', 'docs', '.worktrees', 'coverage', '.turbo'])
|
|
94
|
+
const IGNORE_RE = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.d\.ts$/, /\.min\.js$/]
|
|
9
95
|
|
|
10
96
|
function copyFile(src, dest) {
|
|
11
97
|
const dir = path.dirname(dest)
|
|
@@ -25,8 +111,16 @@ function copyDir(src, dest) {
|
|
|
25
111
|
|
|
26
112
|
function step(msg) { process.stdout.write(` ${msg}\n`) }
|
|
27
113
|
|
|
114
|
+
function openBrowser(url) {
|
|
115
|
+
try {
|
|
116
|
+
const cmd = process.platform === 'win32' ? `start "" "${url}"`
|
|
117
|
+
: process.platform === 'darwin' ? `open "${url}"`
|
|
118
|
+
: `xdg-open "${url}"`
|
|
119
|
+
exec(cmd)
|
|
120
|
+
} catch { /* best-effort */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
28
123
|
function autoDetectTemplate() {
|
|
29
|
-
// Node.js: check package.json
|
|
30
124
|
try {
|
|
31
125
|
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
32
126
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
@@ -34,31 +128,65 @@ function autoDetectTemplate() {
|
|
|
34
128
|
if (pkg.workspaces || fs.existsSync('pnpm-workspace.yaml') || fs.existsSync('turbo.json')) return 'monorepo'
|
|
35
129
|
if (deps.express || deps['@types/express'] || deps.fastify || deps.koa) return 'express'
|
|
36
130
|
} catch { /* not a node project */ }
|
|
37
|
-
|
|
38
|
-
// Python: check for FastAPI
|
|
39
131
|
for (const f of ['requirements.txt', 'pyproject.toml', 'Pipfile']) {
|
|
40
132
|
if (!fs.existsSync(f)) continue
|
|
41
|
-
|
|
42
|
-
if (content.includes('fastapi')) return 'fastapi'
|
|
133
|
+
if (fs.readFileSync(f, 'utf8').toLowerCase().includes('fastapi')) return 'fastapi'
|
|
43
134
|
}
|
|
44
|
-
|
|
45
|
-
// Go
|
|
46
135
|
if (fs.existsSync('go.mod')) return 'go'
|
|
47
|
-
|
|
48
136
|
return null
|
|
49
137
|
}
|
|
50
138
|
|
|
139
|
+
function collectSourceFiles(root) {
|
|
140
|
+
const files = []
|
|
141
|
+
const seen = new Set()
|
|
142
|
+
const walk = (dir, depth = 0) => {
|
|
143
|
+
if (depth > 8 || !fs.existsSync(dir)) return
|
|
144
|
+
let entries
|
|
145
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
146
|
+
for (const e of entries) {
|
|
147
|
+
if (e.isDirectory()) {
|
|
148
|
+
if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) walk(path.join(dir, e.name), depth + 1)
|
|
149
|
+
} else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
150
|
+
if (!IGNORE_RE.some(re => re.test(e.name))) {
|
|
151
|
+
const rel = path.relative(root, path.join(dir, e.name)).replace(/\\/g, '/')
|
|
152
|
+
if (!seen.has(rel)) { seen.add(rel); files.push(rel) }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Walk from root — handles any project structure, not just known conventions
|
|
158
|
+
walk(root)
|
|
159
|
+
return files
|
|
160
|
+
}
|
|
161
|
+
|
|
51
162
|
async function run(args) {
|
|
52
|
-
|
|
163
|
+
const root = process.cwd()
|
|
164
|
+
const noServe = args?.includes('--no-serve')
|
|
53
165
|
|
|
54
|
-
// Guard: already initialized
|
|
166
|
+
// ── Guard: already initialized ────────────────────────────────
|
|
55
167
|
if (fs.existsSync('.docutrack')) {
|
|
56
|
-
|
|
57
|
-
|
|
168
|
+
// If called again, just ensure server is running
|
|
169
|
+
if (!noServe) {
|
|
170
|
+
const portBusy = await isPortInUse(PORT)
|
|
171
|
+
const alive = isServerRunning(root)
|
|
172
|
+
if (!portBusy && !alive) {
|
|
173
|
+
const { pid } = startServerDaemon(root, PORT)
|
|
174
|
+
await new Promise(r => setTimeout(r, 900))
|
|
175
|
+
console.log(`\n DocuTrack viewer → http://localhost:${PORT} (pid ${pid})\n`)
|
|
176
|
+
openBrowser(`http://localhost:${PORT}`)
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`\n DocuTrack already active → http://localhost:${PORT}\n`)
|
|
179
|
+
openBrowser(`http://localhost:${PORT}`)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
console.log('\n DocuTrack already initialized. Run "docutrack status" to see the queue.\n')
|
|
183
|
+
}
|
|
58
184
|
return
|
|
59
185
|
}
|
|
60
186
|
|
|
61
|
-
|
|
187
|
+
console.log('\n DocuTrack — setting up your project\n ' + '─'.repeat(42))
|
|
188
|
+
|
|
189
|
+
// ── Resolve template ───────────────────────────────────────────
|
|
62
190
|
const templateFlag = args?.find(a => a.startsWith('--template='))?.split('=')[1]
|
|
63
191
|
|| (args?.indexOf('--template') !== -1 ? args[args.indexOf('--template') + 1] : null)
|
|
64
192
|
const template = (templateFlag && VALID_TEMPLATES.includes(templateFlag))
|
|
@@ -66,117 +194,154 @@ async function run(args) {
|
|
|
66
194
|
: autoDetectTemplate()
|
|
67
195
|
|
|
68
196
|
if (templateFlag && !VALID_TEMPLATES.includes(templateFlag)) {
|
|
69
|
-
console.
|
|
197
|
+
console.error(`Unknown template: "${templateFlag}". Valid options: ${VALID_TEMPLATES.join(', ')}\n`)
|
|
70
198
|
process.exit(1)
|
|
71
199
|
}
|
|
72
200
|
|
|
73
|
-
// 1. .docutrack/ structure
|
|
201
|
+
// ── 1. .docutrack/ structure ───────────────────────────────────
|
|
74
202
|
fs.mkdirSync('.docutrack/hooks', { recursive: true })
|
|
75
|
-
step('Created .docutrack/')
|
|
203
|
+
step('✓ Created .docutrack/')
|
|
76
204
|
|
|
77
|
-
// 2. Queue
|
|
205
|
+
// ── 2. Queue ───────────────────────────────────────────────────
|
|
78
206
|
fs.writeFileSync('.docutrack/queue.json', JSON.stringify({ pending: [], lastClear: null }, null, 2))
|
|
79
|
-
step('Created .docutrack/queue.json')
|
|
80
207
|
|
|
81
|
-
// 3. Hook scripts
|
|
208
|
+
// ── 3. Hook scripts ────────────────────────────────────────────
|
|
82
209
|
copyFile(path.join(TEMPLATES_DIR, 'hooks', 'post-tool-use.js'), '.docutrack/hooks/post-tool-use.js')
|
|
83
210
|
copyFile(path.join(TEMPLATES_DIR, 'hooks', 'on-stop.js'), '.docutrack/hooks/on-stop.js')
|
|
84
|
-
step('Installed hooks
|
|
211
|
+
step('✓ Installed hooks (PostToolUse + Stop)')
|
|
85
212
|
|
|
86
|
-
// 4. /docs structure
|
|
213
|
+
// ── 4. /docs structure ─────────────────────────────────────────
|
|
87
214
|
copyDir(path.join(TEMPLATES_DIR, 'docs'), 'docs')
|
|
88
|
-
step('Created docs/ (modules/, decisions/, api/)')
|
|
89
215
|
|
|
90
|
-
// 5. ARCHITECTURE.md
|
|
216
|
+
// ── 5. ARCHITECTURE.md ─────────────────────────────────────────
|
|
91
217
|
if (!fs.existsSync('ARCHITECTURE.md')) {
|
|
92
218
|
const stackArch = template && path.join(TEMPLATES_DIR, 'stacks', template, 'ARCHITECTURE.md')
|
|
93
219
|
const archSrc = (stackArch && fs.existsSync(stackArch))
|
|
94
220
|
? stackArch
|
|
95
221
|
: path.join(TEMPLATES_DIR, 'ARCHITECTURE.md')
|
|
96
222
|
copyFile(archSrc, 'ARCHITECTURE.md')
|
|
97
|
-
step('Created ARCHITECTURE.md' + (template ? ` (${template} template)` : ''))
|
|
98
|
-
} else {
|
|
99
|
-
step('ARCHITECTURE.md already exists — skipped')
|
|
100
223
|
}
|
|
224
|
+
step(`✓ Created docs/ and ARCHITECTURE.md${template ? ` (${template})` : ''}`)
|
|
101
225
|
|
|
102
|
-
// 6. Slash commands
|
|
226
|
+
// ── 6. Slash commands ──────────────────────────────────────────
|
|
103
227
|
const commandsDir = path.join(TEMPLATES_DIR, 'commands')
|
|
104
228
|
for (const name of fs.readdirSync(commandsDir)) {
|
|
105
229
|
copyFile(path.join(commandsDir, name), path.join('.claude', 'commands', name))
|
|
106
230
|
}
|
|
107
|
-
step('Installed slash commands → .claude/commands/ (doc-map, arch-review, adr-new, ask-docs)')
|
|
108
231
|
|
|
109
|
-
// 7. Documentalista
|
|
232
|
+
// ── 7. Documentalista subagent ─────────────────────────────────
|
|
110
233
|
const stackAgent = template && path.join(TEMPLATES_DIR, 'stacks', template, 'documentalista.md')
|
|
111
234
|
const agentSrc = (stackAgent && fs.existsSync(stackAgent))
|
|
112
235
|
? stackAgent
|
|
113
236
|
: path.join(TEMPLATES_DIR, 'agents', 'documentalista.md')
|
|
114
237
|
copyFile(agentSrc, '.claude/agents/documentalista.md')
|
|
115
|
-
step('Installed
|
|
238
|
+
step('✓ Installed slash commands + documentalista subagent')
|
|
239
|
+
|
|
240
|
+
// ── 8. Hooks in .claude/settings.json ────────────────────────
|
|
241
|
+
const installed = installHooks()
|
|
242
|
+
step(installed
|
|
243
|
+
? `✓ Registered hooks in ${SETTINGS_PATH}`
|
|
244
|
+
: `✓ Hooks already registered`)
|
|
245
|
+
|
|
246
|
+
// ── 9. Questionnaire — language, description, audience, depth ─
|
|
247
|
+
console.log('\n ' + '─'.repeat(42))
|
|
248
|
+
const prefs = await runQuestionnaire(args)
|
|
116
249
|
|
|
117
|
-
//
|
|
118
|
-
|
|
250
|
+
// ── 10. docutrack.config.json — merge template + prefs ────────
|
|
251
|
+
{
|
|
119
252
|
const cfgSrc = path.join(TEMPLATES_DIR, 'docutrack.config.json')
|
|
120
253
|
let cfg = JSON.parse(fs.readFileSync(cfgSrc, 'utf8'))
|
|
121
254
|
if (template) cfg.template = template
|
|
122
255
|
else delete cfg.template
|
|
256
|
+
cfg.lang = prefs.lang
|
|
257
|
+
cfg.projectDescription = prefs.projectDescription
|
|
258
|
+
cfg.audience = prefs.audience
|
|
259
|
+
cfg.docDepth = prefs.docDepth
|
|
123
260
|
fs.writeFileSync('docutrack.config.json', JSON.stringify(cfg, null, 2))
|
|
124
|
-
step('Created docutrack.config.json')
|
|
125
261
|
}
|
|
126
262
|
|
|
127
|
-
//
|
|
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
|
|
263
|
+
// ── 12. Auto-write snippet to CLAUDE.md (with language injected) ─
|
|
137
264
|
const snippetPath = path.join(TEMPLATES_DIR, 'claude-snippet.md')
|
|
138
|
-
const
|
|
265
|
+
const snippetBase = fs.readFileSync(snippetPath, 'utf8')
|
|
139
266
|
copyFile(snippetPath, '.docutrack/claude-snippet.md')
|
|
140
267
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
: ''
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
}
|
|
268
|
+
// Inject the configured language so Claude always knows — without needing to read config.json
|
|
269
|
+
const LANG_LINE = {
|
|
270
|
+
es: '> **Idioma de documentación**: Español. Escribe TODA la documentación en español, sin excepción.',
|
|
271
|
+
en: '> **Documentation language**: English. Write ALL documentation in English.',
|
|
272
|
+
}
|
|
273
|
+
const langLine = LANG_LINE[prefs.lang] || `> **Documentation language**: ${prefs.lang}. Write ALL documentation in ${prefs.lang}.`
|
|
274
|
+
// Insert lang note after the first line (the heading) — avoids em-dash encoding mismatches
|
|
275
|
+
const snippetLines = snippetBase.split('\n')
|
|
276
|
+
snippetLines.splice(1, 0, '', langLine)
|
|
277
|
+
const snippet = snippetLines.join('\n')
|
|
169
278
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
279
|
+
const CLAUDE_MD = 'CLAUDE.md'
|
|
280
|
+
const SNIPPET_MARKER = 'DocuTrack — documentation auto-pilot'
|
|
281
|
+
if (!fs.existsSync(CLAUDE_MD)) {
|
|
282
|
+
fs.writeFileSync(CLAUDE_MD, snippet + '\n')
|
|
283
|
+
step('✓ Created CLAUDE.md with DocuTrack auto-pilot')
|
|
284
|
+
} else {
|
|
285
|
+
const existing = fs.readFileSync(CLAUDE_MD, 'utf8')
|
|
286
|
+
if (!existing.includes(SNIPPET_MARKER)) {
|
|
287
|
+
fs.writeFileSync(CLAUDE_MD, existing.trimEnd() + '\n\n---\n\n' + snippet + '\n')
|
|
288
|
+
step('✓ Added DocuTrack auto-pilot to existing CLAUDE.md')
|
|
289
|
+
} else {
|
|
290
|
+
step('✓ CLAUDE.md already has DocuTrack auto-pilot')
|
|
177
291
|
}
|
|
178
292
|
}
|
|
179
|
-
|
|
293
|
+
|
|
294
|
+
// ── 13. Scan existing source files ────────────────────────────
|
|
295
|
+
const sourceFiles = collectSourceFiles(root)
|
|
296
|
+
if (sourceFiles.length > 0) {
|
|
297
|
+
const now = new Date().toISOString()
|
|
298
|
+
writeQueue({ pending: sourceFiles.map(f => ({ file: f, addedAt: now })), lastClear: null })
|
|
299
|
+
step(`✓ Scanned ${sourceFiles.length} existing source file(s) — queued for documentation`)
|
|
300
|
+
} else {
|
|
301
|
+
step('✓ No existing source files — starting fresh')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── 14. Start viewer server ────────────────────────────────────
|
|
305
|
+
if (!noServe) {
|
|
306
|
+
const portBusy = await isPortInUse(PORT)
|
|
307
|
+
if (!portBusy) {
|
|
308
|
+
const { pid } = startServerDaemon(root, PORT)
|
|
309
|
+
await new Promise(r => setTimeout(r, 900))
|
|
310
|
+
step(`✓ Viewer started → http://localhost:${PORT} (pid ${pid})`)
|
|
311
|
+
openBrowser(`http://localhost:${PORT}`)
|
|
312
|
+
} else {
|
|
313
|
+
step(`✓ Viewer already running → http://localhost:${PORT}`)
|
|
314
|
+
openBrowser(`http://localhost:${PORT}`)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Done ───────────────────────────────────────────────────────
|
|
319
|
+
console.log('\n ' + '─'.repeat(42))
|
|
320
|
+
|
|
321
|
+
// Briefing message — copied to clipboard so the user can paste it into an open Claude session
|
|
322
|
+
const isEs = prefs.lang === 'es'
|
|
323
|
+
const briefing = isEs
|
|
324
|
+
? 'DocuTrack fue inicializado en este proyecto. Revisa el CLAUDE.md actualizado y ejecuta el documentalista para los archivos pendientes. Para el resto de esta sesión documenta manualmente cada archivo nuevo que crees.'
|
|
325
|
+
: 'DocuTrack was just initialized in this project. Read the updated CLAUDE.md and run the documentalista for pending files. For the rest of this session, manually document each new file you create.'
|
|
326
|
+
|
|
327
|
+
copyToClipboard(briefing)
|
|
328
|
+
|
|
329
|
+
const queueNote = sourceFiles.length > 0
|
|
330
|
+
? (isEs ? `${sourceFiles.length} archivo(s) en cola.` : `${sourceFiles.length} file(s) queued.`)
|
|
331
|
+
: (isEs ? 'Listo para nuevos archivos.' : 'Ready for new files.')
|
|
332
|
+
|
|
333
|
+
console.log(isEs
|
|
334
|
+
? `\n DocuTrack listo. ${queueNote}\n\n Si ya tienes Claude Code abierto en este proyecto, pega esto (ya está en el portapapeles):\n\n ${briefing}\n\n Si no, abre Claude Code — documentará todo automáticamente.\n`
|
|
335
|
+
: `\n DocuTrack ready. ${queueNote}\n\n If Claude Code is already open in this project, paste this (already in clipboard):\n\n ${briefing}\n\n Otherwise, open Claude Code — it will document everything automatically.\n`
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function copyToClipboard(text) {
|
|
340
|
+
try {
|
|
341
|
+
const { spawnSync } = require('child_process')
|
|
342
|
+
if (process.platform === 'win32') spawnSync('clip', [], { input: text, encoding: 'utf8' })
|
|
343
|
+
else if (process.platform === 'darwin') spawnSync('pbcopy', [], { input: text, encoding: 'utf8' })
|
|
344
|
+
} catch { /* best-effort */ }
|
|
180
345
|
}
|
|
181
346
|
|
|
182
347
|
module.exports = { run }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
|
|
7
|
+
const GLOBAL_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json')
|
|
8
|
+
const GLOBAL_CLAUDE_MD = path.join(os.homedir(), '.claude', 'CLAUDE.md')
|
|
9
|
+
const MARKER = '<!-- docutrack -->'
|
|
10
|
+
|
|
11
|
+
const GLOBAL_SNIPPET = `
|
|
12
|
+
## DocuTrack — documentation auto-pilot
|
|
13
|
+
|
|
14
|
+
DocuTrack is installed globally on this machine. When a user asks to use DocuTrack,
|
|
15
|
+
or asks to build a project "with docutrack" or "with documentation":
|
|
16
|
+
|
|
17
|
+
**Step 1 — ask these questions in the chat** (in the user's language):
|
|
18
|
+
1. Documentation language? (e.g. Español, English, Português…)
|
|
19
|
+
2. Describe the project in one sentence
|
|
20
|
+
3. Who reads the docs? → \`team\` (technical) / \`onboarding\` (new devs) / \`mixed\`
|
|
21
|
+
4. Documentation depth? → \`concise\` / \`standard\` / \`detailed\`
|
|
22
|
+
|
|
23
|
+
**Step 2 — run init with the answers as flags:**
|
|
24
|
+
\`\`\`bash
|
|
25
|
+
docutrack init --lang=<es|en|…> --description="<desc>" --audience=<team|onboarding|mixed> --depth=<concise|standard|detailed>
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
**Step 3 — build the project normally.**
|
|
29
|
+
Files you create will be queued automatically. When the session ends, run the
|
|
30
|
+
documentalista subagent to document everything.
|
|
31
|
+
|
|
32
|
+
If the project already has \`.docutrack/\` (already initialized), skip to Step 3.
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
async function run() {
|
|
36
|
+
const hooksDir = path.join(__dirname, '..', 'hooks')
|
|
37
|
+
const postHookPath = path.join(hooksDir, 'global-post-tool-use.js')
|
|
38
|
+
const stopHookPath = path.join(hooksDir, 'global-on-stop.js')
|
|
39
|
+
const dir = path.dirname(GLOBAL_SETTINGS)
|
|
40
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
// ── 1. settings.json — hooks ───────────────────────────────────
|
|
43
|
+
let settings = {}
|
|
44
|
+
if (fs.existsSync(GLOBAL_SETTINGS)) {
|
|
45
|
+
try { settings = JSON.parse(fs.readFileSync(GLOBAL_SETTINGS, 'utf8')) } catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hooksAlreadyInstalled = settings?.hooks?.PostToolUse?.some(h =>
|
|
49
|
+
h.hooks?.some(c => c.command?.includes('global-post-tool-use'))
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if (!hooksAlreadyInstalled) {
|
|
53
|
+
if (!settings.hooks) settings.hooks = {}
|
|
54
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = []
|
|
55
|
+
settings.hooks.PostToolUse.push({
|
|
56
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
57
|
+
hooks: [{ type: 'command', command: `node "${postHookPath}"` }],
|
|
58
|
+
})
|
|
59
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = []
|
|
60
|
+
settings.hooks.Stop.push({
|
|
61
|
+
hooks: [{ type: 'command', command: `node "${stopHookPath}"` }],
|
|
62
|
+
})
|
|
63
|
+
fs.writeFileSync(GLOBAL_SETTINGS, JSON.stringify(settings, null, 2))
|
|
64
|
+
console.log(` ✓ Hooks registered → ${GLOBAL_SETTINGS}`)
|
|
65
|
+
} else {
|
|
66
|
+
console.log(` ✓ Hooks already registered`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── 2. CLAUDE.md — global instructions ────────────────────────
|
|
70
|
+
const claudeMdAlreadyInstalled = fs.existsSync(GLOBAL_CLAUDE_MD) &&
|
|
71
|
+
fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf8').includes(MARKER)
|
|
72
|
+
|
|
73
|
+
if (!claudeMdAlreadyInstalled) {
|
|
74
|
+
const existing = fs.existsSync(GLOBAL_CLAUDE_MD)
|
|
75
|
+
? fs.readFileSync(GLOBAL_CLAUDE_MD, 'utf8').trimEnd() + '\n\n---\n'
|
|
76
|
+
: ''
|
|
77
|
+
fs.writeFileSync(GLOBAL_CLAUDE_MD, existing + MARKER + GLOBAL_SNIPPET)
|
|
78
|
+
console.log(` ✓ Global instructions written → ${GLOBAL_CLAUDE_MD}`)
|
|
79
|
+
} else {
|
|
80
|
+
console.log(` ✓ Global instructions already present`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`
|
|
84
|
+
DocuTrack is ready globally.
|
|
85
|
+
|
|
86
|
+
From now on, in any Claude Code session just say:
|
|
87
|
+
"build me X and use docutrack for documentation"
|
|
88
|
+
|
|
89
|
+
Claude will ask for your preferences and set everything up automatically.
|
|
90
|
+
`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { run }
|
package/src/commands/scan.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { write: writeQueue, read: readQueue } = require('../utils/queue')
|
|
6
6
|
|
|
7
|
-
const SOURCE_DIRS = ['src', 'lib', 'app', 'pkg', 'internal', 'api', 'routes', 'controllers', 'handlers']
|
|
7
|
+
const SOURCE_DIRS = ['src', 'lib', 'app', 'pkg', 'internal', 'api', 'routes', 'controllers', 'handlers', 'packages']
|
|
8
8
|
const SOURCE_EXTS = new Set(['.js', '.ts', '.mjs', '.jsx', '.tsx', '.py', '.go'])
|
|
9
9
|
const IGNORE_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '__pycache__', '.docutrack', 'docs', '.worktrees', 'coverage', '.turbo'])
|
|
10
10
|
const IGNORE_PATTERNS = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.d\.ts$/, /\.min\.js$/]
|
|
@@ -24,24 +24,12 @@ async function run(args) {
|
|
|
24
24
|
const existing = readQueue(root)
|
|
25
25
|
const alreadyQueued = new Set(existing.pending.map(e => e.file))
|
|
26
26
|
|
|
27
|
-
// Find all source files
|
|
27
|
+
// Find all source files — walk from root to handle any project structure
|
|
28
28
|
const files = []
|
|
29
|
-
|
|
30
|
-
const full = path.join(root, dir)
|
|
31
|
-
if (fs.existsSync(full)) walk(full, files, root)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Also scan root-level source files (index.js, server.js, main.go, etc.)
|
|
35
|
-
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
36
|
-
if (entry.isFile() && SOURCE_EXTS.has(path.extname(entry.name))) {
|
|
37
|
-
const rel = entry.name
|
|
38
|
-
if (!IGNORE_PATTERNS.some(re => re.test(rel))) files.push(rel)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
29
|
+
walk(root, files, root)
|
|
41
30
|
|
|
42
31
|
if (files.length === 0) {
|
|
43
32
|
console.log(' No source files found to scan.\n')
|
|
44
|
-
console.log(' Checked: ' + SOURCE_DIRS.join(', ') + '\n')
|
|
45
33
|
return
|
|
46
34
|
}
|
|
47
35
|
|