@malamute/ai-rules 1.0.0 → 1.2.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.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
package/src/cli.js ADDED
@@ -0,0 +1,220 @@
1
+ const readline = require('readline');
2
+ const { colors, log } = require('./utils');
3
+ const { VERSION, AVAILABLE_TECHS } = require('./config');
4
+ const { init, update, status, listTechnologies } = require('./installer');
5
+
6
+ function printUsage() {
7
+ console.log(`
8
+ ${colors.bold('AI Rules')} v${VERSION} - Claude Code configuration boilerplates
9
+
10
+ ${colors.bold('Usage:')}
11
+ ai-rules init [tech] [tech2] [options]
12
+ ai-rules update [options]
13
+ ai-rules status
14
+ ai-rules list
15
+
16
+ ${colors.bold('Commands:')}
17
+ init Install configuration (interactive if no tech specified)
18
+ update Update installed configs to latest version
19
+ status Show current installation status
20
+ list List available technologies
21
+
22
+ ${colors.bold('Technologies:')}
23
+ angular Angular 21 + Nx + NgRx
24
+ nextjs Next.js 15 + React 19
25
+ nestjs NestJS + Prisma/TypeORM
26
+ dotnet .NET 9 + EF Core
27
+ fastapi FastAPI + SQLAlchemy + Pydantic
28
+ flask Flask + SQLAlchemy + Marshmallow
29
+
30
+ ${colors.bold('Options:')}
31
+ --minimal Only install CLAUDE.md, settings.json, and tech rules (no shared skills/rules)
32
+ --target <dir> Target directory (default: current directory)
33
+ --dry-run Preview changes without writing files
34
+ --force Overwrite files without backup (update command)
35
+
36
+ ${colors.bold('Examples:')}
37
+ ai-rules init # Interactive mode
38
+ ai-rules init angular # Full install (skills + rules)
39
+ ai-rules init angular --minimal # Minimal install
40
+ ai-rules init nextjs --dry-run
41
+ ai-rules update
42
+ ai-rules update --force
43
+ ai-rules status
44
+ `);
45
+ }
46
+
47
+ async function prompt(question) {
48
+ const rl = readline.createInterface({
49
+ input: process.stdin,
50
+ output: process.stdout,
51
+ });
52
+
53
+ return new Promise((resolve) => {
54
+ rl.question(question, (answer) => {
55
+ rl.close();
56
+ resolve(answer.trim());
57
+ });
58
+ });
59
+ }
60
+
61
+ async function multiSelect(message, choices) {
62
+ console.log(`\n${colors.bold(message)}`);
63
+ console.log(colors.dim('(enter numbers separated by spaces, or "all")'));
64
+ console.log('');
65
+
66
+ choices.forEach((choice, i) => {
67
+ console.log(` ${colors.cyan(i + 1)}. ${choice.name} ${colors.dim(`- ${choice.description}`)}`);
68
+ });
69
+
70
+ console.log('');
71
+ const answer = await prompt('Your selection: ');
72
+
73
+ if (answer.toLowerCase() === 'all') {
74
+ return choices.map((c) => c.value);
75
+ }
76
+
77
+ const indices = answer
78
+ .split(/[\s,]+/)
79
+ .map((s) => parseInt(s, 10) - 1)
80
+ .filter((i) => i >= 0 && i < choices.length);
81
+
82
+ return indices.map((i) => choices[i].value);
83
+ }
84
+
85
+ async function interactiveInit() {
86
+ console.log(`\n${colors.bold('AI Rules')} - Interactive Setup\n`);
87
+
88
+ const techChoices = [
89
+ { name: 'Angular', value: 'angular', description: 'Angular 21 + Nx + NgRx + Signals' },
90
+ { name: 'Next.js', value: 'nextjs', description: 'Next.js 15 + React 19 + App Router' },
91
+ { name: 'NestJS', value: 'nestjs', description: 'NestJS 11 + Prisma/TypeORM + Passport' },
92
+ { name: '.NET', value: 'dotnet', description: '.NET 9 + ASP.NET Core + EF Core' },
93
+ { name: 'FastAPI', value: 'fastapi', description: 'FastAPI + SQLAlchemy 2.0 + Pydantic v2' },
94
+ { name: 'Flask', value: 'flask', description: 'Flask + SQLAlchemy 2.0 + Marshmallow' },
95
+ ];
96
+
97
+ const techs = await multiSelect('Select technologies:', techChoices);
98
+
99
+ if (techs.length === 0) {
100
+ log.error('No technology selected');
101
+ process.exit(1);
102
+ }
103
+
104
+ const extraChoices = [
105
+ { name: 'Skills', value: 'skills', description: '/learning, /review, /spec, /debug, etc.' },
106
+ { name: 'Shared Rules', value: 'rules', description: 'security, performance, accessibility' },
107
+ ];
108
+
109
+ const extras = await multiSelect('Include extras:', extraChoices);
110
+
111
+ const targetDir = await prompt(`Target directory ${colors.dim('(. for current)')}: `) || '.';
112
+
113
+ const options = {
114
+ target: targetDir === '.' ? null : targetDir,
115
+ withSkills: extras.includes('skills'),
116
+ withRules: extras.includes('rules'),
117
+ all: false,
118
+ dryRun: false,
119
+ force: false,
120
+ };
121
+
122
+ console.log('');
123
+ return { techs, options };
124
+ }
125
+
126
+ async function run(args) {
127
+ if (args.includes('--help') || args.includes('-h')) {
128
+ printUsage();
129
+ return;
130
+ }
131
+
132
+ if (args.length === 0) {
133
+ printUsage();
134
+ return;
135
+ }
136
+
137
+ const command = args[0];
138
+
139
+ if (command === 'list') {
140
+ listTechnologies();
141
+ return;
142
+ }
143
+
144
+ if (command === 'status') {
145
+ const targetIndex = args.indexOf('--target');
146
+ const targetDir = targetIndex !== -1 ? args[targetIndex + 1] : process.cwd();
147
+ status(targetDir);
148
+ return;
149
+ }
150
+
151
+ if (command === 'update') {
152
+ const options = {
153
+ target: null,
154
+ dryRun: args.includes('--dry-run'),
155
+ force: args.includes('--force'),
156
+ };
157
+
158
+ const targetIndex = args.indexOf('--target');
159
+ if (targetIndex !== -1) {
160
+ options.target = args[targetIndex + 1];
161
+ }
162
+
163
+ await update(options);
164
+ return;
165
+ }
166
+
167
+ if (command === 'init') {
168
+ const minimal = args.includes('--minimal');
169
+ const options = {
170
+ target: null,
171
+ withSkills: !minimal,
172
+ withRules: !minimal,
173
+ dryRun: args.includes('--dry-run'),
174
+ force: args.includes('--force'),
175
+ };
176
+
177
+ const techs = [];
178
+
179
+ for (let i = 1; i < args.length; i++) {
180
+ const arg = args[i];
181
+
182
+ if (arg === '--minimal') {
183
+ // Already handled above
184
+ } else if (arg === '--target') {
185
+ options.target = args[++i];
186
+ } else if (arg === '--dry-run' || arg === '--force') {
187
+ // Already handled
188
+ } else if (!arg.startsWith('-')) {
189
+ if (AVAILABLE_TECHS.includes(arg)) {
190
+ techs.push(arg);
191
+ } else {
192
+ log.error(`Unknown technology: ${arg}`);
193
+ console.log(`Available: ${AVAILABLE_TECHS.join(', ')}`);
194
+ process.exit(1);
195
+ }
196
+ }
197
+ }
198
+
199
+ // Interactive mode if no techs specified
200
+ if (techs.length === 0) {
201
+ try {
202
+ const result = await interactiveInit();
203
+ init(result.techs, { ...options, ...result.options });
204
+ } catch (_e) {
205
+ console.log('\nAborted.');
206
+ process.exit(0);
207
+ }
208
+ return;
209
+ }
210
+
211
+ init(techs, options);
212
+ return;
213
+ }
214
+
215
+ log.error(`Unknown command: ${command}`);
216
+ printUsage();
217
+ process.exit(1);
218
+ }
219
+
220
+ module.exports = { run };
package/src/config.js ADDED
@@ -0,0 +1,29 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const CONFIGS_DIR = path.join(__dirname, '..', 'configs');
5
+ const AVAILABLE_TECHS = ['angular', 'nextjs', 'nestjs', 'dotnet', 'fastapi', 'flask'];
6
+ const VERSION = require('../package.json').version;
7
+
8
+ const TECH_CONFIG = JSON.parse(
9
+ fs.readFileSync(path.join(__dirname, 'tech-config.json'), 'utf8')
10
+ );
11
+
12
+ function getRuleCategoriesToInclude(techs) {
13
+ const categories = new Set();
14
+ for (const tech of techs) {
15
+ const config = TECH_CONFIG.technologies[tech];
16
+ if (config?.includeRules) {
17
+ config.includeRules.forEach((cat) => categories.add(cat));
18
+ }
19
+ }
20
+ return categories;
21
+ }
22
+
23
+ module.exports = {
24
+ CONFIGS_DIR,
25
+ AVAILABLE_TECHS,
26
+ VERSION,
27
+ TECH_CONFIG,
28
+ getRuleCategoriesToInclude,
29
+ };
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ const { run } = require('./cli');
2
+ const { init, update, status } = require('./installer');
3
+ const { readManifest } = require('./merge');
4
+ const { VERSION } = require('./config');
5
+
6
+ module.exports = {
7
+ run,
8
+ init,
9
+ update,
10
+ status,
11
+ readManifest,
12
+ VERSION,
13
+ };
@@ -0,0 +1,361 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { colors, log, getFilesRecursive, copyDirRecursive, backupFile } = require('./utils');
4
+ const { CONFIGS_DIR, AVAILABLE_TECHS, VERSION, TECH_CONFIG, getRuleCategoriesToInclude } = require('./config');
5
+ const { mergeClaudeMd, mergeSettingsJson, readManifest, writeManifest } = require('./merge');
6
+
7
+ function listTechnologies() {
8
+ console.log(`\n${colors.bold('Available technologies:')}\n`);
9
+
10
+ const techInfo = {
11
+ angular: 'Angular 21 + Nx + NgRx + Signals',
12
+ nextjs: 'Next.js 15 + React 19 + App Router',
13
+ nestjs: 'NestJS 11 + Prisma/TypeORM + Passport',
14
+ dotnet: '.NET 9 + ASP.NET Core + EF Core',
15
+ fastapi: 'FastAPI + SQLAlchemy 2.0 + Pydantic v2',
16
+ flask: 'Flask + SQLAlchemy 2.0 + Marshmallow',
17
+ };
18
+
19
+ for (const tech of AVAILABLE_TECHS) {
20
+ const techPath = path.join(CONFIGS_DIR, tech);
21
+ const exists = fs.existsSync(techPath);
22
+ const status = exists ? colors.green('✓') : colors.red('✗');
23
+ console.log(` ${status} ${colors.bold(tech.padEnd(10))} ${techInfo[tech]}`);
24
+ }
25
+
26
+ console.log(`\n${colors.bold('Shared resources:')}\n`);
27
+
28
+ const sharedPath = path.join(CONFIGS_DIR, '_shared');
29
+ const skills = fs.existsSync(path.join(sharedPath, '.claude', 'skills'));
30
+ const rules = fs.existsSync(path.join(sharedPath, '.claude', 'rules'));
31
+
32
+ console.log(` ${skills ? colors.green('✓') : colors.red('✗')} skills /learning, /review, /spec, /debug, and more`);
33
+ console.log(` ${rules ? colors.green('✓') : colors.red('✗')} rules security, performance, accessibility`);
34
+ console.log('');
35
+ }
36
+
37
+ function status(targetDir) {
38
+ const manifest = readManifest(targetDir);
39
+
40
+ console.log(`\n${colors.bold('AI Rules Status')}\n`);
41
+ console.log(` Directory: ${targetDir}`);
42
+ console.log('');
43
+
44
+ if (!manifest) {
45
+ log.warning('No ai-rules installation detected');
46
+ console.log('');
47
+ console.log(` Run ${colors.cyan('ai-rules init')} to install configurations.`);
48
+ console.log('');
49
+ return;
50
+ }
51
+
52
+ console.log(` ${colors.bold('Installed version:')} ${manifest.version}`);
53
+ console.log(` ${colors.bold('Latest version:')} ${VERSION}`);
54
+ console.log(` ${colors.bold('Installed at:')} ${new Date(manifest.installedAt).toLocaleString()}`);
55
+ console.log('');
56
+
57
+ if (manifest.technologies?.length) {
58
+ console.log(` ${colors.bold('Technologies:')}`);
59
+ manifest.technologies.forEach((tech) => {
60
+ console.log(` ${colors.green('✓')} ${tech}`);
61
+ });
62
+ console.log('');
63
+ }
64
+
65
+ if (manifest.options) {
66
+ console.log(` ${colors.bold('Options:')}`);
67
+ if (manifest.options.withSkills) console.log(` ${colors.green('✓')} skills`);
68
+ if (manifest.options.withRules) console.log(` ${colors.green('✓')} shared rules`);
69
+ console.log('');
70
+ }
71
+
72
+ if (manifest.version !== VERSION) {
73
+ console.log(` ${colors.yellow('⚠')} Update available! Run ${colors.cyan('ai-rules update')} to update.`);
74
+ console.log('');
75
+ }
76
+
77
+ const backupDir = path.join(targetDir, '.claude', 'backups');
78
+ if (fs.existsSync(backupDir)) {
79
+ const backups = getFilesRecursive(backupDir);
80
+ if (backups.length > 0) {
81
+ console.log(` ${colors.bold('Backups:')} ${backups.length} file(s) in .claude/backups/`);
82
+ console.log('');
83
+ }
84
+ }
85
+ }
86
+
87
+ function init(techs, options) {
88
+ const targetDir = path.resolve(options.target || process.cwd());
89
+ const { dryRun, force } = options;
90
+ const backup = !force;
91
+
92
+ if (dryRun) {
93
+ console.log(`\n${colors.cyan('DRY RUN')} - No files will be modified\n`);
94
+ }
95
+
96
+ log.info(`${dryRun ? 'Would install' : 'Installing'} to: ${targetDir}`);
97
+ console.log('');
98
+
99
+ const operations = [];
100
+
101
+ if (!dryRun) {
102
+ fs.mkdirSync(path.join(targetDir, '.claude', 'rules'), { recursive: true });
103
+ }
104
+
105
+ let isFirstClaudeMd = true;
106
+
107
+ for (const tech of techs) {
108
+ log.info(`${dryRun ? 'Would install' : 'Installing'} ${tech}...`);
109
+
110
+ const techDir = path.join(CONFIGS_DIR, tech);
111
+
112
+ if (!fs.existsSync(techDir)) {
113
+ log.error(`Technology directory not found: ${tech}`);
114
+ process.exit(1);
115
+ }
116
+
117
+ const claudeMdPath = path.join(techDir, 'CLAUDE.md');
118
+ if (fs.existsSync(claudeMdPath)) {
119
+ const op = mergeClaudeMd(
120
+ path.join(targetDir, 'CLAUDE.md'),
121
+ claudeMdPath,
122
+ isFirstClaudeMd,
123
+ { dryRun, backup, targetDir }
124
+ );
125
+ operations.push(op);
126
+ isFirstClaudeMd = false;
127
+
128
+ if (dryRun) {
129
+ log.dry(` CLAUDE.md (${op.type})`);
130
+ } else {
131
+ log.success(` CLAUDE.md`);
132
+ }
133
+ }
134
+
135
+ const settingsPath = path.join(techDir, '.claude', 'settings.json');
136
+ if (fs.existsSync(settingsPath)) {
137
+ const op = mergeSettingsJson(
138
+ path.join(targetDir, '.claude', 'settings.json'),
139
+ settingsPath,
140
+ { dryRun, backup, targetDir }
141
+ );
142
+ operations.push(op);
143
+
144
+ if (dryRun) {
145
+ log.dry(` settings.json (${op.type})`);
146
+ } else {
147
+ log.success(` settings.json`);
148
+ }
149
+ }
150
+
151
+ const rulesDir = path.join(techDir, '.claude', 'rules');
152
+ if (fs.existsSync(rulesDir)) {
153
+ const ops = copyDirRecursive(
154
+ rulesDir,
155
+ path.join(targetDir, '.claude', 'rules'),
156
+ { dryRun, backup, targetDir }
157
+ );
158
+ operations.push(...ops);
159
+
160
+ if (dryRun) {
161
+ log.dry(` rules/ (${ops.length} files)`);
162
+ } else {
163
+ log.success(` rules/`);
164
+ }
165
+ }
166
+
167
+ if (options.withSkills) {
168
+ const techSkillsDir = path.join(techDir, '.claude', 'skills');
169
+ if (fs.existsSync(techSkillsDir)) {
170
+ const ops = copyDirRecursive(
171
+ techSkillsDir,
172
+ path.join(targetDir, '.claude', 'skills'),
173
+ { dryRun, backup, targetDir }
174
+ );
175
+ operations.push(...ops);
176
+ }
177
+ }
178
+ }
179
+
180
+ const sharedDir = path.join(CONFIGS_DIR, '_shared');
181
+
182
+ if (options.withSkills) {
183
+ log.info(`${dryRun ? 'Would install' : 'Installing'} skills...`);
184
+ const skillsDir = path.join(sharedDir, '.claude', 'skills');
185
+ if (fs.existsSync(skillsDir)) {
186
+ const ops = copyDirRecursive(
187
+ skillsDir,
188
+ path.join(targetDir, '.claude', 'skills'),
189
+ { dryRun, backup, targetDir }
190
+ );
191
+ operations.push(...ops);
192
+
193
+ if (dryRun) {
194
+ log.dry(` skills/ (${ops.length} files)`);
195
+ } else {
196
+ log.success(` skills/`);
197
+ }
198
+ }
199
+ }
200
+
201
+ if (options.withRules) {
202
+ log.info(`${dryRun ? 'Would install' : 'Installing'} shared rules...`);
203
+ const rulesDir = path.join(sharedDir, '.claude', 'rules');
204
+ if (fs.existsSync(rulesDir)) {
205
+ const categoriesToInclude = getRuleCategoriesToInclude(techs);
206
+ const selectiveCategories = Object.keys(TECH_CONFIG.ruleCategories);
207
+ const skippedCategories = [];
208
+
209
+ const entries = fs.readdirSync(rulesDir, { withFileTypes: true });
210
+ let totalOps = 0;
211
+
212
+ for (const entry of entries) {
213
+ if (selectiveCategories.includes(entry.name) && !categoriesToInclude.has(entry.name)) {
214
+ skippedCategories.push(entry.name);
215
+ continue;
216
+ }
217
+
218
+ const srcPath = path.join(rulesDir, entry.name);
219
+ const destPath = path.join(targetDir, '.claude', 'rules', entry.name);
220
+
221
+ if (entry.isDirectory()) {
222
+ const ops = copyDirRecursive(srcPath, destPath, { dryRun, backup, targetDir });
223
+ operations.push(...ops);
224
+ totalOps += ops.length;
225
+ } else {
226
+ const exists = fs.existsSync(destPath);
227
+ const relativePath = path.relative(targetDir, destPath);
228
+
229
+ if (dryRun) {
230
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativePath });
231
+ } else {
232
+ if (exists && backup) {
233
+ backupFile(destPath, targetDir);
234
+ }
235
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
236
+ fs.copyFileSync(srcPath, destPath);
237
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativePath });
238
+ }
239
+ totalOps++;
240
+ }
241
+ }
242
+
243
+ if (dryRun) {
244
+ log.dry(` shared rules/ (${totalOps} files)`);
245
+ } else {
246
+ log.success(` shared rules/`);
247
+ }
248
+
249
+ if (skippedCategories.length > 0) {
250
+ log.info(` (skipped: ${skippedCategories.join(', ')} - not applicable)`);
251
+ }
252
+ }
253
+ }
254
+
255
+ // Resolve @../_shared/CLAUDE.md imports
256
+ const targetClaudeMd = path.join(targetDir, 'CLAUDE.md');
257
+ if (!dryRun && fs.existsSync(targetClaudeMd)) {
258
+ let content = fs.readFileSync(targetClaudeMd, 'utf8');
259
+
260
+ if (content.includes('@../_shared/CLAUDE.md')) {
261
+ const sharedClaudeMd = path.join(sharedDir, 'CLAUDE.md');
262
+ if (fs.existsSync(sharedClaudeMd)) {
263
+ const sharedContent = fs.readFileSync(sharedClaudeMd, 'utf8');
264
+ content = content.replace(/@..\/_shared\/CLAUDE\.md/g, '');
265
+ content = sharedContent + '\n\n' + content;
266
+ fs.writeFileSync(targetClaudeMd, content);
267
+ log.success('Merged shared conventions into CLAUDE.md');
268
+ }
269
+ }
270
+ }
271
+
272
+ writeManifest(
273
+ targetDir,
274
+ {
275
+ technologies: techs,
276
+ options: {
277
+ withSkills: options.withSkills,
278
+ withRules: options.withRules,
279
+ },
280
+ },
281
+ dryRun
282
+ );
283
+
284
+ console.log('');
285
+
286
+ if (dryRun) {
287
+ const creates = operations.filter((op) => op.type === 'create').length;
288
+ const overwrites = operations.filter((op) => ['overwrite', 'merge'].includes(op.type)).length;
289
+
290
+ console.log(colors.bold('Summary:'));
291
+ console.log(` ${colors.green(creates)} file(s) would be created`);
292
+ console.log(` ${colors.yellow(overwrites)} file(s) would be modified`);
293
+ console.log('');
294
+ console.log(`Run without ${colors.cyan('--dry-run')} to apply changes.`);
295
+ } else {
296
+ log.success('Installation complete!');
297
+ console.log('');
298
+ console.log('Installed:');
299
+ console.log(` - Technologies: ${techs.join(', ')}`);
300
+ if (options.withSkills) {
301
+ console.log(' - Skills: /learning, /review, /spec, /debug, and more');
302
+ }
303
+ if (options.withRules) {
304
+ console.log(' - Rules: security, performance, accessibility');
305
+ }
306
+ console.log('');
307
+ console.log(`Files created in: ${targetDir}`);
308
+
309
+ if (backup) {
310
+ const backupDir = path.join(targetDir, '.claude', 'backups');
311
+ if (fs.existsSync(backupDir) && getFilesRecursive(backupDir).length > 0) {
312
+ console.log(`Backups saved in: ${path.join('.claude', 'backups')}`);
313
+ }
314
+ }
315
+ }
316
+
317
+ console.log('');
318
+ }
319
+
320
+ async function update(options) {
321
+ const targetDir = path.resolve(options.target || process.cwd());
322
+ const { dryRun, force } = options;
323
+
324
+ const manifest = readManifest(targetDir);
325
+
326
+ if (!manifest) {
327
+ log.error('No ai-rules installation found.');
328
+ console.log(`Run ${colors.cyan('ai-rules init')} first.`);
329
+ process.exit(1);
330
+ }
331
+
332
+ if (manifest.version === VERSION && !force) {
333
+ log.success(`Already up to date (v${VERSION})`);
334
+ return;
335
+ }
336
+
337
+ console.log('');
338
+ log.info(`Updating from v${manifest.version} to v${VERSION}`);
339
+
340
+ if (dryRun) {
341
+ console.log(`\n${colors.cyan('DRY RUN')} - No files will be modified\n`);
342
+ }
343
+
344
+ const initOptions = {
345
+ target: targetDir,
346
+ withSkills: manifest.options?.withSkills || false,
347
+ withRules: manifest.options?.withRules || false,
348
+ all: false,
349
+ dryRun,
350
+ force,
351
+ };
352
+
353
+ init(manifest.technologies, initOptions);
354
+ }
355
+
356
+ module.exports = {
357
+ init,
358
+ update,
359
+ status,
360
+ listTechnologies,
361
+ };