@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.
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +44 -0
- package/.claude/agents/stack-resolver.md +84 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/settings.json +44 -0
- package/.claude/skills/README.md +332 -0
- package/.claude/skills/active-project/SKILL.md +129 -0
- package/.claude/skills/api-integration-test/SKILL.md +64 -0
- package/.claude/skills/auto-test-guard/SKILL.md +237 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +60 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +187 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +79 -0
- package/.claude/skills/honcho-memory/SKILL.md +207 -0
- package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
- package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
- package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
- package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
- package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
- package/.claude/skills/honcho-memory/package.json +32 -0
- package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
- package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
- package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
- package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
- package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
- package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
- package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
- package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
- package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
- package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
- package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
- package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
- package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
- package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
- package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +58 -0
- package/.claude/skills/project-manager/SKILL.md +167 -0
- package/.claude/skills/quality-standards/SKILL.md +201 -0
- package/.claude/skills/quick-feature/SKILL.md +264 -0
- package/.claude/skills/run-sprint/SKILL.md +342 -0
- package/.claude/skills/scaffold/SKILL.md +58 -0
- package/.claude/skills/stack-discovery/SKILL.md +159 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +254 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +88 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +453 -0
- package/README.md +391 -0
- package/bin/cli.js +773 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/help.js +233 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/local-dir.js +69 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +199 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +709 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
- package/dashboard/public/content/docs/pipeline.en.md +400 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +500 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
- package/dashboard/public/content/manifest.json +102 -0
- package/dashboard/public/content/manual/backend.en.md +1138 -0
- package/dashboard/public/content/manual/existing-project.en.md +831 -0
- package/dashboard/public/content/manual/frontend.en.md +1065 -0
- package/dashboard/public/content/manual/fullstack.en.md +1508 -0
- package/dashboard/public/content/manual/mobile.en.md +866 -0
- package/dashboard/public/index.html +108 -0
- package/dashboard/public/style.css +610 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +403 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- 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
|
+
};
|
package/dashboard/db.js
ADDED
|
@@ -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
|
+
}
|