rulesync 0.34.0 → 0.37.0

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