@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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/bin/cli.js +100 -27
  3. 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
- args.preset = argv[++i];
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
- args.preset = a.slice('--preset='.length);
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
- if (!preset) {
112
- console.error('Missing --preset <name>');
159
+ const nameErr = validatePresetName(preset);
160
+ if (nameErr) {
161
+ console.error(nameErr);
113
162
  process.exit(1);
114
163
  }
115
- const base = path.join(packageRoot, ...cfg.relBase, preset);
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
- if (tool === 'claude' && cfg.claudeMdSrc) {
126
- const mdFrom = path.join(base, cfg.claudeMdSrc);
127
- if (fs.existsSync(mdFrom)) {
128
- const claudeRoot = path.join(cwd, '.claude');
129
- fs.mkdirSync(claudeRoot, { recursive: true });
130
- fs.copyFileSync(mdFrom, path.join(claudeRoot, 'CLAUDE.md'));
131
- console.log('Copied CLAUDE.md → .claude/CLAUDE.md');
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
- if (fs.existsSync(rulesSrc)) {
136
- copyDir(rulesSrc, path.join(cwd, cfg.rulesDir));
137
- console.log(`Copied rules → ${cfg.rulesDir}`);
138
- }
139
- if (fs.existsSync(commandsSrc)) {
140
- copyDir(commandsSrc, path.join(cwd, cfg.commandsDir));
141
- console.log(`Copied commands → ${cfg.commandsDir}`);
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
- if (tool === 'claude' && cfg.optionalClaudeDirs) {
145
- for (const name of cfg.optionalClaudeDirs) {
146
- const src = path.join(base, name);
147
- if (fs.existsSync(src)) {
148
- copyDir(src, path.join(cwd, '.claude', name));
149
- console.log(`Copied ${name}/ → .claude/${name}/`);
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.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
  ],