@intellectronica/ruler 0.1.2 → 0.1.4

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,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.
@@ -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
- return { defaultAgents, agentConfigs, cliAgents, mcp: globalMcpConfig };
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,162 @@
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
+ 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
+ }
80
+ return relative.replace(/\\/g, '/'); // Convert to POSIX format
81
+ });
82
+ // Get all existing paths from .gitignore (excluding Ruler block)
83
+ const existingPaths = getExistingPathsExcludingRulerBlock(existingContent);
84
+ // Filter out paths that already exist outside the Ruler block
85
+ const newPaths = relativePaths.filter((p) => !existingPaths.includes(p));
86
+ // The Ruler block should contain only the new paths (replacement behavior)
87
+ const allRulerPaths = [...new Set(newPaths)].sort();
88
+ // Create new content
89
+ const newContent = updateGitignoreContent(existingContent, allRulerPaths);
90
+ // Write the updated content
91
+ await fs_1.promises.writeFile(gitignorePath, newContent);
92
+ }
93
+ /**
94
+ * Gets all paths from .gitignore content excluding those in the Ruler block.
95
+ */
96
+ function getExistingPathsExcludingRulerBlock(content) {
97
+ const lines = content.split('\n');
98
+ const paths = [];
99
+ let inRulerBlock = false;
100
+ for (const line of lines) {
101
+ const trimmed = line.trim();
102
+ if (trimmed === RULER_START_MARKER) {
103
+ inRulerBlock = true;
104
+ continue;
105
+ }
106
+ if (trimmed === RULER_END_MARKER) {
107
+ inRulerBlock = false;
108
+ continue;
109
+ }
110
+ if (!inRulerBlock && trimmed && !trimmed.startsWith('#')) {
111
+ paths.push(trimmed);
112
+ }
113
+ }
114
+ return paths;
115
+ }
116
+ /**
117
+ * Updates the .gitignore content by replacing or adding the Ruler block.
118
+ */
119
+ function updateGitignoreContent(existingContent, rulerPaths) {
120
+ const lines = existingContent.split('\n');
121
+ const newLines = [];
122
+ let inFirstRulerBlock = false;
123
+ let hasRulerBlock = false;
124
+ let processedFirstBlock = false;
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ if (trimmed === RULER_START_MARKER && !processedFirstBlock) {
128
+ inFirstRulerBlock = true;
129
+ hasRulerBlock = true;
130
+ newLines.push(line);
131
+ // Add the new Ruler paths
132
+ rulerPaths.forEach((p) => newLines.push(p));
133
+ continue;
134
+ }
135
+ if (trimmed === RULER_END_MARKER && inFirstRulerBlock) {
136
+ inFirstRulerBlock = false;
137
+ processedFirstBlock = true;
138
+ newLines.push(line);
139
+ continue;
140
+ }
141
+ if (!inFirstRulerBlock) {
142
+ newLines.push(line);
143
+ }
144
+ // Skip lines that are in the first Ruler block (they get replaced)
145
+ }
146
+ // If no Ruler block exists, add one at the end
147
+ if (!hasRulerBlock) {
148
+ // Add blank line if content exists and doesn't end with blank line
149
+ if (existingContent.trim() && !existingContent.endsWith('\n\n')) {
150
+ newLines.push('');
151
+ }
152
+ newLines.push(RULER_START_MARKER);
153
+ rulerPaths.forEach((p) => newLines.push(p));
154
+ newLines.push(RULER_END_MARKER);
155
+ }
156
+ // Ensure file ends with a newline
157
+ let result = newLines.join('\n');
158
+ if (!result.endsWith('\n')) {
159
+ result += '\n';
160
+ }
161
+ return result;
162
+ }
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intellectronica/ruler",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Ruler — apply the same rules to all coding agents",
5
5
  "main": "dist/lib.js",
6
6
  "scripts": {