rulesync 0.4.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,10 @@ A Node.js CLI tool that automatically generates configuration files for various
8
8
  ## Supported Tools
9
9
 
10
10
  - **GitHub Copilot Custom Instructions** (`.github/instructions/*.instructions.md`)
11
- - **Cursor Project Rules** (`.cursor/rules/*.md`)
11
+ - **Cursor Project Rules** (`.cursor/rules/*.mdc`)
12
12
  - **Cline Rules** (`.clinerules/*.md`)
13
- - **Claude Code Memory** (`./CLAUDE.md`)
13
+ - **Claude Code Memory** (`./CLAUDE.md` + `.claude/memories/*.md`)
14
+ - **Roo Code Rules** (`.roo/rules/*.md`)
14
15
 
15
16
  ## Installation
16
17
 
@@ -22,6 +23,132 @@ pnpm add -g rulesync
22
23
  yarn global add rulesync
23
24
  ```
24
25
 
26
+ ## Getting Started
27
+
28
+ ### Quick Start Example
29
+
30
+ 1. **Initialize your project:**
31
+ ```bash
32
+ rulesync init
33
+ ```
34
+
35
+ 2. **Create an overview file** (`.rulesync/overview.md`):
36
+ ```markdown
37
+ ---
38
+ root: true
39
+ targets: ["*"]
40
+ description: "Project overview and development philosophy"
41
+ globs: ["src/**/*.ts", "src/**/*.js"]
42
+ ---
43
+
44
+ # Project Development Guidelines
45
+
46
+ This is a TypeScript/JavaScript project following clean architecture principles.
47
+ We prioritize code readability, maintainability, and type safety.
48
+
49
+ ## Tech Stack
50
+ - TypeScript for type safety
51
+ - Node.js runtime
52
+ - Modern ES6+ features
53
+ ```
54
+
55
+ 3. **Create detail rules** (`.rulesync/coding-rules.md`):
56
+ ```markdown
57
+ ---
58
+ root: false
59
+ targets: ["copilot", "cursor", "cline"]
60
+ description: "TypeScript coding standards and best practices"
61
+ globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
62
+ ---
63
+
64
+ # TypeScript Coding Rules
65
+
66
+ ## Code Style
67
+ - Use strict TypeScript configuration
68
+ - Prefer `const` over `let` when possible
69
+ - Use meaningful, descriptive variable names
70
+ - Write JSDoc comments for public APIs
71
+
72
+ ## Type Definitions
73
+ - Prefer interfaces over types for object shapes
74
+ - Use union types for controlled values
75
+ - Avoid `any` type - use `unknown` instead
76
+ - Define return types for functions explicitly
77
+
78
+ ## Error Handling
79
+ - Use Result pattern for error handling
80
+ - Throw errors only for unexpected conditions
81
+ - Validate input parameters at function boundaries
82
+ ```
83
+
84
+ 4. **Generate configuration files:**
85
+ ```bash
86
+ rulesync generate
87
+ ```
88
+
89
+ 5. **Optional: Add generated files to .gitignore:**
90
+ ```bash
91
+ rulesync gitignore
92
+ ```
93
+
94
+ This will create tool-specific configuration files that your AI coding assistants can use automatically.
95
+
96
+ ## Why rulesync?
97
+
98
+ ### 🔧 **Tool Flexibility**
99
+ Team members can freely choose their preferred AI coding tools. Whether it's GitHub Copilot, Cursor, Cline, or Claude Code, each developer can use the tool that maximizes their productivity.
100
+
101
+ ### 📈 **Future-Proof Development**
102
+ AI development tools evolve rapidly with new tools emerging frequently. With rulesync, switching between tools doesn't require redefining your rules from scratch.
103
+
104
+ ### 🎯 **Multi-Tool Workflow**
105
+ Enable hybrid development workflows combining multiple AI tools:
106
+ - GitHub Copilot for code completion
107
+ - Cursor for refactoring
108
+ - Claude Code for architecture design
109
+ - Cline for debugging assistance
110
+
111
+ ### 🔓 **No Vendor Lock-in**
112
+ Avoid vendor lock-in completely. If you decide to stop using rulesync, you can continue using the generated rule files (`.github/instructions/`, `.cursor/rules/`, `.clinerules/`, `CLAUDE.md`, etc.) as-is.
113
+
114
+ ### 🎯 **Consistency Across Tools**
115
+ Apply consistent rules across all AI tools, improving code quality and development experience for the entire team.
116
+
117
+ ## Claude Code Integration
118
+
119
+ ### Creating Custom Slash Commands
120
+
121
+ Instead of using Claude Code's built-in `/init` command, we recommend creating a custom slash command specifically for rulesync.
122
+
123
+ Refer to the [Claude Code slash commands documentation](https://docs.anthropic.com/en/docs/claude-code/slash-commands) and add the following custom command:
124
+
125
+ **`.claude/commands/init-rulesync.md`**
126
+
127
+ ```markdown
128
+ Review this project's content and update .rulesync/*.md files as needed.
129
+
130
+ Steps:
131
+ 1. Analyze project structure and codebase
132
+ 2. Review existing .rulesync/ files
133
+ 3. Consider project's tech stack, architecture, and coding conventions
134
+ 4. Update .rulesync/*.md files if missing elements or improvements are found
135
+ 5. Run rulesync generate if necessary
136
+
137
+ Project characteristics to consider:
138
+ - Technology stack
139
+ - Architecture patterns
140
+ - Coding conventions
141
+ - Security requirements
142
+ - Performance considerations
143
+ ```
144
+
145
+ ### Integration Benefits
146
+
147
+ - **Project-Specific Initialization**: Optimized rule configuration for each project
148
+ - **Automatic Rule Updates**: Rules adapt to project changes automatically
149
+ - **Team Standardization**: All members use the same rule set
150
+ - **Continuous Improvement**: Rules evolve with project growth
151
+
25
152
  ## Usage
26
153
 
27
154
  ### 1. Initialize
@@ -38,8 +165,8 @@ Define metadata in front matter for each Markdown file:
38
165
 
39
166
  ```markdown
40
167
  ---
41
- ruleLevel: overview # or detail
42
- targets: ["*"] # or [copilot, cursor, cline, claudecode]
168
+ root: true # or false
169
+ targets: ["*"] # or [copilot, cursor, cline, claude, roo]
43
170
  description: "TypeScript coding rules"
44
171
  globs: ["**/*.ts", "**/*.tsx"]
45
172
  ---
@@ -52,12 +179,26 @@ globs: ["**/*.ts", "**/*.tsx"]
52
179
 
53
180
  ### Rule Levels
54
181
 
55
- - **overview**: Project-wide overview and policies (only one file allowed)
56
- - **detail**: Specific implementation rules and detailed guidelines (multiple files allowed)
182
+ rulesync uses a two-level rule system:
183
+
184
+ - **root: true**: Project-wide overview and policies
185
+ - Only **one** root file is allowed per project
186
+ - Contains high-level guidelines and project context
187
+ - **root: false**: Specific implementation rules and detailed guidelines
188
+ - Multiple non-root files are allowed
189
+ - Contains specific coding rules, naming conventions, etc.
190
+
191
+ #### Tool-Specific Behavior
192
+
193
+ Each AI tool handles rule levels differently:
57
194
 
58
- Each tool handles rule levels differently:
59
- - **Claude Code**: overview → `CLAUDE.md`, detail → `.claude/memories/*.md`
60
- - **Cursor**: overview `ruletype: always`, detail `ruletype: autoattached`
195
+ | Tool | Root Rules | Non-Root Rules | Special Behavior |
196
+ |------|------------|----------------|------------------|
197
+ | **Claude Code** | `./CLAUDE.md` | `.claude/memories/*.md` | CLAUDE.md includes `@filename` references to detail files |
198
+ | **Cursor** | `ruletype: always` | `ruletype: autoattached` | Detail rules without globs use `ruletype: agentrequested` |
199
+ | **GitHub Copilot** | Standard format | Standard format | All rules use same format with frontmatter |
200
+ | **Cline** | Standard format | Standard format | All rules use plain Markdown format |
201
+ | **Roo Code** | Standard format | Standard format | All rules use plain Markdown format with description header |
61
202
 
62
203
  ### 3. Generate Configuration Files
63
204
 
@@ -70,62 +211,125 @@ rulesync generate --copilot
70
211
  rulesync generate --cursor
71
212
  rulesync generate --cline
72
213
  rulesync generate --claude
214
+ rulesync generate --roo
215
+
216
+ # Clean build (delete existing files first)
217
+ rulesync generate --delete
218
+
219
+ # Clean build for specific tools
220
+ rulesync generate --copilot --cursor --delete
221
+
222
+ # Verbose output
223
+ rulesync generate --verbose
224
+ rulesync generate --delete --verbose
73
225
  ```
74
226
 
227
+ #### Generate Options
228
+
229
+ - `--delete`: Remove all existing generated files before creating new ones
230
+ - `--verbose`: Show detailed output during generation process
231
+ - `--copilot`, `--cursor`, `--cline`, `--claude`, `--roo`: Generate only for specified tools
232
+
75
233
  ### 4. Other Commands
76
234
 
77
235
  ```bash
78
- # Validate configuration
236
+ # Initialize project with sample files
237
+ rulesync init
238
+
239
+ # Validate rule files
79
240
  rulesync validate
80
241
 
81
- # Check current status
242
+ # Check current status
82
243
  rulesync status
83
244
 
84
245
  # Watch files and auto-generate
85
246
  rulesync watch
247
+
248
+ # Add generated files to .gitignore
249
+ rulesync gitignore
86
250
  ```
87
251
 
88
252
  ## Configuration File Structure
89
253
 
90
254
  ```
91
255
  .rulesync/
92
- ├── coding-rules.md # Coding rules
93
- ├── naming-conventions.md # Naming conventions
94
- ├── architecture.md # Architecture guidelines
95
- ├── security.md # Security rules
96
- └── custom.md # Project-specific rules
256
+ ├── overview.md # Project overview (root: true, only one)
257
+ ├── coding-rules.md # Coding rules (root: false)
258
+ ├── naming-conventions.md # Naming conventions (root: false)
259
+ ├── architecture.md # Architecture guidelines (root: false)
260
+ ├── security.md # Security rules (root: false)
261
+ └── custom.md # Project-specific rules (root: false)
97
262
  ```
98
263
 
99
- ## Generated Configuration Files
264
+ ### Frontmatter Schema
100
265
 
101
- | Tool | Output Path | Format |
102
- |------|------------|--------|
103
- | GitHub Copilot | `.github/instructions/*.instructions.md` | Front Matter + Markdown |
104
- | Cursor | `.cursor/rules/*.md` | MDC (YAML header + Markdown) |
105
- | Cline | `.clinerules/*.md` | Plain Markdown |
106
- | Claude Code | `./CLAUDE.md` (overview), `.claude/memories/*.md` (detail) | Plain Markdown |
266
+ Each rule file must include frontmatter with the following fields:
107
267
 
108
- ## Development
268
+ ```yaml
269
+ ---
270
+ root: true | false # Required: Rule level (true for overview, false for details)
271
+ targets: ["*"] # Required: Target tools (* = all, or specific tools)
272
+ description: "Brief description" # Required: Rule description
273
+ globs: ["**/*.ts", "**/*.js"] # Required: File patterns (can be empty array)
274
+ ---
275
+ ```
109
276
 
110
- ```bash
111
- # Install dependencies
112
- pnpm install
277
+ ### Example Files
113
278
 
114
- # Development run
115
- pnpm dev
279
+ **Root file** (`.rulesync/overview.md`):
280
+ ```markdown
281
+ ---
282
+ root: true
283
+ targets: ["*"]
284
+ description: "Project overview and development philosophy"
285
+ globs: ["src/**/*.ts"]
286
+ ---
116
287
 
