@malamute/ai-rules 1.2.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 (134) hide show
  1. package/README.md +4 -2
  2. package/bin/cli.js +1 -1
  3. package/configs/_shared/rules/conventions/npm.md +80 -0
  4. package/configs/angular/{.claude/settings.json → settings.json} +2 -0
  5. package/configs/dotnet/{.claude/settings.json → settings.json} +2 -0
  6. package/configs/fastapi/{.claude/settings.json → settings.json} +2 -0
  7. package/configs/flask/{.claude/settings.json → settings.json} +2 -0
  8. package/configs/nestjs/{.claude/settings.json → settings.json} +2 -0
  9. package/configs/nextjs/{.claude/settings.json → settings.json} +2 -0
  10. package/package.json +2 -1
  11. package/src/cli.js +5 -7
  12. package/src/config.js +52 -18
  13. package/src/index.js +4 -13
  14. package/src/installer.js +118 -65
  15. package/src/merge.js +8 -15
  16. package/src/tech-config.json +29 -13
  17. package/src/utils.js +7 -15
  18. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +0 -362
  19. package/configs/angular/.claude/skills/signal-store/SKILL.md +0 -445
  20. /package/configs/_shared/{.claude/rules → rules}/conventions/documentation.md +0 -0
  21. /package/configs/_shared/{.claude/rules → rules}/conventions/git.md +0 -0
  22. /package/configs/_shared/{.claude/rules → rules}/conventions/performance.md +0 -0
  23. /package/configs/_shared/{.claude/rules → rules}/conventions/principles.md +0 -0
  24. /package/configs/_shared/{.claude/rules → rules}/devops/ci-cd.md +0 -0
  25. /package/configs/_shared/{.claude/rules → rules}/devops/docker.md +0 -0
  26. /package/configs/_shared/{.claude/rules → rules}/devops/nx.md +0 -0
  27. /package/configs/_shared/{.claude/rules → rules}/domain/backend/api-design.md +0 -0
  28. /package/configs/_shared/{.claude/rules → rules}/domain/frontend/accessibility.md +0 -0
  29. /package/configs/_shared/{.claude/rules → rules}/lang/csharp/async.md +0 -0
  30. /package/configs/_shared/{.claude/rules → rules}/lang/csharp/csharp.md +0 -0
  31. /package/configs/_shared/{.claude/rules → rules}/lang/csharp/linq.md +0 -0
  32. /package/configs/_shared/{.claude/rules → rules}/lang/python/async.md +0 -0
  33. /package/configs/_shared/{.claude/rules → rules}/lang/python/celery.md +0 -0
  34. /package/configs/_shared/{.claude/rules → rules}/lang/python/config.md +0 -0
  35. /package/configs/_shared/{.claude/rules → rules}/lang/python/database/sqlalchemy.md +0 -0
  36. /package/configs/_shared/{.claude/rules → rules}/lang/python/deployment.md +0 -0
  37. /package/configs/_shared/{.claude/rules → rules}/lang/python/error-handling.md +0 -0
  38. /package/configs/_shared/{.claude/rules → rules}/lang/python/migrations.md +0 -0
  39. /package/configs/_shared/{.claude/rules → rules}/lang/python/python.md +0 -0
  40. /package/configs/_shared/{.claude/rules → rules}/lang/python/repository.md +0 -0
  41. /package/configs/_shared/{.claude/rules → rules}/lang/python/testing.md +0 -0
  42. /package/configs/_shared/{.claude/rules → rules}/lang/typescript/async.md +0 -0
  43. /package/configs/_shared/{.claude/rules → rules}/lang/typescript/generics.md +0 -0
  44. /package/configs/_shared/{.claude/rules → rules}/lang/typescript/typescript.md +0 -0
  45. /package/configs/_shared/{.claude/rules → rules}/quality/error-handling.md +0 -0
  46. /package/configs/_shared/{.claude/rules → rules}/quality/logging.md +0 -0
  47. /package/configs/_shared/{.claude/rules → rules}/quality/observability.md +0 -0
  48. /package/configs/_shared/{.claude/rules → rules}/quality/testing-patterns.md +0 -0
  49. /package/configs/_shared/{.claude/rules → rules}/security/secrets-management.md +0 -0
  50. /package/configs/_shared/{.claude/rules → rules}/security/security.md +0 -0
  51. /package/configs/_shared/{.claude/skills → skills}/analysis/explore/SKILL.md +0 -0
  52. /package/configs/_shared/{.claude/skills → skills}/analysis/security-audit/SKILL.md +0 -0
  53. /package/configs/_shared/{.claude/skills → skills}/dev/api-endpoint/SKILL.md +0 -0
  54. /package/configs/_shared/{.claude/skills → skills}/dev/debug/SKILL.md +0 -0
  55. /package/configs/_shared/{.claude/skills → skills}/dev/generate-tests/SKILL.md +0 -0
  56. /package/configs/_shared/{.claude/skills → skills}/dev/learning/SKILL.md +0 -0
  57. /package/configs/_shared/{.claude/skills → skills}/dev/spec/SKILL.md +0 -0
  58. /package/configs/_shared/{.claude/skills → skills}/git/fix-issue/SKILL.md +0 -0
  59. /package/configs/_shared/{.claude/skills → skills}/git/review/SKILL.md +0 -0
  60. /package/configs/_shared/{.claude/skills → skills}/git/review-pr/SKILL.md +0 -0
  61. /package/configs/_shared/{.claude/skills → skills}/infra/deploy/SKILL.md +0 -0
  62. /package/configs/_shared/{.claude/skills → skills}/infra/docker/SKILL.md +0 -0
  63. /package/configs/_shared/{.claude/skills → skills}/infra/migration/SKILL.md +0 -0
  64. /package/configs/_shared/{.claude/skills → skills}/nx/nx-affected/SKILL.md +0 -0
  65. /package/configs/_shared/{.claude/skills → skills}/nx/nx-lib/SKILL.md +0 -0
  66. /package/configs/angular/{.claude/rules → rules}/core/components.md +0 -0
  67. /package/configs/angular/{.claude/rules → rules}/core/resource.md +0 -0
  68. /package/configs/angular/{.claude/rules → rules}/core/signals.md +0 -0
  69. /package/configs/angular/{.claude/rules → rules}/http.md +0 -0
  70. /package/configs/angular/{.claude/rules → rules}/routing.md +0 -0
  71. /package/configs/angular/{.claude/rules → rules}/ssr.md +0 -0
  72. /package/configs/angular/{.claude/rules → rules}/state/signal-store.md +0 -0
  73. /package/configs/angular/{.claude/rules → rules}/state/state.md +0 -0
  74. /package/configs/angular/{.claude/rules → rules}/testing.md +0 -0
  75. /package/configs/angular/{.claude/rules → rules}/ui/aria.md +0 -0
  76. /package/configs/angular/{.claude/rules → rules}/ui/forms.md +0 -0
  77. /package/configs/angular/{.claude/rules → rules}/ui/pipes-directives.md +0 -0
  78. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  79. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  80. /package/configs/dotnet/{.claude/rules → rules}/background-services.md +0 -0
  81. /package/configs/dotnet/{.claude/rules → rules}/configuration.md +0 -0
  82. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  83. /package/configs/dotnet/{.claude/rules → rules}/ddd.md +0 -0
  84. /package/configs/dotnet/{.claude/rules → rules}/dependency-injection.md +0 -0
  85. /package/configs/dotnet/{.claude/rules → rules}/mediatr.md +0 -0
  86. /package/configs/dotnet/{.claude/rules → rules}/middleware.md +0 -0
  87. /package/configs/dotnet/{.claude/rules → rules}/result-pattern.md +0 -0
  88. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  89. /package/configs/dotnet/{.claude/rules → rules}/validation.md +0 -0
  90. /package/configs/fastapi/{.claude/rules → rules}/background-tasks.md +0 -0
  91. /package/configs/fastapi/{.claude/rules → rules}/dependencies.md +0 -0
  92. /package/configs/fastapi/{.claude/rules → rules}/fastapi.md +0 -0
  93. /package/configs/fastapi/{.claude/rules → rules}/lifespan.md +0 -0
  94. /package/configs/fastapi/{.claude/rules → rules}/middleware.md +0 -0
  95. /package/configs/fastapi/{.claude/rules → rules}/pydantic.md +0 -0
  96. /package/configs/fastapi/{.claude/rules → rules}/responses.md +0 -0
  97. /package/configs/fastapi/{.claude/rules → rules}/routers.md +0 -0
  98. /package/configs/fastapi/{.claude/rules → rules}/security.md +0 -0
  99. /package/configs/fastapi/{.claude/rules → rules}/testing.md +0 -0
  100. /package/configs/fastapi/{.claude/rules → rules}/websockets.md +0 -0
  101. /package/configs/flask/{.claude/rules → rules}/blueprints.md +0 -0
  102. /package/configs/flask/{.claude/rules → rules}/cli.md +0 -0
  103. /package/configs/flask/{.claude/rules → rules}/configuration.md +0 -0
  104. /package/configs/flask/{.claude/rules → rules}/context.md +0 -0
  105. /package/configs/flask/{.claude/rules → rules}/error-handlers.md +0 -0
  106. /package/configs/flask/{.claude/rules → rules}/extensions.md +0 -0
  107. /package/configs/flask/{.claude/rules → rules}/flask.md +0 -0
  108. /package/configs/flask/{.claude/rules → rules}/marshmallow.md +0 -0
  109. /package/configs/flask/{.claude/rules → rules}/security.md +0 -0
  110. /package/configs/flask/{.claude/rules → rules}/testing.md +0 -0
  111. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  112. /package/configs/nestjs/{.claude/rules → rules}/common-patterns.md +0 -0
  113. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  114. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  115. /package/configs/nestjs/{.claude/rules → rules}/filters.md +0 -0
  116. /package/configs/nestjs/{.claude/rules → rules}/interceptors.md +0 -0
  117. /package/configs/nestjs/{.claude/rules → rules}/middleware.md +0 -0
  118. /package/configs/nestjs/{.claude/rules → rules}/modules.md +0 -0
  119. /package/configs/nestjs/{.claude/rules → rules}/pipes.md +0 -0
  120. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  121. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  122. /package/configs/nestjs/{.claude/rules → rules}/websockets.md +0 -0
  123. /package/configs/nextjs/{.claude/rules → rules}/api-routes.md +0 -0
  124. /package/configs/nextjs/{.claude/rules → rules}/authentication.md +0 -0
  125. /package/configs/nextjs/{.claude/rules → rules}/components.md +0 -0
  126. /package/configs/nextjs/{.claude/rules → rules}/data-fetching.md +0 -0
  127. /package/configs/nextjs/{.claude/rules → rules}/database.md +0 -0
  128. /package/configs/nextjs/{.claude/rules → rules}/middleware.md +0 -0
  129. /package/configs/nextjs/{.claude/rules → rules}/routing.md +0 -0
  130. /package/configs/nextjs/{.claude/rules → rules}/seo.md +0 -0
  131. /package/configs/nextjs/{.claude/rules → rules}/server-actions.md +0 -0
  132. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  133. /package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +0 -0
  134. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
