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 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 {
@@ -96,9 +96,9 @@ async function cmdInit(parsedArgs) {
96
96
 
97
97
  if (existingPass1.length > 0 || pass2Exists) {
98
98
  if (parsedArgs.force) {
99
- // --force: delete without prompt
100
- for (const f of existingPass1) fs.unlinkSync(path.join(GENERATED_DIR, f));
101
- if (pass2Exists) fs.unlinkSync(path.join(GENERATED_DIR, "pass2-merged.json"));
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. Pass 3 may have failed silently.");
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, perl
9
- # For cross-platform use without perl, use: node bin/cli.js init
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
- if ! command -v perl &> /dev/null; then
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 (avoids sed special char issues and $() newline stripping)
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
- perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$TMP_PROMPT" > "${TMP_PROMPT}.final"
260
- mv "${TMP_PROMPT}.final" "$TMP_PROMPT"
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
- perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass2-prompt.md" > "$TMP_PASS2"
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
- perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass3-prompt.md" > "$TMP_PASS3"
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(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "im");
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 (---)" });
@@ -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 || e.stderr || e.message || "", exitCode: e.status || 1 };
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. Subsequent sync-checker results may be inaccurate.");
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" }
@@ -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
  };
@@ -27,8 +27,7 @@ function updateStaleReport(genDir, key, data, summaryPatch) {
27
27
  }
28
28
  ex[key] = data;
29
29
  if (summaryPatch) {
30
- if (!ex.summary) ex.summary = {};
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
- const s = fs.statSync(f);
42
- const c = fs.readFileSync(f, "utf-8");
43
- return {
44
- lines: c.split("\n").length,
45
- bytes: s.size,
46
- modified: s.mtime.toISOString().split("T")[0],
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 blocks = extractFileBlocksFromFile(p);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeos-core",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Auto-generate Claude Code documentation from your actual source code — Standards, Rules, Skills, and Guides tailored to your project",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -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 >= MAX_FILES_PER_GROUP || current.length >= MAX_DOMAINS_PER_GROUP)) {
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;
@@ -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
- console.log(` 🌐 Language: ${langData.labels[lang]} (Pass 3 output)`);
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
- let combinedBody = primaryBody || "";
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) detectedPattern = domainMap[Object.keys(domainMap)[0]].pattern;
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 => f.includes(`/${m}/`) || f.startsWith(`${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
- const wm = [...wy.matchAll(/- ['"]?([^'"#\n]+)['"]?/g)].map(m => m[1].trim());
256
- if (wm.length > 0) wsPatterns = wm;
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.replace(/\/?\*?\*?$/, "/*/package.json");
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.framework === "django" || stack.framework === "fastapi" || stack.framework === "flask") {
44
+ if (stack.language === "python") {
45
45
  const r = await scanPythonDomains(stack, ROOT);
46
46
  backendDomains.push(...r.backendDomains);
47
47
  }
@@ -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
- const resolvedAbs = path.resolve(abs);
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 only)
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++;
@@ -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
- const resolvedAbs = path.resolve(abs);
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
  }