rulesync 0.34.0 → 0.37.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/README.ja.md +95 -6
- package/README.md +95 -6
- package/dist/chunk-2BYTQ4LL.mjs +67 -0
- package/dist/chunk-2CDVII2R.mjs +62 -0
- package/dist/chunk-3GVVX3G5.mjs +78 -0
- package/dist/chunk-KZATM2CQ.mjs +65 -0
- package/dist/chunk-PBOKMNYU.mjs +67 -0
- package/dist/chunk-QQN5GTOV.mjs +56 -0
- package/dist/chunk-TKNVMYAC.mjs +78 -0
- package/dist/claude-2NLZ2CDE.mjs +9 -0
- package/dist/cline-HUXPTQP7.mjs +9 -0
- package/dist/copilot-376H5OXX.mjs +9 -0
- package/dist/cursor-XWLBQYWY.mjs +9 -0
- package/dist/geminicli-EREPFSDP.mjs +9 -0
- package/dist/index.js +1295 -224
- package/dist/index.mjs +955 -215
- package/dist/roo-YS23AEWJ.mjs +9 -0
- package/package.json +33 -12
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
generateClaudeMcp
|
|
4
|
+
} from "./chunk-2BYTQ4LL.mjs";
|
|
5
|
+
import {
|
|
6
|
+
generateClineMcp
|
|
7
|
+
} from "./chunk-2CDVII2R.mjs";
|
|
8
|
+
import {
|
|
9
|
+
generateCopilotMcp
|
|
10
|
+
} from "./chunk-TKNVMYAC.mjs";
|
|
11
|
+
import {
|
|
12
|
+
generateCursorMcp
|
|
13
|
+
} from "./chunk-KZATM2CQ.mjs";
|
|
14
|
+
import {
|
|
15
|
+
generateGeminiCliMcp
|
|
16
|
+
} from "./chunk-PBOKMNYU.mjs";
|
|
17
|
+
import {
|
|
18
|
+
generateRooMcp
|
|
19
|
+
} from "./chunk-3GVVX3G5.mjs";
|
|
20
|
+
import "./chunk-QQN5GTOV.mjs";
|
|
2
21
|
|
|
3
22
|
// src/cli/index.ts
|
|
4
23
|
import { Command } from "commander";
|
|
@@ -16,11 +35,12 @@ function getDefaultConfig() {
|
|
|
16
35
|
cursor: ".cursor/rules",
|
|
17
36
|
cline: ".clinerules",
|
|
18
37
|
claudecode: ".",
|
|
38
|
+
claude: ".",
|
|
19
39
|
roo: ".roo/rules",
|
|
20
40
|
geminicli: ".gemini/memories"
|
|
21
41
|
},
|
|
22
42
|
watchEnabled: false,
|
|
23
|
-
defaultTargets: ["copilot", "cursor", "cline", "claudecode", "roo", "geminicli"]
|
|
43
|
+
defaultTargets: ["copilot", "cursor", "cline", "claudecode", "claude", "roo", "geminicli"]
|
|
24
44
|
};
|
|
25
45
|
}
|
|
26
46
|
function resolveTargets(targets, config) {
|
|
@@ -66,27 +86,162 @@ async function addCommand(filename) {
|
|
|
66
86
|
}
|
|
67
87
|
}
|
|
68
88
|
|
|
69
|
-
// src/generators/claudecode.ts
|
|
89
|
+
// src/generators/rules/claudecode.ts
|
|
90
|
+
import { join as join3 } from "path";
|
|
91
|
+
|
|
92
|
+
// src/utils/file.ts
|
|
93
|
+
import { readdir, rm } from "fs/promises";
|
|
94
|
+
import { join as join2 } from "path";
|
|
95
|
+
|
|
96
|
+
// src/utils/file-ops.ts
|
|
97
|
+
import { mkdir as mkdir2, readFile, stat, writeFile as writeFile2 } from "fs/promises";
|
|
98
|
+
import { dirname } from "path";
|
|
99
|
+
async function ensureDir(dirPath) {
|
|
100
|
+
try {
|
|
101
|
+
await stat(dirPath);
|
|
102
|
+
} catch {
|
|
103
|
+
await mkdir2(dirPath, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function readFileContent(filepath) {
|
|
107
|
+
return readFile(filepath, "utf-8");
|
|
108
|
+
}
|
|
109
|
+
async function writeFileContent(filepath, content) {
|
|
110
|
+
await ensureDir(dirname(filepath));
|
|
111
|
+
await writeFile2(filepath, content, "utf-8");
|
|
112
|
+
}
|
|
113
|
+
async function fileExists(filepath) {
|
|
114
|
+
try {
|
|
115
|
+
await stat(filepath);
|
|
116
|
+
return true;
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/utils/ignore.ts
|
|
70
123
|
import { join } from "path";
|
|
124
|
+
import micromatch from "micromatch";
|
|
125
|
+
var cachedIgnorePatterns = null;
|
|
126
|
+
async function loadIgnorePatterns(baseDir = process.cwd()) {
|
|
127
|
+
if (cachedIgnorePatterns) {
|
|
128
|
+
return cachedIgnorePatterns;
|
|
129
|
+
}
|
|
130
|
+
const ignorePath = join(baseDir, ".rulesyncignore");
|
|
131
|
+
if (!await fileExists(ignorePath)) {
|
|
132
|
+
cachedIgnorePatterns = { patterns: [] };
|
|
133
|
+
return cachedIgnorePatterns;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const content = await readFileContent(ignorePath);
|
|
137
|
+
const patterns = parseIgnoreFile(content);
|
|
138
|
+
cachedIgnorePatterns = { patterns };
|
|
139
|
+
return cachedIgnorePatterns;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn(`Failed to read .rulesyncignore: ${error}`);
|
|
142
|
+
cachedIgnorePatterns = { patterns: [] };
|
|
143
|
+
return cachedIgnorePatterns;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function parseIgnoreFile(content) {
|
|
147
|
+
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
148
|
+
}
|
|
149
|
+
function isFileIgnored(filepath, ignorePatterns) {
|
|
150
|
+
if (ignorePatterns.length === 0) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const negationPatterns = ignorePatterns.filter((p) => p.startsWith("!"));
|
|
154
|
+
const positivePatterns = ignorePatterns.filter((p) => !p.startsWith("!"));
|
|
155
|
+
const isIgnored = positivePatterns.length > 0 && micromatch.isMatch(filepath, positivePatterns, {
|
|
156
|
+
dot: true
|
|
157
|
+
});
|
|
158
|
+
if (isIgnored && negationPatterns.length > 0) {
|
|
159
|
+
const negationPatternsWithoutPrefix = negationPatterns.map((p) => p.substring(1));
|
|
160
|
+
return !micromatch.isMatch(filepath, negationPatternsWithoutPrefix, {
|
|
161
|
+
dot: true
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return isIgnored;
|
|
165
|
+
}
|
|
166
|
+
function filterIgnoredFiles(files, ignorePatterns) {
|
|
167
|
+
if (ignorePatterns.length === 0) {
|
|
168
|
+
return files;
|
|
169
|
+
}
|
|
170
|
+
return files.filter((file) => !isFileIgnored(file, ignorePatterns));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/utils/file.ts
|
|
174
|
+
async function findFiles(dir, extension = ".md", ignorePatterns) {
|
|
175
|
+
try {
|
|
176
|
+
const files = await readdir(dir);
|
|
177
|
+
const filtered = files.filter((file) => file.endsWith(extension)).map((file) => join2(dir, file));
|
|
178
|
+
if (ignorePatterns && ignorePatterns.length > 0) {
|
|
179
|
+
return filterIgnoredFiles(filtered, ignorePatterns);
|
|
180
|
+
}
|
|
181
|
+
return filtered;
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function removeDirectory(dirPath) {
|
|
187
|
+
const dangerousPaths = [".", "/", "~", "src", "node_modules"];
|
|
188
|
+
if (dangerousPaths.includes(dirPath) || dirPath === "") {
|
|
189
|
+
console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if (await fileExists(dirPath)) {
|
|
194
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn(`Failed to remove directory ${dirPath}:`, error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function removeFile(filepath) {
|
|
201
|
+
try {
|
|
202
|
+
if (await fileExists(filepath)) {
|
|
203
|
+
await rm(filepath);
|
|
204
|
+
}
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn(`Failed to remove file ${filepath}:`, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function removeClaudeGeneratedFiles() {
|
|
210
|
+
const filesToRemove = ["CLAUDE.md", ".claude/memories"];
|
|
211
|
+
for (const fileOrDir of filesToRemove) {
|
|
212
|
+
if (fileOrDir.endsWith("/memories")) {
|
|
213
|
+
await removeDirectory(fileOrDir);
|
|
214
|
+
} else {
|
|
215
|
+
await removeFile(fileOrDir);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/generators/rules/claudecode.ts
|
|
71
221
|
async function generateClaudecodeConfig(rules, config, baseDir) {
|
|
72
222
|
const outputs = [];
|
|
73
223
|
const rootRules = rules.filter((r) => r.frontmatter.root === true);
|
|
74
224
|
const detailRules = rules.filter((r) => r.frontmatter.root === false);
|
|
75
225
|
const claudeMdContent = generateClaudeMarkdown(rootRules, detailRules);
|
|
76
|
-
const claudeOutputDir = baseDir ?
|
|
226
|
+
const claudeOutputDir = baseDir ? join3(baseDir, config.outputPaths.claudecode) : config.outputPaths.claudecode;
|
|
77
227
|
outputs.push({
|
|
78
228
|
tool: "claudecode",
|
|
79
|
-
filepath:
|
|
229
|
+
filepath: join3(claudeOutputDir, "CLAUDE.md"),
|
|
80
230
|
content: claudeMdContent
|
|
81
231
|
});
|
|
82
232
|
for (const rule of detailRules) {
|
|
83
233
|
const memoryContent = generateMemoryFile(rule);
|
|
84
234
|
outputs.push({
|
|
85
235
|
tool: "claudecode",
|
|
86
|
-
filepath:
|
|
236
|
+
filepath: join3(claudeOutputDir, ".claude", "memories", `${rule.filename}.md`),
|
|
87
237
|
content: memoryContent
|
|
88
238
|
});
|
|
89
239
|
}
|
|
240
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
241
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
242
|
+
const settingsPath = baseDir ? join3(baseDir, ".claude", "settings.json") : join3(".claude", "settings.json");
|
|
243
|
+
await updateClaudeSettings(settingsPath, ignorePatterns.patterns);
|
|
244
|
+
}
|
|
90
245
|
return outputs;
|
|
91
246
|
}
|
|
92
247
|
function generateClaudeMarkdown(rootRules, detailRules) {
|
|
@@ -115,42 +270,108 @@ function generateClaudeMarkdown(rootRules, detailRules) {
|
|
|
115
270
|
function generateMemoryFile(rule) {
|
|
116
271
|
return rule.content.trim();
|
|
117
272
|
}
|
|
273
|
+
async function updateClaudeSettings(settingsPath, ignorePatterns) {
|
|
274
|
+
let settings = {};
|
|
275
|
+
if (await fileExists(settingsPath)) {
|
|
276
|
+
try {
|
|
277
|
+
const content = await readFileContent(settingsPath);
|
|
278
|
+
settings = JSON.parse(content);
|
|
279
|
+
} catch {
|
|
280
|
+
console.warn(`Failed to parse existing ${settingsPath}, creating new settings`);
|
|
281
|
+
settings = {};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (typeof settings !== "object" || settings === null) {
|
|
285
|
+
settings = {};
|
|
286
|
+
}
|
|
287
|
+
const settingsObj = settings;
|
|
288
|
+
if (!settingsObj.permissions || typeof settingsObj.permissions !== "object" || settingsObj.permissions === null) {
|
|
289
|
+
settingsObj.permissions = {};
|
|
290
|
+
}
|
|
291
|
+
const permissions = settingsObj.permissions;
|
|
292
|
+
if (!Array.isArray(permissions.deny)) {
|
|
293
|
+
permissions.deny = [];
|
|
294
|
+
}
|
|
295
|
+
const readDenyRules = ignorePatterns.map((pattern) => `Read(${pattern})`);
|
|
296
|
+
const denyArray = permissions.deny;
|
|
297
|
+
const filteredDeny = denyArray.filter((rule) => {
|
|
298
|
+
if (typeof rule !== "string") return false;
|
|
299
|
+
if (!rule.startsWith("Read(")) return true;
|
|
300
|
+
const match = rule.match(/^Read\((.*)\)$/);
|
|
301
|
+
if (!match) return true;
|
|
302
|
+
return !ignorePatterns.includes(match[1] ?? "");
|
|
303
|
+
});
|
|
304
|
+
filteredDeny.push(...readDenyRules);
|
|
305
|
+
permissions.deny = [...new Set(filteredDeny)];
|
|
306
|
+
const jsonContent = JSON.stringify(settingsObj, null, 2);
|
|
307
|
+
await writeFileContent(settingsPath, jsonContent);
|
|
308
|
+
console.log(`\u2705 Updated Claude Code settings: ${settingsPath}`);
|
|
309
|
+
}
|
|
118
310
|
|
|
119
|
-
// src/generators/cline.ts
|
|
120
|
-
import { join as
|
|
311
|
+
// src/generators/rules/cline.ts
|
|
312
|
+
import { join as join4 } from "path";
|
|
121
313
|
async function generateClineConfig(rules, config, baseDir) {
|
|
122
314
|
const outputs = [];
|
|
123
315
|
for (const rule of rules) {
|
|
124
316
|
const content = generateClineMarkdown(rule);
|
|
125
|
-
const outputDir = baseDir ?
|
|
126
|
-
const filepath =
|
|
317
|
+
const outputDir = baseDir ? join4(baseDir, config.outputPaths.cline) : config.outputPaths.cline;
|
|
318
|
+
const filepath = join4(outputDir, `${rule.filename}.md`);
|
|
127
319
|
outputs.push({
|
|
128
320
|
tool: "cline",
|
|
129
321
|
filepath,
|
|
130
322
|
content
|
|
131
323
|
});
|
|
132
324
|
}
|
|
325
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
326
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
327
|
+
const clineIgnorePath = baseDir ? join4(baseDir, ".clineignore") : ".clineignore";
|
|
328
|
+
const clineIgnoreContent = generateClineIgnore(ignorePatterns.patterns);
|
|
329
|
+
outputs.push({
|
|
330
|
+
tool: "cline",
|
|
331
|
+
filepath: clineIgnorePath,
|
|
332
|
+
content: clineIgnoreContent
|
|
333
|
+
});
|
|
334
|
+
}
|
|
133
335
|
return outputs;
|
|
134
336
|
}
|
|
135
337
|
function generateClineMarkdown(rule) {
|
|
136
338
|
return rule.content.trim();
|
|
137
339
|
}
|
|
340
|
+
function generateClineIgnore(patterns) {
|
|
341
|
+
const lines = [
|
|
342
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
343
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
344
|
+
"",
|
|
345
|
+
...patterns
|
|
346
|
+
];
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
138
349
|
|
|
139
|
-
// src/generators/copilot.ts
|
|
140
|
-
import { join as
|
|
350
|
+
// src/generators/rules/copilot.ts
|
|
351
|
+
import { join as join5 } from "path";
|
|
141
352
|
async function generateCopilotConfig(rules, config, baseDir) {
|
|
142
353
|
const outputs = [];
|
|
143
354
|
for (const rule of rules) {
|
|
144
355
|
const content = generateCopilotMarkdown(rule);
|
|
145
356
|
const baseFilename = rule.filename.replace(/\.md$/, "");
|
|
146
|
-
const outputDir = baseDir ?
|
|
147
|
-
const filepath =
|
|
357
|
+
const outputDir = baseDir ? join5(baseDir, config.outputPaths.copilot) : config.outputPaths.copilot;
|
|
358
|
+
const filepath = join5(outputDir, `${baseFilename}.instructions.md`);
|
|
148
359
|
outputs.push({
|
|
149
360
|
tool: "copilot",
|
|
150
361
|
filepath,
|
|
151
362
|
content
|
|
152
363
|
});
|
|
153
364
|
}
|
|
365
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
366
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
367
|
+
const copilotIgnorePath = baseDir ? join5(baseDir, ".copilotignore") : ".copilotignore";
|
|
368
|
+
const copilotIgnoreContent = generateCopilotIgnore(ignorePatterns.patterns);
|
|
369
|
+
outputs.push({
|
|
370
|
+
tool: "copilot",
|
|
371
|
+
filepath: copilotIgnorePath,
|
|
372
|
+
content: copilotIgnoreContent
|
|
373
|
+
});
|
|
374
|
+
}
|
|
154
375
|
return outputs;
|
|
155
376
|
}
|
|
156
377
|
function generateCopilotMarkdown(rule) {
|
|
@@ -166,21 +387,42 @@ function generateCopilotMarkdown(rule) {
|
|
|
166
387
|
lines.push(rule.content);
|
|
167
388
|
return lines.join("\n");
|
|
168
389
|
}
|
|
390
|
+
function generateCopilotIgnore(patterns) {
|
|
391
|
+
const lines = [
|
|
392
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
393
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
394
|
+
"# Note: .copilotignore is not officially supported by GitHub Copilot.",
|
|
395
|
+
"# This file is for use with community tools like copilotignore-vscode extension.",
|
|
396
|
+
"",
|
|
397
|
+
...patterns
|
|
398
|
+
];
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|
|
169
401
|
|
|
170
|
-
// src/generators/cursor.ts
|
|
171
|
-
import { join as
|
|
402
|
+
// src/generators/rules/cursor.ts
|
|
403
|
+
import { join as join6 } from "path";
|
|
172
404
|
async function generateCursorConfig(rules, config, baseDir) {
|
|
173
405
|
const outputs = [];
|
|
174
406
|
for (const rule of rules) {
|
|
175
407
|
const content = generateCursorMarkdown(rule);
|
|
176
|
-
const outputDir = baseDir ?
|
|
177
|
-
const filepath =
|
|
408
|
+
const outputDir = baseDir ? join6(baseDir, config.outputPaths.cursor) : config.outputPaths.cursor;
|
|
409
|
+
const filepath = join6(outputDir, `${rule.filename}.mdc`);
|
|
178
410
|
outputs.push({
|
|
179
411
|
tool: "cursor",
|
|
180
412
|
filepath,
|
|
181
413
|
content
|
|
182
414
|
});
|
|
183
415
|
}
|
|
416
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
417
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
418
|
+
const cursorIgnorePath = baseDir ? join6(baseDir, ".cursorignore") : ".cursorignore";
|
|
419
|
+
const cursorIgnoreContent = generateCursorIgnore(ignorePatterns.patterns);
|
|
420
|
+
outputs.push({
|
|
421
|
+
tool: "cursor",
|
|
422
|
+
filepath: cursorIgnorePath,
|
|
423
|
+
content: cursorIgnoreContent
|
|
424
|
+
});
|
|
425
|
+
}
|
|
184
426
|
return outputs;
|
|
185
427
|
}
|
|
186
428
|
function generateCursorMarkdown(rule) {
|
|
@@ -203,17 +445,26 @@ function generateCursorMarkdown(rule) {
|
|
|
203
445
|
lines.push(rule.content);
|
|
204
446
|
return lines.join("\n");
|
|
205
447
|
}
|
|
448
|
+
function generateCursorIgnore(patterns) {
|
|
449
|
+
const lines = [
|
|
450
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
451
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
452
|
+
"",
|
|
453
|
+
...patterns
|
|
454
|
+
];
|
|
455
|
+
return lines.join("\n");
|
|
456
|
+
}
|
|
206
457
|
|
|
207
|
-
// src/generators/geminicli.ts
|
|
208
|
-
import { join as
|
|
458
|
+
// src/generators/rules/geminicli.ts
|
|
459
|
+
import { join as join7 } from "path";
|
|
209
460
|
async function generateGeminiConfig(rules, config, baseDir) {
|
|
210
461
|
const outputs = [];
|
|
211
462
|
const rootRule = rules.find((rule) => rule.frontmatter.root === true);
|
|
212
463
|
const memoryRules = rules.filter((rule) => rule.frontmatter.root === false);
|
|
213
464
|
for (const rule of memoryRules) {
|
|
214
465
|
const content = generateGeminiMemoryMarkdown(rule);
|
|
215
|
-
const outputDir = baseDir ?
|
|
216
|
-
const filepath =
|
|
466
|
+
const outputDir = baseDir ? join7(baseDir, config.outputPaths.geminicli) : config.outputPaths.geminicli;
|
|
467
|
+
const filepath = join7(outputDir, `${rule.filename}.md`);
|
|
217
468
|
outputs.push({
|
|
218
469
|
tool: "geminicli",
|
|
219
470
|
filepath,
|
|
@@ -221,12 +472,22 @@ async function generateGeminiConfig(rules, config, baseDir) {
|
|
|
221
472
|
});
|
|
222
473
|
}
|
|
223
474
|
const rootContent = generateGeminiRootMarkdown(rootRule, memoryRules, baseDir);
|
|
224
|
-
const rootFilepath = baseDir ?
|
|
475
|
+
const rootFilepath = baseDir ? join7(baseDir, "GEMINI.md") : "GEMINI.md";
|
|
225
476
|
outputs.push({
|
|
226
477
|
tool: "geminicli",
|
|
227
478
|
filepath: rootFilepath,
|
|
228
479
|
content: rootContent
|
|
229
480
|
});
|
|
481
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
482
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
483
|
+
const aiexcludePath = baseDir ? join7(baseDir, ".aiexclude") : ".aiexclude";
|
|
484
|
+
const aiexcludeContent = generateAiexclude(ignorePatterns.patterns);
|
|
485
|
+
outputs.push({
|
|
486
|
+
tool: "geminicli",
|
|
487
|
+
filepath: aiexcludePath,
|
|
488
|
+
content: aiexcludeContent
|
|
489
|
+
});
|
|
490
|
+
}
|
|
230
491
|
return outputs;
|
|
231
492
|
}
|
|
232
493
|
function generateGeminiMemoryMarkdown(rule) {
|
|
@@ -256,92 +517,53 @@ function generateGeminiRootMarkdown(rootRule, memoryRules, _baseDir) {
|
|
|
256
517
|
}
|
|
257
518
|
return lines.join("\n");
|
|
258
519
|
}
|
|
520
|
+
function generateAiexclude(patterns) {
|
|
521
|
+
const lines = [
|
|
522
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
523
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
524
|
+
"",
|
|
525
|
+
...patterns
|
|
526
|
+
];
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|
|
259
529
|
|
|
260
|
-
// src/generators/roo.ts
|
|
261
|
-
import { join as
|
|
530
|
+
// src/generators/rules/roo.ts
|
|
531
|
+
import { join as join8 } from "path";
|
|
262
532
|
async function generateRooConfig(rules, config, baseDir) {
|
|
263
533
|
const outputs = [];
|
|
264
534
|
for (const rule of rules) {
|
|
265
535
|
const content = generateRooMarkdown(rule);
|
|
266
|
-
const outputDir = baseDir ?
|
|
267
|
-
const filepath =
|
|
536
|
+
const outputDir = baseDir ? join8(baseDir, config.outputPaths.roo) : config.outputPaths.roo;
|
|
537
|
+
const filepath = join8(outputDir, `${rule.filename}.md`);
|
|
268
538
|
outputs.push({
|
|
269
539
|
tool: "roo",
|
|
270
540
|
filepath,
|
|
271
541
|
content
|
|
272
542
|
});
|
|
273
543
|
}
|
|
544
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
545
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
546
|
+
const rooIgnorePath = baseDir ? join8(baseDir, ".rooignore") : ".rooignore";
|
|
547
|
+
const rooIgnoreContent = generateRooIgnore(ignorePatterns.patterns);
|
|
548
|
+
outputs.push({
|
|
549
|
+
tool: "roo",
|
|
550
|
+
filepath: rooIgnorePath,
|
|
551
|
+
content: rooIgnoreContent
|
|
552
|
+
});
|
|
553
|
+
}
|
|
274
554
|
return outputs;
|
|
275
555
|
}
|
|
276
556
|
function generateRooMarkdown(rule) {
|
|
277
557
|
return rule.content.trim();
|
|
278
558
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await mkdir2(dirPath, { recursive: true });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
async function readFileContent(filepath) {
|
|
291
|
-
return readFile(filepath, "utf-8");
|
|
292
|
-
}
|
|
293
|
-
async function writeFileContent(filepath, content) {
|
|
294
|
-
await ensureDir(dirname(filepath));
|
|
295
|
-
await writeFile2(filepath, content, "utf-8");
|
|
296
|
-
}
|
|
297
|
-
async function findFiles(dir, extension = ".md") {
|
|
298
|
-
try {
|
|
299
|
-
const files = await readdir(dir);
|
|
300
|
-
return files.filter((file) => file.endsWith(extension)).map((file) => join7(dir, file));
|
|
301
|
-
} catch {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
async function fileExists(filepath) {
|
|
306
|
-
try {
|
|
307
|
-
await stat(filepath);
|
|
308
|
-
return true;
|
|
309
|
-
} catch {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function removeDirectory(dirPath) {
|
|
314
|
-
const dangerousPaths = [".", "/", "~", "src", "node_modules"];
|
|
315
|
-
if (dangerousPaths.includes(dirPath) || dirPath === "") {
|
|
316
|
-
console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
try {
|
|
320
|
-
if (await fileExists(dirPath)) {
|
|
321
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
322
|
-
}
|
|
323
|
-
} catch (error) {
|
|
324
|
-
console.warn(`Failed to remove directory ${dirPath}:`, error);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
async function removeFile(filepath) {
|
|
328
|
-
try {
|
|
329
|
-
if (await fileExists(filepath)) {
|
|
330
|
-
await rm(filepath);
|
|
331
|
-
}
|
|
332
|
-
} catch (error) {
|
|
333
|
-
console.warn(`Failed to remove file ${filepath}:`, error);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function removeClaudeGeneratedFiles() {
|
|
337
|
-
const filesToRemove = ["CLAUDE.md", ".claude/memories"];
|
|
338
|
-
for (const fileOrDir of filesToRemove) {
|
|
339
|
-
if (fileOrDir.endsWith("/memories")) {
|
|
340
|
-
await removeDirectory(fileOrDir);
|
|
341
|
-
} else {
|
|
342
|
-
await removeFile(fileOrDir);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
559
|
+
function generateRooIgnore(patterns) {
|
|
560
|
+
const lines = [
|
|
561
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
562
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
563
|
+
"",
|
|
564
|
+
...patterns
|
|
565
|
+
];
|
|
566
|
+
return lines.join("\n");
|
|
345
567
|
}
|
|
346
568
|
|
|
347
569
|
// src/core/generator.ts
|
|
@@ -397,9 +619,13 @@ async function generateForTool(tool, rules, config, baseDir) {
|
|
|
397
619
|
import { basename } from "path";
|
|
398
620
|
import matter from "gray-matter";
|
|
399
621
|
async function parseRulesFromDirectory(aiRulesDir) {
|
|
400
|
-
const
|
|
622
|
+
const ignorePatterns = await loadIgnorePatterns();
|
|
623
|
+
const ruleFiles = await findFiles(aiRulesDir, ".md", ignorePatterns.patterns);
|
|
401
624
|
const rules = [];
|
|
402
625
|
const errors = [];
|
|
626
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
627
|
+
console.log(`Loaded ${ignorePatterns.patterns.length} ignore patterns from .rulesyncignore`);
|
|
628
|
+
}
|
|
403
629
|
for (const filepath of ruleFiles) {
|
|
404
630
|
try {
|
|
405
631
|
const rule = await parseRuleFile(filepath);
|
|
@@ -555,6 +781,140 @@ async function validateRule(rule) {
|
|
|
555
781
|
};
|
|
556
782
|
}
|
|
557
783
|
|
|
784
|
+
// src/core/mcp-generator.ts
|
|
785
|
+
import os from "os";
|
|
786
|
+
import path3 from "path";
|
|
787
|
+
|
|
788
|
+
// src/core/mcp-parser.ts
|
|
789
|
+
import fs from "fs";
|
|
790
|
+
import path2 from "path";
|
|
791
|
+
function parseMcpConfig(projectRoot) {
|
|
792
|
+
const mcpPath = path2.join(projectRoot, ".rulesync", ".mcp.json");
|
|
793
|
+
if (!fs.existsSync(mcpPath)) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
try {
|
|
797
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
798
|
+
const rawConfig = JSON.parse(content);
|
|
799
|
+
if (rawConfig.servers && !rawConfig.mcpServers) {
|
|
800
|
+
rawConfig.mcpServers = rawConfig.servers;
|
|
801
|
+
delete rawConfig.servers;
|
|
802
|
+
}
|
|
803
|
+
if (!rawConfig.mcpServers || typeof rawConfig.mcpServers !== "object") {
|
|
804
|
+
throw new Error("Invalid mcp.json: 'mcpServers' field must be an object");
|
|
805
|
+
}
|
|
806
|
+
if (rawConfig.tools) {
|
|
807
|
+
delete rawConfig.tools;
|
|
808
|
+
}
|
|
809
|
+
return { mcpServers: rawConfig.mcpServers };
|
|
810
|
+
} catch (error) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`Failed to parse mcp.json: ${error instanceof Error ? error.message : String(error)}`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/core/mcp-generator.ts
|
|
818
|
+
async function generateMcpConfigs(projectRoot, baseDir) {
|
|
819
|
+
const results = [];
|
|
820
|
+
const targetRoot = baseDir || projectRoot;
|
|
821
|
+
const config = parseMcpConfig(projectRoot);
|
|
822
|
+
if (!config) {
|
|
823
|
+
return results;
|
|
824
|
+
}
|
|
825
|
+
const generators = [
|
|
826
|
+
{
|
|
827
|
+
tool: "claude-project",
|
|
828
|
+
path: path3.join(targetRoot, ".mcp.json"),
|
|
829
|
+
generate: () => generateClaudeMcp(config, "project")
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
tool: "copilot-editor",
|
|
833
|
+
path: path3.join(targetRoot, ".vscode", "mcp.json"),
|
|
834
|
+
generate: () => generateCopilotMcp(config, "editor")
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
tool: "cursor-project",
|
|
838
|
+
path: path3.join(targetRoot, ".cursor", "mcp.json"),
|
|
839
|
+
generate: () => generateCursorMcp(config, "project")
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
tool: "cline-project",
|
|
843
|
+
path: path3.join(targetRoot, ".cline", "mcp.json"),
|
|
844
|
+
generate: () => generateClineMcp(config, "project")
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
tool: "gemini-project",
|
|
848
|
+
path: path3.join(targetRoot, ".gemini", "settings.json"),
|
|
849
|
+
generate: () => generateGeminiCliMcp(config, "project")
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
tool: "roo-project",
|
|
853
|
+
path: path3.join(targetRoot, ".roo", "mcp.json"),
|
|
854
|
+
generate: () => generateRooMcp(config, "project")
|
|
855
|
+
}
|
|
856
|
+
];
|
|
857
|
+
if (!baseDir) {
|
|
858
|
+
generators.push(
|
|
859
|
+
{
|
|
860
|
+
tool: "claude-global",
|
|
861
|
+
path: path3.join(os.homedir(), ".claude", "settings.json"),
|
|
862
|
+
generate: () => generateClaudeMcp(config, "global")
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
tool: "cursor-global",
|
|
866
|
+
path: path3.join(os.homedir(), ".cursor", "mcp.json"),
|
|
867
|
+
generate: () => generateCursorMcp(config, "global")
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
tool: "gemini-global",
|
|
871
|
+
path: path3.join(os.homedir(), ".gemini", "settings.json"),
|
|
872
|
+
generate: () => generateGeminiCliMcp(config, "global")
|
|
873
|
+
}
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
for (const generator of generators) {
|
|
877
|
+
try {
|
|
878
|
+
const content = generator.generate();
|
|
879
|
+
const parsed = JSON.parse(content);
|
|
880
|
+
if (generator.tool.includes("claude") || generator.tool.includes("cline") || generator.tool.includes("cursor") || generator.tool.includes("gemini") || generator.tool.includes("roo")) {
|
|
881
|
+
if (!parsed.mcpServers || Object.keys(parsed.mcpServers).length === 0) {
|
|
882
|
+
results.push({
|
|
883
|
+
tool: generator.tool,
|
|
884
|
+
path: generator.path,
|
|
885
|
+
status: "skipped"
|
|
886
|
+
});
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
} else if (generator.tool.includes("copilot")) {
|
|
890
|
+
const key = generator.tool.includes("codingAgent") ? "mcpServers" : "servers";
|
|
891
|
+
if (!parsed[key] || Object.keys(parsed[key]).length === 0) {
|
|
892
|
+
results.push({
|
|
893
|
+
tool: generator.tool,
|
|
894
|
+
path: generator.path,
|
|
895
|
+
status: "skipped"
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
await writeFileContent(generator.path, content);
|
|
901
|
+
results.push({
|
|
902
|
+
tool: generator.tool,
|
|
903
|
+
path: generator.path,
|
|
904
|
+
status: "success"
|
|
905
|
+
});
|
|
906
|
+
} catch (error) {
|
|
907
|
+
results.push({
|
|
908
|
+
tool: generator.tool,
|
|
909
|
+
path: generator.path,
|
|
910
|
+
status: "error",
|
|
911
|
+
error: error instanceof Error ? error.message : String(error)
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return results;
|
|
916
|
+
}
|
|
917
|
+
|
|
558
918
|
// src/cli/commands/generate.ts
|
|
559
919
|
async function generateCommand(options = {}) {
|
|
560
920
|
const config = getDefaultConfig();
|
|
@@ -635,6 +995,30 @@ Generating configurations for base directory: ${baseDir}`);
|
|
|
635
995
|
}
|
|
636
996
|
console.log(`
|
|
637
997
|
\u{1F389} Successfully generated ${totalOutputs} configuration file(s)!`);
|
|
998
|
+
if (options.verbose) {
|
|
999
|
+
console.log("\nGenerating MCP configurations...");
|
|
1000
|
+
}
|
|
1001
|
+
for (const baseDir of baseDirs) {
|
|
1002
|
+
const mcpResults = await generateMcpConfigs(
|
|
1003
|
+
process.cwd(),
|
|
1004
|
+
baseDir === process.cwd() ? void 0 : baseDir
|
|
1005
|
+
);
|
|
1006
|
+
if (mcpResults.length === 0) {
|
|
1007
|
+
if (options.verbose) {
|
|
1008
|
+
console.log(`No MCP configuration found for ${baseDir}`);
|
|
1009
|
+
}
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
for (const result of mcpResults) {
|
|
1013
|
+
if (result.status === "success") {
|
|
1014
|
+
console.log(`\u2705 Generated ${result.tool} MCP configuration: ${result.path}`);
|
|
1015
|
+
} else if (result.status === "error") {
|
|
1016
|
+
console.error(`\u274C Failed to generate ${result.tool} MCP configuration: ${result.error}`);
|
|
1017
|
+
} else if (options.verbose && result.status === "skipped") {
|
|
1018
|
+
console.log(`\u23ED\uFE0F Skipped ${result.tool} MCP configuration (no servers configured)`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
638
1022
|
} catch (error) {
|
|
639
1023
|
console.error("\u274C Failed to generate configurations:", error);
|
|
640
1024
|
process.exit(1);
|
|
@@ -643,20 +1027,31 @@ Generating configurations for base directory: ${baseDir}`);
|
|
|
643
1027
|
|
|
644
1028
|
// src/cli/commands/gitignore.ts
|
|
645
1029
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
646
|
-
import { join as
|
|
1030
|
+
import { join as join9 } from "path";
|
|
647
1031
|
var gitignoreCommand = async () => {
|
|
648
|
-
const gitignorePath =
|
|
1032
|
+
const gitignorePath = join9(process.cwd(), ".gitignore");
|
|
649
1033
|
const rulesFilesToIgnore = [
|
|
650
1034
|
"# Generated by rulesync - AI tool configuration files",
|
|
651
1035
|
"**/.github/copilot-instructions.md",
|
|
652
1036
|
"**/.github/instructions/",
|
|
653
1037
|
"**/.cursor/rules/",
|
|
1038
|
+
"**/.cursorignore",
|
|
654
1039
|
"**/.clinerules/",
|
|
1040
|
+
"**/.clineignore",
|
|
655
1041
|
"**/CLAUDE.md",
|
|
656
1042
|
"**/.claude/memories/",
|
|
657
1043
|
"**/.roo/rules/",
|
|
1044
|
+
"**/.rooignore",
|
|
1045
|
+
"**/.copilotignore",
|
|
658
1046
|
"**/GEMINI.md",
|
|
659
|
-
"**/.gemini/memories/"
|
|
1047
|
+
"**/.gemini/memories/",
|
|
1048
|
+
"**/.aiexclude",
|
|
1049
|
+
"**/.mcp.json",
|
|
1050
|
+
"**/.cursor/mcp.json",
|
|
1051
|
+
"**/.cline/mcp.json",
|
|
1052
|
+
"**/.vscode/mcp.json",
|
|
1053
|
+
"**/.gemini/settings.json",
|
|
1054
|
+
"**/.roo/mcp.json"
|
|
660
1055
|
];
|
|
661
1056
|
let gitignoreContent = "";
|
|
662
1057
|
if (existsSync(gitignorePath)) {
|
|
@@ -687,15 +1082,17 @@ ${linesToAdd.join("\n")}
|
|
|
687
1082
|
};
|
|
688
1083
|
|
|
689
1084
|
// src/core/importer.ts
|
|
690
|
-
import { join as
|
|
1085
|
+
import { join as join16 } from "path";
|
|
691
1086
|
import matter4 from "gray-matter";
|
|
692
1087
|
|
|
693
1088
|
// src/parsers/claudecode.ts
|
|
694
|
-
import { basename as basename2, join as
|
|
1089
|
+
import { basename as basename2, join as join10 } from "path";
|
|
695
1090
|
async function parseClaudeConfiguration(baseDir = process.cwd()) {
|
|
696
1091
|
const errors = [];
|
|
697
1092
|
const rules = [];
|
|
698
|
-
|
|
1093
|
+
let ignorePatterns;
|
|
1094
|
+
let mcpServers;
|
|
1095
|
+
const claudeFilePath = join10(baseDir, "CLAUDE.md");
|
|
699
1096
|
if (!await fileExists(claudeFilePath)) {
|
|
700
1097
|
errors.push("CLAUDE.md file not found");
|
|
701
1098
|
return { rules, errors };
|
|
@@ -706,16 +1103,32 @@ async function parseClaudeConfiguration(baseDir = process.cwd()) {
|
|
|
706
1103
|
if (mainRule) {
|
|
707
1104
|
rules.push(mainRule);
|
|
708
1105
|
}
|
|
709
|
-
const memoryDir =
|
|
1106
|
+
const memoryDir = join10(baseDir, ".claude", "memories");
|
|
710
1107
|
if (await fileExists(memoryDir)) {
|
|
711
1108
|
const memoryRules = await parseClaudeMemoryFiles(memoryDir);
|
|
712
1109
|
rules.push(...memoryRules);
|
|
713
1110
|
}
|
|
1111
|
+
const settingsPath = join10(baseDir, ".claude", "settings.json");
|
|
1112
|
+
if (await fileExists(settingsPath)) {
|
|
1113
|
+
const settingsResult = await parseClaudeSettings(settingsPath);
|
|
1114
|
+
if (settingsResult.ignorePatterns) {
|
|
1115
|
+
ignorePatterns = settingsResult.ignorePatterns;
|
|
1116
|
+
}
|
|
1117
|
+
if (settingsResult.mcpServers) {
|
|
1118
|
+
mcpServers = settingsResult.mcpServers;
|
|
1119
|
+
}
|
|
1120
|
+
errors.push(...settingsResult.errors);
|
|
1121
|
+
}
|
|
714
1122
|
} catch (error) {
|
|
715
1123
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
716
1124
|
errors.push(`Failed to parse Claude configuration: ${errorMessage}`);
|
|
717
1125
|
}
|
|
718
|
-
return {
|
|
1126
|
+
return {
|
|
1127
|
+
rules,
|
|
1128
|
+
errors,
|
|
1129
|
+
...ignorePatterns && { ignorePatterns },
|
|
1130
|
+
...mcpServers && { mcpServers }
|
|
1131
|
+
};
|
|
719
1132
|
}
|
|
720
1133
|
function parseClaudeMainFile(content, filepath) {
|
|
721
1134
|
const lines = content.split("\n");
|
|
@@ -752,7 +1165,7 @@ async function parseClaudeMemoryFiles(memoryDir) {
|
|
|
752
1165
|
const files = await readdir2(memoryDir);
|
|
753
1166
|
for (const file of files) {
|
|
754
1167
|
if (file.endsWith(".md")) {
|
|
755
|
-
const filePath =
|
|
1168
|
+
const filePath = join10(memoryDir, file);
|
|
756
1169
|
const content = await readFileContent(filePath);
|
|
757
1170
|
if (content.trim()) {
|
|
758
1171
|
const filename = basename2(file, ".md");
|
|
@@ -771,51 +1184,125 @@ async function parseClaudeMemoryFiles(memoryDir) {
|
|
|
771
1184
|
}
|
|
772
1185
|
}
|
|
773
1186
|
}
|
|
774
|
-
} catch
|
|
1187
|
+
} catch {
|
|
775
1188
|
}
|
|
776
1189
|
return rules;
|
|
777
1190
|
}
|
|
1191
|
+
async function parseClaudeSettings(settingsPath) {
|
|
1192
|
+
const errors = [];
|
|
1193
|
+
let ignorePatterns;
|
|
1194
|
+
let mcpServers;
|
|
1195
|
+
try {
|
|
1196
|
+
const content = await readFileContent(settingsPath);
|
|
1197
|
+
const settings = JSON.parse(content);
|
|
1198
|
+
if (typeof settings === "object" && settings !== null && "permissions" in settings) {
|
|
1199
|
+
const permissions = settings.permissions;
|
|
1200
|
+
if (permissions && "deny" in permissions && Array.isArray(permissions.deny)) {
|
|
1201
|
+
const readPatterns = permissions.deny.filter(
|
|
1202
|
+
(rule) => typeof rule === "string" && rule.startsWith("Read(") && rule.endsWith(")")
|
|
1203
|
+
).map((rule) => {
|
|
1204
|
+
const match = rule.match(/^Read\((.+)\)$/);
|
|
1205
|
+
return match ? match[1] : null;
|
|
1206
|
+
}).filter((pattern) => pattern !== null);
|
|
1207
|
+
if (readPatterns.length > 0) {
|
|
1208
|
+
ignorePatterns = readPatterns;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof settings === "object" && settings !== null && "mcpServers" in settings) {
|
|
1213
|
+
const servers = settings.mcpServers;
|
|
1214
|
+
if (servers && typeof servers === "object" && Object.keys(servers).length > 0) {
|
|
1215
|
+
mcpServers = servers;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1220
|
+
errors.push(`Failed to parse settings.json: ${errorMessage}`);
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
errors,
|
|
1224
|
+
...ignorePatterns && { ignorePatterns },
|
|
1225
|
+
...mcpServers && { mcpServers }
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
778
1228
|
|
|
779
1229
|
// src/parsers/cline.ts
|
|
780
|
-
import { join as
|
|
1230
|
+
import { join as join11 } from "path";
|
|
781
1231
|
async function parseClineConfiguration(baseDir = process.cwd()) {
|
|
782
1232
|
const errors = [];
|
|
783
1233
|
const rules = [];
|
|
784
|
-
const clineFilePath =
|
|
785
|
-
if (
|
|
786
|
-
|
|
787
|
-
|
|
1234
|
+
const clineFilePath = join11(baseDir, ".cline", "instructions.md");
|
|
1235
|
+
if (await fileExists(clineFilePath)) {
|
|
1236
|
+
try {
|
|
1237
|
+
const content = await readFileContent(clineFilePath);
|
|
1238
|
+
if (content.trim()) {
|
|
1239
|
+
const frontmatter = {
|
|
1240
|
+
root: false,
|
|
1241
|
+
targets: ["cline"],
|
|
1242
|
+
description: "Cline instructions",
|
|
1243
|
+
globs: ["**/*"]
|
|
1244
|
+
};
|
|
1245
|
+
rules.push({
|
|
1246
|
+
frontmatter,
|
|
1247
|
+
content: content.trim(),
|
|
1248
|
+
filename: "cline-instructions",
|
|
1249
|
+
filepath: clineFilePath
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1254
|
+
errors.push(`Failed to parse .cline/instructions.md: ${errorMessage}`);
|
|
1255
|
+
}
|
|
788
1256
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1257
|
+
const clinerulesDirPath = join11(baseDir, ".clinerules");
|
|
1258
|
+
if (await fileExists(clinerulesDirPath)) {
|
|
1259
|
+
try {
|
|
1260
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1261
|
+
const files = await readdir2(clinerulesDirPath);
|
|
1262
|
+
for (const file of files) {
|
|
1263
|
+
if (file.endsWith(".md")) {
|
|
1264
|
+
const filePath = join11(clinerulesDirPath, file);
|
|
1265
|
+
try {
|
|
1266
|
+
const content = await readFileContent(filePath);
|
|
1267
|
+
if (content.trim()) {
|
|
1268
|
+
const filename = file.replace(".md", "");
|
|
1269
|
+
const frontmatter = {
|
|
1270
|
+
root: false,
|
|
1271
|
+
targets: ["cline"],
|
|
1272
|
+
description: `Cline rule: ${filename}`,
|
|
1273
|
+
globs: ["**/*"]
|
|
1274
|
+
};
|
|
1275
|
+
rules.push({
|
|
1276
|
+
frontmatter,
|
|
1277
|
+
content: content.trim(),
|
|
1278
|
+
filename: `cline-${filename}`,
|
|
1279
|
+
filepath: filePath
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1284
|
+
errors.push(`Failed to parse ${filePath}: ${errorMessage}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1290
|
+
errors.push(`Failed to parse .clinerules files: ${errorMessage}`);
|
|
804
1291
|
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
errors.push(
|
|
1292
|
+
}
|
|
1293
|
+
if (rules.length === 0) {
|
|
1294
|
+
errors.push("No Cline configuration files found (.cline/instructions.md or .clinerules/*.md)");
|
|
808
1295
|
}
|
|
809
1296
|
return { rules, errors };
|
|
810
1297
|
}
|
|
811
1298
|
|
|
812
1299
|
// src/parsers/copilot.ts
|
|
813
|
-
import { basename as basename3, join as
|
|
1300
|
+
import { basename as basename3, join as join12 } from "path";
|
|
814
1301
|
import matter2 from "gray-matter";
|
|
815
1302
|
async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
816
1303
|
const errors = [];
|
|
817
1304
|
const rules = [];
|
|
818
|
-
const copilotFilePath =
|
|
1305
|
+
const copilotFilePath = join12(baseDir, ".github", "copilot-instructions.md");
|
|
819
1306
|
if (await fileExists(copilotFilePath)) {
|
|
820
1307
|
try {
|
|
821
1308
|
const rawContent = await readFileContent(copilotFilePath);
|
|
@@ -840,14 +1327,14 @@ async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
|
840
1327
|
errors.push(`Failed to parse copilot-instructions.md: ${errorMessage}`);
|
|
841
1328
|
}
|
|
842
1329
|
}
|
|
843
|
-
const instructionsDir =
|
|
1330
|
+
const instructionsDir = join12(baseDir, ".github", "instructions");
|
|
844
1331
|
if (await fileExists(instructionsDir)) {
|
|
845
1332
|
try {
|
|
846
1333
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
847
1334
|
const files = await readdir2(instructionsDir);
|
|
848
1335
|
for (const file of files) {
|
|
849
1336
|
if (file.endsWith(".instructions.md")) {
|
|
850
|
-
const filePath =
|
|
1337
|
+
const filePath = join12(instructionsDir, file);
|
|
851
1338
|
const rawContent = await readFileContent(filePath);
|
|
852
1339
|
const parsed = matter2(rawContent);
|
|
853
1340
|
const content = parsed.content.trim();
|
|
@@ -882,19 +1369,19 @@ async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
|
882
1369
|
}
|
|
883
1370
|
|
|
884
1371
|
// src/parsers/cursor.ts
|
|
885
|
-
import { basename as basename4, join as
|
|
1372
|
+
import { basename as basename4, join as join13 } from "path";
|
|
886
1373
|
import matter3 from "gray-matter";
|
|
887
|
-
import
|
|
1374
|
+
import { DEFAULT_SCHEMA, FAILSAFE_SCHEMA, load } from "js-yaml";
|
|
888
1375
|
var customMatterOptions = {
|
|
889
1376
|
engines: {
|
|
890
1377
|
yaml: {
|
|
891
1378
|
parse: (str) => {
|
|
892
1379
|
try {
|
|
893
1380
|
const preprocessed = str.replace(/^(\s*globs:\s*)\*\s*$/gm, '$1"*"');
|
|
894
|
-
return
|
|
1381
|
+
return load(preprocessed, { schema: DEFAULT_SCHEMA });
|
|
895
1382
|
} catch (error) {
|
|
896
1383
|
try {
|
|
897
|
-
return
|
|
1384
|
+
return load(str, { schema: FAILSAFE_SCHEMA });
|
|
898
1385
|
} catch {
|
|
899
1386
|
throw error;
|
|
900
1387
|
}
|
|
@@ -906,7 +1393,9 @@ var customMatterOptions = {
|
|
|
906
1393
|
async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
907
1394
|
const errors = [];
|
|
908
1395
|
const rules = [];
|
|
909
|
-
|
|
1396
|
+
let ignorePatterns;
|
|
1397
|
+
let mcpServers;
|
|
1398
|
+
const cursorFilePath = join13(baseDir, ".cursorrules");
|
|
910
1399
|
if (await fileExists(cursorFilePath)) {
|
|
911
1400
|
try {
|
|
912
1401
|
const rawContent = await readFileContent(cursorFilePath);
|
|
@@ -931,14 +1420,14 @@ async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
|
931
1420
|
errors.push(`Failed to parse .cursorrules file: ${errorMessage}`);
|
|
932
1421
|
}
|
|
933
1422
|
}
|
|
934
|
-
const cursorRulesDir =
|
|
1423
|
+
const cursorRulesDir = join13(baseDir, ".cursor", "rules");
|
|
935
1424
|
if (await fileExists(cursorRulesDir)) {
|
|
936
1425
|
try {
|
|
937
1426
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
938
1427
|
const files = await readdir2(cursorRulesDir);
|
|
939
1428
|
for (const file of files) {
|
|
940
1429
|
if (file.endsWith(".mdc")) {
|
|
941
|
-
const filePath =
|
|
1430
|
+
const filePath = join13(cursorRulesDir, file);
|
|
942
1431
|
try {
|
|
943
1432
|
const rawContent = await readFileContent(filePath);
|
|
944
1433
|
const parsed = matter3(rawContent, customMatterOptions);
|
|
@@ -972,38 +1461,244 @@ async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
|
972
1461
|
if (rules.length === 0) {
|
|
973
1462
|
errors.push("No Cursor configuration files found (.cursorrules or .cursor/rules/*.mdc)");
|
|
974
1463
|
}
|
|
975
|
-
|
|
1464
|
+
const cursorIgnorePath = join13(baseDir, ".cursorignore");
|
|
1465
|
+
if (await fileExists(cursorIgnorePath)) {
|
|
1466
|
+
try {
|
|
1467
|
+
const content = await readFileContent(cursorIgnorePath);
|
|
1468
|
+
const patterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1469
|
+
if (patterns.length > 0) {
|
|
1470
|
+
ignorePatterns = patterns;
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1474
|
+
errors.push(`Failed to parse .cursorignore: ${errorMessage}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const cursorMcpPath = join13(baseDir, ".cursor", "mcp.json");
|
|
1478
|
+
if (await fileExists(cursorMcpPath)) {
|
|
1479
|
+
try {
|
|
1480
|
+
const content = await readFileContent(cursorMcpPath);
|
|
1481
|
+
const mcp = JSON.parse(content);
|
|
1482
|
+
if (mcp.mcpServers && Object.keys(mcp.mcpServers).length > 0) {
|
|
1483
|
+
mcpServers = mcp.mcpServers;
|
|
1484
|
+
}
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1487
|
+
errors.push(`Failed to parse .cursor/mcp.json: ${errorMessage}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
rules,
|
|
1492
|
+
errors,
|
|
1493
|
+
...ignorePatterns && { ignorePatterns },
|
|
1494
|
+
...mcpServers && { mcpServers }
|
|
1495
|
+
};
|
|
976
1496
|
}
|
|
977
1497
|
|
|
978
|
-
// src/parsers/
|
|
979
|
-
import { join as
|
|
980
|
-
async function
|
|
1498
|
+
// src/parsers/geminicli.ts
|
|
1499
|
+
import { basename as basename5, join as join14 } from "path";
|
|
1500
|
+
async function parseGeminiConfiguration(baseDir = process.cwd()) {
|
|
981
1501
|
const errors = [];
|
|
982
1502
|
const rules = [];
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1503
|
+
let ignorePatterns;
|
|
1504
|
+
let mcpServers;
|
|
1505
|
+
const geminiFilePath = join14(baseDir, "GEMINI.md");
|
|
1506
|
+
if (!await fileExists(geminiFilePath)) {
|
|
1507
|
+
errors.push("GEMINI.md file not found");
|
|
986
1508
|
return { rules, errors };
|
|
987
1509
|
}
|
|
988
1510
|
try {
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
rules.push(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1511
|
+
const geminiContent = await readFileContent(geminiFilePath);
|
|
1512
|
+
const mainRule = parseGeminiMainFile(geminiContent, geminiFilePath);
|
|
1513
|
+
if (mainRule) {
|
|
1514
|
+
rules.push(mainRule);
|
|
1515
|
+
}
|
|
1516
|
+
const memoryDir = join14(baseDir, ".gemini", "memories");
|
|
1517
|
+
if (await fileExists(memoryDir)) {
|
|
1518
|
+
const memoryRules = await parseGeminiMemoryFiles(memoryDir);
|
|
1519
|
+
rules.push(...memoryRules);
|
|
1520
|
+
}
|
|
1521
|
+
const settingsPath = join14(baseDir, ".gemini", "settings.json");
|
|
1522
|
+
if (await fileExists(settingsPath)) {
|
|
1523
|
+
const settingsResult = await parseGeminiSettings(settingsPath);
|
|
1524
|
+
if (settingsResult.ignorePatterns) {
|
|
1525
|
+
ignorePatterns = settingsResult.ignorePatterns;
|
|
1526
|
+
}
|
|
1527
|
+
if (settingsResult.mcpServers) {
|
|
1528
|
+
mcpServers = settingsResult.mcpServers;
|
|
1529
|
+
}
|
|
1530
|
+
errors.push(...settingsResult.errors);
|
|
1531
|
+
}
|
|
1532
|
+
const aiexcludePath = join14(baseDir, ".aiexclude");
|
|
1533
|
+
if (await fileExists(aiexcludePath)) {
|
|
1534
|
+
const aiexcludePatterns = await parseAiexclude(aiexcludePath);
|
|
1535
|
+
if (aiexcludePatterns.length > 0) {
|
|
1536
|
+
ignorePatterns = ignorePatterns ? [...ignorePatterns, ...aiexcludePatterns] : aiexcludePatterns;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1541
|
+
errors.push(`Failed to parse Gemini configuration: ${errorMessage}`);
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
rules,
|
|
1545
|
+
errors,
|
|
1546
|
+
...ignorePatterns && { ignorePatterns },
|
|
1547
|
+
...mcpServers && { mcpServers }
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function parseGeminiMainFile(content, filepath) {
|
|
1551
|
+
const lines = content.split("\n");
|
|
1552
|
+
let contentStartIndex = 0;
|
|
1553
|
+
if (lines.some((line) => line.includes("| Document | Description | File Patterns |"))) {
|
|
1554
|
+
const tableEndIndex = lines.findIndex(
|
|
1555
|
+
(line, index) => index > 0 && line.trim() === "" && lines[index - 1]?.includes("|") && !lines[index + 1]?.includes("|")
|
|
1556
|
+
);
|
|
1557
|
+
if (tableEndIndex !== -1) {
|
|
1558
|
+
contentStartIndex = tableEndIndex + 1;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const mainContent = lines.slice(contentStartIndex).join("\n").trim();
|
|
1562
|
+
if (!mainContent) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
const frontmatter = {
|
|
1566
|
+
root: false,
|
|
1567
|
+
targets: ["geminicli"],
|
|
1568
|
+
description: "Main Gemini CLI configuration",
|
|
1569
|
+
globs: ["**/*"]
|
|
1570
|
+
};
|
|
1571
|
+
return {
|
|
1572
|
+
frontmatter,
|
|
1573
|
+
content: mainContent,
|
|
1574
|
+
filename: "gemini-main",
|
|
1575
|
+
filepath
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
async function parseGeminiMemoryFiles(memoryDir) {
|
|
1579
|
+
const rules = [];
|
|
1580
|
+
try {
|
|
1581
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1582
|
+
const files = await readdir2(memoryDir);
|
|
1583
|
+
for (const file of files) {
|
|
1584
|
+
if (file.endsWith(".md")) {
|
|
1585
|
+
const filePath = join14(memoryDir, file);
|
|
1586
|
+
const content = await readFileContent(filePath);
|
|
1587
|
+
if (content.trim()) {
|
|
1588
|
+
const filename = basename5(file, ".md");
|
|
1589
|
+
const frontmatter = {
|
|
1590
|
+
root: false,
|
|
1591
|
+
targets: ["geminicli"],
|
|
1592
|
+
description: `Memory file: ${filename}`,
|
|
1593
|
+
globs: ["**/*"]
|
|
1594
|
+
};
|
|
1595
|
+
rules.push({
|
|
1596
|
+
frontmatter,
|
|
1597
|
+
content: content.trim(),
|
|
1598
|
+
filename: `gemini-memory-${filename}`,
|
|
1599
|
+
filepath: filePath
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
return rules;
|
|
1607
|
+
}
|
|
1608
|
+
async function parseGeminiSettings(settingsPath) {
|
|
1609
|
+
const errors = [];
|
|
1610
|
+
let mcpServers;
|
|
1611
|
+
try {
|
|
1612
|
+
const content = await readFileContent(settingsPath);
|
|
1613
|
+
const settings = JSON.parse(content);
|
|
1614
|
+
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
|
|
1615
|
+
mcpServers = settings.mcpServers;
|
|
1003
1616
|
}
|
|
1004
1617
|
} catch (error) {
|
|
1005
1618
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1006
|
-
errors.push(`Failed to parse
|
|
1619
|
+
errors.push(`Failed to parse settings.json: ${errorMessage}`);
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
errors,
|
|
1623
|
+
...mcpServers && { mcpServers }
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
async function parseAiexclude(aiexcludePath) {
|
|
1627
|
+
try {
|
|
1628
|
+
const content = await readFileContent(aiexcludePath);
|
|
1629
|
+
const patterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1630
|
+
return patterns;
|
|
1631
|
+
} catch {
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/parsers/roo.ts
|
|
1637
|
+
import { join as join15 } from "path";
|
|
1638
|
+
async function parseRooConfiguration(baseDir = process.cwd()) {
|
|
1639
|
+
const errors = [];
|
|
1640
|
+
const rules = [];
|
|
1641
|
+
const rooFilePath = join15(baseDir, ".roo", "instructions.md");
|
|
1642
|
+
if (await fileExists(rooFilePath)) {
|
|
1643
|
+
try {
|
|
1644
|
+
const content = await readFileContent(rooFilePath);
|
|
1645
|
+
if (content.trim()) {
|
|
1646
|
+
const frontmatter = {
|
|
1647
|
+
root: false,
|
|
1648
|
+
targets: ["roo"],
|
|
1649
|
+
description: "Roo Code instructions",
|
|
1650
|
+
globs: ["**/*"]
|
|
1651
|
+
};
|
|
1652
|
+
rules.push({
|
|
1653
|
+
frontmatter,
|
|
1654
|
+
content: content.trim(),
|
|
1655
|
+
filename: "roo-instructions",
|
|
1656
|
+
filepath: rooFilePath
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1661
|
+
errors.push(`Failed to parse .roo/instructions.md: ${errorMessage}`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const rooRulesDir = join15(baseDir, ".roo", "rules");
|
|
1665
|
+
if (await fileExists(rooRulesDir)) {
|
|
1666
|
+
try {
|
|
1667
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1668
|
+
const files = await readdir2(rooRulesDir);
|
|
1669
|
+
for (const file of files) {
|
|
1670
|
+
if (file.endsWith(".md")) {
|
|
1671
|
+
const filePath = join15(rooRulesDir, file);
|
|
1672
|
+
try {
|
|
1673
|
+
const content = await readFileContent(filePath);
|
|
1674
|
+
if (content.trim()) {
|
|
1675
|
+
const filename = file.replace(".md", "");
|
|
1676
|
+
const frontmatter = {
|
|
1677
|
+
root: false,
|
|
1678
|
+
targets: ["roo"],
|
|
1679
|
+
description: `Roo rule: ${filename}`,
|
|
1680
|
+
globs: ["**/*"]
|
|
1681
|
+
};
|
|
1682
|
+
rules.push({
|
|
1683
|
+
frontmatter,
|
|
1684
|
+
content: content.trim(),
|
|
1685
|
+
filename: `roo-${filename}`,
|
|
1686
|
+
filepath: filePath
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1691
|
+
errors.push(`Failed to parse ${filePath}: ${errorMessage}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
} catch (error) {
|
|
1696
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1697
|
+
errors.push(`Failed to parse .roo/rules files: ${errorMessage}`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (rules.length === 0) {
|
|
1701
|
+
errors.push("No Roo Code configuration files found (.roo/instructions.md or .roo/rules/*.md)");
|
|
1007
1702
|
}
|
|
1008
1703
|
return { rules, errors };
|
|
1009
1704
|
}
|
|
@@ -1013,6 +1708,8 @@ async function importConfiguration(options) {
|
|
|
1013
1708
|
const { tool, baseDir = process.cwd(), rulesDir = ".rulesync", verbose = false } = options;
|
|
1014
1709
|
const errors = [];
|
|
1015
1710
|
let rules = [];
|
|
1711
|
+
let ignorePatterns;
|
|
1712
|
+
let mcpServers;
|
|
1016
1713
|
if (verbose) {
|
|
1017
1714
|
console.log(`Importing ${tool} configuration from ${baseDir}...`);
|
|
1018
1715
|
}
|
|
@@ -1022,12 +1719,16 @@ async function importConfiguration(options) {
|
|
|
1022
1719
|
const claudeResult = await parseClaudeConfiguration(baseDir);
|
|
1023
1720
|
rules = claudeResult.rules;
|
|
1024
1721
|
errors.push(...claudeResult.errors);
|
|
1722
|
+
ignorePatterns = claudeResult.ignorePatterns;
|
|
1723
|
+
mcpServers = claudeResult.mcpServers;
|
|
1025
1724
|
break;
|
|
1026
1725
|
}
|
|
1027
1726
|
case "cursor": {
|
|
1028
1727
|
const cursorResult = await parseCursorConfiguration(baseDir);
|
|
1029
1728
|
rules = cursorResult.rules;
|
|
1030
1729
|
errors.push(...cursorResult.errors);
|
|
1730
|
+
ignorePatterns = cursorResult.ignorePatterns;
|
|
1731
|
+
mcpServers = cursorResult.mcpServers;
|
|
1031
1732
|
break;
|
|
1032
1733
|
}
|
|
1033
1734
|
case "copilot": {
|
|
@@ -1048,6 +1749,14 @@ async function importConfiguration(options) {
|
|
|
1048
1749
|
errors.push(...rooResult.errors);
|
|
1049
1750
|
break;
|
|
1050
1751
|
}
|
|
1752
|
+
case "geminicli": {
|
|
1753
|
+
const geminiResult = await parseGeminiConfiguration(baseDir);
|
|
1754
|
+
rules = geminiResult.rules;
|
|
1755
|
+
errors.push(...geminiResult.errors);
|
|
1756
|
+
ignorePatterns = geminiResult.ignorePatterns;
|
|
1757
|
+
mcpServers = geminiResult.mcpServers;
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1051
1760
|
default:
|
|
1052
1761
|
errors.push(`Unsupported tool: ${tool}`);
|
|
1053
1762
|
return { success: false, rulesCreated: 0, errors };
|
|
@@ -1057,10 +1766,10 @@ async function importConfiguration(options) {
|
|
|
1057
1766
|
errors.push(`Failed to parse ${tool} configuration: ${errorMessage}`);
|
|
1058
1767
|
return { success: false, rulesCreated: 0, errors };
|
|
1059
1768
|
}
|
|
1060
|
-
if (rules.length === 0) {
|
|
1769
|
+
if (rules.length === 0 && !ignorePatterns && !mcpServers) {
|
|
1061
1770
|
return { success: false, rulesCreated: 0, errors };
|
|
1062
1771
|
}
|
|
1063
|
-
const rulesDirPath =
|
|
1772
|
+
const rulesDirPath = join16(baseDir, rulesDir);
|
|
1064
1773
|
try {
|
|
1065
1774
|
const { mkdir: mkdir3 } = await import("fs/promises");
|
|
1066
1775
|
await mkdir3(rulesDirPath, { recursive: true });
|
|
@@ -1074,7 +1783,7 @@ async function importConfiguration(options) {
|
|
|
1074
1783
|
try {
|
|
1075
1784
|
const baseFilename = `${tool}__${rule.filename}`;
|
|
1076
1785
|
const filename = await generateUniqueFilename(rulesDirPath, baseFilename);
|
|
1077
|
-
const filePath =
|
|
1786
|
+
const filePath = join16(rulesDirPath, `${filename}.md`);
|
|
1078
1787
|
const content = generateRuleFileContent(rule);
|
|
1079
1788
|
await writeFileContent(filePath, content);
|
|
1080
1789
|
rulesCreated++;
|
|
@@ -1086,10 +1795,44 @@ async function importConfiguration(options) {
|
|
|
1086
1795
|
errors.push(`Failed to create rule file for ${rule.filename}: ${errorMessage}`);
|
|
1087
1796
|
}
|
|
1088
1797
|
}
|
|
1798
|
+
let ignoreFileCreated = false;
|
|
1799
|
+
if (ignorePatterns && ignorePatterns.length > 0) {
|
|
1800
|
+
try {
|
|
1801
|
+
const rulesyncignorePath = join16(baseDir, ".rulesyncignore");
|
|
1802
|
+
const ignoreContent = `${ignorePatterns.join("\n")}
|
|
1803
|
+
`;
|
|
1804
|
+
await writeFileContent(rulesyncignorePath, ignoreContent);
|
|
1805
|
+
ignoreFileCreated = true;
|
|
1806
|
+
if (verbose) {
|
|
1807
|
+
console.log(`\u2705 Created .rulesyncignore with ${ignorePatterns.length} patterns`);
|
|
1808
|
+
}
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1811
|
+
errors.push(`Failed to create .rulesyncignore: ${errorMessage}`);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
let mcpFileCreated = false;
|
|
1815
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
1816
|
+
try {
|
|
1817
|
+
const mcpPath = join16(baseDir, rulesDir, ".mcp.json");
|
|
1818
|
+
const mcpContent = `${JSON.stringify({ mcpServers }, null, 2)}
|
|
1819
|
+
`;
|
|
1820
|
+
await writeFileContent(mcpPath, mcpContent);
|
|
1821
|
+
mcpFileCreated = true;
|
|
1822
|
+
if (verbose) {
|
|
1823
|
+
console.log(`\u2705 Created .mcp.json with ${Object.keys(mcpServers).length} servers`);
|
|
1824
|
+
}
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1827
|
+
errors.push(`Failed to create .mcp.json: ${errorMessage}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1089
1830
|
return {
|
|
1090
|
-
success: rulesCreated > 0,
|
|
1831
|
+
success: rulesCreated > 0 || ignoreFileCreated || mcpFileCreated,
|
|
1091
1832
|
rulesCreated,
|
|
1092
|
-
errors
|
|
1833
|
+
errors,
|
|
1834
|
+
ignoreFileCreated,
|
|
1835
|
+
mcpFileCreated
|
|
1093
1836
|
};
|
|
1094
1837
|
}
|
|
1095
1838
|
function generateRuleFileContent(rule) {
|
|
@@ -1099,7 +1842,7 @@ function generateRuleFileContent(rule) {
|
|
|
1099
1842
|
async function generateUniqueFilename(rulesDir, baseFilename) {
|
|
1100
1843
|
let filename = baseFilename;
|
|
1101
1844
|
let counter = 1;
|
|
1102
|
-
while (await fileExists(
|
|
1845
|
+
while (await fileExists(join16(rulesDir, `${filename}.md`))) {
|
|
1103
1846
|
filename = `${baseFilename}-${counter}`;
|
|
1104
1847
|
counter++;
|
|
1105
1848
|
}
|
|
@@ -1117,57 +1860,54 @@ async function importCommand(options = {}) {
|
|
|
1117
1860
|
if (options.geminicli) tools.push("geminicli");
|
|
1118
1861
|
if (tools.length === 0) {
|
|
1119
1862
|
console.error(
|
|
1120
|
-
"\u274C Please specify
|
|
1863
|
+
"\u274C Please specify one tool to import from (--claudecode, --cursor, --copilot, --cline, --roo, --geminicli)"
|
|
1121
1864
|
);
|
|
1122
1865
|
process.exit(1);
|
|
1123
1866
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
for (const tool of tools) {
|
|
1128
|
-
if (options.verbose) {
|
|
1129
|
-
console.log(`
|
|
1130
|
-
Importing from ${tool}...`);
|
|
1131
|
-
}
|
|
1132
|
-
try {
|
|
1133
|
-
const result = await importConfiguration({
|
|
1134
|
-
tool,
|
|
1135
|
-
verbose: options.verbose ?? false
|
|
1136
|
-
});
|
|
1137
|
-
if (result.success) {
|
|
1138
|
-
console.log(`\u2705 Imported ${result.rulesCreated} rule(s) from ${tool}`);
|
|
1139
|
-
totalRulesCreated += result.rulesCreated;
|
|
1140
|
-
} else if (result.errors.length > 0) {
|
|
1141
|
-
console.warn(`\u26A0\uFE0F Failed to import from ${tool}: ${result.errors[0]}`);
|
|
1142
|
-
if (options.verbose) {
|
|
1143
|
-
allErrors.push(...result.errors);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
} catch (error) {
|
|
1147
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1148
|
-
console.error(`\u274C Error importing from ${tool}: ${errorMessage}`);
|
|
1149
|
-
allErrors.push(`${tool}: ${errorMessage}`);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
if (totalRulesCreated > 0) {
|
|
1153
|
-
console.log(`
|
|
1154
|
-
\u{1F389} Successfully imported ${totalRulesCreated} rule(s) total!`);
|
|
1155
|
-
console.log("You can now run 'rulesync generate' to create tool-specific configurations.");
|
|
1156
|
-
} else {
|
|
1157
|
-
console.warn(
|
|
1158
|
-
"\n\u26A0\uFE0F No rules were imported. Please check that configuration files exist for the selected tools."
|
|
1867
|
+
if (tools.length > 1) {
|
|
1868
|
+
console.error(
|
|
1869
|
+
"\u274C Only one tool can be specified at a time. Please run the import command separately for each tool."
|
|
1159
1870
|
);
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
const tool = tools[0];
|
|
1874
|
+
if (!tool) {
|
|
1875
|
+
console.error("Error: No tool specified");
|
|
1876
|
+
process.exit(1);
|
|
1160
1877
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1878
|
+
console.log(`Importing configuration files from ${tool}...`);
|
|
1879
|
+
try {
|
|
1880
|
+
const result = await importConfiguration({
|
|
1881
|
+
tool,
|
|
1882
|
+
verbose: options.verbose ?? false
|
|
1883
|
+
});
|
|
1884
|
+
if (result.success) {
|
|
1885
|
+
console.log(`\u2705 Imported ${result.rulesCreated} rule(s) from ${tool}`);
|
|
1886
|
+
if (result.ignoreFileCreated) {
|
|
1887
|
+
console.log("\u2705 Created .rulesyncignore file from ignore patterns");
|
|
1888
|
+
}
|
|
1889
|
+
if (result.mcpFileCreated) {
|
|
1890
|
+
console.log("\u2705 Created .rulesync/.mcp.json file from MCP configuration");
|
|
1891
|
+
}
|
|
1892
|
+
console.log("You can now run 'rulesync generate' to create tool-specific configurations.");
|
|
1893
|
+
} else if (result.errors.length > 0) {
|
|
1894
|
+
console.warn(`\u26A0\uFE0F Failed to import from ${tool}: ${result.errors[0]}`);
|
|
1895
|
+
if (options.verbose && result.errors.length > 1) {
|
|
1896
|
+
console.log("\nDetailed errors:");
|
|
1897
|
+
for (const error of result.errors) {
|
|
1898
|
+
console.log(` - ${error}`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1165
1901
|
}
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1904
|
+
console.error(`\u274C Error importing from ${tool}: ${errorMessage}`);
|
|
1905
|
+
process.exit(1);
|
|
1166
1906
|
}
|
|
1167
1907
|
}
|
|
1168
1908
|
|
|
1169
1909
|
// src/cli/commands/init.ts
|
|
1170
|
-
import { join as
|
|
1910
|
+
import { join as join17 } from "path";
|
|
1171
1911
|
async function initCommand() {
|
|
1172
1912
|
const aiRulesDir = ".rulesync";
|
|
1173
1913
|
console.log("Initializing rulesync...");
|
|
@@ -1297,7 +2037,7 @@ globs: ["src/api/**/*.ts", "src/services/**/*.ts", "src/models/**/*.ts"]
|
|
|
1297
2037
|
}
|
|
1298
2038
|
];
|
|
1299
2039
|
for (const file of sampleFiles) {
|
|
1300
|
-
const filepath =
|
|
2040
|
+
const filepath = join17(aiRulesDir, file.filename);
|
|
1301
2041
|
if (!await fileExists(filepath)) {
|
|
1302
2042
|
await writeFileContent(filepath, file.content);
|
|
1303
2043
|
console.log(`Created ${filepath}`);
|
|
@@ -1399,22 +2139,22 @@ async function validateCommand() {
|
|
|
1399
2139
|
}
|
|
1400
2140
|
|
|
1401
2141
|
// src/cli/commands/watch.ts
|
|
1402
|
-
import
|
|
2142
|
+
import { watch } from "chokidar";
|
|
1403
2143
|
async function watchCommand() {
|
|
1404
2144
|
const config = getDefaultConfig();
|
|
1405
2145
|
console.log("\u{1F440} Watching for changes in .rulesync directory...");
|
|
1406
2146
|
console.log("Press Ctrl+C to stop watching");
|
|
1407
2147
|
await generateCommand({ verbose: false });
|
|
1408
|
-
const watcher =
|
|
2148
|
+
const watcher = watch(`${config.aiRulesDir}/**/*.md`, {
|
|
1409
2149
|
ignoreInitial: true,
|
|
1410
2150
|
persistent: true
|
|
1411
2151
|
});
|
|
1412
2152
|
let isGenerating = false;
|
|
1413
|
-
const handleChange = async (
|
|
2153
|
+
const handleChange = async (path4) => {
|
|
1414
2154
|
if (isGenerating) return;
|
|
1415
2155
|
isGenerating = true;
|
|
1416
2156
|
console.log(`
|
|
1417
|
-
\u{1F4DD} Detected change in ${
|
|
2157
|
+
\u{1F4DD} Detected change in ${path4}`);
|
|
1418
2158
|
try {
|
|
1419
2159
|
await generateCommand({ verbose: false });
|
|
1420
2160
|
console.log("\u2705 Regenerated configuration files");
|
|
@@ -1424,10 +2164,10 @@ async function watchCommand() {
|
|
|
1424
2164
|
isGenerating = false;
|
|
1425
2165
|
}
|
|
1426
2166
|
};
|
|
1427
|
-
watcher.on("change", handleChange).on("add", handleChange).on("unlink", (
|
|
2167
|
+
watcher.on("change", handleChange).on("add", handleChange).on("unlink", (path4) => {
|
|
1428
2168
|
console.log(`
|
|
1429
|
-
\u{1F5D1}\uFE0F Removed ${
|
|
1430
|
-
handleChange(
|
|
2169
|
+
\u{1F5D1}\uFE0F Removed ${path4}`);
|
|
2170
|
+
handleChange(path4);
|
|
1431
2171
|
}).on("error", (error) => {
|
|
1432
2172
|
console.error("\u274C Watcher error:", error);
|
|
1433
2173
|
});
|
|
@@ -1440,7 +2180,7 @@ async function watchCommand() {
|
|
|
1440
2180
|
|
|
1441
2181
|
// src/cli/index.ts
|
|
1442
2182
|
var program = new Command();
|
|
1443
|
-
program.name("rulesync").description("Unified AI rules management CLI tool").version("0.
|
|
2183
|
+
program.name("rulesync").description("Unified AI rules management CLI tool").version("0.37.0");
|
|
1444
2184
|
program.command("init").description("Initialize rulesync in current directory").action(initCommand);
|
|
1445
2185
|
program.command("add <filename>").description("Add a new rule file").action(addCommand);
|
|
1446
2186
|
program.command("gitignore").description("Add generated files to .gitignore").action(gitignoreCommand);
|