@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,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Move targetDir/.claude/ to targetDir/.claude.bak/ (overwrites prior backup).
|
|
8
|
+
* No-op if .claude/ does not exist.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: this is the legacy single-slot backup used by `duk install`. The
|
|
11
|
+
* timestamped backup folders described in ADR-032 are still future work.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} targetDir
|
|
14
|
+
* @param {boolean} dryRun
|
|
15
|
+
*/
|
|
16
|
+
function backupClaudeDir(targetDir, dryRun) {
|
|
17
|
+
const src = path.join(targetDir, '.claude');
|
|
18
|
+
const dest = path.join(targetDir, '.claude.bak');
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(src)) return;
|
|
21
|
+
|
|
22
|
+
if (dryRun) {
|
|
23
|
+
process.stdout.write(' [dry-run] would backup .claude/ -> .claude.bak/\n');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(dest)) {
|
|
28
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
fs.renameSync(src, dest);
|
|
31
|
+
process.stdout.write('Note: existing .claude/ backed up to .claude.bak/ (previous backup replaced)\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively copy src directory into dest.
|
|
36
|
+
* Uses fs.cpSync when available (Node 16.7+), falls back to manual walk.
|
|
37
|
+
* @param {string} src
|
|
38
|
+
* @param {string} dest
|
|
39
|
+
*/
|
|
40
|
+
function copyDir(src, dest) {
|
|
41
|
+
if (typeof fs.cpSync === 'function') {
|
|
42
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
47
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const srcPath = path.join(src, entry.name);
|
|
50
|
+
const destPath = path.join(dest, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
copyDir(srcPath, destPath);
|
|
53
|
+
} else {
|
|
54
|
+
fs.copyFileSync(srcPath, destPath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
backupClaudeDir,
|
|
61
|
+
copyDir,
|
|
62
|
+
};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* detect-stack.js — Auto-detect project stack from filesystem signals.
|
|
5
|
+
*
|
|
6
|
+
* Reads pom.xml, package.json, pyproject.toml, requirements.txt, go.mod,
|
|
7
|
+
* and docker-compose.yml from root + 1 level of subdirs. Returns a
|
|
8
|
+
* normalized stack descriptor. Zero deps; pure fs + regex + JSON.parse.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{
|
|
16
|
+
* type: 'backend'|'frontend'|'fullstack'|'mobile'|'library'|'cli'|'unknown',
|
|
17
|
+
* language: 'java'|'typescript'|'javascript'|'python'|'go'|'node'|'unknown',
|
|
18
|
+
* framework: string,
|
|
19
|
+
* version: string|null,
|
|
20
|
+
* java_version: string|null,
|
|
21
|
+
* node_version: string|null,
|
|
22
|
+
* python_version: string|null,
|
|
23
|
+
* go_version: string|null,
|
|
24
|
+
* database: string[],
|
|
25
|
+
* ui_lib: string|null,
|
|
26
|
+
* detected_from: string[]
|
|
27
|
+
* }} StackDescriptor
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a file as utf8 safely; returns null on any error.
|
|
32
|
+
* @param {string} filePath
|
|
33
|
+
* @returns {string|null}
|
|
34
|
+
*/
|
|
35
|
+
function readFileSafe(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a file as JSON safely.
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @returns {object|null}
|
|
47
|
+
*/
|
|
48
|
+
function readJsonSafe(filePath) {
|
|
49
|
+
const raw = readFileSafe(filePath);
|
|
50
|
+
if (raw === null) return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract a tag value from a snippet of XML using a permissive regex.
|
|
60
|
+
* Returns null if not found.
|
|
61
|
+
* @param {string} xml
|
|
62
|
+
* @param {string} tag
|
|
63
|
+
* @returns {string|null}
|
|
64
|
+
*/
|
|
65
|
+
function extractXmlTag(xml, tag) {
|
|
66
|
+
const re = new RegExp(`<${tag}>\\s*([^<\\s]+)\\s*</${tag}>`, 'i');
|
|
67
|
+
const m = xml.match(re);
|
|
68
|
+
return m ? m[1].trim() : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detect Spring Boot version + Java version from a pom.xml string.
|
|
73
|
+
* @param {string} xml
|
|
74
|
+
* @returns {{ framework: string, version: string|null, java_version: string|null }|null}
|
|
75
|
+
*/
|
|
76
|
+
function parsePom(xml) {
|
|
77
|
+
if (!xml) return null;
|
|
78
|
+
// Java version (multiple common property names)
|
|
79
|
+
const javaVersion =
|
|
80
|
+
extractXmlTag(xml, 'java\\.version') ||
|
|
81
|
+
extractXmlTag(xml, 'maven\\.compiler\\.release') ||
|
|
82
|
+
extractXmlTag(xml, 'maven\\.compiler\\.source') ||
|
|
83
|
+
null;
|
|
84
|
+
|
|
85
|
+
// Spring Boot detection: <parent> with spring-boot-starter-parent OR explicit dependency
|
|
86
|
+
const isSpringBoot =
|
|
87
|
+
/<artifactId>\s*spring-boot-starter-parent\s*<\/artifactId>/i.test(xml) ||
|
|
88
|
+
/<artifactId>\s*spring-boot-dependencies\s*<\/artifactId>/i.test(xml) ||
|
|
89
|
+
/org\.springframework\.boot/i.test(xml);
|
|
90
|
+
|
|
91
|
+
let version = null;
|
|
92
|
+
if (isSpringBoot) {
|
|
93
|
+
// Try parent block first (most reliable)
|
|
94
|
+
const parentMatch = xml.match(
|
|
95
|
+
/<parent>[\s\S]*?<artifactId>\s*spring-boot-starter-parent\s*<\/artifactId>[\s\S]*?<version>\s*([^<\s]+)\s*<\/version>[\s\S]*?<\/parent>/i
|
|
96
|
+
);
|
|
97
|
+
if (parentMatch) {
|
|
98
|
+
version = parentMatch[1].trim();
|
|
99
|
+
} else {
|
|
100
|
+
version = extractXmlTag(xml, 'spring-boot\\.version') || null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
framework: isSpringBoot ? 'spring-boot' : 'java',
|
|
106
|
+
version,
|
|
107
|
+
java_version: javaVersion,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detect language/framework from a package.json object.
|
|
113
|
+
* @param {object} pkg
|
|
114
|
+
* @returns {{
|
|
115
|
+
* language: string, framework: string, version: string|null,
|
|
116
|
+
* ui_lib: string|null, isMobile: boolean
|
|
117
|
+
* }|null}
|
|
118
|
+
*/
|
|
119
|
+
function parsePackageJson(pkg) {
|
|
120
|
+
if (!pkg || typeof pkg !== 'object') return null;
|
|
121
|
+
const deps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
122
|
+
|
|
123
|
+
let framework = 'unknown';
|
|
124
|
+
let version = null;
|
|
125
|
+
let ui_lib = null;
|
|
126
|
+
let isMobile = false;
|
|
127
|
+
let language = 'javascript';
|
|
128
|
+
|
|
129
|
+
if (deps['typescript']) language = 'typescript';
|
|
130
|
+
|
|
131
|
+
if (deps['react-native']) {
|
|
132
|
+
framework = 'react-native';
|
|
133
|
+
version = stripSemverPrefix(deps['react-native']);
|
|
134
|
+
isMobile = true;
|
|
135
|
+
} else if (deps['@angular/core']) {
|
|
136
|
+
framework = 'angular';
|
|
137
|
+
version = stripSemverPrefix(deps['@angular/core']);
|
|
138
|
+
if (deps['@ng-bootstrap/ng-bootstrap']) ui_lib = 'ng-bootstrap';
|
|
139
|
+
else if (deps['@angular/material']) ui_lib = 'material';
|
|
140
|
+
else if (deps['tailwindcss']) ui_lib = 'tailwind';
|
|
141
|
+
} else if (deps['@nestjs/core']) {
|
|
142
|
+
framework = 'nest';
|
|
143
|
+
version = stripSemverPrefix(deps['@nestjs/core']);
|
|
144
|
+
} else if (deps['next']) {
|
|
145
|
+
framework = 'next';
|
|
146
|
+
version = stripSemverPrefix(deps['next']);
|
|
147
|
+
if (deps['tailwindcss']) ui_lib = 'tailwind';
|
|
148
|
+
} else if (deps['nuxt'] || deps['nuxt3']) {
|
|
149
|
+
framework = 'nuxt';
|
|
150
|
+
version = stripSemverPrefix(deps['nuxt'] || deps['nuxt3']);
|
|
151
|
+
} else if (deps['vue']) {
|
|
152
|
+
framework = 'vue';
|
|
153
|
+
version = stripSemverPrefix(deps['vue']);
|
|
154
|
+
} else if (deps['react']) {
|
|
155
|
+
framework = 'react';
|
|
156
|
+
version = stripSemverPrefix(deps['react']);
|
|
157
|
+
if (deps['@mui/material']) ui_lib = 'material';
|
|
158
|
+
else if (deps['tailwindcss']) ui_lib = 'tailwind';
|
|
159
|
+
} else if (deps['express'] || deps['fastify'] || deps['koa']) {
|
|
160
|
+
framework = deps['fastify'] ? 'fastify' : deps['koa'] ? 'koa' : 'express';
|
|
161
|
+
version = stripSemverPrefix(deps[framework]);
|
|
162
|
+
language = deps['typescript'] ? 'typescript' : 'node';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Node engine pin if present
|
|
166
|
+
return { language, framework, version, ui_lib, isMobile };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Remove leading ^, ~, >=, < from a semver range to extract a usable version.
|
|
171
|
+
* @param {string} s
|
|
172
|
+
* @returns {string|null}
|
|
173
|
+
*/
|
|
174
|
+
function stripSemverPrefix(s) {
|
|
175
|
+
if (!s || typeof s !== 'string') return null;
|
|
176
|
+
const m = s.match(/(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
177
|
+
return m ? m[1] : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detect Python framework from pyproject.toml or requirements.txt text.
|
|
182
|
+
* @param {string} text
|
|
183
|
+
* @returns {{ framework: string, version: string|null, python_version: string|null }|null}
|
|
184
|
+
*/
|
|
185
|
+
function parsePythonDeps(text) {
|
|
186
|
+
if (!text) return null;
|
|
187
|
+
const lower = text.toLowerCase();
|
|
188
|
+
|
|
189
|
+
// python_requires in pyproject ([project] requires-python = ">=3.12")
|
|
190
|
+
let python_version = null;
|
|
191
|
+
const pyReq = text.match(/requires-python\s*=\s*"([^"]+)"/i);
|
|
192
|
+
if (pyReq) {
|
|
193
|
+
const m = pyReq[1].match(/(\d+\.\d+)/);
|
|
194
|
+
python_version = m ? m[1] : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let framework = 'unknown';
|
|
198
|
+
let version = null;
|
|
199
|
+
|
|
200
|
+
const findPkg = (name) => {
|
|
201
|
+
// requirements.txt style: "django==5.1.0" or "django>=5"
|
|
202
|
+
// pyproject style: '"django>=5.1"' or 'django = "5.1"'
|
|
203
|
+
const re = new RegExp(`["\\s]${name}[\\s=<>~!]+["\\s]*([\\d.]+)`, 'i');
|
|
204
|
+
const m = text.match(re);
|
|
205
|
+
return m ? m[1] : null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (lower.includes('django')) {
|
|
209
|
+
framework = 'django';
|
|
210
|
+
version = findPkg('django');
|
|
211
|
+
} else if (lower.includes('fastapi')) {
|
|
212
|
+
framework = 'fastapi';
|
|
213
|
+
version = findPkg('fastapi');
|
|
214
|
+
} else if (lower.includes('flask')) {
|
|
215
|
+
framework = 'flask';
|
|
216
|
+
version = findPkg('flask');
|
|
217
|
+
} else if (lower.match(/^\s*pyramid\s*[=<>~!]/im)) {
|
|
218
|
+
framework = 'pyramid';
|
|
219
|
+
version = findPkg('pyramid');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { framework, version, python_version };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Detect Go framework + version from go.mod text.
|
|
227
|
+
* @param {string} text
|
|
228
|
+
* @returns {{ framework: string, version: string|null, go_version: string|null }|null}
|
|
229
|
+
*/
|
|
230
|
+
function parseGoMod(text) {
|
|
231
|
+
if (!text) return null;
|
|
232
|
+
|
|
233
|
+
let go_version = null;
|
|
234
|
+
const goLine = text.match(/^\s*go\s+(\d+\.\d+(?:\.\d+)?)/m);
|
|
235
|
+
if (goLine) go_version = goLine[1];
|
|
236
|
+
|
|
237
|
+
let framework = 'unknown';
|
|
238
|
+
let version = null;
|
|
239
|
+
|
|
240
|
+
const requirePkg = (pkgPart) => {
|
|
241
|
+
const re = new RegExp(
|
|
242
|
+
`${pkgPart.replace(/[/.]/g, '\\$&')}[^\\n]*\\s+v([\\d.]+)`,
|
|
243
|
+
'i'
|
|
244
|
+
);
|
|
245
|
+
const m = text.match(re);
|
|
246
|
+
return m ? m[1] : null;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (/github\.com\/gin-gonic\/gin/.test(text)) {
|
|
250
|
+
framework = 'gin';
|
|
251
|
+
version = requirePkg('github.com/gin-gonic/gin');
|
|
252
|
+
} else if (/github\.com\/labstack\/echo/.test(text)) {
|
|
253
|
+
framework = 'echo';
|
|
254
|
+
version = requirePkg('github.com/labstack/echo');
|
|
255
|
+
} else if (/github\.com\/gofiber\/fiber/.test(text)) {
|
|
256
|
+
framework = 'fiber';
|
|
257
|
+
version = requirePkg('github.com/gofiber/fiber');
|
|
258
|
+
} else if (/google\.golang\.org\/grpc/.test(text)) {
|
|
259
|
+
framework = 'grpc';
|
|
260
|
+
version = requirePkg('google.golang.org/grpc');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { framework, version, go_version };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Detect databases referenced in a docker-compose YAML text.
|
|
268
|
+
* @param {string} yaml
|
|
269
|
+
* @returns {string[]}
|
|
270
|
+
*/
|
|
271
|
+
function parseDockerCompose(yaml) {
|
|
272
|
+
if (!yaml) return [];
|
|
273
|
+
const dbs = [];
|
|
274
|
+
const lower = yaml.toLowerCase();
|
|
275
|
+
if (/\bpostgres(ql)?\b/.test(lower) || /image:\s*postgres/i.test(yaml)) dbs.push('postgres');
|
|
276
|
+
if (/\bmysql\b/.test(lower) || /image:\s*mysql/i.test(yaml)) dbs.push('mysql');
|
|
277
|
+
if (/\bmariadb\b/.test(lower)) dbs.push('mariadb');
|
|
278
|
+
if (/\bmongo(db)?\b/.test(lower) || /image:\s*mongo/i.test(yaml)) dbs.push('mongodb');
|
|
279
|
+
if (/\bredis\b/.test(lower) || /image:\s*redis/i.test(yaml)) dbs.push('redis');
|
|
280
|
+
if (/\bcockroach(db)?\b/.test(lower)) dbs.push('cockroachdb');
|
|
281
|
+
if (/\belasticsearch\b/.test(lower)) dbs.push('elasticsearch');
|
|
282
|
+
if (/\bkafka\b/.test(lower)) dbs.push('kafka');
|
|
283
|
+
return Array.from(new Set(dbs));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* List files at root + 1 level deep (skip dot-dirs, node_modules, dist, build).
|
|
288
|
+
* @param {string} cwd
|
|
289
|
+
* @returns {string[]} absolute paths
|
|
290
|
+
*/
|
|
291
|
+
function listFilesShallow(cwd) {
|
|
292
|
+
const out = [];
|
|
293
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'target', 'out', '.idea', '.vscode']);
|
|
294
|
+
|
|
295
|
+
let rootEntries;
|
|
296
|
+
try {
|
|
297
|
+
rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
298
|
+
} catch {
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const ent of rootEntries) {
|
|
303
|
+
const full = path.join(cwd, ent.name);
|
|
304
|
+
if (ent.isFile()) {
|
|
305
|
+
out.push(full);
|
|
306
|
+
} else if (ent.isDirectory() && !ent.name.startsWith('.') && !SKIP_DIRS.has(ent.name)) {
|
|
307
|
+
try {
|
|
308
|
+
const subEntries = fs.readdirSync(full, { withFileTypes: true });
|
|
309
|
+
for (const sub of subEntries) {
|
|
310
|
+
if (sub.isFile()) out.push(path.join(full, sub.name));
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// ignore
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Detect the project stack from a directory.
|
|
323
|
+
* @param {string} cwd
|
|
324
|
+
* @returns {StackDescriptor}
|
|
325
|
+
*/
|
|
326
|
+
function detectStack(cwd) {
|
|
327
|
+
/** @type {StackDescriptor} */
|
|
328
|
+
const result = {
|
|
329
|
+
type: 'unknown',
|
|
330
|
+
language: 'unknown',
|
|
331
|
+
framework: 'unknown',
|
|
332
|
+
version: null,
|
|
333
|
+
java_version: null,
|
|
334
|
+
node_version: null,
|
|
335
|
+
python_version: null,
|
|
336
|
+
go_version: null,
|
|
337
|
+
database: [],
|
|
338
|
+
ui_lib: null,
|
|
339
|
+
detected_from: [],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const files = listFilesShallow(cwd);
|
|
343
|
+
const basenames = new Map(files.map((f) => [path.basename(f).toLowerCase(), f]));
|
|
344
|
+
|
|
345
|
+
let hasBackend = false;
|
|
346
|
+
let hasFrontend = false;
|
|
347
|
+
let hasMobile = false;
|
|
348
|
+
|
|
349
|
+
// Java / Spring Boot
|
|
350
|
+
for (const f of files) {
|
|
351
|
+
if (path.basename(f).toLowerCase() === 'pom.xml') {
|
|
352
|
+
const xml = readFileSafe(f);
|
|
353
|
+
const parsed = parsePom(xml);
|
|
354
|
+
if (parsed) {
|
|
355
|
+
result.language = 'java';
|
|
356
|
+
result.framework = parsed.framework;
|
|
357
|
+
result.version = parsed.version;
|
|
358
|
+
result.java_version = parsed.java_version;
|
|
359
|
+
result.detected_from.push(path.relative(cwd, f) || 'pom.xml');
|
|
360
|
+
hasBackend = true;
|
|
361
|
+
break; // first pom wins
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// package.json (can coexist with pom.xml → fullstack)
|
|
367
|
+
for (const f of files) {
|
|
368
|
+
if (path.basename(f).toLowerCase() === 'package.json') {
|
|
369
|
+
const pkg = readJsonSafe(f);
|
|
370
|
+
const parsed = parsePackageJson(pkg);
|
|
371
|
+
if (parsed && parsed.framework !== 'unknown') {
|
|
372
|
+
// Don't overwrite Java-detected language; record JS/TS separately
|
|
373
|
+
if (result.language === 'unknown' || result.language === 'java') {
|
|
374
|
+
// first non-java package.json takes the language slot if java not set
|
|
375
|
+
if (result.language === 'unknown') result.language = parsed.language;
|
|
376
|
+
}
|
|
377
|
+
// If we don't have a backend framework yet, this becomes primary
|
|
378
|
+
if (result.framework === 'unknown') {
|
|
379
|
+
result.framework = parsed.framework;
|
|
380
|
+
result.version = parsed.version;
|
|
381
|
+
result.ui_lib = parsed.ui_lib;
|
|
382
|
+
} else if (result.ui_lib === null && parsed.ui_lib) {
|
|
383
|
+
result.ui_lib = parsed.ui_lib;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// engines.node
|
|
387
|
+
if (pkg && pkg.engines && pkg.engines.node) {
|
|
388
|
+
result.node_version = stripSemverPrefix(pkg.engines.node);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
result.detected_from.push(path.relative(cwd, f) || 'package.json');
|
|
392
|
+
|
|
393
|
+
if (parsed.isMobile) hasMobile = true;
|
|
394
|
+
else if (['angular', 'react', 'vue', 'next', 'nuxt'].includes(parsed.framework)) hasFrontend = true;
|
|
395
|
+
else if (['nest', 'express', 'fastify', 'koa'].includes(parsed.framework)) hasBackend = true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Python
|
|
401
|
+
for (const f of files) {
|
|
402
|
+
const base = path.basename(f).toLowerCase();
|
|
403
|
+
if (base === 'pyproject.toml' || base === 'requirements.txt' || base === 'setup.py') {
|
|
404
|
+
const text = readFileSafe(f);
|
|
405
|
+
const parsed = parsePythonDeps(text);
|
|
406
|
+
if (parsed) {
|
|
407
|
+
if (result.language === 'unknown') result.language = 'python';
|
|
408
|
+
if (result.framework === 'unknown' && parsed.framework !== 'unknown') {
|
|
409
|
+
result.framework = parsed.framework;
|
|
410
|
+
result.version = parsed.version;
|
|
411
|
+
}
|
|
412
|
+
if (parsed.python_version && !result.python_version) {
|
|
413
|
+
result.python_version = parsed.python_version;
|
|
414
|
+
}
|
|
415
|
+
result.detected_from.push(path.relative(cwd, f) || base);
|
|
416
|
+
hasBackend = true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Go
|
|
422
|
+
if (basenames.has('go.mod')) {
|
|
423
|
+
const goModPath = basenames.get('go.mod');
|
|
424
|
+
const text = readFileSafe(goModPath);
|
|
425
|
+
const parsed = parseGoMod(text);
|
|
426
|
+
if (parsed) {
|
|
427
|
+
if (result.language === 'unknown') result.language = 'go';
|
|
428
|
+
if (result.framework === 'unknown') {
|
|
429
|
+
result.framework = parsed.framework;
|
|
430
|
+
result.version = parsed.version;
|
|
431
|
+
}
|
|
432
|
+
result.go_version = parsed.go_version;
|
|
433
|
+
result.detected_from.push(path.relative(cwd, goModPath) || 'go.mod');
|
|
434
|
+
hasBackend = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// docker-compose for DB detection
|
|
439
|
+
for (const f of files) {
|
|
440
|
+
if (/^docker-compose.*\.ya?ml$/i.test(path.basename(f))) {
|
|
441
|
+
const yaml = readFileSafe(f);
|
|
442
|
+
const dbs = parseDockerCompose(yaml);
|
|
443
|
+
if (dbs.length > 0) {
|
|
444
|
+
result.database = Array.from(new Set([...result.database, ...dbs]));
|
|
445
|
+
result.detected_from.push(path.relative(cwd, f) || path.basename(f));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Compute type
|
|
451
|
+
if (hasMobile && !hasBackend && !hasFrontend) {
|
|
452
|
+
result.type = 'mobile';
|
|
453
|
+
} else if (hasBackend && hasFrontend) {
|
|
454
|
+
result.type = 'fullstack';
|
|
455
|
+
} else if (hasBackend) {
|
|
456
|
+
result.type = 'backend';
|
|
457
|
+
} else if (hasFrontend) {
|
|
458
|
+
result.type = 'frontend';
|
|
459
|
+
} else if (result.framework !== 'unknown' || result.language !== 'unknown') {
|
|
460
|
+
// Detected something but couldn't categorize → library / cli fallback
|
|
461
|
+
result.type = 'library';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
module.exports = {
|
|
468
|
+
detectStack,
|
|
469
|
+
// Exported for unit-test surface
|
|
470
|
+
parsePom,
|
|
471
|
+
parsePackageJson,
|
|
472
|
+
parsePythonDeps,
|
|
473
|
+
parseGoMod,
|
|
474
|
+
parseDockerCompose,
|
|
475
|
+
stripSemverPrefix,
|
|
476
|
+
};
|