rulesync 0.33.0 → 0.36.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 +104 -8
- package/README.md +104 -8
- package/dist/chunk-6PQ4APY4.mjs +70 -0
- package/dist/chunk-QHXMJZTJ.mjs +62 -0
- package/dist/chunk-QVPQ2X4L.mjs +77 -0
- package/dist/chunk-SBYRCTWS.mjs +64 -0
- package/dist/chunk-UNTCJDMQ.mjs +73 -0
- package/dist/chunk-YGXGGUBG.mjs +80 -0
- package/dist/claude-O4SRX6VC.mjs +8 -0
- package/dist/cline-H5JF2OPT.mjs +8 -0
- package/dist/copilot-GCIYHK4Q.mjs +8 -0
- package/dist/cursor-N75OH6WS.mjs +8 -0
- package/dist/geminicli-AGOQ32ZE.mjs +8 -0
- package/dist/index.js +1291 -214
- package/dist/index.mjs +990 -207
- package/dist/roo-V5YVC222.mjs +8 -0
- package/package.json +14 -6
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
generateClaudeMcp
|
|
4
|
+
} from "./chunk-UNTCJDMQ.mjs";
|
|
5
|
+
import {
|
|
6
|
+
generateClineMcp
|
|
7
|
+
} from "./chunk-QHXMJZTJ.mjs";
|
|
8
|
+
import {
|
|
9
|
+
generateCopilotMcp
|
|
10
|
+
} from "./chunk-YGXGGUBG.mjs";
|
|
11
|
+
import {
|
|
12
|
+
generateCursorMcp
|
|
13
|
+
} from "./chunk-SBYRCTWS.mjs";
|
|
14
|
+
import {
|
|
15
|
+
generateGeminiCliMcp
|
|
16
|
+
} from "./chunk-6PQ4APY4.mjs";
|
|
17
|
+
import {
|
|
18
|
+
generateRooMcp
|
|
19
|
+
} from "./chunk-QVPQ2X4L.mjs";
|
|
2
20
|
|
|
3
21
|
// src/cli/index.ts
|
|
4
22
|
import { Command } from "commander";
|
|
@@ -16,10 +34,12 @@ function getDefaultConfig() {
|
|
|
16
34
|
cursor: ".cursor/rules",
|
|
17
35
|
cline: ".clinerules",
|
|
18
36
|
claudecode: ".",
|
|
19
|
-
|
|
37
|
+
claude: ".",
|
|
38
|
+
roo: ".roo/rules",
|
|
39
|
+
geminicli: ".gemini/memories"
|
|
20
40
|
},
|
|
21
41
|
watchEnabled: false,
|
|
22
|
-
defaultTargets: ["copilot", "cursor", "cline", "claudecode", "roo"]
|
|
42
|
+
defaultTargets: ["copilot", "cursor", "cline", "claudecode", "claude", "roo", "geminicli"]
|
|
23
43
|
};
|
|
24
44
|
}
|
|
25
45
|
function resolveTargets(targets, config) {
|
|
@@ -65,27 +85,158 @@ async function addCommand(filename) {
|
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
// src/generators/claudecode.ts
|
|
88
|
+
// src/generators/rules/claudecode.ts
|
|
89
|
+
import { join as join3 } from "path";
|
|
90
|
+
|
|
91
|
+
// src/utils/file.ts
|
|
92
|
+
import { mkdir as mkdir2, readdir, readFile, rm, stat, writeFile as writeFile2 } from "fs/promises";
|
|
93
|
+
import { dirname, join as join2 } from "path";
|
|
94
|
+
|
|
95
|
+
// src/utils/ignore.ts
|
|
69
96
|
import { join } from "path";
|
|
97
|
+
import micromatch from "micromatch";
|
|
98
|
+
var cachedIgnorePatterns = null;
|
|
99
|
+
async function loadIgnorePatterns(baseDir = process.cwd()) {
|
|
100
|
+
if (cachedIgnorePatterns) {
|
|
101
|
+
return cachedIgnorePatterns;
|
|
102
|
+
}
|
|
103
|
+
const ignorePath = join(baseDir, ".rulesyncignore");
|
|
104
|
+
if (!await fileExists(ignorePath)) {
|
|
105
|
+
cachedIgnorePatterns = { patterns: [] };
|
|
106
|
+
return cachedIgnorePatterns;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const content = await readFileContent(ignorePath);
|
|
110
|
+
const patterns = parseIgnoreFile(content);
|
|
111
|
+
cachedIgnorePatterns = { patterns };
|
|
112
|
+
return cachedIgnorePatterns;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn(`Failed to read .rulesyncignore: ${error}`);
|
|
115
|
+
cachedIgnorePatterns = { patterns: [] };
|
|
116
|
+
return cachedIgnorePatterns;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseIgnoreFile(content) {
|
|
120
|
+
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
121
|
+
}
|
|
122
|
+
function isFileIgnored(filepath, ignorePatterns) {
|
|
123
|
+
if (ignorePatterns.length === 0) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const negationPatterns = ignorePatterns.filter((p) => p.startsWith("!"));
|
|
127
|
+
const positivePatterns = ignorePatterns.filter((p) => !p.startsWith("!"));
|
|
128
|
+
const isIgnored = positivePatterns.length > 0 && micromatch.isMatch(filepath, positivePatterns, {
|
|
129
|
+
dot: true
|
|
130
|
+
});
|
|
131
|
+
if (isIgnored && negationPatterns.length > 0) {
|
|
132
|
+
const negationPatternsWithoutPrefix = negationPatterns.map((p) => p.substring(1));
|
|
133
|
+
return !micromatch.isMatch(filepath, negationPatternsWithoutPrefix, {
|
|
134
|
+
dot: true
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return isIgnored;
|
|
138
|
+
}
|
|
139
|
+
function filterIgnoredFiles(files, ignorePatterns) {
|
|
140
|
+
if (ignorePatterns.length === 0) {
|
|
141
|
+
return files;
|
|
142
|
+
}
|
|
143
|
+
return files.filter((file) => !isFileIgnored(file, ignorePatterns));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/utils/file.ts
|
|
147
|
+
async function ensureDir(dirPath) {
|
|
148
|
+
try {
|
|
149
|
+
await stat(dirPath);
|
|
150
|
+
} catch {
|
|
151
|
+
await mkdir2(dirPath, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function readFileContent(filepath) {
|
|
155
|
+
return readFile(filepath, "utf-8");
|
|
156
|
+
}
|
|
157
|
+
async function writeFileContent(filepath, content) {
|
|
158
|
+
await ensureDir(dirname(filepath));
|
|
159
|
+
await writeFile2(filepath, content, "utf-8");
|
|
160
|
+
}
|
|
161
|
+
async function findFiles(dir, extension = ".md", ignorePatterns) {
|
|
162
|
+
try {
|
|
163
|
+
const files = await readdir(dir);
|
|
164
|
+
const filtered = files.filter((file) => file.endsWith(extension)).map((file) => join2(dir, file));
|
|
165
|
+
if (ignorePatterns && ignorePatterns.length > 0) {
|
|
166
|
+
return filterIgnoredFiles(filtered, ignorePatterns);
|
|
167
|
+
}
|
|
168
|
+
return filtered;
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function fileExists(filepath) {
|
|
174
|
+
try {
|
|
175
|
+
await stat(filepath);
|
|
176
|
+
return true;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function removeDirectory(dirPath) {
|
|
182
|
+
const dangerousPaths = [".", "/", "~", "src", "node_modules"];
|
|
183
|
+
if (dangerousPaths.includes(dirPath) || dirPath === "") {
|
|
184
|
+
console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
if (await fileExists(dirPath)) {
|
|
189
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn(`Failed to remove directory ${dirPath}:`, error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function removeFile(filepath) {
|
|
196
|
+
try {
|
|
197
|
+
if (await fileExists(filepath)) {
|
|
198
|
+
await rm(filepath);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn(`Failed to remove file ${filepath}:`, error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function removeClaudeGeneratedFiles() {
|
|
205
|
+
const filesToRemove = ["CLAUDE.md", ".claude/memories"];
|
|
206
|
+
for (const fileOrDir of filesToRemove) {
|
|
207
|
+
if (fileOrDir.endsWith("/memories")) {
|
|
208
|
+
await removeDirectory(fileOrDir);
|
|
209
|
+
} else {
|
|
210
|
+
await removeFile(fileOrDir);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/generators/rules/claudecode.ts
|
|
70
216
|
async function generateClaudecodeConfig(rules, config, baseDir) {
|
|
71
217
|
const outputs = [];
|
|
72
218
|
const rootRules = rules.filter((r) => r.frontmatter.root === true);
|
|
73
219
|
const detailRules = rules.filter((r) => r.frontmatter.root === false);
|
|
74
220
|
const claudeMdContent = generateClaudeMarkdown(rootRules, detailRules);
|
|
75
|
-
const claudeOutputDir = baseDir ?
|
|
221
|
+
const claudeOutputDir = baseDir ? join3(baseDir, config.outputPaths.claudecode) : config.outputPaths.claudecode;
|
|
76
222
|
outputs.push({
|
|
77
223
|
tool: "claudecode",
|
|
78
|
-
filepath:
|
|
224
|
+
filepath: join3(claudeOutputDir, "CLAUDE.md"),
|
|
79
225
|
content: claudeMdContent
|
|
80
226
|
});
|
|
81
227
|
for (const rule of detailRules) {
|
|
82
228
|
const memoryContent = generateMemoryFile(rule);
|
|
83
229
|
outputs.push({
|
|
84
230
|
tool: "claudecode",
|
|
85
|
-
filepath:
|
|
231
|
+
filepath: join3(claudeOutputDir, ".claude", "memories", `${rule.filename}.md`),
|
|
86
232
|
content: memoryContent
|
|
87
233
|
});
|
|
88
234
|
}
|
|
235
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
236
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
237
|
+
const settingsPath = baseDir ? join3(baseDir, ".claude", "settings.json") : join3(".claude", "settings.json");
|
|
238
|
+
await updateClaudeSettings(settingsPath, ignorePatterns.patterns);
|
|
239
|
+
}
|
|
89
240
|
return outputs;
|
|
90
241
|
}
|
|
91
242
|
function generateClaudeMarkdown(rootRules, detailRules) {
|
|
@@ -114,42 +265,101 @@ function generateClaudeMarkdown(rootRules, detailRules) {
|
|
|
114
265
|
function generateMemoryFile(rule) {
|
|
115
266
|
return rule.content.trim();
|
|
116
267
|
}
|
|
268
|
+
async function updateClaudeSettings(settingsPath, ignorePatterns) {
|
|
269
|
+
let settings = {};
|
|
270
|
+
if (await fileExists(settingsPath)) {
|
|
271
|
+
try {
|
|
272
|
+
const content = await readFileContent(settingsPath);
|
|
273
|
+
settings = JSON.parse(content);
|
|
274
|
+
} catch (_error) {
|
|
275
|
+
console.warn(`Failed to parse existing ${settingsPath}, creating new settings`);
|
|
276
|
+
settings = {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!settings.permissions) {
|
|
280
|
+
settings.permissions = {};
|
|
281
|
+
}
|
|
282
|
+
if (!Array.isArray(settings.permissions.deny)) {
|
|
283
|
+
settings.permissions.deny = [];
|
|
284
|
+
}
|
|
285
|
+
const readDenyRules = ignorePatterns.map((pattern) => `Read(${pattern})`);
|
|
286
|
+
settings.permissions.deny = settings.permissions.deny.filter((rule) => {
|
|
287
|
+
if (!rule.startsWith("Read(")) return true;
|
|
288
|
+
const match = rule.match(/^Read\((.*)\)$/);
|
|
289
|
+
if (!match) return true;
|
|
290
|
+
return !ignorePatterns.includes(match[1] ?? "");
|
|
291
|
+
});
|
|
292
|
+
settings.permissions.deny.push(...readDenyRules);
|
|
293
|
+
settings.permissions.deny = [...new Set(settings.permissions.deny)];
|
|
294
|
+
const jsonContent = JSON.stringify(settings, null, 2);
|
|
295
|
+
await writeFileContent(settingsPath, jsonContent);
|
|
296
|
+
console.log(`\u2705 Updated Claude Code settings: ${settingsPath}`);
|
|
297
|
+
}
|
|
117
298
|
|
|
118
|
-
// src/generators/cline.ts
|
|
119
|
-
import { join as
|
|
299
|
+
// src/generators/rules/cline.ts
|
|
300
|
+
import { join as join4 } from "path";
|
|
120
301
|
async function generateClineConfig(rules, config, baseDir) {
|
|
121
302
|
const outputs = [];
|
|
122
303
|
for (const rule of rules) {
|
|
123
304
|
const content = generateClineMarkdown(rule);
|
|
124
|
-
const outputDir = baseDir ?
|
|
125
|
-
const filepath =
|
|
305
|
+
const outputDir = baseDir ? join4(baseDir, config.outputPaths.cline) : config.outputPaths.cline;
|
|
306
|
+
const filepath = join4(outputDir, `${rule.filename}.md`);
|
|
126
307
|
outputs.push({
|
|
127
308
|
tool: "cline",
|
|
128
309
|
filepath,
|
|
129
310
|
content
|
|
130
311
|
});
|
|
131
312
|
}
|
|
313
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
314
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
315
|
+
const clineIgnorePath = baseDir ? join4(baseDir, ".clineignore") : ".clineignore";
|
|
316
|
+
const clineIgnoreContent = generateClineIgnore(ignorePatterns.patterns);
|
|
317
|
+
outputs.push({
|
|
318
|
+
tool: "cline",
|
|
319
|
+
filepath: clineIgnorePath,
|
|
320
|
+
content: clineIgnoreContent
|
|
321
|
+
});
|
|
322
|
+
}
|
|
132
323
|
return outputs;
|
|
133
324
|
}
|
|
134
325
|
function generateClineMarkdown(rule) {
|
|
135
326
|
return rule.content.trim();
|
|
136
327
|
}
|
|
328
|
+
function generateClineIgnore(patterns) {
|
|
329
|
+
const lines = [
|
|
330
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
331
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
332
|
+
"",
|
|
333
|
+
...patterns
|
|
334
|
+
];
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
137
337
|
|
|
138
|
-
// src/generators/copilot.ts
|
|
139
|
-
import { join as
|
|
338
|
+
// src/generators/rules/copilot.ts
|
|
339
|
+
import { join as join5 } from "path";
|
|
140
340
|
async function generateCopilotConfig(rules, config, baseDir) {
|
|
141
341
|
const outputs = [];
|
|
142
342
|
for (const rule of rules) {
|
|
143
343
|
const content = generateCopilotMarkdown(rule);
|
|
144
344
|
const baseFilename = rule.filename.replace(/\.md$/, "");
|
|
145
|
-
const outputDir = baseDir ?
|
|
146
|
-
const filepath =
|
|
345
|
+
const outputDir = baseDir ? join5(baseDir, config.outputPaths.copilot) : config.outputPaths.copilot;
|
|
346
|
+
const filepath = join5(outputDir, `${baseFilename}.instructions.md`);
|
|
147
347
|
outputs.push({
|
|
148
348
|
tool: "copilot",
|
|
149
349
|
filepath,
|
|
150
350
|
content
|
|
151
351
|
});
|
|
152
352
|
}
|
|
353
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
354
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
355
|
+
const copilotIgnorePath = baseDir ? join5(baseDir, ".copilotignore") : ".copilotignore";
|
|
356
|
+
const copilotIgnoreContent = generateCopilotIgnore(ignorePatterns.patterns);
|
|
357
|
+
outputs.push({
|
|
358
|
+
tool: "copilot",
|
|
359
|
+
filepath: copilotIgnorePath,
|
|
360
|
+
content: copilotIgnoreContent
|
|
361
|
+
});
|
|
362
|
+
}
|
|
153
363
|
return outputs;
|
|
154
364
|
}
|
|
155
365
|
function generateCopilotMarkdown(rule) {
|
|
@@ -165,21 +375,42 @@ function generateCopilotMarkdown(rule) {
|
|
|
165
375
|
lines.push(rule.content);
|
|
166
376
|
return lines.join("\n");
|
|
167
377
|
}
|
|
378
|
+
function generateCopilotIgnore(patterns) {
|
|
379
|
+
const lines = [
|
|
380
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
381
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
382
|
+
"# Note: .copilotignore is not officially supported by GitHub Copilot.",
|
|
383
|
+
"# This file is for use with community tools like copilotignore-vscode extension.",
|
|
384
|
+
"",
|
|
385
|
+
...patterns
|
|
386
|
+
];
|
|
387
|
+
return lines.join("\n");
|
|
388
|
+
}
|
|
168
389
|
|
|
169
|
-
// src/generators/cursor.ts
|
|
170
|
-
import { join as
|
|
390
|
+
// src/generators/rules/cursor.ts
|
|
391
|
+
import { join as join6 } from "path";
|
|
171
392
|
async function generateCursorConfig(rules, config, baseDir) {
|
|
172
393
|
const outputs = [];
|
|
173
394
|
for (const rule of rules) {
|
|
174
395
|
const content = generateCursorMarkdown(rule);
|
|
175
|
-
const outputDir = baseDir ?
|
|
176
|
-
const filepath =
|
|
396
|
+
const outputDir = baseDir ? join6(baseDir, config.outputPaths.cursor) : config.outputPaths.cursor;
|
|
397
|
+
const filepath = join6(outputDir, `${rule.filename}.mdc`);
|
|
177
398
|
outputs.push({
|
|
178
399
|
tool: "cursor",
|
|
179
400
|
filepath,
|
|
180
401
|
content
|
|
181
402
|
});
|
|
182
403
|
}
|
|
404
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
405
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
406
|
+
const cursorIgnorePath = baseDir ? join6(baseDir, ".cursorignore") : ".cursorignore";
|
|
407
|
+
const cursorIgnoreContent = generateCursorIgnore(ignorePatterns.patterns);
|
|
408
|
+
outputs.push({
|
|
409
|
+
tool: "cursor",
|
|
410
|
+
filepath: cursorIgnorePath,
|
|
411
|
+
content: cursorIgnoreContent
|
|
412
|
+
});
|
|
413
|
+
}
|
|
183
414
|
return outputs;
|
|
184
415
|
}
|
|
185
416
|
function generateCursorMarkdown(rule) {
|
|
@@ -202,92 +433,125 @@ function generateCursorMarkdown(rule) {
|
|
|
202
433
|
lines.push(rule.content);
|
|
203
434
|
return lines.join("\n");
|
|
204
435
|
}
|
|
436
|
+
function generateCursorIgnore(patterns) {
|
|
437
|
+
const lines = [
|
|
438
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
439
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
440
|
+
"",
|
|
441
|
+
...patterns
|
|
442
|
+
];
|
|
443
|
+
return lines.join("\n");
|
|
444
|
+
}
|
|
205
445
|
|
|
206
|
-
// src/generators/
|
|
207
|
-
import { join as
|
|
208
|
-
async function
|
|
446
|
+
// src/generators/rules/geminicli.ts
|
|
447
|
+
import { join as join7 } from "path";
|
|
448
|
+
async function generateGeminiConfig(rules, config, baseDir) {
|
|
209
449
|
const outputs = [];
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
450
|
+
const rootRule = rules.find((rule) => rule.frontmatter.root === true);
|
|
451
|
+
const memoryRules = rules.filter((rule) => rule.frontmatter.root === false);
|
|
452
|
+
for (const rule of memoryRules) {
|
|
453
|
+
const content = generateGeminiMemoryMarkdown(rule);
|
|
454
|
+
const outputDir = baseDir ? join7(baseDir, config.outputPaths.geminicli) : config.outputPaths.geminicli;
|
|
455
|
+
const filepath = join7(outputDir, `${rule.filename}.md`);
|
|
214
456
|
outputs.push({
|
|
215
|
-
tool: "
|
|
457
|
+
tool: "geminicli",
|
|
216
458
|
filepath,
|
|
217
459
|
content
|
|
218
460
|
});
|
|
219
461
|
}
|
|
462
|
+
const rootContent = generateGeminiRootMarkdown(rootRule, memoryRules, baseDir);
|
|
463
|
+
const rootFilepath = baseDir ? join7(baseDir, "GEMINI.md") : "GEMINI.md";
|
|
464
|
+
outputs.push({
|
|
465
|
+
tool: "geminicli",
|
|
466
|
+
filepath: rootFilepath,
|
|
467
|
+
content: rootContent
|
|
468
|
+
});
|
|
469
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
470
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
471
|
+
const aiexcludePath = baseDir ? join7(baseDir, ".aiexclude") : ".aiexclude";
|
|
472
|
+
const aiexcludeContent = generateAiexclude(ignorePatterns.patterns);
|
|
473
|
+
outputs.push({
|
|
474
|
+
tool: "geminicli",
|
|
475
|
+
filepath: aiexcludePath,
|
|
476
|
+
content: aiexcludeContent
|
|
477
|
+
});
|
|
478
|
+
}
|
|
220
479
|
return outputs;
|
|
221
480
|
}
|
|
222
|
-
function
|
|
481
|
+
function generateGeminiMemoryMarkdown(rule) {
|
|
223
482
|
return rule.content.trim();
|
|
224
483
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
484
|
+
function generateGeminiRootMarkdown(rootRule, memoryRules, _baseDir) {
|
|
485
|
+
const lines = [];
|
|
486
|
+
if (memoryRules.length > 0) {
|
|
487
|
+
lines.push("Please also reference the following documents as needed:");
|
|
488
|
+
lines.push("");
|
|
489
|
+
lines.push("| Document | Description | File Patterns |");
|
|
490
|
+
lines.push("|----------|-------------|---------------|");
|
|
491
|
+
for (const rule of memoryRules) {
|
|
492
|
+
const relativePath = `@.gemini/memories/${rule.filename}.md`;
|
|
493
|
+
const filePatterns = rule.frontmatter.globs.length > 0 ? rule.frontmatter.globs.join(", ") : "-";
|
|
494
|
+
lines.push(`| ${relativePath} | ${rule.frontmatter.description} | ${filePatterns} |`);
|
|
495
|
+
}
|
|
496
|
+
lines.push("");
|
|
497
|
+
lines.push("");
|
|
234
498
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
await writeFile2(filepath, content, "utf-8");
|
|
242
|
-
}
|
|
243
|
-
async function findFiles(dir, extension = ".md") {
|
|
244
|
-
try {
|
|
245
|
-
const files = await readdir(dir);
|
|
246
|
-
return files.filter((file) => file.endsWith(extension)).map((file) => join6(dir, file));
|
|
247
|
-
} catch {
|
|
248
|
-
return [];
|
|
499
|
+
if (rootRule) {
|
|
500
|
+
lines.push(rootRule.content.trim());
|
|
501
|
+
} else if (memoryRules.length === 0) {
|
|
502
|
+
lines.push("# Gemini CLI Configuration");
|
|
503
|
+
lines.push("");
|
|
504
|
+
lines.push("No configuration rules have been defined yet.");
|
|
249
505
|
}
|
|
506
|
+
return lines.join("\n");
|
|
250
507
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
508
|
+
function generateAiexclude(patterns) {
|
|
509
|
+
const lines = [
|
|
510
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
511
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
512
|
+
"",
|
|
513
|
+
...patterns
|
|
514
|
+
];
|
|
515
|
+
return lines.join("\n");
|
|
258
516
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
517
|
+
|
|
518
|
+
// src/generators/rules/roo.ts
|
|
519
|
+
import { join as join8 } from "path";
|
|
520
|
+
async function generateRooConfig(rules, config, baseDir) {
|
|
521
|
+
const outputs = [];
|
|
522
|
+
for (const rule of rules) {
|
|
523
|
+
const content = generateRooMarkdown(rule);
|
|
524
|
+
const outputDir = baseDir ? join8(baseDir, config.outputPaths.roo) : config.outputPaths.roo;
|
|
525
|
+
const filepath = join8(outputDir, `${rule.filename}.md`);
|
|
526
|
+
outputs.push({
|
|
527
|
+
tool: "roo",
|
|
528
|
+
filepath,
|
|
529
|
+
content
|
|
530
|
+
});
|
|
264
531
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
532
|
+
const ignorePatterns = await loadIgnorePatterns(baseDir);
|
|
533
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
534
|
+
const rooIgnorePath = baseDir ? join8(baseDir, ".rooignore") : ".rooignore";
|
|
535
|
+
const rooIgnoreContent = generateRooIgnore(ignorePatterns.patterns);
|
|
536
|
+
outputs.push({
|
|
537
|
+
tool: "roo",
|
|
538
|
+
filepath: rooIgnorePath,
|
|
539
|
+
content: rooIgnoreContent
|
|
540
|
+
});
|
|
271
541
|
}
|
|
542
|
+
return outputs;
|
|
272
543
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (await fileExists(filepath)) {
|
|
276
|
-
await rm(filepath);
|
|
277
|
-
}
|
|
278
|
-
} catch (error) {
|
|
279
|
-
console.warn(`Failed to remove file ${filepath}:`, error);
|
|
280
|
-
}
|
|
544
|
+
function generateRooMarkdown(rule) {
|
|
545
|
+
return rule.content.trim();
|
|
281
546
|
}
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
547
|
+
function generateRooIgnore(patterns) {
|
|
548
|
+
const lines = [
|
|
549
|
+
"# Generated by rulesync from .rulesyncignore",
|
|
550
|
+
"# This file is automatically generated. Do not edit manually.",
|
|
551
|
+
"",
|
|
552
|
+
...patterns
|
|
553
|
+
];
|
|
554
|
+
return lines.join("\n");
|
|
291
555
|
}
|
|
292
556
|
|
|
293
557
|
// src/core/generator.ts
|
|
@@ -331,6 +595,8 @@ async function generateForTool(tool, rules, config, baseDir) {
|
|
|
331
595
|
return await generateClaudecodeConfig(rules, config, baseDir);
|
|
332
596
|
case "roo":
|
|
333
597
|
return generateRooConfig(rules, config, baseDir);
|
|
598
|
+
case "geminicli":
|
|
599
|
+
return generateGeminiConfig(rules, config, baseDir);
|
|
334
600
|
default:
|
|
335
601
|
console.warn(`Unknown tool: ${tool}`);
|
|
336
602
|
return null;
|
|
@@ -341,9 +607,13 @@ async function generateForTool(tool, rules, config, baseDir) {
|
|
|
341
607
|
import { basename } from "path";
|
|
342
608
|
import matter from "gray-matter";
|
|
343
609
|
async function parseRulesFromDirectory(aiRulesDir) {
|
|
344
|
-
const
|
|
610
|
+
const ignorePatterns = await loadIgnorePatterns();
|
|
611
|
+
const ruleFiles = await findFiles(aiRulesDir, ".md", ignorePatterns.patterns);
|
|
345
612
|
const rules = [];
|
|
346
613
|
const errors = [];
|
|
614
|
+
if (ignorePatterns.patterns.length > 0) {
|
|
615
|
+
console.log(`Loaded ${ignorePatterns.patterns.length} ignore patterns from .rulesyncignore`);
|
|
616
|
+
}
|
|
347
617
|
for (const filepath of ruleFiles) {
|
|
348
618
|
try {
|
|
349
619
|
const rule = await parseRuleFile(filepath);
|
|
@@ -412,7 +682,7 @@ function validateFrontmatter(data, filepath) {
|
|
|
412
682
|
`Invalid "targets" field in ${filepath}: must be an array, got ${typeof obj.targets}`
|
|
413
683
|
);
|
|
414
684
|
}
|
|
415
|
-
const validTargets = ["copilot", "cursor", "cline", "claudecode", "roo", "*"];
|
|
685
|
+
const validTargets = ["copilot", "cursor", "cline", "claudecode", "roo", "geminicli", "*"];
|
|
416
686
|
for (const target of obj.targets) {
|
|
417
687
|
if (typeof target !== "string" || !validTargets.includes(target)) {
|
|
418
688
|
throw new Error(
|
|
@@ -499,6 +769,140 @@ async function validateRule(rule) {
|
|
|
499
769
|
};
|
|
500
770
|
}
|
|
501
771
|
|
|
772
|
+
// src/core/mcp-generator.ts
|
|
773
|
+
import os from "os";
|
|
774
|
+
import path3 from "path";
|
|
775
|
+
|
|
776
|
+
// src/core/mcp-parser.ts
|
|
777
|
+
import fs from "fs";
|
|
778
|
+
import path2 from "path";
|
|
779
|
+
function parseMcpConfig(projectRoot) {
|
|
780
|
+
const mcpPath = path2.join(projectRoot, ".rulesync", ".mcp.json");
|
|
781
|
+
if (!fs.existsSync(mcpPath)) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const content = fs.readFileSync(mcpPath, "utf-8");
|
|
786
|
+
const rawConfig = JSON.parse(content);
|
|
787
|
+
if (rawConfig.servers && !rawConfig.mcpServers) {
|
|
788
|
+
rawConfig.mcpServers = rawConfig.servers;
|
|
789
|
+
delete rawConfig.servers;
|
|
790
|
+
}
|
|
791
|
+
if (!rawConfig.mcpServers || typeof rawConfig.mcpServers !== "object") {
|
|
792
|
+
throw new Error("Invalid mcp.json: 'mcpServers' field must be an object");
|
|
793
|
+
}
|
|
794
|
+
if (rawConfig.tools) {
|
|
795
|
+
delete rawConfig.tools;
|
|
796
|
+
}
|
|
797
|
+
return { mcpServers: rawConfig.mcpServers };
|
|
798
|
+
} catch (error) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
`Failed to parse mcp.json: ${error instanceof Error ? error.message : String(error)}`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/core/mcp-generator.ts
|
|
806
|
+
async function generateMcpConfigs(projectRoot, baseDir) {
|
|
807
|
+
const results = [];
|
|
808
|
+
const targetRoot = baseDir || projectRoot;
|
|
809
|
+
const config = parseMcpConfig(projectRoot);
|
|
810
|
+
if (!config) {
|
|
811
|
+
return results;
|
|
812
|
+
}
|
|
813
|
+
const generators = [
|
|
814
|
+
{
|
|
815
|
+
tool: "claude-project",
|
|
816
|
+
path: path3.join(targetRoot, ".mcp.json"),
|
|
817
|
+
generate: () => generateClaudeMcp(config, "project")
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
tool: "copilot-editor",
|
|
821
|
+
path: path3.join(targetRoot, ".vscode", "mcp.json"),
|
|
822
|
+
generate: () => generateCopilotMcp(config, "editor")
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
tool: "cursor-project",
|
|
826
|
+
path: path3.join(targetRoot, ".cursor", "mcp.json"),
|
|
827
|
+
generate: () => generateCursorMcp(config, "project")
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
tool: "cline-project",
|
|
831
|
+
path: path3.join(targetRoot, ".cline", "mcp.json"),
|
|
832
|
+
generate: () => generateClineMcp(config, "project")
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
tool: "gemini-project",
|
|
836
|
+
path: path3.join(targetRoot, ".gemini", "settings.json"),
|
|
837
|
+
generate: () => generateGeminiCliMcp(config, "project")
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
tool: "roo-project",
|
|
841
|
+
path: path3.join(targetRoot, ".roo", "mcp.json"),
|
|
842
|
+
generate: () => generateRooMcp(config, "project")
|
|
843
|
+
}
|
|
844
|
+
];
|
|
845
|
+
if (!baseDir) {
|
|
846
|
+
generators.push(
|
|
847
|
+
{
|
|
848
|
+
tool: "claude-global",
|
|
849
|
+
path: path3.join(os.homedir(), ".claude", "settings.json"),
|
|
850
|
+
generate: () => generateClaudeMcp(config, "global")
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
tool: "cursor-global",
|
|
854
|
+
path: path3.join(os.homedir(), ".cursor", "mcp.json"),
|
|
855
|
+
generate: () => generateCursorMcp(config, "global")
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
tool: "gemini-global",
|
|
859
|
+
path: path3.join(os.homedir(), ".gemini", "settings.json"),
|
|
860
|
+
generate: () => generateGeminiCliMcp(config, "global")
|
|
861
|
+
}
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
for (const generator of generators) {
|
|
865
|
+
try {
|
|
866
|
+
const content = generator.generate();
|
|
867
|
+
const parsed = JSON.parse(content);
|
|
868
|
+
if (generator.tool.includes("claude") || generator.tool.includes("cline") || generator.tool.includes("cursor") || generator.tool.includes("gemini") || generator.tool.includes("roo")) {
|
|
869
|
+
if (!parsed.mcpServers || Object.keys(parsed.mcpServers).length === 0) {
|
|
870
|
+
results.push({
|
|
871
|
+
tool: generator.tool,
|
|
872
|
+
path: generator.path,
|
|
873
|
+
status: "skipped"
|
|
874
|
+
});
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
} else if (generator.tool.includes("copilot")) {
|
|
878
|
+
const key = generator.tool.includes("codingAgent") ? "mcpServers" : "servers";
|
|
879
|
+
if (!parsed[key] || Object.keys(parsed[key]).length === 0) {
|
|
880
|
+
results.push({
|
|
881
|
+
tool: generator.tool,
|
|
882
|
+
path: generator.path,
|
|
883
|
+
status: "skipped"
|
|
884
|
+
});
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
await writeFileContent(generator.path, content);
|
|
889
|
+
results.push({
|
|
890
|
+
tool: generator.tool,
|
|
891
|
+
path: generator.path,
|
|
892
|
+
status: "success"
|
|
893
|
+
});
|
|
894
|
+
} catch (error) {
|
|
895
|
+
results.push({
|
|
896
|
+
tool: generator.tool,
|
|
897
|
+
path: generator.path,
|
|
898
|
+
status: "error",
|
|
899
|
+
error: error instanceof Error ? error.message : String(error)
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return results;
|
|
904
|
+
}
|
|
905
|
+
|
|
502
906
|
// src/cli/commands/generate.ts
|
|
503
907
|
async function generateCommand(options = {}) {
|
|
504
908
|
const config = getDefaultConfig();
|
|
@@ -544,6 +948,9 @@ async function generateCommand(options = {}) {
|
|
|
544
948
|
case "roo":
|
|
545
949
|
deleteTasks.push(removeDirectory(config.outputPaths.roo));
|
|
546
950
|
break;
|
|
951
|
+
case "geminicli":
|
|
952
|
+
deleteTasks.push(removeDirectory(config.outputPaths.geminicli));
|
|
953
|
+
break;
|
|
547
954
|
}
|
|
548
955
|
}
|
|
549
956
|
await Promise.all(deleteTasks);
|
|
@@ -574,8 +981,32 @@ Generating configurations for base directory: ${baseDir}`);
|
|
|
574
981
|
console.warn("\u26A0\uFE0F No configurations generated");
|
|
575
982
|
return;
|
|
576
983
|
}
|
|
577
|
-
console.log(`
|
|
578
|
-
\u{1F389} Successfully generated ${totalOutputs} configuration file(s)!`);
|
|
984
|
+
console.log(`
|
|
985
|
+
\u{1F389} Successfully generated ${totalOutputs} configuration file(s)!`);
|
|
986
|
+
if (options.verbose) {
|
|
987
|
+
console.log("\nGenerating MCP configurations...");
|
|
988
|
+
}
|
|
989
|
+
for (const baseDir of baseDirs) {
|
|
990
|
+
const mcpResults = await generateMcpConfigs(
|
|
991
|
+
process.cwd(),
|
|
992
|
+
baseDir === process.cwd() ? void 0 : baseDir
|
|
993
|
+
);
|
|
994
|
+
if (mcpResults.length === 0) {
|
|
995
|
+
if (options.verbose) {
|
|
996
|
+
console.log(`No MCP configuration found for ${baseDir}`);
|
|
997
|
+
}
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
for (const result of mcpResults) {
|
|
1001
|
+
if (result.status === "success") {
|
|
1002
|
+
console.log(`\u2705 Generated ${result.tool} MCP configuration: ${result.path}`);
|
|
1003
|
+
} else if (result.status === "error") {
|
|
1004
|
+
console.error(`\u274C Failed to generate ${result.tool} MCP configuration: ${result.error}`);
|
|
1005
|
+
} else if (options.verbose && result.status === "skipped") {
|
|
1006
|
+
console.log(`\u23ED\uFE0F Skipped ${result.tool} MCP configuration (no servers configured)`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
579
1010
|
} catch (error) {
|
|
580
1011
|
console.error("\u274C Failed to generate configurations:", error);
|
|
581
1012
|
process.exit(1);
|
|
@@ -584,18 +1015,31 @@ Generating configurations for base directory: ${baseDir}`);
|
|
|
584
1015
|
|
|
585
1016
|
// src/cli/commands/gitignore.ts
|
|
586
1017
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
587
|
-
import { join as
|
|
1018
|
+
import { join as join9 } from "path";
|
|
588
1019
|
var gitignoreCommand = async () => {
|
|
589
|
-
const gitignorePath =
|
|
1020
|
+
const gitignorePath = join9(process.cwd(), ".gitignore");
|
|
590
1021
|
const rulesFilesToIgnore = [
|
|
591
1022
|
"# Generated by rulesync - AI tool configuration files",
|
|
592
1023
|
"**/.github/copilot-instructions.md",
|
|
593
1024
|
"**/.github/instructions/",
|
|
594
1025
|
"**/.cursor/rules/",
|
|
1026
|
+
"**/.cursorignore",
|
|
595
1027
|
"**/.clinerules/",
|
|
1028
|
+
"**/.clineignore",
|
|
596
1029
|
"**/CLAUDE.md",
|
|
597
1030
|
"**/.claude/memories/",
|
|
598
|
-
"**/.roo/rules/"
|
|
1031
|
+
"**/.roo/rules/",
|
|
1032
|
+
"**/.rooignore",
|
|
1033
|
+
"**/.copilotignore",
|
|
1034
|
+
"**/GEMINI.md",
|
|
1035
|
+
"**/.gemini/memories/",
|
|
1036
|
+
"**/.aiexclude",
|
|
1037
|
+
"**/.mcp.json",
|
|
1038
|
+
"**/.cursor/mcp.json",
|
|
1039
|
+
"**/.cline/mcp.json",
|
|
1040
|
+
"**/.vscode/mcp.json",
|
|
1041
|
+
"**/.gemini/settings.json",
|
|
1042
|
+
"**/.roo/mcp.json"
|
|
599
1043
|
];
|
|
600
1044
|
let gitignoreContent = "";
|
|
601
1045
|
if (existsSync(gitignorePath)) {
|
|
@@ -626,15 +1070,17 @@ ${linesToAdd.join("\n")}
|
|
|
626
1070
|
};
|
|
627
1071
|
|
|
628
1072
|
// src/core/importer.ts
|
|
629
|
-
import { join as
|
|
1073
|
+
import { join as join16 } from "path";
|
|
630
1074
|
import matter4 from "gray-matter";
|
|
631
1075
|
|
|
632
1076
|
// src/parsers/claudecode.ts
|
|
633
|
-
import { basename as basename2, join as
|
|
1077
|
+
import { basename as basename2, join as join10 } from "path";
|
|
634
1078
|
async function parseClaudeConfiguration(baseDir = process.cwd()) {
|
|
635
1079
|
const errors = [];
|
|
636
1080
|
const rules = [];
|
|
637
|
-
|
|
1081
|
+
let ignorePatterns;
|
|
1082
|
+
let mcpServers;
|
|
1083
|
+
const claudeFilePath = join10(baseDir, "CLAUDE.md");
|
|
638
1084
|
if (!await fileExists(claudeFilePath)) {
|
|
639
1085
|
errors.push("CLAUDE.md file not found");
|
|
640
1086
|
return { rules, errors };
|
|
@@ -645,16 +1091,32 @@ async function parseClaudeConfiguration(baseDir = process.cwd()) {
|
|
|
645
1091
|
if (mainRule) {
|
|
646
1092
|
rules.push(mainRule);
|
|
647
1093
|
}
|
|
648
|
-
const memoryDir =
|
|
1094
|
+
const memoryDir = join10(baseDir, ".claude", "memories");
|
|
649
1095
|
if (await fileExists(memoryDir)) {
|
|
650
1096
|
const memoryRules = await parseClaudeMemoryFiles(memoryDir);
|
|
651
1097
|
rules.push(...memoryRules);
|
|
652
1098
|
}
|
|
1099
|
+
const settingsPath = join10(baseDir, ".claude", "settings.json");
|
|
1100
|
+
if (await fileExists(settingsPath)) {
|
|
1101
|
+
const settingsResult = await parseClaudeSettings(settingsPath);
|
|
1102
|
+
if (settingsResult.ignorePatterns) {
|
|
1103
|
+
ignorePatterns = settingsResult.ignorePatterns;
|
|
1104
|
+
}
|
|
1105
|
+
if (settingsResult.mcpServers) {
|
|
1106
|
+
mcpServers = settingsResult.mcpServers;
|
|
1107
|
+
}
|
|
1108
|
+
errors.push(...settingsResult.errors);
|
|
1109
|
+
}
|
|
653
1110
|
} catch (error) {
|
|
654
1111
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
655
1112
|
errors.push(`Failed to parse Claude configuration: ${errorMessage}`);
|
|
656
1113
|
}
|
|
657
|
-
return {
|
|
1114
|
+
return {
|
|
1115
|
+
rules,
|
|
1116
|
+
errors,
|
|
1117
|
+
...ignorePatterns && { ignorePatterns },
|
|
1118
|
+
...mcpServers && { mcpServers }
|
|
1119
|
+
};
|
|
658
1120
|
}
|
|
659
1121
|
function parseClaudeMainFile(content, filepath) {
|
|
660
1122
|
const lines = content.split("\n");
|
|
@@ -691,7 +1153,7 @@ async function parseClaudeMemoryFiles(memoryDir) {
|
|
|
691
1153
|
const files = await readdir2(memoryDir);
|
|
692
1154
|
for (const file of files) {
|
|
693
1155
|
if (file.endsWith(".md")) {
|
|
694
|
-
const filePath =
|
|
1156
|
+
const filePath = join10(memoryDir, file);
|
|
695
1157
|
const content = await readFileContent(filePath);
|
|
696
1158
|
if (content.trim()) {
|
|
697
1159
|
const filename = basename2(file, ".md");
|
|
@@ -714,47 +1176,113 @@ async function parseClaudeMemoryFiles(memoryDir) {
|
|
|
714
1176
|
}
|
|
715
1177
|
return rules;
|
|
716
1178
|
}
|
|
1179
|
+
async function parseClaudeSettings(settingsPath) {
|
|
1180
|
+
const errors = [];
|
|
1181
|
+
let ignorePatterns;
|
|
1182
|
+
let mcpServers;
|
|
1183
|
+
try {
|
|
1184
|
+
const content = await readFileContent(settingsPath);
|
|
1185
|
+
const settings = JSON.parse(content);
|
|
1186
|
+
if (settings.permissions?.deny) {
|
|
1187
|
+
const readPatterns = settings.permissions.deny.filter((rule) => rule.startsWith("Read(") && rule.endsWith(")")).map((rule) => {
|
|
1188
|
+
const match = rule.match(/^Read\((.+)\)$/);
|
|
1189
|
+
return match ? match[1] : null;
|
|
1190
|
+
}).filter((pattern) => pattern !== null);
|
|
1191
|
+
if (readPatterns.length > 0) {
|
|
1192
|
+
ignorePatterns = readPatterns;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
|
|
1196
|
+
mcpServers = settings.mcpServers;
|
|
1197
|
+
}
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1200
|
+
errors.push(`Failed to parse settings.json: ${errorMessage}`);
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
errors,
|
|
1204
|
+
...ignorePatterns && { ignorePatterns },
|
|
1205
|
+
...mcpServers && { mcpServers }
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
717
1208
|
|
|
718
1209
|
// src/parsers/cline.ts
|
|
719
|
-
import { join as
|
|
1210
|
+
import { join as join11 } from "path";
|
|
720
1211
|
async function parseClineConfiguration(baseDir = process.cwd()) {
|
|
721
1212
|
const errors = [];
|
|
722
1213
|
const rules = [];
|
|
723
|
-
const clineFilePath =
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
|
|
1214
|
+
const clineFilePath = join11(baseDir, ".cline", "instructions.md");
|
|
1215
|
+
if (await fileExists(clineFilePath)) {
|
|
1216
|
+
try {
|
|
1217
|
+
const content = await readFileContent(clineFilePath);
|
|
1218
|
+
if (content.trim()) {
|
|
1219
|
+
const frontmatter = {
|
|
1220
|
+
root: false,
|
|
1221
|
+
targets: ["cline"],
|
|
1222
|
+
description: "Cline instructions",
|
|
1223
|
+
globs: ["**/*"]
|
|
1224
|
+
};
|
|
1225
|
+
rules.push({
|
|
1226
|
+
frontmatter,
|
|
1227
|
+
content: content.trim(),
|
|
1228
|
+
filename: "cline-instructions",
|
|
1229
|
+
filepath: clineFilePath
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1234
|
+
errors.push(`Failed to parse .cline/instructions.md: ${errorMessage}`);
|
|
1235
|
+
}
|
|
727
1236
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1237
|
+
const clinerulesDirPath = join11(baseDir, ".clinerules");
|
|
1238
|
+
if (await fileExists(clinerulesDirPath)) {
|
|
1239
|
+
try {
|
|
1240
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1241
|
+
const files = await readdir2(clinerulesDirPath);
|
|
1242
|
+
for (const file of files) {
|
|
1243
|
+
if (file.endsWith(".md")) {
|
|
1244
|
+
const filePath = join11(clinerulesDirPath, file);
|
|
1245
|
+
try {
|
|
1246
|
+
const content = await readFileContent(filePath);
|
|
1247
|
+
if (content.trim()) {
|
|
1248
|
+
const filename = file.replace(".md", "");
|
|
1249
|
+
const frontmatter = {
|
|
1250
|
+
root: false,
|
|
1251
|
+
targets: ["cline"],
|
|
1252
|
+
description: `Cline rule: ${filename}`,
|
|
1253
|
+
globs: ["**/*"]
|
|
1254
|
+
};
|
|
1255
|
+
rules.push({
|
|
1256
|
+
frontmatter,
|
|
1257
|
+
content: content.trim(),
|
|
1258
|
+
filename: `cline-${filename}`,
|
|
1259
|
+
filepath: filePath
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1264
|
+
errors.push(`Failed to parse ${filePath}: ${errorMessage}`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1270
|
+
errors.push(`Failed to parse .clinerules files: ${errorMessage}`);
|
|
743
1271
|
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
errors.push(
|
|
1272
|
+
}
|
|
1273
|
+
if (rules.length === 0) {
|
|
1274
|
+
errors.push("No Cline configuration files found (.cline/instructions.md or .clinerules/*.md)");
|
|
747
1275
|
}
|
|
748
1276
|
return { rules, errors };
|
|
749
1277
|
}
|
|
750
1278
|
|
|
751
1279
|
// src/parsers/copilot.ts
|
|
752
|
-
import { basename as basename3, join as
|
|
1280
|
+
import { basename as basename3, join as join12 } from "path";
|
|
753
1281
|
import matter2 from "gray-matter";
|
|
754
1282
|
async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
755
1283
|
const errors = [];
|
|
756
1284
|
const rules = [];
|
|
757
|
-
const copilotFilePath =
|
|
1285
|
+
const copilotFilePath = join12(baseDir, ".github", "copilot-instructions.md");
|
|
758
1286
|
if (await fileExists(copilotFilePath)) {
|
|
759
1287
|
try {
|
|
760
1288
|
const rawContent = await readFileContent(copilotFilePath);
|
|
@@ -779,14 +1307,14 @@ async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
|
779
1307
|
errors.push(`Failed to parse copilot-instructions.md: ${errorMessage}`);
|
|
780
1308
|
}
|
|
781
1309
|
}
|
|
782
|
-
const instructionsDir =
|
|
1310
|
+
const instructionsDir = join12(baseDir, ".github", "instructions");
|
|
783
1311
|
if (await fileExists(instructionsDir)) {
|
|
784
1312
|
try {
|
|
785
1313
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
786
1314
|
const files = await readdir2(instructionsDir);
|
|
787
1315
|
for (const file of files) {
|
|
788
1316
|
if (file.endsWith(".instructions.md")) {
|
|
789
|
-
const filePath =
|
|
1317
|
+
const filePath = join12(instructionsDir, file);
|
|
790
1318
|
const rawContent = await readFileContent(filePath);
|
|
791
1319
|
const parsed = matter2(rawContent);
|
|
792
1320
|
const content = parsed.content.trim();
|
|
@@ -821,7 +1349,7 @@ async function parseCopilotConfiguration(baseDir = process.cwd()) {
|
|
|
821
1349
|
}
|
|
822
1350
|
|
|
823
1351
|
// src/parsers/cursor.ts
|
|
824
|
-
import { basename as basename4, join as
|
|
1352
|
+
import { basename as basename4, join as join13 } from "path";
|
|
825
1353
|
import matter3 from "gray-matter";
|
|
826
1354
|
import yaml from "js-yaml";
|
|
827
1355
|
var customMatterOptions = {
|
|
@@ -845,7 +1373,9 @@ var customMatterOptions = {
|
|
|
845
1373
|
async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
846
1374
|
const errors = [];
|
|
847
1375
|
const rules = [];
|
|
848
|
-
|
|
1376
|
+
let ignorePatterns;
|
|
1377
|
+
let mcpServers;
|
|
1378
|
+
const cursorFilePath = join13(baseDir, ".cursorrules");
|
|
849
1379
|
if (await fileExists(cursorFilePath)) {
|
|
850
1380
|
try {
|
|
851
1381
|
const rawContent = await readFileContent(cursorFilePath);
|
|
@@ -870,14 +1400,14 @@ async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
|
870
1400
|
errors.push(`Failed to parse .cursorrules file: ${errorMessage}`);
|
|
871
1401
|
}
|
|
872
1402
|
}
|
|
873
|
-
const cursorRulesDir =
|
|
1403
|
+
const cursorRulesDir = join13(baseDir, ".cursor", "rules");
|
|
874
1404
|
if (await fileExists(cursorRulesDir)) {
|
|
875
1405
|
try {
|
|
876
1406
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
877
1407
|
const files = await readdir2(cursorRulesDir);
|
|
878
1408
|
for (const file of files) {
|
|
879
1409
|
if (file.endsWith(".mdc")) {
|
|
880
|
-
const filePath =
|
|
1410
|
+
const filePath = join13(cursorRulesDir, file);
|
|
881
1411
|
try {
|
|
882
1412
|
const rawContent = await readFileContent(filePath);
|
|
883
1413
|
const parsed = matter3(rawContent, customMatterOptions);
|
|
@@ -911,38 +1441,244 @@ async function parseCursorConfiguration(baseDir = process.cwd()) {
|
|
|
911
1441
|
if (rules.length === 0) {
|
|
912
1442
|
errors.push("No Cursor configuration files found (.cursorrules or .cursor/rules/*.mdc)");
|
|
913
1443
|
}
|
|
914
|
-
|
|
1444
|
+
const cursorIgnorePath = join13(baseDir, ".cursorignore");
|
|
1445
|
+
if (await fileExists(cursorIgnorePath)) {
|
|
1446
|
+
try {
|
|
1447
|
+
const content = await readFileContent(cursorIgnorePath);
|
|
1448
|
+
const patterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1449
|
+
if (patterns.length > 0) {
|
|
1450
|
+
ignorePatterns = patterns;
|
|
1451
|
+
}
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1454
|
+
errors.push(`Failed to parse .cursorignore: ${errorMessage}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const cursorMcpPath = join13(baseDir, ".cursor", "mcp.json");
|
|
1458
|
+
if (await fileExists(cursorMcpPath)) {
|
|
1459
|
+
try {
|
|
1460
|
+
const content = await readFileContent(cursorMcpPath);
|
|
1461
|
+
const mcp = JSON.parse(content);
|
|
1462
|
+
if (mcp.mcpServers && Object.keys(mcp.mcpServers).length > 0) {
|
|
1463
|
+
mcpServers = mcp.mcpServers;
|
|
1464
|
+
}
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1467
|
+
errors.push(`Failed to parse .cursor/mcp.json: ${errorMessage}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
rules,
|
|
1472
|
+
errors,
|
|
1473
|
+
...ignorePatterns && { ignorePatterns },
|
|
1474
|
+
...mcpServers && { mcpServers }
|
|
1475
|
+
};
|
|
915
1476
|
}
|
|
916
1477
|
|
|
917
|
-
// src/parsers/
|
|
918
|
-
import { join as
|
|
919
|
-
async function
|
|
1478
|
+
// src/parsers/geminicli.ts
|
|
1479
|
+
import { basename as basename5, join as join14 } from "path";
|
|
1480
|
+
async function parseGeminiConfiguration(baseDir = process.cwd()) {
|
|
920
1481
|
const errors = [];
|
|
921
1482
|
const rules = [];
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1483
|
+
let ignorePatterns;
|
|
1484
|
+
let mcpServers;
|
|
1485
|
+
const geminiFilePath = join14(baseDir, "GEMINI.md");
|
|
1486
|
+
if (!await fileExists(geminiFilePath)) {
|
|
1487
|
+
errors.push("GEMINI.md file not found");
|
|
925
1488
|
return { rules, errors };
|
|
926
1489
|
}
|
|
927
1490
|
try {
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
rules.push(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1491
|
+
const geminiContent = await readFileContent(geminiFilePath);
|
|
1492
|
+
const mainRule = parseGeminiMainFile(geminiContent, geminiFilePath);
|
|
1493
|
+
if (mainRule) {
|
|
1494
|
+
rules.push(mainRule);
|
|
1495
|
+
}
|
|
1496
|
+
const memoryDir = join14(baseDir, ".gemini", "memories");
|
|
1497
|
+
if (await fileExists(memoryDir)) {
|
|
1498
|
+
const memoryRules = await parseGeminiMemoryFiles(memoryDir);
|
|
1499
|
+
rules.push(...memoryRules);
|
|
1500
|
+
}
|
|
1501
|
+
const settingsPath = join14(baseDir, ".gemini", "settings.json");
|
|
1502
|
+
if (await fileExists(settingsPath)) {
|
|
1503
|
+
const settingsResult = await parseGeminiSettings(settingsPath);
|
|
1504
|
+
if (settingsResult.ignorePatterns) {
|
|
1505
|
+
ignorePatterns = settingsResult.ignorePatterns;
|
|
1506
|
+
}
|
|
1507
|
+
if (settingsResult.mcpServers) {
|
|
1508
|
+
mcpServers = settingsResult.mcpServers;
|
|
1509
|
+
}
|
|
1510
|
+
errors.push(...settingsResult.errors);
|
|
1511
|
+
}
|
|
1512
|
+
const aiexcludePath = join14(baseDir, ".aiexclude");
|
|
1513
|
+
if (await fileExists(aiexcludePath)) {
|
|
1514
|
+
const aiexcludePatterns = await parseAiexclude(aiexcludePath);
|
|
1515
|
+
if (aiexcludePatterns.length > 0) {
|
|
1516
|
+
ignorePatterns = ignorePatterns ? [...ignorePatterns, ...aiexcludePatterns] : aiexcludePatterns;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1521
|
+
errors.push(`Failed to parse Gemini configuration: ${errorMessage}`);
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
rules,
|
|
1525
|
+
errors,
|
|
1526
|
+
...ignorePatterns && { ignorePatterns },
|
|
1527
|
+
...mcpServers && { mcpServers }
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function parseGeminiMainFile(content, filepath) {
|
|
1531
|
+
const lines = content.split("\n");
|
|
1532
|
+
let contentStartIndex = 0;
|
|
1533
|
+
if (lines.some((line) => line.includes("| Document | Description | File Patterns |"))) {
|
|
1534
|
+
const tableEndIndex = lines.findIndex(
|
|
1535
|
+
(line, index) => index > 0 && line.trim() === "" && lines[index - 1]?.includes("|") && !lines[index + 1]?.includes("|")
|
|
1536
|
+
);
|
|
1537
|
+
if (tableEndIndex !== -1) {
|
|
1538
|
+
contentStartIndex = tableEndIndex + 1;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const mainContent = lines.slice(contentStartIndex).join("\n").trim();
|
|
1542
|
+
if (!mainContent) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
const frontmatter = {
|
|
1546
|
+
root: false,
|
|
1547
|
+
targets: ["geminicli"],
|
|
1548
|
+
description: "Main Gemini CLI configuration",
|
|
1549
|
+
globs: ["**/*"]
|
|
1550
|
+
};
|
|
1551
|
+
return {
|
|
1552
|
+
frontmatter,
|
|
1553
|
+
content: mainContent,
|
|
1554
|
+
filename: "gemini-main",
|
|
1555
|
+
filepath
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
async function parseGeminiMemoryFiles(memoryDir) {
|
|
1559
|
+
const rules = [];
|
|
1560
|
+
try {
|
|
1561
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1562
|
+
const files = await readdir2(memoryDir);
|
|
1563
|
+
for (const file of files) {
|
|
1564
|
+
if (file.endsWith(".md")) {
|
|
1565
|
+
const filePath = join14(memoryDir, file);
|
|
1566
|
+
const content = await readFileContent(filePath);
|
|
1567
|
+
if (content.trim()) {
|
|
1568
|
+
const filename = basename5(file, ".md");
|
|
1569
|
+
const frontmatter = {
|
|
1570
|
+
root: false,
|
|
1571
|
+
targets: ["geminicli"],
|
|
1572
|
+
description: `Memory file: ${filename}`,
|
|
1573
|
+
globs: ["**/*"]
|
|
1574
|
+
};
|
|
1575
|
+
rules.push({
|
|
1576
|
+
frontmatter,
|
|
1577
|
+
content: content.trim(),
|
|
1578
|
+
filename: `gemini-memory-${filename}`,
|
|
1579
|
+
filepath: filePath
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
} catch (_error) {
|
|
1585
|
+
}
|
|
1586
|
+
return rules;
|
|
1587
|
+
}
|
|
1588
|
+
async function parseGeminiSettings(settingsPath) {
|
|
1589
|
+
const errors = [];
|
|
1590
|
+
let mcpServers;
|
|
1591
|
+
try {
|
|
1592
|
+
const content = await readFileContent(settingsPath);
|
|
1593
|
+
const settings = JSON.parse(content);
|
|
1594
|
+
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
|
|
1595
|
+
mcpServers = settings.mcpServers;
|
|
942
1596
|
}
|
|
943
1597
|
} catch (error) {
|
|
944
1598
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
945
|
-
errors.push(`Failed to parse
|
|
1599
|
+
errors.push(`Failed to parse settings.json: ${errorMessage}`);
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
errors,
|
|
1603
|
+
...mcpServers && { mcpServers }
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
async function parseAiexclude(aiexcludePath) {
|
|
1607
|
+
try {
|
|
1608
|
+
const content = await readFileContent(aiexcludePath);
|
|
1609
|
+
const patterns = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1610
|
+
return patterns;
|
|
1611
|
+
} catch (_error) {
|
|
1612
|
+
return [];
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/parsers/roo.ts
|
|
1617
|
+
import { join as join15 } from "path";
|
|
1618
|
+
async function parseRooConfiguration(baseDir = process.cwd()) {
|
|
1619
|
+
const errors = [];
|
|
1620
|
+
const rules = [];
|
|
1621
|
+
const rooFilePath = join15(baseDir, ".roo", "instructions.md");
|
|
1622
|
+
if (await fileExists(rooFilePath)) {
|
|
1623
|
+
try {
|
|
1624
|
+
const content = await readFileContent(rooFilePath);
|
|
1625
|
+
if (content.trim()) {
|
|
1626
|
+
const frontmatter = {
|
|
1627
|
+
root: false,
|
|
1628
|
+
targets: ["roo"],
|
|
1629
|
+
description: "Roo Code instructions",
|
|
1630
|
+
globs: ["**/*"]
|
|
1631
|
+
};
|
|
1632
|
+
rules.push({
|
|
1633
|
+
frontmatter,
|
|
1634
|
+
content: content.trim(),
|
|
1635
|
+
filename: "roo-instructions",
|
|
1636
|
+
filepath: rooFilePath
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1641
|
+
errors.push(`Failed to parse .roo/instructions.md: ${errorMessage}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const rooRulesDir = join15(baseDir, ".roo", "rules");
|
|
1645
|
+
if (await fileExists(rooRulesDir)) {
|
|
1646
|
+
try {
|
|
1647
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1648
|
+
const files = await readdir2(rooRulesDir);
|
|
1649
|
+
for (const file of files) {
|
|
1650
|
+
if (file.endsWith(".md")) {
|
|
1651
|
+
const filePath = join15(rooRulesDir, file);
|
|
1652
|
+
try {
|
|
1653
|
+
const content = await readFileContent(filePath);
|
|
1654
|
+
if (content.trim()) {
|
|
1655
|
+
const filename = file.replace(".md", "");
|
|
1656
|
+
const frontmatter = {
|
|
1657
|
+
root: false,
|
|
1658
|
+
targets: ["roo"],
|
|
1659
|
+
description: `Roo rule: ${filename}`,
|
|
1660
|
+
globs: ["**/*"]
|
|
1661
|
+
};
|
|
1662
|
+
rules.push({
|
|
1663
|
+
frontmatter,
|
|
1664
|
+
content: content.trim(),
|
|
1665
|
+
filename: `roo-${filename}`,
|
|
1666
|
+
filepath: filePath
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1671
|
+
errors.push(`Failed to parse ${filePath}: ${errorMessage}`);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1677
|
+
errors.push(`Failed to parse .roo/rules files: ${errorMessage}`);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (rules.length === 0) {
|
|
1681
|
+
errors.push("No Roo Code configuration files found (.roo/instructions.md or .roo/rules/*.md)");
|
|
946
1682
|
}
|
|
947
1683
|
return { rules, errors };
|
|
948
1684
|
}
|
|
@@ -952,6 +1688,8 @@ async function importConfiguration(options) {
|
|
|
952
1688
|
const { tool, baseDir = process.cwd(), rulesDir = ".rulesync", verbose = false } = options;
|
|
953
1689
|
const errors = [];
|
|
954
1690
|
let rules = [];
|
|
1691
|
+
let ignorePatterns;
|
|
1692
|
+
let mcpServers;
|
|
955
1693
|
if (verbose) {
|
|
956
1694
|
console.log(`Importing ${tool} configuration from ${baseDir}...`);
|
|
957
1695
|
}
|
|
@@ -961,12 +1699,16 @@ async function importConfiguration(options) {
|
|
|
961
1699
|
const claudeResult = await parseClaudeConfiguration(baseDir);
|
|
962
1700
|
rules = claudeResult.rules;
|
|
963
1701
|
errors.push(...claudeResult.errors);
|
|
1702
|
+
ignorePatterns = claudeResult.ignorePatterns;
|
|
1703
|
+
mcpServers = claudeResult.mcpServers;
|
|
964
1704
|
break;
|
|
965
1705
|
}
|
|
966
1706
|
case "cursor": {
|
|
967
1707
|
const cursorResult = await parseCursorConfiguration(baseDir);
|
|
968
1708
|
rules = cursorResult.rules;
|
|
969
1709
|
errors.push(...cursorResult.errors);
|
|
1710
|
+
ignorePatterns = cursorResult.ignorePatterns;
|
|
1711
|
+
mcpServers = cursorResult.mcpServers;
|
|
970
1712
|
break;
|
|
971
1713
|
}
|
|
972
1714
|
case "copilot": {
|
|
@@ -987,6 +1729,14 @@ async function importConfiguration(options) {
|
|
|
987
1729
|
errors.push(...rooResult.errors);
|
|
988
1730
|
break;
|
|
989
1731
|
}
|
|
1732
|
+
case "geminicli": {
|
|
1733
|
+
const geminiResult = await parseGeminiConfiguration(baseDir);
|
|
1734
|
+
rules = geminiResult.rules;
|
|
1735
|
+
errors.push(...geminiResult.errors);
|
|
1736
|
+
ignorePatterns = geminiResult.ignorePatterns;
|
|
1737
|
+
mcpServers = geminiResult.mcpServers;
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
990
1740
|
default:
|
|
991
1741
|
errors.push(`Unsupported tool: ${tool}`);
|
|
992
1742
|
return { success: false, rulesCreated: 0, errors };
|
|
@@ -996,10 +1746,10 @@ async function importConfiguration(options) {
|
|
|
996
1746
|
errors.push(`Failed to parse ${tool} configuration: ${errorMessage}`);
|
|
997
1747
|
return { success: false, rulesCreated: 0, errors };
|
|
998
1748
|
}
|
|
999
|
-
if (rules.length === 0) {
|
|
1749
|
+
if (rules.length === 0 && !ignorePatterns && !mcpServers) {
|
|
1000
1750
|
return { success: false, rulesCreated: 0, errors };
|
|
1001
1751
|
}
|
|
1002
|
-
const rulesDirPath =
|
|
1752
|
+
const rulesDirPath = join16(baseDir, rulesDir);
|
|
1003
1753
|
try {
|
|
1004
1754
|
const { mkdir: mkdir3 } = await import("fs/promises");
|
|
1005
1755
|
await mkdir3(rulesDirPath, { recursive: true });
|
|
@@ -1013,7 +1763,7 @@ async function importConfiguration(options) {
|
|
|
1013
1763
|
try {
|
|
1014
1764
|
const baseFilename = `${tool}__${rule.filename}`;
|
|
1015
1765
|
const filename = await generateUniqueFilename(rulesDirPath, baseFilename);
|
|
1016
|
-
const filePath =
|
|
1766
|
+
const filePath = join16(rulesDirPath, `${filename}.md`);
|
|
1017
1767
|
const content = generateRuleFileContent(rule);
|
|
1018
1768
|
await writeFileContent(filePath, content);
|
|
1019
1769
|
rulesCreated++;
|
|
@@ -1025,10 +1775,44 @@ async function importConfiguration(options) {
|
|
|
1025
1775
|
errors.push(`Failed to create rule file for ${rule.filename}: ${errorMessage}`);
|
|
1026
1776
|
}
|
|
1027
1777
|
}
|
|
1778
|
+
let ignoreFileCreated = false;
|
|
1779
|
+
if (ignorePatterns && ignorePatterns.length > 0) {
|
|
1780
|
+
try {
|
|
1781
|
+
const rulesyncignorePath = join16(baseDir, ".rulesyncignore");
|
|
1782
|
+
const ignoreContent = `${ignorePatterns.join("\n")}
|
|
1783
|
+
`;
|
|
1784
|
+
await writeFileContent(rulesyncignorePath, ignoreContent);
|
|
1785
|
+
ignoreFileCreated = true;
|
|
1786
|
+
if (verbose) {
|
|
1787
|
+
console.log(`\u2705 Created .rulesyncignore with ${ignorePatterns.length} patterns`);
|
|
1788
|
+
}
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1791
|
+
errors.push(`Failed to create .rulesyncignore: ${errorMessage}`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
let mcpFileCreated = false;
|
|
1795
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
1796
|
+
try {
|
|
1797
|
+
const mcpPath = join16(baseDir, rulesDir, ".mcp.json");
|
|
1798
|
+
const mcpContent = `${JSON.stringify({ mcpServers }, null, 2)}
|
|
1799
|
+
`;
|
|
1800
|
+
await writeFileContent(mcpPath, mcpContent);
|
|
1801
|
+
mcpFileCreated = true;
|
|
1802
|
+
if (verbose) {
|
|
1803
|
+
console.log(`\u2705 Created .mcp.json with ${Object.keys(mcpServers).length} servers`);
|
|
1804
|
+
}
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1807
|
+
errors.push(`Failed to create .mcp.json: ${errorMessage}`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1028
1810
|
return {
|
|
1029
|
-
success: rulesCreated > 0,
|
|
1811
|
+
success: rulesCreated > 0 || ignoreFileCreated || mcpFileCreated,
|
|
1030
1812
|
rulesCreated,
|
|
1031
|
-
errors
|
|
1813
|
+
errors,
|
|
1814
|
+
ignoreFileCreated,
|
|
1815
|
+
mcpFileCreated
|
|
1032
1816
|
};
|
|
1033
1817
|
}
|
|
1034
1818
|
function generateRuleFileContent(rule) {
|
|
@@ -1038,7 +1822,7 @@ function generateRuleFileContent(rule) {
|
|
|
1038
1822
|
async function generateUniqueFilename(rulesDir, baseFilename) {
|
|
1039
1823
|
let filename = baseFilename;
|
|
1040
1824
|
let counter = 1;
|
|
1041
|
-
while (await fileExists(
|
|
1825
|
+
while (await fileExists(join16(rulesDir, `${filename}.md`))) {
|
|
1042
1826
|
filename = `${baseFilename}-${counter}`;
|
|
1043
1827
|
counter++;
|
|
1044
1828
|
}
|
|
@@ -1053,59 +1837,57 @@ async function importCommand(options = {}) {
|
|
|
1053
1837
|
if (options.copilot) tools.push("copilot");
|
|
1054
1838
|
if (options.cline) tools.push("cline");
|
|
1055
1839
|
if (options.roo) tools.push("roo");
|
|
1840
|
+
if (options.geminicli) tools.push("geminicli");
|
|
1056
1841
|
if (tools.length === 0) {
|
|
1057
1842
|
console.error(
|
|
1058
|
-
"\u274C Please specify
|
|
1843
|
+
"\u274C Please specify one tool to import from (--claudecode, --cursor, --copilot, --cline, --roo, --geminicli)"
|
|
1059
1844
|
);
|
|
1060
1845
|
process.exit(1);
|
|
1061
1846
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
for (const tool of tools) {
|
|
1066
|
-
if (options.verbose) {
|
|
1067
|
-
console.log(`
|
|
1068
|
-
Importing from ${tool}...`);
|
|
1069
|
-
}
|
|
1070
|
-
try {
|
|
1071
|
-
const result = await importConfiguration({
|
|
1072
|
-
tool,
|
|
1073
|
-
verbose: options.verbose ?? false
|
|
1074
|
-
});
|
|
1075
|
-
if (result.success) {
|
|
1076
|
-
console.log(`\u2705 Imported ${result.rulesCreated} rule(s) from ${tool}`);
|
|
1077
|
-
totalRulesCreated += result.rulesCreated;
|
|
1078
|
-
} else if (result.errors.length > 0) {
|
|
1079
|
-
console.warn(`\u26A0\uFE0F Failed to import from ${tool}: ${result.errors[0]}`);
|
|
1080
|
-
if (options.verbose) {
|
|
1081
|
-
allErrors.push(...result.errors);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1086
|
-
console.error(`\u274C Error importing from ${tool}: ${errorMessage}`);
|
|
1087
|
-
allErrors.push(`${tool}: ${errorMessage}`);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
if (totalRulesCreated > 0) {
|
|
1091
|
-
console.log(`
|
|
1092
|
-
\u{1F389} Successfully imported ${totalRulesCreated} rule(s) total!`);
|
|
1093
|
-
console.log("You can now run 'rulesync generate' to create tool-specific configurations.");
|
|
1094
|
-
} else {
|
|
1095
|
-
console.warn(
|
|
1096
|
-
"\n\u26A0\uFE0F No rules were imported. Please check that configuration files exist for the selected tools."
|
|
1847
|
+
if (tools.length > 1) {
|
|
1848
|
+
console.error(
|
|
1849
|
+
"\u274C Only one tool can be specified at a time. Please run the import command separately for each tool."
|
|
1097
1850
|
);
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
const tool = tools[0];
|
|
1854
|
+
if (!tool) {
|
|
1855
|
+
console.error("Error: No tool specified");
|
|
1856
|
+
process.exit(1);
|
|
1098
1857
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1858
|
+
console.log(`Importing configuration files from ${tool}...`);
|
|
1859
|
+
try {
|
|
1860
|
+
const result = await importConfiguration({
|
|
1861
|
+
tool,
|
|
1862
|
+
verbose: options.verbose ?? false
|
|
1863
|
+
});
|
|
1864
|
+
if (result.success) {
|
|
1865
|
+
console.log(`\u2705 Imported ${result.rulesCreated} rule(s) from ${tool}`);
|
|
1866
|
+
if (result.ignoreFileCreated) {
|
|
1867
|
+
console.log("\u2705 Created .rulesyncignore file from ignore patterns");
|
|
1868
|
+
}
|
|
1869
|
+
if (result.mcpFileCreated) {
|
|
1870
|
+
console.log("\u2705 Created .rulesync/.mcp.json file from MCP configuration");
|
|
1871
|
+
}
|
|
1872
|
+
console.log("You can now run 'rulesync generate' to create tool-specific configurations.");
|
|
1873
|
+
} else if (result.errors.length > 0) {
|
|
1874
|
+
console.warn(`\u26A0\uFE0F Failed to import from ${tool}: ${result.errors[0]}`);
|
|
1875
|
+
if (options.verbose && result.errors.length > 1) {
|
|
1876
|
+
console.log("\nDetailed errors:");
|
|
1877
|
+
for (const error of result.errors) {
|
|
1878
|
+
console.log(` - ${error}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1103
1881
|
}
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1884
|
+
console.error(`\u274C Error importing from ${tool}: ${errorMessage}`);
|
|
1885
|
+
process.exit(1);
|
|
1104
1886
|
}
|
|
1105
1887
|
}
|
|
1106
1888
|
|
|
1107
1889
|
// src/cli/commands/init.ts
|
|
1108
|
-
import { join as
|
|
1890
|
+
import { join as join17 } from "path";
|
|
1109
1891
|
async function initCommand() {
|
|
1110
1892
|
const aiRulesDir = ".rulesync";
|
|
1111
1893
|
console.log("Initializing rulesync...");
|
|
@@ -1235,7 +2017,7 @@ globs: ["src/api/**/*.ts", "src/services/**/*.ts", "src/models/**/*.ts"]
|
|
|
1235
2017
|
}
|
|
1236
2018
|
];
|
|
1237
2019
|
for (const file of sampleFiles) {
|
|
1238
|
-
const filepath =
|
|
2020
|
+
const filepath = join17(aiRulesDir, file.filename);
|
|
1239
2021
|
if (!await fileExists(filepath)) {
|
|
1240
2022
|
await writeFileContent(filepath, file.content);
|
|
1241
2023
|
console.log(`Created ${filepath}`);
|
|
@@ -1348,11 +2130,11 @@ async function watchCommand() {
|
|
|
1348
2130
|
persistent: true
|
|
1349
2131
|
});
|
|
1350
2132
|
let isGenerating = false;
|
|
1351
|
-
const handleChange = async (
|
|
2133
|
+
const handleChange = async (path4) => {
|
|
1352
2134
|
if (isGenerating) return;
|
|
1353
2135
|
isGenerating = true;
|
|
1354
2136
|
console.log(`
|
|
1355
|
-
\u{1F4DD} Detected change in ${
|
|
2137
|
+
\u{1F4DD} Detected change in ${path4}`);
|
|
1356
2138
|
try {
|
|
1357
2139
|
await generateCommand({ verbose: false });
|
|
1358
2140
|
console.log("\u2705 Regenerated configuration files");
|
|
@@ -1362,10 +2144,10 @@ async function watchCommand() {
|
|
|
1362
2144
|
isGenerating = false;
|
|
1363
2145
|
}
|
|
1364
2146
|
};
|
|
1365
|
-
watcher.on("change", handleChange).on("add", handleChange).on("unlink", (
|
|
2147
|
+
watcher.on("change", handleChange).on("add", handleChange).on("unlink", (path4) => {
|
|
1366
2148
|
console.log(`
|
|
1367
|
-
\u{1F5D1}\uFE0F Removed ${
|
|
1368
|
-
handleChange(
|
|
2149
|
+
\u{1F5D1}\uFE0F Removed ${path4}`);
|
|
2150
|
+
handleChange(path4);
|
|
1369
2151
|
}).on("error", (error) => {
|
|
1370
2152
|
console.error("\u274C Watcher error:", error);
|
|
1371
2153
|
});
|
|
@@ -1378,12 +2160,12 @@ async function watchCommand() {
|
|
|
1378
2160
|
|
|
1379
2161
|
// src/cli/index.ts
|
|
1380
2162
|
var program = new Command();
|
|
1381
|
-
program.name("rulesync").description("Unified AI rules management CLI tool").version("0.
|
|
2163
|
+
program.name("rulesync").description("Unified AI rules management CLI tool").version("0.36.0");
|
|
1382
2164
|
program.command("init").description("Initialize rulesync in current directory").action(initCommand);
|
|
1383
2165
|
program.command("add <filename>").description("Add a new rule file").action(addCommand);
|
|
1384
2166
|
program.command("gitignore").description("Add generated files to .gitignore").action(gitignoreCommand);
|
|
1385
|
-
program.command("import").description("Import configurations from AI tools to rulesync format").option("--claudecode", "Import from Claude Code (CLAUDE.md)").option("--cursor", "Import from Cursor (.cursorrules)").option("--copilot", "Import from GitHub Copilot (.github/copilot-instructions.md)").option("--cline", "Import from Cline (.cline/instructions.md)").option("--roo", "Import from Roo Code (.roo/instructions.md)").option("-v, --verbose", "Verbose output").action(importCommand);
|
|
1386
|
-
program.command("generate").description("Generate configuration files for AI tools").option("--copilot", "Generate only for GitHub Copilot").option("--cursor", "Generate only for Cursor").option("--cline", "Generate only for Cline").option("--claudecode", "Generate only for Claude Code").option("--roo", "Generate only for Roo Code").option("--delete", "Delete all existing files in output directories before generating").option(
|
|
2167
|
+
program.command("import").description("Import configurations from AI tools to rulesync format").option("--claudecode", "Import from Claude Code (CLAUDE.md)").option("--cursor", "Import from Cursor (.cursorrules)").option("--copilot", "Import from GitHub Copilot (.github/copilot-instructions.md)").option("--cline", "Import from Cline (.cline/instructions.md)").option("--roo", "Import from Roo Code (.roo/instructions.md)").option("--geminicli", "Import from Gemini CLI (GEMINI.md)").option("-v, --verbose", "Verbose output").action(importCommand);
|
|
2168
|
+
program.command("generate").description("Generate configuration files for AI tools").option("--copilot", "Generate only for GitHub Copilot").option("--cursor", "Generate only for Cursor").option("--cline", "Generate only for Cline").option("--claudecode", "Generate only for Claude Code").option("--roo", "Generate only for Roo Code").option("--geminicli", "Generate only for Gemini CLI").option("--delete", "Delete all existing files in output directories before generating").option(
|
|
1387
2169
|
"-b, --base-dir <paths>",
|
|
1388
2170
|
"Base directories to generate files (comma-separated for multiple paths)"
|
|
1389
2171
|
).option("-v, --verbose", "Verbose output").action(async (options) => {
|
|
@@ -1393,6 +2175,7 @@ program.command("generate").description("Generate configuration files for AI too
|
|
|
1393
2175
|
if (options.cline) tools.push("cline");
|
|
1394
2176
|
if (options.claudecode) tools.push("claudecode");
|
|
1395
2177
|
if (options.roo) tools.push("roo");
|
|
2178
|
+
if (options.geminicli) tools.push("geminicli");
|
|
1396
2179
|
const generateOptions = {
|
|
1397
2180
|
verbose: options.verbose,
|
|
1398
2181
|
delete: options.delete
|