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/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
- roo: ".roo/rules"
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 ? join(baseDir, config.outputPaths.claudecode) : config.outputPaths.claudecode;
221
+ const claudeOutputDir = baseDir ? join3(baseDir, config.outputPaths.claudecode) : config.outputPaths.claudecode;
76
222
  outputs.push({
77
223
  tool: "claudecode",
78
- filepath: join(claudeOutputDir, "CLAUDE.md"),
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: join(claudeOutputDir, ".claude", "memories", `${rule.filename}.md`),
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 join2 } from "path";
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 ? join2(baseDir, config.outputPaths.cline) : config.outputPaths.cline;
125
- const filepath = join2(outputDir, `${rule.filename}.md`);
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 join3 } from "path";
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 ? join3(baseDir, config.outputPaths.copilot) : config.outputPaths.copilot;
146
- const filepath = join3(outputDir, `${baseFilename}.instructions.md`);
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 join4 } from "path";
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 ? join4(baseDir, config.outputPaths.cursor) : config.outputPaths.cursor;
176
- const filepath = join4(outputDir, `${rule.filename}.mdc`);
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/roo.ts
207
- import { join as join5 } from "path";
208
- async function generateRooConfig(rules, config, baseDir) {
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
- for (const rule of rules) {
211
- const content = generateRooMarkdown(rule);
212
- const outputDir = baseDir ? join5(baseDir, config.outputPaths.roo) : config.outputPaths.roo;
213
- const filepath = join5(outputDir, `${rule.filename}.md`);
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: "roo",
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 generateRooMarkdown(rule) {
481
+ function generateGeminiMemoryMarkdown(rule) {
223
482
  return rule.content.trim();
224
483
  }
225
-
226
- // src/utils/file.ts
227
- import { mkdir as mkdir2, readdir, readFile, rm, stat, writeFile as writeFile2 } from "fs/promises";
228
- import { dirname, join as join6 } from "path";
229
- async function ensureDir(dirPath) {
230
- try {
231
- await stat(dirPath);
232
- } catch {
233
- await mkdir2(dirPath, { recursive: true });
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
- async function readFileContent(filepath) {
237
- return readFile(filepath, "utf-8");
238
- }
239
- async function writeFileContent(filepath, content) {
240
- await ensureDir(dirname(filepath));
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
- async function fileExists(filepath) {
252
- try {
253
- await stat(filepath);
254
- return true;
255
- } catch {
256
- return false;
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
- async function removeDirectory(dirPath) {
260
- const dangerousPaths = [".", "/", "~", "src", "node_modules"];
261
- if (dangerousPaths.includes(dirPath) || dirPath === "") {
262
- console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
263
- return;
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
- try {
266
- if (await fileExists(dirPath)) {
267
- await rm(dirPath, { recursive: true, force: true });
268
- }
269
- } catch (error) {
270
- console.warn(`Failed to remove directory ${dirPath}:`, error);
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
- async function removeFile(filepath) {
274
- try {
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
- async function removeClaudeGeneratedFiles() {
283
- const filesToRemove = ["CLAUDE.md", ".claude/memories"];
284
- for (const fileOrDir of filesToRemove) {
285
- if (fileOrDir.endsWith("/memories")) {
286
- await removeDirectory(fileOrDir);
287
- } else {
288
- await removeFile(fileOrDir);
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 ruleFiles = await findFiles(aiRulesDir);
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 join7 } from "path";
1018
+ import { join as join9 } from "path";
588
1019
  var gitignoreCommand = async () => {
589
- const gitignorePath = join7(process.cwd(), ".gitignore");
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 join13 } from "path";
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 join8 } from "path";
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
- const claudeFilePath = join8(baseDir, "CLAUDE.md");
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 = join8(baseDir, ".claude", "memories");
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 { rules, errors };
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 = join8(memoryDir, file);
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 join9 } from "path";
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 = join9(baseDir, ".cline", "instructions.md");
724
- if (!await fileExists(clineFilePath)) {
725
- errors.push(".cline/instructions.md file not found");
726
- return { rules, errors };
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
- try {
729
- const content = await readFileContent(clineFilePath);
730
- if (content.trim()) {
731
- const frontmatter = {
732
- root: false,
733
- targets: ["cline"],
734
- description: "Cline AI assistant instructions",
735
- globs: ["**/*"]
736
- };
737
- rules.push({
738
- frontmatter,
739
- content: content.trim(),
740
- filename: "cline-instructions",
741
- filepath: clineFilePath
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
- } catch (error) {
745
- const errorMessage = error instanceof Error ? error.message : String(error);
746
- errors.push(`Failed to parse Cline configuration: ${errorMessage}`);
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 join10 } from "path";
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 = join10(baseDir, ".github", "copilot-instructions.md");
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 = join10(baseDir, ".github", "instructions");
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 = join10(instructionsDir, file);
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 join11 } from "path";
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
- const cursorFilePath = join11(baseDir, ".cursorrules");
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 = join11(baseDir, ".cursor", "rules");
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 = join11(cursorRulesDir, file);
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
- return { rules, errors };
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/roo.ts
918
- import { join as join12 } from "path";
919
- async function parseRooConfiguration(baseDir = process.cwd()) {
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
- const rooFilePath = join12(baseDir, ".roo", "instructions.md");
923
- if (!await fileExists(rooFilePath)) {
924
- errors.push(".roo/instructions.md file not found");
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 content = await readFileContent(rooFilePath);
929
- if (content.trim()) {
930
- const frontmatter = {
931
- root: false,
932
- targets: ["roo"],
933
- description: "Roo Code AI assistant instructions",
934
- globs: ["**/*"]
935
- };
936
- rules.push({
937
- frontmatter,
938
- content: content.trim(),
939
- filename: "roo-instructions",
940
- filepath: rooFilePath
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 Roo configuration: ${errorMessage}`);
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 = join13(baseDir, rulesDir);
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 = join13(rulesDirPath, `${filename}.md`);
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(join13(rulesDir, `${filename}.md`))) {
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 at least one tool to import from (--claudecode, --cursor, --copilot, --cline, --roo)"
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
- console.log("Importing configuration files...");
1063
- let totalRulesCreated = 0;
1064
- const allErrors = [];
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
- if (options.verbose && allErrors.length > 0) {
1100
- console.log("\nDetailed errors:");
1101
- for (const error of allErrors) {
1102
- console.log(` - ${error}`);
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 join14 } from "path";
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 = join14(aiRulesDir, file.filename);
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 (path2) => {
2133
+ const handleChange = async (path4) => {
1352
2134
  if (isGenerating) return;
1353
2135
  isGenerating = true;
1354
2136
  console.log(`
1355
- \u{1F4DD} Detected change in ${path2}`);
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", (path2) => {
2147
+ watcher.on("change", handleChange).on("add", handleChange).on("unlink", (path4) => {
1366
2148
  console.log(`
1367
- \u{1F5D1}\uFE0F Removed ${path2}`);
1368
- handleChange(path2);
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.33.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