@eltonssouza/development-utility-kit 1.0.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.
Files changed (137) hide show
  1. package/.claude/agents/analyst.md +198 -0
  2. package/.claude/agents/backend-developer.md +126 -0
  3. package/.claude/agents/brain-keeper.md +229 -0
  4. package/.claude/agents/code-reviewer.md +181 -0
  5. package/.claude/agents/database-engineer.md +94 -0
  6. package/.claude/agents/devops-engineer.md +141 -0
  7. package/.claude/agents/frontend-developer.md +97 -0
  8. package/.claude/agents/gate-keeper.md +118 -0
  9. package/.claude/agents/migrator.md +291 -0
  10. package/.claude/agents/mobile-developer.md +80 -0
  11. package/.claude/agents/n8n-specialist.md +94 -0
  12. package/.claude/agents/product-owner.md +115 -0
  13. package/.claude/agents/qa-engineer.md +232 -0
  14. package/.claude/agents/release-engineer.md +204 -0
  15. package/.claude/agents/scaffold.md +87 -0
  16. package/.claude/agents/security-engineer.md +199 -0
  17. package/.claude/agents/sprint-runner.md +44 -0
  18. package/.claude/agents/stack-resolver.md +84 -0
  19. package/.claude/agents/tech-lead.md +182 -0
  20. package/.claude/agents/update-template.md +54 -0
  21. package/.claude/agents/ux-designer.md +118 -0
  22. package/.claude/settings.json +44 -0
  23. package/.claude/skills/README.md +332 -0
  24. package/.claude/skills/active-project/SKILL.md +129 -0
  25. package/.claude/skills/api-integration-test/SKILL.md +64 -0
  26. package/.claude/skills/auto-test-guard/SKILL.md +237 -0
  27. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  28. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  29. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  30. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  31. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  32. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  33. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  34. package/.claude/skills/brain-keeper/SKILL.md +60 -0
  35. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  36. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  37. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  38. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  39. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  40. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  41. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  42. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  43. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  44. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  45. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  46. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  47. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  48. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  49. package/.claude/skills/caveman/SKILL.md +187 -0
  50. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  51. package/.claude/skills/grill-me/SKILL.md +79 -0
  52. package/.claude/skills/honcho-memory/SKILL.md +207 -0
  53. package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
  54. package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
  55. package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
  56. package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
  57. package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
  58. package/.claude/skills/honcho-memory/package.json +32 -0
  59. package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
  60. package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
  61. package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
  62. package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
  63. package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
  64. package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
  65. package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
  66. package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
  67. package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
  68. package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
  69. package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
  70. package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
  71. package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
  72. package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
  73. package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
  74. package/.claude/skills/pair-debug/SKILL.md +288 -0
  75. package/.claude/skills/prd-ready-check/SKILL.md +58 -0
  76. package/.claude/skills/project-manager/SKILL.md +167 -0
  77. package/.claude/skills/quality-standards/SKILL.md +201 -0
  78. package/.claude/skills/quick-feature/SKILL.md +264 -0
  79. package/.claude/skills/run-sprint/SKILL.md +342 -0
  80. package/.claude/skills/scaffold/SKILL.md +58 -0
  81. package/.claude/skills/stack-discovery/SKILL.md +159 -0
  82. package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
  83. package/.claude/skills/to-issues/SKILL.md +163 -0
  84. package/.claude/skills/to-prd/SKILL.md +130 -0
  85. package/.claude/skills/update-template/SKILL.md +254 -0
  86. package/.claude/stacks/CODEOWNERS +30 -0
  87. package/.claude/stacks/README.md +88 -0
  88. package/.claude/stacks/_template.md +116 -0
  89. package/.claude/stacks/java/spring-boot-3.md +376 -0
  90. package/.claude/stacks/java/spring-boot-4.md +438 -0
  91. package/.claude/stacks/typescript/angular-18.md +420 -0
  92. package/.claude/stacks/typescript/angular-19.md +397 -0
  93. package/.claude/stacks/typescript/angular-21.md +494 -0
  94. package/CLAUDE.md +453 -0
  95. package/README.md +391 -0
  96. package/bin/cli.js +773 -0
  97. package/bin/lib/backup.js +62 -0
  98. package/bin/lib/detect-stack.js +476 -0
  99. package/bin/lib/help.js +233 -0
  100. package/bin/lib/identity.js +108 -0
  101. package/bin/lib/local-dir.js +69 -0
  102. package/bin/lib/manifest.js +236 -0
  103. package/bin/lib/sync-all.js +394 -0
  104. package/bin/lib/version-check.js +398 -0
  105. package/dashboard/db.js +199 -0
  106. package/dashboard/package.json +22 -0
  107. package/dashboard/public/app.js +709 -0
  108. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  109. package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
  110. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  111. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  112. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  113. package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
  114. package/dashboard/public/content/docs/pipeline.en.md +400 -0
  115. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  116. package/dashboard/public/content/docs/skills-reference.en.md +500 -0
  117. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  118. package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
  119. package/dashboard/public/content/manifest.json +102 -0
  120. package/dashboard/public/content/manual/backend.en.md +1138 -0
  121. package/dashboard/public/content/manual/existing-project.en.md +831 -0
  122. package/dashboard/public/content/manual/frontend.en.md +1065 -0
  123. package/dashboard/public/content/manual/fullstack.en.md +1508 -0
  124. package/dashboard/public/content/manual/mobile.en.md +866 -0
  125. package/dashboard/public/index.html +108 -0
  126. package/dashboard/public/style.css +610 -0
  127. package/dashboard/public/vendor/marked.min.js +69 -0
  128. package/dashboard/rtk.js +143 -0
  129. package/dashboard/server-app.js +403 -0
  130. package/dashboard/server.js +104 -0
  131. package/dashboard/test/sprint1.test.js +406 -0
  132. package/dashboard/test/sprint2.test.js +571 -0
  133. package/dashboard/test/sprint3.test.js +560 -0
  134. package/package.json +33 -0
  135. package/scripts/hooks/subagent-telemetry.sh +14 -0
  136. package/scripts/hooks/telemetry-writer.js +250 -0
  137. package/scripts/latest-versions.json +56 -0
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * version-check.js — Fetch latest versions for ecosystems and compare semver.
5
+ *
6
+ * Uses Node's native https module (no fetch — Node 18 compat). Caches at
7
+ * ~/.duk/cache/versions.json (TTL 24h). Falls back to scripts/latest-versions.json
8
+ * when network fails. All network operations are best-effort and silent on error.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const https = require('https');
15
+
16
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
17
+ const NETWORK_TIMEOUT_MS = 5000;
18
+
19
+ /**
20
+ * Resolve the cache file path.
21
+ * @returns {string}
22
+ */
23
+ function getCachePath() {
24
+ return path.join(os.homedir(), '.duk', 'cache', 'versions.json');
25
+ }
26
+
27
+ /**
28
+ * Resolve the offline fallback JSON path (shipped with the harness).
29
+ * @returns {string}
30
+ */
31
+ function getFallbackPath() {
32
+ return path.resolve(__dirname, '..', '..', 'scripts', 'latest-versions.json');
33
+ }
34
+
35
+ /**
36
+ * Read JSON file safely; returns null on any error.
37
+ * @param {string} filePath
38
+ * @returns {object|null}
39
+ */
40
+ function readJsonSafe(filePath) {
41
+ try {
42
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Write JSON file, creating parent dirs as needed. Best-effort; swallows errors.
50
+ * @param {string} filePath
51
+ * @param {object} data
52
+ */
53
+ function writeJsonSafe(filePath, data) {
54
+ try {
55
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
56
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
57
+ } catch {
58
+ // best-effort
59
+ }
60
+ }
61
+
62
+ /**
63
+ * HTTPS GET with timeout. Resolves to response body string or rejects.
64
+ * @param {string} url
65
+ * @returns {Promise<string>}
66
+ */
67
+ function httpsGet(url) {
68
+ return new Promise((resolve, reject) => {
69
+ const req = https.get(
70
+ url,
71
+ {
72
+ headers: {
73
+ 'User-Agent': 'duk-cli/0.1 (https://github.com/eltonssouza/development-utility-kit)',
74
+ Accept: 'application/json',
75
+ },
76
+ },
77
+ (res) => {
78
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
79
+ // Single redirect hop
80
+ httpsGet(res.headers.location).then(resolve, reject);
81
+ return;
82
+ }
83
+ if (!res.statusCode || res.statusCode >= 400) {
84
+ res.resume();
85
+ reject(new Error(`HTTP ${res.statusCode}`));
86
+ return;
87
+ }
88
+ let data = '';
89
+ res.setEncoding('utf8');
90
+ res.on('data', (chunk) => (data += chunk));
91
+ res.on('end', () => resolve(data));
92
+ res.on('error', reject);
93
+ }
94
+ );
95
+
96
+ req.setTimeout(NETWORK_TIMEOUT_MS, () => {
97
+ req.destroy(new Error('timeout'));
98
+ });
99
+ req.on('error', reject);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Query Maven Central for the latest version of a g:a coordinate.
105
+ * @param {string} groupId
106
+ * @param {string} artifactId
107
+ * @returns {Promise<string|null>}
108
+ */
109
+ async function mavenCentralLatest(groupId, artifactId) {
110
+ const url =
111
+ `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}` +
112
+ `&rows=1&wt=json`;
113
+ try {
114
+ const body = await httpsGet(url);
115
+ const json = JSON.parse(body);
116
+ const doc = json && json.response && Array.isArray(json.response.docs) && json.response.docs[0];
117
+ return doc && doc.latestVersion ? String(doc.latestVersion) : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Query npm registry for dist-tags.latest of a package.
125
+ * @param {string} pkgName
126
+ * @returns {Promise<string|null>}
127
+ */
128
+ async function npmRegistryLatest(pkgName) {
129
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName).replace('%40', '@')}`;
130
+ try {
131
+ const body = await httpsGet(url);
132
+ const json = JSON.parse(body);
133
+ return json && json['dist-tags'] && json['dist-tags'].latest
134
+ ? String(json['dist-tags'].latest)
135
+ : null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Query GitHub releases for tag_name of latest release.
143
+ * @param {string} owner
144
+ * @param {string} repo
145
+ * @returns {Promise<string|null>}
146
+ */
147
+ async function githubReleasesLatest(owner, repo) {
148
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
149
+ try {
150
+ const body = await httpsGet(url);
151
+ const json = JSON.parse(body);
152
+ const tag = json && json.tag_name ? String(json.tag_name) : null;
153
+ if (!tag) return null;
154
+ // strip leading 'v'
155
+ return tag.replace(/^v/i, '');
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Determine ecosystem + identifier per framework, then call the right API.
163
+ * @param {string} framework
164
+ * @returns {Promise<string|null>}
165
+ */
166
+ async function fetchFrameworkLatest(framework) {
167
+ switch (framework) {
168
+ case 'spring-boot':
169
+ return mavenCentralLatest('org.springframework.boot', 'spring-boot');
170
+ case 'angular':
171
+ return npmRegistryLatest('@angular/core');
172
+ case 'react':
173
+ return npmRegistryLatest('react');
174
+ case 'react-native':
175
+ return npmRegistryLatest('react-native');
176
+ case 'next':
177
+ return npmRegistryLatest('next');
178
+ case 'nuxt':
179
+ return npmRegistryLatest('nuxt');
180
+ case 'vue':
181
+ return npmRegistryLatest('vue');
182
+ case 'nest':
183
+ return npmRegistryLatest('@nestjs/core');
184
+ case 'express':
185
+ return npmRegistryLatest('express');
186
+ case 'fastify':
187
+ return npmRegistryLatest('fastify');
188
+ case 'django':
189
+ return githubReleasesLatest('django', 'django');
190
+ case 'fastapi':
191
+ return githubReleasesLatest('fastapi', 'fastapi');
192
+ case 'flask':
193
+ return githubReleasesLatest('pallets', 'flask');
194
+ case 'gin':
195
+ return githubReleasesLatest('gin-gonic', 'gin');
196
+ case 'echo':
197
+ return githubReleasesLatest('labstack', 'echo');
198
+ default:
199
+ return null;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Determine the latest version of a language runtime (uses fallback JSON in
205
+ * practice — language runtime cadence does not move fast enough to warrant
206
+ * an API call per install).
207
+ * @param {string} language
208
+ * @param {object} fallback
209
+ * @returns {string|null}
210
+ */
211
+ function languageLatestFromFallback(language, fallback) {
212
+ if (!fallback) return null;
213
+ switch (language) {
214
+ case 'java':
215
+ return (fallback.java && (fallback.java.latest_lts || fallback.java.latest)) || null;
216
+ case 'node':
217
+ case 'typescript':
218
+ case 'javascript':
219
+ return (fallback.node && (fallback.node.latest_lts || fallback.node.latest)) || null;
220
+ case 'python':
221
+ return (fallback.python && (fallback.python.latest_stable || fallback.python.latest)) || null;
222
+ case 'go':
223
+ return (fallback.go && fallback.go.latest) || null;
224
+ default:
225
+ return null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Cache the result of fetchFrameworkLatest using the local versions.json cache.
231
+ * @param {string} framework
232
+ * @returns {Promise<string|null>}
233
+ */
234
+ async function getFrameworkLatestCached(framework) {
235
+ const cachePath = getCachePath();
236
+ const cache = readJsonSafe(cachePath) || { _ts: 0, frameworks: {} };
237
+
238
+ const now = Date.now();
239
+ const fresh = cache._ts && now - cache._ts < CACHE_TTL_MS;
240
+ if (fresh && cache.frameworks && cache.frameworks[framework]) {
241
+ return cache.frameworks[framework];
242
+ }
243
+
244
+ const latest = await fetchFrameworkLatest(framework);
245
+ if (latest) {
246
+ cache.frameworks = cache.frameworks || {};
247
+ cache.frameworks[framework] = latest;
248
+ cache._ts = now;
249
+ writeJsonSafe(cachePath, cache);
250
+ return latest;
251
+ }
252
+
253
+ // No network — fall back to static JSON shipped with the harness
254
+ const fallback = readJsonSafe(getFallbackPath());
255
+ if (fallback && fallback[framework] && fallback[framework].latest) {
256
+ return fallback[framework].latest;
257
+ }
258
+
259
+ // Stale cache > nothing
260
+ if (cache.frameworks && cache.frameworks[framework]) {
261
+ return cache.frameworks[framework];
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /**
267
+ * Get latest versions for a stack descriptor.
268
+ * @param {{language: string, framework: string}} stack
269
+ * @returns {Promise<{language_latest: string|null, framework_latest: string|null}>}
270
+ */
271
+ async function getLatestVersions(stack) {
272
+ if (!stack || typeof stack !== 'object') {
273
+ return { language_latest: null, framework_latest: null };
274
+ }
275
+
276
+ const fallback = readJsonSafe(getFallbackPath());
277
+ const language_latest = languageLatestFromFallback(stack.language, fallback);
278
+ const framework_latest = await getFrameworkLatestCached(stack.framework);
279
+ return { language_latest, framework_latest };
280
+ }
281
+
282
+ /**
283
+ * Force-refresh the entire cache for all known frameworks.
284
+ * @returns {Promise<object>} the new cache contents
285
+ */
286
+ async function refreshLatestVersions() {
287
+ const frameworks = [
288
+ 'spring-boot',
289
+ 'angular',
290
+ 'react',
291
+ 'react-native',
292
+ 'next',
293
+ 'nuxt',
294
+ 'vue',
295
+ 'nest',
296
+ 'express',
297
+ 'fastify',
298
+ 'django',
299
+ 'fastapi',
300
+ 'flask',
301
+ 'gin',
302
+ 'echo',
303
+ ];
304
+ const cache = { _ts: Date.now(), frameworks: {} };
305
+ for (const f of frameworks) {
306
+ const v = await fetchFrameworkLatest(f);
307
+ if (v) cache.frameworks[f] = v;
308
+ }
309
+ writeJsonSafe(getCachePath(), cache);
310
+ return cache;
311
+ }
312
+
313
+ // ─── semver compare ──────────────────────────────────────────────────────────
314
+
315
+ /**
316
+ * Parse a semver-ish string into [major, minor, patch] integers.
317
+ * Pre-release and build metadata are dropped.
318
+ * @param {string} v
319
+ * @returns {[number, number, number]}
320
+ */
321
+ function parseSemver(v) {
322
+ if (!v || typeof v !== 'string') return [0, 0, 0];
323
+ const m = v.match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
324
+ if (!m) return [0, 0, 0];
325
+ return [parseInt(m[1], 10) || 0, parseInt(m[2] || '0', 10), parseInt(m[3] || '0', 10)];
326
+ }
327
+
328
+ /**
329
+ * Compare two versions. Returns -1, 0, or 1.
330
+ * Accepts plain semver only.
331
+ * @param {string} a
332
+ * @param {string} b
333
+ * @returns {number}
334
+ */
335
+ function compareSemverPair(a, b) {
336
+ const [a1, a2, a3] = parseSemver(a);
337
+ const [b1, b2, b3] = parseSemver(b);
338
+ if (a1 !== b1) return a1 < b1 ? -1 : 1;
339
+ if (a2 !== b2) return a2 < b2 ? -1 : 1;
340
+ if (a3 !== b3) return a3 < b3 ? -1 : 1;
341
+ return 0;
342
+ }
343
+
344
+ /**
345
+ * Compare two versions (or a version against a range expression).
346
+ *
347
+ * Plain compare (both args plain versions): returns -1, 0, 1.
348
+ *
349
+ * Range compare (b is "<X", ">=X", "<=X", ">X", "=X"): returns -1/0/1 where
350
+ * -1 = a satisfies "less than b"
351
+ * 0 = a satisfies the expression exactly
352
+ * 1 = a does not satisfy (a is greater/outside the constraint)
353
+ *
354
+ * Practical contract used by sync-all filters: `compareVersions(localVersion, "<0.2") === -1`
355
+ * means the local version is strictly less than 0.2 (i.e. matches the filter).
356
+ *
357
+ * @param {string} a
358
+ * @param {string} b
359
+ * @returns {number}
360
+ */
361
+ function compareVersions(a, b) {
362
+ if (typeof b === 'string' && /^(>=|<=|>|<|=)/.test(b)) {
363
+ const m = b.match(/^(>=|<=|>|<|=)\s*(.+)$/);
364
+ if (!m) return compareSemverPair(a, b);
365
+ const op = m[1];
366
+ const ref = m[2].trim();
367
+ const cmp = compareSemverPair(a, ref);
368
+ switch (op) {
369
+ case '<':
370
+ return cmp < 0 ? -1 : 1;
371
+ case '<=':
372
+ return cmp <= 0 ? -1 : 1;
373
+ case '>':
374
+ return cmp > 0 ? -1 : 1;
375
+ case '>=':
376
+ return cmp >= 0 ? -1 : 1;
377
+ case '=':
378
+ return cmp === 0 ? 0 : 1;
379
+ default:
380
+ return cmp;
381
+ }
382
+ }
383
+ return compareSemverPair(a, b);
384
+ }
385
+
386
+ module.exports = {
387
+ getLatestVersions,
388
+ compareVersions,
389
+ refreshLatestVersions,
390
+ // exported for tests
391
+ parseSemver,
392
+ compareSemverPair,
393
+ mavenCentralLatest,
394
+ npmRegistryLatest,
395
+ githubReleasesLatest,
396
+ getCachePath,
397
+ getFallbackPath,
398
+ };
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const DB_PATH = path.join(os.homedir(), '.claude', 'telemetry.db');
8
+
9
+ /**
10
+ * Open the telemetry SQLite database.
11
+ * Tries better-sqlite3 first; falls back to sql.js.
12
+ * Throws a descriptive error if both are unavailable.
13
+ *
14
+ * @param {string} [dbPath] - path to the SQLite file (defaults to ~/.claude/telemetry.db)
15
+ * @returns {object} database handle (better-sqlite3 or sql.js Database)
16
+ */
17
+ function openDb(dbPath) {
18
+ const target = dbPath || DB_PATH;
19
+
20
+ // Ensure parent directory exists
21
+ const dir = path.dirname(target);
22
+ if (!fs.existsSync(dir)) {
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ }
25
+
26
+ // Attempt better-sqlite3 (synchronous, native)
27
+ try {
28
+ const BetterSqlite3 = require('better-sqlite3');
29
+ const db = new BetterSqlite3(target);
30
+ db._isBetterSqlite3 = true;
31
+ return db;
32
+ } catch {
33
+ // fall through to sql.js
34
+ }
35
+
36
+ // Attempt sql.js (WASM, no native bindings needed)
37
+ try {
38
+ const initSqlJs = require('sql.js');
39
+ // sql.js is async but we simulate sync by using a wrapper approach.
40
+ // For the dashboard use case we return a promise-aware wrapper.
41
+ const wasmDb = openSqlJs(target, initSqlJs);
42
+ return wasmDb;
43
+ } catch {
44
+ // fall through
45
+ }
46
+
47
+ throw new Error(
48
+ 'Neither better-sqlite3 nor sql.js is available. ' +
49
+ 'Run: npm install --prefix <dashboard-dir> to install dependencies.'
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Open a sql.js database from file (returns a synchronous-looking wrapper).
55
+ * sql.js loads the WASM synchronously when initSqlJs is called with no fetch.
56
+ */
57
+ function openSqlJs(dbPath, initSqlJs) {
58
+ // initSqlJs can be the module itself (sql.js <= 1.6) or a factory fn (sql.js >= 1.8)
59
+ const SqlJs = typeof initSqlJs === 'function' ? initSqlJs({ wasmBinary: null }) : initSqlJs;
60
+
61
+ let fileBuffer = null;
62
+ if (fs.existsSync(dbPath)) {
63
+ fileBuffer = fs.readFileSync(dbPath);
64
+ }
65
+
66
+ const db = fileBuffer ? new SqlJs.Database(fileBuffer) : new SqlJs.Database();
67
+ db._isSqlJs = true;
68
+ db._path = dbPath;
69
+
70
+ // Patch db.close to persist
71
+ const origClose = db.close.bind(db);
72
+ db.close = () => {
73
+ const data = db.export();
74
+ fs.writeFileSync(dbPath, Buffer.from(data));
75
+ origClose();
76
+ };
77
+
78
+ return db;
79
+ }
80
+
81
+ /**
82
+ * Create the events table and indexes if they don't exist.
83
+ * Also adds missing columns to existing tables (migration-safe).
84
+ * Works with both better-sqlite3 and sql.js handles.
85
+ *
86
+ * @param {object} db
87
+ */
88
+ function initSchema(db) {
89
+ const ddl = `
90
+ CREATE TABLE IF NOT EXISTS events (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ project_path TEXT NOT NULL DEFAULT 'unknown',
93
+ project_name TEXT NOT NULL DEFAULT 'unknown',
94
+ model TEXT NOT NULL DEFAULT 'other',
95
+ input_tokens INTEGER NOT NULL DEFAULT 0,
96
+ output_tokens INTEGER NOT NULL DEFAULT 0,
97
+ subagent_type TEXT,
98
+ session_id TEXT,
99
+ agent_id TEXT,
100
+ agent_type TEXT,
101
+ ts INTEGER NOT NULL
102
+ );
103
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
104
+ CREATE INDEX IF NOT EXISTS idx_events_project ON events(project_path);
105
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
106
+ `;
107
+
108
+ // Idempotent migrations for DBs created by older writer versions.
109
+ const alters = [
110
+ 'ALTER TABLE events ADD COLUMN session_id TEXT',
111
+ 'ALTER TABLE events ADD COLUMN agent_id TEXT',
112
+ 'ALTER TABLE events ADD COLUMN agent_type TEXT',
113
+ ];
114
+
115
+ if (db._isBetterSqlite3) {
116
+ db.exec(ddl);
117
+ for (const stmt of alters) {
118
+ try { db.exec(stmt); } catch { /* column exists */ }
119
+ }
120
+ } else if (db._isSqlJs) {
121
+ db.run(ddl);
122
+ for (const stmt of alters) {
123
+ try { db.run(stmt); } catch { /* exists */ }
124
+ }
125
+ const data = db.export();
126
+ fs.writeFileSync(db._path, Buffer.from(data));
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Query projects active since `sinceTs` (Unix seconds).
132
+ * Groups by project_path, returns last seen + session count.
133
+ *
134
+ * @param {object} db
135
+ * @param {number} sinceTs - Unix timestamp in seconds
136
+ * @returns {Array<{name: string, path: string, last_seen: number, session_count: number}>}
137
+ */
138
+ function queryProjects(db, sinceTs) {
139
+ // session_count uses COUNT(DISTINCT session_id) so multiple SubagentStop
140
+ // events from the same session aggregate to 1. Events with NULL session_id
141
+ // (older rows or non-session contexts) fall back to being counted as one
142
+ // session per event via COALESCE on the row id.
143
+ const sql = `
144
+ SELECT
145
+ project_path AS path,
146
+ project_name AS name,
147
+ MAX(ts) AS last_seen,
148
+ COUNT(DISTINCT COALESCE(session_id, CAST(id AS TEXT))) AS session_count
149
+ FROM events
150
+ WHERE ts > ?
151
+ GROUP BY project_path
152
+ ORDER BY last_seen DESC
153
+ `;
154
+
155
+ if (db._isBetterSqlite3) {
156
+ return db.prepare(sql).all(sinceTs);
157
+ } else if (db._isSqlJs) {
158
+ const stmt = db.prepare(sql);
159
+ const rows = [];
160
+ stmt.bind([sinceTs]);
161
+ while (stmt.step()) {
162
+ const r = stmt.getAsObject();
163
+ rows.push(r);
164
+ }
165
+ stmt.free();
166
+ return rows;
167
+ }
168
+ return [];
169
+ }
170
+
171
+ /**
172
+ * Query model usage counts.
173
+ *
174
+ * @param {object} db
175
+ * @returns {Array<{model: string, count: number}>}
176
+ */
177
+ function queryModels(db) {
178
+ const sql = `
179
+ SELECT model, COUNT(*) AS count
180
+ FROM events
181
+ GROUP BY model
182
+ ORDER BY count DESC
183
+ `;
184
+
185
+ if (db._isBetterSqlite3) {
186
+ return db.prepare(sql).all();
187
+ } else if (db._isSqlJs) {
188
+ const stmt = db.prepare(sql);
189
+ const rows = [];
190
+ while (stmt.step()) {
191
+ rows.push(stmt.getAsObject());
192
+ }
193
+ stmt.free();
194
+ return rows;
195
+ }
196
+ return [];
197
+ }
198
+
199
+ module.exports = { openDb, initSchema, queryProjects, queryModels };
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "development-utility-kit-dashboard",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "server.js",
6
+ "dependencies": {
7
+ "better-sqlite3": "^12.0.0",
8
+ "express": "^4.18.0"
9
+ },
10
+ "optionalDependencies": {
11
+ "sql.js": "^1.10.0"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test test/sprint1.test.js"
18
+ },
19
+ "devDependencies": {
20
+ "supertest": "^7.2.2"
21
+ }
22
+ }