rules-md 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/index.js +574 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Agent Rules 🤖
|
|
2
|
+
|
|
3
|
+
A command-line tool that allows you to manage a single **Source of Truth** markdown file for your AI coding instructions and links/syncs it to all your AI coding agents (Claude Code, Roo Code, Windsurf, Cline, Copilot, Aider, Codex, ZCode, OpenCode, Pi Agent, etc.).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Centralized Source of Truth**: Define your persona, coding standards, preferred libraries, and workflow configurations in one place (defaults to `~/global-rules.md`).
|
|
8
|
+
- **Interactive Console UI**: An easy-to-use checklist terminal interface to select which agents to target.
|
|
9
|
+
- **Smart Merger**: If you already have existing rule files for specific agents, you can append their contents to your Source of Truth or back them up.
|
|
10
|
+
- **Robust Symlink/Hardlink Fallbacks**:
|
|
11
|
+
- Symlinks on Windows require Developer Mode or Administrator privileges.
|
|
12
|
+
- If a symlink fails, **Agent Rules** automatically falls back to a **Hard Link** (`fs.linkSync`), which behaves identically to a symlink for file edits but requires no special privileges!
|
|
13
|
+
- If both fail, it falls back to a copy.
|
|
14
|
+
- **Special Aider Support**: Automatically appends the Conventions file to your global `~/.aider.conf.yml` list so Aider loads it automatically.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation & Running
|
|
19
|
+
|
|
20
|
+
Since it has zero external dependencies, you can run it directly using Node.js.
|
|
21
|
+
|
|
22
|
+
### Run directly with Node
|
|
23
|
+
|
|
24
|
+
Navigate to this directory and run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node index.js
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Install globally (Optional)
|
|
31
|
+
|
|
32
|
+
To make the `rules-md` command available anywhere on your system:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm link
|
|
36
|
+
# or
|
|
37
|
+
npm install -g .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then simply run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rules-md
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Supported AI Coding Agents
|
|
49
|
+
|
|
50
|
+
| Agent | Global Instructions Path | Notes |
|
|
51
|
+
| ------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------- |
|
|
52
|
+
| **Antigravity** | `~/.gemini/GEMINI.md` | Google Antigravity global agent rules file. |
|
|
53
|
+
| **Claude Code** | `~/.claude/CLAUDE.md` | Automatically loaded at startup. |
|
|
54
|
+
| **Roo Code / Roo Cline** | `~/.roo/rules/global_rules.md` | Reads all markdown files in rules folder. |
|
|
55
|
+
| **Windsurf (Cascade)** | `~/.codeium/windsurf/memories/global_rules.md` | Central memories configuration. |
|
|
56
|
+
| **Cline (Documents)** | `~/Documents/Cline/Rules/global_rules.md` | Cline's central rules directory. |
|
|
57
|
+
| **Cline / Common** | `~/.agents/AGENTS.md` | Shared standard agent instructions path. |
|
|
58
|
+
| **GitHub Copilot (JetBrains)** | `%LOCALAPPDATA%/github-copilot/intellij/global-copilot-instructions.md` | Local appdata path. |
|
|
59
|
+
| **GitHub Copilot (CLI)** | `~/.copilot/copilot-instructions.md` | Default CLI config. |
|
|
60
|
+
| **Aider** | `~/.aider.conventions.md` | Handled via `~/.aider.conf.yml` read block. |
|
|
61
|
+
| **Z.ai ZCode** | `~/.zcode/AGENTS.md` | Central ZCode instructions path. |
|
|
62
|
+
| **OpenCode** | `~/.config/opencode/AGENTS.md` | Standard OpenCode path. |
|
|
63
|
+
| **Pi Agent** | `~/.pi/agent/AGENTS.md` | Pi Agent instructions path. |
|
|
64
|
+
| **OpenAI Codex** | `~/.codex/AGENTS.md` | OpenAI Codex agent global rules file. |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Editor Integration Tips
|
|
69
|
+
|
|
70
|
+
For tools that store rules in Settings UI or databases instead of the filesystem:
|
|
71
|
+
|
|
72
|
+
- **Cursor**: Open **Cursor Settings > General > Rules for AI** and copy-paste your rules there.
|
|
73
|
+
- **VS Code Copilot**: Add your source rules folder path to the `chat.instructionsFilesLocations` setting in VS Code.
|
package/index.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const prompts = require("prompts");
|
|
7
|
+
|
|
8
|
+
// ANSI Color Codes
|
|
9
|
+
const colors = {
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
bold: "\x1b[1m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
green: "\x1b[32m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
yellow: "\x1b[33m",
|
|
16
|
+
red: "\x1b[31m",
|
|
17
|
+
blue: "\x1b[34m",
|
|
18
|
+
magenta: "\x1b[35m",
|
|
19
|
+
bgGreen: "\x1b[42m",
|
|
20
|
+
bgBlue: "\x1b[44m",
|
|
21
|
+
black: "\x1b[30m",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Tool Configuration
|
|
25
|
+
const tools = [
|
|
26
|
+
{
|
|
27
|
+
id: "antigravity",
|
|
28
|
+
name: "Antigravity",
|
|
29
|
+
path: "~/.gemini/GEMINI.md",
|
|
30
|
+
desc: "Google Antigravity global agent rules file.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "claude",
|
|
34
|
+
name: "Claude Code",
|
|
35
|
+
path: "~/.claude/CLAUDE.md",
|
|
36
|
+
desc: "Automatically read by Claude Code CLI at session start.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "roo-code",
|
|
40
|
+
name: "Roo Code / Roo Cline",
|
|
41
|
+
path: "~/.roo/rules/global_rules.md",
|
|
42
|
+
desc: "Reads all markdown files in this rules directory.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "windsurf",
|
|
46
|
+
name: "Windsurf (Cascade)",
|
|
47
|
+
path: "~/.codeium/windsurf/memories/global_rules.md",
|
|
48
|
+
desc: "Cascade's global rule memory file.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "cline-docs",
|
|
52
|
+
name: "Cline (Documents Rules)",
|
|
53
|
+
path: "~/Documents/Cline/Rules/global_rules.md",
|
|
54
|
+
desc: "Cline global rules directory.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "cline-agents",
|
|
58
|
+
name: "Common (AGENTS.md)",
|
|
59
|
+
path: "~/.agents/AGENTS.md",
|
|
60
|
+
desc: "Common path recognized by Cline and other agents.",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "copilot-jetbrains",
|
|
64
|
+
name: "GitHub Copilot (JetBrains)",
|
|
65
|
+
path:
|
|
66
|
+
process.platform === "win32"
|
|
67
|
+
? "%LOCALAPPDATA%/github-copilot/intellij/global-copilot-instructions.md"
|
|
68
|
+
: process.platform === "darwin"
|
|
69
|
+
? "~/Library/Application Support/github-copilot/intellij/global-copilot-instructions.md"
|
|
70
|
+
: "~/.config/github-copilot/intellij/global-copilot-instructions.md",
|
|
71
|
+
desc: "Global copilot instructions for JetBrains IDEs.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "copilot-cli",
|
|
75
|
+
name: "GitHub Copilot (CLI)",
|
|
76
|
+
path: "~/.copilot/copilot-instructions.md",
|
|
77
|
+
desc: "Default Copilot CLI instructions file.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "aider",
|
|
81
|
+
name: "Aider",
|
|
82
|
+
path: "~/.aider.conventions.md",
|
|
83
|
+
desc: "Global conventions file (will optionally add to ~/.aider.conf.yml).",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "zcode",
|
|
87
|
+
name: "Z.ai ZCode",
|
|
88
|
+
path: "~/.zcode/AGENTS.md",
|
|
89
|
+
desc: "ZCode Agentic IDE global instructions.",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "opencode",
|
|
93
|
+
name: "OpenCode",
|
|
94
|
+
path: "~/.config/opencode/AGENTS.md",
|
|
95
|
+
desc: "OpenCode central instructions path.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "pi-agent",
|
|
99
|
+
name: "Pi Agent",
|
|
100
|
+
path: "~/.pi/agent/AGENTS.md",
|
|
101
|
+
desc: "Pi Coding Agent global rules.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "openai-codex",
|
|
105
|
+
name: "OpenAI Codex",
|
|
106
|
+
path: "~/.codex/AGENTS.md",
|
|
107
|
+
desc: "Codex agent global instructions.",
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Helper to resolve paths with env vars and tilde
|
|
112
|
+
function resolvePath(p) {
|
|
113
|
+
if (!p) return "";
|
|
114
|
+
let resolved = p;
|
|
115
|
+
// Replace Windows %LOCALAPPDATA% or similar env variables
|
|
116
|
+
resolved = resolved.replace(
|
|
117
|
+
/%([^%]+)%/g,
|
|
118
|
+
(_, name) => process.env[name] || "",
|
|
119
|
+
);
|
|
120
|
+
// Replace tilde with home directory
|
|
121
|
+
resolved = resolved.replace(/^\~/, os.homedir());
|
|
122
|
+
return path.resolve(resolved);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Standard Markdown Seed Template
|
|
126
|
+
const defaultTemplate = `# Global AI Agent Rules & Preferences
|
|
127
|
+
|
|
128
|
+
This file serves as the single source of truth for all AI coding assistants and agents.
|
|
129
|
+
|
|
130
|
+
## Coding Style & Rules
|
|
131
|
+
- Keep functions small, focused, and single-purpose.
|
|
132
|
+
- Always use descriptive variable and function names.
|
|
133
|
+
- Handle errors gracefully; log errors with appropriate context.
|
|
134
|
+
- Keep documentation up-to-date and maintain comment/docstring integrity.
|
|
135
|
+
|
|
136
|
+
## Git & Workflow
|
|
137
|
+
- Use standard conventional commits (e.g., \`feat: add rules linker\`, \`fix: handle EPERM symlinks\`).
|
|
138
|
+
- Keep commits atomic; write clear, concise commit messages.
|
|
139
|
+
- Never commit secrets, credentials, or \`.env\` files.
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
// Helper to back up files
|
|
143
|
+
function backupFile(filePath) {
|
|
144
|
+
if (!fs.existsSync(filePath)) return null;
|
|
145
|
+
const stats = fs.lstatSync(filePath);
|
|
146
|
+
if (stats.isSymbolicLink()) {
|
|
147
|
+
try {
|
|
148
|
+
fs.unlinkSync(filePath);
|
|
149
|
+
return "symlink_removed";
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const dateStr = new Date().toISOString().replace(/[:.]/g, "-");
|
|
156
|
+
const backupPath = `${filePath}.bak_${dateStr}`;
|
|
157
|
+
fs.renameSync(filePath, backupPath);
|
|
158
|
+
return backupPath;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Helper to create link/copy
|
|
162
|
+
function linkFile(sourceFile, targetFile, preserveOption) {
|
|
163
|
+
const targetDir = path.dirname(targetFile);
|
|
164
|
+
if (!fs.existsSync(targetDir)) {
|
|
165
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let stats = null;
|
|
169
|
+
try {
|
|
170
|
+
stats = fs.lstatSync(targetFile);
|
|
171
|
+
} catch (e) {}
|
|
172
|
+
|
|
173
|
+
if (stats) {
|
|
174
|
+
if (preserveOption === "append") {
|
|
175
|
+
try {
|
|
176
|
+
const existingContent = fs.readFileSync(targetFile, "utf8");
|
|
177
|
+
fs.appendFileSync(
|
|
178
|
+
sourceFile,
|
|
179
|
+
`\n\n## Imported from ${path.basename(targetFile)}\n\n${existingContent}`,
|
|
180
|
+
);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.log(
|
|
183
|
+
` ${colors.red}Error appending contents of ${path.basename(targetFile)}: ${e.message}${colors.reset}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
backupFile(targetFile);
|
|
187
|
+
} else if (preserveOption === "backup") {
|
|
188
|
+
const backupPath = backupFile(targetFile);
|
|
189
|
+
if (backupPath && backupPath !== "symlink_removed") {
|
|
190
|
+
console.log(
|
|
191
|
+
` ${colors.dim}Backed up existing file to ${path.basename(backupPath)}${colors.reset}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
} else if (preserveOption === "overwrite") {
|
|
195
|
+
try {
|
|
196
|
+
fs.unlinkSync(targetFile);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
try {
|
|
199
|
+
fs.rmSync(targetFile, { force: true });
|
|
200
|
+
} catch (err) {}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// Skip
|
|
204
|
+
return { success: false, skipped: true };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create link
|
|
209
|
+
try {
|
|
210
|
+
fs.symlinkSync(sourceFile, targetFile, "file");
|
|
211
|
+
return { success: true, type: "symlink" };
|
|
212
|
+
} catch (symlinkErr) {
|
|
213
|
+
// Fall back to hard link (requires no admin privileges on Windows)
|
|
214
|
+
try {
|
|
215
|
+
fs.linkSync(sourceFile, targetFile);
|
|
216
|
+
return { success: true, type: "hardlink" };
|
|
217
|
+
} catch (hardlinkErr) {
|
|
218
|
+
// Fall back to copy
|
|
219
|
+
try {
|
|
220
|
+
fs.copyFileSync(sourceFile, targetFile);
|
|
221
|
+
return { success: true, type: "copy" };
|
|
222
|
+
} catch (copyErr) {
|
|
223
|
+
return { success: false, error: copyErr };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check if a target file is linked to the source file (symlink or hardlink)
|
|
230
|
+
function isLinked(sourceFile, targetFile) {
|
|
231
|
+
try {
|
|
232
|
+
const lstats = fs.lstatSync(targetFile);
|
|
233
|
+
|
|
234
|
+
// 1. Check if it's a symbolic link
|
|
235
|
+
if (lstats.isSymbolicLink()) {
|
|
236
|
+
const linkTarget = fs.readlinkSync(targetFile);
|
|
237
|
+
const resolvedLinkTarget = path.resolve(
|
|
238
|
+
path.dirname(targetFile),
|
|
239
|
+
linkTarget,
|
|
240
|
+
);
|
|
241
|
+
return resolvedLinkTarget === path.resolve(sourceFile);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. Check if it's a hard link (same inode and device)
|
|
245
|
+
if (fs.existsSync(sourceFile)) {
|
|
246
|
+
const sourceStats = fs.statSync(sourceFile);
|
|
247
|
+
const targetStats = fs.statSync(targetFile);
|
|
248
|
+
return (
|
|
249
|
+
sourceStats.dev === targetStats.dev &&
|
|
250
|
+
sourceStats.ino === targetStats.ino
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Remove Aider conventions file config from ~/.aider.conf.yml
|
|
258
|
+
function removeAiderConfig() {
|
|
259
|
+
const configPath = resolvePath("~/.aider.conf.yml");
|
|
260
|
+
if (!fs.existsSync(configPath)) return;
|
|
261
|
+
let content = fs.readFileSync(configPath, "utf8");
|
|
262
|
+
|
|
263
|
+
const lines = content.split("\n");
|
|
264
|
+
const filteredLines = lines.filter(
|
|
265
|
+
(line) => !line.includes(".aider.conventions.md"),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
fs.writeFileSync(configPath, filteredLines.join("\n"), "utf8");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Update Aider config file
|
|
272
|
+
function updateAiderConfig() {
|
|
273
|
+
const configPath = resolvePath("~/.aider.conf.yml");
|
|
274
|
+
let content = "";
|
|
275
|
+
if (fs.existsSync(configPath)) {
|
|
276
|
+
content = fs.readFileSync(configPath, "utf8");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (content.includes(".aider.conventions.md")) {
|
|
280
|
+
return "already_configured";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const relativePath = "~/.aider.conventions.md";
|
|
284
|
+
if (!content.trim()) {
|
|
285
|
+
content = `# Aider Configuration\nread:\n - ${relativePath}\n`;
|
|
286
|
+
} else if (content.includes("read:")) {
|
|
287
|
+
content = content.replace(/(read:\s*)/, `$1\n - ${relativePath}`);
|
|
288
|
+
} else {
|
|
289
|
+
content += `\n\n# Added by Agent Rules Linker\nread:\n - ${relativePath}\n`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fs.writeFileSync(configPath, content, "utf8");
|
|
293
|
+
return "updated";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Main flow
|
|
297
|
+
async function main() {
|
|
298
|
+
console.log(`
|
|
299
|
+
┌────────────────────────────────────────────────────────┐
|
|
300
|
+
│ ${colors.bold}${colors.cyan}AGENT RULES${colors.reset} │
|
|
301
|
+
│ Sync your AI coding instructions centrally │
|
|
302
|
+
└────────────────────────────────────────────────────────┘
|
|
303
|
+
`);
|
|
304
|
+
|
|
305
|
+
// Handle Ctrl+C gracefully for prompts
|
|
306
|
+
const onCancel = (prompt) => {
|
|
307
|
+
console.log("\n" + colors.red + "Cancelled by user." + colors.reset);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// 1. Confirm Source of Truth file
|
|
312
|
+
const defaultSource = "~/global-rules.md";
|
|
313
|
+
const sourcePrompt = await prompts(
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
name: "source",
|
|
317
|
+
message: "Specify your global instructions file:",
|
|
318
|
+
initial: defaultSource,
|
|
319
|
+
},
|
|
320
|
+
{ onCancel },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const sourceFile = resolvePath(sourcePrompt.source);
|
|
324
|
+
console.log(
|
|
325
|
+
`Global instructions path: ${colors.bold}${sourceFile}${colors.reset}\n`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// 2. Initialize Source of Truth if missing
|
|
329
|
+
if (!fs.existsSync(sourceFile)) {
|
|
330
|
+
const confirmPrompt = await prompts(
|
|
331
|
+
{
|
|
332
|
+
type: "confirm",
|
|
333
|
+
name: "create",
|
|
334
|
+
message:
|
|
335
|
+
"Global instructions file does not exist. Create it with a standard template?",
|
|
336
|
+
initial: true,
|
|
337
|
+
},
|
|
338
|
+
{ onCancel },
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (confirmPrompt.create) {
|
|
342
|
+
const sourceDir = path.dirname(sourceFile);
|
|
343
|
+
if (!fs.existsSync(sourceDir)) {
|
|
344
|
+
fs.mkdirSync(sourceDir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
fs.writeFileSync(sourceFile, defaultTemplate, "utf8");
|
|
347
|
+
console.log(
|
|
348
|
+
`${colors.green}Created global instructions file at ${sourceFile}${colors.reset}\n`,
|
|
349
|
+
);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(
|
|
352
|
+
`${colors.red}Abort: Global instructions file is required to proceed.${colors.reset}`,
|
|
353
|
+
);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 3. Detect currently linked agents and map to choices
|
|
359
|
+
const choices = tools.map((t) => {
|
|
360
|
+
const targetFile = resolvePath(t.path);
|
|
361
|
+
const linked = isLinked(sourceFile, targetFile);
|
|
362
|
+
return {
|
|
363
|
+
title: t.name,
|
|
364
|
+
value: t,
|
|
365
|
+
description:
|
|
366
|
+
`${t.path} ${linked ? colors.cyan + "(linked)" + colors.reset : ""}`.trim(),
|
|
367
|
+
selected: linked,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// 4. Let user select tools
|
|
372
|
+
const agentsPrompt = await prompts(
|
|
373
|
+
{
|
|
374
|
+
type: "multiselect",
|
|
375
|
+
name: "agents",
|
|
376
|
+
message: "Select coding agents you want to link:",
|
|
377
|
+
choices,
|
|
378
|
+
hint: "- Space to toggle, Enter to confirm",
|
|
379
|
+
min: 0,
|
|
380
|
+
},
|
|
381
|
+
{ onCancel },
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const selectedTools = agentsPrompt.agents || [];
|
|
385
|
+
const previouslyLinked = tools.filter((t) =>
|
|
386
|
+
isLinked(sourceFile, resolvePath(t.path)),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Tools to link: selected ones that are NOT currently linked
|
|
390
|
+
const newToolsToLink = selectedTools.filter(
|
|
391
|
+
(t) => !isLinked(sourceFile, resolvePath(t.path)),
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Tools to unlink: previously linked ones that are NOT selected anymore
|
|
395
|
+
const toolsToUnlink = previouslyLinked.filter(
|
|
396
|
+
(t) => !selectedTools.some((sel) => sel.id === t.id),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// If no changes needed
|
|
400
|
+
if (newToolsToLink.length === 0 && toolsToUnlink.length === 0) {
|
|
401
|
+
console.log(
|
|
402
|
+
`\n${colors.yellow}No changes needed. All active links are up to date.${colors.reset}`,
|
|
403
|
+
);
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 5. Unlink deselected tools
|
|
408
|
+
if (toolsToUnlink.length > 0) {
|
|
409
|
+
console.log(
|
|
410
|
+
`\n${colors.bold}Unlinking deselected agents:${colors.reset}`,
|
|
411
|
+
);
|
|
412
|
+
for (const tool of toolsToUnlink) {
|
|
413
|
+
const targetFile = resolvePath(tool.path);
|
|
414
|
+
console.log(
|
|
415
|
+
` Unlinking ${colors.bold}${tool.name}${colors.reset}...`,
|
|
416
|
+
);
|
|
417
|
+
try {
|
|
418
|
+
const stats = fs.lstatSync(targetFile);
|
|
419
|
+
if (stats.isSymbolicLink()) {
|
|
420
|
+
fs.unlinkSync(targetFile);
|
|
421
|
+
} else {
|
|
422
|
+
fs.rmSync(targetFile, { force: true });
|
|
423
|
+
}
|
|
424
|
+
console.log(
|
|
425
|
+
` ${colors.green}Unlinked successfully.${colors.reset}`,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (tool.id === "aider") {
|
|
429
|
+
try {
|
|
430
|
+
removeAiderConfig();
|
|
431
|
+
console.log(
|
|
432
|
+
` ${colors.dim}Cleaned Aider conventions from ~/.aider.conf.yml.${colors.reset}`,
|
|
433
|
+
);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
console.log(
|
|
436
|
+
` ${colors.yellow}Could not clean ~/.aider.conf.yml: ${e.message}${colors.reset}`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (e) {
|
|
441
|
+
console.log(
|
|
442
|
+
` ${colors.red}Failed to unlink: ${e.message}${colors.reset}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 6. Link new tools
|
|
449
|
+
if (newToolsToLink.length > 0) {
|
|
450
|
+
console.log(`\n${colors.bold}Linking new agents:${colors.reset}`);
|
|
451
|
+
const preservePrompt = await prompts(
|
|
452
|
+
{
|
|
453
|
+
type: "select",
|
|
454
|
+
name: "option",
|
|
455
|
+
message:
|
|
456
|
+
"If a target instructions file already exists for new agents, what should we do?",
|
|
457
|
+
choices: [
|
|
458
|
+
{
|
|
459
|
+
title: "Append & Link",
|
|
460
|
+
value: "append",
|
|
461
|
+
description:
|
|
462
|
+
"Append existing instructions to Source of Truth, back up the file, and link",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
title: "Backup & Link",
|
|
466
|
+
value: "backup",
|
|
467
|
+
description: "Rename existing file to .bak and link",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
title: "Overwrite & Link",
|
|
471
|
+
value: "overwrite",
|
|
472
|
+
description: "Directly delete existing file and link",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
title: "Skip",
|
|
476
|
+
value: "skip",
|
|
477
|
+
description: "Do not modify the file for this agent",
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
{ onCancel },
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const preserveOption = preservePrompt.option;
|
|
485
|
+
console.log();
|
|
486
|
+
|
|
487
|
+
let aiderSelected = false;
|
|
488
|
+
for (const tool of newToolsToLink) {
|
|
489
|
+
const targetFile = resolvePath(tool.path);
|
|
490
|
+
console.log(`Linking ${colors.bold}${tool.name}${colors.reset}...`);
|
|
491
|
+
console.log(` Target: ${colors.dim}${targetFile}${colors.reset}`);
|
|
492
|
+
|
|
493
|
+
const result = linkFile(sourceFile, targetFile, preserveOption);
|
|
494
|
+
|
|
495
|
+
if (result.skipped) {
|
|
496
|
+
console.log(` ${colors.yellow}Skipped.${colors.reset}`);
|
|
497
|
+
} else if (result.success) {
|
|
498
|
+
let typeLabel = "";
|
|
499
|
+
if (result.type === "symlink")
|
|
500
|
+
typeLabel = `${colors.green}Symlink created successfully.${colors.reset}`;
|
|
501
|
+
if (result.type === "hardlink")
|
|
502
|
+
typeLabel = `${colors.cyan}Hard link created successfully (Windows symlink fallback).${colors.reset}`;
|
|
503
|
+
if (result.type === "copy")
|
|
504
|
+
typeLabel = `${colors.yellow}Copy created successfully (linking not possible on this environment).${colors.reset}`;
|
|
505
|
+
console.log(` ${typeLabel}`);
|
|
506
|
+
} else {
|
|
507
|
+
console.log(
|
|
508
|
+
` ${colors.red}Failed: ${result.error ? result.error.message : "Unknown error"}${colors.reset}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (tool.id === "aider") {
|
|
513
|
+
aiderSelected = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Aider Post-process
|
|
518
|
+
if (aiderSelected) {
|
|
519
|
+
console.log(`\n${colors.bold}Aider Post-Setup:${colors.reset}`);
|
|
520
|
+
const configureAiderPrompt = await prompts(
|
|
521
|
+
{
|
|
522
|
+
type: "confirm",
|
|
523
|
+
name: "configure",
|
|
524
|
+
message:
|
|
525
|
+
"Would you like to auto-update ~/.aider.conf.yml to load ~/.aider.conventions.md?",
|
|
526
|
+
initial: true,
|
|
527
|
+
},
|
|
528
|
+
{ onCancel },
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (configureAiderPrompt.configure) {
|
|
532
|
+
try {
|
|
533
|
+
const aiderRes = updateAiderConfig();
|
|
534
|
+
if (aiderRes === "updated") {
|
|
535
|
+
console.log(
|
|
536
|
+
` ${colors.green}Successfully updated ~/.aider.conf.yml.${colors.reset}`,
|
|
537
|
+
);
|
|
538
|
+
} else if (aiderRes === "already_configured") {
|
|
539
|
+
console.log(
|
|
540
|
+
` ${colors.dim}Already configured in ~/.aider.conf.yml.${colors.reset}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
} catch (e) {
|
|
544
|
+
console.log(
|
|
545
|
+
` ${colors.red}Failed to update ~/.aider.conf.yml: ${e.message}${colors.reset}`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// 7. Render tips for editors without file-based global rules
|
|
553
|
+
console.log(`
|
|
554
|
+
┌────────────────────────────────────────────────────────┐
|
|
555
|
+
│ ✨ ${colors.bold}SUCCESSFULLY COMPLETED${colors.reset} │
|
|
556
|
+
│ │
|
|
557
|
+
│ Your central rule file is now linked. Any changes to │
|
|
558
|
+
│ your global instructions will automatically sync to │
|
|
559
|
+
│ the active agents! │
|
|
560
|
+
└────────────────────────────────────────────────────────┘
|
|
561
|
+
|
|
562
|
+
💡 ${colors.bold}Editor integration tips:${colors.reset}
|
|
563
|
+
- ${colors.bold}Cursor:${colors.reset} Copy your rules to ${colors.cyan}Cursor Settings > General > Rules for AI${colors.reset}.
|
|
564
|
+
- ${colors.bold}VS Code Copilot Chat:${colors.reset} In settings, configure ${colors.cyan}chat.instructionsFilesLocations${colors.reset}
|
|
565
|
+
to point to the directory containing your source rules file.
|
|
566
|
+
`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
main().catch((err) => {
|
|
570
|
+
console.error(
|
|
571
|
+
`${colors.red}An error occurred: ${err.message}${colors.reset}`,
|
|
572
|
+
);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rules-md",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive CLI tool to symlink/hardlink coding agent rules to a central Source of Truth",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rules-md": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"agent",
|
|
16
|
+
"rules",
|
|
17
|
+
"claude",
|
|
18
|
+
"codex",
|
|
19
|
+
"cursor",
|
|
20
|
+
"copilot",
|
|
21
|
+
"aider",
|
|
22
|
+
"windsurf"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
}
|
|
28
|
+
}
|