claudeos-core 1.2.4 → 1.3.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.
Potentially problematic release.
This version of claudeos-core might be problematic. Click here for more details.
- package/CHANGELOG.md +76 -0
- package/README.de.md +50 -10
- package/README.es.md +51 -10
- package/README.fr.md +51 -10
- package/README.hi.md +51 -10
- package/README.ja.md +52 -10
- package/README.ko.md +51 -10
- package/README.md +62 -15
- package/README.ru.md +51 -10
- package/README.vi.md +51 -10
- package/README.zh-CN.md +51 -10
- package/bin/cli.js +171 -36
- package/bootstrap.sh +71 -23
- package/content-validator/index.js +16 -13
- package/health-checker/index.js +4 -3
- package/lib/safe-fs.js +110 -0
- package/manifest-generator/index.js +13 -7
- package/package.json +4 -2
- package/pass-json-validator/index.js +3 -5
- package/pass-prompts/templates/java-spring/pass1.md +4 -1
- package/pass-prompts/templates/java-spring/pass2.md +3 -3
- package/pass-prompts/templates/java-spring/pass3.md +42 -5
- package/pass-prompts/templates/kotlin-spring/pass1.md +4 -1
- package/pass-prompts/templates/kotlin-spring/pass2.md +5 -5
- package/pass-prompts/templates/kotlin-spring/pass3.md +42 -5
- package/pass-prompts/templates/node-express/pass1.md +4 -1
- package/pass-prompts/templates/node-express/pass2.md +4 -1
- package/pass-prompts/templates/node-express/pass3.md +44 -6
- package/pass-prompts/templates/node-nextjs/pass1.md +14 -4
- package/pass-prompts/templates/node-nextjs/pass2.md +6 -4
- package/pass-prompts/templates/node-nextjs/pass3.md +45 -6
- package/pass-prompts/templates/python-django/pass1.md +4 -2
- package/pass-prompts/templates/python-django/pass2.md +4 -4
- package/pass-prompts/templates/python-django/pass3.md +42 -5
- package/pass-prompts/templates/python-fastapi/pass1.md +4 -1
- package/pass-prompts/templates/python-fastapi/pass2.md +4 -4
- package/pass-prompts/templates/python-fastapi/pass3.md +42 -5
- package/plan-installer/domain-grouper.js +74 -0
- package/plan-installer/index.js +35 -1305
- package/plan-installer/prompt-generator.js +94 -0
- package/plan-installer/stack-detector.js +326 -0
- package/plan-installer/structure-scanner.js +783 -0
- package/plan-validator/index.js +84 -20
- package/sync-checker/index.js +7 -3
package/bin/cli.js
CHANGED
|
@@ -49,42 +49,154 @@ function isValidLang(lang) {
|
|
|
49
49
|
return LANG_CODES.includes(lang);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Interactive language selection (
|
|
52
|
+
// Interactive language selection (arrow key selector)
|
|
53
53
|
function selectLangInteractive() {
|
|
54
54
|
return new Promise((resolve) => {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// Fallback to number input if stdin is not a TTY (e.g., piped input)
|
|
56
|
+
if (!process.stdin.isTTY) {
|
|
57
|
+
const readline = require("readline");
|
|
58
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
log("");
|
|
60
|
+
log("╔══════════════════════════════════════════════════╗");
|
|
61
|
+
log("║ Select generated document language (required) ║");
|
|
62
|
+
log("╚══════════════════════════════════════════════════╝");
|
|
63
|
+
log("");
|
|
64
|
+
log(" Generated files (CLAUDE.md, Standards, Rules,");
|
|
65
|
+
log(" Skills, Guides) will be written in this language.");
|
|
66
|
+
log("");
|
|
67
|
+
LANG_CODES.forEach((code, i) => {
|
|
68
|
+
log(` ${String(i + 1).padStart(2)}. ${code.padEnd(6)} — ${SUPPORTED_LANGS[code]}`);
|
|
69
|
+
});
|
|
70
|
+
log("");
|
|
71
|
+
rl.question(` Enter number (1-${LANG_CODES.length}) or language code: `, (answer) => {
|
|
72
|
+
rl.close();
|
|
73
|
+
const trimmed = answer.trim();
|
|
74
|
+
const num = parseInt(trimmed);
|
|
75
|
+
if (num >= 1 && num <= LANG_CODES.length) { resolve(LANG_CODES[num - 1]); return; }
|
|
76
|
+
if (isValidLang(trimmed)) { resolve(trimmed); return; }
|
|
77
|
+
log(`\n ❌ Invalid selection: "${trimmed}"`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Arrow key interactive selector
|
|
84
|
+
let selected = 0;
|
|
85
|
+
const total = LANG_CODES.length;
|
|
86
|
+
|
|
87
|
+
// Description text per language (shown when hovering)
|
|
88
|
+
const DESC = {
|
|
89
|
+
en: "Generated files (CLAUDE.md, Standards, Rules,\n Skills, Guides) will be written in English.",
|
|
90
|
+
ko: "생성되는 파일(CLAUDE.md, Standards, Rules,\n Skills, Guides)이 한국어로 작성됩니다.",
|
|
91
|
+
"zh-CN": "生成的文件(CLAUDE.md、Standards、Rules、\n Skills、Guides)将以简体中文编写。",
|
|
92
|
+
ja: "生成されるファイル(CLAUDE.md、Standards、Rules、\n Skills、Guides)は日本語で作成されます。",
|
|
93
|
+
es: "Los archivos generados (CLAUDE.md, Standards, Rules,\n Skills, Guides) se escribirán en español.",
|
|
94
|
+
vi: "Các file được tạo (CLAUDE.md, Standards, Rules,\n Skills, Guides) sẽ được viết bằng tiếng Việt.",
|
|
95
|
+
hi: "जनरेट की गई फ़ाइलें (CLAUDE.md, Standards, Rules,\n Skills, Guides) हिन्दी में लिखी जाएंगी।",
|
|
96
|
+
ru: "Сгенерированные файлы (CLAUDE.md, Standards, Rules,\n Skills, Guides) будут написаны на русском языке.",
|
|
97
|
+
fr: "Les fichiers générés (CLAUDE.md, Standards, Rules,\n Skills, Guides) seront rédigés en français.",
|
|
98
|
+
de: "Die generierten Dateien (CLAUDE.md, Standards, Rules,\n Skills, Guides) werden auf Deutsch verfasst.",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function render() {
|
|
102
|
+
const output = [];
|
|
103
|
+
// Description line (changes with selection)
|
|
104
|
+
const code = LANG_CODES[selected];
|
|
105
|
+
const descLines = (DESC[code] || DESC.en).split("\n");
|
|
106
|
+
descLines.forEach(l => output.push(` ${l}`));
|
|
107
|
+
output.push("");
|
|
108
|
+
// Language list
|
|
109
|
+
const BOLD_CYAN = "\x1b[1;36m";
|
|
110
|
+
const RESET = "\x1b[0m";
|
|
111
|
+
for (let i = 0; i < total; i++) {
|
|
112
|
+
const c = LANG_CODES[i];
|
|
113
|
+
const label = SUPPORTED_LANGS[c];
|
|
114
|
+
const num = String(i + 1).padStart(2);
|
|
115
|
+
if (i === selected) {
|
|
116
|
+
output.push(` ${BOLD_CYAN}❯ ${num}. ${c.padEnd(6)} — ${label}${RESET}`);
|
|
117
|
+
} else {
|
|
118
|
+
output.push(` ${num}. ${c.padEnd(6)} — ${label}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
output.push("");
|
|
122
|
+
const DIM = "\x1b[2m";
|
|
123
|
+
output.push(` \x1b[1m↑↓\x1b[0m${DIM} Move ${RESET} \x1b[1mEnter\x1b[0m${DIM} Select ${RESET} \x1b[1mESC\x1b[0m${DIM} Cancel${RESET}`);
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
57
126
|
|
|
127
|
+
// Print header once
|
|
58
128
|
log("");
|
|
59
129
|
log("╔══════════════════════════════════════════════════╗");
|
|
60
|
-
log("║ Select
|
|
130
|
+
log("║ Select generated document language (required) ║");
|
|
61
131
|
log("╚══════════════════════════════════════════════════╝");
|
|
62
132
|
log("");
|
|
63
|
-
LANG_CODES.forEach((code, i) => {
|
|
64
|
-
log(` ${String(i + 1).padStart(2)}. ${code.padEnd(6)} — ${SUPPORTED_LANGS[code]}`);
|
|
65
|
-
});
|
|
66
|
-
log("");
|
|
67
133
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
134
|
+
// Initial render
|
|
135
|
+
const lines = render();
|
|
136
|
+
const listHeight = lines.length;
|
|
137
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
71
138
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
139
|
+
// Raw mode for keypress detection
|
|
140
|
+
process.stdin.setRawMode(true);
|
|
141
|
+
process.stdin.resume();
|
|
142
|
+
|
|
143
|
+
process.stdin.on("data", (key) => {
|
|
144
|
+
const k = key.toString();
|
|
145
|
+
|
|
146
|
+
// Ctrl+C
|
|
147
|
+
if (k === "\x03") {
|
|
148
|
+
process.stdin.setRawMode(false);
|
|
149
|
+
log("\n Cancelled.\n");
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ESC (single byte only — arrow keys send \x1b[ which is 3 bytes)
|
|
154
|
+
if (k === "\x1b" && key.length === 1) {
|
|
155
|
+
process.stdin.setRawMode(false);
|
|
156
|
+
log("\n Cancelled.\n");
|
|
157
|
+
process.exit(0);
|
|
77
158
|
}
|
|
78
159
|
|
|
79
|
-
//
|
|
80
|
-
if (
|
|
81
|
-
|
|
160
|
+
// Up arrow
|
|
161
|
+
if (k === "\x1b[A") {
|
|
162
|
+
selected = (selected - 1 + total) % total;
|
|
163
|
+
}
|
|
164
|
+
// Down arrow
|
|
165
|
+
else if (k === "\x1b[B") {
|
|
166
|
+
selected = (selected + 1) % total;
|
|
167
|
+
}
|
|
168
|
+
// Number keys 1-9 (direct jump)
|
|
169
|
+
else if (k >= "1" && k <= "9" && parseInt(k) <= total) {
|
|
170
|
+
selected = parseInt(k) - 1;
|
|
171
|
+
}
|
|
172
|
+
// 0 for 10
|
|
173
|
+
else if (k === "0" && total >= 10) {
|
|
174
|
+
selected = 9;
|
|
175
|
+
}
|
|
176
|
+
// Enter
|
|
177
|
+
else if (k === "\r" || k === "\n") {
|
|
178
|
+
process.stdin.setRawMode(false);
|
|
179
|
+
process.stdin.pause();
|
|
180
|
+
process.stdin.removeAllListeners("data");
|
|
181
|
+
// Clear the list and reprint final selection
|
|
182
|
+
process.stdout.write(`\x1b[${listHeight}A`);
|
|
183
|
+
for (let i = 0; i < listHeight; i++) {
|
|
184
|
+
process.stdout.write("\x1b[2K\n");
|
|
185
|
+
}
|
|
186
|
+
process.stdout.write(`\x1b[${listHeight}A`);
|
|
187
|
+
log(` ✅ ${LANG_CODES[selected]} — ${SUPPORTED_LANGS[LANG_CODES[selected]]}`);
|
|
188
|
+
log("");
|
|
189
|
+
resolve(LANG_CODES[selected]);
|
|
82
190
|
return;
|
|
83
191
|
}
|
|
192
|
+
else {
|
|
193
|
+
return; // Ignore other keys
|
|
194
|
+
}
|
|
84
195
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
196
|
+
// Redraw list (clear each line to prevent ghost text from different-length strings)
|
|
197
|
+
process.stdout.write(`\x1b[${listHeight}A`);
|
|
198
|
+
const updated = render();
|
|
199
|
+
process.stdout.write(updated.map(l => `\x1b[2K${l}`).join("\n") + "\n");
|
|
88
200
|
});
|
|
89
201
|
});
|
|
90
202
|
}
|
|
@@ -147,7 +259,9 @@ function readFile(p) {
|
|
|
147
259
|
}
|
|
148
260
|
|
|
149
261
|
function injectProjectRoot(text) {
|
|
150
|
-
|
|
262
|
+
// Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
|
|
263
|
+
const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
|
|
264
|
+
return text.replace(/\{\{PROJECT_ROOT\}\}/g, normalizedRoot);
|
|
151
265
|
}
|
|
152
266
|
|
|
153
267
|
// ─── Command: init ───────────────────────────────────────
|
|
@@ -269,6 +383,11 @@ async function cmdInit() {
|
|
|
269
383
|
process.exit(1);
|
|
270
384
|
}
|
|
271
385
|
const totalGroups = domainGroups.totalGroups;
|
|
386
|
+
if (!totalGroups || typeof totalGroups !== "number" || totalGroups < 1) {
|
|
387
|
+
log(` ❌ domain-groups.json has invalid totalGroups: ${totalGroups}`);
|
|
388
|
+
log(" Re-run plan-installer or check claudeos-core/generated/");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
272
391
|
|
|
273
392
|
// Load pass1 prompts by type
|
|
274
393
|
const pass1Prompts = {};
|
|
@@ -302,8 +421,14 @@ async function cmdInit() {
|
|
|
302
421
|
|
|
303
422
|
const pass1Json = path.join(GENERATED_DIR, `pass1-${i}.json`);
|
|
304
423
|
if (fileExists(pass1Json)) {
|
|
305
|
-
|
|
306
|
-
|
|
424
|
+
try {
|
|
425
|
+
const existing = JSON.parse(readFile(pass1Json));
|
|
426
|
+
if (existing && existing.analysisPerDomain) {
|
|
427
|
+
log(` ⏭️ pass1-${i}.json already exists, skipping`);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
} catch { /* malformed — re-run */ }
|
|
431
|
+
log(` ⚠️ pass1-${i}.json exists but is malformed, re-running`);
|
|
307
432
|
}
|
|
308
433
|
|
|
309
434
|
// Select prompt for this type
|
|
@@ -342,9 +467,12 @@ async function cmdInit() {
|
|
|
342
467
|
if (fileExists(pass2Json)) {
|
|
343
468
|
log(" ⏭️ pass2-merged.json already exists, skipping");
|
|
344
469
|
} else {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
470
|
+
const pass2PromptFile = path.join(GENERATED_DIR, "pass2-prompt.md");
|
|
471
|
+
if (!fileExists(pass2PromptFile)) {
|
|
472
|
+
log(" ❌ pass2-prompt.md not found. Re-run plan-installer.");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
let prompt = injectProjectRoot(readFile(pass2PromptFile));
|
|
348
476
|
|
|
349
477
|
const ok = runClaudePrompt(prompt, { ignoreError: true });
|
|
350
478
|
|
|
@@ -365,9 +493,12 @@ async function cmdInit() {
|
|
|
365
493
|
// ─── [6] Pass 3: Generate + verify ─────────────────────────
|
|
366
494
|
header("[6] Pass 3 — Generating all files...");
|
|
367
495
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
496
|
+
const pass3PromptFile = path.join(GENERATED_DIR, "pass3-prompt.md");
|
|
497
|
+
if (!fileExists(pass3PromptFile)) {
|
|
498
|
+
log(" ❌ pass3-prompt.md not found. Re-run plan-installer.");
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
let prompt = injectProjectRoot(readFile(pass3PromptFile));
|
|
371
502
|
|
|
372
503
|
const ok = runClaudePrompt(prompt, { ignoreError: true });
|
|
373
504
|
|
|
@@ -453,8 +584,12 @@ function countFiles() {
|
|
|
453
584
|
try {
|
|
454
585
|
let count = 0;
|
|
455
586
|
const skipDirs = ["node_modules", "generated"];
|
|
587
|
+
const visited = new Set();
|
|
456
588
|
const scan = (dir) => {
|
|
457
589
|
if (!fs.existsSync(dir)) return;
|
|
590
|
+
const realDir = fs.realpathSync(dir);
|
|
591
|
+
if (visited.has(realDir)) return;
|
|
592
|
+
visited.add(realDir);
|
|
458
593
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
459
594
|
if (skipDirs.includes(entry.name)) continue;
|
|
460
595
|
const full = path.join(dir, entry.name);
|
|
@@ -465,7 +600,7 @@ function countFiles() {
|
|
|
465
600
|
scan(path.join(PROJECT_ROOT, ".claude"));
|
|
466
601
|
scan(path.join(PROJECT_ROOT, "claudeos-core"));
|
|
467
602
|
return count;
|
|
468
|
-
} catch {
|
|
603
|
+
} catch (e) {
|
|
469
604
|
return "?";
|
|
470
605
|
}
|
|
471
606
|
}
|
|
@@ -475,7 +610,7 @@ function countPass1Files() {
|
|
|
475
610
|
return fs
|
|
476
611
|
.readdirSync(GENERATED_DIR)
|
|
477
612
|
.filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
|
|
478
|
-
} catch {
|
|
613
|
+
} catch (e) {
|
|
479
614
|
return 0;
|
|
480
615
|
}
|
|
481
616
|
}
|
|
@@ -535,18 +670,18 @@ const args = process.argv.slice(2);
|
|
|
535
670
|
const parsedArgs = parseArgs(args);
|
|
536
671
|
const command = parsedArgs.command;
|
|
537
672
|
|
|
538
|
-
if (!command || command === "--help"
|
|
673
|
+
if (!command || command === "--help") {
|
|
539
674
|
showHelp();
|
|
540
675
|
process.exit(0);
|
|
541
676
|
}
|
|
542
677
|
|
|
543
|
-
if (command === "--version"
|
|
678
|
+
if (command === "--version") {
|
|
544
679
|
try {
|
|
545
680
|
const pkg = JSON.parse(
|
|
546
681
|
readFile(path.join(TOOLS_DIR, "package.json"))
|
|
547
682
|
);
|
|
548
683
|
log(`claudeos-core v${pkg.version}`);
|
|
549
|
-
} catch {
|
|
684
|
+
} catch (e) {
|
|
550
685
|
log("claudeos-core (version unknown)");
|
|
551
686
|
}
|
|
552
687
|
process.exit(0);
|
package/bootstrap.sh
CHANGED
|
@@ -23,6 +23,9 @@ GENERATED_DIR="$PROJECT_ROOT/claudeos-core/generated"
|
|
|
23
23
|
|
|
24
24
|
cd "$PROJECT_ROOT"
|
|
25
25
|
|
|
26
|
+
# Cleanup temp files on exit (Ctrl+C, errors, etc.)
|
|
27
|
+
trap 'rm -f "$GENERATED_DIR"/_tmp_*.md "$GENERATED_DIR"/_tmp_*.md.final 2>/dev/null' EXIT
|
|
28
|
+
|
|
26
29
|
# ─── Language selection (required) ──────────────────────────────
|
|
27
30
|
SUPPORTED_LANGS=("en" "ko" "zh-CN" "ja" "es" "vi" "hi" "ru" "fr" "de")
|
|
28
31
|
LANG_LABELS=("English" "한국어 (Korean)" "简体中文 (Chinese Simplified)" "日本語 (Japanese)" "Español (Spanish)" "Tiếng Việt (Vietnamese)" "हिन्दी (Hindi)" "Русский (Russian)" "Français (French)" "Deutsch (German)")
|
|
@@ -32,7 +35,7 @@ CLAUDEOS_LANG=""
|
|
|
32
35
|
# Parse --lang argument
|
|
33
36
|
while [[ $# -gt 0 ]]; do
|
|
34
37
|
case $1 in
|
|
35
|
-
--lang) CLAUDEOS_LANG="$2"; shift 2 ;;
|
|
38
|
+
--lang) [ -z "$2" ] && echo " ❌ --lang requires a value" && exit 1; CLAUDEOS_LANG="$2"; shift 2 ;;
|
|
36
39
|
--lang=*) CLAUDEOS_LANG="${1#*=}"; shift ;;
|
|
37
40
|
*) echo "⚠️ Unknown argument: $1 (ignored)"; shift ;;
|
|
38
41
|
esac
|
|
@@ -42,17 +45,21 @@ done
|
|
|
42
45
|
if [ -z "$CLAUDEOS_LANG" ]; then
|
|
43
46
|
echo ""
|
|
44
47
|
echo "╔══════════════════════════════════════════════════╗"
|
|
45
|
-
echo "║ Select
|
|
48
|
+
echo "║ Select generated document language (required) ║"
|
|
46
49
|
echo "╚══════════════════════════════════════════════════╝"
|
|
47
50
|
echo ""
|
|
51
|
+
echo " Generated files (CLAUDE.md, Standards, Rules,"
|
|
52
|
+
echo " Skills, Guides) will be written in this language."
|
|
53
|
+
echo ""
|
|
48
54
|
for i in "${!SUPPORTED_LANGS[@]}"; do
|
|
49
55
|
printf " %2d. %-6s — %s\n" "$((i+1))" "${SUPPORTED_LANGS[$i]}" "${LANG_LABELS[$i]}"
|
|
50
56
|
done
|
|
51
57
|
echo ""
|
|
52
|
-
|
|
58
|
+
LANG_COUNT=${#SUPPORTED_LANGS[@]}
|
|
59
|
+
read -rp " Enter number (1-${LANG_COUNT}) or language code: " LANG_INPUT
|
|
53
60
|
|
|
54
61
|
# Accept number
|
|
55
|
-
if [[ "$LANG_INPUT" =~ ^[0-9]+$ ]] && [ "$LANG_INPUT" -ge 1 ] && [ "$LANG_INPUT" -le
|
|
62
|
+
if [[ "$LANG_INPUT" =~ ^[0-9]+$ ]] && [ "$LANG_INPUT" -ge 1 ] && [ "$LANG_INPUT" -le "$LANG_COUNT" ]; then
|
|
56
63
|
CLAUDEOS_LANG="${SUPPORTED_LANGS[$((LANG_INPUT-1))]}"
|
|
57
64
|
else
|
|
58
65
|
# Accept language code
|
|
@@ -76,12 +83,42 @@ fi
|
|
|
76
83
|
export CLAUDEOS_LANG
|
|
77
84
|
export CLAUDEOS_ROOT="$PROJECT_ROOT"
|
|
78
85
|
|
|
79
|
-
# ───
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
# ─── Prerequisites check ──────────────────────────────────────
|
|
87
|
+
if ! command -v node &> /dev/null; then
|
|
88
|
+
echo ""
|
|
89
|
+
echo " ❌ Node.js not found."
|
|
90
|
+
echo " Install: https://nodejs.org/"
|
|
91
|
+
echo ""
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
|
|
96
|
+
if ! [[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || [ "$NODE_MAJOR" -lt 18 ]; then
|
|
97
|
+
echo ""
|
|
98
|
+
echo " ❌ Node.js v18+ required (current: v$(node --version))"
|
|
99
|
+
echo " Install: https://nodejs.org/"
|
|
100
|
+
echo ""
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if ! command -v claude &> /dev/null; then
|
|
105
|
+
echo ""
|
|
106
|
+
echo " ❌ Claude Code CLI not found."
|
|
107
|
+
echo " Install: https://code.claude.com/docs/en/overview"
|
|
108
|
+
echo " Then run: claude (and complete authentication)"
|
|
109
|
+
echo ""
|
|
110
|
+
exit 1
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if ! command -v perl &> /dev/null; then
|
|
114
|
+
echo ""
|
|
115
|
+
echo " ❌ perl not found (required for placeholder substitution)."
|
|
116
|
+
echo " Install perl or use the Node.js CLI instead: npx claudeos-core init"
|
|
117
|
+
echo ""
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
|
|
85
122
|
|
|
86
123
|
echo ""
|
|
87
124
|
echo "╔════════════════════════════════════════════════════╗"
|
|
@@ -176,24 +213,25 @@ for i in $(seq 1 "$TOTAL_GROUPS"); do
|
|
|
176
213
|
if [ ! -f "$PROMPT_FILE" ]; then
|
|
177
214
|
PROMPT_FILE="$GENERATED_DIR/pass1-prompt.md"
|
|
178
215
|
fi
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# Substitute placeholders via temp file (avoids sed special char issues with &, \, etc.)
|
|
216
|
+
# Substitute placeholders via temp file (avoids sed special char issues and $() newline stripping)
|
|
182
217
|
TMP_PROMPT="$GENERATED_DIR/_tmp_pass1_prompt.md"
|
|
183
|
-
|
|
218
|
+
cp "$PROMPT_FILE" "$TMP_PROMPT"
|
|
184
219
|
# Use perl with $ENV{} for safe literal replacement (no shell interpolation into Perl code)
|
|
185
220
|
export _DOMAIN_LIST="$DOMAIN_LIST"
|
|
186
221
|
export _PASS_NUM="$i"
|
|
187
222
|
perl -pi -e 's/\{\{DOMAIN_GROUP\}\}/$ENV{_DOMAIN_LIST}/g' "$TMP_PROMPT"
|
|
188
223
|
perl -pi -e 's/\{\{PASS_NUM\}\}/$ENV{_PASS_NUM}/g' "$TMP_PROMPT"
|
|
189
|
-
# inject_project_root
|
|
190
|
-
|
|
191
|
-
|
|
224
|
+
# inject_project_root: pipe through perl, write to final temp file
|
|
225
|
+
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
226
|
+
perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$TMP_PROMPT" > "${TMP_PROMPT}.final"
|
|
227
|
+
mv "${TMP_PROMPT}.final" "$TMP_PROMPT"
|
|
192
228
|
|
|
193
|
-
if ! (cd "$PROJECT_ROOT" && claude -p
|
|
229
|
+
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PROMPT" | claude -p --dangerously-skip-permissions); then
|
|
230
|
+
rm -f "$TMP_PROMPT"
|
|
194
231
|
echo " ❌ Pass 1-${i} failed. Aborting."
|
|
195
232
|
exit 1
|
|
196
233
|
fi
|
|
234
|
+
rm -f "$TMP_PROMPT"
|
|
197
235
|
|
|
198
236
|
# Verify JSON was created
|
|
199
237
|
if [ ! -f "$GENERATED_DIR/pass1-${i}.json" ]; then
|
|
@@ -203,6 +241,7 @@ for i in $(seq 1 "$TOTAL_GROUPS"); do
|
|
|
203
241
|
|
|
204
242
|
echo " ✅ pass1-${i}.json created"
|
|
205
243
|
done
|
|
244
|
+
unset _DOMAIN_LIST _PASS_NUM _PROJECT_ROOT
|
|
206
245
|
echo ""
|
|
207
246
|
|
|
208
247
|
# ─── [5] Pass 2: Merge analysis results ─────────────────────────
|
|
@@ -213,12 +252,16 @@ echo "━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
213
252
|
if [ -f "$GENERATED_DIR/pass2-merged.json" ]; then
|
|
214
253
|
echo " ⏭️ pass2-merged.json already exists, skipping"
|
|
215
254
|
else
|
|
216
|
-
|
|
255
|
+
TMP_PASS2="$GENERATED_DIR/_tmp_pass2_prompt.md"
|
|
256
|
+
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
257
|
+
perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass2-prompt.md" > "$TMP_PASS2"
|
|
217
258
|
|
|
218
|
-
if ! (cd "$PROJECT_ROOT" && claude -p
|
|
259
|
+
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS2" | claude -p --dangerously-skip-permissions); then
|
|
260
|
+
rm -f "$TMP_PASS2"
|
|
219
261
|
echo " ❌ Pass 2 failed. Aborting."
|
|
220
262
|
exit 1
|
|
221
263
|
fi
|
|
264
|
+
rm -f "$TMP_PASS2"
|
|
222
265
|
|
|
223
266
|
if [ ! -f "$GENERATED_DIR/pass2-merged.json" ]; then
|
|
224
267
|
echo " ❌ pass2-merged.json was not created. Aborting."
|
|
@@ -233,17 +276,22 @@ echo ""
|
|
|
233
276
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
234
277
|
echo "[6] Pass 3 — Generating all files..."
|
|
235
278
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
236
|
-
|
|
279
|
+
TMP_PASS3="$GENERATED_DIR/_tmp_pass3_prompt.md"
|
|
280
|
+
export _PROJECT_ROOT="$PROJECT_ROOT"
|
|
281
|
+
perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g' "$GENERATED_DIR/pass3-prompt.md" > "$TMP_PASS3"
|
|
237
282
|
|
|
238
|
-
if ! (cd "$PROJECT_ROOT" && claude -p
|
|
283
|
+
if ! (cd "$PROJECT_ROOT" && cat "$TMP_PASS3" | claude -p --dangerously-skip-permissions); then
|
|
284
|
+
rm -f "$TMP_PASS3"
|
|
239
285
|
echo " ❌ Pass 3 failed. Aborting."
|
|
240
286
|
exit 1
|
|
241
287
|
fi
|
|
288
|
+
rm -f "$TMP_PASS3"
|
|
242
289
|
|
|
243
290
|
if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
|
|
244
291
|
echo " ❌ CLAUDE.md was not created. Pass 3 may have failed silently."
|
|
245
292
|
exit 1
|
|
246
293
|
fi
|
|
294
|
+
unset _PROJECT_ROOT
|
|
247
295
|
echo ""
|
|
248
296
|
|
|
249
297
|
# ─── [7] Run verification tools ───────────────────────────────
|
|
@@ -271,7 +319,7 @@ fi
|
|
|
271
319
|
echo ""
|
|
272
320
|
|
|
273
321
|
# ─── Complete ───────────────────────────────────────────────
|
|
274
|
-
TOTAL_FILES=$(find .claude claudeos-core -type f 2>/dev/null | grep -v node_modules | wc -l | tr -d ' ')
|
|
322
|
+
TOTAL_FILES=$(find .claude claudeos-core -type f 2>/dev/null | grep -v '/node_modules/' | grep -v '/generated/' | wc -l | tr -d ' ')
|
|
275
323
|
TOTAL_GROUPS_DONE=$TOTAL_GROUPS
|
|
276
324
|
PASS1_FILES=$(ls -1 "$GENERATED_DIR"/pass1-*.json 2>/dev/null | wc -l | tr -d ' ')
|
|
277
325
|
|
|
@@ -33,9 +33,9 @@ const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
|
|
|
33
33
|
function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
|
|
34
34
|
|
|
35
35
|
async function main() {
|
|
36
|
-
console.log("\n
|
|
36
|
+
console.log("\n╔═══════════════════════════════════════╗");
|
|
37
37
|
console.log("║ ClaudeOS-Core — Content Validator ║");
|
|
38
|
-
console.log("
|
|
38
|
+
console.log("╚═══════════════════════════════════════╝\n");
|
|
39
39
|
|
|
40
40
|
const errors = [];
|
|
41
41
|
const warnings = [];
|
|
@@ -83,7 +83,10 @@ async function main() {
|
|
|
83
83
|
const enKeywords = SECTION_KEYWORDS.en;
|
|
84
84
|
for (let i = 0; i < enKeywords.length; i++) {
|
|
85
85
|
const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
|
|
86
|
-
const found = candidates.some(kw =>
|
|
86
|
+
const found = candidates.some(kw => {
|
|
87
|
+
const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "im");
|
|
88
|
+
return re.test(content);
|
|
89
|
+
});
|
|
87
90
|
if (!found) {
|
|
88
91
|
warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${enKeywords[i]}' / '${langKeywords[i]}' section is missing` });
|
|
89
92
|
}
|
|
@@ -102,15 +105,13 @@ async function main() {
|
|
|
102
105
|
errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
105
|
-
// All rules
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key — use paths: [\"**/*\"] to ensure rule is always loaded" });
|
|
113
|
-
}
|
|
108
|
+
// All rules must have paths: frontmatter (value varies by category — e.g. ["**/*"] for core/backend, scoped patterns for infra/sync)
|
|
109
|
+
const hasFrontmatter = c.startsWith("---");
|
|
110
|
+
const hasPathsKey = c.includes("paths:");
|
|
111
|
+
if (!hasFrontmatter) {
|
|
112
|
+
warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
|
|
113
|
+
} else if (!hasPathsKey) {
|
|
114
|
+
warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key" });
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
console.log(` ${ruleFiles.length} files checked`);
|
|
@@ -164,7 +165,9 @@ async function main() {
|
|
|
164
165
|
if (!badKeywords.some(kw => c.includes(kw))) {
|
|
165
166
|
warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "No incorrect example (❌) found" });
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
// Check for markdown table: at least one line with | col | col | pattern
|
|
169
|
+
const hasMarkdownTable = /\|.+\|.+\|/.test(c);
|
|
170
|
+
if (!hasMarkdownTable) {
|
|
168
171
|
warnings.push({ file: r, type: "NO_TABLE", msg: "Rules summary table appears to be missing" });
|
|
169
172
|
}
|
|
170
173
|
// Kotlin code block check: core and backend standard files should contain ```kotlin blocks
|
package/health-checker/index.js
CHANGED
|
@@ -36,9 +36,9 @@ function run(name, script) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function main() {
|
|
39
|
-
console.log("\n
|
|
39
|
+
console.log("\n╔═══════════════════════════════════════╗");
|
|
40
40
|
console.log("║ ClaudeOS-Core — Health Checker ║");
|
|
41
|
-
console.log("
|
|
41
|
+
console.log("╚═══════════════════════════════════════╝\n");
|
|
42
42
|
|
|
43
43
|
// ─── [0] Run manifest-generator first (prerequisite) ──────────────────
|
|
44
44
|
// Must run first because sync-checker reads sync-map.json
|
|
@@ -112,10 +112,11 @@ function main() {
|
|
|
112
112
|
}
|
|
113
113
|
ex.generatedAt = new Date().toISOString();
|
|
114
114
|
ex.healthCheck = { results, status: hasErr ? "fail" : "pass" };
|
|
115
|
+
if (!ex.summary) ex.summary = {};
|
|
115
116
|
ex.summary = {
|
|
116
117
|
...ex.summary,
|
|
117
118
|
totalIssues: results.filter((r) => r.status === "fail").length,
|
|
118
|
-
|
|
119
|
+
healthStatus: hasErr ? "fail" : "ok",
|
|
119
120
|
};
|
|
120
121
|
fs.writeFileSync(rp, JSON.stringify(ex, null, 2));
|
|
121
122
|
}
|