117
- # Build
118
- pnpm build
288
+ # Project Development Guidelines
119
289
 
120
- # Test
121
- pnpm test
290
+ This project follows TypeScript-first development with clean architecture principles.
291
+ ```
122
292
 
123
- # Code quality checks
124
- pnpm lint
125
- pnpm format
126
- pnpm secretlint
293
+ **Non-root file** (`.rulesync/coding-rules.md`):
294
+ ```markdown
295
+ ---
296
+ root: false
297
+ targets: ["copilot", "cursor", "roo"]
298
+ description: "TypeScript coding standards"
299
+ globs: ["**/*.ts", "**/*.tsx"]
300
+ ---
301
+
302
+ # TypeScript Coding Rules
303
+
304
+ - Use strict TypeScript configuration
305
+ - Prefer interfaces over types for object shapes
306
+ - Use meaningful variable names
307
+ ```
308
+
309
+ ## Generated Configuration Files
310
+
311
+ | Tool | Output Path | Format | Rule Level Handling |
312
+ |------|------------|--------|-------------------|
313
+ | **GitHub Copilot** | `.github/instructions/*.instructions.md` | Front Matter + Markdown | Both levels use same format |
314
+ | **Cursor** | `.cursor/rules/*.mdc` | MDC (YAML header + Markdown) | Root: `ruletype: always`<br>Non-root: `ruletype: autoattached`<br>Non-root without globs: `ruletype: agentrequested` |
315
+ | **Cline** | `.clinerules/*.md` | Plain Markdown | Both levels use same format |
316
+ | **Claude Code** | `./CLAUDE.md` (root)<br>`.claude/memories/*.md` (non-root) | Plain Markdown | Root goes to CLAUDE.md<br>Non-root go to separate memory files<br>CLAUDE.md includes `@filename` references |
317
+ | **Roo Code** | `.roo/rules/*.md` | Plain Markdown | Both levels use same format with description header |
318
+
319
+ ## Validation
320
+
321
+ rulesync validates your rule files and provides helpful error messages:
322
+
323
+ ```bash
324
+ rulesync validate
127
325
  ```
128
326
 
327
+ Common validation rules:
328
+ - Only one root file (root: true) is allowed per project
329
+ - All frontmatter fields are required and properly formatted
330
+ - File patterns (globs) use valid syntax
331
+ - Target tools are recognized values
332
+
129
333
  ## License
130
334
 
131
335
  MIT License
@@ -134,4 +338,4 @@ MIT License
134
338
 
135
339
  Issues and Pull Requests are welcome!
136
340
 
137
- For detailed specifications, see [SPECIFICATION.md](./SPECIFICATION.md).
341
+ For development setup and contribution guidelines, see [CONTRIBUTING.md](./CONTRIBUTING.md).
package/dist/index.js CHANGED
@@ -30,9 +30,9 @@ var import_commander = require("commander");
30
30
  var import_node_path = require("path");
31
31
  async function generateClaudeConfig(rules, config) {
32
32
  const outputs = [];
33
- const overviewRules = rules.filter((r) => r.frontmatter.ruleLevel === "overview");
34
- const detailRules = rules.filter((r) => r.frontmatter.ruleLevel === "detail");
35
- const claudeMdContent = generateClaudeMarkdown(overviewRules, detailRules);
33
+ const rootRules = rules.filter((r) => r.frontmatter.root === true);
34
+ const detailRules = rules.filter((r) => r.frontmatter.root === false);
35
+ const claudeMdContent = generateClaudeMarkdown(rootRules, detailRules);
36
36
  outputs.push({
37
37
  tool: "claude",
38
38
  filepath: (0, import_node_path.join)(config.outputPaths.claude, "CLAUDE.md"),
@@ -48,11 +48,13 @@ async function generateClaudeConfig(rules, config) {
48
48
  }
49
49
  return outputs;
50
50
  }
51
- function generateClaudeMarkdown(overviewRules, detailRules) {
51
+ function generateClaudeMarkdown(rootRules, detailRules) {
52
52
  const lines = [];
53
53
  if (detailRules.length > 0) {
54
+ lines.push("Please also reference the following documents as needed:");
55
+ lines.push("");
54
56
  for (const rule of detailRules) {
55
- lines.push(`@${rule.filename}`);
57
+ lines.push(`@.claude/memories/${rule.filename}.md`);
56
58
  }
57
59
  lines.push("");
58
60
  }
@@ -62,8 +64,8 @@ function generateClaudeMarkdown(overviewRules, detailRules) {
62
64
  "Generated from rulesync configuration. These instructions guide Claude Code's behavior for this project."
63
65
  );
64
66
  lines.push("");
65
- if (overviewRules.length > 0) {
66
- for (const rule of overviewRules) {
67
+ if (rootRules.length > 0) {
68
+ for (const rule of rootRules) {
67
69
  lines.push(...formatRuleForClaude(rule));
68
70
  }
69
71
  }
@@ -87,10 +89,6 @@ function formatRuleForClaude(rule) {
87
89
  }
88
90
  function generateMemoryFile(rule) {
89
91
  const lines = [];
90
- lines.push("Please also refer to the following files as needed:");
91
- lines.push("");
92
- lines.push("---");
93
- lines.push("");
94
92
  lines.push(`# ${rule.filename}`);
95
93
  lines.push("");
96
94
  if (rule.frontmatter.description) {
@@ -158,7 +156,6 @@ function generateCopilotMarkdown(rule) {
158
156
  lines.push('applyTo: "**"');
159
157
  }
160
158
  lines.push("---");
161
- lines.push("");
162
159
  lines.push(rule.content);
163
160
  return lines.join("\n");
164
161
  }
@@ -169,7 +166,7 @@ async function generateCursorConfig(rules, config) {
169
166
  const outputs = [];
170
167
  for (const rule of rules) {
171
168
  const content = generateCursorMarkdown(rule);
172
- const filepath = (0, import_node_path4.join)(config.outputPaths.cursor, `${rule.filename}.md`);
169
+ const filepath = (0, import_node_path4.join)(config.outputPaths.cursor, `${rule.filename}.mdc`);
173
170
  outputs.push({
174
171
  tool: "cursor",
175
172
  filepath,
@@ -186,15 +183,37 @@ function generateCursorMarkdown(rule) {
186
183
  lines.push(`globs: [${rule.frontmatter.globs.map((g) => `"${g}"`).join(", ")}]`);
187
184
  }
188
185
  let ruletype;
189
- if (rule.frontmatter.ruleLevel === "overview") {
186
+ if (rule.frontmatter.root === true) {
190
187
  ruletype = "always";
191
- } else if (rule.frontmatter.ruleLevel === "detail" && rule.frontmatter.globs.length === 0) {
188
+ } else if (rule.frontmatter.root === false && rule.frontmatter.globs.length === 0) {
192
189
  ruletype = "agentrequested";
193
190
  } else {
194
191
  ruletype = "autoattached";
195
192
  }
196
193
  lines.push(`ruletype: ${ruletype}`);
197
194
  lines.push("---");
195
+ lines.push(rule.content);
196
+ return lines.join("\n");
197
+ }
198
+
199
+ // src/generators/roo.ts
200
+ var import_node_path5 = require("path");
201
+ async function generateRooConfig(rules, config) {
202
+ const outputs = [];
203
+ for (const rule of rules) {
204
+ const content = generateRooMarkdown(rule);
205
+ const filepath = (0, import_node_path5.join)(config.outputPaths.roo, `${rule.filename}.md`);
206
+ outputs.push({
207
+ tool: "roo",
208
+ filepath,
209
+ content
210
+ });
211
+ }
212
+ return outputs;
213
+ }
214
+ function generateRooMarkdown(rule) {
215
+ const lines = [];
216
+ lines.push(`# ${rule.frontmatter.description}`);
198
217
  lines.push("");
199
218
  lines.push(rule.content);
200
219
  return lines.join("\n");
@@ -208,10 +227,11 @@ function getDefaultConfig() {
208
227
  copilot: ".github/instructions",
209
228
  cursor: ".cursor/rules",
210
229
  cline: ".clinerules",
211
- claude: "."
230
+ claude: ".",
231
+ roo: ".roo/rules"
212
232
  },
213
233
  watchEnabled: false,
214
- defaultTargets: ["copilot", "cursor", "cline", "claude"]
234
+ defaultTargets: ["copilot", "cursor", "cline", "claude", "roo"]
215
235
  };
216
236
  }
217
237
  function resolveTargets(targets, config) {
@@ -223,7 +243,7 @@ function resolveTargets(targets, config) {
223
243
 
224
244
  // src/utils/file.ts
225
245
  var import_promises = require("fs/promises");
226
- var import_node_path5 = require("path");
246
+ var import_node_path6 = require("path");
227
247
  async function ensureDir(dirPath) {
228
248
  try {
229
249
  await (0, import_promises.stat)(dirPath);
@@ -235,13 +255,13 @@ async function readFileContent(filepath) {
235
255
  return (0, import_promises.readFile)(filepath, "utf-8");
236
256
  }
237
257
  async function writeFileContent(filepath, content) {
238
- await ensureDir((0, import_node_path5.dirname)(filepath));
258
+ await ensureDir((0, import_node_path6.dirname)(filepath));
239
259
  await (0, import_promises.writeFile)(filepath, content, "utf-8");
240
260
  }
241
261
  async function findFiles(dir, extension = ".md") {
242
262
  try {
243
263
  const files = await (0, import_promises.readdir)(dir);
244
- return files.filter((file) => file.endsWith(extension)).map((file) => (0, import_node_path5.join)(dir, file));
264
+ return files.filter((file) => file.endsWith(extension)).map((file) => (0, import_node_path6.join)(dir, file));
245
265
  } catch {
246
266
  return [];
247
267
  }
@@ -255,6 +275,11 @@ async function fileExists(filepath) {
255
275
  }
256
276
  }
257
277
  async function removeDirectory(dirPath) {
278
+ const dangerousPaths = [".", "/", "~", "src", "node_modules"];
279
+ if (dangerousPaths.includes(dirPath) || dirPath === "") {
280
+ console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
281
+ return;
282
+ }
258
283
  try {
259
284
  if (await fileExists(dirPath)) {
260
285
  await (0, import_promises.rm)(dirPath, { recursive: true, force: true });
@@ -263,6 +288,25 @@ async function removeDirectory(dirPath) {
263
288
  console.warn(`Failed to remove directory ${dirPath}:`, error);
264
289
  }
265
290
  }
291
+ async function removeFile(filepath) {
292
+ try {
293
+ if (await fileExists(filepath)) {
294
+ await (0, import_promises.rm)(filepath);
295
+ }
296
+ } catch (error) {
297
+ console.warn(`Failed to remove file ${filepath}:`, error);
298
+ }
299
+ }
300
+ async function removeClaudeGeneratedFiles() {
301
+ const filesToRemove = ["CLAUDE.md", ".claude/memories"];
302
+ for (const fileOrDir of filesToRemove) {
303
+ if (fileOrDir.endsWith("/memories")) {
304
+ await removeDirectory(fileOrDir);
305
+ } else {
306
+ await removeFile(fileOrDir);
307
+ }
308
+ }
309
+ }
266
310
 
267
311
  // src/core/generator.ts
268
312
  async function generateConfigurations(rules, config, targetTools) {
@@ -297,6 +341,8 @@ async function generateForTool(tool, rules, config) {
297
341
  return generateClineConfig(rules, config);
298
342
  case "claude":
299
343
  return await generateClaudeConfig(rules, config);
344
+ case "roo":
345
+ return generateRooConfig(rules, config);
300
346
  default:
301
347
  console.warn(`Unknown tool: ${tool}`);
302
348
  return null;
@@ -304,7 +350,7 @@ async function generateForTool(tool, rules, config) {
304
350
  }
305
351
 
306
352
  // src/core/parser.ts
307
- var import_node_path6 = require("path");
353
+ var import_node_path7 = require("path");
308
354
  var import_gray_matter = __toESM(require("gray-matter"));
309
355
  async function parseRulesFromDirectory(aiRulesDir) {
310
356
  const ruleFiles = await findFiles(aiRulesDir);
@@ -324,7 +370,7 @@ async function parseRuleFile(filepath) {
324
370
  const parsed = (0, import_gray_matter.default)(content);
325
371
  validateFrontmatter(parsed.data, filepath);
326
372
  const frontmatter = parsed.data;
327
- const filename = (0, import_node_path6.basename)(filepath, ".md");
373
+ const filename = (0, import_node_path7.basename)(filepath, ".md");
328
374
  return {
329
375
  frontmatter,
330
376
  content: parsed.content,
@@ -337,8 +383,8 @@ function validateFrontmatter(data, filepath) {
337
383
  throw new Error(`Invalid frontmatter in ${filepath}: must be an object`);
338
384
  }
339
385
  const obj = data;
340
- if (!obj.ruleLevel || !["overview", "detail"].includes(obj.ruleLevel)) {
341
- throw new Error(`Invalid ruleLevel in ${filepath}: must be "overview" or "detail"`);
386
+ if (typeof obj.root !== "boolean") {
387
+ throw new Error(`Invalid root in ${filepath}: must be a boolean`);
342
388
  }
343
389
  if (!Array.isArray(obj.targets)) {
344
390
  throw new Error(`Invalid targets in ${filepath}: must be an array`);
@@ -375,9 +421,11 @@ async function validateRules(rules) {
375
421
  }
376
422
  filenames.add(rule.filename);
377
423
  }
378
- const overviewRules = rules.filter((rule) => rule.frontmatter.ruleLevel === "overview");
379
- if (overviewRules.length > 1) {
380
- errors.push(`Multiple overview rules found: ${overviewRules.map((r) => r.filename).join(", ")}. Only one overview rule is allowed.`);
424
+ const rootRules = rules.filter((rule) => rule.frontmatter.root === true);
425
+ if (rootRules.length > 1) {
426
+ errors.push(
427
+ `Multiple root rules found: ${rootRules.map((r) => r.filename).join(", ")}. Only one root rule is allowed.`
428
+ );
381
429
  }
382
430
  for (const rule of rules) {
383
431
  const ruleValidation = await validateRule(rule);
@@ -450,7 +498,10 @@ async function generateCommand(options = {}) {
450
498
  deleteTasks.push(removeDirectory(config.outputPaths.cline));
451
499
  break;
452
500
  case "claude":
453
- deleteTasks.push(removeDirectory(config.outputPaths.claude));
501
+ deleteTasks.push(removeClaudeGeneratedFiles());
502
+ break;
503
+ case "roo":
504
+ deleteTasks.push(removeDirectory(config.outputPaths.roo));
454
505
  break;
455
506
  }
456
507
  }
@@ -478,15 +529,18 @@ async function generateCommand(options = {}) {
478
529
 
479
530
  // src/cli/commands/gitignore.ts
480
531
  var import_node_fs = require("fs");
481
- var import_node_path7 = require("path");
532
+ var import_node_path8 = require("path");
482
533
  var gitignoreCommand = async () => {
483
- const gitignorePath = (0, import_node_path7.join)(process.cwd(), ".gitignore");
534
+ const gitignorePath = (0, import_node_path8.join)(process.cwd(), ".gitignore");
484
535
  const rulesFilesToIgnore = [
485
536
  "# Generated by rulesync - AI tool configuration files",
537
+ ".github/copilot-instructions.md",
486
538
  ".github/instructions/",
487
539
  ".cursor/rules/",
488
540
  ".clinerules/",
489
- "CLAUDE.md"
541
+ "CLAUDE.md",
542
+ ".claude/memories/",
543
+ ".roo/rules/"
490
544
  ];
491
545
  let gitignoreContent = "";
492
546
  if ((0, import_node_fs.existsSync)(gitignorePath)) {
@@ -517,7 +571,7 @@ ${linesToAdd.join("\n")}
517
571
  };
518
572
 
519
573
  // src/cli/commands/init.ts
520
- var import_node_path8 = require("path");
574
+ var import_node_path9 = require("path");
521
575
  async function initCommand() {
522
576
  const aiRulesDir = ".rulesync";
523
577
  console.log("Initializing rulesync...");
@@ -607,7 +661,7 @@ globs: ["src/**/*.ts"]
607
661
  }
