claudeos-core 1.5.0 → 1.6.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 +38 -0
- package/README.de.md +54 -27
- package/README.es.md +54 -26
- package/README.fr.md +54 -26
- package/README.hi.md +54 -27
- package/README.ja.md +53 -26
- package/README.ko.md +56 -29
- package/README.md +53 -26
- package/README.ru.md +54 -27
- package/README.vi.md +54 -27
- package/README.zh-CN.md +54 -27
- package/bin/cli.js +6 -2
- package/bin/commands/init.js +48 -46
- package/bin/lib/cli-utils.js +6 -10
- package/bin/lib/lang-selector.js +10 -8
- package/bin/lib/resume-selector.js +10 -8
- package/content-validator/index.js +7 -12
- package/health-checker/index.js +7 -17
- package/lib/plan-parser.js +149 -0
- package/lib/safe-fs.js +2 -2
- package/lib/stale-report.js +36 -0
- package/manifest-generator/index.js +13 -36
- package/package.json +87 -87
- package/pass-json-validator/index.js +7 -12
- package/pass-prompts/templates/node-nestjs/pass1.md +132 -0
- package/pass-prompts/templates/node-nestjs/pass2.md +97 -0
- package/pass-prompts/templates/node-nestjs/pass3.md +121 -0
- package/pass-prompts/templates/vue-nuxt/pass1.md +119 -0
- package/pass-prompts/templates/vue-nuxt/pass2.md +83 -0
- package/pass-prompts/templates/vue-nuxt/pass3.md +100 -0
- package/plan-installer/domain-grouper.js +5 -2
- package/plan-installer/prompt-generator.js +1 -4
- package/plan-installer/scanners/scan-frontend.js +14 -7
- package/plan-installer/scanners/scan-java.js +18 -13
- package/plan-installer/scanners/scan-kotlin.js +47 -23
- package/plan-installer/scanners/scan-node.js +16 -2
- package/plan-installer/stack-detector.js +42 -1
- package/plan-validator/index.js +11 -121
- package/sync-checker/index.js +6 -19
|
@@ -20,7 +20,7 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
20
20
|
|
|
21
21
|
const javaFiles = await glob("src/main/java/**/*.java", { cwd: ROOT });
|
|
22
22
|
for (const f of javaFiles) {
|
|
23
|
-
const m = f.match(/src\/main\/java\/(.+?)\/(controller|aggregator|service|mapper|dao|dto|entity|repository|adapter)/);
|
|
23
|
+
const m = f.match(/src\/main\/java\/(.+?)\/(controller|aggregator|facade|usecase|orchestrator|service|mapper|dao|dto|entity|repository|adapter)/);
|
|
24
24
|
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
25
25
|
}
|
|
26
26
|
const domainMap = {};
|
|
@@ -100,16 +100,16 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
100
100
|
if (Object.keys(domainMap).length > 0) detectedPattern = "C";
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// ── Supplementary scan: detect domains without controllers (service/dao/aggregator only) ──
|
|
103
|
+
// ── Supplementary scan: detect domains without controllers (service/dao/aggregator/facade/usecase only) ──
|
|
104
104
|
// Runs for ALL detected patterns (A/B/C/D/E) to catch core-only domains
|
|
105
105
|
{
|
|
106
106
|
const serviceDirs = await glob("src/main/java/**/*/service/*.java", { cwd: ROOT });
|
|
107
107
|
const mapperDirs = await glob("src/main/java/**/*/{mapper,repository,dao}/*.java", { cwd: ROOT });
|
|
108
|
-
const
|
|
109
|
-
const allServiceFiles = [...serviceDirs, ...mapperDirs, ...
|
|
108
|
+
const orchestrationDirs = await glob("src/main/java/**/*/{aggregator,facade,usecase,orchestrator}/*.java", { cwd: ROOT });
|
|
109
|
+
const allServiceFiles = [...serviceDirs, ...mapperDirs, ...orchestrationDirs];
|
|
110
110
|
const skipDomains = ["common", "config", "util", "utils", "base", "core", "shared", "global", "framework", "infra", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "main", "generated", "build"];
|
|
111
111
|
for (const f of allServiceFiles) {
|
|
112
|
-
const m = f.match(/\/([^/]+)\/(service|mapper|repository|dao|aggregator)\/[^/]+\.java$/);
|
|
112
|
+
const m = f.match(/\/([^/]+)\/(service|mapper|repository|dao|aggregator|facade|usecase|orchestrator)\/[^/]+\.java$/);
|
|
113
113
|
if (m) {
|
|
114
114
|
const d = m[1];
|
|
115
115
|
if (!domainMap[d] && !skipDomains.includes(d) && !/^v\d+$/.test(d)) {
|
|
@@ -119,7 +119,7 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Scan service/mapper/dao/aggregator/dto/xml files for each domain
|
|
122
|
+
// Scan service/mapper/dao/aggregator/facade/usecase/dto/xml files for each domain
|
|
123
123
|
for (const d of Object.keys(domainMap)) {
|
|
124
124
|
const p = domainMap[d].pattern;
|
|
125
125
|
const dn = domainMap[d].domainName || d;
|
|
@@ -129,12 +129,12 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
129
129
|
svcGlob = `src/main/java/**/service/${d}/*.java`;
|
|
130
130
|
mprGlob = `src/main/java/**/{mapper,repository,dao}/${d}/*.java`;
|
|
131
131
|
dtoGlob = `src/main/java/**/dto/${d}/**/*.java`;
|
|
132
|
-
aggGlob = `src/main/java/**/aggregator/${d}/*.java`;
|
|
132
|
+
aggGlob = `src/main/java/**/{aggregator,facade,usecase,orchestrator}/${d}/*.java`;
|
|
133
133
|
} else if (p === "B" || p === "D") {
|
|
134
134
|
svcGlob = `src/main/java/**/${dn}/service/*.java`;
|
|
135
135
|
mprGlob = `src/main/java/**/${dn}/{mapper,repository,dao}/*.java`;
|
|
136
136
|
dtoGlob = `src/main/java/**/${dn}/dto/**/*.java`;
|
|
137
|
-
aggGlob = `src/main/java/**/${dn}/aggregator/*.java`;
|
|
137
|
+
aggGlob = `src/main/java/**/${dn}/{aggregator,facade,usecase,orchestrator}/*.java`;
|
|
138
138
|
} else if (p === "E") {
|
|
139
139
|
svcGlob = `src/main/java/**/${d}/{application,domain}/**/*.java`;
|
|
140
140
|
mprGlob = `src/main/java/**/${d}/adapter/out/{persistence,repository}/*.java`;
|
|
@@ -146,9 +146,14 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
146
146
|
svcGlob = `src/main/java/**/service/${cap}*.java`;
|
|
147
147
|
mprGlob = `src/main/java/**/{mapper,repository,dao}/${cap}*.java`;
|
|
148
148
|
dtoGlob = `src/main/java/**/dto/${cap}*.java`;
|
|
149
|
-
aggGlob = `src/main/java/**/aggregator/${cap}*.java`;
|
|
149
|
+
aggGlob = `src/main/java/**/{aggregator,facade,usecase,orchestrator}/${cap}*.java`;
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
// Pattern C (flat): XML may be in flat directory without domain subdirectory (e.g., mapper/OrderMapper.xml)
|
|
152
|
+
// Other patterns: XML is in domain subdirectory (e.g., mapper/order/OrderMapper.xml)
|
|
153
|
+
const capDn = dn.charAt(0).toUpperCase() + dn.slice(1);
|
|
154
|
+
const xmlGlob = p === "C"
|
|
155
|
+
? `src/main/resources/{mapper,mybatis}/**/{${dn}/${capDn}*.xml,${capDn}*.xml}`
|
|
156
|
+
: `src/main/resources/{mapper,mybatis}/**/${dn}/*.xml`;
|
|
152
157
|
|
|
153
158
|
const svc = await glob(svcGlob, { cwd: ROOT });
|
|
154
159
|
const mpr = await glob(mprGlob, { cwd: ROOT });
|
|
@@ -169,7 +174,7 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
169
174
|
const javaDomains = {};
|
|
170
175
|
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "api", "main", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "generated", "build"];
|
|
171
176
|
const versionPattern = /^v\d+$/;
|
|
172
|
-
const layerNames = ["controller", "aggregator", "service", "mapper", "repository", "dao", "dto", "vo", "entity", "adapter"];
|
|
177
|
+
const layerNames = ["controller", "aggregator", "facade", "usecase", "orchestrator", "service", "mapper", "repository", "dao", "dto", "vo", "entity", "adapter"];
|
|
173
178
|
|
|
174
179
|
for (const f of allJava) {
|
|
175
180
|
const parts = f.replace(/\\/g, "/").split("/");
|
|
@@ -182,7 +187,7 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
182
187
|
if (i > 0 && !skipNames.includes(prevDir) && !layerNames.includes(prevDir) && !prevDir.includes(".") && !versionPattern.test(prevDir)) {
|
|
183
188
|
if (!javaDomains[prevDir]) javaDomains[prevDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
184
189
|
if (parts[i] === "controller") javaDomains[prevDir].controllers++;
|
|
185
|
-
else if (
|
|
190
|
+
else if (["aggregator", "facade", "usecase", "orchestrator", "service"].includes(parts[i])) javaDomains[prevDir].services++;
|
|
186
191
|
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[prevDir].mappers++;
|
|
187
192
|
else if (["dto", "vo"].includes(parts[i])) javaDomains[prevDir].dtos++;
|
|
188
193
|
}
|
|
@@ -190,7 +195,7 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
190
195
|
if (nextDir && !nextDir.endsWith(".java") && !skipNames.includes(nextDir) && !layerNames.includes(nextDir) && !versionPattern.test(nextDir)) {
|
|
191
196
|
if (!javaDomains[nextDir]) javaDomains[nextDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
192
197
|
if (parts[i] === "controller") javaDomains[nextDir].controllers++;
|
|
193
|
-
else if (
|
|
198
|
+
else if (["aggregator", "facade", "usecase", "orchestrator", "service"].includes(parts[i])) javaDomains[nextDir].services++;
|
|
194
199
|
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[nextDir].mappers++;
|
|
195
200
|
else if (["dto", "vo"].includes(parts[i])) javaDomains[nextDir].dtos++;
|
|
196
201
|
}
|
|
@@ -9,15 +9,18 @@
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const { glob } = require("glob");
|
|
11
11
|
|
|
12
|
+
// Normalize backslash paths from glob on Windows to forward slashes
|
|
13
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
14
|
+
|
|
12
15
|
async function scanKotlinDomains(stack, ROOT) {
|
|
13
16
|
const backendDomains = [];
|
|
14
17
|
let rootPackage = null;
|
|
15
18
|
|
|
16
19
|
// Scan all .kt files across all submodules
|
|
17
|
-
const ktFiles = await glob("**/src/main/kotlin/**/*.kt", {
|
|
20
|
+
const ktFiles = (await glob("**/src/main/kotlin/**/*.kt", {
|
|
18
21
|
cwd: ROOT,
|
|
19
22
|
ignore: ["**/node_modules/**", "**/build/**", "**/test/**", "**/generated/**"],
|
|
20
|
-
});
|
|
23
|
+
})).map(norm);
|
|
21
24
|
|
|
22
25
|
// Detect root package from first controller/service file
|
|
23
26
|
for (const f of ktFiles) {
|
|
@@ -33,13 +36,19 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
33
36
|
];
|
|
34
37
|
const moduleSet = {};
|
|
35
38
|
for (const mg of moduleGlobs) {
|
|
36
|
-
const moduleDirs = await glob(mg, { cwd: ROOT });
|
|
39
|
+
const moduleDirs = (await glob(mg, { cwd: ROOT })).map(norm);
|
|
37
40
|
for (const md of moduleDirs) {
|
|
38
|
-
const parts = md.replace(/\/src\/main\/kotlin
|
|
41
|
+
const parts = md.replace(/\/src\/main\/kotlin\/?$/, "").split("/");
|
|
39
42
|
const moduleName = parts[parts.length - 1]; // e.g., "reservation-command-server"
|
|
40
43
|
if (moduleSet[moduleName]) {
|
|
41
|
-
// Name conflict:
|
|
42
|
-
|
|
44
|
+
// Name conflict: upgrade first entry to full key too
|
|
45
|
+
if (!moduleName.includes("__")) {
|
|
46
|
+
const existingPath = moduleSet[moduleName];
|
|
47
|
+
const existingFullKey = existingPath.replace(/\/src\/main\/kotlin\/?$/, "").replace(/\//g, "__");
|
|
48
|
+
moduleSet[existingFullKey] = existingPath;
|
|
49
|
+
delete moduleSet[moduleName];
|
|
50
|
+
}
|
|
51
|
+
const fullKey = md.replace(/\/src\/main\/kotlin\/?$/, "").replace(/\//g, "__");
|
|
43
52
|
moduleSet[fullKey] = md;
|
|
44
53
|
} else {
|
|
45
54
|
moduleSet[moduleName] = md;
|
|
@@ -53,16 +62,21 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
53
62
|
const domainMap = {};
|
|
54
63
|
|
|
55
64
|
for (const [moduleName, modulePath] of Object.entries(moduleSet)) {
|
|
56
|
-
|
|
65
|
+
// Full key (e.g., "servers__command__reservation-command-server") → extract actual module name
|
|
66
|
+
const actualModuleName = moduleName.includes("__")
|
|
67
|
+
? moduleName.split("__").pop()
|
|
68
|
+
: moduleName;
|
|
69
|
+
|
|
70
|
+
if (skipModules.some(s => actualModuleName.includes(s))) continue;
|
|
57
71
|
|
|
58
72
|
// Determine server type from module name
|
|
59
73
|
let serverType = "standalone";
|
|
60
74
|
for (const [key, val] of Object.entries(serverTypes)) {
|
|
61
|
-
if (
|
|
75
|
+
if (actualModuleName.includes(key)) { serverType = val; break; }
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
// Extract domain name from module name
|
|
65
|
-
let domainName =
|
|
79
|
+
let domainName = actualModuleName
|
|
66
80
|
.replace(/-(?:command|query|bff|integration|adapter)-server$/, "")
|
|
67
81
|
.replace(/-server$/, "");
|
|
68
82
|
|
|
@@ -73,7 +87,7 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
// Scan files in this module (append "/" to prevent prefix overlap)
|
|
76
|
-
const modulePrefix = modulePath.replace(
|
|
90
|
+
const modulePrefix = modulePath.replace(/\/?$/, "").replace(/\/src\/main\/kotlin$/, "") + "/";
|
|
77
91
|
const moduleKtFiles = ktFiles.filter(f => f.startsWith(modulePrefix) || f === modulePrefix.slice(0, -1));
|
|
78
92
|
const controllers = moduleKtFiles.filter(f => /controller/i.test(f)).length;
|
|
79
93
|
const services = moduleKtFiles.filter(f => /service/i.test(f)).length;
|
|
@@ -84,17 +98,27 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
84
98
|
if (totalFiles === 0) continue;
|
|
85
99
|
|
|
86
100
|
const key = `${domainName}:${serverType}`;
|
|
87
|
-
domainMap[key]
|
|
88
|
-
|
|
89
|
-
moduleName
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
if (domainMap[key]) {
|
|
102
|
+
// Merge into existing entry (e.g., same domain+type from collided module names)
|
|
103
|
+
domainMap[key].moduleNames.push(moduleName);
|
|
104
|
+
domainMap[key].controllers += controllers;
|
|
105
|
+
domainMap[key].services += services;
|
|
106
|
+
domainMap[key].repositories += repositories;
|
|
107
|
+
domainMap[key].dtos += dtos;
|
|
108
|
+
domainMap[key].totalFiles += totalFiles;
|
|
109
|
+
} else {
|
|
110
|
+
domainMap[key] = {
|
|
111
|
+
name: domainName,
|
|
112
|
+
moduleNames: [moduleName],
|
|
113
|
+
modulePath,
|
|
114
|
+
serverType,
|
|
115
|
+
controllers,
|
|
116
|
+
services,
|
|
117
|
+
repositories,
|
|
118
|
+
dtos,
|
|
119
|
+
totalFiles,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
98
122
|
}
|
|
99
123
|
|
|
100
124
|
// Group by domain: merge command/query/bff of same domain
|
|
@@ -105,7 +129,7 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
105
129
|
domainGroups[name] = { name, type: "backend", modules: [], controllers: 0, services: 0, repositories: 0, dtos: 0, totalFiles: 0, serverTypes: [] };
|
|
106
130
|
}
|
|
107
131
|
const dg = domainGroups[name];
|
|
108
|
-
dg.modules.push(info.
|
|
132
|
+
dg.modules.push(...info.moduleNames);
|
|
109
133
|
dg.controllers += info.controllers;
|
|
110
134
|
dg.services += info.services;
|
|
111
135
|
dg.repositories += info.repositories;
|
|
@@ -131,7 +155,7 @@ async function scanKotlinDomains(stack, ROOT) {
|
|
|
131
155
|
}
|
|
132
156
|
|
|
133
157
|
// Also scan shared libraries as special domains
|
|
134
|
-
const libDirs = await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT });
|
|
158
|
+
const libDirs = (await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT })).map(norm);
|
|
135
159
|
for (const ld of libDirs) {
|
|
136
160
|
const libName = ld.split("/")[0];
|
|
137
161
|
if (domainGroups[libName]) continue; // Already captured
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* ClaudeOS-Core — Node.js Structure Scanner
|
|
3
3
|
*
|
|
4
4
|
* Scans Node.js backend (Express/NestJS/Fastify) project structure to discover domains.
|
|
5
|
+
* Supports monorepo layouts (apps/*, packages/*) in addition to single-project src/.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const path = require("path");
|
|
@@ -9,10 +10,23 @@ const { glob } = require("glob");
|
|
|
9
10
|
|
|
10
11
|
async function scanNodeDomains(stack, ROOT) {
|
|
11
12
|
const backendDomains = [];
|
|
13
|
+
const skipDirs = ["common", "shared", "config", "utils", "lib", "core", "main", "interfaces", "types", "constants", "guards", "decorators", "pipes", "filters", "interceptors"];
|
|
12
14
|
|
|
15
|
+
// Collect candidate directories: standard src/ + monorepo apps/*/src/
|
|
13
16
|
const nestModules = await glob("src/modules/*/", { cwd: ROOT });
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
let srcDirs = nestModules.length > 0 ? nestModules : await glob("src/*/", { cwd: ROOT });
|
|
18
|
+
|
|
19
|
+
// Monorepo: scan apps/*/src/ and packages/*/src/ when standard src/ yields nothing backend-relevant
|
|
20
|
+
if (stack.monorepo || srcDirs.length === 0) {
|
|
21
|
+
const monoModules = await glob("{apps,packages}/*/src/modules/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
22
|
+
if (monoModules.length > 0) {
|
|
23
|
+
srcDirs = [...srcDirs, ...monoModules];
|
|
24
|
+
} else {
|
|
25
|
+
const monoDirs = await glob("{apps,packages}/*/src/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
26
|
+
srcDirs = [...srcDirs, ...monoDirs];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
for (let dir of srcDirs) {
|
|
17
31
|
if (!dir.endsWith("/")) dir += "/";
|
|
18
32
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
@@ -90,7 +90,8 @@ async function detectStack(ROOT) {
|
|
|
90
90
|
framework: null, frameworkVersion: null,
|
|
91
91
|
buildTool: null, database: null, orm: null,
|
|
92
92
|
frontend: null, frontendVersion: null,
|
|
93
|
-
packageManager: null,
|
|
93
|
+
packageManager: null, monorepo: null, workspaces: null,
|
|
94
|
+
detected: [],
|
|
94
95
|
};
|
|
95
96
|
|
|
96
97
|
// ── Java/Kotlin: Gradle ──
|
|
@@ -115,6 +116,8 @@ async function detectStack(ROOT) {
|
|
|
115
116
|
if (jv) stack.languageVersion = jv[1];
|
|
116
117
|
|
|
117
118
|
detectFirst(stack, "orm", g, GRADLE_ORM_RULES);
|
|
119
|
+
// Exclude "postgres" (substring of postgresql — false positive on r2dbc-postgres) and "sqlite" (rare in Gradle deps)
|
|
120
|
+
// "postgresql" is still matched via DB_KEYWORD_RULES; h2 uses word-boundary check separately
|
|
118
121
|
detectDb(stack, g, DB_KEYWORD_RULES.filter(([kw]) => !["postgres", "sqlite"].includes(kw)));
|
|
119
122
|
|
|
120
123
|
// Kotlin detection: override language if Kotlin plugin found
|
|
@@ -235,7 +238,45 @@ async function detectStack(ROOT) {
|
|
|
235
238
|
const pkg = readJsonSafe(path.join(ROOT, "package.json"));
|
|
236
239
|
if (pkg) {
|
|
237
240
|
stack.detected.push("package.json");
|
|
241
|
+
|
|
242
|
+
// ── Monorepo detection ──
|
|
243
|
+
// Detect monorepo markers: turbo.json, pnpm-workspace.yaml, lerna.json, package.json#workspaces
|
|
244
|
+
if (existsSafe(path.join(ROOT, "turbo.json"))) { stack.monorepo = "turborepo"; stack.detected.push("turbo.json"); }
|
|
245
|
+
else if (existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) { stack.monorepo = "pnpm-workspace"; stack.detected.push("pnpm-workspace.yaml"); }
|
|
246
|
+
else if (existsSafe(path.join(ROOT, "lerna.json"))) { stack.monorepo = "lerna"; stack.detected.push("lerna.json"); }
|
|
247
|
+
else if (pkg.workspaces) { stack.monorepo = "npm-workspaces"; stack.detected.push("npm-workspaces"); }
|
|
248
|
+
if (stack.monorepo) {
|
|
249
|
+
// Resolve workspace paths from package.json#workspaces or pnpm-workspace.yaml
|
|
250
|
+
let wsPatterns = [];
|
|
251
|
+
if (Array.isArray(pkg.workspaces)) wsPatterns = pkg.workspaces;
|
|
252
|
+
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) wsPatterns = pkg.workspaces.packages;
|
|
253
|
+
if (wsPatterns.length === 0 && existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) {
|
|
254
|
+
const wy = readFileSafe(path.join(ROOT, "pnpm-workspace.yaml"));
|
|
255
|
+
const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
|
|
256
|
+
if (wm.length > 0) wsPatterns = wm;
|
|
257
|
+
}
|
|
258
|
+
if (wsPatterns.length > 0) stack.workspaces = wsPatterns;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Merge deps from root + sub-package package.json files (monorepo)
|
|
262
|
+
// Sub-packages provide framework/frontend/ORM that root may lack
|
|
238
263
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
264
|
+
if (stack.monorepo) {
|
|
265
|
+
const subPkgGlobs = ["{apps,packages}/*/package.json"];
|
|
266
|
+
if (stack.workspaces) {
|
|
267
|
+
for (const ws of stack.workspaces) {
|
|
268
|
+
const wsGlob = ws.replace(/\/?\*?\*?$/, "/*/package.json");
|
|
269
|
+
if (!subPkgGlobs.includes(wsGlob)) subPkgGlobs.push(wsGlob);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const spg of subPkgGlobs) {
|
|
273
|
+
const subPkgs = await glob(spg, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
274
|
+
for (const sp of subPkgs) {
|
|
275
|
+
const sub = readJsonSafe(path.join(ROOT, sp));
|
|
276
|
+
if (sub) Object.assign(deps, sub.dependencies, sub.devDependencies);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
239
280
|
|
|
240
281
|
if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
|
|
241
282
|
if (deps.typescript) { stack.detected.push("typescript"); const tv = deps.typescript.match(/(\d+(?:\.\d+)*)/); if (tv) stack.languageVersion = tv[1]; }
|
package/plan-validator/index.js
CHANGED
|
@@ -15,126 +15,22 @@
|
|
|
15
15
|
const fs = require("fs");
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const { glob } = require("glob");
|
|
18
|
+
const { parseFileBlocks, parseCodeBlocks, replaceFileBlock, replaceCodeBlock, CODE_BLOCK_PLANS } = require("../lib/plan-parser");
|
|
19
|
+
const { updateStaleReport } = require("../lib/stale-report");
|
|
18
20
|
|
|
19
21
|
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
20
22
|
const PLAN = path.join(ROOT, "claudeos-core/plan");
|
|
21
23
|
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
22
24
|
|
|
23
|
-
//
|
|
24
|
-
const CODE_BLOCK_PLANS = ["21.sync-rules-master.md"];
|
|
25
|
+
// CODE_BLOCK_PLANS imported from lib/plan-parser.js
|
|
25
26
|
|
|
26
27
|
function rel(p) {
|
|
27
28
|
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
-
function extractFileBlocks(content) {
|
|
32
|
-
|
|
33
|
-
let m;
|
|
34
|
-
const re = /<file\s+path="([^"]+)">\s*\n([\s\S]*?)\n<\/file>/g;
|
|
35
|
-
while ((m = re.exec(content)) !== null) {
|
|
36
|
-
result.push({ path: m[1], content: m[2] });
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Extract ## N. `path` \n```markdown ... ``` blocks
|
|
42
|
-
// Uses indexOf-based parsing to correctly handle nested code fences inside markdown content
|
|
43
|
-
function extractCodeBlocks(content) {
|
|
44
|
-
const result = [];
|
|
45
|
-
const headingRe = /^##\s+\d+\.\s+([^\n]+)/gm;
|
|
46
|
-
let headingMatch;
|
|
47
|
-
while ((headingMatch = headingRe.exec(content)) !== null) {
|
|
48
|
-
const filePath = headingMatch[1].replace(/`/g, "").trim();
|
|
49
|
-
// Find the opening ```markdown after the heading
|
|
50
|
-
const openFence = content.indexOf("```markdown\n", headingMatch.index);
|
|
51
|
-
if (openFence < 0) continue;
|
|
52
|
-
const contentStart = openFence + "```markdown\n".length;
|
|
53
|
-
// Find the matching closing ``` — track nesting depth to skip inner fenced blocks
|
|
54
|
-
let searchPos = contentStart;
|
|
55
|
-
let closingPos = -1;
|
|
56
|
-
let nestDepth = 0;
|
|
57
|
-
while (searchPos < content.length) {
|
|
58
|
-
const nextFence = content.indexOf("\n```", searchPos);
|
|
59
|
-
if (nextFence < 0) break;
|
|
60
|
-
const fenceLineStart = nextFence + 1; // position of ```
|
|
61
|
-
// Determine end of this ``` line
|
|
62
|
-
const nextNewline = content.indexOf("\n", fenceLineStart + 3);
|
|
63
|
-
const restOfLine = nextNewline >= 0
|
|
64
|
-
? content.substring(fenceLineStart + 3, nextNewline)
|
|
65
|
-
: content.substring(fenceLineStart + 3);
|
|
66
|
-
// Opening fence: ``` followed by a language tag (non-empty alphanumeric text)
|
|
67
|
-
const isOpening = /^[a-zA-Z]/.test(restOfLine.trim());
|
|
68
|
-
if (isOpening) {
|
|
69
|
-
nestDepth++;
|
|
70
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
71
|
-
} else if (nestDepth > 0) {
|
|
72
|
-
nestDepth--;
|
|
73
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
74
|
-
} else {
|
|
75
|
-
closingPos = fenceLineStart;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (closingPos < 0) continue;
|
|
80
|
-
const blockContent = content.substring(contentStart, closingPos).trimEnd();
|
|
81
|
-
result.push({ path: filePath, content: blockContent });
|
|
82
|
-
// Advance headingRe past this block to avoid re-matching inside content
|
|
83
|
-
headingRe.lastIndex = closingPos;
|
|
84
|
-
}
|
|
85
|
-
return result;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Replace <file> block content with new content
|
|
89
|
-
function replaceFileBlock(content, filePath, newContent) {
|
|
90
|
-
const escaped = filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
-
return content.replace(
|
|
92
|
-
new RegExp(`(<file\\s+path="${escaped}">\\s*\\n)[\\s\\S]*?(\\n</file>)`, "g"),
|
|
93
|
-
`$1${newContent}$2`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Replace code block content with new content
|
|
98
|
-
// Uses indexOf-based approach to avoid regex issues with nested code fences
|
|
99
|
-
function replaceCodeBlock(content, filePath, newContent) {
|
|
100
|
-
const cleanPath = filePath.replace(/`/g, "");
|
|
101
|
-
// Find the heading line containing the file path
|
|
102
|
-
const headingPattern = new RegExp(`^##\\s+\\d+\\.\\s+\`?${cleanPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\`?`, "m");
|
|
103
|
-
const headingMatch = headingPattern.exec(content);
|
|
104
|
-
if (!headingMatch) return content;
|
|
105
|
-
// Find the opening ```markdown after the heading
|
|
106
|
-
const afterHeading = content.indexOf("```markdown\n", headingMatch.index);
|
|
107
|
-
if (afterHeading < 0) return content;
|
|
108
|
-
const contentStart = afterHeading + "```markdown\n".length;
|
|
109
|
-
// Find the matching closing ``` — track nesting depth to skip inner fenced blocks
|
|
110
|
-
let searchPos = contentStart;
|
|
111
|
-
let closingPos = -1;
|
|
112
|
-
let nestDepth = 0;
|
|
113
|
-
while (searchPos < content.length) {
|
|
114
|
-
const nextFence = content.indexOf("\n```", searchPos);
|
|
115
|
-
if (nextFence < 0) break;
|
|
116
|
-
const fenceLineStart = nextFence + 1; // position of ```
|
|
117
|
-
// Determine end of this ``` line
|
|
118
|
-
const nextNewline = content.indexOf("\n", fenceLineStart + 3);
|
|
119
|
-
const restOfLine = nextNewline >= 0
|
|
120
|
-
? content.substring(fenceLineStart + 3, nextNewline)
|
|
121
|
-
: content.substring(fenceLineStart + 3);
|
|
122
|
-
// Opening fence: ``` followed by a language tag (non-empty alphanumeric text)
|
|
123
|
-
const isOpening = /^[a-zA-Z]/.test(restOfLine.trim());
|
|
124
|
-
if (isOpening) {
|
|
125
|
-
nestDepth++;
|
|
126
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
127
|
-
} else if (nestDepth > 0) {
|
|
128
|
-
nestDepth--;
|
|
129
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
130
|
-
} else {
|
|
131
|
-
closingPos = fenceLineStart;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (closingPos < 0) return content;
|
|
136
|
-
return content.substring(0, contentStart) + newContent + "\n" + content.substring(closingPos);
|
|
137
|
-
}
|
|
31
|
+
// Aliases for backward compatibility (used by tests and main())
|
|
32
|
+
function extractFileBlocks(content) { return parseFileBlocks(content, { includeContent: true }); }
|
|
33
|
+
function extractCodeBlocks(content) { return parseCodeBlocks(content, { includeContent: true }); }
|
|
138
34
|
|
|
139
35
|
async function main() {
|
|
140
36
|
const validModes = ["--check", "--refresh", "--execute"];
|
|
@@ -244,16 +140,10 @@ async function main() {
|
|
|
244
140
|
);
|
|
245
141
|
|
|
246
142
|
// Also write to stale-report.json for consolidated health reporting
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
ex.planValidation = { checkedAt: new Date().toISOString(), mode, total, synced, drift, missing };
|
|
253
|
-
if (!ex.summary) ex.summary = {};
|
|
254
|
-
ex.summary.planDrift = drift;
|
|
255
|
-
ex.summary.planMissing = missing;
|
|
256
|
-
fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
|
|
143
|
+
updateStaleReport(GEN, "planValidation",
|
|
144
|
+
{ checkedAt: new Date().toISOString(), mode, total, synced, drift, missing },
|
|
145
|
+
{ planDrift: drift, planMissing: missing }
|
|
146
|
+
);
|
|
257
147
|
}
|
|
258
148
|
|
|
259
149
|
process.exit(drift + missing > 0 ? 1 : 0);
|
|
@@ -261,7 +151,7 @@ async function main() {
|
|
|
261
151
|
|
|
262
152
|
// Export for testing; run main() only when executed directly
|
|
263
153
|
if (require.main === module) {
|
|
264
|
-
main().catch(console.error);
|
|
154
|
+
main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
|
|
265
155
|
}
|
|
266
156
|
|
|
267
157
|
module.exports = { extractFileBlocks, extractCodeBlocks, replaceFileBlock, replaceCodeBlock };
|
package/sync-checker/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
const fs = require("fs");
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const { glob } = require("glob");
|
|
18
|
+
const { updateStaleReport } = require("../lib/stale-report");
|
|
18
19
|
|
|
19
20
|
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
20
21
|
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
@@ -105,24 +106,10 @@ async function main() {
|
|
|
105
106
|
console.log(total === 0 ? " ✅ All in sync\n" : ` ⚠️ ${total} issues\n`);
|
|
106
107
|
|
|
107
108
|
// ─── Update stale-report.json ────────────────────────────
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
try { ex = JSON.parse(fs.readFileSync(rp, "utf-8")); } catch { ex = {}; }
|
|
113
|
-
}
|
|
114
|
-
ex.syncMisses = {
|
|
115
|
-
checkedAt: new Date().toISOString(),
|
|
116
|
-
unregistered: issues.unreg,
|
|
117
|
-
orphaned: issues.orphan,
|
|
118
|
-
};
|
|
119
|
-
ex.summary = {
|
|
120
|
-
...ex.summary,
|
|
121
|
-
syncIssues: total,
|
|
122
|
-
status: total === 0 ? "ok" : "warning",
|
|
123
|
-
};
|
|
124
|
-
fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
|
|
125
|
-
}
|
|
109
|
+
updateStaleReport(GEN, "syncMisses",
|
|
110
|
+
{ checkedAt: new Date().toISOString(), unregistered: issues.unreg, orphaned: issues.orphan },
|
|
111
|
+
{ syncIssues: total, status: total === 0 ? "ok" : "warning" }
|
|
112
|
+
);
|
|
126
113
|
|
|
127
114
|
// Exit 1 only for orphaned files (actual breakage), not for unregistered (informational)
|
|
128
115
|
const orphanCount = issues.orphan.length;
|
|
@@ -130,7 +117,7 @@ async function main() {
|
|
|
130
117
|
}
|
|
131
118
|
|
|
132
119
|
if (require.main === module) {
|
|
133
|
-
main().catch(console.error);
|
|
120
|
+
main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
|
|
134
121
|
}
|
|
135
122
|
|
|
136
123
|
module.exports = { main };
|