@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.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /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
|
+
};
|
package/src/installer.js
ADDED
|
@@ -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
|
+
};
|