claudeos-core 1.6.0 → 1.6.2
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 +47 -0
- package/bin/cli.js +1 -1
- package/bin/commands/init.js +15 -11
- package/bootstrap.sh +26 -20
- package/content-validator/index.js +2 -2
- package/health-checker/index.js +10 -3
- package/lib/plan-parser.js +1 -1
- package/lib/safe-fs.js +1 -1
- package/lib/stale-report.js +1 -2
- package/manifest-generator/index.js +16 -9
- package/package.json +1 -1
- package/pass-json-validator/index.js +2 -2
- package/plan-installer/domain-grouper.js +1 -1
- package/plan-installer/index.js +1 -0
- package/plan-installer/prompt-generator.js +8 -3
- package/plan-installer/scanners/scan-java.js +6 -1
- package/plan-installer/scanners/scan-kotlin.js +4 -1
- package/plan-installer/scanners/scan-python.js +5 -5
- package/plan-installer/stack-detector.js +7 -3
- package/plan-installer/structure-scanner.js +1 -1
- package/plan-validator/index.js +18 -6
- package/sync-checker/index.js +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.2] — 2026-04-09
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Sync command crash bypass** — `cli.js` sync throw from `cmdHealth`/`cmdValidate`/`cmdRestore`/`cmdRefresh` now correctly caught by `.catch()` handler; previously caused unhandled exception
|
|
8
|
+
- **`init.js` group.domains crash** — Null guard added for `group.domains` and `group.estimatedFiles` in domain-groups iteration; prevents TypeError on malformed `domain-groups.json`
|
|
9
|
+
- **Kotlin shared query resolution failure** — `scan-kotlin.js` full key (`__` separator) module names now converted back to path form (`/`) before file matching; `resolveSharedQueryDomains` was silently failing to find any files
|
|
10
|
+
- **Python scanner Windows glob failure** — `scan-python.js` added `dir.replace(/\\/g, "/")` for Django and FastAPI/Flask glob patterns; Windows `path.dirname` returns backslashes that break glob (same fix `scan-node.js` already had)
|
|
11
|
+
- **`prompt-generator.js` langData.labels crash** — Added null guard for `langData.labels` access; prevents TypeError when `lang-instructions.json` has `instructions` but missing `labels` key
|
|
12
|
+
- **Plan parser heading description leakage** — `plan-parser.js` `parseCodeBlocks` now strips trailing ` — description` / ` – description` / ` - description` from heading; previously included in `filePath`
|
|
13
|
+
- **Content validator regex escape** — `content-validator/index.js` regex character class now correctly escapes `[` and `]`; previously `[` was unescaped, causing runtime error when keyword contains `[`
|
|
14
|
+
- **Manifest generator CODE_BLOCK_PLANS count** — `plan-manifest.json` now uses `extractCodeBlockPathsFromFile` for code-block-format plans (e.g., `21.sync-rules-master.md`); `fileBlocks` count was always 0
|
|
15
|
+
- **Resume pass1/pass2 inconsistency** — When "continue" is selected but no pass1 files exist while pass2 does, pass2 is now deleted to force re-run; previously new pass1 + stale pass2 caused data mismatch
|
|
16
|
+
- **`--force` incomplete cleanup** — Now deletes all `.json` and `.md` files in `generated/` directory (not just pass1/pass2); ensures truly fresh start including stale prompts, manifests, and reports
|
|
17
|
+
- **Workspace path without wildcard** — `stack-detector.js` now handles concrete workspace paths (e.g., `packages/backend`) by scanning both direct and child `package.json` files; previously only glob patterns with `*` worked
|
|
18
|
+
- **Framework-less Python projects skipped** — `structure-scanner.js` now triggers Python scanner for all `language === "python"` projects; previously required `framework` to be `django`/`fastapi`/`flask`
|
|
19
|
+
- **Root directory router.py false domain** — `scan-python.js` now skips `name === "."` when `router.py` is in project root; previously created a domain named `.`
|
|
20
|
+
- **Sync checker null sourcePath** — `sync-checker/index.js` now skips mappings with null/undefined `sourcePath`; previously produced `path.join(ROOT, undefined)` = `"ROOT/undefined"`
|
|
21
|
+
- **Java Pattern B/D detection instability** — `scan-java.js` `detectedPattern` now determined by majority vote across all domains; previously depended on first `Object.keys` insertion order
|
|
22
|
+
- **Duplicate pass1 prompt overwrite** — `prompt-generator.js` deduplicates `activeTemplates` via `Set`; when backend and frontend share the same template, pass1 is generated once instead of being overwritten
|
|
23
|
+
- **Health checker stale-report overwrite** — Removed redundant `generatedAt` write that was overwriting `manifest-generator`'s `summaryPatch`; manifest-generator (run as prerequisite) already sets this key
|
|
24
|
+
- **Plan validator empty file creation** — `--execute` mode now skips file creation when plan block has empty/whitespace-only content; previously created blank files
|
|
25
|
+
|
|
26
|
+
## [1.6.1] — 2026-04-09
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Path traversal hardening (Windows)** — `plan-validator` and `sync-checker` now use case-insensitive path comparison on Windows, preventing UNC/case-mismatch bypass of root boundary check
|
|
31
|
+
- **Null pointer crash in `stack-detector.js`** — `readFileSafe()` return value for `pnpm-workspace.yaml` now guarded; prevents crash when file exists but is unreadable
|
|
32
|
+
- **Empty pass3 prompt generation** — `prompt-generator.js` now early-returns with warning when pass3 template is missing, instead of silently writing header+footer-only prompt
|
|
33
|
+
- **Domain group boundary off-by-one** — `splitDomainGroups` changed `>=` to `>` for file count threshold; groups now fill up to exactly `MAX_FILES_PER_GROUP` (40) instead of flushing one file early
|
|
34
|
+
- **Perl regex injection in `bootstrap.sh`** — All placeholder substitution migrated from `perl -pi -e` to Node.js `String.replace()`; eliminates regex special character risk in domain names; `perl` is no longer a prerequisite
|
|
35
|
+
- **Flask default port** — `plan-installer` now maps Flask to port 5000 (was falling through to 8080)
|
|
36
|
+
- **Health-checker dependency chain** — `sync-checker` is now automatically skipped when `manifest-generator` fails, instead of running against missing `sync-map.json`
|
|
37
|
+
- **`pass-json-validator` null template crash** — Added null guard before `typeof` check; `null` no longer passes `typeof === "object"` gate
|
|
38
|
+
- **`pass-json-validator` missing backend frameworks** — Added `"fastify"` and `"flask"` to backend framework list; these stacks previously skipped backend section validation
|
|
39
|
+
- **Init error messages** — Pass 1/2/3 failure messages now include actionable guidance (check output above, retry with `--force`, verify prompt file)
|
|
40
|
+
- **Manifest-generator error context** — `.catch()` handler now prefixes error with tool name
|
|
41
|
+
- **Line counting off-by-one** — `statSafe()` and `manifest-generator stat()` no longer count trailing newline as an extra line
|
|
42
|
+
- **Windows CRLF drift** — `plan-validator` now normalizes `\r\n` → `\n` before content comparison; prevents false drift on Windows
|
|
43
|
+
- **`stale-report.js` mutation** — `Object.assign(ex.summary, patch)` replaced with spread operator to avoid in-place mutation
|
|
44
|
+
- **Undefined in sync-checker Set** — Malformed mappings with missing `sourcePath` no longer insert `undefined` into the registered paths Set
|
|
45
|
+
- **BOM frontmatter detection** — `content-validator` now strips UTF-8 BOM (`\uFEFF`) before checking `---` frontmatter marker
|
|
46
|
+
- **Health-checker stderr loss** — Error output now combines both `stdout` and `stderr` instead of preferring one
|
|
47
|
+
- **`bootstrap.sh` exit code preservation** — EXIT trap now captures and restores `$?` instead of always exiting 0
|
|
48
|
+
- **`bootstrap.sh` NODE_MAJOR stderr** — `node -e` stderr redirected to `/dev/null` to prevent parse failure from noise
|
|
49
|
+
|
|
3
50
|
## [1.6.0] — 2026-04-08
|
|
4
51
|
|
|
5
52
|
### Added
|
package/bin/cli.js
CHANGED
|
@@ -130,7 +130,7 @@ if (!commands[command]) {
|
|
|
130
130
|
process.exit(1);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
Promise.resolve(commands[command]()).catch((e) => {
|
|
133
|
+
Promise.resolve().then(() => commands[command]()).catch((e) => {
|
|
134
134
|
if (e instanceof InitError) {
|
|
135
135
|
log(`\n ❌ ${e.message}\n`);
|
|
136
136
|
} else {
|
package/bin/commands/init.js
CHANGED
|
@@ -96,9 +96,9 @@ async function cmdInit(parsedArgs) {
|
|
|
96
96
|
|
|
97
97
|
if (existingPass1.length > 0 || pass2Exists) {
|
|
98
98
|
if (parsedArgs.force) {
|
|
99
|
-
// --force:
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// --force: clean all generated files for truly fresh start
|
|
100
|
+
const genFiles = fs.readdirSync(GENERATED_DIR).filter(f => f.endsWith(".json") || f.endsWith(".md"));
|
|
101
|
+
for (const f of genFiles) fs.unlinkSync(path.join(GENERATED_DIR, f));
|
|
102
102
|
log(" 🔄 Previous results deleted (--force)\n");
|
|
103
103
|
} else {
|
|
104
104
|
const status = { pass1Done: existingPass1.length, pass2Done: pass2Exists };
|
|
@@ -107,6 +107,10 @@ async function cmdInit(parsedArgs) {
|
|
|
107
107
|
if (mode === "fresh") {
|
|
108
108
|
for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
|
|
109
109
|
if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
|
|
110
|
+
} else if (mode === "continue" && existingPass1.length === 0 && pass2Exists) {
|
|
111
|
+
// pass2 exists but no pass1 → pass2 is stale, force re-run
|
|
112
|
+
fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
|
|
113
|
+
log(" ⚠️ pass2-merged.json deleted (no pass1 files to continue from)");
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
}
|
|
@@ -202,8 +206,8 @@ async function cmdInit(parsedArgs) {
|
|
|
202
206
|
}
|
|
203
207
|
for (let i = 1; i <= totalGroups; i++) {
|
|
204
208
|
const group = domainGroups.groups[i - 1];
|
|
205
|
-
const domainList = group.domains.join(", ");
|
|
206
|
-
const estFiles = group.estimatedFiles;
|
|
209
|
+
const domainList = (group.domains || []).join(", ") || "(unknown)";
|
|
210
|
+
const estFiles = group.estimatedFiles || 0;
|
|
207
211
|
const groupType = group.type || "backend";
|
|
208
212
|
const icon = groupType === "frontend" ? "🎨" : "⚙️";
|
|
209
213
|
|
|
@@ -242,11 +246,11 @@ async function cmdInit(parsedArgs) {
|
|
|
242
246
|
const elapsed1 = formatElapsed(Date.now() - t1);
|
|
243
247
|
|
|
244
248
|
if (!ok) {
|
|
245
|
-
throw new InitError(`Pass 1-${i} failed`);
|
|
249
|
+
throw new InitError(`Pass 1-${i} failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force`);
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
if (!fileExists(pass1Json)) {
|
|
249
|
-
throw new InitError(`pass1-${i}.json was not created`);
|
|
253
|
+
throw new InitError(`pass1-${i}.json was not created. Claude may have run but not produced expected output.\n Ensure the prompt instructs Claude to write to claudeos-core/generated/pass1-${i}.json`);
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
log(` ✅ pass1-${i}.json created (${elapsed1})`);
|
|
@@ -272,11 +276,11 @@ async function cmdInit(parsedArgs) {
|
|
|
272
276
|
const elapsed2 = formatElapsed(Date.now() - t2);
|
|
273
277
|
|
|
274
278
|
if (!ok) {
|
|
275
|
-
throw new InitError("Pass 2 failed");
|
|
279
|
+
throw new InitError("Pass 2 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
if (!fileExists(pass2Json)) {
|
|
279
|
-
throw new InitError("pass2-merged.json was not created");
|
|
283
|
+
throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
log(` ✅ pass2-merged.json created (${elapsed2})`);
|
|
@@ -298,11 +302,11 @@ async function cmdInit(parsedArgs) {
|
|
|
298
302
|
const elapsed3 = formatElapsed(Date.now() - t3);
|
|
299
303
|
|
|
300
304
|
if (!ok3) {
|
|
301
|
-
throw new InitError("Pass 3 failed");
|
|
305
|
+
throw new InitError("Pass 3 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
|
|
302
306
|
}
|
|
303
307
|
|
|
304
308
|
if (!fileExists(path.join(PROJECT_ROOT, "CLAUDE.md"))) {
|
|
305
|
-
throw new InitError("CLAUDE.md was not created.
|
|
309
|
+
throw new InitError("CLAUDE.md was not created. Claude ran but did not produce CLAUDE.md.\n Verify pass3-prompt.md instructs Claude to create CLAUDE.md at project root.");
|
|
306
310
|
}
|
|
307
311
|
log(` ✅ Pass 3 complete (${elapsed3})`);
|
|
308
312
|
log("");
|
package/bootstrap.sh
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# One-click full system auto-build
|
|
6
6
|
# Automatically splits Pass 1 into N runs based on project size
|
|
7
7
|
#
|
|
8
|
-
# Prerequisites: bash, node (v18+), claude CLI
|
|
9
|
-
#
|
|
8
|
+
# Prerequisites: bash, node (v18+), claude CLI
|
|
9
|
+
# Cross-platform alternative: node bin/cli.js init
|
|
10
10
|
#
|
|
11
11
|
# Usage: bash claudeos-core-tools/bootstrap.sh --lang ko
|
|
12
12
|
# bash claudeos-core-tools/bootstrap.sh (interactive)
|
|
@@ -27,7 +27,7 @@ GENERATED_DIR="$PROJECT_ROOT/claudeos-core/generated"
|
|
|
27
27
|
cd "$PROJECT_ROOT"
|
|
28
28
|
|
|
29
29
|
# Cleanup temp files on exit (Ctrl+C, errors, etc.)
|
|
30
|
-
trap 'rm -f "$GENERATED_DIR"/_tmp_*.md "$GENERATED_DIR"/_tmp_*.md.final 2>/dev/null' EXIT
|
|
30
|
+
trap 'rc=$?; rm -f "$GENERATED_DIR"/_tmp_*.md "$GENERATED_DIR"/_tmp_*.md.final 2>/dev/null; exit $rc' EXIT
|
|
31
31
|
|
|
32
32
|
# ─── Language selection (required) ──────────────────────────────
|
|
33
33
|
SUPPORTED_LANGS=("en" "ko" "zh-CN" "ja" "es" "vi" "hi" "ru" "fr" "de")
|
|
@@ -97,7 +97,7 @@ if ! command -v node &> /dev/null; then
|
|
|
97
97
|
exit 1
|
|
98
98
|
fi
|
|
99
99
|
|
|
100
|
-
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
|
|
100
|
+
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])" 2>/dev/null)
|
|
101
101
|
if ! [[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || [ "$NODE_MAJOR" -lt 18 ]; then
|
|
102
102
|
echo ""
|
|
103
103
|
echo " ❌ Node.js v18+ required (current: v$(node --version))"
|
|
@@ -115,13 +115,7 @@ if ! command -v claude &> /dev/null; then
|
|
|
115
115
|
exit 1
|
|
116
116
|
fi
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
echo ""
|
|
120
|
-
echo " ❌ perl not found (required for placeholder substitution)."
|
|
121
|
-
echo " Install perl or use the Node.js CLI instead: npx claudeos-core init"
|
|
122
|
-
echo ""
|
|
123
|
-
exit 1
|
|
124
|
-
fi
|
|
118
|
+
## perl is no longer required — placeholder substitution now uses Node.js
|
|
125
119
|
|
|
126
120
|
|
|
127
121
|
|
|
@@ -246,18 +240,20 @@ for i in $(seq 1 "$TOTAL_GROUPS"); do
|
|
|
246
240
|
if [ ! -f "$PROMPT_FILE" ]; then
|
|
247
241
|
PROMPT_FILE="$GENERATED_DIR/pass1-prompt.md"
|
|
248
242
|
fi
|
|
249
|
-
# Substitute placeholders via temp file (
|
|
243
|
+
# Substitute placeholders via temp file (uses Node.js for safe literal replacement)
|
|
250
244
|
TMP_PROMPT="$GENERATED_DIR/_tmp_pass1_prompt.md"
|
|
251
245
|
cp "$PROMPT_FILE" "$TMP_PROMPT"
|
|
252
|
-
# Use perl with $ENV{} for safe literal replacement (no shell interpolation into Perl code)
|
|
253
246
|
export _DOMAIN_LIST="$DOMAIN_LIST"
|
|
254
247
|
export _PASS_NUM="$i"
|
|
255
|
-
perl -pi -e 's/\{\{DOMAIN_GROUP\}\}/$ENV{_DOMAIN_LIST}/g' "$TMP_PROMPT"
|
|
256
|
-
perl -pi -e 's/\{\{PASS_NUM\}\}/$ENV{_PASS_NUM}/g' "$TMP_PROMPT"
|
|
257
|
-
# inject_project_root: pipe through perl, write to final temp file
|
|
258
248
|
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
259
|
-
|
|
260
|
-
|
|
249
|
+
node -e "
|
|
250
|
+
const fs = require('fs');
|
|
251
|
+
let c = fs.readFileSync(process.argv[1], 'utf8');
|
|
252
|
+
c = c.replace(/\{\{DOMAIN_GROUP\}\}/g, process.env._DOMAIN_LIST);
|
|
253
|
+
c = c.replace(/\{\{PASS_NUM\}\}/g, process.env._PASS_NUM);
|
|
254
|
+
c = c.replace(/\{\{PROJECT_ROOT\}\}/g, process.env._PROJECT_ROOT);
|
|
255
|
+
fs.writeFileSync(process.argv[1], c);
|
|
256
|
+
" "$TMP_PROMPT"
|
|
261
257
|
|
|
262
258
|
echo " ⏳ [Pass 1-${i}/${TOTAL_GROUPS}] Running claude -p (no output is normal, please wait)..."
|
|
263
259
|
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PROMPT" | claude -p --dangerously-skip-permissions); then
|
|
@@ -288,7 +284,12 @@ if [ -f "$GENERATED_DIR/pass2-merged.json" ]; then
|
|
|
288
284
|
else
|
|
289
285
|
TMP_PASS2="$GENERATED_DIR/_tmp_pass2_prompt.md"
|
|
290
286
|
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
291
|
-
|
|
287
|
+
node -e "
|
|
288
|
+
const fs = require('fs');
|
|
289
|
+
let c = fs.readFileSync(process.argv[1], 'utf8');
|
|
290
|
+
c = c.replace(/\{\{PROJECT_ROOT\}\}/g, process.env._PROJECT_ROOT);
|
|
291
|
+
fs.writeFileSync(process.argv[2], c);
|
|
292
|
+
" "$GENERATED_DIR/pass2-prompt.md" "$TMP_PASS2"
|
|
292
293
|
|
|
293
294
|
echo " ⏳ [Pass 2] Running claude -p (no output is normal, please wait)..."
|
|
294
295
|
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS2" | claude -p --dangerously-skip-permissions); then
|
|
@@ -313,7 +314,12 @@ echo "[6] Pass 3 — Generating all files..."
|
|
|
313
314
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
314
315
|
TMP_PASS3="$GENERATED_DIR/_tmp_pass3_prompt.md"
|
|
315
316
|
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
316
|
-
|
|
317
|
+
node -e "
|
|
318
|
+
const fs = require('fs');
|
|
319
|
+
let c = fs.readFileSync(process.argv[1], 'utf8');
|
|
320
|
+
c = c.replace(/\{\{PROJECT_ROOT\}\}/g, process.env._PROJECT_ROOT);
|
|
321
|
+
fs.writeFileSync(process.argv[2], c);
|
|
322
|
+
" "$GENERATED_DIR/pass3-prompt.md" "$TMP_PASS3"
|
|
317
323
|
|
|
318
324
|
echo " ⏳ [Pass 3] Running claude -p (no output is normal, please wait)..."
|
|
319
325
|
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS3" | claude -p --dangerously-skip-permissions); then
|
|
@@ -85,7 +85,7 @@ async function main() {
|
|
|
85
85
|
for (let i = 0; i < enKeywords.length; i++) {
|
|
86
86
|
const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
|
|
87
87
|
const found = candidates.some(kw => {
|
|
88
|
-
const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()
|
|
88
|
+
const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()|\\[\]\\\\]/g, "\\$&")}`, "im");
|
|
89
89
|
return re.test(content);
|
|
90
90
|
});
|
|
91
91
|
if (!found) {
|
|
@@ -107,7 +107,7 @@ async function main() {
|
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
109
|
// All rules must have paths: frontmatter (value varies by category — e.g. ["**/*"] for core/backend, scoped patterns for infra/sync)
|
|
110
|
-
const hasFrontmatter = c.startsWith("---");
|
|
110
|
+
const hasFrontmatter = c.replace(/^\uFEFF/, "").startsWith("---");
|
|
111
111
|
const hasPathsKey = c.includes("paths:");
|
|
112
112
|
if (!hasFrontmatter) {
|
|
113
113
|
warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
|
package/health-checker/index.js
CHANGED
|
@@ -32,7 +32,7 @@ function run(name, script) {
|
|
|
32
32
|
});
|
|
33
33
|
return { name, ok: true, output };
|
|
34
34
|
} catch (e) {
|
|
35
|
-
return { name, ok: false, output: e.stdout
|
|
35
|
+
return { name, ok: false, output: [e.stdout, e.stderr].filter(Boolean).join("\n") || e.message || "", exitCode: e.status || 1 };
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -43,15 +43,17 @@ function main() {
|
|
|
43
43
|
|
|
44
44
|
// ─── [0] Run manifest-generator first (prerequisite) ──────────────────
|
|
45
45
|
// Must run first because sync-checker reads sync-map.json
|
|
46
|
+
let manifestOk = false;
|
|
46
47
|
const manifestScript = path.join(TOOLS, "manifest-generator/index.js");
|
|
47
48
|
if (fs.existsSync(manifestScript)) {
|
|
48
49
|
process.stdout.write(" ⏳ manifest-generator — generating metadata...");
|
|
49
50
|
const r = run("manifest-generator", manifestScript);
|
|
50
51
|
if (r.ok) {
|
|
51
52
|
console.log(" ✅");
|
|
53
|
+
manifestOk = true;
|
|
52
54
|
} else {
|
|
53
55
|
console.log(" ❌");
|
|
54
|
-
console.log(" ⚠️ manifest-generator failed.
|
|
56
|
+
console.log(" ⚠️ manifest-generator failed. sync-checker will be skipped.");
|
|
55
57
|
}
|
|
56
58
|
} else {
|
|
57
59
|
console.log(" ⏭️ manifest-generator — not found");
|
|
@@ -75,6 +77,12 @@ function main() {
|
|
|
75
77
|
results.push({ name: t.name, status: "skipped" });
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
80
|
+
// Skip sync-checker if manifest-generator failed (depends on sync-map.json)
|
|
81
|
+
if (t.name === "sync-checker" && !manifestOk) {
|
|
82
|
+
console.log(` ⏭️ ${t.name} — skipped (manifest-generator failed)`);
|
|
83
|
+
results.push({ name: t.name, status: "skipped" });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
78
86
|
process.stdout.write(` ⏳ ${t.name} — ${t.desc}...`);
|
|
79
87
|
const r = run(t.name, t.script);
|
|
80
88
|
if (r.ok) {
|
|
@@ -105,7 +113,6 @@ function main() {
|
|
|
105
113
|
console.log(" ══════════════════════════════\n");
|
|
106
114
|
|
|
107
115
|
// ─── Update stale-report.json ────────────────────────────
|
|
108
|
-
updateStaleReport(GEN, "generatedAt", new Date().toISOString());
|
|
109
116
|
updateStaleReport(GEN, "healthCheck",
|
|
110
117
|
{ results, status: hasErr ? "fail" : "pass" },
|
|
111
118
|
{ totalIssues: results.filter((r) => r.status === "fail").length, healthStatus: hasErr ? "fail" : "ok" }
|
package/lib/plan-parser.js
CHANGED
|
@@ -52,7 +52,7 @@ function parseCodeBlocks(content, { includeContent = false } = {}) {
|
|
|
52
52
|
: /^##\s+\d+\.\s+`?([^`\n]+)`?/gm;
|
|
53
53
|
let headingMatch;
|
|
54
54
|
while ((headingMatch = headingRe.exec(content)) !== null) {
|
|
55
|
-
const filePath = headingMatch[1].replace(/`/g, "").trim();
|
|
55
|
+
const filePath = headingMatch[1].replace(/`/g, "").replace(/\s+[—–\-].*$/, "").trim();
|
|
56
56
|
|
|
57
57
|
if (!includeContent) {
|
|
58
58
|
// Path-only mode: just validate and collect
|
package/lib/safe-fs.js
CHANGED
|
@@ -91,7 +91,7 @@ function statSafe(filePath) {
|
|
|
91
91
|
const c = fs.readFileSync(filePath, "utf-8");
|
|
92
92
|
const s = fs.statSync(filePath);
|
|
93
93
|
return {
|
|
94
|
-
lines: c.split("\n").length,
|
|
94
|
+
lines: c.endsWith("\n") ? c.split("\n").length - 1 : c.split("\n").length,
|
|
95
95
|
bytes: s.size,
|
|
96
96
|
modified: s.mtime.toISOString().split("T")[0],
|
|
97
97
|
};
|
package/lib/stale-report.js
CHANGED
|
@@ -27,8 +27,7 @@ function updateStaleReport(genDir, key, data, summaryPatch) {
|
|
|
27
27
|
}
|
|
28
28
|
ex[key] = data;
|
|
29
29
|
if (summaryPatch) {
|
|
30
|
-
|
|
31
|
-
Object.assign(ex.summary, summaryPatch);
|
|
30
|
+
ex.summary = { ...(ex.summary || {}), ...summaryPatch };
|
|
32
31
|
}
|
|
33
32
|
fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
|
|
34
33
|
}
|
|
@@ -38,13 +38,17 @@ function rel(p) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function stat(f) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
try {
|
|
42
|
+
const s = fs.statSync(f);
|
|
43
|
+
const c = fs.readFileSync(f, "utf-8");
|
|
44
|
+
return {
|
|
45
|
+
lines: c.endsWith("\n") ? c.split("\n").length - 1 : c.split("\n").length,
|
|
46
|
+
bytes: s.size,
|
|
47
|
+
modified: s.mtime.toISOString().split("T")[0],
|
|
48
|
+
};
|
|
49
|
+
} catch (_e) {
|
|
50
|
+
return { lines: 0, bytes: 0, modified: "unknown" };
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
function frontmatter(f) {
|
|
@@ -144,7 +148,10 @@ async function main() {
|
|
|
144
148
|
for (const p of await glob("*.md", { cwd: DIRS.plan, absolute: true })) {
|
|
145
149
|
const r = rel(p);
|
|
146
150
|
const s = stat(p);
|
|
147
|
-
const
|
|
151
|
+
const bn = path.basename(p);
|
|
152
|
+
const blocks = CODE_BLOCK_PLANS.includes(bn)
|
|
153
|
+
? extractCodeBlockPathsFromFile(p)
|
|
154
|
+
: extractFileBlocksFromFile(p);
|
|
148
155
|
pm.plans.push({ path: r, ...s, fileBlocks: blocks.length, status: "ok" });
|
|
149
156
|
}
|
|
150
157
|
}
|
|
@@ -158,7 +165,7 @@ async function main() {
|
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
if (require.main === module) {
|
|
161
|
-
main().catch(e => { console.error(e); process.exit(1); });
|
|
168
|
+
main().catch(e => { console.error(`\n ❌ Manifest Generator failed: ${e.message || e}`); process.exit(1); });
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
module.exports = { main };
|
package/package.json
CHANGED
|
@@ -86,7 +86,7 @@ async function main() {
|
|
|
86
86
|
console.log(` domains: ${pa.domains.length}`);
|
|
87
87
|
console.log(` lang: ${pa.lang || "en (default)"}`);
|
|
88
88
|
const tmpl = pa.templates || pa.template;
|
|
89
|
-
if (typeof tmpl === "object") {
|
|
89
|
+
if (tmpl && typeof tmpl === "object") {
|
|
90
90
|
console.log(` templates: backend=${tmpl.backend || "none"}, frontend=${tmpl.frontend || "none"}`);
|
|
91
91
|
if (pa.isMultiStack) console.log(` mode: 🔀 multi-stack`);
|
|
92
92
|
} else {
|
|
@@ -195,7 +195,7 @@ async function main() {
|
|
|
195
195
|
const framework = paData.stack?.framework;
|
|
196
196
|
const language = paData.stack?.language;
|
|
197
197
|
const architecture = paData.stack?.architecture;
|
|
198
|
-
isBackend = !frontend || ["express", "nestjs", "django", "fastapi", "spring-boot"].includes(framework);
|
|
198
|
+
isBackend = !frontend || ["express", "nestjs", "fastify", "django", "fastapi", "flask", "spring-boot"].includes(framework);
|
|
199
199
|
isKotlin = language === "kotlin";
|
|
200
200
|
isKotlinCqrs = isKotlin && (architecture === "cqrs" || paData.stack?.multiModule);
|
|
201
201
|
} catch (_e) { /* If project-analysis parsing fails, conservatively assume backend */ }
|
|
@@ -14,7 +14,7 @@ function splitDomainGroups(domains, type, template) {
|
|
|
14
14
|
|
|
15
15
|
for (const d of domains) {
|
|
16
16
|
// Flush current group before adding if it would exceed limits
|
|
17
|
-
if (current.length > 0 && (fileCount + d.totalFiles
|
|
17
|
+
if (current.length > 0 && (fileCount + d.totalFiles > MAX_FILES_PER_GROUP || current.length >= MAX_DOMAINS_PER_GROUP)) {
|
|
18
18
|
groups.push({ type, template, domains: [...current], estimatedFiles: fileCount });
|
|
19
19
|
current = [];
|
|
20
20
|
fileCount = 0;
|
package/plan-installer/index.js
CHANGED
|
@@ -92,6 +92,7 @@ async function main() {
|
|
|
92
92
|
|
|
93
93
|
// Save outputs
|
|
94
94
|
const defaultPort = (stack.framework === "fastapi" || stack.framework === "django") ? 8000
|
|
95
|
+
: stack.framework === "flask" ? 5000
|
|
95
96
|
: (stack.framework === "express" || stack.framework === "nestjs" || stack.framework === "fastify") ? 3000 : 8080;
|
|
96
97
|
const analysis = {
|
|
97
98
|
analyzedAt: new Date().toISOString(), lang,
|
|
@@ -29,7 +29,8 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
29
29
|
const langData = readJsonSafe(langPath);
|
|
30
30
|
if (langData && langData.instructions && langData.instructions[lang]) {
|
|
31
31
|
langInstruction = langData.instructions[lang];
|
|
32
|
-
|
|
32
|
+
const label = (langData.labels && langData.labels[lang]) || lang;
|
|
33
|
+
console.log(` 🌐 Language: ${label} (Pass 3 output)`);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -39,7 +40,7 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
39
40
|
return readFileSafe(src);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const activeTemplates = [templates.backend, templates.frontend].filter(Boolean);
|
|
43
|
+
const activeTemplates = [...new Set([templates.backend, templates.frontend].filter(Boolean))];
|
|
43
44
|
const primaryTemplate = templates.backend || templates.frontend;
|
|
44
45
|
|
|
45
46
|
for (let ti = 0; ti < activeTemplates.length; ti++) {
|
|
@@ -63,7 +64,11 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
63
64
|
|
|
64
65
|
if (primaryTemplate) {
|
|
65
66
|
const primaryBody = readTemplate(primaryTemplate, "pass3");
|
|
66
|
-
|
|
67
|
+
if (!primaryBody) {
|
|
68
|
+
console.log(` ⚠️ pass3 template not found for ${primaryTemplate}, skipping`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let combinedBody = primaryBody;
|
|
67
72
|
|
|
68
73
|
if (templates.backend && templates.frontend && templates.backend !== templates.frontend) {
|
|
69
74
|
const frontendBody = readTemplate(templates.frontend, "pass3");
|
|
@@ -69,7 +69,12 @@ async function scanJavaDomains(stack, ROOT) {
|
|
|
69
69
|
domainMap[d].controllers += entries.length;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
if (Object.keys(domainMap).length > 0)
|
|
72
|
+
if (Object.keys(domainMap).length > 0) {
|
|
73
|
+
// Determine pattern by majority vote (B vs D)
|
|
74
|
+
const patternCounts = {};
|
|
75
|
+
for (const v of Object.values(domainMap)) patternCounts[v.pattern] = (patternCounts[v.pattern] || 0) + 1;
|
|
76
|
+
detectedPattern = Object.entries(patternCounts).sort((a, b) => b[1] - a[1])[0][0];
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
// Pattern E: DDD/Hexagonal — {domain}/adapter/in/web/*.java or {domain}/adapter/in/rest/*.java
|
|
@@ -262,7 +262,10 @@ function resolveSharedQueryDomains(backendDomains, ktFiles) {
|
|
|
262
262
|
for (const shared of sharedModules) {
|
|
263
263
|
const moduleNames = shared.modules || [];
|
|
264
264
|
const sharedKtFiles = ktFiles.filter(f =>
|
|
265
|
-
moduleNames.some(m =>
|
|
265
|
+
moduleNames.some(m => {
|
|
266
|
+
const p = m.includes("__") ? m.replace(/__/g, "/") : m;
|
|
267
|
+
return f.includes(`/${p}/`) || f.startsWith(`${p}/`);
|
|
268
|
+
})
|
|
266
269
|
);
|
|
267
270
|
if (sharedKtFiles.length === 0) continue;
|
|
268
271
|
|
|
@@ -17,7 +17,7 @@ async function scanPythonDomains(stack, ROOT) {
|
|
|
17
17
|
const dir = path.dirname(f);
|
|
18
18
|
if (dir === "." || dir.includes("venv")) continue;
|
|
19
19
|
const name = path.basename(dir);
|
|
20
|
-
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
20
|
+
const appFiles = await glob(`${dir.replace(/\\/g, "/")}/*.py`, { cwd: ROOT });
|
|
21
21
|
const views = appFiles.filter(x => x.includes("views")).length;
|
|
22
22
|
const models = appFiles.filter(x => x.includes("models")).length;
|
|
23
23
|
const serializers = appFiles.filter(x => x.includes("serializers")).length;
|
|
@@ -25,16 +25,16 @@ async function scanPythonDomains(stack, ROOT) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// ── FastAPI / Flask ──
|
|
29
|
-
if (stack.framework === "fastapi" || stack.framework === "flask") {
|
|
28
|
+
// ── FastAPI / Flask / generic Python ──
|
|
29
|
+
if (stack.framework === "fastapi" || stack.framework === "flask" || (stack.language === "python" && stack.framework !== "django")) {
|
|
30
30
|
const routerFiles = await glob("**/{router,routes,endpoints}*.py", { cwd: ROOT, ignore: ["**/venv/**", "**/.venv/**"] });
|
|
31
31
|
const seen = new Set();
|
|
32
32
|
for (const f of routerFiles) {
|
|
33
33
|
const dir = path.dirname(f);
|
|
34
34
|
const name = path.basename(dir);
|
|
35
|
-
if (seen.has(name) || ["venv", ".venv", "__pycache__"].includes(name)) continue;
|
|
35
|
+
if (name === "." || seen.has(name) || ["venv", ".venv", "__pycache__"].includes(name)) continue;
|
|
36
36
|
seen.add(name);
|
|
37
|
-
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
37
|
+
const appFiles = await glob(`${dir.replace(/\\/g, "/")}/*.py`, { cwd: ROOT });
|
|
38
38
|
backendDomains.push({ name, type: "backend", totalFiles: appFiles.length });
|
|
39
39
|
}
|
|
40
40
|
if (backendDomains.filter(d => d.type === "backend").length === 0) {
|
|
@@ -252,8 +252,10 @@ async function detectStack(ROOT) {
|
|
|
252
252
|
else if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) wsPatterns = pkg.workspaces.packages;
|
|
253
253
|
if (wsPatterns.length === 0 && existsSafe(path.join(ROOT, "pnpm-workspace.yaml"))) {
|
|
254
254
|
const wy = readFileSafe(path.join(ROOT, "pnpm-workspace.yaml"));
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
if (wy) {
|
|
256
|
+
const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
|
|
257
|
+
if (wm.length > 0) wsPatterns = wm;
|
|
258
|
+
}
|
|
257
259
|
}
|
|
258
260
|
if (wsPatterns.length > 0) stack.workspaces = wsPatterns;
|
|
259
261
|
}
|
|
@@ -265,7 +267,9 @@ async function detectStack(ROOT) {
|
|
|
265
267
|
const subPkgGlobs = ["{apps,packages}/*/package.json"];
|
|
266
268
|
if (stack.workspaces) {
|
|
267
269
|
for (const ws of stack.workspaces) {
|
|
268
|
-
const wsGlob = ws
|
|
270
|
+
const wsGlob = /[*?]/.test(ws)
|
|
271
|
+
? ws.replace(/\/?\*?\*?$/, "/*/package.json")
|
|
272
|
+
: `${ws.replace(/\/?$/, "")}/{,*/}package.json`;
|
|
269
273
|
if (!subPkgGlobs.includes(wsGlob)) subPkgGlobs.push(wsGlob);
|
|
270
274
|
}
|
|
271
275
|
}
|
|
@@ -41,7 +41,7 @@ async function scanStructure(stack, ROOT) {
|
|
|
41
41
|
backendDomains.push(...r.backendDomains);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
if (stack.
|
|
44
|
+
if (stack.language === "python") {
|
|
45
45
|
const r = await scanPythonDomains(stack, ROOT);
|
|
46
46
|
backendDomains.push(...r.backendDomains);
|
|
47
47
|
}
|
package/plan-validator/index.js
CHANGED
|
@@ -28,6 +28,16 @@ function rel(p) {
|
|
|
28
28
|
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function isWithinRoot(absPath) {
|
|
32
|
+
let resolved = path.resolve(absPath);
|
|
33
|
+
let root = path.resolve(ROOT);
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
resolved = resolved.toLowerCase();
|
|
36
|
+
root = root.toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// Aliases for backward compatibility (used by tests and main())
|
|
32
42
|
function extractFileBlocks(content) { return parseFileBlocks(content, { includeContent: true }); }
|
|
33
43
|
function extractCodeBlocks(content) { return parseCodeBlocks(content, { includeContent: true }); }
|
|
@@ -70,9 +80,7 @@ async function main() {
|
|
|
70
80
|
const abs = path.join(ROOT, b.path);
|
|
71
81
|
|
|
72
82
|
// Block path traversal attempts (allow files at ROOT level and below)
|
|
73
|
-
|
|
74
|
-
const resolvedRoot = path.resolve(ROOT);
|
|
75
|
-
if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + path.sep)) {
|
|
83
|
+
if (!isWithinRoot(abs)) {
|
|
76
84
|
console.log(` ⚠️ SKIPPED: ${b.path} (path traversal blocked)`);
|
|
77
85
|
continue;
|
|
78
86
|
}
|
|
@@ -80,6 +88,10 @@ async function main() {
|
|
|
80
88
|
// File does not exist
|
|
81
89
|
if (!fs.existsSync(abs)) {
|
|
82
90
|
if (mode === "--execute") {
|
|
91
|
+
if (!b.content || b.content.trim().length === 0) {
|
|
92
|
+
console.log(` ⚠️ SKIPPED: ${b.path} (empty content in plan)`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
83
95
|
const dir = path.dirname(abs);
|
|
84
96
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
85
97
|
fs.writeFileSync(abs, b.content + "\n");
|
|
@@ -93,9 +105,9 @@ async function main() {
|
|
|
93
105
|
continue;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
// File exists — compare content (normalize trailing newlines
|
|
97
|
-
const diskContent = fs.readFileSync(abs, "utf-8").replace(/\n+$/, "");
|
|
98
|
-
const planContent = b.content.replace(/\n+$/, "");
|
|
108
|
+
// File exists — compare content (normalize CRLF + trailing newlines)
|
|
109
|
+
const diskContent = fs.readFileSync(abs, "utf-8").replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
110
|
+
const planContent = b.content.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
99
111
|
|
|
100
112
|
if (diskContent === planContent) {
|
|
101
113
|
synced++;
|
package/sync-checker/index.js
CHANGED
|
@@ -34,6 +34,16 @@ function rel(p) {
|
|
|
34
34
|
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function isWithinRoot(absPath) {
|
|
38
|
+
let resolved = path.resolve(absPath);
|
|
39
|
+
let root = path.resolve(ROOT);
|
|
40
|
+
if (process.platform === "win32") {
|
|
41
|
+
resolved = resolved.toLowerCase();
|
|
42
|
+
root = root.toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
async function main() {
|
|
38
48
|
console.log("\n╔═══════════════════════════════════════╗");
|
|
39
49
|
console.log("║ ClaudeOS-Core — Sync Checker ║");
|
|
@@ -55,7 +65,7 @@ async function main() {
|
|
|
55
65
|
console.log(" ❌ sync-map.json has no mappings array.\n");
|
|
56
66
|
process.exit(1);
|
|
57
67
|
}
|
|
58
|
-
const reg = new Set(sm.mappings.map((m) => m.sourcePath));
|
|
68
|
+
const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
|
|
59
69
|
const issues = { unreg: [], orphan: [] };
|
|
60
70
|
|
|
61
71
|
// ─── [1/2] Disk → Plan: detect unregistered files ───────
|
|
@@ -81,11 +91,10 @@ async function main() {
|
|
|
81
91
|
// ─── [2/2] Plan → Disk: detect orphaned files ───────────────
|
|
82
92
|
console.log(" [2/2] Plan → Disk...");
|
|
83
93
|
for (const m of sm.mappings) {
|
|
94
|
+
if (!m.sourcePath) continue;
|
|
84
95
|
const abs = path.join(ROOT, m.sourcePath);
|
|
85
96
|
// Skip path traversal attempts (allow files at ROOT level and below)
|
|
86
|
-
|
|
87
|
-
const resolvedRoot = path.resolve(ROOT);
|
|
88
|
-
if (resolvedAbs !== resolvedRoot && !resolvedAbs.startsWith(resolvedRoot + path.sep)) continue;
|
|
97
|
+
if (!isWithinRoot(abs)) continue;
|
|
89
98
|
if (!fs.existsSync(abs)) {
|
|
90
99
|
issues.orphan.push({ path: m.sourcePath, plan: m.planFile });
|
|
91
100
|
}
|