package/README.md CHANGED
@@ -313,11 +313,13 @@ mkdir configs/your-tech
313
313
  ### Adding a Technology
314
314
 
315
315
  1. Create `configs/[tech]/CLAUDE.md` — start with `@../_shared/CLAUDE.md`
316
- 2. Add rules in `configs/[tech]/.claude/rules/`
317
- 3. Add `configs/[tech]/.claude/settings.json` for permissions
316
+ 2. Add rules in `configs/[tech]/rules/`
317
+ 3. Add `configs/[tech]/settings.json` for permissions
318
318
  4. Add tests
319
319
  5. Submit a PR
320
320
 
321
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on skills structure.
322
+
321
323
  ## License
322
324
 
323
325
  MIT © Mehdi Chaabi
package/bin/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { run } = require('../src/cli.js');
3
+ import { run } from '../src/cli.js';
4
4
 
5
5
  run(process.argv.slice(2)).catch((err) => {
6
6
  console.error(err);
@@ -0,0 +1,80 @@
1
+ ---
2
+ paths:
3
+ - "**/package.json"
4
+ ---
5
+
6
+ # npm Conventions
7
+
8
+ ## Version Pinning
9
+
10
+ **Always use exact versions** - no `^` or `~` prefixes.
11
+
12
+ ```json
13
+ // GOOD
14
+ {
15
+ "dependencies": {
16
+ "express": "4.18.2",
17
+ "lodash": "4.17.21"
18
+ }
19
+ }
20
+
21
+ // BAD
22
+ {
23
+ "dependencies": {
24
+ "express": "^4.18.2",
25
+ "lodash": "~4.17.21"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Why?
31
+
32
+ - **Reproducible builds** across environments
33
+ - **No surprise breaking changes** from minor/patch updates
34
+ - **Lock file is source of truth** but pinning adds defense in depth
35
+ - **Explicit upgrades** via `npm update` or renovate/dependabot
36
+
37
+ ### Commands
38
+
39
+ ```bash
40
+ # Install with exact version
41
+ npm install express --save-exact
42
+
43
+ # Configure npm to always save exact
44
+ npm config set save-exact true
45
+
46
+ # Or in .npmrc
47
+ save-exact=true
48
+ ```
49
+
50
+ ## Scripts
51
+
52
+ Use consistent script names:
53
+
54
+ ```json
55
+ {
56
+ "scripts": {
57
+ "dev": "...",
58
+ "build": "...",
59
+ "start": "...",
60
+ "test": "...",
61
+ "test:watch": "...",
62
+ "test:cov": "...",
63
+ "lint": "...",
64
+ "lint:fix": "...",
65
+ "format": "..."
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Engine Requirements
71
+
72
+ Specify Node.js version:
73
+
74
+ ```json
75
+ {
76
+ "engines": {
77
+ "node": ">=20.0.0"
78
+ }
79
+ }
80
+ ```
@@ -18,6 +18,8 @@
18
18
  "Write"
19
19
  ],
20
20
  "deny": [
21
+ "Bash(git push *)",
22
+ "Bash(git push)",
21
23
  "Bash(rm -rf *)",
22
24
  "Bash(nx reset)",
23
25
  "Read(.env)",
@@ -14,6 +14,8 @@
14
14
  "Write"
15
15
  ],
16
16
  "deny": [
17
+ "Bash(git push *)",
18
+ "Bash(git push)",
17
19
  "Bash(rm -rf *)",
18
20
  "Read(appsettings.*.json)",
19
21
  "Read(**/secrets.json)",
@@ -19,6 +19,8 @@
19
19
  "Write"
20
20
  ],
21
21
  "deny": [
22
+ "Bash(git push *)",
23
+ "Bash(git push)",
22
24
  "Bash(rm -rf *)",
23
25
  "Read(.env)",
24
26
  "Read(.env.*)",
@@ -18,6 +18,8 @@
18
18
  "Write"
19
19
  ],
20
20
  "deny": [
21
+ "Bash(git push *)",
22
+ "Bash(git push)",
21
23
  "Bash(rm -rf *)",
22
24
  "Read(.env)",
23
25
  "Read(.env.*)",
@@ -17,6 +17,8 @@
17
17
  "Write"
18
18
  ],
19
19
  "deny": [
20
+ "Bash(git push *)",
21
+ "Bash(git push)",
20
22
  "Bash(rm -rf *)",
21
23
  "Read(.env)",
22
24
  "Read(.env.*)",
@@ -20,6 +20,8 @@
20
20
  "Write"
21
21
  ],
22
22
  "deny": [
23
+ "Bash(git push *)",
24
+ "Bash(git push)",
23
25
  "Bash(rm -rf *)",
24
26
  "Bash(nx reset)",
25
27
  "Read(.env)",
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@malamute/ai-rules",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Claude Code configuration boilerplates for Angular, Next.js, NestJS, .NET, Python and more",
5
+ "type": "module",
5
6
  "main": "src/index.js",
6
7
  "bin": "./bin/cli.js",
7
8
  "scripts": {
package/src/cli.js CHANGED
@@ -1,7 +1,7 @@
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');
1
+ import readline from 'readline';
2
+ import { colors, log } from './utils.js';
3
+ import { VERSION, AVAILABLE_TECHS } from './config.js';
4
+ import { init, update, status, listTechnologies } from './installer.js';
5
5
 
6
6
  function printUsage() {
7
7
  console.log(`
@@ -123,7 +123,7 @@ async function interactiveInit() {
123
123
  return { techs, options };
124
124
  }
125
125
 
126
- async function run(args) {
126
+ export async function run(args) {
127
127
  if (args.includes('--help') || args.includes('-h')) {
128
128
  printUsage();
129
129
  return;
@@ -216,5 +216,3 @@ async function run(args) {
216
216
  printUsage();
217
217
  process.exit(1);
218
218
  }
219
-
220
- module.exports = { run };
package/src/config.js CHANGED
@@ -1,29 +1,63 @@
1
- const fs = require('fs');
2
- const path = require('path');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
3
5
 
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;
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
7
8
 
8
- const TECH_CONFIG = JSON.parse(
9
+ const require = createRequire(import.meta.url);
10
+
11
+ export const CONFIGS_DIR = path.join(__dirname, '..', 'configs');
12
+ export const AVAILABLE_TECHS = ['angular', 'nextjs', 'nestjs', 'dotnet', 'fastapi', 'flask'];
13
+ export const VERSION = require('../package.json').version;
14
+
15
+ export const TECH_CONFIG = JSON.parse(
9
16
  fs.readFileSync(path.join(__dirname, 'tech-config.json'), 'utf8')
10
17
  );
11
18
 
12
- function getRuleCategoriesToInclude(techs) {
13
- const categories = new Set();
19
+ /**
20
+ * Get rule directories to include based on selected technologies.
21
+ * Returns paths relative to the rules directory (e.g., "lang/typescript", "domain/frontend").
22
+ */
23
+ export function getRulePathsToInclude(techs) {
24
+ const paths = new Set();
25
+ const { ruleMapping, alwaysInclude } = TECH_CONFIG;
26
+
27
+ // Always include common rules
28
+ alwaysInclude.forEach((dir) => paths.add(dir));
29
+
30
+ // Add language and domain-specific rules based on selected techs
14
31
  for (const tech of techs) {
15
32
  const config = TECH_CONFIG.technologies[tech];
16
- if (config?.includeRules) {
17
- config.includeRules.forEach((cat) => categories.add(cat));
33
+ if (!config) continue;
34
+
35
+ // Add language-specific rules (e.g., "lang/typescript")
36
+ if (config.language && ruleMapping.language[config.language]) {
37
+ paths.add(ruleMapping.language[config.language]);
38
+ }
39
+
40
+ // Add domain-specific rules (e.g., "domain/frontend")
41
+ if (config.type && ruleMapping.type[config.type]) {
42
+ paths.add(ruleMapping.type[config.type]);
18
43
  }
19
44
  }
20
- return categories;
45
+
46
+ return paths;
21
47
  }
22
48
 
23
- module.exports = {
24
- CONFIGS_DIR,
25
- AVAILABLE_TECHS,
26
- VERSION,
27
- TECH_CONFIG,
28
- getRuleCategoriesToInclude,
29
- };
49
+ /**
50
+ * Check if a rule path should be included for the given technologies.
51
+ * @param {string} rulePath - Path relative to rules dir (e.g., "lang/python/async.md")
52
+ * @param {Set<string>} includedPaths - Set of paths to include
53
+ * @returns {boolean}
54
+ */
55
+ export function shouldIncludeRule(rulePath, includedPaths) {
56
+ // Check if the path starts with any of the included paths
57
+ for (const includedPath of includedPaths) {
58
+ if (rulePath === includedPath || rulePath.startsWith(includedPath + '/')) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
package/src/index.js CHANGED
@@ -1,13 +1,4 @@
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
- };
1
+ export { run } from './cli.js';
2
+ export { init, update, status } from './installer.js';
3
+ export { readManifest } from './merge.js';
4
+ export { VERSION } from './config.js';
package/src/installer.js CHANGED
@@ -1,10 +1,95 @@
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');
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);
6
76
 
7
- function listTechnologies() {
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() {
8
93
  console.log(`\n${colors.bold('Available technologies:')}\n`);
9
94
 
10
95
  const techInfo = {
@@ -26,15 +111,15 @@ function listTechnologies() {
26
111
  console.log(`\n${colors.bold('Shared resources:')}\n`);
27
112
 
28
113
  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'));
114
+ const skills = fs.existsSync(path.join(sharedPath, 'skills'));
115
+ const rules = fs.existsSync(path.join(sharedPath, 'rules'));
31
116
 
32
117
  console.log(` ${skills ? colors.green('✓') : colors.red('✗')} skills /learning, /review, /spec, /debug, and more`);
33
118
  console.log(` ${rules ? colors.green('✓') : colors.red('✗')} rules security, performance, accessibility`);
34
119
  console.log('');
35
120
  }
36
121
 
37
- function status(targetDir) {
122
+ export function status(targetDir) {
38
123
  const manifest = readManifest(targetDir);
39
124
 
40
125
  console.log(`\n${colors.bold('AI Rules Status')}\n`);
@@ -84,7 +169,7 @@ function status(targetDir) {
84
169
  }
85
170
  }
86
171
 
87
- function init(techs, options) {
172
+ export function init(techs, options) {
88
173
  const targetDir = path.resolve(options.target || process.cwd());
89
174
  const { dryRun, force } = options;
90
175
  const backup = !force;
@@ -132,7 +217,7 @@ function init(techs, options) {
132
217
  }
133
218
  }
134
219
 
135
- const settingsPath = path.join(techDir, '.claude', 'settings.json');
220
+ const settingsPath = path.join(techDir, 'settings.json');
136
221
  if (fs.existsSync(settingsPath)) {
137
222
  const op = mergeSettingsJson(
138
223
  path.join(targetDir, '.claude', 'settings.json'),
@@ -148,7 +233,7 @@ function init(techs, options) {
148
233
  }
149
234
  }
150
235
 
151
- const rulesDir = path.join(techDir, '.claude', 'rules');
236
+ const rulesDir = path.join(techDir, 'rules');
152
237
  if (fs.existsSync(rulesDir)) {
153
238
  const ops = copyDirRecursive(
154
239
  rulesDir,
@@ -165,9 +250,9 @@ function init(techs, options) {
165
250
  }
166
251
 
167
252
  if (options.withSkills) {
168
- const techSkillsDir = path.join(techDir, '.claude', 'skills');
253
+ const techSkillsDir = path.join(techDir, 'skills');
169
254
  if (fs.existsSync(techSkillsDir)) {
170
- const ops = copyDirRecursive(
255
+ const ops = copySkillsToTarget(
171
256
  techSkillsDir,
172
257
  path.join(targetDir, '.claude', 'skills'),
173
258
  { dryRun, backup, targetDir }
@@ -181,9 +266,9 @@ function init(techs, options) {
181
266
 
182
267
  if (options.withSkills) {
183
268
  log.info(`${dryRun ? 'Would install' : 'Installing'} skills...`);
184
- const skillsDir = path.join(sharedDir, '.claude', 'skills');
269
+ const skillsDir = path.join(sharedDir, 'skills');
185
270
  if (fs.existsSync(skillsDir)) {
186
- const ops = copyDirRecursive(
271
+ const ops = copySkillsToTarget(
187
272
  skillsDir,
188
273
  path.join(targetDir, '.claude', 'skills'),
189
274
  { dryRun, backup, targetDir }
@@ -200,54 +285,29 @@ function init(techs, options) {
200
285
 
201
286
  if (options.withRules) {
202
287
  log.info(`${dryRun ? 'Would install' : 'Installing'} shared rules...`);
203
- const rulesDir = path.join(sharedDir, '.claude', 'rules');
288
+ const rulesDir = path.join(sharedDir, 'rules');
204
289
  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
- }
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);
242
301
 
243
302
  if (dryRun) {
244
- log.dry(` shared rules/ (${totalOps} files)`);
303
+ log.dry(` shared rules/ (${ops.length} files)`);
245
304
  } else {
246
305
  log.success(` shared rules/`);
247
306
  }
248
307
 
249
- if (skippedCategories.length > 0) {
250
- log.info(` (skipped: ${skippedCategories.join(', ')} - not applicable)`);
308
+ if (skippedPaths.length > 0) {
309
+ const uniqueSkipped = [...new Set(skippedPaths)];
310
+ log.info(` (skipped: ${uniqueSkipped.join(', ')} - not applicable)`);
251
311
  }
252
312
  }
253
313
  }
@@ -317,7 +377,7 @@ function init(techs, options) {
317
377
  console.log('');
318
378
  }
319
379
 
320
- async function update(options) {
380
+ export async function update(options) {
321
381
  const targetDir = path.resolve(options.target || process.cwd());
322
382
  const { dryRun, force } = options;
323
383
 
@@ -352,10 +412,3 @@ async function update(options) {
352
412
 
353
413
  init(manifest.technologies, initOptions);
354
414
  }
355
-
356
- module.exports = {
357
- init,
358
- update,
359
- status,
360
- listTechnologies,
361
- };
package/src/merge.js CHANGED
@@ -1,9 +1,9 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const { log, backupFile } = require('./utils');
4
- const { VERSION } = require('./config');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { log, backupFile } from './utils.js';
4
+ import { VERSION } from './config.js';
5
5
 
6
- function mergeClaudeMd(targetPath, sourcePath, isFirst, options = {}) {
6
+ export function mergeClaudeMd(targetPath, sourcePath, isFirst, options = {}) {
7
7
  const { dryRun = false, backup = false, targetDir } = options;
8
8
  const content = fs.readFileSync(sourcePath, 'utf8');
9
9
  const exists = fs.existsSync(targetPath);
@@ -26,7 +26,7 @@ function mergeClaudeMd(targetPath, sourcePath, isFirst, options = {}) {
26
26
  return { type: exists ? 'merge' : 'create', path: 'CLAUDE.md' };
27
27
  }
28
28
 
29
- function mergeSettingsJson(targetPath, sourcePath, options = {}) {
29
+ export function mergeSettingsJson(targetPath, sourcePath, options = {}) {
30
30
  const { dryRun = false, backup = false, targetDir } = options;
31
31
  const exists = fs.existsSync(targetPath);
32
32
 
@@ -83,7 +83,7 @@ function getManifestPath(targetDir) {
83
83
  return path.join(targetDir, '.claude', '.ai-rules.json');
84
84
  }
85
85
 
86
- function readManifest(targetDir) {
86
+ export function readManifest(targetDir) {
87
87
  const manifestPath = getManifestPath(targetDir);
88
88
  if (!fs.existsSync(manifestPath)) return null;
89
89
 
@@ -94,7 +94,7 @@ function readManifest(targetDir) {
94
94
  }
95
95
  }
96
96
 
97
- function writeManifest(targetDir, data, dryRun = false) {
97
+ export function writeManifest(targetDir, data, dryRun = false) {
98
98
  if (dryRun) return;
99
99
 
100
100
  const manifestPath = getManifestPath(targetDir);
@@ -107,10 +107,3 @@ function writeManifest(targetDir, data, dryRun = false) {
107
107
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
108
108
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
109
109
  }
110
-
111
- module.exports = {
112
- mergeClaudeMd,
113
- mergeSettingsJson,
114
- readManifest,
115
- writeManifest,
116
- };