@bonesofspring/ai-rules 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 +31 -0
- package/bin/cli.js +100 -27
- package/package.json +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @bonesofspring/ai-rules
|
|
2
|
+
|
|
3
|
+
Presets for Cursor and Claude Code: rules, commands, plus for Claude `CLAUDE.md` and `skills` / `agents` / `hooks` when the preset includes them. Install from npm into a project so you don’t have to copy files out of the repo by hand.
|
|
4
|
+
|
|
5
|
+
Node 18+.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# from the project root
|
|
9
|
+
npx @bonesofspring/ai-rules init cursor --preset next
|
|
10
|
+
npx @bonesofspring/ai-rules init claude --preset next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Global: `npm i -g @bonesofspring/ai-rules`, then `ai-rules ...`.
|
|
14
|
+
|
|
15
|
+
`init` merges into the current directory: dirs are created, matching files are **overwritten**. `--preset` and the subcommand can be in either order, e.g. `--preset=next init cursor`.
|
|
16
|
+
|
|
17
|
+
`clean cursor` / `clean claude` removes what this CLI installs: for Cursor, `.cursor/rules` and `.cursor/commands`; for Claude also `.claude/skills`, `agents`, `hooks`, and `CLAUDE.md` when those were present. Anything else under `.cursor` / `.claude` is left alone.
|
|
18
|
+
|
|
19
|
+
Where things go on `init`:
|
|
20
|
+
|
|
21
|
+
| | Cursor | Claude |
|
|
22
|
+
|---|--------|--------|
|
|
23
|
+
| rules | `.cursor/rules` | `.claude/rules` |
|
|
24
|
+
| commands | `.cursor/commands` | `.claude/commands` |
|
|
25
|
+
| other | — | root `CLAUDE.md` → `.claude/CLAUDE.md`; folders `skills`, `agents`, `hooks` → under `.claude/` with the same names |
|
|
26
|
+
|
|
27
|
+
The package currently ships the **`next`** preset (Next.js stack and whatever else is bundled there). Other preset names: see `presets/` in the [repo](https://github.com/bonesofspring/ai-rules). Name is a single path segment, no `/` or `..`.
|
|
28
|
+
|
|
29
|
+
Help: `ai-rules --help`.
|
|
30
|
+
|
|
31
|
+
MIT.
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ const path = require('path');
|
|
|
7
7
|
|
|
8
8
|
const packageRoot = path.join(__dirname, '..');
|
|
9
9
|
|
|
10
|
+
/** Single path segment: letters, digits, dot, underscore, hyphen; no separators or "..". */
|
|
11
|
+
const PRESET_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
12
|
+
|
|
10
13
|
const PRESETS = {
|
|
11
14
|
cursor: {
|
|
12
15
|
rulesDir: '.cursor/rules',
|
|
@@ -31,6 +34,10 @@ Usage:
|
|
|
31
34
|
ai-rules init <cursor|claude> --preset <name> Copy preset rules and commands into the current directory
|
|
32
35
|
ai-rules clean <cursor|claude> Remove preset target rules and commands directories
|
|
33
36
|
|
|
37
|
+
Notes:
|
|
38
|
+
init merges into existing target folders; same-named files are overwritten.
|
|
39
|
+
Flags may appear before or after the subcommand (e.g. --preset next init cursor).
|
|
40
|
+
|
|
34
41
|
Examples:
|
|
35
42
|
npx @bonesofspring/ai-rules init cursor --preset next
|
|
36
43
|
npx @bonesofspring/ai-rules clean claude
|
|
@@ -42,11 +49,23 @@ function parseArgs(argv) {
|
|
|
42
49
|
for (let i = 0; i < argv.length; i++) {
|
|
43
50
|
const a = argv[i];
|
|
44
51
|
if (a === '--preset') {
|
|
45
|
-
|
|
52
|
+
const value = argv[i + 1];
|
|
53
|
+
if (value === undefined || value.startsWith('-')) {
|
|
54
|
+
args.presetError =
|
|
55
|
+
'Missing value for --preset (use --preset <name> or --preset=<name>)';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
args.preset = value;
|
|
59
|
+
i++;
|
|
46
60
|
continue;
|
|
47
61
|
}
|
|
48
62
|
if (a.startsWith('--preset=')) {
|
|
49
|
-
|
|
63
|
+
const value = a.slice('--preset='.length);
|
|
64
|
+
if (value === '') {
|
|
65
|
+
args.presetError = 'Missing value after --preset=';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
args.preset = value;
|
|
50
69
|
continue;
|
|
51
70
|
}
|
|
52
71
|
args._.push(a);
|
|
@@ -54,6 +73,35 @@ function parseArgs(argv) {
|
|
|
54
73
|
return args;
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @returns {string | null} Error message or null if valid.
|
|
78
|
+
*/
|
|
79
|
+
function validatePresetName(preset) {
|
|
80
|
+
if (preset === undefined || preset === '') {
|
|
81
|
+
return 'Missing --preset <name>';
|
|
82
|
+
}
|
|
83
|
+
if (!PRESET_NAME_RE.test(preset) || preset.includes('..')) {
|
|
84
|
+
return (
|
|
85
|
+
'Invalid preset name: use one path segment (letters, digits, ., _, -), ' +
|
|
86
|
+
'no slashes or ".." (example: next)'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Ensures resolved preset dir stays under the tool presets root (defense in depth).
|
|
94
|
+
*/
|
|
95
|
+
function assertPresetDirInsideRoot(presetsRoot, presetBase) {
|
|
96
|
+
const root = path.resolve(presetsRoot);
|
|
97
|
+
const resolved = path.resolve(presetBase);
|
|
98
|
+
const rel = path.relative(root, resolved);
|
|
99
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
100
|
+
console.error('Invalid preset path (escapes package presets directory).');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
57
105
|
function copyDir(src, dest) {
|
|
58
106
|
if (!fs.existsSync(src)) {
|
|
59
107
|
throw new Error(`Source not found: ${src}`);
|
|
@@ -108,11 +156,15 @@ function cmdInit(tool, preset) {
|
|
|
108
156
|
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
109
157
|
process.exit(1);
|
|
110
158
|
}
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
const nameErr = validatePresetName(preset);
|
|
160
|
+
if (nameErr) {
|
|
161
|
+
console.error(nameErr);
|
|
113
162
|
process.exit(1);
|
|
114
163
|
}
|
|
115
|
-
const
|
|
164
|
+
const presetsRoot = path.join(packageRoot, ...cfg.relBase);
|
|
165
|
+
const base = path.join(presetsRoot, preset);
|
|
166
|
+
assertPresetDirInsideRoot(presetsRoot, base);
|
|
167
|
+
|
|
116
168
|
const rulesSrc = path.join(base, 'rules');
|
|
117
169
|
const commandsSrc = path.join(base, 'commands');
|
|
118
170
|
const cwd = process.cwd();
|
|
@@ -122,33 +174,39 @@ function cmdInit(tool, preset) {
|
|
|
122
174
|
process.exit(1);
|
|
123
175
|
}
|
|
124
176
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
177
|
+
try {
|
|
178
|
+
if (tool === 'claude' && cfg.claudeMdSrc) {
|
|
179
|
+
const mdFrom = path.join(base, cfg.claudeMdSrc);
|
|
180
|
+
if (fs.existsSync(mdFrom)) {
|
|
181
|
+
const claudeRoot = path.join(cwd, '.claude');
|
|
182
|
+
fs.mkdirSync(claudeRoot, { recursive: true });
|
|
183
|
+
fs.copyFileSync(mdFrom, path.join(claudeRoot, 'CLAUDE.md'));
|
|
184
|
+
console.log('Copied CLAUDE.md → .claude/CLAUDE.md');
|
|
185
|
+
}
|
|
132
186
|
}
|
|
133
|
-
}
|
|
134
187
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
if (fs.existsSync(rulesSrc)) {
|
|
189
|
+
copyDir(rulesSrc, path.join(cwd, cfg.rulesDir));
|
|
190
|
+
console.log(`Copied rules → ${cfg.rulesDir}`);
|
|
191
|
+
}
|
|
192
|
+
if (fs.existsSync(commandsSrc)) {
|
|
193
|
+
copyDir(commandsSrc, path.join(cwd, cfg.commandsDir));
|
|
194
|
+
console.log(`Copied commands → ${cfg.commandsDir}`);
|
|
195
|
+
}
|
|
143
196
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
197
|
+
if (tool === 'claude' && cfg.optionalClaudeDirs) {
|
|
198
|
+
for (const name of cfg.optionalClaudeDirs) {
|
|
199
|
+
const src = path.join(base, name);
|
|
200
|
+
if (fs.existsSync(src)) {
|
|
201
|
+
copyDir(src, path.join(cwd, '.claude', name));
|
|
202
|
+
console.log(`Copied ${name}/ → .claude/${name}/`);
|
|
203
|
+
}
|
|
150
204
|
}
|
|
151
205
|
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
console.error(`init failed: ${msg}`);
|
|
209
|
+
process.exit(1);
|
|
152
210
|
}
|
|
153
211
|
}
|
|
154
212
|
|
|
@@ -186,15 +244,30 @@ function main() {
|
|
|
186
244
|
const parsed = parseArgs(argv);
|
|
187
245
|
const [command, tool] = parsed._;
|
|
188
246
|
|
|
247
|
+
if (parsed.presetError) {
|
|
248
|
+
console.error(parsed.presetError);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
189
252
|
if (command === 'init') {
|
|
253
|
+
if (!tool) {
|
|
254
|
+
console.error('Missing <cursor|claude> after init.');
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
190
257
|
cmdInit(tool, parsed.preset);
|
|
191
258
|
return;
|
|
192
259
|
}
|
|
193
260
|
if (command === 'clean') {
|
|
261
|
+
if (!tool) {
|
|
262
|
+
console.error('Missing <cursor|claude> after clean.');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
194
265
|
cmdClean(tool);
|
|
195
266
|
return;
|
|
196
267
|
}
|
|
197
268
|
|
|
269
|
+
const got = command === undefined ? 'none' : command;
|
|
270
|
+
console.error(`Unknown command: ${got}. Use init or clean.`);
|
|
198
271
|
printHelp();
|
|
199
272
|
process.exit(1);
|
|
200
273
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bonesofspring/ai-rules",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Presets of Cursor and Claude rules/commands for Revy Ross personal use",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Revy Ross",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"ai-rules": "./bin/cli.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
+
"README.md",
|
|
22
23
|
"bin",
|
|
23
24
|
"presets"
|
|
24
25
|
],
|