@bonesofspring/ai-rules 0.1.1 → 0.1.2

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 (2) hide show
  1. package/bin/cli.js +100 -27
  2. package/package.json +1 -1
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.2",
4
4
  "description": "Presets of Cursor and Claude rules/commands for Revy Ross personal use",
5
5
  "license": "MIT",
6
6
  "author": "Revy Ross",