claudeos-core 2.1.1 → 2.3.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/CHANGELOG.md +1649 -481
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +64 -5
- package/README.es.md +64 -5
- package/README.fr.md +64 -5
- package/README.hi.md +64 -5
- package/README.ja.md +64 -5
- package/README.ko.md +1018 -959
- package/README.md +1020 -960
- package/README.ru.md +66 -5
- package/README.vi.md +1019 -960
- package/README.zh-CN.md +64 -5
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1518
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -436
- package/lib/env-parser.js +317 -0
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1052
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/angular/pass3.md +28 -13
- package/pass-prompts/templates/common/claude-md-scaffold.md +686 -0
- package/pass-prompts/templates/common/pass3-footer.md +402 -39
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -302
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/java-spring/pass3.md +31 -21
- package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
- package/pass-prompts/templates/node-express/pass3.md +30 -21
- package/pass-prompts/templates/node-fastify/pass3.md +28 -14
- package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
- package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/node-vite/pass3.md +30 -13
- package/pass-prompts/templates/python-django/pass3.md +32 -21
- package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/pass-prompts/templates/python-flask/pass3.md +31 -13
- package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -129
- package/plan-installer/prompt-generator.js +188 -128
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -466
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
|
@@ -1,466 +1,482 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Stack Detector
|
|
3
|
-
*
|
|
4
|
-
* Detects project language, framework, build tool, database, ORM, and frontend.
|
|
5
|
-
* Supports: Java, Kotlin, TypeScript/JavaScript, Python
|
|
6
|
-
* Multi-stack aware (backend + frontend simultaneous detection).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const { glob } = require("glob");
|
|
11
|
-
const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
["
|
|
19
|
-
["
|
|
20
|
-
["
|
|
21
|
-
["
|
|
22
|
-
["
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
["
|
|
28
|
-
["
|
|
29
|
-
["
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[["
|
|
35
|
-
[["
|
|
36
|
-
[["
|
|
37
|
-
[["
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
["
|
|
44
|
-
["
|
|
45
|
-
["
|
|
46
|
-
["
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
stack
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
stack.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
stack.
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
* @
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/
|
|
109
|
-
/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// "
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/
|
|
129
|
-
/kotlin\
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
else if (!stack.orm &&
|
|
157
|
-
if (!stack.
|
|
158
|
-
if (!stack.database && vc.includes("
|
|
159
|
-
if (!stack.database && vc.includes("
|
|
160
|
-
if (!stack.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
stack.
|
|
205
|
-
stack.
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (!stack.database && pom.includes("
|
|
229
|
-
if (!stack.database && pom.includes("
|
|
230
|
-
if (!stack.database && pom.includes("
|
|
231
|
-
if (!stack.database &&
|
|
232
|
-
if (!stack.database &&
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
else if (existsSafe(path.join(ROOT, "
|
|
247
|
-
else if (
|
|
248
|
-
if (stack.monorepo)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
["
|
|
292
|
-
["
|
|
293
|
-
["
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
stack.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
["
|
|
307
|
-
["
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
stack.
|
|
321
|
-
stack.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
stack.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (pp.includes("
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (r.includes("
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (!
|
|
402
|
-
if (!stack.database && c.includes("
|
|
403
|
-
if (!stack.database && c.includes("
|
|
404
|
-
if (!stack.database && c.includes("
|
|
405
|
-
if (!stack.database &&
|
|
406
|
-
if (!stack.database &&
|
|
407
|
-
if (!stack.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (ec.includes("
|
|
422
|
-
if (ec.includes("
|
|
423
|
-
if (ec.includes("
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
[["
|
|
446
|
-
[["
|
|
447
|
-
[["
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
stack.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Stack Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects project language, framework, build tool, database, ORM, and frontend.
|
|
5
|
+
* Supports: Java, Kotlin, TypeScript/JavaScript, Python
|
|
6
|
+
* Multi-stack aware (backend + frontend simultaneous detection).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { glob } = require("glob");
|
|
11
|
+
const { readFileSafe, readJsonSafe, existsSafe } = require("../lib/safe-fs");
|
|
12
|
+
const { readStackEnvInfo } = require("../lib/env-parser");
|
|
13
|
+
|
|
14
|
+
// ─── Lookup tables ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
// ORM detection rules: [keyword, ormName] (order = priority, first match wins)
|
|
17
|
+
const GRADLE_ORM_RULES = [
|
|
18
|
+
["mybatis", "mybatis"],
|
|
19
|
+
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
20
|
+
["exposed", "exposed"],
|
|
21
|
+
["jooq", "jooq"],
|
|
22
|
+
["spring-data-jdbc", "spring-data-jdbc"],
|
|
23
|
+
["r2dbc", "r2dbc"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const MAVEN_ORM_RULES = [
|
|
27
|
+
["mybatis", "mybatis"],
|
|
28
|
+
["jpa", "jpa"], ["hibernate", "jpa"],
|
|
29
|
+
["exposed", "exposed"],
|
|
30
|
+
["jooq", "jooq"],
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const NODE_ORM_RULES = [
|
|
34
|
+
[["@prisma/client", "prisma"], "prisma"],
|
|
35
|
+
[["typeorm"], "typeorm"],
|
|
36
|
+
[["sequelize"], "sequelize"],
|
|
37
|
+
[["drizzle-orm"], "drizzle"],
|
|
38
|
+
[["knex"], "knex"],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// DB detection rules: [keyword, dbName]
|
|
42
|
+
const DB_KEYWORD_RULES = [
|
|
43
|
+
["postgresql", "postgresql"], ["postgres", "postgresql"],
|
|
44
|
+
["mysql", "mysql"],
|
|
45
|
+
["oracle", "oracle"],
|
|
46
|
+
["mongodb", "mongodb"],
|
|
47
|
+
["sqlite", "sqlite"],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// h2 needs word-boundary check (avoid oauth2, cache2k false positives)
|
|
51
|
+
const H2_REGEX = /\bh2\b/;
|
|
52
|
+
|
|
53
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function detectFirst(stack, field, content, rules) {
|
|
56
|
+
if (stack[field]) return;
|
|
57
|
+
for (const [keyword, value] of rules) {
|
|
58
|
+
if (content.includes(keyword)) {
|
|
59
|
+
stack[field] = value;
|
|
60
|
+
stack.detected.push(value);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectDb(stack, content, rules) {
|
|
67
|
+
if (stack.database) return;
|
|
68
|
+
for (const [keyword, value] of rules) {
|
|
69
|
+
if (content.includes(keyword)) {
|
|
70
|
+
stack.database = value;
|
|
71
|
+
stack.detected.push(value);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// h2 with word boundary
|
|
76
|
+
if (!stack.database && H2_REGEX.test(content)) {
|
|
77
|
+
stack.database = "h2";
|
|
78
|
+
stack.detected.push("h2");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Detect the project's technology stack.
|
|
85
|
+
* @param {string} ROOT - project root path
|
|
86
|
+
* @returns {Promise<object>} stack info
|
|
87
|
+
*/
|
|
88
|
+
async function detectStack(ROOT) {
|
|
89
|
+
const stack = {
|
|
90
|
+
language: null, languageVersion: null,
|
|
91
|
+
framework: null, frameworkVersion: null,
|
|
92
|
+
buildTool: null, database: null, orm: null,
|
|
93
|
+
frontend: null, frontendVersion: null,
|
|
94
|
+
packageManager: null, monorepo: null, workspaces: null,
|
|
95
|
+
detected: [],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ── Java/Kotlin: Gradle ──
|
|
99
|
+
const gradleFile = existsSafe(path.join(ROOT, "build.gradle.kts"))
|
|
100
|
+
? "build.gradle.kts"
|
|
101
|
+
: existsSafe(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
|
|
102
|
+
if (gradleFile) {
|
|
103
|
+
const g = readFileSafe(path.join(ROOT, gradleFile));
|
|
104
|
+
if (g) {
|
|
105
|
+
stack.buildTool = "gradle"; stack.detected.push(gradleFile);
|
|
106
|
+
if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
107
|
+
const svPatterns = [
|
|
108
|
+
/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
|
|
109
|
+
/id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
|
|
110
|
+
/spring-boot-dependencies:([^'")\s]+)/,
|
|
111
|
+
];
|
|
112
|
+
for (const pattern of svPatterns) {
|
|
113
|
+
const sv = g.match(pattern);
|
|
114
|
+
if (sv) { stack.frameworkVersion = sv[1]; break; }
|
|
115
|
+
}
|
|
116
|
+
const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
|
|
117
|
+
if (jv) stack.languageVersion = jv[1];
|
|
118
|
+
|
|
119
|
+
detectFirst(stack, "orm", g, GRADLE_ORM_RULES);
|
|
120
|
+
// Exclude "postgres" (substring of postgresql — false positive on r2dbc-postgres) and "sqlite" (rare in Gradle deps)
|
|
121
|
+
// "postgresql" is still matched via DB_KEYWORD_RULES; h2 uses word-boundary check separately
|
|
122
|
+
detectDb(stack, g, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
123
|
+
|
|
124
|
+
// Kotlin detection: override language if Kotlin plugin found
|
|
125
|
+
if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
|
|
126
|
+
stack.language = "kotlin"; stack.detected.push("kotlin");
|
|
127
|
+
const kvPatterns = [
|
|
128
|
+
/kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
129
|
+
/org\.jetbrains\.kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
|
|
130
|
+
/kotlin\("jvm"\)\s*version\s*["']([^"']+)["']/,
|
|
131
|
+
];
|
|
132
|
+
for (const pattern of kvPatterns) {
|
|
133
|
+
const match = g.match(pattern);
|
|
134
|
+
if (match) { stack.languageVersion = match[1]; break; }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Gradle: version catalogs (libs.versions.toml) ──
|
|
141
|
+
const versionCatalog = path.join(ROOT, "gradle/libs.versions.toml");
|
|
142
|
+
if (existsSafe(versionCatalog)) {
|
|
143
|
+
const vc = readFileSafe(versionCatalog);
|
|
144
|
+
if (vc) {
|
|
145
|
+
stack.detected.push("libs.versions.toml");
|
|
146
|
+
if (!stack.languageVersion) {
|
|
147
|
+
const kvMatch = vc.match(/kotlin\s*=\s*["']([^"']+)["']/);
|
|
148
|
+
if (kvMatch) stack.languageVersion = kvMatch[1];
|
|
149
|
+
}
|
|
150
|
+
if (!stack.frameworkVersion) {
|
|
151
|
+
const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
|
|
152
|
+
if (sbMatch) stack.frameworkVersion = sbMatch[1];
|
|
153
|
+
}
|
|
154
|
+
// Version catalog ORM (labels include " (catalog)" suffix)
|
|
155
|
+
if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
|
|
156
|
+
else if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
|
|
157
|
+
else if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
|
|
158
|
+
if (!stack.database && vc.includes("postgresql")) { stack.database = "postgresql"; }
|
|
159
|
+
if (!stack.database && vc.includes("mysql")) { stack.database = "mysql"; }
|
|
160
|
+
if (!stack.database && vc.includes("mongodb")) { stack.database = "mongodb"; }
|
|
161
|
+
if (!stack.language && vc.includes("kotlin")) {
|
|
162
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Kotlin: multi-module Gradle detection ──
|
|
168
|
+
if (stack.language !== "kotlin" && stack.buildTool === "gradle") {
|
|
169
|
+
const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**"] });
|
|
170
|
+
for (const sbf of subBuildFiles.slice(0, 5)) {
|
|
171
|
+
const sc = readFileSafe(path.join(ROOT, sbf));
|
|
172
|
+
if (sc && (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin"))) {
|
|
173
|
+
stack.language = "kotlin"; stack.detected.push("kotlin (submodule)");
|
|
174
|
+
const kv = sc.match(/kotlin\S*\s*version\s*['"]([^'"]+)['"]/);
|
|
175
|
+
if (kv) stack.languageVersion = kv[1];
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Kotlin: detect CQRS/multi-module from settings.gradle ──
|
|
182
|
+
const settingsFile = existsSafe(path.join(ROOT, "settings.gradle.kts"))
|
|
183
|
+
? "settings.gradle.kts"
|
|
184
|
+
: existsSafe(path.join(ROOT, "settings.gradle")) ? "settings.gradle" : null;
|
|
185
|
+
if (settingsFile && stack.language === "kotlin") {
|
|
186
|
+
const sg = readFileSafe(path.join(ROOT, settingsFile));
|
|
187
|
+
if (sg) {
|
|
188
|
+
const sgClean = sg.split("\n").filter(l => !l.trimStart().startsWith("//")).join("\n");
|
|
189
|
+
const includes = [];
|
|
190
|
+
const includeBlocks = [...sgClean.matchAll(/include\s*\(([^)]*)\)/gs)];
|
|
191
|
+
for (const block of includeBlocks) {
|
|
192
|
+
const quotedValues = [...block[1].matchAll(/["']([^"']+)["']/g)].map(m => m[1]);
|
|
193
|
+
includes.push(...quotedValues);
|
|
194
|
+
}
|
|
195
|
+
if (includes.length === 0) {
|
|
196
|
+
const groovyIncludes = [...sgClean.matchAll(/include\s+(.+)/g)];
|
|
197
|
+
for (const line of groovyIncludes) {
|
|
198
|
+
const quotedValues = [...line[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
199
|
+
includes.push(...quotedValues);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const cleanModules = includes.map(m => m.replace(/^:/, ""));
|
|
203
|
+
if (cleanModules.length > 0) {
|
|
204
|
+
stack.multiModule = true;
|
|
205
|
+
stack.modules = cleanModules;
|
|
206
|
+
stack.detected.push(`multi-module (${cleanModules.length} modules)`);
|
|
207
|
+
const hasCommand = cleanModules.some(m => m.includes("command"));
|
|
208
|
+
const hasQuery = cleanModules.some(m => m.includes("query"));
|
|
209
|
+
const hasBff = cleanModules.some(m => m.includes("bff"));
|
|
210
|
+
if (hasCommand && hasQuery) { stack.architecture = "cqrs"; stack.detected.push("cqrs"); }
|
|
211
|
+
if (hasBff) { stack.detected.push("bff"); }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Java: Maven ──
|
|
217
|
+
if (existsSafe(path.join(ROOT, "pom.xml"))) {
|
|
218
|
+
const pom = readFileSafe(path.join(ROOT, "pom.xml"));
|
|
219
|
+
if (pom) {
|
|
220
|
+
if (!stack.buildTool) { stack.buildTool = "maven"; stack.language = "java"; stack.detected.push("pom.xml"); }
|
|
221
|
+
const sv = pom.match(/<spring-boot[^>]*version>([^<]+)/);
|
|
222
|
+
if (sv) stack.frameworkVersion = sv[1];
|
|
223
|
+
const jv = pom.match(/<java\.version>(\d+)/);
|
|
224
|
+
if (jv) stack.languageVersion = jv[1];
|
|
225
|
+
if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
|
|
226
|
+
detectFirst(stack, "orm", pom, MAVEN_ORM_RULES);
|
|
227
|
+
// Maven DB: original does not push to detected (unlike Gradle)
|
|
228
|
+
if (!stack.database && pom.includes("postgresql")) stack.database = "postgresql";
|
|
229
|
+
if (!stack.database && pom.includes("mysql")) stack.database = "mysql";
|
|
230
|
+
if (!stack.database && pom.includes("oracle")) stack.database = "oracle";
|
|
231
|
+
if (!stack.database && pom.includes("mongodb")) stack.database = "mongodb";
|
|
232
|
+
if (!stack.database && H2_REGEX.test(pom)) stack.database = "h2";
|
|
233
|
+
if (!stack.database && pom.includes("sqlite")) stack.database = "sqlite";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Node.js ──
|
|
238
|
+
if (existsSafe(path.join(ROOT, "package.json"))) {
|
|
239
|
+
const pkg = readJsonSafe(path.join(ROOT, "package.json"));
|
|
240
|
+
if (pkg) {
|
|
241
|
+
stack.detected.push("package.json");
|
|
242
|
+
|
|
243
|
+
// ── Monorepo detection ──
|
|
244
|
+
// Detect monorepo markers: turbo.json, pnpm-workspace.yaml, lerna.json, package.json#workspaces
|
|
245
|
+
if (existsSafe(path.join(ROOT, "turbo.json"))) { stack.monorepo = "turborepo"; stack.detected.push("turbo.json"); }
|
|
246
|
+
else if (existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) { stack.monorepo = "pnpm-workspace"; stack.detected.push("pnpm-workspace.yaml"); }
|
|
247
|
+
else if (existsSafe(path.join(ROOT, "lerna.json"))) { stack.monorepo = "lerna"; stack.detected.push("lerna.json"); }
|
|
248
|
+
else if (pkg.workspaces) { stack.monorepo = "npm-workspaces"; stack.detected.push("npm-workspaces"); }
|
|
249
|
+
if (stack.monorepo) {
|
|
250
|
+
// Resolve workspace paths from package.json#workspaces or pnpm-workspace.yaml
|
|
251
|
+
let wsPatterns = [];
|
|
252
|
+
if (Array.isArray(pkg.workspaces)) wsPatterns = pkg.workspaces;
|
|
253
|
+
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) wsPatterns = pkg.workspaces.packages;
|
|
254
|
+
if (wsPatterns.length === 0 && existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) {
|
|
255
|
+
const wy = readFileSafe(path.join(ROOT, "pnpm-workspace.yaml"));
|
|
256
|
+
if (wy) {
|
|
257
|
+
const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
|
|
258
|
+
if (wm.length > 0) wsPatterns = wm;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (wsPatterns.length > 0) stack.workspaces = wsPatterns;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Merge deps from root + sub-package package.json files (monorepo)
|
|
265
|
+
// Sub-packages provide framework/frontend/ORM that root may lack
|
|
266
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
267
|
+
if (stack.monorepo) {
|
|
268
|
+
const subPkgGlobs = ["{apps,packages}/*/package.json"];
|
|
269
|
+
if (stack.workspaces) {
|
|
270
|
+
for (const ws of stack.workspaces) {
|
|
271
|
+
const wsGlob = /[*?]/.test(ws)
|
|
272
|
+
? ws.replace(/\/?\*?\*?$/, "/*/package.json")
|
|
273
|
+
: `${ws.replace(/\/?$/, "")}/{,*/}package.json`;
|
|
274
|
+
if (!subPkgGlobs.includes(wsGlob)) subPkgGlobs.push(wsGlob);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const spg of subPkgGlobs) {
|
|
278
|
+
const subPkgs = await glob(spg, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
279
|
+
for (const sp of subPkgs) {
|
|
280
|
+
const sub = readJsonSafe(path.join(ROOT, sp));
|
|
281
|
+
if (sub) Object.assign(deps, sub.dependencies, sub.devDependencies);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
287
|
+
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
|
288
|
+
|
|
289
|
+
// Frontend (Angular checked before React — Angular projects may include react in devDependencies)
|
|
290
|
+
const frontendRules = [
|
|
291
|
+
["next", "nextjs", "next.js"],
|
|
292
|
+
["@angular/core", "angular", "angular"],
|
|
293
|
+
["react", "react", "react"],
|
|
294
|
+
["vue", "vue", "vue"],
|
|
295
|
+
];
|
|
296
|
+
for (const [dep, name, label] of frontendRules) {
|
|
297
|
+
if (deps[dep] && !stack.frontend) {
|
|
298
|
+
stack.frontend = name; stack.detected.push(label);
|
|
299
|
+
stack.frontendVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Backend framework (NestJS > Fastify > Express — more specific first)
|
|
305
|
+
const frameworkRules = [
|
|
306
|
+
["@nestjs/core", "nestjs", "nestjs"],
|
|
307
|
+
["fastify", "fastify", "fastify"],
|
|
308
|
+
["express", "express", "express"],
|
|
309
|
+
];
|
|
310
|
+
for (const [dep, name, label] of frameworkRules) {
|
|
311
|
+
if (deps[dep] && !stack.framework) {
|
|
312
|
+
stack.framework = name; stack.detected.push(label);
|
|
313
|
+
if (dep !== "express") stack.frameworkVersion = deps[dep].replace(/[^0-9.]/g, "");
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Vite as framework (only if no backend framework was detected — Vite is a build/dev tool for SPA)
|
|
319
|
+
if (deps.vite && !stack.framework) {
|
|
320
|
+
stack.framework = "vite";
|
|
321
|
+
stack.detected.push("vite");
|
|
322
|
+
stack.frameworkVersion = deps.vite.replace(/[^0-9.]/g, "");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ORM
|
|
326
|
+
for (const [depKeys, ormName] of NODE_ORM_RULES) {
|
|
327
|
+
if (stack.orm) break;
|
|
328
|
+
if (depKeys.some(d => deps[d])) {
|
|
329
|
+
stack.orm = ormName; stack.detected.push(ormName);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (deps.mongoose) { if (!stack.database) stack.database = "mongodb"; if (!stack.orm) stack.orm = "mongoose"; stack.detected.push("mongoose"); }
|
|
333
|
+
|
|
334
|
+
// DB
|
|
335
|
+
const nodeDbRules = [["pg", "postgresql"], ["mysql2", "mysql"], ["mongodb", "mongodb"]];
|
|
336
|
+
for (const [dep, db] of nodeDbRules) {
|
|
337
|
+
if (deps[dep] && !stack.database) { stack.database = db; break; }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Package manager
|
|
341
|
+
stack.packageManager = existsSafe(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
|
|
342
|
+
: existsSafe(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
|
|
343
|
+
|
|
344
|
+
if (pkg.engines && pkg.engines.node && !stack.languageVersion) {
|
|
345
|
+
const nv = pkg.engines.node.match(/(\d+(?:\.\d+)*)/);
|
|
346
|
+
if (nv) stack.languageVersion = nv[1];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Python ──
|
|
352
|
+
const hasPyproject = existsSafe(path.join(ROOT, "pyproject.toml"));
|
|
353
|
+
const hasRequirements = existsSafe(path.join(ROOT, "requirements.txt"));
|
|
354
|
+
if (hasPyproject || hasRequirements) {
|
|
355
|
+
if (!stack.language) stack.language = "python";
|
|
356
|
+
stack.detected.push("python");
|
|
357
|
+
|
|
358
|
+
const pyFrameworkRules = [["django", "django"], ["fastapi", "fastapi"], ["flask", "flask"]];
|
|
359
|
+
const pyOrmRules = [["sqlalchemy", "sqlalchemy"], ["tortoise", "tortoise-orm"]];
|
|
360
|
+
|
|
361
|
+
if (hasPyproject) {
|
|
362
|
+
const pp = readFileSafe(path.join(ROOT, "pyproject.toml"));
|
|
363
|
+
if (pp) {
|
|
364
|
+
const pv = pp.match(/python\s*=\s*"[><=^~]*(\d+\.\d+)/);
|
|
365
|
+
if (pv && !stack.languageVersion) stack.languageVersion = pv[1];
|
|
366
|
+
for (const [kw, name] of pyFrameworkRules) {
|
|
367
|
+
if (pp.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
368
|
+
}
|
|
369
|
+
for (const [kw, name] of pyOrmRules) {
|
|
370
|
+
if (pp.includes(kw) && !stack.orm) { stack.orm = name; stack.detected.push(name); break; }
|
|
371
|
+
}
|
|
372
|
+
if (pp.includes("poetry")) { stack.packageManager = "poetry"; }
|
|
373
|
+
if (pp.includes("pdm")) { stack.packageManager = "pdm"; }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (hasRequirements) {
|
|
378
|
+
const r = readFileSafe(path.join(ROOT, "requirements.txt"));
|
|
379
|
+
if (r) {
|
|
380
|
+
for (const [kw, name] of pyFrameworkRules) {
|
|
381
|
+
if (r.includes(kw) && !stack.framework) { stack.framework = name; stack.detected.push(name); break; }
|
|
382
|
+
}
|
|
383
|
+
for (const [kw, name] of pyOrmRules) {
|
|
384
|
+
if (r.includes(kw) && !stack.orm) { stack.orm = name; break; }
|
|
385
|
+
}
|
|
386
|
+
if (r.includes("psycopg") && !stack.database) stack.database = "postgresql";
|
|
387
|
+
if (r.includes("mysqlclient") && !stack.database) stack.database = "mysql";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!stack.packageManager) {
|
|
392
|
+
stack.packageManager = existsSafe(path.join(ROOT, "Pipfile")) ? "pipenv"
|
|
393
|
+
: existsSafe(path.join(ROOT, "poetry.lock")) ? "poetry" : "pip";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── DB from config files ──
|
|
398
|
+
const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/.gradle/**"] });
|
|
399
|
+
for (const y of ymls) {
|
|
400
|
+
const c = readFileSafe(y);
|
|
401
|
+
if (!c) continue;
|
|
402
|
+
if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
|
|
403
|
+
if (!stack.database && c.includes("mysql")) stack.database = "mysql";
|
|
404
|
+
if (!stack.database && c.includes("oracle")) stack.database = "oracle";
|
|
405
|
+
if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
|
|
406
|
+
if (!stack.database && H2_REGEX.test(c)) stack.database = "h2";
|
|
407
|
+
if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
|
|
408
|
+
if (!stack.port) {
|
|
409
|
+
const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
|
|
410
|
+
if (pm) stack.port = parseInt(pm[1]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// .env
|
|
415
|
+
for (const ef of [".env", ".env.local", ".env.development"]) {
|
|
416
|
+
const ep = path.join(ROOT, ef);
|
|
417
|
+
if (existsSafe(ep)) {
|
|
418
|
+
const ec = readFileSafe(ep);
|
|
419
|
+
if (ec && ec.includes("DATABASE_URL")) {
|
|
420
|
+
// .env: original checks postgres (not postgresql), no oracle/h2
|
|
421
|
+
if (ec.includes("postgres") && !stack.database) stack.database = "postgresql";
|
|
422
|
+
if (ec.includes("mysql") && !stack.database) stack.database = "mysql";
|
|
423
|
+
if (ec.includes("mongodb") && !stack.database) stack.database = "mongodb";
|
|
424
|
+
if (ec.includes("sqlite") && !stack.database) stack.database = "sqlite";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Prisma schema
|
|
430
|
+
const prismaSchema = path.join(ROOT, "prisma/schema.prisma");
|
|
431
|
+
if (existsSafe(prismaSchema)) {
|
|
432
|
+
const ps = readFileSafe(prismaSchema);
|
|
433
|
+
if (ps) {
|
|
434
|
+
const prov = ps.match(/provider\s*=\s*"(\w+)"/);
|
|
435
|
+
if (prov && !stack.database) {
|
|
436
|
+
const db = { postgresql: "postgresql", mysql: "mysql", sqlite: "sqlite", mongodb: "mongodb" };
|
|
437
|
+
if (db[prov[1]]) stack.database = db[prov[1]];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Config file fallback (monorepo) ──
|
|
443
|
+
// [configFiles, frontendName, frameworkName (optional), label]
|
|
444
|
+
const frontendFallbacks = [
|
|
445
|
+
[["next.config.js", "next.config.mjs", "next.config.ts"], "nextjs", null, "next.config (fallback)"],
|
|
446
|
+
[["vite.config.ts", "vite.config.js"], "react", "vite", "vite.config (fallback)"],
|
|
447
|
+
[["nuxt.config.ts", "nuxt.config.js"], "vue", null, "nuxt.config (fallback)"],
|
|
448
|
+
[["angular.json", ".angular.json"], "angular", null, "angular.json (fallback)"],
|
|
449
|
+
];
|
|
450
|
+
if (!stack.frontend) {
|
|
451
|
+
for (const [files, frontendName, frameworkName, label] of frontendFallbacks) {
|
|
452
|
+
if (files.some(f => existsSafe(path.join(ROOT, f)))) {
|
|
453
|
+
stack.frontend = frontendName; stack.detected.push(label);
|
|
454
|
+
if (!stack.language) stack.language = "typescript";
|
|
455
|
+
if (frameworkName && !stack.framework) {
|
|
456
|
+
stack.framework = frameworkName;
|
|
457
|
+
stack.detected.push(frameworkName + " (fallback)");
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── .env-derived factual config ──
|
|
465
|
+
// Read .env.example (preferred) or .env to capture ports/hosts/API targets
|
|
466
|
+
// the project actually declares. This overrides framework-default guesses
|
|
467
|
+
// in downstream code (plan-installer/index.js defaultPort) and exposes the
|
|
468
|
+
// full variable map to Pass 3 prompts via project-analysis.json.
|
|
469
|
+
const envInfo = readStackEnvInfo(ROOT);
|
|
470
|
+
if (envInfo) {
|
|
471
|
+
stack.envInfo = envInfo;
|
|
472
|
+
// Promote .env-declared port to stack.port if no earlier detection won
|
|
473
|
+
// (e.g., Spring application.yml parsing at line 407).
|
|
474
|
+
if (!stack.port && envInfo.port) {
|
|
475
|
+
stack.port = envInfo.port;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return stack;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = { detectStack };
|