claudeos-core 1.6.0 → 1.6.1
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 +24 -0
- package/bin/commands/init.js +6 -6
- package/bootstrap.sh +26 -20
- package/content-validator/index.js +1 -1
- package/health-checker/index.js +10 -2
- package/lib/safe-fs.js +1 -1
- package/lib/stale-report.js +1 -2
- package/manifest-generator/index.js +12 -8
- 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 +5 -1
- package/plan-installer/stack-detector.js +4 -2
- package/plan-validator/index.js +14 -6
- package/sync-checker/index.js +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.1] — 2026-04-09
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **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
|
|
8
|
+
- **Null pointer crash in `stack-detector.js`** — `readFileSafe()` return value for `pnpm-workspace.yaml` now guarded; prevents crash when file exists but is unreadable
|
|
9
|
+
- **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
|
|
10
|
+
- **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
|
|
11
|
+
- **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
|
|
12
|
+
- **Flask default port** — `plan-installer` now maps Flask to port 5000 (was falling through to 8080)
|
|
13
|
+
- **Health-checker dependency chain** — `sync-checker` is now automatically skipped when `manifest-generator` fails, instead of running against missing `sync-map.json`
|
|
14
|
+
- **`pass-json-validator` null template crash** — Added null guard before `typeof` check; `null` no longer passes `typeof === "object"` gate
|
|
15
|
+
- **`pass-json-validator` missing backend frameworks** — Added `"fastify"` and `"flask"` to backend framework list; these stacks previously skipped backend section validation
|
|
16
|
+
- **Init error messages** — Pass 1/2/3 failure messages now include actionable guidance (check output above, retry with `--force`, verify prompt file)
|
|
17
|
+
- **Manifest-generator error context** — `.catch()` handler now prefixes error with tool name
|
|
18
|
+
- **Line counting off-by-one** — `statSafe()` and `manifest-generator stat()` no longer count trailing newline as an extra line
|
|
19
|
+
- **Windows CRLF drift** — `plan-validator` now normalizes `\r\n` → `\n` before content comparison; prevents false drift on Windows
|
|
20
|
+
- **`stale-report.js` mutation** — `Object.assign(ex.summary, patch)` replaced with spread operator to avoid in-place mutation
|
|
21
|
+
- **Undefined in sync-checker Set** — Malformed mappings with missing `sourcePath` no longer insert `undefined` into the registered paths Set
|
|
22
|
+
- **BOM frontmatter detection** — `content-validator` now strips UTF-8 BOM (`\uFEFF`) before checking `---` frontmatter marker
|
|
23
|
+
- **Health-checker stderr loss** — Error output now combines both `stdout` and `stderr` instead of preferring one
|
|
24
|
+
- **`bootstrap.sh` exit code preservation** — EXIT trap now captures and restores `$?` instead of always exiting 0
|
|
25
|
+
- **`bootstrap.sh` NODE_MAJOR stderr** — `node -e` stderr redirected to `/dev/null` to prevent parse failure from noise
|
|
26
|
+
|
|
3
27
|
## [1.6.0] — 2026-04-08
|
|
4
28
|
|
|
5
29
|
### Added
|
package/bin/commands/init.js
CHANGED
|
@@ -242,11 +242,11 @@ async function cmdInit(parsedArgs) {
|
|
|
242
242
|
const elapsed1 = formatElapsed(Date.now() - t1);
|
|
243
243
|
|
|
244
244
|
if (!ok) {
|
|
245
|
-
throw new InitError(`Pass 1-${i} failed`);
|
|
245
|
+
throw new InitError(`Pass 1-${i} failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force`);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
if (!fileExists(pass1Json)) {
|
|
249
|
-
throw new InitError(`pass1-${i}.json was not created`);
|
|
249
|
+
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
250
|
}
|
|
251
251
|
|
|
252
252
|
log(` ✅ pass1-${i}.json created (${elapsed1})`);
|
|
@@ -272,11 +272,11 @@ async function cmdInit(parsedArgs) {
|
|
|
272
272
|
const elapsed2 = formatElapsed(Date.now() - t2);
|
|
273
273
|
|
|
274
274
|
if (!ok) {
|
|
275
|
-
throw new InitError("Pass 2 failed");
|
|
275
|
+
throw new InitError("Pass 2 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
if (!fileExists(pass2Json)) {
|
|
279
|
-
throw new InitError("pass2-merged.json was not created");
|
|
279
|
+
throw new InitError("pass2-merged.json was not created. Claude may have run but not produced expected output.");
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
log(` ✅ pass2-merged.json created (${elapsed2})`);
|
|
@@ -298,11 +298,11 @@ async function cmdInit(parsedArgs) {
|
|
|
298
298
|
const elapsed3 = formatElapsed(Date.now() - t3);
|
|
299
299
|
|
|
300
300
|
if (!ok3) {
|
|
301
|
-
throw new InitError("Pass 3 failed");
|
|
301
|
+
throw new InitError("Pass 3 failed. Check the claude error output above.\n If this persists, try: npx claudeos-core init --force");
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (!fileExists(path.join(PROJECT_ROOT, "CLAUDE.md"))) {
|
|
305
|
-
throw new InitError("CLAUDE.md was not created.
|
|
305
|
+
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
306
|
}
|
|
307
307
|
log(` ✅ Pass 3 complete (${elapsed3})`);
|
|
308
308
|
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
|
|
@@ -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) {
|
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) {
|
|
@@ -158,7 +162,7 @@ async function main() {
|
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
if (require.main === module) {
|
|
161
|
-
main().catch(e => { console.error(e); process.exit(1); });
|
|
165
|
+
main().catch(e => { console.error(`\n ❌ Manifest Generator failed: ${e.message || e}`); process.exit(1); });
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
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,
|
|
@@ -63,7 +63,11 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
63
63
|
|
|
64
64
|
if (primaryTemplate) {
|
|
65
65
|
const primaryBody = readTemplate(primaryTemplate, "pass3");
|
|
66
|
-
|
|
66
|
+
if (!primaryBody) {
|
|
67
|
+
console.log(` ⚠️ pass3 template not found for ${primaryTemplate}, skipping`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let combinedBody = primaryBody;
|
|
67
71
|
|
|
68
72
|
if (templates.backend && templates.frontend && templates.backend !== templates.frontend) {
|
|
69
73
|
const frontendBody = readTemplate(templates.frontend, "pass3");
|
|
@@ -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
|
}
|
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
|
}
|
|
@@ -93,9 +101,9 @@ async function main() {
|
|
|
93
101
|
continue;
|
|
94
102
|
}
|
|
95
103
|
|
|
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+$/, "");
|
|
104
|
+
// File exists — compare content (normalize CRLF + trailing newlines)
|
|
105
|
+
const diskContent = fs.readFileSync(abs, "utf-8").replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
106
|
+
const planContent = b.content.replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
99
107
|
|
|
100
108
|
if (diskContent === planContent) {
|
|
101
109
|
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 ───────
|
|
@@ -83,9 +93,7 @@ async function main() {
|
|
|
83
93
|
for (const m of sm.mappings) {
|
|
84
94
|
const abs = path.join(ROOT, m.sourcePath);
|
|
85
95
|
// 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;
|
|
96
|
+
if (!isWithinRoot(abs)) continue;
|
|
89
97
|
if (!fs.existsSync(abs)) {
|
|
90
98
|
issues.orphan.push({ path: m.sourcePath, plan: m.planFile });
|
|
91
99
|
}
|