claudeos-core 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1649 -481
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +64 -5
- package/README.es.md +64 -5
- package/README.fr.md +64 -5
- package/README.hi.md +64 -5
- package/README.ja.md +64 -5
- package/README.ko.md +1018 -959
- package/README.md +1020 -960
- package/README.ru.md +66 -5
- package/README.vi.md +1019 -960
- package/README.zh-CN.md +64 -5
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1518
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -436
- package/lib/env-parser.js +317 -0
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1052
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/angular/pass3.md +28 -13
- package/pass-prompts/templates/common/claude-md-scaffold.md +686 -0
- package/pass-prompts/templates/common/pass3-footer.md +402 -39
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -302
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/java-spring/pass3.md +31 -21
- package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
- package/pass-prompts/templates/node-express/pass3.md +30 -21
- package/pass-prompts/templates/node-fastify/pass3.md +28 -14
- package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
- package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/node-vite/pass3.md +30 -13
- package/pass-prompts/templates/python-django/pass3.md +32 -21
- package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/pass-prompts/templates/python-flask/pass3.md +31 -13
- package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -129
- package/plan-installer/prompt-generator.js +188 -128
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -466
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
package/bin/lib/cli-utils.js
CHANGED
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — CLI Utilities
|
|
3
|
-
*
|
|
4
|
-
* Shared constants, execution helpers, and filesystem utilities for the CLI.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { execSync, spawn } = require("child_process");
|
|
8
|
-
const fs = require("fs");
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const { ensureDir, existsSafe } = require("../../lib/safe-fs");
|
|
11
|
-
|
|
12
|
-
// ─── Path configuration ──────────────────────────────────────────
|
|
13
|
-
const TOOLS_DIR = path.resolve(__dirname, "../..");
|
|
14
|
-
const PROJECT_ROOT = process.cwd();
|
|
15
|
-
const GENERATED_DIR = path.join(PROJECT_ROOT, "claudeos-core/generated");
|
|
16
|
-
|
|
17
|
-
// ─── Language configuration ──────────────────────────────────────
|
|
18
|
-
// Single source of truth: lib/language-config.js. We re-export under the
|
|
19
|
-
// historical name SUPPORTED_LANGS so existing imports keep working.
|
|
20
|
-
const { LANGUAGES: SUPPORTED_LANGS, LANG_CODES, isValidLang } = require("../../lib/language-config");
|
|
21
|
-
|
|
22
|
-
// ─── Output ─────────────────────────────────────────────────────
|
|
23
|
-
function log(msg) {
|
|
24
|
-
console.log(msg);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function header(title) {
|
|
28
|
-
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
29
|
-
log(title);
|
|
30
|
-
log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── Execution ──────────────────────────────────────────────────
|
|
34
|
-
function run(cmd, options = {}) {
|
|
35
|
-
try {
|
|
36
|
-
execSync(cmd, {
|
|
37
|
-
cwd: options.cwd || PROJECT_ROOT,
|
|
38
|
-
stdio: options.silent ? ["pipe", "pipe", "pipe"] : "inherit",
|
|
39
|
-
encoding: "utf-8",
|
|
40
|
-
timeout: options.timeout || 0,
|
|
41
|
-
});
|
|
42
|
-
return true;
|
|
43
|
-
} catch (e) {
|
|
44
|
-
if (options.ignoreError) return false;
|
|
45
|
-
throw e;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Run claude -p: pass prompt via stdin (no shell pipe — avoids command injection)
|
|
50
|
-
function runClaudePrompt(prompt, options = {}) {
|
|
51
|
-
try {
|
|
52
|
-
execSync("claude -p --dangerously-skip-permissions", {
|
|
53
|
-
input: prompt,
|
|
54
|
-
cwd: options.cwd || PROJECT_ROOT,
|
|
55
|
-
stdio: ["pipe", "inherit", "inherit"],
|
|
56
|
-
encoding: "utf-8",
|
|
57
|
-
timeout: 0,
|
|
58
|
-
});
|
|
59
|
-
return true;
|
|
60
|
-
} catch (e) {
|
|
61
|
-
if (options.ignoreError) return false;
|
|
62
|
-
throw e;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Async variant of runClaudePrompt using spawn — does NOT block the event loop,
|
|
67
|
-
// so the caller can run setInterval-based progress callbacks concurrently.
|
|
68
|
-
// Resolves to true on exit code 0, false otherwise (no throw; mirrors ignoreError:true).
|
|
69
|
-
// onTick is invoked every tickMs while claude is running; stopped on close.
|
|
70
|
-
// shell:true on Windows so that `claude.cmd`/`claude.ps1` shims resolve via PATH.
|
|
71
|
-
function runClaudePromptAsync(prompt, options = {}) {
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
let child = null;
|
|
74
|
-
let tickTimer = null;
|
|
75
|
-
const cleanup = () => { if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } };
|
|
76
|
-
const bail = () => {
|
|
77
|
-
cleanup();
|
|
78
|
-
// Best-effort kill so we don't leave orphaned claude processes when we
|
|
79
|
-
// fail to hand off the prompt or encounter an unexpected spawn error.
|
|
80
|
-
if (child && !child.killed) { try { child.kill(); } catch (_e) { /* ignore */ } }
|
|
81
|
-
resolve(false);
|
|
82
|
-
};
|
|
83
|
-
try {
|
|
84
|
-
// Node 18+ emits DEP0190 when mixing shell:true with an args array (the
|
|
85
|
-
// args aren't escaped — just concatenated). On Windows we need shell:true
|
|
86
|
-
// so `claude.cmd`/`claude.ps1` shims resolve via PATH, so we build the
|
|
87
|
-
// whole command as a single string there and pass an empty args array.
|
|
88
|
-
// The flags are hardcoded literals (no user input) so there's no
|
|
89
|
-
// injection surface either way; this just silences the warning.
|
|
90
|
-
const isWin = process.platform === "win32";
|
|
91
|
-
const spawnCmd = isWin ? "claude -p --dangerously-skip-permissions" : "claude";
|
|
92
|
-
const spawnArgs = isWin ? [] : ["-p", "--dangerously-skip-permissions"];
|
|
93
|
-
child = spawn(spawnCmd, spawnArgs, {
|
|
94
|
-
cwd: options.cwd || PROJECT_ROOT,
|
|
95
|
-
stdio: ["pipe", "inherit", "inherit"],
|
|
96
|
-
shell: isWin,
|
|
97
|
-
});
|
|
98
|
-
if (typeof options.onTick === "function" && options.tickMs > 0) {
|
|
99
|
-
// Fire once immediately so the user sees the progress line right away,
|
|
100
|
-
// instead of waiting a full tick interval for any feedback.
|
|
101
|
-
try { options.onTick(); } catch (_e) { /* swallow */ }
|
|
102
|
-
tickTimer = setInterval(() => {
|
|
103
|
-
try { options.onTick(); } catch (_e) { /* swallow — progress is best-effort */ }
|
|
104
|
-
}, options.tickMs);
|
|
105
|
-
}
|
|
106
|
-
child.on("close", (code) => { cleanup(); resolve(code === 0); });
|
|
107
|
-
child.on("error", () => { cleanup(); resolve(false); });
|
|
108
|
-
child.stdin.on("error", () => { bail(); });
|
|
109
|
-
child.stdin.write(prompt);
|
|
110
|
-
child.stdin.end();
|
|
111
|
-
} catch (_e) {
|
|
112
|
-
// Catches synchronous throws from spawn (e.g. ENAMETOOLONG on some
|
|
113
|
-
// platforms) or from the initial stdin.write before listeners were
|
|
114
|
-
// attached. Either way: no orphan child, no leaked interval.
|
|
115
|
-
bail();
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Run claude -p but CAPTURE stdout instead of inheriting it.
|
|
121
|
-
// Returns the captured stdout string on success, or null on failure.
|
|
122
|
-
// Used for short tasks where we need the response content (e.g. translation).
|
|
123
|
-
// maxBuffer default is 10MB — enough for document translation.
|
|
124
|
-
function runClaudeCapture(prompt, options = {}) {
|
|
125
|
-
try {
|
|
126
|
-
const out = execSync("claude -p --dangerously-skip-permissions", {
|
|
127
|
-
input: prompt,
|
|
128
|
-
cwd: options.cwd || PROJECT_ROOT,
|
|
129
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
-
encoding: "utf-8",
|
|
131
|
-
timeout: options.timeout || 0,
|
|
132
|
-
maxBuffer: options.maxBuffer || 10 * 1024 * 1024,
|
|
133
|
-
});
|
|
134
|
-
return out;
|
|
135
|
-
} catch (_e) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── Filesystem ─────────────────────────────────────────────────
|
|
141
|
-
// ensureDir: delegated to lib/safe-fs.js (single source of truth)
|
|
142
|
-
// fileExists: alias for existsSafe from lib/safe-fs.js
|
|
143
|
-
const fileExists = existsSafe;
|
|
144
|
-
|
|
145
|
-
// readFile: intentionally throws on error (CLI needs hard failure, unlike safe-fs fallback)
|
|
146
|
-
function readFile(p) {
|
|
147
|
-
return fs.readFileSync(p, "utf-8");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function injectProjectRoot(text) {
|
|
151
|
-
// Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
|
|
152
|
-
const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
|
|
153
|
-
// Use a replacement function so that `$`, `$1`, `$&`, `$$` in the
|
|
154
|
-
// project path (rare but possible on some systems) are preserved as
|
|
155
|
-
// literal characters rather than interpreted as regex specials.
|
|
156
|
-
return text.replace(/\{\{PROJECT_ROOT\}\}/g, () => normalizedRoot);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ─── Helpers ────────────────────────────────────────────────────
|
|
160
|
-
function pad(str, len) {
|
|
161
|
-
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function countFiles() {
|
|
165
|
-
try {
|
|
166
|
-
let count = 0;
|
|
167
|
-
const skipDirs = ["node_modules", "generated"];
|
|
168
|
-
const visited = new Set();
|
|
169
|
-
const scan = (dir) => {
|
|
170
|
-
if (!fs.existsSync(dir)) return;
|
|
171
|
-
let realDir;
|
|
172
|
-
try { realDir = fs.realpathSync(dir); } catch (_e) { realDir = dir; }
|
|
173
|
-
if (visited.has(realDir)) return;
|
|
174
|
-
visited.add(realDir);
|
|
175
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
-
if (skipDirs.includes(entry.name)) continue;
|
|
177
|
-
const full = path.join(dir, entry.name);
|
|
178
|
-
if (entry.isDirectory()) scan(full);
|
|
179
|
-
else count++;
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
scan(path.join(PROJECT_ROOT, ".claude"));
|
|
183
|
-
scan(path.join(PROJECT_ROOT, "claudeos-core"));
|
|
184
|
-
return count;
|
|
185
|
-
} catch (e) {
|
|
186
|
-
return "?";
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function countPass1Files() {
|
|
191
|
-
try {
|
|
192
|
-
return fs
|
|
193
|
-
.readdirSync(GENERATED_DIR)
|
|
194
|
-
.filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
|
|
195
|
-
} catch (e) {
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
module.exports = {
|
|
201
|
-
TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
|
|
202
|
-
SUPPORTED_LANGS, LANG_CODES, isValidLang,
|
|
203
|
-
log, header, run, runClaudePrompt, runClaudePromptAsync, runClaudeCapture,
|
|
204
|
-
ensureDir, fileExists, readFile, injectProjectRoot,
|
|
205
|
-
pad, countFiles, countPass1Files,
|
|
206
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — CLI Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared constants, execution helpers, and filesystem utilities for the CLI.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync, spawn } = require("child_process");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { ensureDir, existsSafe } = require("../../lib/safe-fs");
|
|
11
|
+
|
|
12
|
+
// ─── Path configuration ──────────────────────────────────────────
|
|
13
|
+
const TOOLS_DIR = path.resolve(__dirname, "../..");
|
|
14
|
+
const PROJECT_ROOT = process.cwd();
|
|
15
|
+
const GENERATED_DIR = path.join(PROJECT_ROOT, "claudeos-core/generated");
|
|
16
|
+
|
|
17
|
+
// ─── Language configuration ──────────────────────────────────────
|
|
18
|
+
// Single source of truth: lib/language-config.js. We re-export under the
|
|
19
|
+
// historical name SUPPORTED_LANGS so existing imports keep working.
|
|
20
|
+
const { LANGUAGES: SUPPORTED_LANGS, LANG_CODES, isValidLang } = require("../../lib/language-config");
|
|
21
|
+
|
|
22
|
+
// ─── Output ─────────────────────────────────────────────────────
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.log(msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function header(title) {
|
|
28
|
+
log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
29
|
+
log(title);
|
|
30
|
+
log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Execution ──────────────────────────────────────────────────
|
|
34
|
+
function run(cmd, options = {}) {
|
|
35
|
+
try {
|
|
36
|
+
execSync(cmd, {
|
|
37
|
+
cwd: options.cwd || PROJECT_ROOT,
|
|
38
|
+
stdio: options.silent ? ["pipe", "pipe", "pipe"] : "inherit",
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
timeout: options.timeout || 0,
|
|
41
|
+
});
|
|
42
|
+
return true;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (options.ignoreError) return false;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Run claude -p: pass prompt via stdin (no shell pipe — avoids command injection)
|
|
50
|
+
function runClaudePrompt(prompt, options = {}) {
|
|
51
|
+
try {
|
|
52
|
+
execSync("claude -p --dangerously-skip-permissions", {
|
|
53
|
+
input: prompt,
|
|
54
|
+
cwd: options.cwd || PROJECT_ROOT,
|
|
55
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
timeout: 0,
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (options.ignoreError) return false;
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Async variant of runClaudePrompt using spawn — does NOT block the event loop,
|
|
67
|
+
// so the caller can run setInterval-based progress callbacks concurrently.
|
|
68
|
+
// Resolves to true on exit code 0, false otherwise (no throw; mirrors ignoreError:true).
|
|
69
|
+
// onTick is invoked every tickMs while claude is running; stopped on close.
|
|
70
|
+
// shell:true on Windows so that `claude.cmd`/`claude.ps1` shims resolve via PATH.
|
|
71
|
+
function runClaudePromptAsync(prompt, options = {}) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
let child = null;
|
|
74
|
+
let tickTimer = null;
|
|
75
|
+
const cleanup = () => { if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } };
|
|
76
|
+
const bail = () => {
|
|
77
|
+
cleanup();
|
|
78
|
+
// Best-effort kill so we don't leave orphaned claude processes when we
|
|
79
|
+
// fail to hand off the prompt or encounter an unexpected spawn error.
|
|
80
|
+
if (child && !child.killed) { try { child.kill(); } catch (_e) { /* ignore */ } }
|
|
81
|
+
resolve(false);
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
// Node 18+ emits DEP0190 when mixing shell:true with an args array (the
|
|
85
|
+
// args aren't escaped — just concatenated). On Windows we need shell:true
|
|
86
|
+
// so `claude.cmd`/`claude.ps1` shims resolve via PATH, so we build the
|
|
87
|
+
// whole command as a single string there and pass an empty args array.
|
|
88
|
+
// The flags are hardcoded literals (no user input) so there's no
|
|
89
|
+
// injection surface either way; this just silences the warning.
|
|
90
|
+
const isWin = process.platform === "win32";
|
|
91
|
+
const spawnCmd = isWin ? "claude -p --dangerously-skip-permissions" : "claude";
|
|
92
|
+
const spawnArgs = isWin ? [] : ["-p", "--dangerously-skip-permissions"];
|
|
93
|
+
child = spawn(spawnCmd, spawnArgs, {
|
|
94
|
+
cwd: options.cwd || PROJECT_ROOT,
|
|
95
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
96
|
+
shell: isWin,
|
|
97
|
+
});
|
|
98
|
+
if (typeof options.onTick === "function" && options.tickMs > 0) {
|
|
99
|
+
// Fire once immediately so the user sees the progress line right away,
|
|
100
|
+
// instead of waiting a full tick interval for any feedback.
|
|
101
|
+
try { options.onTick(); } catch (_e) { /* swallow */ }
|
|
102
|
+
tickTimer = setInterval(() => {
|
|
103
|
+
try { options.onTick(); } catch (_e) { /* swallow — progress is best-effort */ }
|
|
104
|
+
}, options.tickMs);
|
|
105
|
+
}
|
|
106
|
+
child.on("close", (code) => { cleanup(); resolve(code === 0); });
|
|
107
|
+
child.on("error", () => { cleanup(); resolve(false); });
|
|
108
|
+
child.stdin.on("error", () => { bail(); });
|
|
109
|
+
child.stdin.write(prompt);
|
|
110
|
+
child.stdin.end();
|
|
111
|
+
} catch (_e) {
|
|
112
|
+
// Catches synchronous throws from spawn (e.g. ENAMETOOLONG on some
|
|
113
|
+
// platforms) or from the initial stdin.write before listeners were
|
|
114
|
+
// attached. Either way: no orphan child, no leaked interval.
|
|
115
|
+
bail();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Run claude -p but CAPTURE stdout instead of inheriting it.
|
|
121
|
+
// Returns the captured stdout string on success, or null on failure.
|
|
122
|
+
// Used for short tasks where we need the response content (e.g. translation).
|
|
123
|
+
// maxBuffer default is 10MB — enough for document translation.
|
|
124
|
+
function runClaudeCapture(prompt, options = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const out = execSync("claude -p --dangerously-skip-permissions", {
|
|
127
|
+
input: prompt,
|
|
128
|
+
cwd: options.cwd || PROJECT_ROOT,
|
|
129
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
+
encoding: "utf-8",
|
|
131
|
+
timeout: options.timeout || 0,
|
|
132
|
+
maxBuffer: options.maxBuffer || 10 * 1024 * 1024,
|
|
133
|
+
});
|
|
134
|
+
return out;
|
|
135
|
+
} catch (_e) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Filesystem ─────────────────────────────────────────────────
|
|
141
|
+
// ensureDir: delegated to lib/safe-fs.js (single source of truth)
|
|
142
|
+
// fileExists: alias for existsSafe from lib/safe-fs.js
|
|
143
|
+
const fileExists = existsSafe;
|
|
144
|
+
|
|
145
|
+
// readFile: intentionally throws on error (CLI needs hard failure, unlike safe-fs fallback)
|
|
146
|
+
function readFile(p) {
|
|
147
|
+
return fs.readFileSync(p, "utf-8");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function injectProjectRoot(text) {
|
|
151
|
+
// Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
|
|
152
|
+
const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
|
|
153
|
+
// Use a replacement function so that `$`, `$1`, `$&`, `$$` in the
|
|
154
|
+
// project path (rare but possible on some systems) are preserved as
|
|
155
|
+
// literal characters rather than interpreted as regex specials.
|
|
156
|
+
return text.replace(/\{\{PROJECT_ROOT\}\}/g, () => normalizedRoot);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Helpers ────────────────────────────────────────────────────
|
|
160
|
+
function pad(str, len) {
|
|
161
|
+
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function countFiles() {
|
|
165
|
+
try {
|
|
166
|
+
let count = 0;
|
|
167
|
+
const skipDirs = ["node_modules", "generated"];
|
|
168
|
+
const visited = new Set();
|
|
169
|
+
const scan = (dir) => {
|
|
170
|
+
if (!fs.existsSync(dir)) return;
|
|
171
|
+
let realDir;
|
|
172
|
+
try { realDir = fs.realpathSync(dir); } catch (_e) { realDir = dir; }
|
|
173
|
+
if (visited.has(realDir)) return;
|
|
174
|
+
visited.add(realDir);
|
|
175
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
176
|
+
if (skipDirs.includes(entry.name)) continue;
|
|
177
|
+
const full = path.join(dir, entry.name);
|
|
178
|
+
if (entry.isDirectory()) scan(full);
|
|
179
|
+
else count++;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
scan(path.join(PROJECT_ROOT, ".claude"));
|
|
183
|
+
scan(path.join(PROJECT_ROOT, "claudeos-core"));
|
|
184
|
+
return count;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return "?";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function countPass1Files() {
|
|
191
|
+
try {
|
|
192
|
+
return fs
|
|
193
|
+
.readdirSync(GENERATED_DIR)
|
|
194
|
+
.filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
|
|
202
|
+
SUPPORTED_LANGS, LANG_CODES, isValidLang,
|
|
203
|
+
log, header, run, runClaudePrompt, runClaudePromptAsync, runClaudeCapture,
|
|
204
|
+
ensureDir, fileExists, readFile, injectProjectRoot,
|
|
205
|
+
pad, countFiles, countPass1Files,
|
|
206
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-md-validator — Post-generation structural validation for CLAUDE.md.
|
|
3
|
+
*
|
|
4
|
+
* Runs language-invariant structural checks against a generated CLAUDE.md
|
|
5
|
+
* to detect the §9 re-declaration anti-pattern and other structural drift
|
|
6
|
+
* that the scaffold + prompt-level instructions alone cannot reliably
|
|
7
|
+
* prevent.
|
|
8
|
+
*
|
|
9
|
+
* Usage (as a library):
|
|
10
|
+
* const { validate } = require("./claude-md-validator");
|
|
11
|
+
* const report = validate("/path/to/CLAUDE.md");
|
|
12
|
+
* if (!report.valid) { ... }
|
|
13
|
+
*
|
|
14
|
+
* Usage (as a CLI):
|
|
15
|
+
* node claude-md-validator/index.js /path/to/CLAUDE.md
|
|
16
|
+
* node claude-md-validator/index.js # defaults to ./CLAUDE.md
|
|
17
|
+
*
|
|
18
|
+
* Design principle: every check here must pass/fail identically regardless
|
|
19
|
+
* of the language used to generate CLAUDE.md. See structural-checks.js
|
|
20
|
+
* for the rationale.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
|
|
28
|
+
const checks = require("./structural-checks");
|
|
29
|
+
const { formatReport } = require("./reporter");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate a CLAUDE.md file.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} claudeMdPath - Absolute or relative path to CLAUDE.md
|
|
35
|
+
* @returns {object} report with { valid, path, checksRun, errors, warnings, summary }
|
|
36
|
+
*/
|
|
37
|
+
function validate(claudeMdPath) {
|
|
38
|
+
// Guard against non-string inputs (null, undefined, numbers, objects).
|
|
39
|
+
// path.resolve() throws TypeError on these, which surfaces as a raw
|
|
40
|
+
// stack trace to CLI users. Returning a structured error keeps the
|
|
41
|
+
// validator's contract simple: every call returns a report object.
|
|
42
|
+
if (typeof claudeMdPath !== "string" || claudeMdPath.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
path: String(claudeMdPath),
|
|
46
|
+
checksRun: 0,
|
|
47
|
+
errors: [
|
|
48
|
+
{
|
|
49
|
+
id: "INVALID_PATH",
|
|
50
|
+
pass: false,
|
|
51
|
+
severity: "error",
|
|
52
|
+
message: `Path must be a non-empty string, got ${typeof claudeMdPath}: ${JSON.stringify(claudeMdPath)}`,
|
|
53
|
+
remediation: "Pass a filesystem path to a CLAUDE.md file.",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
warnings: [],
|
|
57
|
+
summary: "❌ Invalid path argument.",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const absPath = path.resolve(claudeMdPath);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(absPath)) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
path: absPath,
|
|
67
|
+
checksRun: 0,
|
|
68
|
+
errors: [
|
|
69
|
+
{
|
|
70
|
+
id: "FILE_MISSING",
|
|
71
|
+
pass: false,
|
|
72
|
+
severity: "error",
|
|
73
|
+
message: `File not found: ${absPath}`,
|
|
74
|
+
remediation:
|
|
75
|
+
"Run `npx claudeos-core init --force` to generate CLAUDE.md.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
warnings: [],
|
|
79
|
+
summary: "❌ File not found.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Guard against the user pointing the validator at a directory or an
|
|
84
|
+
// unreadable file. Without this guard `readFileSync` throws EISDIR /
|
|
85
|
+
// EACCES and the CLI exits with a raw stack trace, which looks like
|
|
86
|
+
// a validator bug to end users.
|
|
87
|
+
const stat = fs.statSync(absPath);
|
|
88
|
+
if (!stat.isFile()) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
path: absPath,
|
|
92
|
+
checksRun: 0,
|
|
93
|
+
errors: [
|
|
94
|
+
{
|
|
95
|
+
id: "NOT_A_FILE",
|
|
96
|
+
pass: false,
|
|
97
|
+
severity: "error",
|
|
98
|
+
message: `Path is not a regular file: ${absPath}`,
|
|
99
|
+
remediation:
|
|
100
|
+
"Point the validator at a CLAUDE.md file, not a directory or special file.",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
warnings: [],
|
|
104
|
+
summary: "❌ Path is not a file.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let rawContent;
|
|
109
|
+
try {
|
|
110
|
+
rawContent = fs.readFileSync(absPath, "utf8");
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
path: absPath,
|
|
115
|
+
checksRun: 0,
|
|
116
|
+
errors: [
|
|
117
|
+
{
|
|
118
|
+
id: "FILE_UNREADABLE",
|
|
119
|
+
pass: false,
|
|
120
|
+
severity: "error",
|
|
121
|
+
message: `Could not read file: ${e.message || e}`,
|
|
122
|
+
remediation:
|
|
123
|
+
"Check file permissions and re-run, or regenerate via `npx claudeos-core init --force`.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
warnings: [],
|
|
127
|
+
summary: "❌ File unreadable.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Strip UTF-8 BOM (U+FEFF) if present at the start of the file.
|
|
131
|
+
// Some Windows editors and cross-platform generators prepend a BOM to
|
|
132
|
+
// UTF-8 files; without this, the first `## ` line fails to match
|
|
133
|
+
// `^## ` regex checks and the section count is silently off by one.
|
|
134
|
+
const content = rawContent.charCodeAt(0) === 0xfeff ? rawContent.slice(1) : rawContent;
|
|
135
|
+
const sections = checks.splitByH2(content);
|
|
136
|
+
|
|
137
|
+
const results = [];
|
|
138
|
+
|
|
139
|
+
// Structural checks (order does not matter; reporter groups them)
|
|
140
|
+
results.push(checks.checkH2Count(sections));
|
|
141
|
+
results.push(...checks.checkH3Counts(sections));
|
|
142
|
+
results.push(...checks.checkH4Counts(sections));
|
|
143
|
+
results.push(...checks.checkMemoryFileUniqueness(content));
|
|
144
|
+
results.push(...checks.checkMemoryScopedToSection8(sections, content));
|
|
145
|
+
results.push(...checks.checkSectionsHaveContent(sections));
|
|
146
|
+
// Title-invariant check — English canonical token must appear in every
|
|
147
|
+
// `## N.` heading so that multi-repo grep stays consistent across
|
|
148
|
+
// projects generated in different output languages.
|
|
149
|
+
results.push(...checks.checkCanonicalHeadings(sections));
|
|
150
|
+
|
|
151
|
+
const errors = results.filter((r) => !r.pass && r.severity === "error");
|
|
152
|
+
const warnings = results.filter((r) => !r.pass && r.severity === "warning");
|
|
153
|
+
const valid = errors.length === 0;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
valid,
|
|
157
|
+
path: absPath,
|
|
158
|
+
checksRun: results.length,
|
|
159
|
+
errors,
|
|
160
|
+
warnings,
|
|
161
|
+
allResults: results,
|
|
162
|
+
summary: valid
|
|
163
|
+
? `✅ ${results.length} structural checks passed` +
|
|
164
|
+
(warnings.length > 0 ? ` (${warnings.length} warning(s))` : "") +
|
|
165
|
+
"."
|
|
166
|
+
: `❌ ${errors.length} error(s), ${warnings.length} warning(s) out of ${results.length} checks.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── CLI entry ────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
if (require.main === module) {
|
|
173
|
+
const target = process.argv[2] || path.join(process.cwd(), "CLAUDE.md");
|
|
174
|
+
const report = validate(target);
|
|
175
|
+
console.log(formatReport(report));
|
|
176
|
+
process.exit(report.valid ? 0 : 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
validate,
|
|
181
|
+
// Expose checks for advanced callers / testing
|
|
182
|
+
checks,
|
|
183
|
+
formatReport,
|
|
184
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reporter.js — Human-readable formatter for validator reports.
|
|
3
|
+
*
|
|
4
|
+
* Keeps all user-facing messages in one place so the CLI and the
|
|
5
|
+
* plan-installer integration produce consistent output.
|
|
6
|
+
*
|
|
7
|
+
* No color codes by default — the goal is to work in any terminal
|
|
8
|
+
* without ANSI dependencies and to be easy to grep in CI logs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
function formatReport(report) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push(` 🔍 Validating: ${report.path}`);
|
|
17
|
+
lines.push("");
|
|
18
|
+
lines.push(` ${report.summary}`);
|
|
19
|
+
|
|
20
|
+
if (report.errors.length > 0) {
|
|
21
|
+
lines.push("");
|
|
22
|
+
lines.push(" Errors:");
|
|
23
|
+
for (const err of report.errors) {
|
|
24
|
+
lines.push(` ❌ [${err.id}] ${err.message || "(no message)"}`);
|
|
25
|
+
if (err.remediation) {
|
|
26
|
+
lines.push(` → ${err.remediation}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (report.warnings.length > 0) {
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push(" Warnings:");
|
|
34
|
+
for (const warn of report.warnings) {
|
|
35
|
+
lines.push(` ⚠️ [${warn.id}] ${warn.message || "(no message)"}`);
|
|
36
|
+
if (warn.remediation) {
|
|
37
|
+
lines.push(` → ${warn.remediation}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!report.valid) {
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(" To regenerate CLAUDE.md from scratch:");
|
|
45
|
+
lines.push(" npx claudeos-core init --force");
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(" To inspect the scaffold requirements:");
|
|
48
|
+
lines.push(" See pass-prompts/templates/common/claude-md-scaffold.md");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push("");
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Short one-line summary suitable for inline output during pipeline runs.
|
|
57
|
+
*/
|
|
58
|
+
function formatSummaryLine(report) {
|
|
59
|
+
if (report.valid) {
|
|
60
|
+
const warn = report.warnings.length > 0 ? ` (${report.warnings.length} warning(s))` : "";
|
|
61
|
+
return `✅ CLAUDE.md structure valid (${report.checksRun} checks)${warn}`;
|
|
62
|
+
}
|
|
63
|
+
return `⚠️ CLAUDE.md structure: ${report.errors.length} error(s), ${report.warnings.length} warning(s)`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { formatReport, formatSummaryLine };
|