claudeos-core 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/CONTRIBUTING.md +92 -59
- package/README.de.md +465 -240
- package/README.es.md +446 -223
- package/README.fr.md +461 -238
- package/README.hi.md +485 -261
- package/README.ja.md +440 -235
- package/README.ko.md +244 -56
- package/README.md +215 -47
- package/README.ru.md +462 -238
- package/README.vi.md +454 -230
- package/README.zh-CN.md +476 -252
- package/bin/cli.js +144 -140
- package/bin/commands/init.js +549 -45
- package/bin/commands/memory.js +426 -0
- package/bin/lib/cli-utils.js +206 -143
- package/bootstrap.sh +81 -390
- package/content-validator/index.js +436 -340
- package/lib/expected-guides.js +23 -0
- package/lib/expected-outputs.js +91 -0
- package/lib/language-config.js +35 -0
- package/lib/memory-scaffold.js +1014 -0
- package/lib/plan-parser.js +153 -149
- package/lib/staged-rules.js +118 -0
- package/manifest-generator/index.js +176 -171
- package/package.json +1 -1
- package/pass-json-validator/index.js +337 -299
- package/pass-prompts/templates/common/pass3-footer.md +16 -0
- package/pass-prompts/templates/common/pass4.md +317 -0
- package/pass-prompts/templates/common/staging-override.md +26 -0
- package/plan-installer/prompt-generator.js +120 -96
- package/plan-installer/scanners/scan-frontend.js +216 -9
- package/sync-checker/index.js +133 -132
|
@@ -7,9 +7,60 @@
|
|
|
7
7
|
* Also provides frontend file count statistics.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const fs = require("fs");
|
|
10
11
|
const path = require("path");
|
|
11
12
|
const { glob } = require("glob");
|
|
12
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
|
+
|
|
13
64
|
async function scanFrontendDomains(stack, ROOT) {
|
|
14
65
|
const frontendDomains = [];
|
|
15
66
|
|
|
@@ -20,11 +71,16 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
20
71
|
...await glob("{src/app,app}/*/", { cwd: ROOT }),
|
|
21
72
|
...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
22
73
|
];
|
|
23
|
-
|
|
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"];
|
|
24
80
|
for (const dir of angularAppDirs) {
|
|
25
81
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
26
82
|
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
27
|
-
const files = await glob(`${dir
|
|
83
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
28
84
|
if (files.length > 0) {
|
|
29
85
|
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
30
86
|
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
@@ -42,7 +98,7 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
42
98
|
for (const dir of deepAngularDirs) {
|
|
43
99
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
44
100
|
if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
45
|
-
const files = await glob(`${dir
|
|
101
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
|
|
46
102
|
if (files.length >= 2) {
|
|
47
103
|
const components = files.filter(f => /\.component\.ts$/.test(f)).length;
|
|
48
104
|
const services = files.filter(f => /\.service\.ts$/.test(f)).length;
|
|
@@ -67,11 +123,16 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
67
123
|
// Vite SPA / CRA common paths (src/views/*, src/screens/*, src/routes/*)
|
|
68
124
|
...await glob("src/{views,screens,routes}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
|
|
69
125
|
];
|
|
70
|
-
|
|
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"];
|
|
71
132
|
for (const dir of allDirs) {
|
|
72
133
|
const name = path.basename(dir);
|
|
73
134
|
if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
74
|
-
const files = await glob(`${dir
|
|
135
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT });
|
|
75
136
|
if (files.length > 0) {
|
|
76
137
|
const pages = files.filter(f => /page\.|index\./.test(f)).length;
|
|
77
138
|
const layouts = files.filter(f => /layout\./.test(f)).length;
|
|
@@ -95,7 +156,7 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
95
156
|
for (const dir of fsdDirs) {
|
|
96
157
|
const name = path.basename(dir);
|
|
97
158
|
if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
|
|
98
|
-
const files = await glob(`${dir
|
|
159
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
99
160
|
if (files.length > 0) {
|
|
100
161
|
const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
|
|
101
162
|
const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
|
|
@@ -113,7 +174,7 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
113
174
|
for (const dir of compDirs) {
|
|
114
175
|
const name = path.basename(dir);
|
|
115
176
|
if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
|
|
116
|
-
const files = await glob(`${dir
|
|
177
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,vue}`, { cwd: ROOT });
|
|
117
178
|
if (files.length >= 2) {
|
|
118
179
|
frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
|
|
119
180
|
}
|
|
@@ -187,7 +248,7 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
187
248
|
for (const dir of deepCompDirs) {
|
|
188
249
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
189
250
|
if (skipDomainNames.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
|
|
190
|
-
const files = await glob(`${dir
|
|
251
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
191
252
|
if (files.length >= 2) {
|
|
192
253
|
if (!deepDomains[name]) deepDomains[name] = { components: 0, totalFiles: 0 };
|
|
193
254
|
deepDomains[name].components += files.length;
|
|
@@ -212,7 +273,7 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
212
273
|
for (const dir of dirs) {
|
|
213
274
|
const name = path.basename(dir.replace(/\/$/, ""));
|
|
214
275
|
if (skipDirNames.includes(name) || name.startsWith("_") || name.startsWith(".") || name.startsWith("(") || name.startsWith("[")) continue;
|
|
215
|
-
const files = await glob(`${dir
|
|
276
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
216
277
|
if (files.length >= 2) {
|
|
217
278
|
if (!deepDirDomains[name]) deepDirDomains[name] = { components: 0, pages: 0, totalFiles: 0, sources: [] };
|
|
218
279
|
const tsx = files.filter(f => /\.(tsx|jsx|vue)$/.test(f)).length;
|
|
@@ -226,6 +287,152 @@ async function scanFrontendDomains(stack, ROOT) {
|
|
|
226
287
|
frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles, sources: data.sources });
|
|
227
288
|
}
|
|
228
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
|
+
for (const dir of platformDirs) {
|
|
376
|
+
const dirFwd = dir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
377
|
+
if (seenPlatformDirs.has(dirFwd)) continue;
|
|
378
|
+
seenPlatformDirs.add(dirFwd);
|
|
379
|
+
const parts = dirFwd.split("/");
|
|
380
|
+
// Locate platform segment: the FIRST segment that matches a keyword.
|
|
381
|
+
// findIndex (not findLast) — if the subapp name also happens to be a
|
|
382
|
+
// keyword (e.g., `src/pc/admin/`), the subapp should stay as the
|
|
383
|
+
// second match, not be mistaken for the platform segment.
|
|
384
|
+
// Paths handled: src/<p>/<s>, apps/<workspace>/src/<p>/<s>, apps/<p>/<s>.
|
|
385
|
+
const platformIdx = parts.findIndex(seg => PLATFORM_KEYWORDS.includes(seg));
|
|
386
|
+
if (platformIdx < 0 || platformIdx + 1 >= parts.length) continue;
|
|
387
|
+
const platform = parts[platformIdx];
|
|
388
|
+
const subapp = parts[platformIdx + 1];
|
|
389
|
+
if (!subapp || SKIP_SUBAPP_NAMES.includes(subapp) || subapp.startsWith("_") || subapp.startsWith(".")) continue;
|
|
390
|
+
const files = await glob(`${dirGlobPrefix(dir)}**/*.{tsx,jsx,ts,js,vue}`, {
|
|
391
|
+
cwd: ROOT,
|
|
392
|
+
ignore: [...BUILD_IGNORE_DIRS, ...TEST_FILE_IGNORE],
|
|
393
|
+
});
|
|
394
|
+
if (files.length < MIN_SUBAPP_FILES) continue;
|
|
395
|
+
// Normalize Windows backslashes so the segment regex works cross-platform.
|
|
396
|
+
const filesFwd = files.map(f => f.replace(/\\/g, "/"));
|
|
397
|
+
const routes = filesFwd.filter(f => /\/routes\//.test(f)).length;
|
|
398
|
+
const components = filesFwd.filter(f => /\/components\//.test(f)).length;
|
|
399
|
+
const layouts = filesFwd.filter(f => /\/layouts?\//.test(f)).length;
|
|
400
|
+
const hooks = filesFwd.filter(f => /\/hooks\//.test(f)).length;
|
|
401
|
+
frontendDomains.push({
|
|
402
|
+
name: `${platform}-${subapp}`,
|
|
403
|
+
type: "frontend",
|
|
404
|
+
platform,
|
|
405
|
+
subapp,
|
|
406
|
+
routes, components, layouts, hooks,
|
|
407
|
+
totalFiles: files.length,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Fallback E: React Router file-routing (any depth). Groups by the
|
|
412
|
+
// parent dir of `routes/`. Domain name = parent basename. Covers
|
|
413
|
+
// CRA/Vite + React Router projects that don't match Next.js page.tsx
|
|
414
|
+
// or FSD layout. Only fires when every other primary and fallback
|
|
415
|
+
// scanner returned 0 domains.
|
|
416
|
+
if (frontendDomains.length === 0) {
|
|
417
|
+
const routeFiles = await glob("**/routes/*.{tsx,jsx,ts,js,vue}", {
|
|
418
|
+
cwd: ROOT,
|
|
419
|
+
ignore: [...BUILD_IGNORE_DIRS, ...TEST_FILE_IGNORE],
|
|
420
|
+
});
|
|
421
|
+
const routeDomains = {};
|
|
422
|
+
const skipParents = ["src", "app", "pages", "", "."];
|
|
423
|
+
for (const f of routeFiles) {
|
|
424
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
425
|
+
const routesIdx = parts.lastIndexOf("routes");
|
|
426
|
+
if (routesIdx < 1) continue;
|
|
427
|
+
const parent = parts[routesIdx - 1];
|
|
428
|
+
if (skipParents.includes(parent) || parent.startsWith("_") || parent.startsWith(".")) continue;
|
|
429
|
+
if (!routeDomains[parent]) routeDomains[parent] = { routes: 0, totalFiles: 0 };
|
|
430
|
+
routeDomains[parent].routes++;
|
|
431
|
+
routeDomains[parent].totalFiles++;
|
|
432
|
+
}
|
|
433
|
+
for (const [name, data] of Object.entries(routeDomains)) {
|
|
434
|
+
frontendDomains.push({ name, type: "frontend", routes: data.routes, totalFiles: data.totalFiles });
|
|
435
|
+
}
|
|
229
436
|
}
|
|
230
437
|
}
|
|
231
438
|
|
package/sync-checker/index.js
CHANGED
|
@@ -1,132 +1,133 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ClaudeOS-Core — Sync Checker
|
|
5
|
-
*
|
|
6
|
-
* Role: Check disk ↔ Master Plan sync status based on sync-map.json
|
|
7
|
-
* Detection items:
|
|
8
|
-
* - Unregistered: file exists on disk but not registered in any plan
|
|
9
|
-
* - Orphaned: registered in plan but missing from disk
|
|
10
|
-
*
|
|
11
|
-
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/sync-checker/index.js
|
|
12
|
-
* Depends: manifest-generator must run first (sync-map.json)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require("fs");
|
|
16
|
-
const path = require("path");
|
|
17
|
-
const { glob } = require("glob");
|
|
18
|
-
const { updateStaleReport } = require("../lib/stale-report");
|
|
19
|
-
|
|
20
|
-
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
21
|
-
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
22
|
-
const SMP = path.join(GEN, "sync-map.json");
|
|
23
|
-
|
|
24
|
-
const TRACKED = [
|
|
25
|
-
{ dir: ".claude/rules", pfx: "rules" },
|
|
26
|
-
{ dir: "claudeos-core/standard", pfx: "standard" },
|
|
27
|
-
{ dir: "claudeos-core/skills", pfx: "skills" },
|
|
28
|
-
{ dir: "claudeos-core/guide", pfx: "guide" },
|
|
29
|
-
{ dir: "claudeos-core/database", pfx: "database" },
|
|
30
|
-
{ dir: "claudeos-core/mcp-guide", pfx: "mcp-guide" },
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.log("
|
|
50
|
-
console.log("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.log(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClaudeOS-Core — Sync Checker
|
|
5
|
+
*
|
|
6
|
+
* Role: Check disk ↔ Master Plan sync status based on sync-map.json
|
|
7
|
+
* Detection items:
|
|
8
|
+
* - Unregistered: file exists on disk but not registered in any plan
|
|
9
|
+
* - Orphaned: registered in plan but missing from disk
|
|
10
|
+
*
|
|
11
|
+
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/sync-checker/index.js
|
|
12
|
+
* Depends: manifest-generator must run first (sync-map.json)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { glob } = require("glob");
|
|
18
|
+
const { updateStaleReport } = require("../lib/stale-report");
|
|
19
|
+
|
|
20
|
+
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
21
|
+
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
22
|
+
const SMP = path.join(GEN, "sync-map.json");
|
|
23
|
+
|
|
24
|
+
const TRACKED = [
|
|
25
|
+
{ dir: ".claude/rules", pfx: "rules" },
|
|
26
|
+
{ dir: "claudeos-core/standard", pfx: "standard" },
|
|
27
|
+
{ dir: "claudeos-core/skills", pfx: "skills" },
|
|
28
|
+
{ dir: "claudeos-core/guide", pfx: "guide" },
|
|
29
|
+
{ dir: "claudeos-core/database", pfx: "database" },
|
|
30
|
+
{ dir: "claudeos-core/mcp-guide", pfx: "mcp-guide" },
|
|
31
|
+
{ dir: "claudeos-core/memory", pfx: "memory" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function rel(p) {
|
|
35
|
+
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWithinRoot(absPath) {
|
|
39
|
+
let resolved = path.resolve(absPath);
|
|
40
|
+
let root = path.resolve(ROOT);
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
resolved = resolved.toLowerCase();
|
|
43
|
+
root = root.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
console.log("\n╔═══════════════════════════════════════╗");
|
|
50
|
+
console.log("║ ClaudeOS-Core — Sync Checker ║");
|
|
51
|
+
console.log("╚═══════════════════════════════════════╝\n");
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(SMP)) {
|
|
54
|
+
console.log(" ❌ sync-map.json not found. Run manifest-generator first.\n");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let sm;
|
|
59
|
+
try {
|
|
60
|
+
sm = JSON.parse(fs.readFileSync(SMP, "utf-8"));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.log(` ❌ sync-map.json is malformed: ${e.message}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(sm.mappings)) {
|
|
66
|
+
console.log(" ❌ sync-map.json has no mappings array.\n");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
|
|
70
|
+
const issues = { unreg: [], orphan: [] };
|
|
71
|
+
|
|
72
|
+
// ─── [1/2] Disk → Plan: detect unregistered files ───────
|
|
73
|
+
console.log(" [1/2] Disk → Plan...");
|
|
74
|
+
for (const t of TRACKED) {
|
|
75
|
+
const abs = path.join(ROOT, t.dir);
|
|
76
|
+
if (!fs.existsSync(abs)) continue;
|
|
77
|
+
|
|
78
|
+
for (const f of await glob("**/*.md", { cwd: abs, absolute: true })) {
|
|
79
|
+
const r = rel(f);
|
|
80
|
+
if (path.basename(f) === "README.md") continue;
|
|
81
|
+
if (!reg.has(r)) {
|
|
82
|
+
issues.unreg.push({ path: r, domain: t.pfx });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check CLAUDE.md separately
|
|
88
|
+
if (fs.existsSync(path.join(ROOT, "CLAUDE.md")) && !reg.has("CLAUDE.md")) {
|
|
89
|
+
issues.unreg.push({ path: "CLAUDE.md", domain: "root" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── [2/2] Plan → Disk: detect orphaned files ───────────────
|
|
93
|
+
console.log(" [2/2] Plan → Disk...");
|
|
94
|
+
for (const m of sm.mappings) {
|
|
95
|
+
if (!m.sourcePath) continue;
|
|
96
|
+
const abs = path.join(ROOT, m.sourcePath);
|
|
97
|
+
// Skip path traversal attempts (allow files at ROOT level and below)
|
|
98
|
+
if (!isWithinRoot(abs)) continue;
|
|
99
|
+
if (!fs.existsSync(abs)) {
|
|
100
|
+
issues.orphan.push({ path: m.sourcePath, plan: m.planFile });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Output results ─────────────────────────────────────────
|
|
105
|
+
if (issues.unreg.length) {
|
|
106
|
+
console.log(`\n ⚠️ Unregistered (${issues.unreg.length}):`);
|
|
107
|
+
issues.unreg.forEach((i) => console.log(` + ${i.path}`));
|
|
108
|
+
}
|
|
109
|
+
if (issues.orphan.length) {
|
|
110
|
+
console.log(`\n ⚠️ Orphaned (${issues.orphan.length}):`);
|
|
111
|
+
issues.orphan.forEach((i) => console.log(` - ${i.path}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const total = issues.unreg.length + issues.orphan.length;
|
|
115
|
+
console.log(`\n Registered: ${reg.size} | Unregistered: ${issues.unreg.length} | Orphaned: ${issues.orphan.length}`);
|
|
116
|
+
console.log(total === 0 ? " ✅ All in sync\n" : ` ⚠️ ${total} issues\n`);
|
|
117
|
+
|
|
118
|
+
// ─── Update stale-report.json ────────────────────────────
|
|
119
|
+
updateStaleReport(GEN, "syncMisses",
|
|
120
|
+
{ checkedAt: new Date().toISOString(), unregistered: issues.unreg, orphaned: issues.orphan },
|
|
121
|
+
{ syncIssues: total, status: total === 0 ? "ok" : "warning" }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Exit 1 only for orphaned files (actual breakage), not for unregistered (informational)
|
|
125
|
+
const orphanCount = issues.orphan.length;
|
|
126
|
+
process.exit(orphanCount > 0 ? 1 : 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (require.main === module) {
|
|
130
|
+
main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { main };
|