@intellectronica/ruler 0.1.2 → 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 +64 -8
- package/dist/cli/commands.js +14 -1
- package/dist/core/ConfigLoader.js +16 -1
- package/dist/core/GitignoreUtils.js +145 -0
- package/dist/lib.js +62 -1
- package/package.json +1 -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,7 @@ 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,...>] [--config <path>]
|
|
42
|
+
ruler apply [--project-root <path>] [--agents <agent1,agent2,...>] [--config <path>] [--gitignore] [--no-gitignore]
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
|
|
@@ -135,6 +139,65 @@ enabled = false
|
|
|
135
139
|
merge_strategy = "overwrite"
|
|
136
140
|
```
|
|
137
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
|
+
|
|
138
201
|
## Development
|
|
139
202
|
|
|
140
203
|
Clone the repository and install dependencies:
|
|
@@ -165,13 +228,6 @@ End-to-end tests (run build before tests):
|
|
|
165
228
|
npm run build && npm test
|
|
166
229
|
```
|
|
167
230
|
|
|
168
|
-
### Roadmap
|
|
169
|
-
- [ ] Support for MCP servers config
|
|
170
|
-
- [ ] Support for transforming and rewriting the rules using AI
|
|
171
|
-
- [ ] Support "harmonisation" (reading existing rules of specific agents and combining them with the master config)
|
|
172
|
-
- [ ] Support for additional agents
|
|
173
|
-
- [ ] Support for agent-specific features (for example: apply rules in copilot)
|
|
174
|
-
|
|
175
231
|
## Contributing
|
|
176
232
|
|
|
177
233
|
Contributions are welcome! Please open issues or pull requests on GitHub.
|
package/dist/cli/commands.js
CHANGED
|
@@ -74,6 +74,10 @@ function run() {
|
|
|
74
74
|
description: 'Replace (not merge) the native MCP config(s)',
|
|
75
75
|
default: false,
|
|
76
76
|
});
|
|
77
|
+
y.option('gitignore', {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Enable/disable automatic .gitignore updates (default: enabled)',
|
|
80
|
+
});
|
|
77
81
|
}, async (argv) => {
|
|
78
82
|
const projectRoot = argv['project-root'];
|
|
79
83
|
const agents = argv.agents
|
|
@@ -84,8 +88,17 @@ function run() {
|
|
|
84
88
|
const mcpStrategy = argv['mcp-overwrite']
|
|
85
89
|
? 'overwrite'
|
|
86
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
|
+
}
|
|
87
100
|
try {
|
|
88
|
-
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy);
|
|
101
|
+
await (0, lib_1.applyAllAgentConfigs)(projectRoot, agents, configPath, mcpEnabled, mcpStrategy, gitignorePreference);
|
|
89
102
|
console.log('Ruler apply completed successfully.');
|
|
90
103
|
}
|
|
91
104
|
catch (err) {
|
|
@@ -113,5 +113,20 @@ async function loadConfig(options) {
|
|
|
113
113
|
globalMcpConfig.strategy = strat;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
|
|
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
|
+
};
|
|
117
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
|
@@ -39,6 +39,7 @@ const fs_1 = require("fs");
|
|
|
39
39
|
const FileSystemUtils = __importStar(require("./core/FileSystemUtils"));
|
|
40
40
|
const RuleProcessor_1 = require("./core/RuleProcessor");
|
|
41
41
|
const ConfigLoader_1 = require("./core/ConfigLoader");
|
|
42
|
+
const GitignoreUtils_1 = require("./core/GitignoreUtils");
|
|
42
43
|
const CopilotAgent_1 = require("./agents/CopilotAgent");
|
|
43
44
|
const ClaudeAgent_1 = require("./agents/ClaudeAgent");
|
|
44
45
|
const CodexCliAgent_1 = require("./agents/CodexCliAgent");
|
|
@@ -49,6 +50,40 @@ const AiderAgent_1 = require("./agents/AiderAgent");
|
|
|
49
50
|
const merge_1 = require("./mcp/merge");
|
|
50
51
|
const validate_1 = require("./mcp/validate");
|
|
51
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
|
+
}
|
|
52
87
|
const agents = [
|
|
53
88
|
new CopilotAgent_1.CopilotAgent(),
|
|
54
89
|
new ClaudeAgent_1.ClaudeAgent(),
|
|
@@ -67,7 +102,7 @@ const agents = [
|
|
|
67
102
|
* @param projectRoot Root directory of the project
|
|
68
103
|
* @param includedAgents Optional list of agent name filters (case-insensitive substrings)
|
|
69
104
|
*/
|
|
70
|
-
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy) {
|
|
105
|
+
async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cliMcpEnabled = true, cliMcpStrategy, cliGitignoreEnabled) {
|
|
71
106
|
// Load configuration (default_agents, per-agent overrides, CLI filters)
|
|
72
107
|
const config = await (0, ConfigLoader_1.loadConfig)({
|
|
73
108
|
projectRoot,
|
|
@@ -126,10 +161,15 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
126
161
|
else {
|
|
127
162
|
selected = agents.filter((agent) => config.agentConfigs[agent.getName()]?.enabled !== false);
|
|
128
163
|
}
|
|
164
|
+
// Collect all generated file paths for .gitignore
|
|
165
|
+
const generatedPaths = [];
|
|
129
166
|
for (const agent of selected) {
|
|
130
167
|
console.log(`[ruler] Applying rules for ${agent.getName()}...`);
|
|
131
168
|
const agentConfig = config.agentConfigs[agent.getName()];
|
|
132
169
|
await agent.applyRulerConfig(concatenated, projectRoot, agentConfig);
|
|
170
|
+
// Collect output paths for .gitignore
|
|
171
|
+
const outputPaths = getAgentOutputPaths(agent, projectRoot, agentConfig);
|
|
172
|
+
generatedPaths.push(...outputPaths);
|
|
133
173
|
const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
|
|
134
174
|
const enabled = cliMcpEnabled &&
|
|
135
175
|
(agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
|
|
@@ -143,4 +183,25 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
143
183
|
await (0, mcp_1.writeNativeMcp)(dest, merged);
|
|
144
184
|
}
|
|
145
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
|
+
}
|
|
206
|
+
}
|
|
146
207
|
}
|