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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const readline = require('readline')
|
|
6
|
+
const { write: writeQueue, read: readQueue } = require('../utils/queue')
|
|
7
|
+
const { isPortInUse, startServerDaemon, isServerRunning } = require('../utils/daemon')
|
|
8
|
+
|
|
9
|
+
const PORT = 4242
|
|
10
|
+
const SOURCE_DIRS = ['src', 'lib', 'app', 'pkg', 'internal', 'api', 'routes', 'controllers', 'handlers', 'packages']
|
|
11
|
+
const SOURCE_EXTS = new Set(['.js', '.ts', '.mjs', '.jsx', '.tsx', '.py', '.go'])
|
|
12
|
+
const IGNORE_DIRS = new Set(['node_modules', '.next', '.git', 'dist', 'build', '__pycache__', '.docutrack', 'docs', '.worktrees', 'coverage', '.turbo'])
|
|
13
|
+
const IGNORE_RE = [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.d\.ts$/, /\.min\.js$/]
|
|
14
|
+
|
|
15
|
+
function ask(question) {
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
18
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim().toLowerCase()) })
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function step(icon, msg) { console.log(` ${icon} ${msg}`) }
|
|
23
|
+
|
|
24
|
+
function collectSourceFiles(root) {
|
|
25
|
+
const files = []
|
|
26
|
+
const walk = (dir, depth = 0) => {
|
|
27
|
+
if (depth > 6 || !fs.existsSync(dir)) return
|
|
28
|
+
let entries
|
|
29
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
30
|
+
for (const e of entries) {
|
|
31
|
+
if (e.isDirectory()) {
|
|
32
|
+
if (!IGNORE_DIRS.has(e.name) && !e.name.startsWith('.')) walk(path.join(dir, e.name), depth + 1)
|
|
33
|
+
} else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) {
|
|
34
|
+
if (!IGNORE_RE.some(re => re.test(e.name))) {
|
|
35
|
+
files.push(path.relative(root, path.join(dir, e.name)).replace(/\\/g, '/'))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const dir of SOURCE_DIRS) walk(path.join(root, dir))
|
|
41
|
+
// Root-level source files (index.js, server.js, main.go, etc.)
|
|
42
|
+
try {
|
|
43
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
44
|
+
if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name)) && !IGNORE_RE.some(re => re.test(e.name))) {
|
|
45
|
+
files.push(e.name)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch { /* ok */ }
|
|
49
|
+
return files
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function run(args) {
|
|
53
|
+
const cwd = process.cwd()
|
|
54
|
+
|
|
55
|
+
console.log('\n DocuTrack — setup\n ' + '─'.repeat(40))
|
|
56
|
+
|
|
57
|
+
// ── 1. Initialize if needed ───────────────────────────────────
|
|
58
|
+
if (!fs.existsSync(path.join(cwd, '.docutrack'))) {
|
|
59
|
+
step('⚙', 'Initializing DocuTrack...')
|
|
60
|
+
console.log('')
|
|
61
|
+
await require('./init').run([...(args || []), '--no-serve'])
|
|
62
|
+
console.log('\n ' + '─'.repeat(40))
|
|
63
|
+
} else {
|
|
64
|
+
step('✓', 'DocuTrack already initialized')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 2. Scan existing source files ─────────────────────────────
|
|
68
|
+
// read/write use a relative path (.docutrack/queue.json) — don't pass cwd as queuePath
|
|
69
|
+
const queue = readQueue()
|
|
70
|
+
const alreadyQueued = new Set(queue.pending.map(e => e.file))
|
|
71
|
+
|
|
72
|
+
if (queue.pending.length === 0) {
|
|
73
|
+
const all = collectSourceFiles(cwd)
|
|
74
|
+
const newFiles = all.filter(f => !alreadyQueued.has(f))
|
|
75
|
+
if (newFiles.length > 0) {
|
|
76
|
+
const now = new Date().toISOString()
|
|
77
|
+
for (const f of newFiles) queue.pending.push({ file: f, addedAt: now })
|
|
78
|
+
writeQueue(queue)
|
|
79
|
+
step('📂', `Scanned ${newFiles.length} source file(s) — queued for documentation`)
|
|
80
|
+
} else {
|
|
81
|
+
step('○', 'No existing source files found — fresh project')
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
step('✓', `${queue.pending.length} file(s) already in documentation queue`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('')
|
|
88
|
+
|
|
89
|
+
// ── 3. Start viewer server (with y/n prompt) ──────────────────
|
|
90
|
+
const portBusy = await isPortInUse(PORT)
|
|
91
|
+
const serverAlive = isServerRunning(cwd)
|
|
92
|
+
|
|
93
|
+
if (portBusy || serverAlive) {
|
|
94
|
+
step('✓', `Viewer already running → http://localhost:${PORT}`)
|
|
95
|
+
} else {
|
|
96
|
+
const answer = await ask(` Start DocuTrack viewer on port ${PORT}? (y/n): `)
|
|
97
|
+
console.log('')
|
|
98
|
+
if (answer === 'y' || answer === 'yes') {
|
|
99
|
+
const { pid } = startServerDaemon(cwd, PORT)
|
|
100
|
+
await new Promise(r => setTimeout(r, 900))
|
|
101
|
+
const up = await isPortInUse(PORT)
|
|
102
|
+
step('✓', `Viewer ${up ? 'started' : 'starting'} → http://localhost:${PORT} (pid ${pid})`)
|
|
103
|
+
} else {
|
|
104
|
+
step('○', 'Viewer skipped — run "docutrack serve" when ready')
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 4. Next-step instructions ─────────────────────────────────
|
|
109
|
+
const pending = readQueue().pending.length
|
|
110
|
+
console.log('\n ' + '─'.repeat(40))
|
|
111
|
+
|
|
112
|
+
if (pending > 0) {
|
|
113
|
+
console.log(`\n ${pending} file(s) ready to document.\n`)
|
|
114
|
+
console.log(' In Claude Code, say:')
|
|
115
|
+
console.log(' "Run the documentalista to document all pending files"\n')
|
|
116
|
+
console.log(' The documentalista subagent will process every file in the')
|
|
117
|
+
console.log(' queue and write docs automatically.\n')
|
|
118
|
+
} else {
|
|
119
|
+
console.log('\n DocuTrack is active.\n')
|
|
120
|
+
console.log(' • Every file Claude writes gets added to the queue automatically')
|
|
121
|
+
console.log(' • The Stop hook reminds Claude to document at session end')
|
|
122
|
+
console.log(' • Tell Claude: "Run the documentalista" to process the queue\n')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { run }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Global DocuTrack Stop hook
|
|
4
|
+
// Installed once in ~/.claude/settings.json — fires in every Claude Code session
|
|
5
|
+
// Delegates to the project-level hook only if this project has been initialized
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const { spawnSync } = require('child_process')
|
|
10
|
+
|
|
11
|
+
const projectHook = path.join(process.cwd(), '.docutrack', 'hooks', 'on-stop.js')
|
|
12
|
+
if (!fs.existsSync(projectHook)) process.exit(0)
|
|
13
|
+
|
|
14
|
+
spawnSync(process.execPath, [projectHook], {
|
|
15
|
+
stdio: 'inherit',
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
})
|
|
18
|
+
process.exit(0)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Global DocuTrack PostToolUse hook
|
|
4
|
+
// Installed once in ~/.claude/settings.json — fires in every Claude Code session
|
|
5
|
+
// Delegates to the project-level hook only if this project has been initialized
|
|
6
|
+
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const { spawnSync } = require('child_process')
|
|
10
|
+
|
|
11
|
+
const projectHook = path.join(process.cwd(), '.docutrack', 'hooks', 'post-tool-use.js')
|
|
12
|
+
if (!fs.existsSync(projectHook)) process.exit(0)
|
|
13
|
+
|
|
14
|
+
let input = ''
|
|
15
|
+
process.stdin.setEncoding('utf8')
|
|
16
|
+
process.stdin.on('data', chunk => { input += chunk })
|
|
17
|
+
process.stdin.on('end', () => {
|
|
18
|
+
spawnSync(process.execPath, [projectHook], {
|
|
19
|
+
input,
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
})
|
|
24
|
+
process.exit(0)
|
|
25
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const net = require('net')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const { spawn } = require('child_process')
|
|
7
|
+
|
|
8
|
+
function isPortInUse(port) {
|
|
9
|
+
return new Promise(resolve => {
|
|
10
|
+
const server = net.createServer()
|
|
11
|
+
server.once('error', () => resolve(true))
|
|
12
|
+
server.once('listening', () => { server.close(); resolve(false) })
|
|
13
|
+
server.listen(port, '127.0.0.1')
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function startServerDaemon(projectRoot, port = 4242) {
|
|
18
|
+
const binPath = path.resolve(__dirname, '../../bin/docutrack.js')
|
|
19
|
+
const child = spawn(process.execPath, [binPath, 'serve'], {
|
|
20
|
+
cwd: projectRoot,
|
|
21
|
+
detached: true,
|
|
22
|
+
stdio: 'ignore',
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
})
|
|
25
|
+
child.unref()
|
|
26
|
+
|
|
27
|
+
const pidFile = path.join(projectRoot, '.docutrack', 'server.pid')
|
|
28
|
+
try {
|
|
29
|
+
fs.writeFileSync(pidFile, JSON.stringify({ pid: child.pid, port, startedAt: new Date().toISOString() }))
|
|
30
|
+
} catch { /* ok */ }
|
|
31
|
+
|
|
32
|
+
return { pid: child.pid, port }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isServerRunning(projectRoot) {
|
|
36
|
+
const pidFile = path.join(projectRoot, '.docutrack', 'server.pid')
|
|
37
|
+
if (!fs.existsSync(pidFile)) return false
|
|
38
|
+
try {
|
|
39
|
+
const { pid } = JSON.parse(fs.readFileSync(pidFile, 'utf8'))
|
|
40
|
+
process.kill(pid, 0)
|
|
41
|
+
return true
|
|
42
|
+
} catch {
|
|
43
|
+
try { fs.unlinkSync(pidFile) } catch { /* ok */ }
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { isPortInUse, startServerDaemon, isServerRunning }
|