@malamute/ai-rules 1.0.0 → 1.3.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 (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
@@ -0,0 +1,414 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { colors, log, getFilesRecursive, copyDirRecursive, backupFile } from './utils.js';
4
+ import { CONFIGS_DIR, AVAILABLE_TECHS, VERSION, getRulePathsToInclude, shouldIncludeRule } from './config.js';
5
+ import { mergeClaudeMd, mergeSettingsJson, readManifest, writeManifest } from './merge.js';
6
+
7
+ /**
8
+ * Copy skills to target directory with flat structure.
9
+ * Source: skills/<category>/<skill-name>/SKILL.md
10
+ * Target: .claude/skills/<skill-name>/SKILL.md
11
+ */
12
+ function copySkillsToTarget(srcDir, destDir, options = {}) {
13
+ const { dryRun, backup, targetDir } = options;
14
+ const operations = [];
15
+
16
+ // getFilesRecursive returns paths relative to srcDir
17
+ const relativeFiles = getFilesRecursive(srcDir).filter((f) => f.endsWith('SKILL.md'));
18
+
19
+ for (const relativePath of relativeFiles) {
20
+ const srcFile = path.join(srcDir, relativePath);
21
+ const parts = relativePath.split(path.sep);
22
+
23
+ // Extract skill name from parent directory
24
+ // e.g., dev/debug/SKILL.md → debug
25
+ const skillName = parts[parts.length - 2];
26
+
27
+ // Create .claude/skills/<skill-name>/SKILL.md
28
+ const destSkillDir = path.join(destDir, skillName);
29
+ const destFile = path.join(destSkillDir, 'SKILL.md');
30
+ const exists = fs.existsSync(destFile);
31
+ const relativeDestPath = path.relative(targetDir, destFile);
32
+
33
+ if (dryRun) {
34
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativeDestPath });
35
+ } else {
36
+ if (exists && backup) {
37
+ backupFile(destFile, targetDir);
38
+ }
39
+ fs.mkdirSync(destSkillDir, { recursive: true });
40
+ fs.copyFileSync(srcFile, destFile);
41
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativeDestPath });
42
+ }
43
+ }
44
+
45
+ return operations;
46
+ }
47
+
48
+ /**
49
+ * Copy rules selectively based on technology configuration.
50
+ * Only copies rules that match the included paths.
51
+ */
52
+ function copyRulesSelectively(srcDir, destDir, includedPaths, skippedPaths, options = {}) {
53
+ const { dryRun, backup, targetDir } = options;
54
+ const operations = [];
55
+
56
+ // getFilesRecursive returns paths relative to srcDir
57
+ const relativeFiles = getFilesRecursive(srcDir);
58
+
59
+ for (const relativePath of relativeFiles) {
60
+ const relativeDir = path.dirname(relativePath);
61
+
62
+ // Check if this file should be included
63
+ if (!shouldIncludeRule(relativeDir, includedPaths)) {
64
+ // Track skipped top-level directories for logging
65
+ const topLevel = relativePath.split(path.sep).slice(0, 2).join('/');
66
+ if (!skippedPaths.includes(topLevel)) {
67
+ skippedPaths.push(topLevel);
68
+ }
69
+ continue;
70
+ }
71
+
72
+ const srcFile = path.join(srcDir, relativePath);
73
+ const destFile = path.join(destDir, relativePath);
74
+ const exists = fs.existsSync(destFile);
75
+ const relativeDestPath = path.relative(targetDir, destFile);
76
+
77
+ if (dryRun) {
78
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativeDestPath });
79
+ } else {
80
+ if (exists && backup) {
81
+ backupFile(destFile, targetDir);
82
+ }
83
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
84
+ fs.copyFileSync(srcFile, destFile);
85
+ operations.push({ type: exists ? 'overwrite' : 'create', path: relativeDestPath });
86
+ }
87
+ }
88
+
89
+ return operations;
90
+ }
91
+
92
+ export function listTechnologies() {
93
+ console.log(`\n${colors.bold('Available technologies:')}\n`);
94
+
95
+ const techInfo = {
96
+ angular: 'Angular 21 + Nx + NgRx + Signals',
97
+ nextjs: 'Next.js 15 + React 19 + App Router',
98
+ nestjs: 'NestJS 11 + Prisma/TypeORM + Passport',
99
+ dotnet: '.NET 9 + ASP.NET Core + EF Core',
100
+ fastapi: 'FastAPI + SQLAlchemy 2.0 + Pydantic v2',
101
+ flask: 'Flask + SQLAlchemy 2.0 + Marshmallow',
102
+ };
103
+
104
+ for (const tech of AVAILABLE_TECHS) {
105
+ const techPath = path.join(CONFIGS_DIR, tech);
106
+ const exists = fs.existsSync(techPath);
107
+ const status = exists ? colors.green('✓') : colors.red('✗');
108
+ console.log(` ${status} ${colors.bold(tech.padEnd(10))} ${techInfo[tech]}`);
109
+ }
110
+
111
+ console.log(`\n${colors.bold('Shared resources:')}\n`);
112
+
113
+ const sharedPath = path.join(CONFIGS_DIR, '_shared');
114
+ const skills = fs.existsSync(path.join(sharedPath, 'skills'));
115
+ const rules = fs.existsSync(path.join(sharedPath, 'rules'));
116
+
117
+ console.log(` ${skills ? colors.green('✓') : colors.red('✗')} skills /learning, /review, /spec, /debug, and more`);
118
+ console.log(` ${rules ? colors.green('✓') : colors.red('✗')} rules security, performance, accessibility`);
119
+ console.log('');
120
+ }
121
+
122
+ export function status(targetDir) {
123
+ const manifest = readManifest(targetDir);
124
+
125
+ console.log(`\n${colors.bold('AI Rules Status')}\n`);
126
+ console.log(` Directory: ${targetDir}`);
127
+ console.log('');
128
+
129
+ if (!manifest) {
130
+ log.warning('No ai-rules installation detected');
131
+ console.log('');
132
+ console.log(` Run ${colors.cyan('ai-rules init')} to install configurations.`);
133
+ console.log('');
134
+ return;
135
+ }
136
+
137
+ console.log(` ${colors.bold('Installed version:')} ${manifest.version}`);
138
+ console.log(` ${colors.bold('Latest version:')} ${VERSION}`);
139
+ console.log(` ${colors.bold('Installed at:')} ${new Date(manifest.installedAt).toLocaleString()}`);
140
+ console.log('');
141
+
142
+ if (manifest.technologies?.length) {
143
+ console.log(` ${colors.bold('Technologies:')}`);
144
+ manifest.technologies.forEach((tech) => {
145
+ console.log(` ${colors.green('✓')} ${tech}`);
146
+ });
147
+ console.log('');
148
+ }
149
+
150
+ if (manifest.options) {
151
+ console.log(` ${colors.bold('Options:')}`);
152
+ if (manifest.options.withSkills) console.log(` ${colors.green('✓')} skills`);
153
+ if (manifest.options.withRules) console.log(` ${colors.green('✓')} shared rules`);
154
+ console.log('');
155
+ }
156
+
157
+ if (manifest.version !== VERSION) {
158
+ console.log(` ${colors.yellow('⚠')} Update available! Run ${colors.cyan('ai-rules update')} to update.`);
159
+ console.log('');
160
+ }
161
+
162
+ const backupDir = path.join(targetDir, '.claude', 'backups');
163
+ if (fs.existsSync(backupDir)) {
164
+ const backups = getFilesRecursive(backupDir);
165
+ if (backups.length > 0) {
166
+ console.log(` ${colors.bold('Backups:')} ${backups.length} file(s) in .claude/backups/`);
167
+ console.log('');
168
+ }
169
+ }
170
+ }
171
+
172
+ export function init(techs, options) {
173
+ const targetDir = path.resolve(options.target || process.cwd());
174
+ const { dryRun, force } = options;
175
+ const backup = !force;
176
+
177
+ if (dryRun) {
178
+ console.log(`\n${colors.cyan('DRY RUN')} - No files will be modified\n`);
179
+ }
180
+
181
+ log.info(`${dryRun ? 'Would install' : 'Installing'} to: ${targetDir}`);
182
+ console.log('');
183
+
184
+ const operations = [];
185
+
186
+ if (!dryRun) {
187
+ fs.mkdirSync(path.join(targetDir, '.claude', 'rules'), { recursive: true });
188
+ }
189
+
190
+ let isFirstClaudeMd = true;
191
+
192
+ for (const tech of techs) {
193
+ log.info(`${dryRun ? 'Would install' : 'Installing'} ${tech}...`);
194
+
195
+ const techDir = path.join(CONFIGS_DIR, tech);
196
+
197
+ if (!fs.existsSync(techDir)) {
198
+ log.error(`Technology directory not found: ${tech}`);
199
+ process.exit(1);
200
+ }
201
+
202
+ const claudeMdPath = path.join(techDir, 'CLAUDE.md');
203
+ if (fs.existsSync(claudeMdPath)) {
204
+ const op = mergeClaudeMd(
205
+ path.join(targetDir, 'CLAUDE.md'),
206
+ claudeMdPath,
207
+ isFirstClaudeMd,
208
+ { dryRun, backup, targetDir }
209
+ );
210
+ operations.push(op);
211
+ isFirstClaudeMd = false;
212
+
213
+ if (dryRun) {
214
+ log.dry(` CLAUDE.md (${op.type})`);
215
+ } else {
216
+ log.success(` CLAUDE.md`);
217
+ }
218
+ }
219
+
220
+ const settingsPath = path.join(techDir, 'settings.json');
221
+ if (fs.existsSync(settingsPath)) {
222
+ const op = mergeSettingsJson(
223
+ path.join(targetDir, '.claude', 'settings.json'),
224
+ settingsPath,
225
+ { dryRun, backup, targetDir }
226
+ );
227
+ operations.push(op);
228
+
229
+ if (dryRun) {
230
+ log.dry(` settings.json (${op.type})`);
231
+ } else {
232
+ log.success(` settings.json`);
233
+ }
234
+ }
235
+
236
+ const rulesDir = path.join(techDir, 'rules');
237
+ if (fs.existsSync(rulesDir)) {
238
+ const ops = copyDirRecursive(
239
+ rulesDir,
240
+ path.join(targetDir, '.claude', 'rules'),
241
+ { dryRun, backup, targetDir }
242
+ );
243
+ operations.push(...ops);
244
+
245
+ if (dryRun) {
246
+ log.dry(` rules/ (${ops.length} files)`);
247
+ } else {
248
+ log.success(` rules/`);
249
+ }
250
+ }
251
+
252
+ if (options.withSkills) {
253
+ const techSkillsDir = path.join(techDir, 'skills');
254
+ if (fs.existsSync(techSkillsDir)) {
255
+ const ops = copySkillsToTarget(
256
+ techSkillsDir,
257
+ path.join(targetDir, '.claude', 'skills'),
258
+ { dryRun, backup, targetDir }
259
+ );
260
+ operations.push(...ops);
261
+ }
262
+ }
263
+ }
264
+
265
+ const sharedDir = path.join(CONFIGS_DIR, '_shared');
266
+
267
+ if (options.withSkills) {
268
+ log.info(`${dryRun ? 'Would install' : 'Installing'} skills...`);
269
+ const skillsDir = path.join(sharedDir, 'skills');
270
+ if (fs.existsSync(skillsDir)) {
271
+ const ops = copySkillsToTarget(
272
+ skillsDir,
273
+ path.join(targetDir, '.claude', 'skills'),
274
+ { dryRun, backup, targetDir }
275
+ );
276
+ operations.push(...ops);
277
+
278
+ if (dryRun) {
279
+ log.dry(` skills/ (${ops.length} files)`);
280
+ } else {
281
+ log.success(` skills/`);
282
+ }
283
+ }
284
+ }
285
+
286
+ if (options.withRules) {
287
+ log.info(`${dryRun ? 'Would install' : 'Installing'} shared rules...`);
288
+ const rulesDir = path.join(sharedDir, 'rules');
289
+ if (fs.existsSync(rulesDir)) {
290
+ const includedPaths = getRulePathsToInclude(techs);
291
+ const skippedPaths = [];
292
+
293
+ const ops = copyRulesSelectively(
294
+ rulesDir,
295
+ path.join(targetDir, '.claude', 'rules'),
296
+ includedPaths,
297
+ skippedPaths,
298
+ { dryRun, backup, targetDir }
299
+ );
300
+ operations.push(...ops);
301
+
302
+ if (dryRun) {
303
+ log.dry(` shared rules/ (${ops.length} files)`);
304
+ } else {
305
+ log.success(` shared rules/`);
306
+ }
307
+
308
+ if (skippedPaths.length > 0) {
309
+ const uniqueSkipped = [...new Set(skippedPaths)];
310
+ log.info(` (skipped: ${uniqueSkipped.join(', ')} - not applicable)`);
311
+ }
312
+ }
313
+ }
314
+
315
+ // Resolve @../_shared/CLAUDE.md imports
316
+ const targetClaudeMd = path.join(targetDir, 'CLAUDE.md');
317
+ if (!dryRun && fs.existsSync(targetClaudeMd)) {
318
+ let content = fs.readFileSync(targetClaudeMd, 'utf8');
319
+
320
+ if (content.includes('@../_shared/CLAUDE.md')) {
321
+ const sharedClaudeMd = path.join(sharedDir, 'CLAUDE.md');
322
+ if (fs.existsSync(sharedClaudeMd)) {
323
+ const sharedContent = fs.readFileSync(sharedClaudeMd, 'utf8');
324
+ content = content.replace(/@..\/_shared\/CLAUDE\.md/g, '');
325
+ content = sharedContent + '\n\n' + content;
326
+ fs.writeFileSync(targetClaudeMd, content);
327
+ log.success('Merged shared conventions into CLAUDE.md');
328
+ }
329
+ }
330
+ }
331
+
332
+ writeManifest(
333
+ targetDir,
334
+ {
335
+ technologies: techs,
336
+ options: {
337
+ withSkills: options.withSkills,
338
+ withRules: options.withRules,
339
+ },
340
+ },
341
+ dryRun
342
+ );
343
+
344
+ console.log('');
345
+
346
+ if (dryRun) {
347
+ const creates = operations.filter((op) => op.type === 'create').length;
348
+ const overwrites = operations.filter((op) => ['overwrite', 'merge'].includes(op.type)).length;
349
+
350
+ console.log(colors.bold('Summary:'));
351
+ console.log(` ${colors.green(creates)} file(s) would be created`);
352
+ console.log(` ${colors.yellow(overwrites)} file(s) would be modified`);
353
+ console.log('');
354
+ console.log(`Run without ${colors.cyan('--dry-run')} to apply changes.`);
355
+ } else {
356
+ log.success('Installation complete!');
357
+ console.log('');
358
+ console.log('Installed:');
359
+ console.log(` - Technologies: ${techs.join(', ')}`);
360
+ if (options.withSkills) {
361
+ console.log(' - Skills: /learning, /review, /spec, /debug, and more');
362
+ }
363
+ if (options.withRules) {
364
+ console.log(' - Rules: security, performance, accessibility');
365
+ }
366
+ console.log('');
367
+ console.log(`Files created in: ${targetDir}`);
368
+
369
+ if (backup) {
370
+ const backupDir = path.join(targetDir, '.claude', 'backups');
371
+ if (fs.existsSync(backupDir) && getFilesRecursive(backupDir).length > 0) {
372
+ console.log(`Backups saved in: ${path.join('.claude', 'backups')}`);
373
+ }
374
+ }
375
+ }
376
+
377
+ console.log('');
378
+ }
379
+
380
+ export async function update(options) {
381
+ const targetDir = path.resolve(options.target || process.cwd());
382
+ const { dryRun, force } = options;
383
+
384
+ const manifest = readManifest(targetDir);
385
+
386
+ if (!manifest) {
387
+ log.error('No ai-rules installation found.');
388
+ console.log(`Run ${colors.cyan('ai-rules init')} first.`);
389
+ process.exit(1);
390
+ }
391
+
392
+ if (manifest.version === VERSION && !force) {
393
+ log.success(`Already up to date (v${VERSION})`);
394
+ return;
395
+ }
396
+
397
+ console.log('');
398
+ log.info(`Updating from v${manifest.version} to v${VERSION}`);
399
+
400
+ if (dryRun) {
401
+ console.log(`\n${colors.cyan('DRY RUN')} - No files will be modified\n`);
402
+ }
403
+
404
+ const initOptions = {
405
+ target: targetDir,
406
+ withSkills: manifest.options?.withSkills || false,
407
+ withRules: manifest.options?.withRules || false,
408
+ all: false,
409
+ dryRun,
410
+ force,
411
+ };
412
+
413
+ init(manifest.technologies, initOptions);
414
+ }
package/src/merge.js ADDED
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { log, backupFile } from './utils.js';
4
+ import { VERSION } from './config.js';
5
+
6
+ export function mergeClaudeMd(targetPath, sourcePath, isFirst, options = {}) {
7
+ const { dryRun = false, backup = false, targetDir } = options;
8
+ const content = fs.readFileSync(sourcePath, 'utf8');
9
+ const exists = fs.existsSync(targetPath);
10
+
11
+ if (dryRun) {
12
+ return { type: exists ? 'merge' : 'create', path: 'CLAUDE.md' };
13
+ }
14
+
15
+ if (exists && backup && isFirst) {
16
+ backupFile(targetPath, targetDir);
17
+ }
18
+
19
+ if (isFirst) {
20
+ fs.writeFileSync(targetPath, content);
21
+ } else {
22
+ const existing = fs.readFileSync(targetPath, 'utf8');
23
+ fs.writeFileSync(targetPath, `${existing}\n\n---\n\n${content}`);
24
+ }
25
+
26
+ return { type: exists ? 'merge' : 'create', path: 'CLAUDE.md' };
27
+ }
28
+
29
+ export function mergeSettingsJson(targetPath, sourcePath, options = {}) {
30
+ const { dryRun = false, backup = false, targetDir } = options;
31
+ const exists = fs.existsSync(targetPath);
32
+
33
+ if (dryRun) {
34
+ return { type: exists ? 'merge' : 'create', path: '.claude/settings.json' };
35
+ }
36
+
37
+ if (!exists) {
38
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
39
+ fs.copyFileSync(sourcePath, targetPath);
40
+ return { type: 'create', path: '.claude/settings.json' };
41
+ }
42
+
43
+ if (backup) {
44
+ backupFile(targetPath, targetDir);
45
+ }
46
+
47
+ try {
48
+ const existing = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
49
+ const incoming = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
50
+
51
+ const merged = {
52
+ ...existing,
53
+ permissions: {
54
+ allow: [
55
+ ...new Set([
56
+ ...(existing.permissions?.allow || []),
57
+ ...(incoming.permissions?.allow || []),
58
+ ]),
59
+ ],
60
+ deny: [
61
+ ...new Set([
62
+ ...(existing.permissions?.deny || []),
63
+ ...(incoming.permissions?.deny || []),
64
+ ]),
65
+ ],
66
+ },
67
+ };
68
+
69
+ if (incoming.env) {
70
+ merged.env = { ...(existing.env || {}), ...incoming.env };
71
+ }
72
+
73
+ fs.writeFileSync(targetPath, JSON.stringify(merged, null, 2) + '\n');
74
+ return { type: 'merge', path: '.claude/settings.json' };
75
+ } catch (_e) {
76
+ log.warning('Could not merge settings.json, overwriting');
77
+ fs.copyFileSync(sourcePath, targetPath);
78
+ return { type: 'overwrite', path: '.claude/settings.json' };
79
+ }
80
+ }
81
+
82
+ function getManifestPath(targetDir) {
83
+ return path.join(targetDir, '.claude', '.ai-rules.json');
84
+ }
85
+
86
+ export function readManifest(targetDir) {
87
+ const manifestPath = getManifestPath(targetDir);
88
+ if (!fs.existsSync(manifestPath)) return null;
89
+
90
+ try {
91
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ export function writeManifest(targetDir, data, dryRun = false) {
98
+ if (dryRun) return;
99
+
100
+ const manifestPath = getManifestPath(targetDir);
101
+ const manifest = {
102
+ version: VERSION,
103
+ installedAt: new Date().toISOString(),
104
+ ...data,
105
+ };
106
+
107
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
108
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
109
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "technologies": {
3
+ "angular": {
4
+ "type": "frontend",
5
+ "language": "typescript"
6
+ },
7
+ "nextjs": {
8
+ "type": "frontend",
9
+ "language": "typescript"
10
+ },
11
+ "nestjs": {
12
+ "type": "backend",
13
+ "language": "typescript"
14
+ },
15
+ "dotnet": {
16
+ "type": "backend",
17
+ "language": "csharp"
18
+ },
19
+ "fastapi": {
20
+ "type": "backend",
21
+ "language": "python"
22
+ },
23
+ "flask": {
24
+ "type": "backend",
25
+ "language": "python"
26
+ }
27
+ },
28
+ "ruleMapping": {
29
+ "language": {
30
+ "typescript": "lang/typescript",
31
+ "python": "lang/python",
32
+ "csharp": "lang/csharp"
33
+ },
34
+ "type": {
35
+ "frontend": "domain/frontend",
36
+ "backend": "domain/backend"
37
+ }
38
+ },
39
+ "alwaysInclude": [
40
+ "conventions",
41
+ "quality",
42
+ "security",
43
+ "devops"
44
+ ]
45
+ }
package/src/utils.js ADDED
@@ -0,0 +1,88 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export const colors = {
5
+ red: (text) => `\x1b[31m${text}\x1b[0m`,
6
+ green: (text) => `\x1b[32m${text}\x1b[0m`,
7
+ blue: (text) => `\x1b[34m${text}\x1b[0m`,
8
+ yellow: (text) => `\x1b[33m${text}\x1b[0m`,
9
+ cyan: (text) => `\x1b[36m${text}\x1b[0m`,
10
+ dim: (text) => `\x1b[2m${text}\x1b[0m`,
11
+ bold: (text) => `\x1b[1m${text}\x1b[0m`,
12
+ };
13
+
14
+ export const log = {
15
+ info: (msg) => console.log(`${colors.blue('ℹ')} ${msg}`),
16
+ success: (msg) => console.log(`${colors.green('✓')} ${msg}`),
17
+ warning: (msg) => console.log(`${colors.yellow('⚠')} ${msg}`),
18
+ error: (msg) => console.log(`${colors.red('✗')} ${msg}`),
19
+ dry: (msg) => console.log(`${colors.cyan('○')} ${msg}`),
20
+ };
21
+
22
+ export function getFilesRecursive(dir, baseDir = dir) {
23
+ const files = [];
24
+ if (!fs.existsSync(dir)) return files;
25
+
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ const fullPath = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...getFilesRecursive(fullPath, baseDir));
31
+ } else {
32
+ files.push(path.relative(baseDir, fullPath));
33
+ }
34
+ }
35
+ return files;
36
+ }
37
+
38
+ export function copyDirRecursive(src, dest, options = {}) {
39
+ const { dryRun = false, backup = false, targetDir = dest } = options;
40
+ const operations = [];
41
+
42
+ if (!fs.existsSync(src)) return operations;
43
+
44
+ const entries = fs.readdirSync(src, { withFileTypes: true });
45
+
46
+ for (const entry of entries) {
47
+ const srcPath = path.join(src, entry.name);
48
+ const destPath = path.join(dest, entry.name);
49
+
50
+ if (entry.isDirectory()) {
51
+ operations.push(...copyDirRecursive(srcPath, destPath, options));
52
+ } else {
53
+ const exists = fs.existsSync(destPath);
54
+ const relativePath = path.relative(targetDir, destPath);
55
+
56
+ if (dryRun) {
57
+ operations.push({
58
+ type: exists ? 'overwrite' : 'create',
59
+ path: relativePath,
60
+ });
61
+ } else {
62
+ if (exists && backup) {
63
+ backupFile(destPath, targetDir);
64
+ }
65
+ fs.mkdirSync(dest, { recursive: true });
66
+ fs.copyFileSync(srcPath, destPath);
67
+ operations.push({
68
+ type: exists ? 'overwrite' : 'create',
69
+ path: relativePath,
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ return operations;
76
+ }
77
+
78
+ export function backupFile(filePath, targetDir) {
79
+ const backupDir = path.join(targetDir, '.claude', 'backups');
80
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
81
+ const relativePath = path.relative(targetDir, filePath);
82
+ const backupPath = path.join(backupDir, `${relativePath}.${timestamp}`);
83
+
84
+ fs.mkdirSync(path.dirname(backupPath), { recursive: true });
85
+ fs.copyFileSync(filePath, backupPath);
86
+
87
+ return backupPath;
88
+ }
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(dotnet *)",
5
- "Bash(dotnet-ef *)"
6
- ],
7
- "deny": []
8
- }
9
- }
@@ -1,15 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm run start:dev)",
5
- "Bash(npm run build)",
6
- "Bash(npm run test*)",
7
- "Bash(npm run lint*)",
8
- "Bash(npm run format*)",
9
- "Bash(npx prisma *)",
10
- "Bash(npx typeorm *)",
11
- "Bash(npm install *)"
12
- ],
13
- "deny": []
14
- }
15
- }