@intellectronica/ruler 0.1.1 → 0.1.3
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 +147 -8
- package/dist/agents/AiderAgent.js +16 -7
- package/dist/agents/ClaudeAgent.js +7 -4
- package/dist/agents/ClineAgent.js +7 -4
- package/dist/agents/CodexCliAgent.js +7 -4
- package/dist/agents/CopilotAgent.js +8 -6
- package/dist/agents/CursorAgent.js +8 -6
- package/dist/agents/WindsurfAgent.js +8 -6
- package/dist/cli/commands.js +167 -1
- package/dist/core/ConfigLoader.js +132 -0
- package/dist/core/GitignoreUtils.js +145 -0
- package/dist/lib.js +133 -8
- package/dist/mcp/merge.js +21 -0
- package/dist/mcp/validate.js +17 -0
- package/dist/paths/mcp.js +100 -0
- package/dist/types.js +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
|
4
|
+
|
|
1
5
|
# Ruler
|
|
2
6
|
|
|
3
7
|
A CLI tool to manage custom rules and configs across different AI coding agents.
|
|
@@ -35,7 +39,14 @@ Create a `.ruler/` directory at your project root and add Markdown files definin
|
|
|
35
39
|
Run the apply command:
|
|
36
40
|
|
|
37
41
|
```bash
|
|
38
|
-
ruler apply [--project-root <path>] [--agents <agent1,agent2,...>]
|
|
42
|
+
ruler apply [--project-root <path>] [--agents <agent1,agent2,...>] [--config <path>] [--gitignore] [--no-gitignore]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Run the init command to scaffold a basic `.ruler/` setup:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ruler init [--project-root <path>]
|
|
39
50
|
```
|
|
40
51
|
|
|
41
52
|
Use `--agents` to specify a comma-separated list of agent names (case-insensitive substrings) to limit which agents the rules are applied to.
|
|
@@ -52,6 +63,141 @@ The command will read all `.md` files under `.ruler/`, concatenate their content
|
|
|
52
63
|
| Cline | `.clinerules` |
|
|
53
64
|
| Aider | `ruler_aider_instructions.md` <br>and updates `.aider.conf.yml` |
|
|
54
65
|
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
### Configuration structure
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
# Run only these agents by default (omit to use all agents)
|
|
74
|
+
# default_agents = ["GitHub Copilot", "Claude Code", "Aider"]
|
|
75
|
+
|
|
76
|
+
[agents.Copilot]
|
|
77
|
+
enabled = true
|
|
78
|
+
output_path = ".github/copilot-instructions.md"
|
|
79
|
+
|
|
80
|
+
[agents.Claude]
|
|
81
|
+
enabled = true
|
|
82
|
+
# output_path = "CLAUDE.md"
|
|
83
|
+
|
|
84
|
+
[agents.Aider]
|
|
85
|
+
enabled = false
|
|
86
|
+
# output_path_instructions = "ruler_aider_instructions.md"
|
|
87
|
+
# output_path_config = ".aider.conf.yml"
|
|
88
|
+
```
|
|
89
|
+
|
|
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
|
|
97
|
+
|
|
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)
|
|
101
|
+
|
|
102
|
+
## MCP servers
|
|
103
|
+
|
|
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.
|
|
105
|
+
|
|
106
|
+
### `.ruler/mcp.json`
|
|
107
|
+
|
|
108
|
+
Place your MCP servers config in a file at `.ruler/mcp.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"example": {
|
|
114
|
+
"url": "https://mcp.example.com"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### CLI flags
|
|
121
|
+
|
|
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 |
|
|
127
|
+
|
|
128
|
+
### Configuration (`ruler.toml`)
|
|
129
|
+
|
|
130
|
+
Configure default behavior in your `ruler.toml`:
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[mcp]
|
|
134
|
+
enabled = true
|
|
135
|
+
merge_strategy = "merge" # or "overwrite"
|
|
136
|
+
|
|
137
|
+
[agents.Cursor.mcp]
|
|
138
|
+
enabled = false
|
|
139
|
+
merge_strategy = "overwrite"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## .gitignore Integration
|
|
143
|
+
|
|
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.
|
|
145
|
+
|
|
146
|
+
### Behavior
|
|
147
|
+
|
|
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)
|
|
154
|
+
|
|
155
|
+
### CLI flags
|
|
156
|
+
|
|
157
|
+
| Flag | Effect |
|
|
158
|
+
|-------------------|--------------------------------------------------------------|
|
|
159
|
+
| `--gitignore` | Enable automatic .gitignore updates (default) |
|
|
160
|
+
| `--no-gitignore` | Disable automatic .gitignore updates |
|
|
161
|
+
|
|
162
|
+
### Configuration (`ruler.toml`)
|
|
163
|
+
|
|
164
|
+
Configure the default behavior in your `ruler.toml`:
|
|
165
|
+
|
|
166
|
+
```toml
|
|
167
|
+
[gitignore]
|
|
168
|
+
enabled = true # or false to disable by default
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Precedence
|
|
172
|
+
|
|
173
|
+
The configuration precedence for .gitignore updates is:
|
|
174
|
+
|
|
175
|
+
1. CLI flags (`--gitignore` or `--no-gitignore`)
|
|
176
|
+
2. Configuration file `[gitignore].enabled` setting
|
|
177
|
+
3. Default behavior (enabled)
|
|
178
|
+
|
|
179
|
+
### Example
|
|
180
|
+
|
|
181
|
+
After running `ruler apply`, your `.gitignore` might look like:
|
|
182
|
+
|
|
183
|
+
```gitignore
|
|
184
|
+
node_modules/
|
|
185
|
+
*.log
|
|
186
|
+
|
|
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/
|
|
199
|
+
```
|
|
200
|
+
|
|
55
201
|
## Development
|
|
56
202
|
|
|
57
203
|
Clone the repository and install dependencies:
|
|
@@ -82,13 +228,6 @@ End-to-end tests (run build before tests):
|
|
|
82
228
|
npm run build && npm test
|
|
83
229
|
```
|
|
84
230
|
|
|
85
|
-
### Roadmap
|
|
86
|
-
- [ ] Support for MCP servers config
|
|
87
|
-
- [ ] Support for transforming and rewriting the rules using AI
|
|
88
|
-
- [ ] Support "harmonisation" (reading existing rules of specific agents and combining them with the master config)
|
|
89
|
-
- [ ] Support for additional agents
|
|
90
|
-
- [ ] Support for agent-specific features (for example: apply rules in copilot)
|
|
91
|
-
|
|
92
231
|
## Contributing
|
|
93
232
|
|
|
94
233
|
Contributions are welcome! Please open issues or pull requests on GitHub.
|
|
@@ -45,11 +45,13 @@ class AiderAgent {
|
|
|
45
45
|
getName() {
|
|
46
46
|
return 'Aider';
|
|
47
47
|
}
|
|
48
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
await (0, FileSystemUtils_1.
|
|
52
|
-
|
|
48
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
49
|
+
const mdPath = agentConfig?.outputPathInstructions ??
|
|
50
|
+
this.getDefaultOutputPath(projectRoot).instructions;
|
|
51
|
+
await (0, FileSystemUtils_1.backupFile)(mdPath);
|
|
52
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(mdPath, concatenatedRules);
|
|
53
|
+
const cfgPath = agentConfig?.outputPathConfig ??
|
|
54
|
+
this.getDefaultOutputPath(projectRoot).config;
|
|
53
55
|
let doc = {};
|
|
54
56
|
try {
|
|
55
57
|
await fs.access(cfgPath);
|
|
@@ -63,11 +65,18 @@ class AiderAgent {
|
|
|
63
65
|
if (!Array.isArray(doc.read)) {
|
|
64
66
|
doc.read = [];
|
|
65
67
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const name = path.basename(mdPath);
|
|
69
|
+
if (!doc.read.includes(name)) {
|
|
70
|
+
doc.read.push(name);
|
|
68
71
|
}
|
|
69
72
|
const yamlStr = yaml.dump(doc);
|
|
70
73
|
await (0, FileSystemUtils_1.writeGeneratedFile)(cfgPath, yamlStr);
|
|
71
74
|
}
|
|
75
|
+
getDefaultOutputPath(projectRoot) {
|
|
76
|
+
return {
|
|
77
|
+
instructions: path.join(projectRoot, 'ruler_aider_instructions.md'),
|
|
78
|
+
config: path.join(projectRoot, '.aider.conf.yml'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
72
81
|
}
|
|
73
82
|
exports.AiderAgent = AiderAgent;
|
|
@@ -43,10 +43,13 @@ class ClaudeAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'Claude Code';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.backupFile)(
|
|
49
|
-
await (0, FileSystemUtils_1.writeGeneratedFile)(
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
49
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
50
|
+
}
|
|
51
|
+
getDefaultOutputPath(projectRoot) {
|
|
52
|
+
return path.join(projectRoot, 'CLAUDE.md');
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
exports.ClaudeAgent = ClaudeAgent;
|
|
@@ -43,10 +43,13 @@ class ClineAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'Cline';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.backupFile)(
|
|
49
|
-
await (0, FileSystemUtils_1.writeGeneratedFile)(
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
49
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
50
|
+
}
|
|
51
|
+
getDefaultOutputPath(projectRoot) {
|
|
52
|
+
return path.join(projectRoot, '.clinerules');
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
exports.ClineAgent = ClineAgent;
|
|
@@ -43,10 +43,13 @@ class CodexCliAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'OpenAI Codex CLI';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.backupFile)(
|
|
49
|
-
await (0, FileSystemUtils_1.writeGeneratedFile)(
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
49
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
50
|
+
}
|
|
51
|
+
getDefaultOutputPath(projectRoot) {
|
|
52
|
+
return path.join(projectRoot, 'AGENTS.md');
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
exports.CodexCliAgent = CodexCliAgent;
|
|
@@ -43,12 +43,14 @@ class CopilotAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'GitHub Copilot';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.ensureDirExists)(
|
|
49
|
-
|
|
50
|
-
await (0, FileSystemUtils_1.
|
|
51
|
-
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
|
|
49
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
50
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
51
|
+
}
|
|
52
|
+
getDefaultOutputPath(projectRoot) {
|
|
53
|
+
return path.join(projectRoot, '.github', 'copilot-instructions.md');
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
exports.CopilotAgent = CopilotAgent;
|
|
@@ -43,12 +43,14 @@ class CursorAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'Cursor';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.ensureDirExists)(
|
|
49
|
-
|
|
50
|
-
await (0, FileSystemUtils_1.
|
|
51
|
-
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
|
|
49
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
50
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
51
|
+
}
|
|
52
|
+
getDefaultOutputPath(projectRoot) {
|
|
53
|
+
return path.join(projectRoot, '.cursor', 'rules', 'ruler_cursor_instructions.md');
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
exports.CursorAgent = CursorAgent;
|
|
@@ -43,12 +43,14 @@ class WindsurfAgent {
|
|
|
43
43
|
getName() {
|
|
44
44
|
return 'Windsurf';
|
|
45
45
|
}
|
|
46
|
-
async applyRulerConfig(concatenatedRules, projectRoot) {
|
|
47
|
-
const
|
|
48
|
-
await (0, FileSystemUtils_1.ensureDirExists)(
|
|
49
|
-
|
|
50
|
-
await (0, FileSystemUtils_1.
|
|
51
|
-
|
|
46
|
+
async applyRulerConfig(concatenatedRules, projectRoot, agentConfig) {
|
|
47
|
+
const output = agentConfig?.outputPath ?? this.getDefaultOutputPath(projectRoot);
|
|
48
|
+
await (0, FileSystemUtils_1.ensureDirExists)(path.dirname(output));
|
|
49
|
+
await (0, FileSystemUtils_1.backupFile)(output);
|
|
50
|
+
await (0, FileSystemUtils_1.writeGeneratedFile)(output, concatenatedRules);
|
|
51
|
+
}
|
|
52
|
+
getDefaultOutputPath(projectRoot) {
|
|
53
|
+
return path.join(projectRoot, '.windsurf', 'rules', 'ruler_windsurf_instructions.md');
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
exports.WindsurfAgent = WindsurfAgent;
|
package/dist/cli/commands.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -7,6 +40,8 @@ exports.run = run;
|
|
|
7
40
|
const yargs_1 = __importDefault(require("yargs"));
|
|
8
41
|
const helpers_1 = require("yargs/helpers");
|
|
9
42
|
const lib_1 = require("../lib");
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const fs_1 = require("fs");
|
|
10
45
|
/**
|
|
11
46
|
* Sets up and parses CLI commands.
|
|
12
47
|
*/
|
|
@@ -24,13 +59,46 @@ function run() {
|
|
|
24
59
|
type: 'string',
|
|
25
60
|
description: 'Comma-separated list of agent names to include (e.g. "copilot,claude")',
|
|
26
61
|
});
|
|
62
|
+
y.option('config', {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Path to TOML configuration file',
|
|
65
|
+
});
|
|
66
|
+
y.option('mcp', {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
description: 'Enable or disable applying MCP server config',
|
|
69
|
+
default: true,
|
|
70
|
+
});
|
|
71
|
+
y.alias('mcp', 'with-mcp');
|
|
72
|
+
y.option('mcp-overwrite', {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'Replace (not merge) the native MCP config(s)',
|
|
75
|
+
default: false,
|
|
76
|
+
});
|
|
77
|
+
y.option('gitignore', {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Enable/disable automatic .gitignore updates (default: enabled)',
|
|
80
|
+
});
|
|
27
81
|
}, async (argv) => {
|
|
28
82
|
const projectRoot = argv['project-root'];
|
|
29
83
|
const agents = argv.agents
|
|
30
84
|
? argv.agents.split(',').map((a) => a.trim())
|
|
31
85
|
: undefined;
|
|
86
|
+
const configPath = argv.config;
|
|
87
|
+
const mcpEnabled = argv.mcp;
|
|
88
|
+
const mcpStrategy = argv['mcp-overwrite']
|
|
89
|
+
? 'overwrite'
|
|
90
|
+
: undefined;
|
|
91
|
+
// Determine gitignore preference: CLI > TOML > Default (enabled)
|
|
92
|
+
// yargs handles --no-gitignore by setting gitignore to false
|
|
93
|
+
let gitignorePreference;
|
|
94
|
+
if (argv.gitignore !== undefined) {
|
|
95
|
+
gitignorePreference = argv.gitignore;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
gitignorePreference = undefined; // Let TOML/default decide
|
|
99
|
+
}
|
|
32
100
|
try {
|
|
33
|
-
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents);
|
|
101
|
+
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference);
|
|
34
102
|
console.log('Ruler apply completed successfully.');
|
|
35
103
|
}
|
|
36
104
|
catch (err) {
|
|
@@ -38,6 +106,104 @@ function run() {
|
|
|
38
106
|
console.error('Error applying ruler configurations:', message);
|
|
39
107
|
process.exit(1);
|
|
40
108
|
}
|
|
109
|
+
})
|
|
110
|
+
.command('init', 'Scaffold a .ruler directory with default files', (y) => {
|
|
111
|
+
y.option('project-root', {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Project root directory',
|
|
114
|
+
default: process.cwd(),
|
|
115
|
+
});
|
|
116
|
+
}, async (argv) => {
|
|
117
|
+
const projectRoot = argv['project-root'];
|
|
118
|
+
const rulerDir = path.join(projectRoot, '.ruler');
|
|
119
|
+
await fs_1.promises.mkdir(rulerDir, { recursive: true });
|
|
120
|
+
const instructionsPath = path.join(rulerDir, 'instructions.md');
|
|
121
|
+
const tomlPath = path.join(rulerDir, 'ruler.toml');
|
|
122
|
+
const exists = async (p) => {
|
|
123
|
+
try {
|
|
124
|
+
await fs_1.promises.access(p);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const DEFAULT_INSTRUCTIONS = `# Ruler Instructions
|
|
132
|
+
|
|
133
|
+
These are your centralised AI agent instructions.
|
|
134
|
+
Add your coding guidelines, style guides, and other project-specific context here.
|
|
135
|
+
|
|
136
|
+
Ruler will concatenate all .md files in this directory (and its subdirectories)
|
|
137
|
+
and apply them to your configured AI coding agents.
|
|
138
|
+
`;
|
|
139
|
+
const DEFAULT_TOML = `# Ruler Configuration File
|
|
140
|
+
# See https://ai.intellectronica.net/ruler for documentation.
|
|
141
|
+
|
|
142
|
+
# To specify which agents are active by default when --agents is not used,
|
|
143
|
+
# uncomment and populate the following line. If omitted, all agents are active.
|
|
144
|
+
# default_agents = ["Copilot", "Claude"]
|
|
145
|
+
|
|
146
|
+
# --- Agent Specific Configurations ---
|
|
147
|
+
# You can enable/disable agents and override their default output paths here.
|
|
148
|
+
|
|
149
|
+
# [agents.GitHubCopilot]
|
|
150
|
+
# enabled = true
|
|
151
|
+
# output_path = ".github/copilot-instructions.md"
|
|
152
|
+
|
|
153
|
+
# [agents.ClaudeCode]
|
|
154
|
+
# enabled = true
|
|
155
|
+
# output_path = "CLAUDE.md"
|
|
156
|
+
|
|
157
|
+
# [agents.OpenAICodexCLI]
|
|
158
|
+
# enabled = true
|
|
159
|
+
# output_path = "AGENTS.md"
|
|
160
|
+
|
|
161
|
+
# [agents.Cursor]
|
|
162
|
+
# enabled = true
|
|
163
|
+
# output_path = ".cursor/rules/ruler_cursor_instructions.md"
|
|
164
|
+
|
|
165
|
+
# [agents.Windsurf]
|
|
166
|
+
# enabled = true
|
|
167
|
+
# output_path = ".windsurf/rules/ruler_windsurf_instructions.md"
|
|
168
|
+
|
|
169
|
+
# [agents.Cline]
|
|
170
|
+
# enabled = true
|
|
171
|
+
# output_path = ".clinerules"
|
|
172
|
+
|
|
173
|
+
# [agents.Aider]
|
|
174
|
+
# enabled = true
|
|
175
|
+
# output_path_instructions = "ruler_aider_instructions.md"
|
|
176
|
+
# output_path_config = ".aider.conf.yml"
|
|
177
|
+
`;
|
|
178
|
+
if (!(await exists(instructionsPath))) {
|
|
179
|
+
await fs_1.promises.writeFile(instructionsPath, DEFAULT_INSTRUCTIONS);
|
|
180
|
+
console.log(`[ruler] Created ${instructionsPath}`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.log(`[ruler] instructions.md already exists, skipping`);
|
|
184
|
+
}
|
|
185
|
+
if (!(await exists(tomlPath))) {
|
|
186
|
+
await fs_1.promises.writeFile(tomlPath, DEFAULT_TOML);
|
|
187
|
+
console.log(`[ruler] Created ${tomlPath}`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log(`[ruler] ruler.toml already exists, skipping`);
|
|
191
|
+
}
|
|
192
|
+
const mcpPath = path.join(rulerDir, 'mcp.json');
|
|
193
|
+
const DEFAULT_MCP_JSON = `{
|
|
194
|
+
"mcpServers": {
|
|
195
|
+
"example": {
|
|
196
|
+
"url": "https://mcp.example.com"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}`;
|
|
200
|
+
if (!(await exists(mcpPath))) {
|
|
201
|
+
await fs_1.promises.writeFile(mcpPath, DEFAULT_MCP_JSON);
|
|
202
|
+
console.log(`[ruler] Created ${mcpPath}`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(`[ruler] mcp.json already exists, skipping`);
|
|
206
|
+
}
|
|
41
207
|
})
|
|
42
208
|
.demandCommand(1, 'You need to specify a command')
|
|
43
209
|
.help()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadConfig = loadConfig;
|
|
40
|
+
const fs_1 = require("fs");
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const toml_1 = __importDefault(require("toml"));
|
|
43
|
+
/**
|
|
44
|
+
* Loads and parses the ruler TOML configuration file, applying defaults.
|
|
45
|
+
* If the file is missing or invalid, returns empty/default config.
|
|
46
|
+
*/
|
|
47
|
+
async function loadConfig(options) {
|
|
48
|
+
const { projectRoot, configPath, cliAgents } = options;
|
|
49
|
+
const configFile = configPath
|
|
50
|
+
? path.resolve(configPath)
|
|
51
|
+
: path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
52
|
+
let raw = {};
|
|
53
|
+
try {
|
|
54
|
+
const text = await fs_1.promises.readFile(configFile, 'utf8');
|
|
55
|
+
raw = text.trim() ? toml_1.default.parse(text) : {};
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof Error && err.code !== 'ENOENT') {
|
|
59
|
+
console.warn(`[ruler] Warning: could not read config file at ${configFile}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
raw = {};
|
|
62
|
+
}
|
|
63
|
+
const defaultAgents = Array.isArray(raw.default_agents)
|
|
64
|
+
? raw.default_agents.map((a) => String(a))
|
|
65
|
+
: undefined;
|
|
66
|
+
const agentsSection = raw.agents && typeof raw.agents === 'object' && !Array.isArray(raw.agents)
|
|
67
|
+
? raw.agents
|
|
68
|
+
: {};
|
|
69
|
+
const agentConfigs = {};
|
|
70
|
+
for (const [name, section] of Object.entries(agentsSection)) {
|
|
71
|
+
if (section && typeof section === 'object') {
|
|
72
|
+
const sectionObj = section;
|
|
73
|
+
const cfg = {};
|
|
74
|
+
if (typeof sectionObj.enabled === 'boolean') {
|
|
75
|
+
cfg.enabled = sectionObj.enabled;
|
|
76
|
+
}
|
|
77
|
+
if (typeof sectionObj.output_path === 'string') {
|
|
78
|
+
cfg.outputPath = path.resolve(projectRoot, sectionObj.output_path);
|
|
79
|
+
}
|
|
80
|
+
if (typeof sectionObj.output_path_instructions === 'string') {
|
|
81
|
+
cfg.outputPathInstructions = path.resolve(projectRoot, sectionObj.output_path_instructions);
|
|
82
|
+
}
|
|
83
|
+
if (typeof sectionObj.output_path_config === 'string') {
|
|
84
|
+
cfg.outputPathConfig = path.resolve(projectRoot, sectionObj.output_path_config);
|
|
85
|
+
}
|
|
86
|
+
if (sectionObj.mcp && typeof sectionObj.mcp === 'object') {
|
|
87
|
+
const m = sectionObj.mcp;
|
|
88
|
+
const mcpCfg = {};
|
|
89
|
+
if (typeof m.enabled === 'boolean') {
|
|
90
|
+
mcpCfg.enabled = m.enabled;
|
|
91
|
+
}
|
|
92
|
+
if (typeof m.merge_strategy === 'string') {
|
|
93
|
+
const ms = m.merge_strategy;
|
|
94
|
+
if (ms === 'merge' || ms === 'overwrite') {
|
|
95
|
+
mcpCfg.strategy = ms;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cfg.mcp = mcpCfg;
|
|
99
|
+
}
|
|
100
|
+
agentConfigs[name] = cfg;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const rawMcpSection = raw.mcp && typeof raw.mcp === 'object' && !Array.isArray(raw.mcp)
|
|
104
|
+
? raw.mcp
|
|
105
|
+
: {};
|
|
106
|
+
const globalMcpConfig = {};
|
|
107
|
+
if (typeof rawMcpSection.enabled === 'boolean') {
|
|
108
|
+
globalMcpConfig.enabled = rawMcpSection.enabled;
|
|
109
|
+
}
|
|
110
|
+
if (typeof rawMcpSection.merge_strategy === 'string') {
|
|
111
|
+
const strat = rawMcpSection.merge_strategy;
|
|
112
|
+
if (strat === 'merge' || strat === 'overwrite') {
|
|
113
|
+
globalMcpConfig.strategy = strat;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const rawGitignoreSection = raw.gitignore &&
|
|
117
|
+
typeof raw.gitignore === 'object' &&
|
|
118
|
+
!Array.isArray(raw.gitignore)
|
|
119
|
+
? raw.gitignore
|
|
120
|
+
: {};
|
|
121
|
+
const gitignoreConfig = {};
|
|
122
|
+
if (typeof rawGitignoreSection.enabled === 'boolean') {
|
|
123
|
+
gitignoreConfig.enabled = rawGitignoreSection.enabled;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
defaultAgents,
|
|
127
|
+
agentConfigs,
|
|
128
|
+
cliAgents,
|
|
129
|
+
mcp: globalMcpConfig,
|
|
130
|
+
gitignore: gitignoreConfig,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.updateGitignore = updateGitignore;
|
|
37
|
+
const fs_1 = require("fs");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const RULER_START_MARKER = '# START Ruler Generated Files';
|
|
40
|
+
const RULER_END_MARKER = '# END Ruler Generated Files';
|
|
41
|
+
/**
|
|
42
|
+
* Updates the .gitignore file in the project root with paths in a managed Ruler block.
|
|
43
|
+
* Creates the file if it doesn't exist, and creates or updates the Ruler-managed block.
|
|
44
|
+
*
|
|
45
|
+
* @param projectRoot The project root directory (where .gitignore should be located)
|
|
46
|
+
* @param paths Array of file paths to add to .gitignore (can be absolute or relative)
|
|
47
|
+
*/
|
|
48
|
+
async function updateGitignore(projectRoot, paths) {
|
|
49
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
50
|
+
// Read existing .gitignore or start with empty content
|
|
51
|
+
let existingContent = '';
|
|
52
|
+
try {
|
|
53
|
+
existingContent = await fs_1.promises.readFile(gitignorePath, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err.code !== 'ENOENT') {
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Convert paths to relative POSIX format
|
|
61
|
+
const relativePaths = paths.map((p) => {
|
|
62
|
+
const relative = path.isAbsolute(p) ? path.relative(projectRoot, p) : p;
|
|
63
|
+
return relative.replace(/\\/g, '/'); // Convert to POSIX format
|
|
64
|
+
});
|
|
65
|
+
// Get all existing paths from .gitignore (excluding Ruler block)
|
|
66
|
+
const existingPaths = getExistingPathsExcludingRulerBlock(existingContent);
|
|
67
|
+
// Filter out paths that already exist outside the Ruler block
|
|
68
|
+
const newPaths = relativePaths.filter((p) => !existingPaths.includes(p));
|
|
69
|
+
// The Ruler block should contain only the new paths (replacement behavior)
|
|
70
|
+
const allRulerPaths = [...new Set(newPaths)].sort();
|
|
71
|
+
// Create new content
|
|
72
|
+
const newContent = updateGitignoreContent(existingContent, allRulerPaths);
|
|
73
|
+
// Write the updated content
|
|
74
|
+
await fs_1.promises.writeFile(gitignorePath, newContent);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Gets all paths from .gitignore content excluding those in the Ruler block.
|
|
78
|
+
*/
|
|
79
|
+
function getExistingPathsExcludingRulerBlock(content) {
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
const paths = [];
|
|
82
|
+
let inRulerBlock = false;
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
if (trimmed === RULER_START_MARKER) {
|
|
86
|
+
inRulerBlock = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (trimmed === RULER_END_MARKER) {
|
|
90
|
+
inRulerBlock = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!inRulerBlock && trimmed && !trimmed.startsWith('#')) {
|
|
94
|
+
paths.push(trimmed);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Updates the .gitignore content by replacing or adding the Ruler block.
|
|
101
|
+
*/
|
|
102
|
+
function updateGitignoreContent(existingContent, rulerPaths) {
|
|
103
|
+
const lines = existingContent.split('\n');
|
|
104
|
+
const newLines = [];
|
|
105
|
+
let inFirstRulerBlock = false;
|
|
106
|
+
let hasRulerBlock = false;
|
|
107
|
+
let processedFirstBlock = false;
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (trimmed === RULER_START_MARKER && !processedFirstBlock) {
|
|
111
|
+
inFirstRulerBlock = true;
|
|
112
|
+
hasRulerBlock = true;
|
|
113
|
+
newLines.push(line);
|
|
114
|
+
// Add the new Ruler paths
|
|
115
|
+
rulerPaths.forEach((p) => newLines.push(p));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (trimmed === RULER_END_MARKER && inFirstRulerBlock) {
|
|
119
|
+
inFirstRulerBlock = false;
|
|
120
|
+
processedFirstBlock = true;
|
|
121
|
+
newLines.push(line);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!inFirstRulerBlock) {
|
|
125
|
+
newLines.push(line);
|
|
126
|
+
}
|
|
127
|
+
// Skip lines that are in the first Ruler block (they get replaced)
|
|
128
|
+
}
|
|
129
|
+
// If no Ruler block exists, add one at the end
|
|
130
|
+
if (!hasRulerBlock) {
|
|
131
|
+
// Add blank line if content exists and doesn't end with blank line
|
|
132
|
+
if (existingContent.trim() && !existingContent.endsWith('\n\n')) {
|
|
133
|
+
newLines.push('');
|
|
134
|
+
}
|
|
135
|
+
newLines.push(RULER_START_MARKER);
|
|
136
|
+
rulerPaths.forEach((p) => newLines.push(p));
|
|
137
|
+
newLines.push(RULER_END_MARKER);
|
|
138
|
+
}
|
|
139
|
+
// Ensure file ends with a newline
|
|
140
|
+
let result = newLines.join('\n');
|
|
141
|
+
if (!result.endsWith('\n')) {
|
|
142
|
+
result += '\n';
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
package/dist/lib.js
CHANGED
|
@@ -35,8 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.applyAllAgentConfigs = applyAllAgentConfigs;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
|
-
const
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
|
|
39
40
|
const RuleProcessor_1 = require("./core/RuleProcessor");
|
|
41
|
+
const ConfigLoader_1 = require("./core/ConfigLoader");
|
|
42
|
+
const GitignoreUtils_1 = require("./core/GitignoreUtils");
|
|
40
43
|
const CopilotAgent_1 = require("./agents/CopilotAgent");
|
|
41
44
|
const ClaudeAgent_1 = require("./agents/ClaudeAgent");
|
|
42
45
|
const CodexCliAgent_1 = require("./agents/CodexCliAgent");
|
|
@@ -44,6 +47,43 @@ const CursorAgent_1 = require("./agents/CursorAgent");
|
|
|
44
47
|
const WindsurfAgent_1 = require("./agents/WindsurfAgent");
|
|
45
48
|
const ClineAgent_1 = require("./agents/ClineAgent");
|
|
46
49
|
const AiderAgent_1 = require("./agents/AiderAgent");
|
|
50
|
+
const merge_1 = require("./mcp/merge");
|
|
51
|
+
const validate_1 = require("./mcp/validate");
|
|
52
|
+
const mcp_1 = require("./paths/mcp");
|
|
53
|
+
/**
|
|
54
|
+
* Gets all output paths for an agent, taking into account any config overrides.
|
|
55
|
+
*/
|
|
56
|
+
function getAgentOutputPaths(agent, projectRoot, agentConfig) {
|
|
57
|
+
const paths = [];
|
|
58
|
+
const defaults = agent.getDefaultOutputPath(projectRoot);
|
|
59
|
+
if (typeof defaults === 'string') {
|
|
60
|
+
// Single output path (most agents)
|
|
61
|
+
const actualPath = agentConfig?.outputPath ?? defaults;
|
|
62
|
+
paths.push(actualPath);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Multiple output paths (e.g., AiderAgent)
|
|
66
|
+
const defaultPaths = defaults;
|
|
67
|
+
// Handle instructions path
|
|
68
|
+
if ('instructions' in defaultPaths) {
|
|
69
|
+
const instructionsPath = agentConfig?.outputPathInstructions ?? defaultPaths.instructions;
|
|
70
|
+
paths.push(instructionsPath);
|
|
71
|
+
}
|
|
72
|
+
// Handle config path
|
|
73
|
+
if ('config' in defaultPaths) {
|
|
74
|
+
const configPath = agentConfig?.outputPathConfig ?? defaultPaths.config;
|
|
75
|
+
paths.push(configPath);
|
|
76
|
+
}
|
|
77
|
+
// Handle any other paths in the default paths record
|
|
78
|
+
for (const [key, defaultPath] of Object.entries(defaultPaths)) {
|
|
79
|
+
if (key !== 'instructions' && key !== 'config') {
|
|
80
|
+
// For unknown path types, use the default since we don't have specific config overrides
|
|
81
|
+
paths.push(defaultPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return paths;
|
|
86
|
+
}
|
|
47
87
|
const agents = [
|
|
48
88
|
new CopilotAgent_1.CopilotAgent(),
|
|
49
89
|
new ClaudeAgent_1.ClaudeAgent(),
|
|
@@ -62,21 +102,106 @@ const agents = [
|
|
|
62
102
|
* @param projectRoot Root directory of the project
|
|
63
103
|
* @param includedAgents Optional list of agent name filters (case-insensitive substrings)
|
|
64
104
|
*/
|
|
65
|
-
async function applyAllAgentConfigs(projectRoot, includedAgents) {
|
|
66
|
-
|
|
105
|
+
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled) {
|
|
106
|
+
// Load configuration (default_agents, per-agent overrides, CLI filters)
|
|
107
|
+
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
108
|
+
projectRoot,
|
|
109
|
+
cliAgents: includedAgents,
|
|
110
|
+
configPath,
|
|
111
|
+
});
|
|
112
|
+
// Normalize per-agent config keys to actual agent names (substring match)
|
|
113
|
+
const rawConfigs = config.agentConfigs;
|
|
114
|
+
const mappedConfigs = {};
|
|
115
|
+
for (const [key, cfg] of Object.entries(rawConfigs)) {
|
|
116
|
+
const lowerKey = key.toLowerCase();
|
|
117
|
+
for (const agent of agents) {
|
|
118
|
+
if (agent.getName().toLowerCase().includes(lowerKey)) {
|
|
119
|
+
mappedConfigs[agent.getName()] = cfg;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
config.agentConfigs = mappedConfigs;
|
|
124
|
+
const rulerDir = await FileSystemUtils.findRulerDir(projectRoot);
|
|
67
125
|
if (!rulerDir) {
|
|
68
126
|
throw new Error(`.ruler directory not found from ${projectRoot}`);
|
|
69
127
|
}
|
|
70
|
-
await
|
|
71
|
-
const files = await
|
|
128
|
+
await FileSystemUtils.ensureDirExists(path.join(rulerDir, 'generated'));
|
|
129
|
+
const files = await FileSystemUtils.readMarkdownFiles(rulerDir);
|
|
72
130
|
const concatenated = (0, RuleProcessor_1.concatenateRules)(files);
|
|
131
|
+
const mcpFile = path.join(rulerDir, 'mcp.json');
|
|
132
|
+
let rulerMcpJson = null;
|
|
133
|
+
try {
|
|
134
|
+
const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
|
|
135
|
+
rulerMcpJson = JSON.parse(raw);
|
|
136
|
+
(0, validate_1.validateMcp)(rulerMcpJson);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err.code !== 'ENOENT') {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Determine which agents to run:
|
|
144
|
+
// CLI --agents > config.default_agents > per-agent.enabled flags > default all
|
|
73
145
|
let selected = agents;
|
|
74
|
-
if (
|
|
75
|
-
const filters =
|
|
146
|
+
if (config.cliAgents && config.cliAgents.length > 0) {
|
|
147
|
+
const filters = config.cliAgents.map((n) => n.toLowerCase());
|
|
76
148
|
selected = agents.filter((agent) => filters.some((f) => agent.getName().toLowerCase().includes(f)));
|
|
77
149
|
}
|
|
150
|
+
else if (config.defaultAgents && config.defaultAgents.length > 0) {
|
|
151
|
+
const defaults = config.defaultAgents.map((n) => n.toLowerCase());
|
|
152
|
+
selected = agents.filter((agent) => {
|
|
153
|
+
const key = agent.getName();
|
|
154
|
+
const override = config.agentConfigs[key]?.enabled;
|
|
155
|
+
if (override !== undefined) {
|
|
156
|
+
return override;
|
|
157
|
+
}
|
|
158
|
+
return defaults.includes(key.toLowerCase());
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
selected = agents.filter((agent) => config.agentConfigs[agent.getName()]?.enabled !== false);
|
|
163
|
+
}
|
|
164
|
+
// Collect all generated file paths for .gitignore
|
|
165
|
+
const generatedPaths = [];
|
|
78
166
|
for (const agent of selected) {
|
|
79
167
|
console.log(`[ruler] Applying rules for ${agent.getName()}...`);
|
|
80
|
-
|
|
168
|
+
const agentConfig = config.agentConfigs[agent.getName()];
|
|
169
|
+
await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
|
|
170
|
+
// Collect output paths for .gitignore
|
|
171
|
+
const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
|
|
172
|
+
generatedPaths.push(...outputPaths);
|
|
173
|
+
const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
174
|
+
const enabled = cliMcpEnabled &&
|
|
175
|
+
(agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
|
|
176
|
+
if (dest && rulerMcpJson != null && enabled) {
|
|
177
|
+
const strategy = cliMcpStrategy ??
|
|
178
|
+
agentConfig?.mcp?.strategy ??
|
|
179
|
+
config.mcp?.strategy ??
|
|
180
|
+
'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);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Handle .gitignore updates
|
|
187
|
+
// Configuration precedence: CLI > TOML > Default (enabled)
|
|
188
|
+
let gitignoreEnabled;
|
|
189
|
+
if (cliGitignoreEnabled !== undefined) {
|
|
190
|
+
gitignoreEnabled = cliGitignoreEnabled;
|
|
191
|
+
}
|
|
192
|
+
else if (config.gitignore?.enabled !== undefined) {
|
|
193
|
+
gitignoreEnabled = config.gitignore.enabled;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
gitignoreEnabled = true; // Default enabled
|
|
197
|
+
}
|
|
198
|
+
if (gitignoreEnabled && generatedPaths.length > 0) {
|
|
199
|
+
// Filter out .bak files as specified in requirements
|
|
200
|
+
const pathsToIgnore = generatedPaths.filter((p) => !p.endsWith('.bak'));
|
|
201
|
+
const uniquePaths = [...new Set(pathsToIgnore)];
|
|
202
|
+
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.`);
|
|
205
|
+
}
|
|
81
206
|
}
|
|
82
207
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeMcp = mergeMcp;
|
|
4
|
+
/**
|
|
5
|
+
* Merge native and incoming MCP server configurations according to strategy.
|
|
6
|
+
* @param base Existing native MCP config object.
|
|
7
|
+
* @param incoming Ruler MCP config object.
|
|
8
|
+
* @param strategy Merge strategy: 'merge' to union servers, 'overwrite' to replace.
|
|
9
|
+
* @returns Merged MCP config object.
|
|
10
|
+
*/
|
|
11
|
+
function mergeMcp(base, incoming, strategy) {
|
|
12
|
+
if (strategy === 'overwrite') {
|
|
13
|
+
return incoming;
|
|
14
|
+
}
|
|
15
|
+
const baseServers = base.mcpServers || {};
|
|
16
|
+
const incomingServers = incoming.mcpServers || {};
|
|
17
|
+
return {
|
|
18
|
+
...base,
|
|
19
|
+
mcpServers: { ...baseServers, ...incomingServers },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateMcp = validateMcp;
|
|
4
|
+
/**
|
|
5
|
+
* Validate the structure of the Ruler MCP JSON config.
|
|
6
|
+
* Minimal validation: ensure 'mcpServers' property exists and is an object.
|
|
7
|
+
* @param data Parsed JSON object from .ruler/mcp.json.
|
|
8
|
+
* @throws Error if validation fails.
|
|
9
|
+
*/
|
|
10
|
+
function validateMcp(data) {
|
|
11
|
+
if (!data ||
|
|
12
|
+
typeof data !== 'object' ||
|
|
13
|
+
!('mcpServers' in data) ||
|
|
14
|
+
typeof data.mcpServers !== 'object') {
|
|
15
|
+
throw new Error('[ruler] Invalid .ruler/mcp.json: must contain an object property "mcpServers"');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getNativeMcpPath = getNativeMcpPath;
|
|
37
|
+
exports.readNativeMcp = readNativeMcp;
|
|
38
|
+
exports.writeNativeMcp = writeNativeMcp;
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs_1 = require("fs");
|
|
42
|
+
/** Determine the native MCP config path for a given agent. */
|
|
43
|
+
async function getNativeMcpPath(adapterName, projectRoot) {
|
|
44
|
+
const home = os.homedir();
|
|
45
|
+
const candidates = [];
|
|
46
|
+
switch (adapterName) {
|
|
47
|
+
case 'GitHub Copilot':
|
|
48
|
+
candidates.push(path.join(projectRoot, '.vscode', 'mcp.json'));
|
|
49
|
+
break;
|
|
50
|
+
case 'Visual Studio':
|
|
51
|
+
candidates.push(path.join(projectRoot, '.mcp.json'));
|
|
52
|
+
candidates.push(path.join(projectRoot, '.vs', 'mcp.json'));
|
|
53
|
+
break;
|
|
54
|
+
case 'Cursor':
|
|
55
|
+
candidates.push(path.join(projectRoot, '.cursor', 'mcp.json'));
|
|
56
|
+
candidates.push(path.join(home, '.cursor', 'mcp.json'));
|
|
57
|
+
break;
|
|
58
|
+
case 'Windsurf':
|
|
59
|
+
candidates.push(path.join(home, '.codeium', 'windsurf', 'mcp_config.json'));
|
|
60
|
+
break;
|
|
61
|
+
case 'Claude Code':
|
|
62
|
+
candidates.push(path.join(projectRoot, 'claude_desktop_config.json'));
|
|
63
|
+
break;
|
|
64
|
+
case 'OpenAI Codex CLI':
|
|
65
|
+
candidates.push(path.join(home, '.codex', 'config.json'));
|
|
66
|
+
break;
|
|
67
|
+
case 'Aider':
|
|
68
|
+
candidates.push(path.join(projectRoot, '.mcp.json'));
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
for (const p of candidates) {
|
|
74
|
+
try {
|
|
75
|
+
await fs_1.promises.access(p);
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// continue
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// default to first candidate if none exist
|
|
83
|
+
return candidates.length > 0 ? candidates[0] : null;
|
|
84
|
+
}
|
|
85
|
+
/** Read native MCP config from disk, or return empty object if missing/invalid. */
|
|
86
|
+
async function readNativeMcp(filePath) {
|
|
87
|
+
try {
|
|
88
|
+
const text = await fs_1.promises.readFile(filePath, 'utf8');
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Write native MCP config to disk, creating parent directories as needed. */
|
|
96
|
+
async function writeNativeMcp(filePath, data) {
|
|
97
|
+
await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
98
|
+
const text = JSON.stringify(data, null, 2) + '\n';
|
|
99
|
+
await fs_1.promises.writeFile(filePath, text, 'utf8');
|
|
100
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectronica/ruler",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Ruler — apply the same rules to all coding agents",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"scripts": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
|
+
"toml": "^3.0.0",
|
|
60
61
|
"yargs": "^17.7.2"
|
|
61
62
|
}
|
|
62
63
|
}
|