@intellectronica/ruler 0.3.43 → 0.3.44
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 +4 -4
- package/dist/agents/CopilotAgent.js +1 -1
- package/dist/core/ConfigLoader.js +15 -1
- package/dist/core/FileSystemUtils.d.ts +1 -0
- package/dist/core/FileSystemUtils.js +1 -0
- package/dist/core/SkillsUtils.js +4 -1
- package/dist/core/UnifiedConfigLoader.js +42 -9
- package/dist/core/revert-engine.js +6 -1
- package/dist/mcp/capabilities.js +2 -2
- package/dist/paths/mcp.js +1 -1
- package/dist/revert.js +2 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
|
|
|
57
57
|
| Agent | Rules File(s) | MCP Configuration / Notes | Skills Support / Location | Subagents Support / Location |
|
|
58
58
|
| ---------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------- | ---------------------------- |
|
|
59
59
|
| AGENTS.md | `AGENTS.md` | (pseudo-agent ensuring root `AGENTS.md` exists) | - | - |
|
|
60
|
-
| GitHub Copilot | `AGENTS.md` | `.
|
|
60
|
+
| GitHub Copilot | `AGENTS.md` | `.mcp.json` | `.claude/skills/` | `.github/agents/` |
|
|
61
61
|
| Claude Code | `CLAUDE.md` | `.mcp.json` | `.claude/skills/` | `.claude/agents/` |
|
|
62
62
|
| OpenAI Codex CLI | `AGENTS.md` | `.codex/config.toml` | `.agents/skills/` | `.codex/agents/` (`.toml`) |
|
|
63
63
|
| Pi Coding Agent | `AGENTS.md` | - | `.pi/skills/` | - |
|
|
@@ -953,7 +953,7 @@ ruler apply
|
|
|
953
953
|
|
|
954
954
|
### Scenario 2: Working with worktrees
|
|
955
955
|
|
|
956
|
-
When using the default `git add
|
|
956
|
+
When using the default `git worktree add` command (which is also run by agent apps such as Claude Code or Codex through the interface), the gitignored files are not copied over. You will need to ask your agent to run `ruler apply` at the start of every session.
|
|
957
957
|
|
|
958
958
|
As an alternative you can commit your default agents files to source control.
|
|
959
959
|
|
|
@@ -979,7 +979,7 @@ enabled = false
|
|
|
979
979
|
To avoid having other contributors commit instructions outside of .ruler you can setup a github action to check there is no diff when running `ruler apply` in CI.
|
|
980
980
|
|
|
981
981
|
```yml
|
|
982
|
-
# .github/workflows/ruler-check
|
|
982
|
+
# .github/workflows/ruler-check.yml
|
|
983
983
|
|
|
984
984
|
# Verifies the committed agent files (AGENTS.md, CLAUDE.md, skills) match the .ruler/ source.
|
|
985
985
|
# They are committed so a fresh clone/worktree has guidance immediately; this guards against drift.
|
|
@@ -1014,7 +1014,7 @@ jobs:
|
|
|
1014
1014
|
|
|
1015
1015
|
- name: Verify committed agent files match .ruler/
|
|
1016
1016
|
run: |
|
|
1017
|
-
pnpm dlx @intellectronica/ruler
|
|
1017
|
+
pnpm dlx @intellectronica/ruler apply --no-gitignore --no-mcp
|
|
1018
1018
|
DRIFT="$(git status --porcelain -- AGENTS.md CLAUDE.md .claude/skills .codex/skills)"
|
|
1019
1019
|
if [ -n "$DRIFT" ]; then
|
|
1020
1020
|
echo "::error::Committed agent files are out of sync with .ruler/. Run 'pnpm dlx @intellectronica/ruler apply --no-gitignore --no-mcp' and commit the result."
|
|
@@ -172,7 +172,10 @@ function parseAgentMcpServers(sectionObj) {
|
|
|
172
172
|
const servers = {};
|
|
173
173
|
for (const [name, def] of Object.entries(sectionObj.mcp_servers)) {
|
|
174
174
|
if (def && typeof def === 'object' && !Array.isArray(def)) {
|
|
175
|
-
|
|
175
|
+
const server = normalizeAgentMcpServer(def);
|
|
176
|
+
if (server.command || server.url) {
|
|
177
|
+
servers[name] = server;
|
|
178
|
+
}
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
return Object.keys(servers).length > 0 ? servers : undefined;
|
|
@@ -181,6 +184,17 @@ function normalizeAgentMcpServer(def) {
|
|
|
181
184
|
const server = { ...def };
|
|
182
185
|
const hasCommand = typeof server.command === 'string';
|
|
183
186
|
const hasUrl = typeof server.url === 'string';
|
|
187
|
+
if (!hasCommand) {
|
|
188
|
+
delete server.command;
|
|
189
|
+
delete server.args;
|
|
190
|
+
delete server.env;
|
|
191
|
+
}
|
|
192
|
+
if (!hasUrl) {
|
|
193
|
+
delete server.url;
|
|
194
|
+
delete server.headers;
|
|
195
|
+
delete server.auth;
|
|
196
|
+
delete server.oauth;
|
|
197
|
+
}
|
|
184
198
|
if (hasCommand && hasUrl) {
|
|
185
199
|
delete server.command;
|
|
186
200
|
delete server.args;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function assertNotSymbolicLink(filePath: string, action: string): Promise<void>;
|
|
1
2
|
export declare function assertManagedPathInsideRoot(managedPath: string, rootPath: string, action: string): Promise<void>;
|
|
2
3
|
/**
|
|
3
4
|
* Searches upwards from startPath to find a directory named .ruler.
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.assertNotSymbolicLink = assertNotSymbolicLink;
|
|
36
37
|
exports.assertManagedPathInsideRoot = assertManagedPathInsideRoot;
|
|
37
38
|
exports.findRulerDir = findRulerDir;
|
|
38
39
|
exports.resolveProjectRootForRulerDir = resolveProjectRootForRulerDir;
|
package/dist/core/SkillsUtils.js
CHANGED
|
@@ -137,7 +137,10 @@ function formatValidationWarnings(warnings) {
|
|
|
137
137
|
* Recursively copies a directory and all its contents.
|
|
138
138
|
*/
|
|
139
139
|
async function copyRecursive(src, dest) {
|
|
140
|
-
const stat = await fs.
|
|
140
|
+
const stat = await fs.lstat(src);
|
|
141
|
+
if (stat.isSymbolicLink()) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
141
144
|
if (stat.isDirectory()) {
|
|
142
145
|
await fs.mkdir(dest, { recursive: true });
|
|
143
146
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadUnifiedConfig = loadUnifiedConfig;
|
|
37
37
|
const fs_1 = require("fs");
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
39
40
|
const toml_1 = require("@iarna/toml");
|
|
40
41
|
const hash_1 = require("./hash");
|
|
41
42
|
const RuleProcessor_1 = require("./RuleProcessor");
|
|
@@ -52,6 +53,39 @@ const KNOWN_MCP_SERVER_FIELDS = new Set([
|
|
|
52
53
|
function copyAdditionalMcpServerFields(def) {
|
|
53
54
|
return Object.fromEntries(Object.entries(def).filter(([key]) => !KNOWN_MCP_SERVER_FIELDS.has(key)));
|
|
54
55
|
}
|
|
56
|
+
async function resolveImplicitTomlFile(projectRoot, rulerDir, checkGlobal) {
|
|
57
|
+
const localTomlFile = path.join(rulerDir, 'ruler.toml');
|
|
58
|
+
if (await fileExists(localTomlFile)) {
|
|
59
|
+
return localTomlFile;
|
|
60
|
+
}
|
|
61
|
+
const projectTomlFile = path.join(projectRoot, '.ruler', 'ruler.toml');
|
|
62
|
+
if (path.resolve(projectTomlFile) !== path.resolve(localTomlFile) &&
|
|
63
|
+
(await fileExists(projectTomlFile))) {
|
|
64
|
+
return projectTomlFile;
|
|
65
|
+
}
|
|
66
|
+
if (!checkGlobal) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const xdgConfigDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
70
|
+
const globalTomlFile = path.join(xdgConfigDir, 'ruler', 'ruler.toml');
|
|
71
|
+
if (path.resolve(globalTomlFile) !== path.resolve(localTomlFile) &&
|
|
72
|
+
(await fileExists(globalTomlFile))) {
|
|
73
|
+
return globalTomlFile;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
async function fileExists(filePath) {
|
|
78
|
+
try {
|
|
79
|
+
await fs_1.promises.access(filePath);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err.code === 'ENOENT') {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
55
89
|
async function loadUnifiedConfig(options) {
|
|
56
90
|
// Resolve the effective .ruler directory (local or global), mirroring the main loader behavior
|
|
57
91
|
const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, options.checkGlobal ?? true)) || path.join(options.projectRoot, '.ruler');
|
|
@@ -66,15 +100,14 @@ async function loadUnifiedConfig(options) {
|
|
|
66
100
|
let tomlRaw = {};
|
|
67
101
|
const tomlFile = options.configPath
|
|
68
102
|
? path.resolve(options.configPath)
|
|
69
|
-
:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
err.code !== 'ENOENT') {
|
|
103
|
+
: await resolveImplicitTomlFile(options.projectRoot, meta.rulerDir, options.checkGlobal ?? true);
|
|
104
|
+
if (tomlFile) {
|
|
105
|
+
try {
|
|
106
|
+
const text = await fs_1.promises.readFile(tomlFile, 'utf8');
|
|
107
|
+
tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
|
|
108
|
+
meta.configFile = tomlFile;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
78
111
|
diagnostics.push({
|
|
79
112
|
severity: options.configPath ? 'error' : 'warning',
|
|
80
113
|
code: 'TOML_READ_ERROR',
|
|
@@ -141,6 +141,8 @@ async function restoreFromBackup(filePath, verbose, dryRun, projectRoot) {
|
|
|
141
141
|
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to restore backup through symlinked output path');
|
|
142
142
|
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to restore from backup through symlinked backup path');
|
|
143
143
|
}
|
|
144
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(filePath, 'Refusing to restore backup through symlinked output path');
|
|
145
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(backupPath, 'Refusing to restore from symlinked backup path');
|
|
144
146
|
await fs_1.promises.copyFile(backupPath, filePath);
|
|
145
147
|
(0, constants_1.logVerbose)(`${prefix} Restored: ${filePath} from backup`, verbose);
|
|
146
148
|
}
|
|
@@ -173,6 +175,7 @@ async function removeGeneratedFile(filePath, verbose, dryRun, projectRoot) {
|
|
|
173
175
|
if (projectRoot) {
|
|
174
176
|
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(filePath, projectRoot, 'Refusing to remove generated file through symlinked path');
|
|
175
177
|
}
|
|
178
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(filePath, 'Refusing to remove symlinked generated file');
|
|
176
179
|
await fs_1.promises.unlink(filePath);
|
|
177
180
|
(0, constants_1.logVerbose)(`${prefix} Removed generated file: ${filePath}`, verbose);
|
|
178
181
|
}
|
|
@@ -195,6 +198,7 @@ async function removeBackupFile(filePath, verbose, dryRun, projectRoot) {
|
|
|
195
198
|
if (projectRoot) {
|
|
196
199
|
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(backupPath, projectRoot, 'Refusing to remove backup file through symlinked path');
|
|
197
200
|
}
|
|
201
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(backupPath, 'Refusing to remove symlinked backup file');
|
|
198
202
|
await fs_1.promises.unlink(backupPath);
|
|
199
203
|
(0, constants_1.logVerbose)(`${prefix} Removed backup file: ${backupPath}`, verbose);
|
|
200
204
|
}
|
|
@@ -329,7 +333,6 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
329
333
|
const additionalFiles = [
|
|
330
334
|
'.gemini/settings.json',
|
|
331
335
|
'.mcp.json',
|
|
332
|
-
'.vscode/mcp.json',
|
|
333
336
|
'.cursor/mcp.json',
|
|
334
337
|
'.junie/mcp/mcp.json',
|
|
335
338
|
'.kilocode/mcp.json',
|
|
@@ -359,6 +362,8 @@ async function removeAdditionalAgentFiles(projectRoot, verbose, dryRun) {
|
|
|
359
362
|
(0, constants_1.logVerbose)(`${prefix} Would remove additional file: ${fullPath}`, verbose);
|
|
360
363
|
}
|
|
361
364
|
else {
|
|
365
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(fullPath, projectRoot, 'Refusing to remove additional file through symlinked path');
|
|
366
|
+
await (0, FileSystemUtils_1.assertNotSymbolicLink)(fullPath, 'Refusing to remove symlinked additional file');
|
|
362
367
|
await fs_1.promises.unlink(fullPath);
|
|
363
368
|
(0, constants_1.logVerbose)(`${prefix} Removed additional file: ${fullPath}`, verbose);
|
|
364
369
|
}
|
package/dist/mcp/capabilities.js
CHANGED
|
@@ -35,8 +35,8 @@ function filterMcpConfigForAgent(mcpConfig, agent) {
|
|
|
35
35
|
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
36
36
|
const config = serverConfig;
|
|
37
37
|
// Determine server type
|
|
38
|
-
const hasCommand =
|
|
39
|
-
const hasUrl =
|
|
38
|
+
const hasCommand = typeof config.command === 'string' || Array.isArray(config.command);
|
|
39
|
+
const hasUrl = typeof config.url === 'string';
|
|
40
40
|
const isStdio = hasCommand && !hasUrl;
|
|
41
41
|
const isRemote = hasUrl && !hasCommand;
|
|
42
42
|
// Include server if agent supports its type
|
package/dist/paths/mcp.js
CHANGED
|
@@ -48,7 +48,7 @@ async function getNativeMcpPath(adapterName, projectRoot) {
|
|
|
48
48
|
const candidates = [];
|
|
49
49
|
switch (adapterName) {
|
|
50
50
|
case 'GitHub Copilot':
|
|
51
|
-
candidates.push(path.join(projectRoot, '.
|
|
51
|
+
candidates.push(path.join(projectRoot, '.mcp.json'));
|
|
52
52
|
break;
|
|
53
53
|
case 'Visual Studio':
|
|
54
54
|
candidates.push(path.join(projectRoot, '.mcp.json'));
|
package/dist/revert.js
CHANGED
|
@@ -174,6 +174,8 @@ async function cleanIgnoreFile(projectRoot, ignoreFile, verbose, dryRun) {
|
|
|
174
174
|
(0, constants_1.logVerbose)(`No ${ignoreFile} file found`, verbose);
|
|
175
175
|
return false;
|
|
176
176
|
}
|
|
177
|
+
await FileSystemUtils.assertManagedPathInsideRoot(ignorePath, projectRoot, `Refusing to clean ${ignoreFile} through symlinked path`);
|
|
178
|
+
await FileSystemUtils.assertNotSymbolicLink(ignorePath, `Refusing to clean symlinked ${ignoreFile}`);
|
|
177
179
|
const content = await fs_1.promises.readFile(ignorePath, 'utf8');
|
|
178
180
|
const cleaned = (0, GitignoreUtils_1.removeCompleteRulerBlocks)(content);
|
|
179
181
|
if (!cleaned.removed) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intellectronica/ruler",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.44",
|
|
4
4
|
"description": "Ruler — apply the same rules to all coding agents",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "dist/lib.d.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test:watch": "jest --watch",
|
|
13
13
|
"test:coverage": "jest --coverage",
|
|
14
14
|
"test:integration": "jest tests/e2e/ruler.integration.test.ts --verbose",
|
|
15
|
-
"
|
|
15
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
16
|
+
"build": "npm run clean && tsc",
|
|
16
17
|
"check:package-lock": "node -e \"const pkg=require('./package.json'); const lock=require('./package-lock.json'); const root=lock.packages?.['']; if (lock.version !== pkg.version || root?.version !== pkg.version) { console.error('package-lock.json version metadata must match package.json version'); process.exit(1); }\"",
|
|
17
18
|
"prepublishOnly": "npm run build"
|
|
18
19
|
},
|