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.
@@ -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
- const content = fs.readFileSync(f, 'utf8').toLowerCase()
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
- console.log('\nDocuTrack initializing in current project\n')
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
- console.log('DocuTrack is already initialized in this project.')
57
- console.log('Run "docutrack status" to see the current queue.\n')
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
- // Resolve template
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.log(`Unknown template: "${templateFlag}". Valid options: ${VALID_TEMPLATES.join(', ')}\n`)
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 .docutrack/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 — use stack template if available, else base
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 use stack-specific version if available
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 documentalista subagent .claude/agents/documentalista.md' + (template ? ` (${template})` : ''))
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
- // 8. docutrack.config.json
118
- if (!fs.existsSync('docutrack.config.json')) {
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
- // 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
263
+ // ── 12. Auto-write snippet to CLAUDE.md (with language injected) ─
137
264
  const snippetPath = path.join(TEMPLATES_DIR, 'claude-snippet.md')
138
- const snippet = fs.readFileSync(snippetPath, 'utf8')
265
+ const snippetBase = fs.readFileSync(snippetPath, 'utf8')
139
266
  copyFile(snippetPath, '.docutrack/claude-snippet.md')
140
267
 
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
- }
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
- 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
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
- return false
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 }
@@ -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
- for (const dir of SOURCE_DIRS) {
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