@malamute/ai-rules 1.0.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 +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Python Project Guidelines
|
|
2
|
+
|
|
3
|
+
@../_shared/CLAUDE.md
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- Python 3.11+
|
|
8
|
+
- FastAPI or Flask
|
|
9
|
+
- SQLAlchemy 2.0+ (async support)
|
|
10
|
+
- Pydantic v2 for validation
|
|
11
|
+
- pytest for testing
|
|
12
|
+
- uv or poetry for dependency management
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
### Domain-Based Structure (Recommended for Monoliths)
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── app/
|
|
21
|
+
│ ├── __init__.py
|
|
22
|
+
│ ├── main.py # Application entry point
|
|
23
|
+
│ ├── config.py # Settings with pydantic-settings
|
|
24
|
+
│ ├── database.py # DB session, engine
|
|
25
|
+
│ │
|
|
26
|
+
│ ├── users/ # Domain module
|
|
27
|
+
│ │ ├── __init__.py
|
|
28
|
+
│ │ ├── router.py # API endpoints
|
|
29
|
+
│ │ ├── schemas.py # Pydantic models (request/response)
|
|
30
|
+
│ │ ├── models.py # SQLAlchemy models
|
|
31
|
+
│ │ ├── service.py # Business logic
|
|
32
|
+
│ │ ├── repository.py # Data access
|
|
33
|
+
│ │ ├── dependencies.py # Route dependencies
|
|
34
|
+
│ │ └── exceptions.py # Domain exceptions
|
|
35
|
+
│ │
|
|
36
|
+
│ ├── auth/
|
|
37
|
+
│ │ ├── router.py
|
|
38
|
+
│ │ ├── schemas.py
|
|
39
|
+
│ │ ├── service.py
|
|
40
|
+
│ │ ├── jwt.py
|
|
41
|
+
│ │ └── dependencies.py
|
|
42
|
+
│ │
|
|
43
|
+
│ ├── core/ # Shared utilities
|
|
44
|
+
│ │ ├── __init__.py
|
|
45
|
+
│ │ ├── exceptions.py # Base exceptions
|
|
46
|
+
│ │ ├── security.py # Password hashing, etc.
|
|
47
|
+
│ │ └── pagination.py
|
|
48
|
+
│ │
|
|
49
|
+
│ └── common/
|
|
50
|
+
│ ├── models.py # Base model classes
|
|
51
|
+
│ └── schemas.py # Shared schemas
|
|
52
|
+
│
|
|
53
|
+
├── tests/
|
|
54
|
+
│ ├── conftest.py
|
|
55
|
+
│ ├── users/
|
|
56
|
+
│ │ ├── test_router.py
|
|
57
|
+
│ │ └── test_service.py
|
|
58
|
+
│ └── auth/
|
|
59
|
+
│
|
|
60
|
+
├── alembic/ # Migrations
|
|
61
|
+
│ ├── versions/
|
|
62
|
+
│ └── env.py
|
|
63
|
+
│
|
|
64
|
+
├── pyproject.toml
|
|
65
|
+
└── alembic.ini
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### File-Type Structure (For Microservices)
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
src/
|
|
72
|
+
├── app/
|
|
73
|
+
│ ├── main.py
|
|
74
|
+
│ ├── config.py
|
|
75
|
+
│ ├── routers/
|
|
76
|
+
│ │ ├── users.py
|
|
77
|
+
│ │ └── auth.py
|
|
78
|
+
│ ├── schemas/
|
|
79
|
+
│ │ ├── user.py
|
|
80
|
+
│ │ └── auth.py
|
|
81
|
+
│ ├── models/
|
|
82
|
+
│ │ └── user.py
|
|
83
|
+
│ ├── services/
|
|
84
|
+
│ │ └── user_service.py
|
|
85
|
+
│ └── repositories/
|
|
86
|
+
│ └── user_repository.py
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Code Style
|
|
90
|
+
|
|
91
|
+
### Type Hints (Required)
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Always use type hints
|
|
95
|
+
def get_user_by_email(email: str) -> User | None:
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
async def create_user(user_data: UserCreate) -> User:
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
# Use modern syntax (Python 3.10+)
|
|
102
|
+
def process_items(items: list[str]) -> dict[str, int]: # Not List[str]
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Naming Conventions
|
|
107
|
+
|
|
108
|
+
| Element | Convention | Example |
|
|
109
|
+
|---------|------------|---------|
|
|
110
|
+
| Modules | snake_case | `user_service.py` |
|
|
111
|
+
| Classes | PascalCase | `UserService` |
|
|
112
|
+
| Functions | snake_case | `get_user_by_id` |
|
|
113
|
+
| Variables | snake_case | `user_count` |
|
|
114
|
+
| Constants | UPPER_SNAKE | `MAX_RETRY_COUNT` |
|
|
115
|
+
| Private | _prefix | `_internal_method` |
|
|
116
|
+
|
|
117
|
+
### Async/Await
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# FastAPI is async-first - use async for I/O operations
|
|
121
|
+
async def get_users(db: AsyncSession) -> list[User]:
|
|
122
|
+
result = await db.execute(select(User))
|
|
123
|
+
return result.scalars().all()
|
|
124
|
+
|
|
125
|
+
# Don't mix sync I/O in async functions
|
|
126
|
+
# Bad - blocks event loop
|
|
127
|
+
async def bad_example():
|
|
128
|
+
time.sleep(1) # Blocks!
|
|
129
|
+
requests.get(url) # Blocks!
|
|
130
|
+
|
|
131
|
+
# Good - use async versions
|
|
132
|
+
async def good_example():
|
|
133
|
+
await asyncio.sleep(1)
|
|
134
|
+
async with httpx.AsyncClient() as client:
|
|
135
|
+
await client.get(url)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Commands
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# Development
|
|
142
|
+
uvicorn app.main:app --reload
|
|
143
|
+
# or
|
|
144
|
+
fastapi dev app/main.py
|
|
145
|
+
|
|
146
|
+
# Tests
|
|
147
|
+
pytest
|
|
148
|
+
pytest -v --cov=app
|
|
149
|
+
pytest -k "test_users"
|
|
150
|
+
|
|
151
|
+
# Linting
|
|
152
|
+
ruff check .
|
|
153
|
+
ruff format .
|
|
154
|
+
|
|
155
|
+
# Type checking
|
|
156
|
+
mypy app/
|
|
157
|
+
|
|
158
|
+
# Migrations (Alembic)
|
|
159
|
+
alembic revision --autogenerate -m "Add users table"
|
|
160
|
+
alembic upgrade head
|
|
161
|
+
alembic downgrade -1
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Common Patterns
|
|
165
|
+
|
|
166
|
+
### Pydantic Settings
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
170
|
+
|
|
171
|
+
class Settings(BaseSettings):
|
|
172
|
+
model_config = SettingsConfigDict(
|
|
173
|
+
env_file=".env",
|
|
174
|
+
env_file_encoding="utf-8",
|
|
175
|
+
extra="ignore",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Database
|
|
179
|
+
database_url: str
|
|
180
|
+
|
|
181
|
+
# Auth
|
|
182
|
+
secret_key: str
|
|
183
|
+
algorithm: str = "HS256"
|
|
184
|
+
access_token_expire_minutes: int = 30
|
|
185
|
+
|
|
186
|
+
# App
|
|
187
|
+
debug: bool = False
|
|
188
|
+
api_prefix: str = "/api/v1"
|
|
189
|
+
|
|
190
|
+
settings = Settings()
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Dependency Injection
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from typing import Annotated
|
|
197
|
+
from fastapi import Depends
|
|
198
|
+
|
|
199
|
+
# Database session dependency
|
|
200
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
201
|
+
async with async_session_maker() as session:
|
|
202
|
+
try:
|
|
203
|
+
yield session
|
|
204
|
+
await session.commit()
|
|
205
|
+
except Exception:
|
|
206
|
+
await session.rollback()
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
DbSession = Annotated[AsyncSession, Depends(get_db)]
|
|
210
|
+
|
|
211
|
+
# Service dependency
|
|
212
|
+
def get_user_service(db: DbSession) -> UserService:
|
|
213
|
+
return UserService(UserRepository(db))
|
|
214
|
+
|
|
215
|
+
UserServiceDep = Annotated[UserService, Depends(get_user_service)]
|
|
216
|
+
|
|
217
|
+
# Usage in router
|
|
218
|
+
@router.get("/{user_id}")
|
|
219
|
+
async def get_user(user_id: int, service: UserServiceDep) -> UserResponse:
|
|
220
|
+
return await service.get_by_id(user_id)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Exception Handling
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from fastapi import HTTPException, status
|
|
227
|
+
|
|
228
|
+
# Domain exceptions
|
|
229
|
+
class UserNotFoundError(Exception):
|
|
230
|
+
def __init__(self, user_id: int):
|
|
231
|
+
self.user_id = user_id
|
|
232
|
+
super().__init__(f"User {user_id} not found")
|
|
233
|
+
|
|
234
|
+
class EmailAlreadyExistsError(Exception):
|
|
235
|
+
def __init__(self, email: str):
|
|
236
|
+
self.email = email
|
|
237
|
+
|
|
238
|
+
# Global exception handler
|
|
239
|
+
@app.exception_handler(UserNotFoundError)
|
|
240
|
+
async def user_not_found_handler(request: Request, exc: UserNotFoundError):
|
|
241
|
+
return JSONResponse(
|
|
242
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
243
|
+
content={"detail": str(exc)},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Or use HTTPException directly in service
|
|
247
|
+
class UserService:
|
|
248
|
+
async def get_by_id(self, user_id: int) -> User:
|
|
249
|
+
user = await self.repository.get(user_id)
|
|
250
|
+
if not user:
|
|
251
|
+
raise HTTPException(
|
|
252
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
253
|
+
detail=f"User {user_id} not found",
|
|
254
|
+
)
|
|
255
|
+
return user
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### API Versioning
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from fastapi import APIRouter
|
|
262
|
+
|
|
263
|
+
# Versioned routers
|
|
264
|
+
v1_router = APIRouter(prefix="/api/v1")
|
|
265
|
+
v1_router.include_router(users.router, prefix="/users", tags=["users"])
|
|
266
|
+
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
|
267
|
+
|
|
268
|
+
v2_router = APIRouter(prefix="/api/v2")
|
|
269
|
+
# ... v2 routes
|
|
270
|
+
|
|
271
|
+
app.include_router(v1_router)
|
|
272
|
+
app.include_router(v2_router)
|
|
273
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malamute/ai-rules",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code configuration boilerplates for Angular, Next.js, NestJS, .NET, Python and more",
|
|
5
|
+
"main": "src/install.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ai-rules": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node bin/cli.js --help"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"ai",
|
|
17
|
+
"coding",
|
|
18
|
+
"config",
|
|
19
|
+
"boilerplate",
|
|
20
|
+
"angular",
|
|
21
|
+
"nextjs",
|
|
22
|
+
"nestjs",
|
|
23
|
+
"dotnet",
|
|
24
|
+
"python",
|
|
25
|
+
"fastapi"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": ""
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin",
|
|
35
|
+
"src",
|
|
36
|
+
"configs"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/install.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
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', 'python'];
|
|
6
|
+
|
|
7
|
+
const colors = {
|
|
8
|
+
red: (text) => `\x1b[31m${text}\x1b[0m`,
|
|
9
|
+
green: (text) => `\x1b[32m${text}\x1b[0m`,
|
|
10
|
+
blue: (text) => `\x1b[34m${text}\x1b[0m`,
|
|
11
|
+
yellow: (text) => `\x1b[33m${text}\x1b[0m`,
|
|
12
|
+
bold: (text) => `\x1b[1m${text}\x1b[0m`,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const log = {
|
|
16
|
+
info: (msg) => console.log(`${colors.blue('ℹ')} ${msg}`),
|
|
17
|
+
success: (msg) => console.log(`${colors.green('✓')} ${msg}`),
|
|
18
|
+
warning: (msg) => console.log(`${colors.yellow('⚠')} ${msg}`),
|
|
19
|
+
error: (msg) => console.log(`${colors.red('✗')} ${msg}`),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function printUsage() {
|
|
23
|
+
console.log(`
|
|
24
|
+
${colors.bold('AI Rules')} - Claude Code configuration boilerplates
|
|
25
|
+
|
|
26
|
+
${colors.bold('Usage:')}
|
|
27
|
+
ai-rules init <tech> [tech2] [options]
|
|
28
|
+
ai-rules list
|
|
29
|
+
|
|
30
|
+
${colors.bold('Technologies:')}
|
|
31
|
+
angular Angular 21 + Nx + NgRx
|
|
32
|
+
nextjs Next.js 15 + React 19
|
|
33
|
+
nestjs NestJS + Prisma/TypeORM
|
|
34
|
+
dotnet .NET 8 + EF Core
|
|
35
|
+
python FastAPI/Flask + SQLAlchemy
|
|
36
|
+
|
|
37
|
+
${colors.bold('Options:')}
|
|
38
|
+
--with-skills Include skills (/learning, /review, /spec, /debug)
|
|
39
|
+
--with-commands Include commands (fix-issue, review-pr, generate-tests)
|
|
40
|
+
--with-rules Include shared rules (security, performance, accessibility)
|
|
41
|
+
--all Include skills, commands, and rules
|
|
42
|
+
--target <dir> Target directory (default: current directory)
|
|
43
|
+
|
|
44
|
+
${colors.bold('Examples:')}
|
|
45
|
+
ai-rules init angular
|
|
46
|
+
ai-rules init angular nestjs --all
|
|
47
|
+
ai-rules init nextjs python --with-skills
|
|
48
|
+
ai-rules init dotnet --target ./my-project
|
|
49
|
+
ai-rules list
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function copyDirRecursive(src, dest) {
|
|
54
|
+
if (!fs.existsSync(src)) return;
|
|
55
|
+
|
|
56
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
57
|
+
|
|
58
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const srcPath = path.join(src, entry.name);
|
|
62
|
+
const destPath = path.join(dest, entry.name);
|
|
63
|
+
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
copyDirRecursive(srcPath, destPath);
|
|
66
|
+
} else {
|
|
67
|
+
fs.copyFileSync(srcPath, destPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mergeClaudeMd(targetPath, sourcePath, isFirst) {
|
|
73
|
+
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
74
|
+
|
|
75
|
+
if (isFirst) {
|
|
76
|
+
fs.writeFileSync(targetPath, content);
|
|
77
|
+
} else {
|
|
78
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
79
|
+
fs.writeFileSync(targetPath, `${existing}\n\n---\n\n${content}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listTechnologies() {
|
|
84
|
+
console.log(`\n${colors.bold('Available technologies:')}\n`);
|
|
85
|
+
|
|
86
|
+
const techInfo = {
|
|
87
|
+
angular: 'Angular 21 + Nx + NgRx + Signals',
|
|
88
|
+
nextjs: 'Next.js 15 + React 19 + App Router',
|
|
89
|
+
nestjs: 'NestJS 10 + Prisma/TypeORM + Passport',
|
|
90
|
+
dotnet: '.NET 8 + ASP.NET Core + EF Core',
|
|
91
|
+
python: 'FastAPI/Flask + SQLAlchemy 2.0',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const tech of AVAILABLE_TECHS) {
|
|
95
|
+
const techPath = path.join(CONFIGS_DIR, tech);
|
|
96
|
+
const exists = fs.existsSync(techPath);
|
|
97
|
+
const status = exists ? colors.green('✓') : colors.red('✗');
|
|
98
|
+
console.log(` ${status} ${colors.bold(tech.padEnd(10))} ${techInfo[tech]}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(`\n${colors.bold('Shared resources:')}\n`);
|
|
102
|
+
|
|
103
|
+
const sharedPath = path.join(CONFIGS_DIR, '_shared');
|
|
104
|
+
const skills = fs.existsSync(path.join(sharedPath, '.claude', 'skills'));
|
|
105
|
+
const commands = fs.existsSync(path.join(sharedPath, '.claude', 'commands'));
|
|
106
|
+
const rules = fs.existsSync(path.join(sharedPath, '.claude', 'rules'));
|
|
107
|
+
|
|
108
|
+
console.log(` ${skills ? colors.green('✓') : colors.red('✗')} skills /learning, /review, /spec, /debug`);
|
|
109
|
+
console.log(` ${commands ? colors.green('✓') : colors.red('✗')} commands fix-issue, review-pr, generate-tests`);
|
|
110
|
+
console.log(` ${rules ? colors.green('✓') : colors.red('✗')} rules security, performance, accessibility`);
|
|
111
|
+
console.log('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function init(techs, options) {
|
|
115
|
+
const targetDir = options.target || process.cwd();
|
|
116
|
+
|
|
117
|
+
log.info(`Installing Claude Code config to: ${targetDir}`);
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
// Create directories
|
|
121
|
+
fs.mkdirSync(path.join(targetDir, '.claude', 'rules'), { recursive: true });
|
|
122
|
+
|
|
123
|
+
// Install each technology
|
|
124
|
+
let isFirstClaudeMd = true;
|
|
125
|
+
|
|
126
|
+
for (const tech of techs) {
|
|
127
|
+
log.info(`Installing ${tech}...`);
|
|
128
|
+
|
|
129
|
+
const techDir = path.join(CONFIGS_DIR, tech);
|
|
130
|
+
|
|
131
|
+
if (!fs.existsSync(techDir)) {
|
|
132
|
+
log.error(`Technology directory not found: ${tech}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Copy CLAUDE.md
|
|
137
|
+
const claudeMdPath = path.join(techDir, 'CLAUDE.md');
|
|
138
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
139
|
+
mergeClaudeMd(
|
|
140
|
+
path.join(targetDir, 'CLAUDE.md'),
|
|
141
|
+
claudeMdPath,
|
|
142
|
+
isFirstClaudeMd
|
|
143
|
+
);
|
|
144
|
+
isFirstClaudeMd = false;
|
|
145
|
+
log.success(' CLAUDE.md');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Copy settings.json
|
|
149
|
+
const settingsPath = path.join(techDir, '.claude', 'settings.json');
|
|
150
|
+
if (fs.existsSync(settingsPath)) {
|
|
151
|
+
const targetSettingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
152
|
+
|
|
153
|
+
if (!fs.existsSync(targetSettingsPath)) {
|
|
154
|
+
fs.copyFileSync(settingsPath, targetSettingsPath);
|
|
155
|
+
} else {
|
|
156
|
+
// Merge permissions
|
|
157
|
+
try {
|
|
158
|
+
const existing = JSON.parse(fs.readFileSync(targetSettingsPath, 'utf8'));
|
|
159
|
+
const incoming = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
160
|
+
|
|
161
|
+
const merged = {
|
|
162
|
+
permissions: {
|
|
163
|
+
allow: [...new Set([
|
|
164
|
+
...(existing.permissions?.allow || []),
|
|
165
|
+
...(incoming.permissions?.allow || []),
|
|
166
|
+
])],
|
|
167
|
+
deny: [],
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
fs.writeFileSync(targetSettingsPath, JSON.stringify(merged, null, 2));
|
|
172
|
+
} catch (e) {
|
|
173
|
+
log.warning(' Could not merge settings.json');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
log.success(' settings.json');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Copy rules
|
|
180
|
+
const rulesDir = path.join(techDir, '.claude', 'rules');
|
|
181
|
+
if (fs.existsSync(rulesDir)) {
|
|
182
|
+
copyDirRecursive(rulesDir, path.join(targetDir, '.claude', 'rules'));
|
|
183
|
+
log.success(' rules/');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Install shared resources
|
|
188
|
+
const sharedDir = path.join(CONFIGS_DIR, '_shared');
|
|
189
|
+
|
|
190
|
+
if (options.withSkills || options.all) {
|
|
191
|
+
log.info('Installing skills...');
|
|
192
|
+
const skillsDir = path.join(sharedDir, '.claude', 'skills');
|
|
193
|
+
if (fs.existsSync(skillsDir)) {
|
|
194
|
+
copyDirRecursive(skillsDir, path.join(targetDir, '.claude', 'skills'));
|
|
195
|
+
log.success(' skills/ (learning, review, spec, debug)');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.withCommands || options.all) {
|
|
200
|
+
log.info('Installing commands...');
|
|
201
|
+
const commandsDir = path.join(sharedDir, '.claude', 'commands');
|
|
202
|
+
if (fs.existsSync(commandsDir)) {
|
|
203
|
+
copyDirRecursive(commandsDir, path.join(targetDir, '.claude', 'commands'));
|
|
204
|
+
log.success(' commands/ (fix-issue, review-pr, generate-tests)');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (options.withRules || options.all) {
|
|
209
|
+
log.info('Installing shared rules...');
|
|
210
|
+
const rulesDir = path.join(sharedDir, '.claude', 'rules');
|
|
211
|
+
if (fs.existsSync(rulesDir)) {
|
|
212
|
+
copyDirRecursive(rulesDir, path.join(targetDir, '.claude', 'rules'));
|
|
213
|
+
log.success(' rules/ (security, performance, accessibility)');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Resolve @../_shared/CLAUDE.md imports
|
|
218
|
+
const targetClaudeMd = path.join(targetDir, 'CLAUDE.md');
|
|
219
|
+
if (fs.existsSync(targetClaudeMd)) {
|
|
220
|
+
let content = fs.readFileSync(targetClaudeMd, 'utf8');
|
|
221
|
+
|
|
222
|
+
if (content.includes('@../_shared/CLAUDE.md')) {
|
|
223
|
+
const sharedClaudeMd = path.join(sharedDir, 'CLAUDE.md');
|
|
224
|
+
if (fs.existsSync(sharedClaudeMd)) {
|
|
225
|
+
const sharedContent = fs.readFileSync(sharedClaudeMd, 'utf8');
|
|
226
|
+
content = content.replace(/@..\/_shared\/CLAUDE\.md/g, '');
|
|
227
|
+
content = sharedContent + '\n\n' + content;
|
|
228
|
+
fs.writeFileSync(targetClaudeMd, content);
|
|
229
|
+
log.success('Merged shared conventions into CLAUDE.md');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('');
|
|
235
|
+
log.success('Installation complete!');
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log('Installed:');
|
|
238
|
+
console.log(` - Technologies: ${techs.join(', ')}`);
|
|
239
|
+
if (options.withSkills || options.all) {
|
|
240
|
+
console.log(' - Skills: /learning, /review, /spec, /debug');
|
|
241
|
+
}
|
|
242
|
+
if (options.withCommands || options.all) {
|
|
243
|
+
console.log(' - Commands: fix-issue, review-pr, generate-tests');
|
|
244
|
+
}
|
|
245
|
+
if (options.withRules || options.all) {
|
|
246
|
+
console.log(' - Rules: security, performance, accessibility');
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
console.log(`Files created in: ${targetDir}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function run(args) {
|
|
253
|
+
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
254
|
+
printUsage();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const command = args[0];
|
|
259
|
+
|
|
260
|
+
if (command === 'list') {
|
|
261
|
+
listTechnologies();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'init') {
|
|
266
|
+
const options = {
|
|
267
|
+
target: null,
|
|
268
|
+
withSkills: false,
|
|
269
|
+
withCommands: false,
|
|
270
|
+
withRules: false,
|
|
271
|
+
all: false,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const techs = [];
|
|
275
|
+
|
|
276
|
+
for (let i = 1; i < args.length; i++) {
|
|
277
|
+
const arg = args[i];
|
|
278
|
+
|
|
279
|
+
if (arg === '--with-skills') {
|
|
280
|
+
options.withSkills = true;
|
|
281
|
+
} else if (arg === '--with-commands') {
|
|
282
|
+
options.withCommands = true;
|
|
283
|
+
} else if (arg === '--with-rules') {
|
|
284
|
+
options.withRules = true;
|
|
285
|
+
} else if (arg === '--all') {
|
|
286
|
+
options.all = true;
|
|
287
|
+
} else if (arg === '--target') {
|
|
288
|
+
options.target = args[++i];
|
|
289
|
+
} else if (!arg.startsWith('-')) {
|
|
290
|
+
if (AVAILABLE_TECHS.includes(arg)) {
|
|
291
|
+
techs.push(arg);
|
|
292
|
+
} else {
|
|
293
|
+
log.error(`Unknown technology: ${arg}`);
|
|
294
|
+
console.log(`Available: ${AVAILABLE_TECHS.join(', ')}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (techs.length === 0) {
|
|
301
|
+
log.error('No technology specified');
|
|
302
|
+
printUsage();
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
init(techs, options);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
log.error(`Unknown command: ${command}`);
|
|
311
|
+
printUsage();
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = { run };
|