@intellectronica/ruler 0.1.3 → 0.2.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
@@ -1,207 +1,396 @@
1
- > **Experimental Research Preview**
2
- > - Please test this version with caution in your own setup
3
- > - File issues at https://github.com/intellectronica/ruler/issues
1
+ # Ruler: Centralise Your AI Coding Assistant Instructions
4
2
 
5
- # Ruler
3
+ [![CI](https://github.com/intellectronica/ruler/actions/workflows/ci.yml/badge.svg)](https://github.com/intellectronica/ruler/actions/workflows/ci.yml)
4
+ [![npm version](https://badge.fury.io/js/%40intellectronica%2Fruler.svg)](https://badge.fury.io/js/%40intellectronica%2Fruler)
5
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
6
6
 
7
- A CLI tool to manage custom rules and configs across different AI coding agents.
7
+ - **GitHub**: [intellectronica/ruler](https://github.com/intellectronica/ruler)
8
+ - **NPM**: [@intellectronica/ruler](https://www.npmjs.com/package/@intellectronica/ruler)
8
9
 
9
- ## Features
10
+ ---
11
+
12
+ > **Beta Research Preview**
13
+ > - Please test this version carefully in your environment
14
+ > - Report issues at https://github.com/intellectronica/ruler/issues
15
+
16
+ ## Why Ruler?
17
+
18
+ Managing instructions across multiple AI coding tools becomes complex as your team grows. Different agents (GitHub Copilot, Claude, Cursor, Aider, etc.) require their own configuration files, leading to:
19
+
20
+ - **Inconsistent guidance** across AI tools
21
+ - **Duplicated effort** maintaining multiple config files
22
+ - **Context drift** as project requirements evolve
23
+ - **Onboarding friction** for new AI tools
24
+
25
+ Ruler solves this by providing a **single source of truth** for all your AI agent instructions, automatically distributing them to the right configuration files.
26
+
27
+ ## Core Features
28
+
29
+ - **Centralised Rule Management**: Store all AI instructions in a dedicated `.ruler/` directory using Markdown files
30
+ - **Automatic Distribution**: Ruler applies these rules to configuration files of supported AI agents
31
+ - **Targeted Agent Configuration**: Fine-tune which agents are affected and their specific output paths via `ruler.toml`
32
+ - **MCP Server Propagation**: Manage and distribute Model Context Protocol (MCP) server settings
33
+ - **`.gitignore` Automation**: Keeps generated agent config files out of version control automatically
34
+ - **Simple CLI**: Easy-to-use commands for initialising and applying configurations
35
+
36
+ ## Supported AI Agents
37
+
38
+ | Agent | File(s) Created/Updated |
39
+ | ---------------------- | ----------------------------------------------------------- |
40
+ | GitHub Copilot | `.github/copilot-instructions.md` |
41
+ | Claude Code | `CLAUDE.md` |
42
+ | OpenAI Codex CLI | `AGENTS.md` |
43
+ | Cursor | `.cursor/rules/ruler_cursor_instructions.md` |
44
+ | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
45
+ | Cline | `.clinerules` |
46
+ | Aider | `ruler_aider_instructions.md` and `.aider.conf.yml` |
10
47
 
11
- - Centralise AI agent instructions in a single `.ruler/` directory
12
- - Distribute rules to supported agents (GitHub Copilot, Claude Code, OpenAI Codex CLI, Cursor, Windsurf, Cline, Aider)
13
- - Extensible architecture: add new agent adapters easily
48
+ ## Getting Started
14
49
 
15
- ## Installation
50
+ ### Prerequisites
16
51
 
17
- Install globally:
52
+ Node.js 18.x or higher is required.
18
53
 
54
+ ### Installation
55
+
56
+ **Global Installation (Recommended for CLI use):**
19
57
  ```bash
20
58
  npm install -g @intellectronica/ruler
21
59
  ```
22
60
 
23
- Or use npx:
24
-
61
+ **Using `npx` (for one-off commands):**
25
62
  ```bash
26
63
  npx @intellectronica/ruler apply
27
64
  ```
28
65
 
29
- ## Usage
66
+ ### Project Initialisation
30
67
 
31
- Create a `.ruler/` directory at your project root and add Markdown files defining your rules:
68
+ 1. Navigate to your project's root directory
69
+ 2. Run `ruler init`
70
+ 3. This creates:
71
+ - `.ruler/` directory
72
+ - `.ruler/instructions.md`: A starter Markdown file for your rules
73
+ - `.ruler/ruler.toml`: The main configuration file for Ruler
74
+ - `.ruler/mcp.json`: An example MCP server configuration
32
75
 
33
- ```
34
- .ruler/
35
- ├── coding_guidelines.md
36
- └── style_guide.md
76
+ ## Core Concepts
77
+
78
+ ### The `.ruler/` Directory
79
+
80
+ This is your central hub for all AI agent instructions:
81
+
82
+ - **Rule Files (`*.md`)**: Discovered recursively from `.ruler/` and alphabetically concatenated
83
+ - **Concatenation Marker**: Each file's content is prepended with `--- Source: <relative_path_to_md_file> ---` for traceability
84
+ - **`ruler.toml`**: Master configuration for Ruler's behavior, agent selection, and output paths
85
+ - **`mcp.json`**: Shared MCP server settings
86
+
87
+ ### Best Practices for Rule Files
88
+
89
+ **Granularity**: Break down complex instructions into focused `.md` files:
90
+ - `coding_style.md`
91
+ - `api_conventions.md`
92
+ - `project_architecture.md`
93
+ - `security_guidelines.md`
94
+
95
+ **Example rule file (`.ruler/python_guidelines.md`):**
96
+ ```markdown
97
+ # Python Project Guidelines
98
+
99
+ ## General Style
100
+ - Follow PEP 8 for all Python code
101
+ - Use type hints for all function signatures and complex variables
102
+ - Keep functions short and focused on a single task
103
+
104
+ ## Error Handling
105
+ - Use specific exception types rather than generic `Exception`
106
+ - Log errors effectively with context
107
+
108
+ ## Security
109
+ - Always validate and sanitize user input
110
+ - Be mindful of potential injection vulnerabilities
37
111
  ```
38
112
 
39
- Run the apply command:
113
+ ## Usage: The `apply` Command
40
114
 
115
+ ### Primary Command
41
116
  ```bash
42
- ruler apply [--project-root <path>] [--agents <agent1,agent2,...>] [--config <path>] [--gitignore] [--no-gitignore]
117
+ ruler apply [options]
43
118
  ```
44
119
 
120
+ ### Options
45
121
 
46
- Run the init command to scaffold a basic `.ruler/` setup:
122
+ | Option | Description |
123
+ |--------|-------------|
124
+ | `--project-root <path>` | Path to your project's root (default: current directory) |
125
+ | `--agents <agent1,agent2,...>` | Comma-separated list of agent names to target |
126
+ | `--config <path>` | Path to a custom `ruler.toml` configuration file |
127
+ | `--mcp` / `--with-mcp` | Enable applying MCP server configurations (default: true) |
128
+ | `--no-mcp` | Disable applying MCP server configurations |
129
+ | `--mcp-overwrite` | Overwrite native MCP config entirely instead of merging |
130
+ | `--gitignore` | Enable automatic .gitignore updates (default: true) |
131
+ | `--no-gitignore` | Disable automatic .gitignore updates |
132
+ | `--verbose` / `-v` | Display detailed output during execution |
47
133
 
134
+ ### Common Examples
135
+
136
+ **Apply rules to all configured agents:**
48
137
  ```bash
49
- ruler init [--project-root <path>]
138
+ ruler apply
50
139
  ```
51
140
 
52
- Use `--agents` to specify a comma-separated list of agent names (case-insensitive substrings) to limit which agents the rules are applied to.
141
+ **Apply rules only to GitHub Copilot and Claude:**
142
+ ```bash
143
+ ruler apply --agents copilot,claude
144
+ ```
53
145
 
54
- The command will read all `.md` files under `.ruler/`, concatenate their contents, and generate/update configuration files for the following agents:
146
+ **Use a specific configuration file:**
147
+ ```bash
148
+ ruler apply --config ./team-configs/ruler.frontend.toml
149
+ ```
55
150
 
56
- | Agent | File(s) Created/Updated |
57
- | ---------------------- | ----------------------------------------------------------- |
58
- | GitHub Copilot | `.github/copilot-instructions.md` |
59
- | Claude Code | `CLAUDE.md` |
60
- | OpenAI Codex CLI | `AGENTS.md` |
61
- | Cursor | `.cursor/rules/ruler_cursor_instructions.md` |
62
- | Windsurf | `.windsurf/rules/ruler_windsurf_instructions.md` |
63
- | Cline | `.clinerules` |
64
- | Aider | `ruler_aider_instructions.md` <br>and updates `.aider.conf.yml` |
151
+ **Apply rules with verbose output:**
152
+ ```bash
153
+ ruler apply --verbose
154
+ ```
65
155
 
66
- ## Configuration
156
+ **Apply rules but skip MCP and .gitignore updates:**
157
+ ```bash
158
+ ruler apply --no-mcp --no-gitignore
159
+ ```
67
160
 
68
- Ruler uses a TOML configuration file located at `.ruler/ruler.toml` by default. You can override its location with the `--config <path>` option in the `apply` command.
161
+ ## Configuration (`ruler.toml`) in Detail
69
162
 
70
- ### Configuration structure
163
+ ### Location
164
+ Defaults to `.ruler/ruler.toml` in the project root. Override with `--config` CLI option.
71
165
 
166
+ ### Complete Example
72
167
  ```toml
73
- # Run only these agents by default (omit to use all agents)
74
- # default_agents = ["GitHub Copilot", "Claude Code", "Aider"]
168
+ # Default agents to run when --agents is not specified
169
+ # Uses case-insensitive substring matching
170
+ default_agents = ["copilot", "claude", "aider"]
75
171
 
76
- [agents.Copilot]
172
+ # --- Global MCP Server Configuration ---
173
+ [mcp]
174
+ # Enable/disable MCP propagation globally (default: true)
175
+ enabled = true
176
+ # Global merge strategy: 'merge' or 'overwrite' (default: 'merge')
177
+ merge_strategy = "merge"
178
+
179
+ # --- Global .gitignore Configuration ---
180
+ [gitignore]
181
+ # Enable/disable automatic .gitignore updates (default: true)
182
+ enabled = true
183
+
184
+ # --- Agent-Specific Configurations ---
185
+ [agents.copilot]
77
186
  enabled = true
78
187
  output_path = ".github/copilot-instructions.md"
79
188
 
80
- [agents.Claude]
189
+ [agents.claude]
190
+ enabled = true
191
+ output_path = "CLAUDE.md"
192
+
193
+ [agents.aider]
81
194
  enabled = true
82
- # output_path = "CLAUDE.md"
195
+ output_path_instructions = "ruler_aider_instructions.md"
196
+ output_path_config = ".aider.conf.yml"
83
197
 
84
- [agents.Aider]
198
+ # Agent-specific MCP configuration
199
+ [agents.cursor.mcp]
200
+ enabled = true
201
+ merge_strategy = "merge"
202
+
203
+ # Disable specific agents
204
+ [agents.windsurf]
85
205
  enabled = false
86
- # output_path_instructions = "ruler_aider_instructions.md"
87
- # output_path_config = ".aider.conf.yml"
88
206
  ```
89
207
 
90
- - `default_agents`: array of agent names (case-insensitive substrings) to run by default.
91
- - `[agents.<AgentName>]`: per-agent settings:
92
- - `enabled` (boolean): enable or disable this agent.
93
- - `output_path` (string): custom path for agents that produce a single file.
94
- - `output_path_instructions`/`output_path_config`: custom paths for Aider's instruction and config files.
95
-
96
- ### Precedence
208
+ ### Configuration Precedence
97
209
 
98
- 1. CLI `--agents` option (substring filters)
99
- 2. Config file `default_agents` and `[agents]` overrides
100
- 3. Built-in defaults (all agents enabled, standard output paths)
210
+ 1. **CLI flags** (e.g., `--agents`, `--no-mcp`, `--mcp-overwrite`, `--no-gitignore`)
211
+ 2. **Settings in `ruler.toml`** (`default_agents`, specific agent settings, global sections)
212
+ 3. **Ruler's built-in defaults** (all agents enabled, standard output paths, MCP enabled with 'merge')
101
213
 
102
- ## MCP servers
214
+ ## MCP (Model Context Protocol) Server Configuration
103
215
 
104
- Ruler can propagate a project-level `.ruler/mcp.json` file to native MCP configurations of supported agents, merging (or overwriting) each agent’s existing MCP server settings.
216
+ MCP provides broader context to AI models through server configurations. Ruler can manage and distribute these settings across compatible agents.
105
217
 
106
218
  ### `.ruler/mcp.json`
107
-
108
- Place your MCP servers config in a file at `.ruler/mcp.json`:
109
-
219
+ Define your project's MCP servers:
110
220
  ```json
111
221
  {
112
222
  "mcpServers": {
113
- "example": {
114
- "url": "https://mcp.example.com"
223
+ "filesystem": {
224
+ "command": "npx",
225
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
226
+ },
227
+ "git": {
228
+ "command": "npx",
229
+ "args": ["-y", "@modelcontextprotocol/server-git", "--repository", "."]
115
230
  }
116
231
  }
117
232
  }
118
233
  ```
119
234
 
120
- ### CLI flags
235
+ Ruler uses this file with the `merge` (default) or `overwrite` strategy, controlled by `ruler.toml` or CLI flags.
121
236
 
122
- | Flag | Effect |
123
- |-------------------|--------------------------------------------------------------|
124
- | `--with-mcp` | Enable writing MCP configs for all agents (default) |
125
- | `--no-mcp` | Disable writing MCP configs |
126
- | `--mcp-overwrite` | Overwrite native MCP configs instead of merging |
237
+ ## `.gitignore` Integration
127
238
 
128
- ### Configuration (`ruler.toml`)
239
+ Ruler automatically manages your `.gitignore` file to keep generated agent configuration files out of version control.
129
240
 
130
- Configure default behavior in your `ruler.toml`:
241
+ ### How it Works
242
+ - Creates or updates `.gitignore` in your project root
243
+ - Adds paths to a managed block marked with `# START Ruler Generated Files` and `# END Ruler Generated Files`
244
+ - Preserves existing content outside this block
245
+ - Sorts paths alphabetically and uses relative POSIX-style paths
131
246
 
132
- ```toml
133
- [mcp]
134
- enabled = true
135
- merge_strategy = "merge" # or "overwrite"
247
+ ### Example `.gitignore` Section
248
+ ```gitignore
249
+ # Your existing rules
250
+ node_modules/
251
+ *.log
136
252
 
137
- [agents.Cursor.mcp]
138
- enabled = false
139
- merge_strategy = "overwrite"
253
+ # START Ruler Generated Files
254
+ .aider.conf.yml
255
+ .clinerules
256
+ .cursor/rules/ruler_cursor_instructions.md
257
+ .github/copilot-instructions.md
258
+ .windsurf/rules/ruler_windsurf_instructions.md
259
+ AGENTS.md
260
+ CLAUDE.md
261
+ ruler_aider_instructions.md
262
+ # END Ruler Generated Files
263
+
264
+ dist/
140
265
  ```
141
266
 
142
- ## .gitignore Integration
267
+ ### Control Options
268
+ - **CLI flags**: `--gitignore` or `--no-gitignore`
269
+ - **Configuration**: `[gitignore].enabled` in `ruler.toml`
270
+ - **Default**: enabled
143
271
 
144
- Ruler automatically adds generated agent configuration files to your project's `.gitignore` file to prevent them from being committed to version control. This ensures that the AI agent configuration files remain local to each developer's environment.
272
+ ## Practical Usage Scenarios
145
273
 
146
- ### Behavior
274
+ ### Scenario 1: Getting Started Quickly
275
+ ```bash
276
+ # Initialize Ruler in your project
277
+ cd your-project
278
+ ruler init
147
279
 
148
- When `ruler apply` runs, it will:
149
- - Create or update a `.gitignore` file in your project root
150
- - Add all generated file paths to a managed block marked with `# START Ruler Generated Files` and `# END Ruler Generated Files`
151
- - Preserve any existing `.gitignore` content outside the managed block
152
- - Sort paths alphabetically within the Ruler block
153
- - Use relative POSIX-style paths (forward slashes)
280
+ # Edit the generated files
281
+ # - Add your coding guidelines to .ruler/instructions.md
282
+ # - Customize .ruler/ruler.toml if needed
154
283
 
155
- ### CLI flags
284
+ # Apply rules to all AI agents
285
+ ruler apply
286
+ ```
156
287
 
157
- | Flag | Effect |
158
- |-------------------|--------------------------------------------------------------|
159
- | `--gitignore` | Enable automatic .gitignore updates (default) |
160
- | `--no-gitignore` | Disable automatic .gitignore updates |
288
+ ### Scenario 2: Team Standardization
289
+ 1. Create `.ruler/coding_standards.md`, `.ruler/api_usage.md`
290
+ 2. Commit the `.ruler` directory to your repository
291
+ 3. Team members pull changes and run `ruler apply` to update their local AI agent configurations
161
292
 
162
- ### Configuration (`ruler.toml`)
293
+ ### Scenario 3: Project-Specific Context for AI
294
+ 1. Detail your project's architecture in `.ruler/project_overview.md`
295
+ 2. Describe primary data structures in `.ruler/data_models.md`
296
+ 3. Run `ruler apply` to help AI tools provide more relevant suggestions
163
297
 
164
- Configure the default behavior in your `ruler.toml`:
298
+ ### Integration with NPM Scripts
299
+ ```json
300
+ {
301
+ "scripts": {
302
+ "ruler:apply": "ruler apply",
303
+ "dev": "npm run ruler:apply && your_dev_command",
304
+ "precommit": "npm run ruler:apply"
305
+ }
306
+ }
307
+ ```
165
308
 
166
- ```toml
167
- [gitignore]
168
- enabled = true # or false to disable by default
309
+ ### Integration with GitHub Actions
310
+ ```yaml
311
+ # .github/workflows/ruler-check.yml
312
+ name: Check Ruler Configuration
313
+ on:
314
+ pull_request:
315
+ paths: ['.ruler/**']
316
+
317
+ jobs:
318
+ check-ruler:
319
+ runs-on: ubuntu-latest
320
+ steps:
321
+ - uses: actions/checkout@v4
322
+ - uses: actions/setup-node@v4
323
+ with:
324
+ node-version: '18'
325
+ cache: 'npm'
326
+
327
+ - name: Install Ruler
328
+ run: npm install -g @intellectronica/ruler
329
+
330
+ - name: Apply Ruler configuration
331
+ run: ruler apply --no-gitignore
332
+
333
+ - name: Check for uncommitted changes
334
+ run: |
335
+ if [[ -n $(git status --porcelain) ]]; then
336
+ echo "::error::Ruler configuration is out of sync!"
337
+ echo "Please run 'ruler apply' locally and commit the changes."
338
+ exit 1
339
+ fi
169
340
  ```
170
341
 
171
- ### Precedence
342
+ ## Troubleshooting
172
343
 
173
- The configuration precedence for .gitignore updates is:
344
+ ### Common Issues
174
345
 
175
- 1. CLI flags (`--gitignore` or `--no-gitignore`)
176
- 2. Configuration file `[gitignore].enabled` setting
177
- 3. Default behavior (enabled)
346
+ **"Cannot find module" errors:**
347
+ - Ensure Ruler is installed globally: `npm install -g @intellectronica/ruler`
348
+ - Or use `npx @intellectronica/ruler`
178
349
 
179
- ### Example
350
+ **Permission denied errors:**
351
+ - On Unix systems, you may need `sudo` for global installation
180
352
 
181
- After running `ruler apply`, your `.gitignore` might look like:
353
+ **Agent files not updating:**
354
+ - Check if the agent is enabled in `ruler.toml`
355
+ - Verify agent isn't excluded by `--agents` flag
356
+ - Use `--verbose` to see detailed execution logs
182
357
 
183
- ```gitignore
184
- node_modules/
185
- *.log
358
+ **Configuration validation errors:**
359
+ - Ruler now validates `ruler.toml` format and will show specific error details
360
+ - Check that all configuration values match the expected types and formats
186
361
 
187
- # START Ruler Generated Files
188
- .aider.conf.yml
189
- .clinerules
190
- .cursor/rules/ruler_cursor_instructions.md
191
- .github/copilot-instructions.md
192
- .windsurf/rules/ruler_windsurf_instructions.md
193
- AGENTS.md
194
- CLAUDE.md
195
- ruler_aider_instructions.md
196
- # END Ruler Generated Files
197
-
198
- dist/
362
+ ### Debug Mode
363
+ Use `--verbose` flag to see detailed execution logs:
364
+ ```bash
365
+ ruler apply --verbose
199
366
  ```
200
367
 
201
- ## Development
368
+ This shows:
369
+ - Configuration loading details
370
+ - Agent selection logic
371
+ - File processing information
372
+ - MCP configuration steps
373
+
374
+ ## FAQ
375
+
376
+ **Q: Can I use different rules for different agents?**
377
+ A: Currently, all agents receive the same concatenated rules. For agent-specific instructions, include sections in your rule files like "## GitHub Copilot Specific" or "## Aider Configuration".
202
378
 
203
- Clone the repository and install dependencies:
379
+ **Q: How do I temporarily disable Ruler for an agent?**
380
+ A: Set `enabled = false` in `ruler.toml` under `[agents.agentname]`, or use `--agents` flag to specify only the agents you want.
204
381
 
382
+ **Q: What happens to my existing agent configuration files?**
383
+ A: Ruler creates backups with `.bak` extension before overwriting any existing files.
384
+
385
+ **Q: Can I run Ruler in CI/CD pipelines?**
386
+ A: Yes! Use `ruler apply --no-gitignore` in CI to avoid modifying `.gitignore`. See the GitHub Actions example above.
387
+
388
+ **Q: How do I migrate from version 0.1.x to 0.2.0?**
389
+ A: Version 0.2.0 is backward compatible. Your existing `.ruler/` directory and `ruler.toml` will continue to work. New features like verbose logging and improved error messages are opt-in.
390
+
391
+ ## Development
392
+
393
+ ### Setup
205
394
  ```bash
206
395
  git clone https://github.com/intellectronica/ruler.git
207
396
  cd ruler
@@ -209,28 +398,39 @@ npm install
209
398
  npm run build
210
399
  ```
211
400
 
212
- Run linting and formatting checks:
213
-
401
+ ### Testing
214
402
  ```bash
215
- npm run lint
216
- npm run format
217
- ```
403
+ # Run all tests
404
+ npm test
218
405
 
219
- Run tests:
406
+ # Run tests with coverage
407
+ npm run test:coverage
220
408
 
221
- ```bash
222
- npm test
409
+ # Run tests in watch mode
410
+ npm run test:watch
223
411
  ```
224
412
 
225
- End-to-end tests (run build before tests):
226
-
413
+ ### Code Quality
227
414
  ```bash
228
- npm run build && npm test
415
+ # Run linting
416
+ npm run lint
417
+
418
+ # Run formatting
419
+ npm run format
229
420
  ```
230
421
 
231
422
  ## Contributing
232
423
 
233
- Contributions are welcome! Please open issues or pull requests on GitHub.
424
+ Contributions are welcome! Please:
425
+
426
+ 1. Fork the repository
427
+ 2. Create a feature branch
428
+ 3. Make your changes
429
+ 4. Add tests for new functionality
430
+ 5. Ensure all tests pass
431
+ 6. Submit a pull request
432
+
433
+ For bugs and feature requests, please [open an issue](https://github.com/intellectronica/ruler/issues).
234
434
 
235
435
  ## License
236
436
 
@@ -238,6 +438,5 @@ MIT
238
438
 
239
439
  ---
240
440
 
241
- © Eleanor Berger
242
-
243
- [ai.intellectronica.net](https://ai.intellectronica.net/)
441
+ © Eleanor Berger
442
+ [ai.intellectronica.net](https://ai.intellectronica.net/)
@@ -42,6 +42,9 @@ const yaml = __importStar(require("js-yaml"));
42
42
  * Aider agent adapter (stub implementation).
43
43
  */
44
44
  class AiderAgent {
45
+ getIdentifier() {
46
+ return 'aider';
47
+ }
45
48
  getName() {
46
49
  return 'Aider';
47
50
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * Claude Code agent adapter (stub implementation).
41
41
  */
42
42
  class ClaudeAgent {
43
+ getIdentifier() {
44
+ return 'claude';
45
+ }
43
46
  getName() {
44
47
  return 'Claude Code';
45
48
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * Cline agent adapter (stub implementation).
41
41
  */
42
42
  class ClineAgent {
43
+ getIdentifier() {
44
+ return 'cline';
45
+ }
43
46
  getName() {
44
47
  return 'Cline';
45
48
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * OpenAI Codex CLI agent adapter (stub implementation).
41
41
  */
42
42
  class CodexCliAgent {
43
+ getIdentifier() {
44
+ return 'codex';
45
+ }
43
46
  getName() {
44
47
  return 'OpenAI Codex CLI';
45
48
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * GitHub Copilot agent adapter (stub implementation).
41
41
  */
42
42
  class CopilotAgent {
43
+ getIdentifier() {
44
+ return 'copilot';
45
+ }
43
46
  getName() {
44
47
  return 'GitHub Copilot';
45
48
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * Cursor agent adapter (stub implementation).
41
41
  */
42
42
  class CursorAgent {
43
+ getIdentifier() {
44
+ return 'cursor';
45
+ }
43
46
  getName() {
44
47
  return 'Cursor';
45
48
  }
@@ -40,6 +40,9 @@ const FileSystemUtils_1 = require("../core/FileSystemUtils");
40
40
  * Windsurf agent adapter (stub implementation).
41
41
  */
42
42
  class WindsurfAgent {
43
+ getIdentifier() {
44
+ return 'windsurf';
45
+ }
43
46
  getName() {
44
47
  return 'Windsurf';
45
48
  }
@@ -42,6 +42,7 @@ const helpers_1 = require("yargs/helpers");
42
42
  const lib_1 = require("../lib");
43
43
  const path = __importStar(require("path"));
44
44
  const fs_1 = require("fs");
45
+ const constants_1 = require("../constants");
45
46
  /**
46
47
  * Sets up and parses CLI commands.
47
48
  */
@@ -57,7 +58,7 @@ function run() {
57
58
  });
58
59
  y.option('agents', {
59
60
  type: 'string',
60
- description: 'Comma-separated list of agent names to include (e.g. "copilot,claude")',
61
+ description: 'Comma-separated list of agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider',
61
62
  });
62
63
  y.option('config', {
63
64
  type: 'string',
@@ -78,6 +79,17 @@ function run() {
78
79
  type: 'boolean',
79
80
  description: 'Enable/disable automatic .gitignore updates (default: enabled)',
80
81
  });
82
+ y.option('verbose', {
83
+ type: 'boolean',
84
+ description: 'Enable verbose logging',
85
+ default: false,
86
+ });
87
+ y.alias('verbose', 'v');
88
+ y.option('dry-run', {
89
+ type: 'boolean',
90
+ description: 'Preview changes without writing files',
91
+ default: false,
92
+ });
81
93
  }, async (argv) => {
82
94
  const projectRoot = argv['project-root'];
83
95
  const agents = argv.agents
@@ -88,6 +100,8 @@ function run() {
88
100
  const mcpStrategy = argv['mcp-overwrite']
89
101
  ? 'overwrite'
90
102
  : undefined;
103
+ const verbose = argv.verbose;
104
+ const dryRun = argv['dry-run'];
91
105
  // Determine gitignore preference: CLI > TOML > Default (enabled)
92
106
  // yargs handles --no-gitignore by setting gitignore to false
93
107
  let gitignorePreference;
@@ -98,12 +112,12 @@ function run() {
98
112
  gitignorePreference = undefined; // Let TOML/default decide
99
113
  }
100
114
  try {
101
- await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference);
115
+ await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference, verbose, dryRun);
102
116
  console.log('Ruler apply completed successfully.');
103
117
  }
104
118
  catch (err) {
105
119
  const message = err instanceof Error ? err.message : String(err);
106
- console.error('Error applying ruler configurations:', message);
120
+ console.error(`${constants_1.ERROR_PREFIX} ${message}`);
107
121
  process.exit(1);
108
122
  }
109
123
  })
@@ -141,36 +155,37 @@ and apply them to your configured AI coding agents.
141
155
 
142
156
  # To specify which agents are active by default when --agents is not used,
143
157
  # uncomment and populate the following line. If omitted, all agents are active.
144
- # default_agents = ["Copilot", "Claude"]
158
+ # default_agents = ["copilot", "claude"]
145
159
 
146
160
  # --- Agent Specific Configurations ---
147
161
  # You can enable/disable agents and override their default output paths here.
162
+ # Use lowercase agent identifiers: copilot, claude, codex, cursor, windsurf, cline, aider
148
163
 
149
- # [agents.GitHubCopilot]
164
+ # [agents.copilot]
150
165
  # enabled = true
151
166
  # output_path = ".github/copilot-instructions.md"
152
167
 
153
- # [agents.ClaudeCode]
168
+ # [agents.claude]
154
169
  # enabled = true
155
170
  # output_path = "CLAUDE.md"
156
171
 
157
- # [agents.OpenAICodexCLI]
172
+ # [agents.codex]
158
173
  # enabled = true
159
174
  # output_path = "AGENTS.md"
160
175
 
161
- # [agents.Cursor]
176
+ # [agents.cursor]
162
177
  # enabled = true
163
178
  # output_path = ".cursor/rules/ruler_cursor_instructions.md"
164
179
 
165
- # [agents.Windsurf]
180
+ # [agents.windsurf]
166
181
  # enabled = true
167
182
  # output_path = ".windsurf/rules/ruler_windsurf_instructions.md"
168
183
 
169
- # [agents.Cline]
184
+ # [agents.cline]
170
185
  # enabled = true
171
186
  # output_path = ".clinerules"
172
187
 
173
- # [agents.Aider]
188
+ # [agents.aider]
174
189
  # enabled = true
175
190
  # output_path_instructions = "ruler_aider_instructions.md"
176
191
  # output_path_config = ".aider.conf.yml"
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ERROR_PREFIX = void 0;
4
+ exports.createRulerError = createRulerError;
5
+ exports.logVerbose = logVerbose;
6
+ exports.ERROR_PREFIX = '[RulerError]';
7
+ function createRulerError(message, context) {
8
+ const fullMessage = context
9
+ ? `${exports.ERROR_PREFIX} ${message} (Context: ${context})`
10
+ : `${exports.ERROR_PREFIX} ${message}`;
11
+ return new Error(fullMessage);
12
+ }
13
+ function logVerbose(message, isVerbose) {
14
+ if (isVerbose) {
15
+ console.log(`[ruler:verbose] ${message}`);
16
+ }
17
+ }
@@ -40,6 +40,38 @@ exports.loadConfig = loadConfig;
40
40
  const fs_1 = require("fs");
41
41
  const path = __importStar(require("path"));
42
42
  const toml_1 = __importDefault(require("toml"));
43
+ const zod_1 = require("zod");
44
+ const constants_1 = require("../constants");
45
+ const mcpConfigSchema = zod_1.z
46
+ .object({
47
+ enabled: zod_1.z.boolean().optional(),
48
+ merge_strategy: zod_1.z.enum(['merge', 'overwrite']).optional(),
49
+ })
50
+ .optional();
51
+ const agentConfigSchema = zod_1.z
52
+ .object({
53
+ enabled: zod_1.z.boolean().optional(),
54
+ output_path: zod_1.z.string().optional(),
55
+ output_path_instructions: zod_1.z.string().optional(),
56
+ output_path_config: zod_1.z.string().optional(),
57
+ mcp: mcpConfigSchema,
58
+ })
59
+ .optional();
60
+ const rulerConfigSchema = zod_1.z.object({
61
+ default_agents: zod_1.z.array(zod_1.z.string()).optional(),
62
+ agents: zod_1.z.record(zod_1.z.string(), agentConfigSchema).optional(),
63
+ mcp: zod_1.z
64
+ .object({
65
+ enabled: zod_1.z.boolean().optional(),
66
+ merge_strategy: zod_1.z.enum(['merge', 'overwrite']).optional(),
67
+ })
68
+ .optional(),
69
+ gitignore: zod_1.z
70
+ .object({
71
+ enabled: zod_1.z.boolean().optional(),
72
+ })
73
+ .optional(),
74
+ });
43
75
  /**
44
76
  * Loads and parses the ruler TOML configuration file, applying defaults.
45
77
  * If the file is missing or invalid, returns empty/default config.
@@ -53,9 +85,17 @@ async function loadConfig(options) {
53
85
  try {
54
86
  const text = await fs_1.promises.readFile(configFile, 'utf8');
55
87
  raw = text.trim() ? toml_1.default.parse(text) : {};
88
+ // Validate the configuration with zod
89
+ const validationResult = rulerConfigSchema.safeParse(raw);
90
+ if (!validationResult.success) {
91
+ throw (0, constants_1.createRulerError)('Invalid configuration file format', `File: ${configFile}, Errors: ${validationResult.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join(', ')}`);
92
+ }
56
93
  }
57
94
  catch (err) {
58
95
  if (err instanceof Error && err.code !== 'ENOENT') {
96
+ if (err.message.includes('[RulerError]')) {
97
+ throw err; // Re-throw validation errors
98
+ }
59
99
  console.warn(`[ruler] Warning: could not read config file at ${configFile}: ${err.message}`);
60
100
  }
61
101
  raw = {};
@@ -59,7 +59,24 @@ async function updateGitignore(projectRoot, paths) {
59
59
  }
60
60
  // Convert paths to relative POSIX format
61
61
  const relativePaths = paths.map((p) => {
62
- const relative = path.isAbsolute(p) ? path.relative(projectRoot, p) : p;
62
+ let relative;
63
+ if (path.isAbsolute(p)) {
64
+ relative = path.relative(projectRoot, p);
65
+ }
66
+ else {
67
+ // Handle relative paths that might include the project root prefix
68
+ const normalizedProjectRoot = path.normalize(projectRoot);
69
+ const normalizedPath = path.normalize(p);
70
+ // Get the basename of the project root to match against path prefixes
71
+ const projectBasename = path.basename(normalizedProjectRoot);
72
+ // If the path starts with the project basename, remove it
73
+ if (normalizedPath.startsWith(projectBasename + path.sep)) {
74
+ relative = normalizedPath.substring(projectBasename.length + 1);
75
+ }
76
+ else {
77
+ relative = normalizedPath;
78
+ }
79
+ }
63
80
  return relative.replace(/\\/g, '/'); // Convert to POSIX format
64
81
  });
65
82
  // Get all existing paths from .gitignore (excluding Ruler block)
package/dist/lib.js CHANGED
@@ -50,6 +50,7 @@ const AiderAgent_1 = require("./agents/AiderAgent");
50
50
  const merge_1 = require("./mcp/merge");
51
51
  const validate_1 = require("./mcp/validate");
52
52
  const mcp_1 = require("./paths/mcp");
53
+ const constants_1 = require("./constants");
53
54
  /**
54
55
  * Gets all output paths for an agent, taking into account any config overrides.
55
56
  */
@@ -102,74 +103,98 @@ const agents = [
102
103
  * @param projectRoot Root directory of the project
103
104
  * @param includedAgents Optional list of agent name filters (case-insensitive substrings)
104
105
  */
105
- async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled) {
106
+ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled, verbose = false, dryRun = false) {
106
107
  // Load configuration (default_agents, per-agent overrides, CLI filters)
108
+ (0, constants_1.logVerbose)(`Loading configuration from project root: ${projectRoot}`, verbose);
109
+ if (configPath) {
110
+ (0, constants_1.logVerbose)(`Using custom config path: ${configPath}`, verbose);
111
+ }
107
112
  const config = await (0, ConfigLoader_1.loadConfig)({
108
113
  projectRoot,
109
114
  cliAgents: includedAgents,
110
115
  configPath,
111
116
  });
112
- // Normalize per-agent config keys to actual agent names (substring match)
117
+ (0, constants_1.logVerbose)(`Loaded configuration with ${Object.keys(config.agentConfigs).length} agent configs`, verbose);
118
+ // Normalize per-agent config keys to agent identifiers (exact match or substring match)
113
119
  const rawConfigs = config.agentConfigs;
114
120
  const mappedConfigs = {};
115
121
  for (const [key, cfg] of Object.entries(rawConfigs)) {
116
122
  const lowerKey = key.toLowerCase();
117
123
  for (const agent of agents) {
118
- if (agent.getName().toLowerCase().includes(lowerKey)) {
119
- mappedConfigs[agent.getName()] = cfg;
124
+ const identifier = agent.getIdentifier();
125
+ // Exact match with identifier or substring match with display name for backwards compatibility
126
+ if (identifier === lowerKey ||
127
+ agent.getName().toLowerCase().includes(lowerKey)) {
128
+ mappedConfigs[identifier] = cfg;
120
129
  }
121
130
  }
122
131
  }
123
132
  config.agentConfigs = mappedConfigs;
124
133
  const rulerDir = await FileSystemUtils.findRulerDir(projectRoot);
125
134
  if (!rulerDir) {
126
- throw new Error(`.ruler directory not found from ${projectRoot}`);
135
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
127
136
  }
128
- await FileSystemUtils.ensureDirExists(path.join(rulerDir, 'generated'));
137
+ (0, constants_1.logVerbose)(`Found .ruler directory at: ${rulerDir}`, verbose);
129
138
  const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
139
+ (0, constants_1.logVerbose)(`Found ${files.length} markdown files in .ruler directory`, verbose);
130
140
  const concatenated = (0, RuleProcessor_1.concatenateRules)(files);
141
+ (0, constants_1.logVerbose)(`Concatenated rules length: ${concatenated.length} characters`, verbose);
131
142
  const mcpFile = path.join(rulerDir, 'mcp.json');
132
143
  let rulerMcpJson = null;
133
144
  try {
134
145
  const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
135
146
  rulerMcpJson = JSON.parse(raw);
136
147
  (0, validate_1.validateMcp)(rulerMcpJson);
148
+ (0, constants_1.logVerbose)(`Loaded MCP configuration from: ${mcpFile}`, verbose);
137
149
  }
138
150
  catch (err) {
139
151
  if (err.code !== 'ENOENT') {
140
- throw err;
152
+ throw (0, constants_1.createRulerError)(`Failed to load MCP configuration`, `File: ${mcpFile}, Error: ${err.message}`);
141
153
  }
154
+ (0, constants_1.logVerbose)(`No MCP configuration found at: ${mcpFile}`, verbose);
142
155
  }
143
156
  // Determine which agents to run:
144
157
  // CLI --agents > config.default_agents > per-agent.enabled flags > default all
145
158
  let selected = agents;
146
159
  if (config.cliAgents && config.cliAgents.length > 0) {
147
160
  const filters = config.cliAgents.map((n) => n.toLowerCase());
148
- selected = agents.filter((agent) => filters.some((f) => agent.getName().toLowerCase().includes(f)));
161
+ selected = agents.filter((agent) => filters.some((f) => agent.getIdentifier() === f ||
162
+ agent.getName().toLowerCase().includes(f)));
163
+ (0, constants_1.logVerbose)(`Selected agents via CLI filter: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
149
164
  }
150
165
  else if (config.defaultAgents && config.defaultAgents.length > 0) {
151
166
  const defaults = config.defaultAgents.map((n) => n.toLowerCase());
152
167
  selected = agents.filter((agent) => {
153
- const key = agent.getName();
154
- const override = config.agentConfigs[key]?.enabled;
168
+ const identifier = agent.getIdentifier();
169
+ const override = config.agentConfigs[identifier]?.enabled;
155
170
  if (override !== undefined) {
156
171
  return override;
157
172
  }
158
- return defaults.includes(key.toLowerCase());
173
+ return defaults.some((d) => identifier === d || agent.getName().toLowerCase().includes(d));
159
174
  });
175
+ (0, constants_1.logVerbose)(`Selected agents via config default_agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
160
176
  }
161
177
  else {
162
- selected = agents.filter((agent) => config.agentConfigs[agent.getName()]?.enabled !== false);
178
+ selected = agents.filter((agent) => config.agentConfigs[agent.getIdentifier()]?.enabled !== false);
179
+ (0, constants_1.logVerbose)(`Selected all enabled agents: ${selected.map((a) => a.getName()).join(', ')}`, verbose);
163
180
  }
164
181
  // Collect all generated file paths for .gitignore
165
182
  const generatedPaths = [];
166
183
  for (const agent of selected) {
167
- console.log(`[ruler] Applying rules for ${agent.getName()}...`);
168
- const agentConfig = config.agentConfigs[agent.getName()];
169
- await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
184
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
185
+ console.log(`${actionPrefix} Applying rules for ${agent.getName()}...`);
186
+ (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
187
+ const agentConfig = config.agentConfigs[agent.getIdentifier()];
170
188
  // Collect output paths for .gitignore
171
189
  const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
190
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
172
191
  generatedPaths.push(...outputPaths);
192
+ if (dryRun) {
193
+ (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, true);
194
+ }
195
+ else {
196
+ await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
197
+ }
173
198
  const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
174
199
  const enabled = cliMcpEnabled &&
175
200
  (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
@@ -178,9 +203,15 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
178
203
  agentConfig?.mcp?.strategy ??
179
204
  config.mcp?.strategy ??
180
205
  'merge';
181
- const existing = await (0, mcp_1.readNativeMcp)(dest);
182
- const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
183
- await (0, mcp_1.writeNativeMcp)(dest, merged);
206
+ (0, constants_1.logVerbose)(`Applying MCP config for ${agent.getName()} with strategy: ${strategy}`, verbose);
207
+ if (dryRun) {
208
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, true);
209
+ }
210
+ else {
211
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
212
+ const merged = (0, merge_1.mergeMcp)(existing, rulerMcpJson, strategy);
213
+ await (0, mcp_1.writeNativeMcp)(dest, merged);
214
+ }
184
215
  }
185
216
  }
186
217
  // Handle .gitignore updates
@@ -200,8 +231,14 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
200
231
  const pathsToIgnore = generatedPaths.filter((p) => !p.endsWith('.bak'));
201
232
  const uniquePaths = [...new Set(pathsToIgnore)];
202
233
  if (uniquePaths.length > 0) {
203
- await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths);
204
- console.log(`[ruler] Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`);
234
+ const actionPrefix = dryRun ? '[ruler:dry-run]' : '[ruler]';
235
+ if (dryRun) {
236
+ console.log(`${actionPrefix} Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`);
237
+ }
238
+ else {
239
+ await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths);
240
+ console.log(`${actionPrefix} Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`);
241
+ }
205
242
  }
206
243
  }
207
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@
8
8
  "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
9
9
  "test": "jest",
10
10
  "test:watch": "jest --watch",
11
+ "test:coverage": "jest --coverage",
11
12
  "build": "tsc",
12
13
  "prepare": "npm run build"
13
14
  },
@@ -58,6 +59,7 @@
58
59
  "dependencies": {
59
60
  "js-yaml": "^4.1.0",
60
61
  "toml": "^3.0.0",
61
- "yargs": "^17.7.2"
62
+ "yargs": "^17.7.2",
63
+ "zod": "^3.25.28"
62
64
  }
63
65
  }