@bonesofspring/ai-rules 0.1.41 → 0.1.42
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 +10 -1
- package/bin/cli.js +193 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,16 @@ npx @bonesofspring/ai-rules clean cursor
|
|
|
13
13
|
|
|
14
14
|
Global: `npm i -g @bonesofspring/ai-rules`, then `ai-rules ...`.
|
|
15
15
|
|
|
16
|
-
`init` merges into the current directory: dirs are created, matching files are **overwritten**. `--preset`
|
|
16
|
+
`init` merges into the **current working directory** (or `--cwd <dir>`): dirs are created, matching files are **overwritten**. `--preset` defaults to **`next`**. Supported forms:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @bonesofspring/ai-rules init cursor --preset next
|
|
20
|
+
npx @bonesofspring/ai-rules init cursor next
|
|
21
|
+
npx @bonesofspring/ai-rules init cursor # preset next by default
|
|
22
|
+
npx @bonesofspring/ai-rules cursor init --preset next
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
If `.cursor` does not appear, check the command output for errors and confirm you run init from the **target project root** (where the app lives), not from `node_modules`. Success ends with: `Done. .cursor installed at <absolute-path>`.
|
|
17
26
|
|
|
18
27
|
`clean cursor` / `clean claude` removes what this CLI installs: `rules`, `commands`, optional `agents`, `hooks`, `skills`, `team`, Cursor `hooks.json` / `BUGBOT.md`, and Claude `CLAUDE.md` when present. Other files under `.cursor/` or `.claude/` are left intact.
|
|
19
28
|
|
package/bin/cli.js
CHANGED
|
@@ -5,58 +5,86 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
-
const packageRoot = path.join(__dirname, '..');
|
|
9
|
-
|
|
10
8
|
/** Single path segment: letters, digits, dot, underscore, hyphen; no separators or "..". */
|
|
11
9
|
const PRESET_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
10
|
+
const DEFAULT_PRESET = 'next';
|
|
11
|
+
const TOOLS = new Set(['cursor', 'claude']);
|
|
12
12
|
|
|
13
13
|
const PRESETS = {
|
|
14
14
|
cursor: {
|
|
15
15
|
rulesDir: '.cursor/rules',
|
|
16
16
|
commandsDir: '.cursor/commands',
|
|
17
17
|
relBase: ['presets', 'cursor'],
|
|
18
|
-
/** Optional dirs under preset root → under `.cursor/` (recursive copy). */
|
|
19
18
|
optionalCursorDirs: ['agents', 'hooks', 'skills', 'team'],
|
|
20
|
-
/** Copied from preset root to `.cursor/hooks.json` when present. */
|
|
21
19
|
hooksJsonSrc: 'hooks.json',
|
|
22
|
-
/** Optional files under preset root → under `.cursor/`. */
|
|
23
20
|
toolRootFiles: ['BUGBOT.md', 'AGENTS.md'],
|
|
24
21
|
},
|
|
25
22
|
claude: {
|
|
26
23
|
rulesDir: '.claude/rules',
|
|
27
24
|
commandsDir: '.claude/commands',
|
|
28
25
|
relBase: ['presets', 'claude'],
|
|
29
|
-
/** Optional dirs under preset root → under `.claude/` (recursive copy). */
|
|
30
26
|
optionalClaudeDirs: ['skills', 'agents', 'hooks', 'team'],
|
|
31
|
-
/** Copied from preset root to `.claude/CLAUDE.md` when present. */
|
|
32
27
|
claudeMdSrc: 'CLAUDE.md',
|
|
33
28
|
},
|
|
34
29
|
};
|
|
35
30
|
|
|
31
|
+
function getPackageRoot() {
|
|
32
|
+
try {
|
|
33
|
+
return path.dirname(require.resolve('@bonesofspring/ai-rules/package.json'));
|
|
34
|
+
} catch {
|
|
35
|
+
return path.join(__dirname, '..');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
36
39
|
function printHelp() {
|
|
37
40
|
console.log(`@bonesofspring/ai-rules
|
|
38
41
|
|
|
39
42
|
Usage:
|
|
40
|
-
ai-rules init <cursor|claude> --preset <name>
|
|
41
|
-
ai-rules
|
|
43
|
+
ai-rules init <cursor|claude> [--preset <name>]
|
|
44
|
+
ai-rules init <cursor|claude> <preset-name>
|
|
45
|
+
ai-rules clean <cursor|claude> [--cwd <dir>]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--preset <name> Preset name (default: ${DEFAULT_PRESET})
|
|
49
|
+
--cwd <dir> Target project directory (default: current working directory)
|
|
42
50
|
|
|
43
51
|
Notes:
|
|
44
|
-
init merges into
|
|
45
|
-
Cursor init
|
|
46
|
-
Claude init
|
|
47
|
-
|
|
48
|
-
Flags may appear before or after the subcommand (e.g. --preset next init cursor).
|
|
52
|
+
init merges into the target directory; same-named files are overwritten.
|
|
53
|
+
Cursor init copies rules, commands, agents, hooks, skills, team, hooks.json, BUGBOT.md, AGENTS.md.
|
|
54
|
+
Claude init copies rules, commands, skills, agents, hooks, team, and CLAUDE.md when present.
|
|
55
|
+
Command forms: "init cursor --preset next", "init cursor next", "--preset next init cursor", "cursor init --preset next".
|
|
49
56
|
|
|
50
57
|
Examples:
|
|
51
58
|
npx @bonesofspring/ai-rules init cursor --preset next
|
|
52
|
-
npx @bonesofspring/ai-rules
|
|
59
|
+
npx @bonesofspring/ai-rules init cursor next
|
|
60
|
+
npx @bonesofspring/ai-rules init claude
|
|
61
|
+
npx @bonesofspring/ai-rules clean cursor
|
|
53
62
|
`);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function parseArgs(argv) {
|
|
57
|
-
const args = { _: [] };
|
|
66
|
+
const args = { _: [], cwd: undefined, preset: undefined, presetError: undefined };
|
|
58
67
|
for (let i = 0; i < argv.length; i++) {
|
|
59
68
|
const a = argv[i];
|
|
69
|
+
if (a === '--cwd') {
|
|
70
|
+
const value = argv[i + 1];
|
|
71
|
+
if (value === undefined || value.startsWith('-')) {
|
|
72
|
+
args.cwdError = 'Missing value for --cwd';
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
args.cwd = value;
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (a.startsWith('--cwd=')) {
|
|
80
|
+
const value = a.slice('--cwd='.length);
|
|
81
|
+
if (value === '') {
|
|
82
|
+
args.cwdError = 'Missing value after --cwd=';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
args.cwd = value;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
60
88
|
if (a === '--preset') {
|
|
61
89
|
const value = argv[i + 1];
|
|
62
90
|
if (value === undefined || value.startsWith('-')) {
|
|
@@ -77,17 +105,53 @@ function parseArgs(argv) {
|
|
|
77
105
|
args.preset = value;
|
|
78
106
|
continue;
|
|
79
107
|
}
|
|
108
|
+
if (a === '-h' || a === '--help') {
|
|
109
|
+
args.help = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
80
112
|
args._.push(a);
|
|
81
113
|
}
|
|
82
114
|
return args;
|
|
83
115
|
}
|
|
84
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Supports: init cursor, cursor init, init cursor next (positional preset).
|
|
119
|
+
*/
|
|
120
|
+
function normalizeInitClean(parsed) {
|
|
121
|
+
const positional = [...parsed._];
|
|
122
|
+
let command;
|
|
123
|
+
let tool;
|
|
124
|
+
let presetFromPositional;
|
|
125
|
+
|
|
126
|
+
if (positional.length >= 2 && TOOLS.has(positional[0]) && positional[1] === 'init') {
|
|
127
|
+
command = 'init';
|
|
128
|
+
tool = positional[0];
|
|
129
|
+
presetFromPositional = positional[2];
|
|
130
|
+
} else if (positional.length >= 2 && positional[0] === 'init' && TOOLS.has(positional[1])) {
|
|
131
|
+
command = 'init';
|
|
132
|
+
tool = positional[1];
|
|
133
|
+
presetFromPositional = positional[2];
|
|
134
|
+
} else if (positional.length >= 2 && positional[0] === 'clean' && TOOLS.has(positional[1])) {
|
|
135
|
+
command = 'clean';
|
|
136
|
+
tool = positional[1];
|
|
137
|
+
} else if (positional.length >= 1) {
|
|
138
|
+
command = positional[0];
|
|
139
|
+
tool = positional[1];
|
|
140
|
+
if (command === 'init' && positional[2] && PRESET_NAME_RE.test(positional[2])) {
|
|
141
|
+
presetFromPositional = positional[2];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const preset = parsed.preset || presetFromPositional;
|
|
146
|
+
return { command, tool, preset };
|
|
147
|
+
}
|
|
148
|
+
|
|
85
149
|
/**
|
|
86
150
|
* @returns {string | null} Error message or null if valid.
|
|
87
151
|
*/
|
|
88
152
|
function validatePresetName(preset) {
|
|
89
153
|
if (preset === undefined || preset === '') {
|
|
90
|
-
return 'Missing --preset <name
|
|
154
|
+
return 'Missing preset name (use --preset <name>, e.g. --preset next)';
|
|
91
155
|
}
|
|
92
156
|
if (!PRESET_NAME_RE.test(preset) || preset.includes('..')) {
|
|
93
157
|
return (
|
|
@@ -98,9 +162,6 @@ function validatePresetName(preset) {
|
|
|
98
162
|
return null;
|
|
99
163
|
}
|
|
100
164
|
|
|
101
|
-
/**
|
|
102
|
-
* Ensures resolved preset dir stays under the tool presets root (defense in depth).
|
|
103
|
-
*/
|
|
104
165
|
function assertPresetDirInsideRoot(presetsRoot, presetBase) {
|
|
105
166
|
const root = path.resolve(presetsRoot);
|
|
106
167
|
const resolved = path.resolve(presetBase);
|
|
@@ -147,6 +208,17 @@ function removeFileIfExists(filePath) {
|
|
|
147
208
|
}
|
|
148
209
|
}
|
|
149
210
|
|
|
211
|
+
function listPresetNames(presetsRoot) {
|
|
212
|
+
if (!fs.existsSync(presetsRoot)) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
return fs
|
|
216
|
+
.readdirSync(presetsRoot, { withFileTypes: true })
|
|
217
|
+
.filter((e) => e.isDirectory())
|
|
218
|
+
.map((e) => e.name)
|
|
219
|
+
.filter((name) => PRESET_NAME_RE.test(name));
|
|
220
|
+
}
|
|
221
|
+
|
|
150
222
|
function presetHasAnySource(base, cfg) {
|
|
151
223
|
const rulesSrc = path.join(base, 'rules');
|
|
152
224
|
const commandsSrc = path.join(base, 'commands');
|
|
@@ -172,74 +244,121 @@ function presetHasAnySource(base, cfg) {
|
|
|
172
244
|
return false;
|
|
173
245
|
}
|
|
174
246
|
|
|
175
|
-
function
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
178
|
-
console.error(`
|
|
247
|
+
function resolveTargetCwd(cwdOption) {
|
|
248
|
+
const cwd = cwdOption ? path.resolve(cwdOption) : process.cwd();
|
|
249
|
+
if (!fs.existsSync(cwd)) {
|
|
250
|
+
console.error(`Target directory does not exist: ${cwd}`);
|
|
179
251
|
process.exit(1);
|
|
180
252
|
}
|
|
181
|
-
|
|
253
|
+
if (!fs.statSync(cwd).isDirectory()) {
|
|
254
|
+
console.error(`Target path is not a directory: ${cwd}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
return cwd;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resolvePresetName(tool, preset, packageRoot) {
|
|
261
|
+
const cfg = PRESETS[tool];
|
|
262
|
+
const presetsRoot = path.join(packageRoot, ...cfg.relBase);
|
|
263
|
+
let name = preset || DEFAULT_PRESET;
|
|
264
|
+
|
|
265
|
+
const nameErr = validatePresetName(name);
|
|
182
266
|
if (nameErr) {
|
|
183
267
|
console.error(nameErr);
|
|
184
268
|
process.exit(1);
|
|
185
269
|
}
|
|
186
|
-
|
|
187
|
-
|
|
270
|
+
|
|
271
|
+
let base = path.join(presetsRoot, name);
|
|
272
|
+
if (!presetHasAnySource(base, cfg)) {
|
|
273
|
+
const available = listPresetNames(presetsRoot);
|
|
274
|
+
if (!preset && available.includes(DEFAULT_PRESET)) {
|
|
275
|
+
name = DEFAULT_PRESET;
|
|
276
|
+
base = path.join(presetsRoot, name);
|
|
277
|
+
} else {
|
|
278
|
+
console.error(`Preset "${name}" for ${tool} not found.`);
|
|
279
|
+
console.error(`Looked in: ${base}`);
|
|
280
|
+
console.error(`Package root: ${packageRoot}`);
|
|
281
|
+
if (available.length > 0) {
|
|
282
|
+
console.error(`Available presets: ${available.join(', ')}`);
|
|
283
|
+
} else {
|
|
284
|
+
console.error(`No presets directory at: ${presetsRoot}`);
|
|
285
|
+
console.error(
|
|
286
|
+
'The installed package may be incomplete. Reinstall @bonesofspring/ai-rules or run from the package source.',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
188
293
|
assertPresetDirInsideRoot(presetsRoot, base);
|
|
294
|
+
return { name, base, presetsRoot };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cmdInit(tool, preset, cwdOption) {
|
|
298
|
+
const cfg = PRESETS[tool];
|
|
299
|
+
if (!cfg) {
|
|
300
|
+
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const packageRoot = getPackageRoot();
|
|
305
|
+
const cwd = resolveTargetCwd(cwdOption);
|
|
306
|
+
const { name: presetName, base } = resolvePresetName(tool, preset, packageRoot);
|
|
189
307
|
|
|
190
308
|
const rulesSrc = path.join(base, 'rules');
|
|
191
309
|
const commandsSrc = path.join(base, 'commands');
|
|
192
|
-
const
|
|
310
|
+
const toolRoot = tool === 'cursor' ? '.cursor' : '.claude';
|
|
311
|
+
const toolRootAbs = path.join(cwd, toolRoot);
|
|
193
312
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
313
|
+
fs.mkdirSync(toolRootAbs, { recursive: true });
|
|
314
|
+
|
|
315
|
+
let copiedSteps = 0;
|
|
198
316
|
|
|
199
317
|
try {
|
|
200
318
|
if (tool === 'claude' && cfg.claudeMdSrc) {
|
|
201
319
|
const mdFrom = path.join(base, cfg.claudeMdSrc);
|
|
202
320
|
if (fs.existsSync(mdFrom)) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log('Copied CLAUDE.md → .claude/CLAUDE.md');
|
|
321
|
+
fs.copyFileSync(mdFrom, path.join(toolRootAbs, 'CLAUDE.md'));
|
|
322
|
+
console.log(`Copied CLAUDE.md → ${path.join(toolRoot, 'CLAUDE.md')}`);
|
|
323
|
+
copiedSteps++;
|
|
207
324
|
}
|
|
208
325
|
}
|
|
209
326
|
|
|
210
327
|
if (fs.existsSync(rulesSrc)) {
|
|
211
328
|
copyDir(rulesSrc, path.join(cwd, cfg.rulesDir));
|
|
212
329
|
console.log(`Copied rules → ${cfg.rulesDir}`);
|
|
330
|
+
copiedSteps++;
|
|
213
331
|
}
|
|
214
332
|
if (fs.existsSync(commandsSrc)) {
|
|
215
333
|
copyDir(commandsSrc, path.join(cwd, cfg.commandsDir));
|
|
216
334
|
console.log(`Copied commands → ${cfg.commandsDir}`);
|
|
335
|
+
copiedSteps++;
|
|
217
336
|
}
|
|
218
337
|
|
|
219
|
-
const toolRoot = tool === 'cursor' ? '.cursor' : '.claude';
|
|
220
338
|
for (const name of getOptionalDirs(cfg)) {
|
|
221
339
|
const src = path.join(base, name);
|
|
222
340
|
if (fs.existsSync(src)) {
|
|
223
|
-
copyDir(src, path.join(
|
|
341
|
+
copyDir(src, path.join(toolRootAbs, name));
|
|
224
342
|
console.log(`Copied ${name}/ → ${toolRoot}/${name}/`);
|
|
343
|
+
copiedSteps++;
|
|
225
344
|
}
|
|
226
345
|
}
|
|
227
346
|
|
|
228
347
|
if (tool === 'cursor' && cfg.hooksJsonSrc) {
|
|
229
348
|
const hooksFrom = path.join(base, cfg.hooksJsonSrc);
|
|
230
349
|
if (fs.existsSync(hooksFrom)) {
|
|
231
|
-
fs.
|
|
232
|
-
|
|
233
|
-
|
|
350
|
+
fs.copyFileSync(hooksFrom, path.join(toolRootAbs, 'hooks.json'));
|
|
351
|
+
console.log(`Copied hooks.json → ${toolRoot}/hooks.json`);
|
|
352
|
+
copiedSteps++;
|
|
234
353
|
}
|
|
235
354
|
}
|
|
236
355
|
|
|
237
356
|
for (const name of cfg.toolRootFiles || []) {
|
|
238
357
|
const from = path.join(base, name);
|
|
239
358
|
if (fs.existsSync(from)) {
|
|
240
|
-
fs.
|
|
241
|
-
fs.copyFileSync(from, path.join(cwd, toolRoot, name));
|
|
359
|
+
fs.copyFileSync(from, path.join(toolRootAbs, name));
|
|
242
360
|
console.log(`Copied ${name} → ${toolRoot}/${name}`);
|
|
361
|
+
copiedSteps++;
|
|
243
362
|
}
|
|
244
363
|
}
|
|
245
364
|
} catch (err) {
|
|
@@ -247,15 +366,27 @@ function cmdInit(tool, preset) {
|
|
|
247
366
|
console.error(`init failed: ${msg}`);
|
|
248
367
|
process.exit(1);
|
|
249
368
|
}
|
|
369
|
+
|
|
370
|
+
if (copiedSteps === 0) {
|
|
371
|
+
console.error(`init failed: preset "${presetName}" had no files to copy from ${base}`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!fs.existsSync(toolRootAbs)) {
|
|
376
|
+
console.error(`init failed: ${toolRootAbs} was not created`);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`Done. ${toolRoot} installed at ${toolRootAbs} (preset: ${presetName})`);
|
|
250
381
|
}
|
|
251
382
|
|
|
252
|
-
function cmdClean(tool) {
|
|
383
|
+
function cmdClean(tool, cwdOption) {
|
|
253
384
|
const cfg = PRESETS[tool];
|
|
254
385
|
if (!cfg) {
|
|
255
386
|
console.error(`Unknown tool: ${tool}. Use cursor or claude.`);
|
|
256
387
|
process.exit(1);
|
|
257
388
|
}
|
|
258
|
-
const cwd =
|
|
389
|
+
const cwd = resolveTargetCwd(cwdOption);
|
|
259
390
|
const toolRoot = tool === 'cursor' ? '.cursor' : '.claude';
|
|
260
391
|
const removedParts = [cfg.rulesDir, cfg.commandsDir];
|
|
261
392
|
|
|
@@ -288,33 +419,42 @@ function cmdClean(tool) {
|
|
|
288
419
|
|
|
289
420
|
function main() {
|
|
290
421
|
const argv = process.argv.slice(2);
|
|
291
|
-
if (argv.length === 0
|
|
422
|
+
if (argv.length === 0) {
|
|
292
423
|
printHelp();
|
|
293
424
|
process.exit(0);
|
|
294
425
|
}
|
|
295
426
|
|
|
296
427
|
const parsed = parseArgs(argv);
|
|
297
|
-
|
|
428
|
+
if (parsed.help) {
|
|
429
|
+
printHelp();
|
|
430
|
+
process.exit(0);
|
|
431
|
+
}
|
|
298
432
|
|
|
299
433
|
if (parsed.presetError) {
|
|
300
434
|
console.error(parsed.presetError);
|
|
301
435
|
process.exit(1);
|
|
302
436
|
}
|
|
437
|
+
if (parsed.cwdError) {
|
|
438
|
+
console.error(parsed.cwdError);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const { command, tool, preset } = normalizeInitClean(parsed);
|
|
303
443
|
|
|
304
444
|
if (command === 'init') {
|
|
305
|
-
if (!tool) {
|
|
306
|
-
console.error('Missing
|
|
445
|
+
if (!tool || !TOOLS.has(tool)) {
|
|
446
|
+
console.error('Missing or invalid tool. Use: init cursor|claude [--preset next]');
|
|
307
447
|
process.exit(1);
|
|
308
448
|
}
|
|
309
|
-
cmdInit(tool, parsed.
|
|
449
|
+
cmdInit(tool, preset, parsed.cwd);
|
|
310
450
|
return;
|
|
311
451
|
}
|
|
312
452
|
if (command === 'clean') {
|
|
313
|
-
if (!tool) {
|
|
314
|
-
console.error('Missing
|
|
453
|
+
if (!tool || !TOOLS.has(tool)) {
|
|
454
|
+
console.error('Missing or invalid tool. Use: clean cursor|claude');
|
|
315
455
|
process.exit(1);
|
|
316
456
|
}
|
|
317
|
-
cmdClean(tool);
|
|
457
|
+
cmdClean(tool, parsed.cwd);
|
|
318
458
|
return;
|
|
319
459
|
}
|
|
320
460
|
|