claudeos-core 2.2.0 → 2.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.
- package/CHANGELOG.md +1664 -907
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +28 -0
- package/README.es.md +28 -0
- package/README.fr.md +28 -0
- package/README.hi.md +28 -0
- package/README.ja.md +28 -0
- package/README.ko.md +1014 -986
- package/README.md +1016 -987
- package/README.ru.md +28 -0
- package/README.vi.md +1015 -987
- package/README.zh-CN.md +28 -0
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1554
- 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 -441
- 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 -1054
- 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/common/claude-md-scaffold.md +52 -10
- package/pass-prompts/templates/common/pass3-footer.md +402 -224
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -305
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -137
- package/plan-installer/prompt-generator.js +188 -145
- 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 -482
- package/plan-installer/structure-scanner.js +65 -65
- package/sync-checker/index.js +177 -177
package/lib/plan-parser.js
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Plan Parser (shared)
|
|
3
|
-
*
|
|
4
|
-
* Shared parsing logic for plan files used by both manifest-generator and plan-validator.
|
|
5
|
-
* Two block formats:
|
|
6
|
-
* - <file path="..."> ... </file> (standard plan format)
|
|
7
|
-
* - ## N. `path` \n```markdown ... ``` (code block format, e.g. sync-rules-master)
|
|
8
|
-
*
|
|
9
|
-
* Each function accepts a content string and returns parsed blocks.
|
|
10
|
-
* Replace functions modify content in-place and return the updated string.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// ─── Extract <file path="..."> blocks ───────────────────────
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse <file path="..."> ... </file> blocks from plan content.
|
|
17
|
-
* @param {string} content - plan file content
|
|
18
|
-
* @param {{ includeContent?: boolean }} options
|
|
19
|
-
* @returns {Array<{ path: string, content?: string }>}
|
|
20
|
-
*/
|
|
21
|
-
function parseFileBlocks(content, { includeContent = false } = {}) {
|
|
22
|
-
const result = [];
|
|
23
|
-
// Filter out placeholder paths that can appear in prose/documentation inside
|
|
24
|
-
// the plan file (e.g., a sentence like: use <file path="..."> to wrap files).
|
|
25
|
-
// Real paths are any non-empty string; only the literal ellipsis placeholders
|
|
26
|
-
// and angle-bracket templates are rejected.
|
|
27
|
-
const isRealPath = (p) =>
|
|
28
|
-
typeof p === "string" &&
|
|
29
|
-
p.length > 0 &&
|
|
30
|
-
p !== "..." &&
|
|
31
|
-
!p.startsWith("...") &&
|
|
32
|
-
!/^<[^>]+>$/.test(p);
|
|
33
|
-
if (includeContent) {
|
|
34
|
-
const re = /<file\s+path="([^"]+)">\s*\n([\s\S]*?)\n<\/file>/g;
|
|
35
|
-
let m;
|
|
36
|
-
while ((m = re.exec(content)) !== null) {
|
|
37
|
-
if (!isRealPath(m[1])) continue;
|
|
38
|
-
result.push({ path: m[1], content: m[2] });
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
const re = /<file\s+path="([^"]+)">/g;
|
|
42
|
-
let m;
|
|
43
|
-
while ((m = re.exec(content)) !== null) {
|
|
44
|
-
if (!isRealPath(m[1])) continue;
|
|
45
|
-
result.push({ path: m[1] });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Extract ## N. `path` ```markdown ... ``` blocks ────────
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Parse ## N. `path` + ```markdown ... ``` blocks from plan content.
|
|
55
|
-
* Uses indexOf-based parsing to correctly handle nested code fences.
|
|
56
|
-
* @param {string} content - plan file content
|
|
57
|
-
* @param {{ includeContent?: boolean }} options
|
|
58
|
-
* @returns {Array<{ path: string, content?: string }>}
|
|
59
|
-
*/
|
|
60
|
-
function parseCodeBlocks(content, { includeContent = false } = {}) {
|
|
61
|
-
const result = [];
|
|
62
|
-
const headingRe = includeContent
|
|
63
|
-
? /^##\s+\d+\.\s+([^\n]+)/gm
|
|
64
|
-
: /^##\s+\d+\.\s+`?([^`\n]+)`?/gm;
|
|
65
|
-
let headingMatch;
|
|
66
|
-
while ((headingMatch = headingRe.exec(content)) !== null) {
|
|
67
|
-
const filePath = headingMatch[1].replace(/`/g, "").replace(/\s+[—–\-].*$/, "").trim();
|
|
68
|
-
|
|
69
|
-
if (!includeContent) {
|
|
70
|
-
// Path-only mode: just validate and collect
|
|
71
|
-
if (filePath && filePath.includes("/")) {
|
|
72
|
-
result.push({ path: filePath });
|
|
73
|
-
}
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Content mode: find opening ```markdown and matching closing ```
|
|
78
|
-
const openFence = content.indexOf("```markdown\n", headingMatch.index);
|
|
79
|
-
if (openFence < 0) continue;
|
|
80
|
-
const contentStart = openFence + "```markdown\n".length;
|
|
81
|
-
const closingPos = findClosingFence(content, contentStart);
|
|
82
|
-
if (closingPos < 0) continue;
|
|
83
|
-
const blockContent = content.substring(contentStart, closingPos).trimEnd();
|
|
84
|
-
result.push({ path: filePath, content: blockContent });
|
|
85
|
-
// Advance headingRe past this block to avoid re-matching inside content
|
|
86
|
-
headingRe.lastIndex = closingPos;
|
|
87
|
-
}
|
|
88
|
-
return result;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ─── Replace <file> block content ───────────────────────────
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Replace content inside a <file path="..."> block.
|
|
95
|
-
* @param {string} content - full plan file content
|
|
96
|
-
* @param {string} filePath - path attribute to match
|
|
97
|
-
* @param {string} newContent - replacement content
|
|
98
|
-
* @returns {string} updated content
|
|
99
|
-
*/
|
|
100
|
-
function replaceFileBlock(content, filePath, newContent) {
|
|
101
|
-
// Escape regex special chars in filePath (for the pattern)
|
|
102
|
-
const escaped = filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
-
// CRITICAL: use a replacement *function* (not a string) to avoid
|
|
104
|
-
// `$1`/`$&`/`$$` special-character interpretation inside newContent.
|
|
105
|
-
// When users document things like `$1`, `price: $100`, or shell `$$PID`
|
|
106
|
-
// in their memory/rule/standard files, a string replacement would
|
|
107
|
-
// corrupt the data.
|
|
108
|
-
const re = new RegExp(`(<file\\s+path="${escaped}">\\s*\\n)[\\s\\S]*?(\\n</file>)`, "g");
|
|
109
|
-
return content.replace(re, (_match, open, close) => open + newContent + close);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Replace code block content ─────────────────────────────
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Replace content inside a ## N. `path` ```markdown ... ``` block.
|
|
116
|
-
* Uses indexOf-based approach to handle nested code fences.
|
|
117
|
-
* @param {string} content - full plan file content
|
|
118
|
-
* @param {string} filePath - path to match in heading
|
|
119
|
-
* @param {string} newContent - replacement content
|
|
120
|
-
* @returns {string} updated content
|
|
121
|
-
*/
|
|
122
|
-
function replaceCodeBlock(content, filePath, newContent) {
|
|
123
|
-
const cleanPath = filePath.replace(/`/g, "");
|
|
124
|
-
const headingPattern = new RegExp(`^##\\s+\\d+\\.\\s+\`?${cleanPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\`?`, "m");
|
|
125
|
-
const headingMatch = headingPattern.exec(content);
|
|
126
|
-
if (!headingMatch) return content;
|
|
127
|
-
const afterHeading = content.indexOf("```markdown\n", headingMatch.index);
|
|
128
|
-
if (afterHeading < 0) return content;
|
|
129
|
-
const contentStart = afterHeading + "```markdown\n".length;
|
|
130
|
-
const closingPos = findClosingFence(content, contentStart);
|
|
131
|
-
if (closingPos < 0) return content;
|
|
132
|
-
return content.substring(0, contentStart) + newContent + "\n" + content.substring(closingPos);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ─── Internal: find closing ``` with nesting ────────────────
|
|
136
|
-
|
|
137
|
-
function findClosingFence(content, startPos) {
|
|
138
|
-
let searchPos = startPos;
|
|
139
|
-
let nestDepth = 0;
|
|
140
|
-
while (searchPos < content.length) {
|
|
141
|
-
const nextFence = content.indexOf("\n```", searchPos);
|
|
142
|
-
if (nextFence < 0) break;
|
|
143
|
-
const fenceLineStart = nextFence + 1;
|
|
144
|
-
const nextNewline = content.indexOf("\n", fenceLineStart + 3);
|
|
145
|
-
const restOfLine = nextNewline >= 0
|
|
146
|
-
? content.substring(fenceLineStart + 3, nextNewline)
|
|
147
|
-
: content.substring(fenceLineStart + 3);
|
|
148
|
-
const isOpening = /^[a-zA-Z]/.test(restOfLine.trim());
|
|
149
|
-
if (isOpening) {
|
|
150
|
-
nestDepth++;
|
|
151
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
152
|
-
} else if (nestDepth > 0) {
|
|
153
|
-
nestDepth--;
|
|
154
|
-
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
155
|
-
} else {
|
|
156
|
-
return fenceLineStart;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return -1;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Plan files that use code block format instead of <file> block format
|
|
163
|
-
const CODE_BLOCK_PLANS = ["21.sync-rules-master.md"];
|
|
164
|
-
|
|
165
|
-
module.exports = { parseFileBlocks, parseCodeBlocks, replaceFileBlock, replaceCodeBlock, CODE_BLOCK_PLANS };
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Plan Parser (shared)
|
|
3
|
+
*
|
|
4
|
+
* Shared parsing logic for plan files used by both manifest-generator and plan-validator.
|
|
5
|
+
* Two block formats:
|
|
6
|
+
* - <file path="..."> ... </file> (standard plan format)
|
|
7
|
+
* - ## N. `path` \n```markdown ... ``` (code block format, e.g. sync-rules-master)
|
|
8
|
+
*
|
|
9
|
+
* Each function accepts a content string and returns parsed blocks.
|
|
10
|
+
* Replace functions modify content in-place and return the updated string.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ─── Extract <file path="..."> blocks ───────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse <file path="..."> ... </file> blocks from plan content.
|
|
17
|
+
* @param {string} content - plan file content
|
|
18
|
+
* @param {{ includeContent?: boolean }} options
|
|
19
|
+
* @returns {Array<{ path: string, content?: string }>}
|
|
20
|
+
*/
|
|
21
|
+
function parseFileBlocks(content, { includeContent = false } = {}) {
|
|
22
|
+
const result = [];
|
|
23
|
+
// Filter out placeholder paths that can appear in prose/documentation inside
|
|
24
|
+
// the plan file (e.g., a sentence like: use <file path="..."> to wrap files).
|
|
25
|
+
// Real paths are any non-empty string; only the literal ellipsis placeholders
|
|
26
|
+
// and angle-bracket templates are rejected.
|
|
27
|
+
const isRealPath = (p) =>
|
|
28
|
+
typeof p === "string" &&
|
|
29
|
+
p.length > 0 &&
|
|
30
|
+
p !== "..." &&
|
|
31
|
+
!p.startsWith("...") &&
|
|
32
|
+
!/^<[^>]+>$/.test(p);
|
|
33
|
+
if (includeContent) {
|
|
34
|
+
const re = /<file\s+path="([^"]+)">\s*\n([\s\S]*?)\n<\/file>/g;
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = re.exec(content)) !== null) {
|
|
37
|
+
if (!isRealPath(m[1])) continue;
|
|
38
|
+
result.push({ path: m[1], content: m[2] });
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
const re = /<file\s+path="([^"]+)">/g;
|
|
42
|
+
let m;
|
|
43
|
+
while ((m = re.exec(content)) !== null) {
|
|
44
|
+
if (!isRealPath(m[1])) continue;
|
|
45
|
+
result.push({ path: m[1] });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Extract ## N. `path` ```markdown ... ``` blocks ────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse ## N. `path` + ```markdown ... ``` blocks from plan content.
|
|
55
|
+
* Uses indexOf-based parsing to correctly handle nested code fences.
|
|
56
|
+
* @param {string} content - plan file content
|
|
57
|
+
* @param {{ includeContent?: boolean }} options
|
|
58
|
+
* @returns {Array<{ path: string, content?: string }>}
|
|
59
|
+
*/
|
|
60
|
+
function parseCodeBlocks(content, { includeContent = false } = {}) {
|
|
61
|
+
const result = [];
|
|
62
|
+
const headingRe = includeContent
|
|
63
|
+
? /^##\s+\d+\.\s+([^\n]+)/gm
|
|
64
|
+
: /^##\s+\d+\.\s+`?([^`\n]+)`?/gm;
|
|
65
|
+
let headingMatch;
|
|
66
|
+
while ((headingMatch = headingRe.exec(content)) !== null) {
|
|
67
|
+
const filePath = headingMatch[1].replace(/`/g, "").replace(/\s+[—–\-].*$/, "").trim();
|
|
68
|
+
|
|
69
|
+
if (!includeContent) {
|
|
70
|
+
// Path-only mode: just validate and collect
|
|
71
|
+
if (filePath && filePath.includes("/")) {
|
|
72
|
+
result.push({ path: filePath });
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Content mode: find opening ```markdown and matching closing ```
|
|
78
|
+
const openFence = content.indexOf("```markdown\n", headingMatch.index);
|
|
79
|
+
if (openFence < 0) continue;
|
|
80
|
+
const contentStart = openFence + "```markdown\n".length;
|
|
81
|
+
const closingPos = findClosingFence(content, contentStart);
|
|
82
|
+
if (closingPos < 0) continue;
|
|
83
|
+
const blockContent = content.substring(contentStart, closingPos).trimEnd();
|
|
84
|
+
result.push({ path: filePath, content: blockContent });
|
|
85
|
+
// Advance headingRe past this block to avoid re-matching inside content
|
|
86
|
+
headingRe.lastIndex = closingPos;
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Replace <file> block content ───────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Replace content inside a <file path="..."> block.
|
|
95
|
+
* @param {string} content - full plan file content
|
|
96
|
+
* @param {string} filePath - path attribute to match
|
|
97
|
+
* @param {string} newContent - replacement content
|
|
98
|
+
* @returns {string} updated content
|
|
99
|
+
*/
|
|
100
|
+
function replaceFileBlock(content, filePath, newContent) {
|
|
101
|
+
// Escape regex special chars in filePath (for the pattern)
|
|
102
|
+
const escaped = filePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
// CRITICAL: use a replacement *function* (not a string) to avoid
|
|
104
|
+
// `$1`/`$&`/`$$` special-character interpretation inside newContent.
|
|
105
|
+
// When users document things like `$1`, `price: $100`, or shell `$$PID`
|
|
106
|
+
// in their memory/rule/standard files, a string replacement would
|
|
107
|
+
// corrupt the data.
|
|
108
|
+
const re = new RegExp(`(<file\\s+path="${escaped}">\\s*\\n)[\\s\\S]*?(\\n</file>)`, "g");
|
|
109
|
+
return content.replace(re, (_match, open, close) => open + newContent + close);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Replace code block content ─────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Replace content inside a ## N. `path` ```markdown ... ``` block.
|
|
116
|
+
* Uses indexOf-based approach to handle nested code fences.
|
|
117
|
+
* @param {string} content - full plan file content
|
|
118
|
+
* @param {string} filePath - path to match in heading
|
|
119
|
+
* @param {string} newContent - replacement content
|
|
120
|
+
* @returns {string} updated content
|
|
121
|
+
*/
|
|
122
|
+
function replaceCodeBlock(content, filePath, newContent) {
|
|
123
|
+
const cleanPath = filePath.replace(/`/g, "");
|
|
124
|
+
const headingPattern = new RegExp(`^##\\s+\\d+\\.\\s+\`?${cleanPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\`?`, "m");
|
|
125
|
+
const headingMatch = headingPattern.exec(content);
|
|
126
|
+
if (!headingMatch) return content;
|
|
127
|
+
const afterHeading = content.indexOf("```markdown\n", headingMatch.index);
|
|
128
|
+
if (afterHeading < 0) return content;
|
|
129
|
+
const contentStart = afterHeading + "```markdown\n".length;
|
|
130
|
+
const closingPos = findClosingFence(content, contentStart);
|
|
131
|
+
if (closingPos < 0) return content;
|
|
132
|
+
return content.substring(0, contentStart) + newContent + "\n" + content.substring(closingPos);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Internal: find closing ``` with nesting ────────────────
|
|
136
|
+
|
|
137
|
+
function findClosingFence(content, startPos) {
|
|
138
|
+
let searchPos = startPos;
|
|
139
|
+
let nestDepth = 0;
|
|
140
|
+
while (searchPos < content.length) {
|
|
141
|
+
const nextFence = content.indexOf("\n```", searchPos);
|
|
142
|
+
if (nextFence < 0) break;
|
|
143
|
+
const fenceLineStart = nextFence + 1;
|
|
144
|
+
const nextNewline = content.indexOf("\n", fenceLineStart + 3);
|
|
145
|
+
const restOfLine = nextNewline >= 0
|
|
146
|
+
? content.substring(fenceLineStart + 3, nextNewline)
|
|
147
|
+
: content.substring(fenceLineStart + 3);
|
|
148
|
+
const isOpening = /^[a-zA-Z]/.test(restOfLine.trim());
|
|
149
|
+
if (isOpening) {
|
|
150
|
+
nestDepth++;
|
|
151
|
+
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
152
|
+
} else if (nestDepth > 0) {
|
|
153
|
+
nestDepth--;
|
|
154
|
+
searchPos = nextNewline >= 0 ? nextNewline : fenceLineStart + 3;
|
|
155
|
+
} else {
|
|
156
|
+
return fenceLineStart;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Plan files that use code block format instead of <file> block format
|
|
163
|
+
const CODE_BLOCK_PLANS = ["21.sync-rules-master.md"];
|
|
164
|
+
|
|
165
|
+
module.exports = { parseFileBlocks, parseCodeBlocks, replaceFileBlock, replaceCodeBlock, CODE_BLOCK_PLANS };
|
package/lib/staged-rules.js
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Staged Rules Mover
|
|
3
|
-
*
|
|
4
|
-
* Pass 3 and Pass 4 write rule files into
|
|
5
|
-
* claudeos-core/generated/.staged-rules/**
|
|
6
|
-
* rather than
|
|
7
|
-
* .claude/rules/**
|
|
8
|
-
* because Claude Code's sensitive-path policy blocks direct writes to
|
|
9
|
-
* `.claude/` from the `claude -p` subprocess (even with
|
|
10
|
-
* `--dangerously-skip-permissions`).
|
|
11
|
-
*
|
|
12
|
-
* After each pass, this module moves everything under the staging dir into the
|
|
13
|
-
* real `.claude/rules/` tree, preserving subpaths, then removes the staging
|
|
14
|
-
* dir. The Node.js orchestrator is not subject to Claude Code's policy, so the
|
|
15
|
-
* final writes succeed.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const fs = require("fs");
|
|
19
|
-
const path = require("path");
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Recursively list files under dir (absolute paths). Returns [] if dir missing.
|
|
23
|
-
*/
|
|
24
|
-
function walkFiles(dir) {
|
|
25
|
-
const out = [];
|
|
26
|
-
if (!fs.existsSync(dir)) return out;
|
|
27
|
-
const stack = [dir];
|
|
28
|
-
while (stack.length > 0) {
|
|
29
|
-
const cur = stack.pop();
|
|
30
|
-
let entries;
|
|
31
|
-
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
32
|
-
catch (_e) { continue; }
|
|
33
|
-
for (const e of entries) {
|
|
34
|
-
const full = path.join(cur, e.name);
|
|
35
|
-
if (e.isDirectory()) stack.push(full);
|
|
36
|
-
else if (e.isFile()) out.push(full);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Move a single file, falling back to copy+unlink if rename fails
|
|
44
|
-
* (Windows cross-volume/overwrite edge cases).
|
|
45
|
-
*/
|
|
46
|
-
function moveFile(src, dst) {
|
|
47
|
-
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
48
|
-
try {
|
|
49
|
-
fs.renameSync(src, dst);
|
|
50
|
-
return;
|
|
51
|
-
} catch (_e) {
|
|
52
|
-
// Fallback: copy then unlink. copyFileSync overwrites by default.
|
|
53
|
-
fs.copyFileSync(src, dst);
|
|
54
|
-
try { fs.unlinkSync(src); } catch (_e2) { /* best-effort cleanup */ }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Remove a directory tree. No-op if missing.
|
|
60
|
-
*/
|
|
61
|
-
function removeDir(dir) {
|
|
62
|
-
if (!fs.existsSync(dir)) return;
|
|
63
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Move everything from <projectRoot>/claudeos-core/generated/.staged-rules/**
|
|
68
|
-
* to <projectRoot>/.claude/rules/**, preserving subpaths.
|
|
69
|
-
*
|
|
70
|
-
* @param {string} projectRoot - absolute path to the user's project root
|
|
71
|
-
* @returns {{ moved: number, failed: number, files: string[], errors: string[], skipped: boolean }}
|
|
72
|
-
* - skipped: true when the staging dir does not exist (nothing to move)
|
|
73
|
-
* - files: list of subpaths moved (relative to .claude/rules/)
|
|
74
|
-
* - errors: per-file error messages (empty on full success)
|
|
75
|
-
*/
|
|
76
|
-
function moveStagedRules(projectRoot) {
|
|
77
|
-
const stagingRoot = path.join(projectRoot, "claudeos-core", "generated", ".staged-rules");
|
|
78
|
-
const rulesRoot = path.join(projectRoot, ".claude", "rules");
|
|
79
|
-
|
|
80
|
-
if (!fs.existsSync(stagingRoot)) {
|
|
81
|
-
return { moved: 0, failed: 0, files: [], errors: [], skipped: true };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const staged = walkFiles(stagingRoot);
|
|
85
|
-
const result = { moved: 0, failed: 0, files: [], errors: [], skipped: false };
|
|
86
|
-
|
|
87
|
-
for (const srcAbs of staged) {
|
|
88
|
-
const rel = path.relative(stagingRoot, srcAbs);
|
|
89
|
-
const dstAbs = path.join(rulesRoot, rel);
|
|
90
|
-
try {
|
|
91
|
-
moveFile(srcAbs, dstAbs);
|
|
92
|
-
result.moved++;
|
|
93
|
-
result.files.push(rel.split(path.sep).join("/"));
|
|
94
|
-
} catch (e) {
|
|
95
|
-
result.failed++;
|
|
96
|
-
result.errors.push(`${rel}: ${e.code || e.message}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Clean up the staging tree on full success; keep it on partial failure so
|
|
101
|
-
// the user (or a re-run) can inspect what didn't make it across.
|
|
102
|
-
if (result.failed === 0) {
|
|
103
|
-
try { removeDir(stagingRoot); } catch (_e) { /* non-fatal */ }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Count the total number of regular files under a directory, recursively.
|
|
111
|
-
* Returns 0 if the directory doesn't exist. Unreadable subdirectories are
|
|
112
|
-
* skipped silently (consistent with walkFiles behavior).
|
|
113
|
-
*/
|
|
114
|
-
function countFilesRecursive(dir) {
|
|
115
|
-
return walkFiles(dir).length;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
module.exports = { moveStagedRules, countFilesRecursive };
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Staged Rules Mover
|
|
3
|
+
*
|
|
4
|
+
* Pass 3 and Pass 4 write rule files into
|
|
5
|
+
* claudeos-core/generated/.staged-rules/**
|
|
6
|
+
* rather than
|
|
7
|
+
* .claude/rules/**
|
|
8
|
+
* because Claude Code's sensitive-path policy blocks direct writes to
|
|
9
|
+
* `.claude/` from the `claude -p` subprocess (even with
|
|
10
|
+
* `--dangerously-skip-permissions`).
|
|
11
|
+
*
|
|
12
|
+
* After each pass, this module moves everything under the staging dir into the
|
|
13
|
+
* real `.claude/rules/` tree, preserving subpaths, then removes the staging
|
|
14
|
+
* dir. The Node.js orchestrator is not subject to Claude Code's policy, so the
|
|
15
|
+
* final writes succeed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively list files under dir (absolute paths). Returns [] if dir missing.
|
|
23
|
+
*/
|
|
24
|
+
function walkFiles(dir) {
|
|
25
|
+
const out = [];
|
|
26
|
+
if (!fs.existsSync(dir)) return out;
|
|
27
|
+
const stack = [dir];
|
|
28
|
+
while (stack.length > 0) {
|
|
29
|
+
const cur = stack.pop();
|
|
30
|
+
let entries;
|
|
31
|
+
try { entries = fs.readdirSync(cur, { withFileTypes: true }); }
|
|
32
|
+
catch (_e) { continue; }
|
|
33
|
+
for (const e of entries) {
|
|
34
|
+
const full = path.join(cur, e.name);
|
|
35
|
+
if (e.isDirectory()) stack.push(full);
|
|
36
|
+
else if (e.isFile()) out.push(full);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Move a single file, falling back to copy+unlink if rename fails
|
|
44
|
+
* (Windows cross-volume/overwrite edge cases).
|
|
45
|
+
*/
|
|
46
|
+
function moveFile(src, dst) {
|
|
47
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
48
|
+
try {
|
|
49
|
+
fs.renameSync(src, dst);
|
|
50
|
+
return;
|
|
51
|
+
} catch (_e) {
|
|
52
|
+
// Fallback: copy then unlink. copyFileSync overwrites by default.
|
|
53
|
+
fs.copyFileSync(src, dst);
|
|
54
|
+
try { fs.unlinkSync(src); } catch (_e2) { /* best-effort cleanup */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Remove a directory tree. No-op if missing.
|
|
60
|
+
*/
|
|
61
|
+
function removeDir(dir) {
|
|
62
|
+
if (!fs.existsSync(dir)) return;
|
|
63
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Move everything from <projectRoot>/claudeos-core/generated/.staged-rules/**
|
|
68
|
+
* to <projectRoot>/.claude/rules/**, preserving subpaths.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} projectRoot - absolute path to the user's project root
|
|
71
|
+
* @returns {{ moved: number, failed: number, files: string[], errors: string[], skipped: boolean }}
|
|
72
|
+
* - skipped: true when the staging dir does not exist (nothing to move)
|
|
73
|
+
* - files: list of subpaths moved (relative to .claude/rules/)
|
|
74
|
+
* - errors: per-file error messages (empty on full success)
|
|
75
|
+
*/
|
|
76
|
+
function moveStagedRules(projectRoot) {
|
|
77
|
+
const stagingRoot = path.join(projectRoot, "claudeos-core", "generated", ".staged-rules");
|
|
78
|
+
const rulesRoot = path.join(projectRoot, ".claude", "rules");
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(stagingRoot)) {
|
|
81
|
+
return { moved: 0, failed: 0, files: [], errors: [], skipped: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const staged = walkFiles(stagingRoot);
|
|
85
|
+
const result = { moved: 0, failed: 0, files: [], errors: [], skipped: false };
|
|
86
|
+
|
|
87
|
+
for (const srcAbs of staged) {
|
|
88
|
+
const rel = path.relative(stagingRoot, srcAbs);
|
|
89
|
+
const dstAbs = path.join(rulesRoot, rel);
|
|
90
|
+
try {
|
|
91
|
+
moveFile(srcAbs, dstAbs);
|
|
92
|
+
result.moved++;
|
|
93
|
+
result.files.push(rel.split(path.sep).join("/"));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
result.failed++;
|
|
96
|
+
result.errors.push(`${rel}: ${e.code || e.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Clean up the staging tree on full success; keep it on partial failure so
|
|
101
|
+
// the user (or a re-run) can inspect what didn't make it across.
|
|
102
|
+
if (result.failed === 0) {
|
|
103
|
+
try { removeDir(stagingRoot); } catch (_e) { /* non-fatal */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Count the total number of regular files under a directory, recursively.
|
|
111
|
+
* Returns 0 if the directory doesn't exist. Unreadable subdirectories are
|
|
112
|
+
* skipped silently (consistent with walkFiles behavior).
|
|
113
|
+
*/
|
|
114
|
+
function countFilesRecursive(dir) {
|
|
115
|
+
return walkFiles(dir).length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { moveStagedRules, countFilesRecursive };
|