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,473 +1,505 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Frontend Structure Scanner
|
|
3
|
-
*
|
|
4
|
-
* Scans frontend project directory structure to discover domains.
|
|
5
|
-
* Supports: Angular, Next.js, React, Vue.
|
|
6
|
-
* Includes FSD (Feature-Sliced Design), components scan, and 4-stage fallback.
|
|
7
|
-
* Also provides frontend file count statistics.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const fs = require("fs");
|
|
11
|
-
const path = require("path");
|
|
12
|
-
const { glob } = require("glob");
|
|
13
|
-
|
|
14
|
-
// Project-level override: `.claudeos-scan.json` at project root can extend
|
|
15
|
-
// the defaults. Supported fields (all optional, all additive — never replace
|
|
16
|
-
// defaults):
|
|
17
|
-
// {
|
|
18
|
-
// "frontendScan": {
|
|
19
|
-
// "platformKeywords": ["extra-platform", "custom-tier"],
|
|
20
|
-
// "skipSubappNames": ["my-shared-dir"],
|
|
21
|
-
// "minSubappFiles": 3
|
|
22
|
-
// }
|
|
23
|
-
// }
|
|
24
|
-
// Invalid JSON or missing file: silently falls back to defaults.
|
|
25
|
-
function loadScanOverrides(ROOT) {
|
|
26
|
-
try {
|
|
27
|
-
const content = fs.readFileSync(path.join(ROOT, ".claudeos-scan.json"), "utf-8");
|
|
28
|
-
return JSON.parse(content) || {};
|
|
29
|
-
} catch (_e) {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Build output / cache / generated dirs that should never be scanned for
|
|
35
|
-
// source code. Centralized so platform scan, Fallback E, and future scanners
|
|
36
|
-
// share the same exclusion set.
|
|
37
|
-
const BUILD_IGNORE_DIRS = [
|
|
38
|
-
"**/node_modules/**",
|
|
39
|
-
"**/build/**", "**/dist/**", "**/out/**",
|
|
40
|
-
"**/.next/**", "**/.nuxt/**", "**/.svelte-kit/**", "**/.angular/**",
|
|
41
|
-
"**/.turbo/**", "**/.cache/**", "**/.parcel-cache/**",
|
|
42
|
-
"**/coverage/**", "**/storybook-static/**",
|
|
43
|
-
"**/.vercel/**", "**/.netlify/**",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
// Test / story / type-declaration file globs.
|
|
47
|
-
const TEST_FILE_IGNORE = [
|
|
48
|
-
"**/*.spec.*", "**/*.test.*", "**/*.stories.*",
|
|
49
|
-
"**/*.e2e.*", "**/*.cy.*",
|
|
50
|
-
"**/__snapshots__/**", "**/__tests__/**",
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
// Build a glob prefix from a glob-returned directory path. Glob v10+ strips
|
|
54
|
-
// trailing slashes from results, and on Windows returns backslash paths;
|
|
55
|
-
// without normalization, the pattern `${dir}**/*.tsx` becomes something like
|
|
56
|
-
// `src/foo**/*.tsx` which only matches one level deep (foo/X.tsx) — not
|
|
57
|
-
// nested paths like foo/routes/X.tsx. Ensuring a trailing `/` turns it into
|
|
58
|
-
// `src/foo/**/*.tsx` which matches any depth.
|
|
59
|
-
function dirGlobPrefix(dir) {
|
|
60
|
-
const fwd = dir.replace(/\\/g, "/");
|
|
61
|
-
return fwd.endsWith("/") ? fwd : fwd + "/";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function scanFrontendDomains(stack, ROOT) {
|
|
65
|
-
const frontendDomains = [];
|
|
66
|
-
|
|
67
|
-
// ── Angular ──
|
|
68
|
-
if (stack.frontend === "angular") {
|
|
69
|
-
// Angular feature modules: src/app/*/ with *.component.ts or *.module.ts (+ monorepo apps/*/)
|
|
70
|
-
const angularAppDirs = [
|
|
71
|
-
...await glob("{src/app,app}/*/", { cwd: ROOT }),
|
|
72
|
-
...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
73
|
-
];
|
|
74
|
-
// Skip structural containers (modules/features/pages/views) at the
|
|
75
|
-
// src/app/*/ level — the files INSIDE those containers are the real
|
|
76
|
-
// features, and the Angular deep fallback below extracts them properly.
|
|
77
|
-
const skipAngularDirs = ["shared", "core", "common", "layout", "layouts",
|
|
78
|
-
"environments", "assets", "styles", "testing", "utils",
|
|
79
|
-
"modules", "features", "pages", "views"];
|
|
80
|
-
for (const dir of angularAppDirs) {
|
|
81
|
-
const name = path.basename(dir.replace(/\/$/, ""));
|
|
82
|
-
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
83
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
84
|
-
if (files.length > 0) {
|
|
85
|
-
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
86
|
-
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
87
|
-
const modules = files.filter(f => /\.module\.ts$/.test(f)).length;
|
|
88
|
-
const pipes = files.filter(f => /\.pipe\.ts$/.test(f)).length;
|
|
89
|
-
const directives = files.filter(f => /\.directive\.ts$/.test(f)).length;
|
|
90
|
-
const guards = files.filter(f => /\.guard\.ts$/.test(f)).length;
|
|
91
|
-
frontendDomains.push({ name, type: "frontend", components, services, modules, pipes, directives, guards, totalFiles: files.length });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Angular fallback: scan **/modules/*/ and **/features/*/ with *.component.ts detection
|
|
96
|
-
if (frontendDomains.length === 0) {
|
|
97
|
-
const deepAngularDirs = await glob("**/{modules,features,pages,views}/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**", "**/.angular/**"] });
|
|
98
|
-
for (const dir of deepAngularDirs) {
|
|
99
|
-
const name = path.basename(dir.replace(/\/$/, ""));
|
|
100
|
-
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
101
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
102
|
-
if (files.length >= 2) {
|
|
103
|
-
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
104
|
-
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
105
|
-
frontendDomains.push({ name, type: "frontend", components, services, totalFiles: files.length });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── Next.js/React/Vue ──
|
|
112
|
-
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
113
|
-
// App Router / Pages Router / SPA domains (standard + monorepo + Vite SPA paths)
|
|
114
|
-
const allDirs = [
|
|
115
|
-
...await glob("{app,src/app}/*/", { cwd: ROOT }),
|
|
116
|
-
...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
|
|
117
|
-
...await glob("{apps,packages}/*/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
118
|
-
...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
119
|
-
...await glob("{apps,packages}/*/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
120
|
-
...await glob("{apps,packages}/*/src/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
121
|
-
// Non-standard nested page paths (e.g., src/admin/pages/*, src/dashboard/app/*)
|
|
122
|
-
...await glob("src/*/{app,pages}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
123
|
-
// Vite SPA / CRA common paths (src/views/*, src/screens/*, src/routes/*)
|
|
124
|
-
...await glob("src/{views,screens,routes}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
125
|
-
];
|
|
126
|
-
// Reserved Next.js/Router segments + structural containers that are never
|
|
127
|
-
// real route features. "components"/"hooks"/"widgets" under app/ or
|
|
128
|
-
// pages/ are UI containers handled by dedicated scanners — not routes.
|
|
129
|
-
const skipPages = ["api", "_app", "_document", "fonts", "not-found", "error", "loading",
|
|
130
|
-
"components", "hooks", "widgets", "entities", "features", "modules",
|
|
131
|
-
"lib", "libs", "utils", "util", "config", "types", "shared", "common", "assets"];
|
|
132
|
-
for (const dir of allDirs) {
|
|
133
|
-
const name = path.basename(dir);
|
|
134
|
-
if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
135
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT });
|
|
136
|
-
if (files.length > 0) {
|
|
137
|
-
const pages = files.filter(f => /page\.|index\./.test(f)).length;
|
|
138
|
-
const layouts = files.filter(f => /layout\./.test(f)).length;
|
|
139
|
-
const clientFiles = files.filter(f => /client\./.test(f)).length;
|
|
140
|
-
const serverFiles = pages + layouts;
|
|
141
|
-
const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
|
|
142
|
-
frontendDomains.push({
|
|
143
|
-
name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
|
|
144
|
-
rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
|
|
150
|
-
const fsdLayers = ["features", "widgets", "entities"];
|
|
151
|
-
for (const layer of fsdLayers) {
|
|
152
|
-
const fsdDirs = [...new Set([
|
|
153
|
-
...await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT }),
|
|
154
|
-
...await glob(`src/*/${layer}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
155
|
-
])];
|
|
156
|
-
for (const dir of fsdDirs) {
|
|
157
|
-
const name = path.basename(dir);
|
|
158
|
-
if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
|
|
159
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
160
|
-
if (files.length > 0) {
|
|
161
|
-
const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
|
|
162
|
-
const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
|
|
163
|
-
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// components/* (existing + nested src/*/components/*)
|
|
169
|
-
const compDirSet = new Set([
|
|
170
|
-
...await glob("{src/,}components/*/", { cwd: ROOT }),
|
|
171
|
-
...await glob("src/*/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
172
|
-
]);
|
|
173
|
-
const compDirs = [...compDirSet];
|
|
174
|
-
for (const dir of compDirs) {
|
|
175
|
-
const name = path.basename(dir);
|
|
176
|
-
if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
|
|
177
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,vue}`, { cwd: ROOT });
|
|
178
|
-
if (files.length >= 2) {
|
|
179
|
-
frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── Fallback: extract domains when primary scanners return 0 ──
|
|
184
|
-
if (frontendDomains.length === 0) {
|
|
185
|
-
// Fallback A: Next.js page.tsx / client.tsx based detection
|
|
186
|
-
const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
187
|
-
const domainSet = {};
|
|
188
|
-
const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
|
|
189
|
-
for (const f of pageFiles) {
|
|
190
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
191
|
-
const appIdx = parts.indexOf("app");
|
|
192
|
-
const pagesIdx = parts.indexOf("pages");
|
|
193
|
-
const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
|
|
194
|
-
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
195
|
-
const domain = parts[baseIdx + 1];
|
|
196
|
-
if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith("[") && !domain.startsWith(".")) {
|
|
197
|
-
if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
|
|
198
|
-
domainSet[domain].pages++;
|
|
199
|
-
domainSet[domain].totalFiles++;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
204
|
-
for (const f of clientFiles) {
|
|
205
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
206
|
-
const appIdx = parts.indexOf("app");
|
|
207
|
-
const baseIdx = appIdx >= 0 ? appIdx : -1;
|
|
208
|
-
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
209
|
-
const domain = parts[baseIdx + 1];
|
|
210
|
-
if (domainSet[domain]) {
|
|
211
|
-
domainSet[domain].clientFiles++;
|
|
212
|
-
domainSet[domain].totalFiles++;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const [name, data] of Object.entries(domainSet)) {
|
|
217
|
-
frontendDomains.push({
|
|
218
|
-
name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
|
|
219
|
-
rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Fallback B: FSD widgets/features/entities
|
|
224
|
-
for (const layer of ["widgets", "features", "entities"]) {
|
|
225
|
-
const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
|
|
226
|
-
const layerDomains = {};
|
|
227
|
-
for (const f of layerFiles) {
|
|
228
|
-
const parts = f.replace(/\\/g, "/").split("/");
|
|
229
|
-
const layerIdx = parts.indexOf(layer);
|
|
230
|
-
if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
|
|
231
|
-
const domain = parts[layerIdx + 1];
|
|
232
|
-
if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
|
|
233
|
-
if (!layerDomains[domain]) layerDomains[domain] = 0;
|
|
234
|
-
layerDomains[domain]++;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
for (const [name, count] of Object.entries(layerDomains)) {
|
|
239
|
-
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Fallback C: Deep components/**/components/*/ detection (React/CRA/Vite projects)
|
|
244
|
-
if (frontendDomains.length === 0) {
|
|
245
|
-
const deepCompDirs = await glob("**/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
|
|
246
|
-
const deepDomains = {};
|
|
247
|
-
const skipDomainNames = ["ui", "common", "shared", "layout", "layouts", "icons", "assets", "config", "utils", "lib", "error", "footer", "header", "inputs", "template"];
|
|
248
|
-
for (const dir of deepCompDirs) {
|
|
249
|
-
const name = path.basename(dir.replace(/\/$/, ""));
|
|
250
|
-
if (skipDomainNames.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
251
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
252
|
-
if (files.length >= 2) {
|
|
253
|
-
if (!deepDomains[name]) deepDomains[name] = { components: 0, totalFiles: 0 };
|
|
254
|
-
deepDomains[name].components += files.length;
|
|
255
|
-
deepDomains[name].totalFiles += files.length;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
for (const [name, data] of Object.entries(deepDomains)) {
|
|
259
|
-
frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Fallback D: views/screens/containers/pages/routes deep detection
|
|
264
|
-
if (frontendDomains.length === 0) {
|
|
265
|
-
const domainDirPatterns = ["views", "screens", "containers", "pages", "routes", "modules", "domains"];
|
|
266
|
-
const deepDirDomains = {};
|
|
267
|
-
const skipDirNames = ["api", "auth", "_app", "_document", "index", "ui", "common", "shared",
|
|
268
|
-
"layout", "layouts", "lib", "config", "utils", "assets", "hooks", "store", "types",
|
|
269
|
-
"constants", "helpers", "services", "middleware", "interceptors", "guards", "decorators"];
|
|
270
|
-
|
|
271
|
-
for (const pattern of domainDirPatterns) {
|
|
272
|
-
const dirs = await glob(`**/${pattern}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
|
|
273
|
-
for (const dir of dirs) {
|
|
274
|
-
const name = path.basename(dir.replace(/\/$/, ""));
|
|
275
|
-
if (skipDirNames.includes(name) || name.startsWith("_") || name.startsWith(".") || name.startsWith("(") || name.startsWith("[")) continue;
|
|
276
|
-
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
277
|
-
if (files.length >= 2) {
|
|
278
|
-
if (!deepDirDomains[name]) deepDirDomains[name] = { components: 0, pages: 0, totalFiles: 0, sources: [] };
|
|
279
|
-
const tsx = files.filter(f => /\.(tsx|jsx|vue)$/.test(f)).length;
|
|
280
|
-
deepDirDomains[name].components += tsx;
|
|
281
|
-
deepDirDomains[name].totalFiles += files.length;
|
|
282
|
-
if (!deepDirDomains[name].sources.includes(pattern)) deepDirDomains[name].sources.push(pattern);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
for (const [name, data] of Object.entries(deepDirDomains)) {
|
|
287
|
-
frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles, sources: data.sources });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ── Shared frontend patterns (run for ANY frontend: Angular, Next.js, React, Vue) ──
|
|
295
|
-
//
|
|
296
|
-
// These patterns don't depend on framework-specific file extensions or
|
|
297
|
-
// folder conventions — they look for top-level segmentation by platform or
|
|
298
|
-
// by routes/-file layouts, which appear across all frontend frameworks.
|
|
299
|
-
if (stack.frontend) {
|
|
300
|
-
// Read optional per-project override (.claudeos-scan.json).
|
|
301
|
-
const overrides = loadScanOverrides(ROOT).frontendScan || {};
|
|
302
|
-
// Platform-split layout: src/{platform}/{subapp}/ where platform is a
|
|
303
|
-
// device/target-environment OR access-tier keyword. Both form the same
|
|
304
|
-
// structural pattern (top-level segmentation with a common subapp layout).
|
|
305
|
-
// Subapp name comes from the filesystem at scan time via path.basename —
|
|
306
|
-
// no project-specific names are hardcoded.
|
|
307
|
-
// Produces one domain per (platform, subapp) pair, named `{platform}-{subapp}`.
|
|
308
|
-
// NOTE: 3-letter+ access-tier names only. The short `adm` abbreviation
|
|
309
|
-
// is deliberately excluded — too ambiguous in isolation and false-positive
|
|
310
|
-
// risk isn't worth the small convenience gain. If a project uses `src/adm/`
|
|
311
|
-
// as an admin tier root, rename to `admin` or add `"adm"` to
|
|
312
|
-
// `frontendScan.platformKeywords` in `.claudeos-scan.json`.
|
|
313
|
-
const DEFAULT_PLATFORM_KEYWORDS = [
|
|
314
|
-
// device / target-environment
|
|
315
|
-
"desktop", "pc", "web",
|
|
316
|
-
"mobile", "mc", "mo", "sp",
|
|
317
|
-
"tablet", "tab",
|
|
318
|
-
"pwa",
|
|
319
|
-
"tv", "ctv", "ott",
|
|
320
|
-
"watch", "wear",
|
|
321
|
-
// access-tier / audience
|
|
322
|
-
"admin", "cms", "backoffice", "back-office", "portal",
|
|
323
|
-
];
|
|
324
|
-
const PLATFORM_KEYWORDS = [
|
|
325
|
-
...DEFAULT_PLATFORM_KEYWORDS,
|
|
326
|
-
...(Array.isArray(overrides.platformKeywords) ? overrides.platformKeywords : []),
|
|
327
|
-
];
|
|
328
|
-
// Minimum source files to qualify as a subapp. A single-file directory
|
|
329
|
-
// under a platform root is almost always an accidental fixture or a
|
|
330
|
-
// placeholder, not a real subapp. Raising the floor avoids noisy
|
|
331
|
-
// 1-file "domains" in the Pass 1 group plan.
|
|
332
|
-
const MIN_SUBAPP_FILES = typeof overrides.minSubappFiles === "number" && overrides.minSubappFiles >= 1
|
|
333
|
-
? overrides.minSubappFiles
|
|
334
|
-
: 2;
|
|
335
|
-
// Conservative skip list — never-a-feature names at the subapp level.
|
|
336
|
-
// Includes infrastructure dirs, structural dirs that other scanners
|
|
337
|
-
// already handle (components/hooks/layouts), FSD layer names, and
|
|
338
|
-
// framework router dirs. Patterns like `src/admin/pages/*/` and
|
|
339
|
-
// `src/admin/components/*/` fall through to the App/Pages Router and
|
|
340
|
-
// components scanners instead of being captured as bare subapps.
|
|
341
|
-
// `store`/`stores` are deliberately NOT skipped — e-commerce projects
|
|
342
|
-
// legitimately use them as subapp names.
|
|
343
|
-
const DEFAULT_SKIP_SUBAPP_NAMES = [
|
|
344
|
-
// infrastructure
|
|
345
|
-
"assets", "common", "shared", "utils", "util",
|
|
346
|
-
"lib", "libs", "config", "constants", "helpers", "types",
|
|
347
|
-
"test", "tests", "__mocks__", "mocks", "__tests__",
|
|
348
|
-
// structural (handled by dedicated scanners at a deeper level)
|
|
349
|
-
"components", "hooks", "layouts", "layout",
|
|
350
|
-
// FSD layers (handled by FSD scanner)
|
|
351
|
-
"widgets", "features", "entities",
|
|
352
|
-
// framework router dirs (handled by App/Pages Router scanner + fallback D)
|
|
353
|
-
"app", "pages", "routes", "views", "screens", "containers",
|
|
354
|
-
"modules", "domains",
|
|
355
|
-
];
|
|
356
|
-
const SKIP_SUBAPP_NAMES = [
|
|
357
|
-
...DEFAULT_SKIP_SUBAPP_NAMES,
|
|
358
|
-
...(Array.isArray(overrides.skipSubappNames) ? overrides.skipSubappNames : []),
|
|
359
|
-
];
|
|
360
|
-
// Match both standalone projects (src/{platform}/{subapp}/) and monorepo
|
|
361
|
-
// workspaces ({apps,packages}/*/src/{platform}/{subapp}/ and
|
|
362
|
-
// {apps,packages}/{platform}/{subapp}/ — some monorepos skip the src/ wrapper).
|
|
363
|
-
const platformGlobs = [
|
|
364
|
-
`src/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
365
|
-
`{apps,packages}/*/src/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
366
|
-
`{apps,packages}/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
367
|
-
];
|
|
368
|
-
const platformDirs = [];
|
|
369
|
-
for (const p of platformGlobs) {
|
|
370
|
-
const dirs = await glob(p, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
371
|
-
platformDirs.push(...dirs);
|
|
372
|
-
}
|
|
373
|
-
// Dedupe (the three globs can produce overlapping matches in some layouts)
|
|
374
|
-
const seenPlatformDirs = new Set();
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Frontend Structure Scanner
|
|
3
|
+
*
|
|
4
|
+
* Scans frontend project directory structure to discover domains.
|
|
5
|
+
* Supports: Angular, Next.js, React, Vue.
|
|
6
|
+
* Includes FSD (Feature-Sliced Design), components scan, and 4-stage fallback.
|
|
7
|
+
* Also provides frontend file count statistics.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const { glob } = require("glob");
|
|
13
|
+
|
|
14
|
+
// Project-level override: `.claudeos-scan.json` at project root can extend
|
|
15
|
+
// the defaults. Supported fields (all optional, all additive — never replace
|
|
16
|
+
// defaults):
|
|
17
|
+
// {
|
|
18
|
+
// "frontendScan": {
|
|
19
|
+
// "platformKeywords": ["extra-platform", "custom-tier"],
|
|
20
|
+
// "skipSubappNames": ["my-shared-dir"],
|
|
21
|
+
// "minSubappFiles": 3
|
|
22
|
+
// }
|
|
23
|
+
// }
|
|
24
|
+
// Invalid JSON or missing file: silently falls back to defaults.
|
|
25
|
+
function loadScanOverrides(ROOT) {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(path.join(ROOT, ".claudeos-scan.json"), "utf-8");
|
|
28
|
+
return JSON.parse(content) || {};
|
|
29
|
+
} catch (_e) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Build output / cache / generated dirs that should never be scanned for
|
|
35
|
+
// source code. Centralized so platform scan, Fallback E, and future scanners
|
|
36
|
+
// share the same exclusion set.
|
|
37
|
+
const BUILD_IGNORE_DIRS = [
|
|
38
|
+
"**/node_modules/**",
|
|
39
|
+
"**/build/**", "**/dist/**", "**/out/**",
|
|
40
|
+
"**/.next/**", "**/.nuxt/**", "**/.svelte-kit/**", "**/.angular/**",
|
|
41
|
+
"**/.turbo/**", "**/.cache/**", "**/.parcel-cache/**",
|
|
42
|
+
"**/coverage/**", "**/storybook-static/**",
|
|
43
|
+
"**/.vercel/**", "**/.netlify/**",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Test / story / type-declaration file globs.
|
|
47
|
+
const TEST_FILE_IGNORE = [
|
|
48
|
+
"**/*.spec.*", "**/*.test.*", "**/*.stories.*",
|
|
49
|
+
"**/*.e2e.*", "**/*.cy.*",
|
|
50
|
+
"**/__snapshots__/**", "**/__tests__/**",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Build a glob prefix from a glob-returned directory path. Glob v10+ strips
|
|
54
|
+
// trailing slashes from results, and on Windows returns backslash paths;
|
|
55
|
+
// without normalization, the pattern `${dir}**/*.tsx` becomes something like
|
|
56
|
+
// `src/foo**/*.tsx` which only matches one level deep (foo/X.tsx) — not
|
|
57
|
+
// nested paths like foo/routes/X.tsx. Ensuring a trailing `/` turns it into
|
|
58
|
+
// `src/foo/**/*.tsx` which matches any depth.
|
|
59
|
+
function dirGlobPrefix(dir) {
|
|
60
|
+
const fwd = dir.replace(/\\/g, "/");
|
|
61
|
+
return fwd.endsWith("/") ? fwd : fwd + "/";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function scanFrontendDomains(stack, ROOT) {
|
|
65
|
+
const frontendDomains = [];
|
|
66
|
+
|
|
67
|
+
// ── Angular ──
|
|
68
|
+
if (stack.frontend === "angular") {
|
|
69
|
+
// Angular feature modules: src/app/*/ with *.component.ts or *.module.ts (+ monorepo apps/*/)
|
|
70
|
+
const angularAppDirs = [
|
|
71
|
+
...await glob("{src/app,app}/*/", { cwd: ROOT }),
|
|
72
|
+
...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
73
|
+
];
|
|
74
|
+
// Skip structural containers (modules/features/pages/views) at the
|
|
75
|
+
// src/app/*/ level — the files INSIDE those containers are the real
|
|
76
|
+
// features, and the Angular deep fallback below extracts them properly.
|
|
77
|
+
const skipAngularDirs = ["shared", "core", "common", "layout", "layouts",
|
|
78
|
+
"environments", "assets", "styles", "testing", "utils",
|
|
79
|
+
"modules", "features", "pages", "views"];
|
|
80
|
+
for (const dir of angularAppDirs) {
|
|
81
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
82
|
+
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
83
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
84
|
+
if (files.length > 0) {
|
|
85
|
+
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
86
|
+
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
87
|
+
const modules = files.filter(f => /\.module\.ts$/.test(f)).length;
|
|
88
|
+
const pipes = files.filter(f => /\.pipe\.ts$/.test(f)).length;
|
|
89
|
+
const directives = files.filter(f => /\.directive\.ts$/.test(f)).length;
|
|
90
|
+
const guards = files.filter(f => /\.guard\.ts$/.test(f)).length;
|
|
91
|
+
frontendDomains.push({ name, type: "frontend", components, services, modules, pipes, directives, guards, totalFiles: files.length });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Angular fallback: scan **/modules/*/ and **/features/*/ with *.component.ts detection
|
|
96
|
+
if (frontendDomains.length === 0) {
|
|
97
|
+
const deepAngularDirs = await glob("**/{modules,features,pages,views}/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**", "**/.angular/**"] });
|
|
98
|
+
for (const dir of deepAngularDirs) {
|
|
99
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
100
|
+
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
101
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
102
|
+
if (files.length >= 2) {
|
|
103
|
+
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
104
|
+
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
105
|
+
frontendDomains.push({ name, type: "frontend", components, services, totalFiles: files.length });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Next.js/React/Vue ──
|
|
112
|
+
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
113
|
+
// App Router / Pages Router / SPA domains (standard + monorepo + Vite SPA paths)
|
|
114
|
+
const allDirs = [
|
|
115
|
+
...await glob("{app,src/app}/*/", { cwd: ROOT }),
|
|
116
|
+
...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
|
|
117
|
+
...await glob("{apps,packages}/*/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
118
|
+
...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
119
|
+
...await glob("{apps,packages}/*/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
120
|
+
...await glob("{apps,packages}/*/src/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
121
|
+
// Non-standard nested page paths (e.g., src/admin/pages/*, src/dashboard/app/*)
|
|
122
|
+
...await glob("src/*/{app,pages}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
123
|
+
// Vite SPA / CRA common paths (src/views/*, src/screens/*, src/routes/*)
|
|
124
|
+
...await glob("src/{views,screens,routes}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
125
|
+
];
|
|
126
|
+
// Reserved Next.js/Router segments + structural containers that are never
|
|
127
|
+
// real route features. "components"/"hooks"/"widgets" under app/ or
|
|
128
|
+
// pages/ are UI containers handled by dedicated scanners — not routes.
|
|
129
|
+
const skipPages = ["api", "_app", "_document", "fonts", "not-found", "error", "loading",
|
|
130
|
+
"components", "hooks", "widgets", "entities", "features", "modules",
|
|
131
|
+
"lib", "libs", "utils", "util", "config", "types", "shared", "common", "assets"];
|
|
132
|
+
for (const dir of allDirs) {
|
|
133
|
+
const name = path.basename(dir);
|
|
134
|
+
if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
135
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT });
|
|
136
|
+
if (files.length > 0) {
|
|
137
|
+
const pages = files.filter(f => /page\.|index\./.test(f)).length;
|
|
138
|
+
const layouts = files.filter(f => /layout\./.test(f)).length;
|
|
139
|
+
const clientFiles = files.filter(f => /client\./.test(f)).length;
|
|
140
|
+
const serverFiles = pages + layouts;
|
|
141
|
+
const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
|
|
142
|
+
frontendDomains.push({
|
|
143
|
+
name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
|
|
144
|
+
rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
|
|
150
|
+
const fsdLayers = ["features", "widgets", "entities"];
|
|
151
|
+
for (const layer of fsdLayers) {
|
|
152
|
+
const fsdDirs = [...new Set([
|
|
153
|
+
...await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT }),
|
|
154
|
+
...await glob(`src/*/${layer}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
155
|
+
])];
|
|
156
|
+
for (const dir of fsdDirs) {
|
|
157
|
+
const name = path.basename(dir);
|
|
158
|
+
if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
|
|
159
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
160
|
+
if (files.length > 0) {
|
|
161
|
+
const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
|
|
162
|
+
const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
|
|
163
|
+
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// components/* (existing + nested src/*/components/*)
|
|
169
|
+
const compDirSet = new Set([
|
|
170
|
+
...await glob("{src/,}components/*/", { cwd: ROOT }),
|
|
171
|
+
...await glob("src/*/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
172
|
+
]);
|
|
173
|
+
const compDirs = [...compDirSet];
|
|
174
|
+
for (const dir of compDirs) {
|
|
175
|
+
const name = path.basename(dir);
|
|
176
|
+
if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
|
|
177
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,vue}`, { cwd: ROOT });
|
|
178
|
+
if (files.length >= 2) {
|
|
179
|
+
frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Fallback: extract domains when primary scanners return 0 ──
|
|
184
|
+
if (frontendDomains.length === 0) {
|
|
185
|
+
// Fallback A: Next.js page.tsx / client.tsx based detection
|
|
186
|
+
const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
187
|
+
const domainSet = {};
|
|
188
|
+
const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
|
|
189
|
+
for (const f of pageFiles) {
|
|
190
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
191
|
+
const appIdx = parts.indexOf("app");
|
|
192
|
+
const pagesIdx = parts.indexOf("pages");
|
|
193
|
+
const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
|
|
194
|
+
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
195
|
+
const domain = parts[baseIdx + 1];
|
|
196
|
+
if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith("[") && !domain.startsWith(".")) {
|
|
197
|
+
if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
|
|
198
|
+
domainSet[domain].pages++;
|
|
199
|
+
domainSet[domain].totalFiles++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
204
|
+
for (const f of clientFiles) {
|
|
205
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
206
|
+
const appIdx = parts.indexOf("app");
|
|
207
|
+
const baseIdx = appIdx >= 0 ? appIdx : -1;
|
|
208
|
+
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
209
|
+
const domain = parts[baseIdx + 1];
|
|
210
|
+
if (domainSet[domain]) {
|
|
211
|
+
domainSet[domain].clientFiles++;
|
|
212
|
+
domainSet[domain].totalFiles++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const [name, data] of Object.entries(domainSet)) {
|
|
217
|
+
frontendDomains.push({
|
|
218
|
+
name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
|
|
219
|
+
rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fallback B: FSD widgets/features/entities
|
|
224
|
+
for (const layer of ["widgets", "features", "entities"]) {
|
|
225
|
+
const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
|
|
226
|
+
const layerDomains = {};
|
|
227
|
+
for (const f of layerFiles) {
|
|
228
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
229
|
+
const layerIdx = parts.indexOf(layer);
|
|
230
|
+
if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
|
|
231
|
+
const domain = parts[layerIdx + 1];
|
|
232
|
+
if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
|
|
233
|
+
if (!layerDomains[domain]) layerDomains[domain] = 0;
|
|
234
|
+
layerDomains[domain]++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const [name, count] of Object.entries(layerDomains)) {
|
|
239
|
+
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Fallback C: Deep components/**/components/*/ detection (React/CRA/Vite projects)
|
|
244
|
+
if (frontendDomains.length === 0) {
|
|
245
|
+
const deepCompDirs = await glob("**/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
|
|
246
|
+
const deepDomains = {};
|
|
247
|
+
const skipDomainNames = ["ui", "common", "shared", "layout", "layouts", "icons", "assets", "config", "utils", "lib", "error", "footer", "header", "inputs", "template"];
|
|
248
|
+
for (const dir of deepCompDirs) {
|
|
249
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
250
|
+
if (skipDomainNames.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
251
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
252
|
+
if (files.length >= 2) {
|
|
253
|
+
if (!deepDomains[name]) deepDomains[name] = { components: 0, totalFiles: 0 };
|
|
254
|
+
deepDomains[name].components += files.length;
|
|
255
|
+
deepDomains[name].totalFiles += files.length;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const [name, data] of Object.entries(deepDomains)) {
|
|
259
|
+
frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Fallback D: views/screens/containers/pages/routes deep detection
|
|
264
|
+
if (frontendDomains.length === 0) {
|
|
265
|
+
const domainDirPatterns = ["views", "screens", "containers", "pages", "routes", "modules", "domains"];
|
|
266
|
+
const deepDirDomains = {};
|
|
267
|
+
const skipDirNames = ["api", "auth", "_app", "_document", "index", "ui", "common", "shared",
|
|
268
|
+
"layout", "layouts", "lib", "config", "utils", "assets", "hooks", "store", "types",
|
|
269
|
+
"constants", "helpers", "services", "middleware", "interceptors", "guards", "decorators"];
|
|
270
|
+
|
|
271
|
+
for (const pattern of domainDirPatterns) {
|
|
272
|
+
const dirs = await glob(`**/${pattern}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
|
|
273
|
+
for (const dir of dirs) {
|
|
274
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
275
|
+
if (skipDirNames.includes(name) || name.startsWith("_") || name.startsWith(".") || name.startsWith("(") || name.startsWith("[")) continue;
|
|
276
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
277
|
+
if (files.length >= 2) {
|
|
278
|
+
if (!deepDirDomains[name]) deepDirDomains[name] = { components: 0, pages: 0, totalFiles: 0, sources: [] };
|
|
279
|
+
const tsx = files.filter(f => /\.(tsx|jsx|vue)$/.test(f)).length;
|
|
280
|
+
deepDirDomains[name].components += tsx;
|
|
281
|
+
deepDirDomains[name].totalFiles += files.length;
|
|
282
|
+
if (!deepDirDomains[name].sources.includes(pattern)) deepDirDomains[name].sources.push(pattern);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const [name, data] of Object.entries(deepDirDomains)) {
|
|
287
|
+
frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles, sources: data.sources });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Shared frontend patterns (run for ANY frontend: Angular, Next.js, React, Vue) ──
|
|
295
|
+
//
|
|
296
|
+
// These patterns don't depend on framework-specific file extensions or
|
|
297
|
+
// folder conventions — they look for top-level segmentation by platform or
|
|
298
|
+
// by routes/-file layouts, which appear across all frontend frameworks.
|
|
299
|
+
if (stack.frontend) {
|
|
300
|
+
// Read optional per-project override (.claudeos-scan.json).
|
|
301
|
+
const overrides = loadScanOverrides(ROOT).frontendScan || {};
|
|
302
|
+
// Platform-split layout: src/{platform}/{subapp}/ where platform is a
|
|
303
|
+
// device/target-environment OR access-tier keyword. Both form the same
|
|
304
|
+
// structural pattern (top-level segmentation with a common subapp layout).
|
|
305
|
+
// Subapp name comes from the filesystem at scan time via path.basename —
|
|
306
|
+
// no project-specific names are hardcoded.
|
|
307
|
+
// Produces one domain per (platform, subapp) pair, named `{platform}-{subapp}`.
|
|
308
|
+
// NOTE: 3-letter+ access-tier names only. The short `adm` abbreviation
|
|
309
|
+
// is deliberately excluded — too ambiguous in isolation and false-positive
|
|
310
|
+
// risk isn't worth the small convenience gain. If a project uses `src/adm/`
|
|
311
|
+
// as an admin tier root, rename to `admin` or add `"adm"` to
|
|
312
|
+
// `frontendScan.platformKeywords` in `.claudeos-scan.json`.
|
|
313
|
+
const DEFAULT_PLATFORM_KEYWORDS = [
|
|
314
|
+
// device / target-environment
|
|
315
|
+
"desktop", "pc", "web",
|
|
316
|
+
"mobile", "mc", "mo", "sp",
|
|
317
|
+
"tablet", "tab",
|
|
318
|
+
"pwa",
|
|
319
|
+
"tv", "ctv", "ott",
|
|
320
|
+
"watch", "wear",
|
|
321
|
+
// access-tier / audience
|
|
322
|
+
"admin", "cms", "backoffice", "back-office", "portal",
|
|
323
|
+
];
|
|
324
|
+
const PLATFORM_KEYWORDS = [
|
|
325
|
+
...DEFAULT_PLATFORM_KEYWORDS,
|
|
326
|
+
...(Array.isArray(overrides.platformKeywords) ? overrides.platformKeywords : []),
|
|
327
|
+
];
|
|
328
|
+
// Minimum source files to qualify as a subapp. A single-file directory
|
|
329
|
+
// under a platform root is almost always an accidental fixture or a
|
|
330
|
+
// placeholder, not a real subapp. Raising the floor avoids noisy
|
|
331
|
+
// 1-file "domains" in the Pass 1 group plan.
|
|
332
|
+
const MIN_SUBAPP_FILES = typeof overrides.minSubappFiles === "number" && overrides.minSubappFiles >= 1
|
|
333
|
+
? overrides.minSubappFiles
|
|
334
|
+
: 2;
|
|
335
|
+
// Conservative skip list — never-a-feature names at the subapp level.
|
|
336
|
+
// Includes infrastructure dirs, structural dirs that other scanners
|
|
337
|
+
// already handle (components/hooks/layouts), FSD layer names, and
|
|
338
|
+
// framework router dirs. Patterns like `src/admin/pages/*/` and
|
|
339
|
+
// `src/admin/components/*/` fall through to the App/Pages Router and
|
|
340
|
+
// components scanners instead of being captured as bare subapps.
|
|
341
|
+
// `store`/`stores` are deliberately NOT skipped — e-commerce projects
|
|
342
|
+
// legitimately use them as subapp names.
|
|
343
|
+
const DEFAULT_SKIP_SUBAPP_NAMES = [
|
|
344
|
+
// infrastructure
|
|
345
|
+
"assets", "common", "shared", "utils", "util",
|
|
346
|
+
"lib", "libs", "config", "constants", "helpers", "types",
|
|
347
|
+
"test", "tests", "__mocks__", "mocks", "__tests__",
|
|
348
|
+
// structural (handled by dedicated scanners at a deeper level)
|
|
349
|
+
"components", "hooks", "layouts", "layout",
|
|
350
|
+
// FSD layers (handled by FSD scanner)
|
|
351
|
+
"widgets", "features", "entities",
|
|
352
|
+
// framework router dirs (handled by App/Pages Router scanner + fallback D)
|
|
353
|
+
"app", "pages", "routes", "views", "screens", "containers",
|
|
354
|
+
"modules", "domains",
|
|
355
|
+
];
|
|
356
|
+
const SKIP_SUBAPP_NAMES = [
|
|
357
|
+
...DEFAULT_SKIP_SUBAPP_NAMES,
|
|
358
|
+
...(Array.isArray(overrides.skipSubappNames) ? overrides.skipSubappNames : []),
|
|
359
|
+
];
|
|
360
|
+
// Match both standalone projects (src/{platform}/{subapp}/) and monorepo
|
|
361
|
+
// workspaces ({apps,packages}/*/src/{platform}/{subapp}/ and
|
|
362
|
+
// {apps,packages}/{platform}/{subapp}/ — some monorepos skip the src/ wrapper).
|
|
363
|
+
const platformGlobs = [
|
|
364
|
+
`src/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
365
|
+
`{apps,packages}/*/src/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
366
|
+
`{apps,packages}/{${PLATFORM_KEYWORDS.join(",")}}/*/`,
|
|
367
|
+
];
|
|
368
|
+
const platformDirs = [];
|
|
369
|
+
for (const p of platformGlobs) {
|
|
370
|
+
const dirs = await glob(p, { cwd: ROOT, ignore: ["**/node_modules/**"] });
|
|
371
|
+
platformDirs.push(...dirs);
|
|
372
|
+
}
|
|
373
|
+
// Dedupe (the three globs can produce overlapping matches in some layouts)
|
|
374
|
+
const seenPlatformDirs = new Set();
|
|
375
|
+
|
|
376
|
+
// v2.3.0 — Single-platform skip rule.
|
|
377
|
+
// Scanner designed for dual-platform layouts where the same subapp
|
|
378
|
+
// is implemented twice (e.g., `src/pc/admin/` + `src/mobile/admin/`).
|
|
379
|
+
// When platform keywords match only ONE distinct root — as in a
|
|
380
|
+
// single-SPA project with `src/admin/` but no sibling platform —
|
|
381
|
+
// the "subapps" underneath are not subapps at all; they are
|
|
382
|
+
// internal layers of one SPA (api, context, dto, routers, …).
|
|
383
|
+
// Emitting those as domains fragments one logical app into 5+
|
|
384
|
+
// pseudo-domains, which in turn primes Pass 3 to fabricate
|
|
385
|
+
// prefixed filenames (featureRoutePath.ts, admin-api.service.ts) —
|
|
386
|
+
// the hallucination class observed in frontend-react-B dogfooding.
|
|
387
|
+
//
|
|
388
|
+
// Detection of "single-SPA" mode: inspect the top-level platform
|
|
389
|
+
// segment for every glob match and count distinct values. If only
|
|
390
|
+
// one platform is present, skip subapp emission entirely and let
|
|
391
|
+
// the downstream scanners (routes/pages/components/FSD) identify
|
|
392
|
+
// real feature domains inside that single SPA.
|
|
393
|
+
//
|
|
394
|
+
// Overrides: `frontendScan.forceSubappSplit` in .claudeos-scan.json
|
|
395
|
+
// opts back into the old behavior for projects that intentionally
|
|
396
|
+
// treat their lone platform's children as feature domains.
|
|
397
|
+
const distinctPlatforms = new Set();
|
|
398
|
+
for (const dir of platformDirs) {
|
|
399
|
+
const dirFwd = dir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
400
|
+
const parts = dirFwd.split("/");
|
|
401
|
+
const platformIdx = parts.findIndex(seg => PLATFORM_KEYWORDS.includes(seg));
|
|
402
|
+
if (platformIdx >= 0) distinctPlatforms.add(parts[platformIdx]);
|
|
403
|
+
}
|
|
404
|
+
const forceSubappSplit = overrides.forceSubappSplit === true;
|
|
405
|
+
const singleSpaMode = distinctPlatforms.size <= 1 && !forceSubappSplit;
|
|
406
|
+
|
|
407
|
+
for (const dir of (singleSpaMode ? [] : platformDirs)) {
|
|
408
|
+
const dirFwd = dir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
409
|
+
if (seenPlatformDirs.has(dirFwd)) continue;
|
|
410
|
+
seenPlatformDirs.add(dirFwd);
|
|
411
|
+
const parts = dirFwd.split("/");
|
|
412
|
+
// Locate platform segment: the FIRST segment that matches a keyword.
|
|
413
|
+
// findIndex (not findLast) — if the subapp name also happens to be a
|
|
414
|
+
// keyword (e.g., `src/pc/admin/`), the subapp should stay as the
|
|
415
|
+
// second match, not be mistaken for the platform segment.
|
|
416
|
+
// Paths handled: src/<p>/<s>, apps/<workspace>/src/<p>/<s>, apps/<p>/<s>.
|
|
417
|
+
const platformIdx = parts.findIndex(seg => PLATFORM_KEYWORDS.includes(seg));
|
|
418
|
+
if (platformIdx < 0 || platformIdx + 1 >= parts.length) continue;
|
|
419
|
+
const platform = parts[platformIdx];
|
|
420
|
+
const subapp = parts[platformIdx + 1];
|
|
421
|
+
if (!subapp || SKIP_SUBAPP_NAMES.includes(subapp) || subapp.startsWith("_") || subapp.startsWith(".")) continue;
|
|
422
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, {
|
|
423
|
+
cwd: ROOT,
|
|
424
|
+
ignore: [...BUILD_IGNORE_DIRS, ...TEST_FILE_IGNORE],
|
|
425
|
+
});
|
|
426
|
+
if (files.length < MIN_SUBAPP_FILES) continue;
|
|
427
|
+
// Normalize Windows backslashes so the segment regex works cross-platform.
|
|
428
|
+
const filesFwd = files.map(f => f.replace(/\\/g, "/"));
|
|
429
|
+
const routes = filesFwd.filter(f => /\/routes\//.test(f)).length;
|
|
430
|
+
const components = filesFwd.filter(f => /\/components\//.test(f)).length;
|
|
431
|
+
const layouts = filesFwd.filter(f => /\/layouts?\//.test(f)).length;
|
|
432
|
+
const hooks = filesFwd.filter(f => /\/hooks\//.test(f)).length;
|
|
433
|
+
frontendDomains.push({
|
|
434
|
+
name: `${platform}-${subapp}`,
|
|
435
|
+
type: "frontend",
|
|
436
|
+
platform,
|
|
437
|
+
subapp,
|
|
438
|
+
routes, components, layouts, hooks,
|
|
439
|
+
totalFiles: files.length,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Fallback E: React Router file-routing (any depth). Groups by the
|
|
444
|
+
// parent dir of `routes/`. Domain name = parent basename. Covers
|
|
445
|
+
// CRA/Vite + React Router projects that don't match Next.js page.tsx
|
|
446
|
+
// or FSD layout. Only fires when every other primary and fallback
|
|
447
|
+
// scanner returned 0 domains.
|
|
448
|
+
if (frontendDomains.length === 0) {
|
|
449
|
+
const routeFiles = await glob("**/routes/*.{tsx,jsx,ts,js,vue}", {
|
|
450
|
+
cwd: ROOT,
|
|
451
|
+
ignore: [...BUILD_IGNORE_DIRS, ...TEST_FILE_IGNORE],
|
|
452
|
+
});
|
|
453
|
+
const routeDomains = {};
|
|
454
|
+
const skipParents = ["src", "app", "pages", "", "."];
|
|
455
|
+
for (const f of routeFiles) {
|
|
456
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
457
|
+
const routesIdx = parts.lastIndexOf("routes");
|
|
458
|
+
if (routesIdx < 1) continue;
|
|
459
|
+
const parent = parts[routesIdx - 1];
|
|
460
|
+
if (skipParents.includes(parent) || parent.startsWith("_") || parent.startsWith(".")) continue;
|
|
461
|
+
if (!routeDomains[parent]) routeDomains[parent] = { routes: 0, totalFiles: 0 };
|
|
462
|
+
routeDomains[parent].routes++;
|
|
463
|
+
routeDomains[parent].totalFiles++;
|
|
464
|
+
}
|
|
465
|
+
for (const [name, data] of Object.entries(routeDomains)) {
|
|
466
|
+
frontendDomains.push({ name, type: "frontend", routes: data.routes, totalFiles: data.totalFiles });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { frontendDomains };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── Frontend file count statistics ──
|
|
475
|
+
async function countFrontendStats(stack, ROOT) {
|
|
476
|
+
const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
|
|
477
|
+
if (stack.frontend) {
|
|
478
|
+
frontend.exists = true;
|
|
479
|
+
if (stack.frontend === "angular") {
|
|
480
|
+
frontend.components = (await glob("{src/,}**/*.component.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
|
|
481
|
+
frontend.pages = (await glob("{src/,}**/*.module.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
|
|
482
|
+
frontend.hooks = (await glob("{src/,}**/*.service.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
|
|
483
|
+
} else {
|
|
484
|
+
frontend.components = (await glob("{src/,}**/components/**/*.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
485
|
+
frontend.pages = (await glob("{src/,}{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length
|
|
486
|
+
+ (await glob("src/*/{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
487
|
+
frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// App Router RSC/Client overall stats (for project-analysis.json)
|
|
492
|
+
if (stack.frontend === "nextjs") {
|
|
493
|
+
const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
494
|
+
const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
495
|
+
const allLayoutFiles = await glob("{app,src/app}/**/layout.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
496
|
+
frontend.clientComponents = allClientFiles.length;
|
|
497
|
+
frontend.serverPages = allPageFiles.length;
|
|
498
|
+
frontend.layouts = allLayoutFiles.length;
|
|
499
|
+
frontend.rscPattern = allClientFiles.length > 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return frontend;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports = { scanFrontendDomains, countFrontendStats };
|