608
662
  ];
609
663
  for (const file of sampleFiles) {
610
- const filepath = (0, import_node_path8.join)(aiRulesDir, file.filename);
664
+ const filepath = (0, import_node_path9.join)(aiRulesDir, file.filename);
611
665
  if (!await fileExists(filepath)) {
612
666
  await writeFileContent(filepath, file.content);
613
667
  console.log(`Created ${filepath}`);
package/dist/index.mjs CHANGED
@@ -7,9 +7,9 @@ import { Command } from "commander";
7
7
  import { join } from "path";
8
8
  async function generateClaudeConfig(rules, config) {
9
9
  const outputs = [];
10
- const overviewRules = rules.filter((r) => r.frontmatter.ruleLevel === "overview");
11
- const detailRules = rules.filter((r) => r.frontmatter.ruleLevel === "detail");
12
- const claudeMdContent = generateClaudeMarkdown(overviewRules, detailRules);
10
+ const rootRules = rules.filter((r) => r.frontmatter.root === true);
11
+ const detailRules = rules.filter((r) => r.frontmatter.root === false);
12
+ const claudeMdContent = generateClaudeMarkdown(rootRules, detailRules);
13
13
  outputs.push({
14
14
  tool: "claude",
15
15
  filepath: join(config.outputPaths.claude, "CLAUDE.md"),
@@ -25,11 +25,13 @@ async function generateClaudeConfig(rules, config) {
25
25
  }
26
26
  return outputs;
27
27
  }
28
- function generateClaudeMarkdown(overviewRules, detailRules) {
28
+ function generateClaudeMarkdown(rootRules, detailRules) {
29
29
  const lines = [];
30
30
  if (detailRules.length > 0) {
31
+ lines.push("Please also reference the following documents as needed:");
32
+ lines.push("");
31
33
  for (const rule of detailRules) {
32
- lines.push(`@${rule.filename}`);
34
+ lines.push(`@.claude/memories/${rule.filename}.md`);
33
35
  }
34
36
  lines.push("");
35
37
  }
@@ -39,8 +41,8 @@ function generateClaudeMarkdown(overviewRules, detailRules) {
39
41
  "Generated from rulesync configuration. These instructions guide Claude Code's behavior for this project."
40
42
  );
41
43
  lines.push("");
42
- if (overviewRules.length > 0) {
43
- for (const rule of overviewRules) {
44
+ if (rootRules.length > 0) {
45
+ for (const rule of rootRules) {
44
46
  lines.push(...formatRuleForClaude(rule));
45
47
  }
46
48
  }
@@ -64,10 +66,6 @@ function formatRuleForClaude(rule) {
64
66
  }
65
67
  function generateMemoryFile(rule) {
66
68
  const lines = [];
67
- lines.push("Please also refer to the following files as needed:");
68
- lines.push("");
69
- lines.push("---");
70
- lines.push("");
71
69
  lines.push(`# ${rule.filename}`);
72
70
  lines.push("");
73
71
  if (rule.frontmatter.description) {
@@ -135,7 +133,6 @@ function generateCopilotMarkdown(rule) {
135
133
  lines.push('applyTo: "**"');
136
134
  }
137
135
  lines.push("---");
138
- lines.push("");
139
136
  lines.push(rule.content);
140
137
  return lines.join("\n");
141
138
  }
@@ -146,7 +143,7 @@ async function generateCursorConfig(rules, config) {
146
143
  const outputs = [];
147
144
  for (const rule of rules) {
148
145
  const content = generateCursorMarkdown(rule);
149
- const filepath = join4(config.outputPaths.cursor, `${rule.filename}.md`);
146
+ const filepath = join4(config.outputPaths.cursor, `${rule.filename}.mdc`);
150
147
  outputs.push({
151
148
  tool: "cursor",
152
149
  filepath,
@@ -163,15 +160,37 @@ function generateCursorMarkdown(rule) {
163
160
  lines.push(`globs: [${rule.frontmatter.globs.map((g) => `"${g}"`).join(", ")}]`);
164
161
  }
165
162
  let ruletype;
166
- if (rule.frontmatter.ruleLevel === "overview") {
163
+ if (rule.frontmatter.root === true) {
167
164
  ruletype = "always";
168
- } else if (rule.frontmatter.ruleLevel === "detail" && rule.frontmatter.globs.length === 0) {
165
+ } else if (rule.frontmatter.root === false && rule.frontmatter.globs.length === 0) {
169
166
  ruletype = "agentrequested";
170
167
  } else {
171
168
  ruletype = "autoattached";
172
169
  }
173
170
  lines.push(`ruletype: ${ruletype}`);
174
171
  lines.push("---");
172
+ lines.push(rule.content);
173
+ return lines.join("\n");
174
+ }
175
+
176
+ // src/generators/roo.ts
177
+ import { join as join5 } from "path";
178
+ async function generateRooConfig(rules, config) {
179
+ const outputs = [];
180
+ for (const rule of rules) {
181
+ const content = generateRooMarkdown(rule);
182
+ const filepath = join5(config.outputPaths.roo, `${rule.filename}.md`);
183
+ outputs.push({
184
+ tool: "roo",
185
+ filepath,
186
+ content
187
+ });
188
+ }
189
+ return outputs;
190
+ }
191
+ function generateRooMarkdown(rule) {
192
+ const lines = [];
193
+ lines.push(`# ${rule.frontmatter.description}`);
175
194
  lines.push("");
176
195
  lines.push(rule.content);
177
196
  return lines.join("\n");
@@ -185,10 +204,11 @@ function getDefaultConfig() {
185
204
  copilot: ".github/instructions",
186
205
  cursor: ".cursor/rules",
187
206
  cline: ".clinerules",
188
- claude: "."
207
+ claude: ".",
208
+ roo: ".roo/rules"
189
209
  },
190
210
  watchEnabled: false,
191
- defaultTargets: ["copilot", "cursor", "cline", "claude"]
211
+ defaultTargets: ["copilot", "cursor", "cline", "claude", "roo"]
192
212
  };
193
213
  }
194
214
  function resolveTargets(targets, config) {
@@ -200,7 +220,7 @@ function resolveTargets(targets, config) {
200
220
 
201
221
  // src/utils/file.ts
202
222
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
203
- import { dirname, join as join5 } from "path";
223
+ import { dirname, join as join6 } from "path";
204
224
  async function ensureDir(dirPath) {
205
225
  try {
206
226
  await stat(dirPath);
@@ -218,7 +238,7 @@ async function writeFileContent(filepath, content) {
218
238
  async function findFiles(dir, extension = ".md") {
219
239
  try {
220
240
  const files = await readdir(dir);
221
- return files.filter((file) => file.endsWith(extension)).map((file) => join5(dir, file));
241
+ return files.filter((file) => file.endsWith(extension)).map((file) => join6(dir, file));
222
242
  } catch {
223
243
  return [];
224
244
  }
@@ -232,6 +252,11 @@ async function fileExists(filepath) {
232
252
  }
233
253
  }
234
254
  async function removeDirectory(dirPath) {
255
+ const dangerousPaths = [".", "/", "~", "src", "node_modules"];
256
+ if (dangerousPaths.includes(dirPath) || dirPath === "") {
257
+ console.warn(`Skipping deletion of dangerous path: ${dirPath}`);
258
+ return;
259
+ }
235
260
  try {
236
261
  if (await fileExists(dirPath)) {
237
262
  await rm(dirPath, { recursive: true, force: true });
@@ -240,6 +265,25 @@ async function removeDirectory(dirPath) {
240
265
  console.warn(`Failed to remove directory ${dirPath}:`, error);
241
266
  }
242
267
  }
268
+ async function removeFile(filepath) {
269
+ try {
270
+ if (await fileExists(filepath)) {
271
+ await rm(filepath);
272
+ }
273
+ } catch (error) {
274
+ console.warn(`Failed to remove file ${filepath}:`, error);
275
+ }
276
+ }
277
+ async function removeClaudeGeneratedFiles() {
278
+ const filesToRemove = ["CLAUDE.md", ".claude/memories"];
279
+ for (const fileOrDir of filesToRemove) {
280
+ if (fileOrDir.endsWith("/memories")) {
281
+ await removeDirectory(fileOrDir);
282
+ } else {
283
+ await removeFile(fileOrDir);
284
+ }
285
+ }
286
+ }
243
287
 
244
288
  // src/core/generator.ts
245
289
  async function generateConfigurations(rules, config, targetTools) {
@@ -274,6 +318,8 @@ async function generateForTool(tool, rules, config) {
274
318
  return generateClineConfig(rules, config);
275
319
  case "claude":
276
320
  return await generateClaudeConfig(rules, config);
321
+ case "roo":
322
+ return generateRooConfig(rules, config);
277
323
  default:
278
324
  console.warn(`Unknown tool: ${tool}`);
279
325
  return null;
@@ -314,8 +360,8 @@ function validateFrontmatter(data, filepath) {
314
360
  throw new Error(`Invalid frontmatter in ${filepath}: must be an object`);
315
361
  }
316
362
  const obj = data;
317
- if (!obj.ruleLevel || !["overview", "detail"].includes(obj.ruleLevel)) {
318
- throw new Error(`Invalid ruleLevel in ${filepath}: must be "overview" or "detail"`);
363
+ if (typeof obj.root !== "boolean") {
364
+ throw new Error(`Invalid root in ${filepath}: must be a boolean`);
319
365
  }
320
366
  if (!Array.isArray(obj.targets)) {
321
367
  throw new Error(`Invalid targets in ${filepath}: must be an array`);
@@ -352,9 +398,11 @@ async function validateRules(rules) {
352
398
  }
353
399
  filenames.add(rule.filename);
354
400
  }
355
- const overviewRules = rules.filter((rule) => rule.frontmatter.ruleLevel === "overview");
356
- if (overviewRules.length > 1) {
357
- errors.push(`Multiple overview rules found: ${overviewRules.map((r) => r.filename).join(", ")}. Only one overview rule is allowed.`);
401
+ const rootRules = rules.filter((rule) => rule.frontmatter.root === true);
402
+ if (rootRules.length > 1) {
403
+ errors.push(
404
+ `Multiple root rules found: ${rootRules.map((r) => r.filename).join(", ")}. Only one root rule is allowed.`
405
+ );
358
406
  }
359
407
  for (const rule of rules) {
360
408
  const ruleValidation = await validateRule(rule);
@@ -427,7 +475,10 @@ async function generateCommand(options = {}) {
427
475
  deleteTasks.push(removeDirectory(config.outputPaths.cline));
428
476
  break;
429
477
  case "claude":
430
- deleteTasks.push(removeDirectory(config.outputPaths.claude));
478
+ deleteTasks.push(removeClaudeGeneratedFiles());
479
+ break;
480
+ case "roo":
481
+ deleteTasks.push(removeDirectory(config.outputPaths.roo));
431
482
  break;
432
483
  }
433
484
  }
@@ -455,15 +506,18 @@ async function generateCommand(options = {}) {
455
506
 
456
507
  // src/cli/commands/gitignore.ts
457
508
  import { existsSync, readFileSync, writeFileSync } from "fs";
458
- import { join as join6 } from "path";
509
+ import { join as join7 } from "path";
459
510
  var gitignoreCommand = async () => {
460
- const gitignorePath = join6(process.cwd(), ".gitignore");
511
+ const gitignorePath = join7(process.cwd(), ".gitignore");
461
512
  const rulesFilesToIgnore = [
462
513
  "# Generated by rulesync - AI tool configuration files",
514
+ ".github/copilot-instructions.md",
463
515
  ".github/instructions/",
464
516
  ".cursor/rules/",
465
517
  ".clinerules/",
466
- "CLAUDE.md"
518
+ "CLAUDE.md",
519
+ ".claude/memories/",
520
+ ".roo/rules/"
467
521
  ];
468
522
  let gitignoreContent = "";
469
523
  if (existsSync(gitignorePath)) {
@@ -494,7 +548,7 @@ ${linesToAdd.join("\n")}
494
548
  };
495
549
 
496
550
  // src/cli/commands/init.ts
497
- import { join as join7 } from "path";
551
+ import { join as join8 } from "path";
498
552
  async function initCommand() {
499
553
  const aiRulesDir = ".rulesync";
500
554
  console.log("Initializing rulesync...");
@@ -584,7 +638,7 @@ globs: ["src/**/*.ts"]
584
638
  }
585
639
  ];
586
640
  for (const file of sampleFiles) {
587
- const filepath = join7(aiRulesDir, file.filename);
641
+ const filepath = join8(aiRulesDir, file.filename);
588
642
  if (!await fileExists(filepath)) {
589
643
  await writeFileContent(filepath, file.content);
590
644
  console.log(`Created ${filepath}`);
package/package.json CHANGED
@@ -1,71 +1,72 @@
1
1
  {
2
- "name": "rulesync",
3
- "version": "0.4.0",
4
- "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
7
- "types": "dist/index.d.ts",
8
- "bin": {
9
- "rulesync": "dist/index.js"
10
- },
11
- "files": [
12
- "dist"
13
- ],
14
- "engines": {
15
- "node": ">=20.0.0"
16
- },
17
- "keywords": [
18
- "ai",
19
- "rules",
20
- "cli",
21
- "copilot",
22
- "cursor",
23
- "cline",
24
- "configuration",
25
- "development"
26
- ],
27
- "author": "dyoshikawa",
28
- "license": "MIT",
29
- "repository": {
30
- "type": "git",
31
- "url": "https://github.com/dyoshikawa/rulesync.git"
32
- },
33
- "bugs": {
34
- "url": "https://github.com/dyoshikawa/rulesync/issues"
35
- },
36
- "homepage": "https://github.com/dyoshikawa/rulesync#readme",
37
- "publishConfig": {
38
- "access": "public"
39
- },
40
- "packageManager": "pnpm@7.33.7",
41
- "devDependencies": {
42
- "@biomejs/biome": "2.0.0",
43
- "@secretlint/secretlint-rule-preset-recommend": "10.1.0",
44
- "@tsconfig/node24": "24.0.1",
45
- "@types/node": "24.0.3",
46
- "@vitest/coverage-v8": "3.2.4",
47
- "secretlint": "10.1.0",
48
- "tsup": "8.5.0",
49
- "tsx": "4.20.3",
50
- "typescript": "5.8.3",
51
- "vitest": "3.2.4"
52
- },
53
- "dependencies": {
54
- "chokidar": "4.0.3",
55
- "commander": "14.0.0",
56
- "gray-matter": "4.0.3",
57
- "marked": "15.0.12"
58
- },
59
- "scripts": {
60
- "dev": "tsx src/cli/index.ts",
61
- "build": "tsup src/cli/index.ts --format cjs,esm --dts --clean",
62
- "lint": "biome lint src/",
63
- "format": "biome format --write src/",
64
- "format:check": "biome format src/",
65
- "check": "biome check src/",
66
- "secretlint": "secretlint \"**/*\"",
67
- "test": "vitest",
68
- "test:watch": "vitest --watch",
69
- "test:coverage": "vitest --coverage"
70
- }
71
- }
2
+ "name": "rulesync",
3
+ "version": "0.9.0",
4
+ "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "rulesync": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "dev": "tsx src/cli/index.ts",
16
+ "build": "tsup src/cli/index.ts --format cjs,esm --dts --clean",
17
+ "lint": "biome lint src/",
18
+ "format": "biome format --write src/",
19
+ "format:check": "biome format src/",
20
+ "check": "biome check src/",
21
+ "secretlint": "secretlint \"**/*\"",
22
+ "test": "vitest",
23
+ "test:watch": "vitest --watch",
24
+ "test:coverage": "vitest --coverage",
25
+ "prepublishOnly": "pnpm build"
26
+ },
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "keywords": [
31
+ "ai",
32
+ "rules",
33
+ "cli",
34
+ "copilot",
35
+ "cursor",
36
+ "cline",
37
+ "configuration",
38
+ "development"
39
+ ],
40
+ "author": "dyoshikawa",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/dyoshikawa/rulesync.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/dyoshikawa/rulesync/issues"
48
+ },
49
+ "homepage": "https://github.com/dyoshikawa/rulesync#readme",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "packageManager": "pnpm@7.33.7",
54
+ "devDependencies": {
55
+ "@biomejs/biome": "2.0.0",
56
+ "@secretlint/secretlint-rule-preset-recommend": "10.1.0",
57
+ "@tsconfig/node24": "24.0.1",
58
+ "@types/node": "24.0.3",
59
+ "@vitest/coverage-v8": "3.2.4",
60
+ "secretlint": "10.1.0",
61
+ "tsup": "8.5.0",
62
+ "tsx": "4.20.3",
63
+ "typescript": "5.8.3",
64
+ "vitest": "3.2.4"
65
+ },
66
+ "dependencies": {
67
+ "chokidar": "4.0.3",
68
+ "commander": "14.0.0",
69
+ "gray-matter": "4.0.3",
70
+ "marked": "15.0.12"
71
+ }
72
+ }