@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,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
+ };