axiom-coding-agent-setup 1.0.1 → 1.0.3
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/.agents/skills/fastapi-templates/SKILL.md +540 -0
- package/.agents/skills/git-commit/SKILL.md +124 -0
- package/bin/cli.js +138 -138
- package/package.json +29 -29
- package/skills-lock.json +15 -0
- package/docs/npx-cli-setup-guide.md +0 -318
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastapi-templates
|
|
3
|
+
description: Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Project Templates
|
|
7
|
+
|
|
8
|
+
Production-ready FastAPI project structures with async patterns, dependency injection, middleware, and best practices for building high-performance APIs.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Starting new FastAPI projects from scratch
|
|
13
|
+
- Implementing async REST APIs with Python
|
|
14
|
+
- Building high-performance web services and microservices
|
|
15
|
+
- Creating async applications with PostgreSQL, MongoDB
|
|
16
|
+
- Setting up API projects with proper structure and testing
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
20
|
+
### 1. Project Structure
|
|
21
|
+
|
|
22
|
+
**Recommended Layout:**
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
app/
|
|
26
|
+
├── api/ # API routes
|
|
27
|
+
│ ├── v1/
|
|
28
|
+
│ │ ├── endpoints/
|
|
29
|
+
│ │ │ ├── users.py
|
|
30
|
+
│ │ │ ├── auth.py
|
|
31
|
+
│ │ │ └── items.py
|
|
32
|
+
│ │ └── router.py
|
|
33
|
+
│ └── dependencies.py # Shared dependencies
|
|
34
|
+
├── core/ # Core configuration
|
|
35
|
+
│ ├── config.py
|
|
36
|
+
│ ├── security.py
|
|
37
|
+
│ └── database.py
|
|
38
|
+
├── models/ # Database models
|
|
39
|
+
│ ├── user.py
|
|
40
|
+
│ └── item.py
|
|
41
|
+
├── schemas/ # Pydantic schemas
|
|
42
|
+
│ ├── user.py
|
|
43
|
+
│ └── item.py
|
|
44
|
+
├── services/ # Business logic
|
|
45
|
+
│ ├── user_service.py
|
|
46
|
+
│ └── auth_service.py
|
|
47
|
+
├── repositories/ # Data access
|
|
48
|
+
│ ├── user_repository.py
|
|
49
|
+
│ └── item_repository.py
|
|
50
|
+
└── main.py # Application entry
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Dependency Injection
|
|
54
|
+
|
|
55
|
+
FastAPI's built-in DI system using `Depends`:
|
|
56
|
+
|
|
57
|
+
- Database session management
|
|
58
|
+
- Authentication/authorization
|
|
59
|
+
- Shared business logic
|
|
60
|
+
- Configuration injection
|
|
61
|
+
|
|
62
|
+
### 3. Async Patterns
|
|
63
|
+
|
|
64
|
+
Proper async/await usage:
|
|
65
|
+
|
|
66
|
+
- Async route handlers
|
|
67
|
+
- Async database operations
|
|
68
|
+
- Async background tasks
|
|
69
|
+
- Async middleware
|
|
70
|
+
|
|
71
|
+
## Implementation Patterns
|
|
72
|
+
|
|
73
|
+
### Pattern 1: Complete FastAPI Application
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# main.py
|
|
77
|
+
from fastapi import FastAPI, Depends
|
|
78
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
79
|
+
from contextlib import asynccontextmanager
|
|
80
|
+
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def lifespan(app: FastAPI):
|
|
83
|
+
"""Application lifespan events."""
|
|
84
|
+
# Startup
|
|
85
|
+
await database.connect()
|
|
86
|
+
yield
|
|
87
|
+
# Shutdown
|
|
88
|
+
await database.disconnect()
|
|
89
|
+
|
|
90
|
+
app = FastAPI(
|
|
91
|
+
title="API Template",
|
|
92
|
+
version="1.0.0",
|
|
93
|
+
lifespan=lifespan
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# CORS middleware
|
|
97
|
+
app.add_middleware(
|
|
98
|
+
CORSMiddleware,
|
|
99
|
+
allow_origins=["*"],
|
|
100
|
+
allow_credentials=True,
|
|
101
|
+
allow_methods=["*"],
|
|
102
|
+
allow_headers=["*"],
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Include routers
|
|
106
|
+
from app.api.v1.router import api_router
|
|
107
|
+
app.include_router(api_router, prefix="/api/v1")
|
|
108
|
+
|
|
109
|
+
# core/config.py
|
|
110
|
+
from pydantic_settings import BaseSettings
|
|
111
|
+
from functools import lru_cache
|
|
112
|
+
|
|
113
|
+
class Settings(BaseSettings):
|
|
114
|
+
"""Application settings."""
|
|
115
|
+
DATABASE_URL: str
|
|
116
|
+
SECRET_KEY: str
|
|
117
|
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
|
118
|
+
API_V1_STR: str = "/api/v1"
|
|
119
|
+
|
|
120
|
+
class Config:
|
|
121
|
+
env_file = ".env"
|
|
122
|
+
|
|
123
|
+
@lru_cache()
|
|
124
|
+
def get_settings() -> Settings:
|
|
125
|
+
return Settings()
|
|
126
|
+
|
|
127
|
+
# core/database.py
|
|
128
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
129
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
130
|
+
from sqlalchemy.orm import sessionmaker
|
|
131
|
+
from app.core.config import get_settings
|
|
132
|
+
|
|
133
|
+
settings = get_settings()
|
|
134
|
+
|
|
135
|
+
engine = create_async_engine(
|
|
136
|
+
settings.DATABASE_URL,
|
|
137
|
+
echo=True,
|
|
138
|
+
future=True
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
AsyncSessionLocal = sessionmaker(
|
|
142
|
+
engine,
|
|
143
|
+
class_=AsyncSession,
|
|
144
|
+
expire_on_commit=False
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
Base = declarative_base()
|
|
148
|
+
|
|
149
|
+
async def get_db() -> AsyncSession:
|
|
150
|
+
"""Dependency for database session."""
|
|
151
|
+
async with AsyncSessionLocal() as session:
|
|
152
|
+
try:
|
|
153
|
+
yield session
|
|
154
|
+
await session.commit()
|
|
155
|
+
except Exception:
|
|
156
|
+
await session.rollback()
|
|
157
|
+
raise
|
|
158
|
+
finally:
|
|
159
|
+
await session.close()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Pattern 2: CRUD Repository Pattern
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# repositories/base_repository.py
|
|
166
|
+
from typing import Generic, TypeVar, Type, Optional, List
|
|
167
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
168
|
+
from sqlalchemy import select
|
|
169
|
+
from pydantic import BaseModel
|
|
170
|
+
|
|
171
|
+
ModelType = TypeVar("ModelType")
|
|
172
|
+
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
|
173
|
+
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
|
174
|
+
|
|
175
|
+
class BaseRepository(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|
176
|
+
"""Base repository for CRUD operations."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, model: Type[ModelType]):
|
|
179
|
+
self.model = model
|
|
180
|
+
|
|
181
|
+
async def get(self, db: AsyncSession, id: int) -> Optional[ModelType]:
|
|
182
|
+
"""Get by ID."""
|
|
183
|
+
result = await db.execute(
|
|
184
|
+
select(self.model).where(self.model.id == id)
|
|
185
|
+
)
|
|
186
|
+
return result.scalars().first()
|
|
187
|
+
|
|
188
|
+
async def get_multi(
|
|
189
|
+
self,
|
|
190
|
+
db: AsyncSession,
|
|
191
|
+
skip: int = 0,
|
|
192
|
+
limit: int = 100
|
|
193
|
+
) -> List[ModelType]:
|
|
194
|
+
"""Get multiple records."""
|
|
195
|
+
result = await db.execute(
|
|
196
|
+
select(self.model).offset(skip).limit(limit)
|
|
197
|
+
)
|
|
198
|
+
return result.scalars().all()
|
|
199
|
+
|
|
200
|
+
async def create(
|
|
201
|
+
self,
|
|
202
|
+
db: AsyncSession,
|
|
203
|
+
obj_in: CreateSchemaType
|
|
204
|
+
) -> ModelType:
|
|
205
|
+
"""Create new record."""
|
|
206
|
+
db_obj = self.model(**obj_in.dict())
|
|
207
|
+
db.add(db_obj)
|
|
208
|
+
await db.flush()
|
|
209
|
+
await db.refresh(db_obj)
|
|
210
|
+
return db_obj
|
|
211
|
+
|
|
212
|
+
async def update(
|
|
213
|
+
self,
|
|
214
|
+
db: AsyncSession,
|
|
215
|
+
db_obj: ModelType,
|
|
216
|
+
obj_in: UpdateSchemaType
|
|
217
|
+
) -> ModelType:
|
|
218
|
+
"""Update record."""
|
|
219
|
+
update_data = obj_in.dict(exclude_unset=True)
|
|
220
|
+
for field, value in update_data.items():
|
|
221
|
+
setattr(db_obj, field, value)
|
|
222
|
+
await db.flush()
|
|
223
|
+
await db.refresh(db_obj)
|
|
224
|
+
return db_obj
|
|
225
|
+
|
|
226
|
+
async def delete(self, db: AsyncSession, id: int) -> bool:
|
|
227
|
+
"""Delete record."""
|
|
228
|
+
obj = await self.get(db, id)
|
|
229
|
+
if obj:
|
|
230
|
+
await db.delete(obj)
|
|
231
|
+
return True
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
# repositories/user_repository.py
|
|
235
|
+
from app.repositories.base_repository import BaseRepository
|
|
236
|
+
from app.models.user import User
|
|
237
|
+
from app.schemas.user import UserCreate, UserUpdate
|
|
238
|
+
|
|
239
|
+
class UserRepository(BaseRepository[User, UserCreate, UserUpdate]):
|
|
240
|
+
"""User-specific repository."""
|
|
241
|
+
|
|
242
|
+
async def get_by_email(self, db: AsyncSession, email: str) -> Optional[User]:
|
|
243
|
+
"""Get user by email."""
|
|
244
|
+
result = await db.execute(
|
|
245
|
+
select(User).where(User.email == email)
|
|
246
|
+
)
|
|
247
|
+
return result.scalars().first()
|
|
248
|
+
|
|
249
|
+
async def is_active(self, db: AsyncSession, user_id: int) -> bool:
|
|
250
|
+
"""Check if user is active."""
|
|
251
|
+
user = await self.get(db, user_id)
|
|
252
|
+
return user.is_active if user else False
|
|
253
|
+
|
|
254
|
+
user_repository = UserRepository(User)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Pattern 3: Service Layer
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
# services/user_service.py
|
|
261
|
+
from typing import Optional
|
|
262
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
263
|
+
from app.repositories.user_repository import user_repository
|
|
264
|
+
from app.schemas.user import UserCreate, UserUpdate, User
|
|
265
|
+
from app.core.security import get_password_hash, verify_password
|
|
266
|
+
|
|
267
|
+
class UserService:
|
|
268
|
+
"""Business logic for users."""
|
|
269
|
+
|
|
270
|
+
def __init__(self):
|
|
271
|
+
self.repository = user_repository
|
|
272
|
+
|
|
273
|
+
async def create_user(
|
|
274
|
+
self,
|
|
275
|
+
db: AsyncSession,
|
|
276
|
+
user_in: UserCreate
|
|
277
|
+
) -> User:
|
|
278
|
+
"""Create new user with hashed password."""
|
|
279
|
+
# Check if email exists
|
|
280
|
+
existing = await self.repository.get_by_email(db, user_in.email)
|
|
281
|
+
if existing:
|
|
282
|
+
raise ValueError("Email already registered")
|
|
283
|
+
|
|
284
|
+
# Hash password
|
|
285
|
+
user_in_dict = user_in.dict()
|
|
286
|
+
user_in_dict["hashed_password"] = get_password_hash(user_in_dict.pop("password"))
|
|
287
|
+
|
|
288
|
+
# Create user
|
|
289
|
+
user = await self.repository.create(db, UserCreate(**user_in_dict))
|
|
290
|
+
return user
|
|
291
|
+
|
|
292
|
+
async def authenticate(
|
|
293
|
+
self,
|
|
294
|
+
db: AsyncSession,
|
|
295
|
+
email: str,
|
|
296
|
+
password: str
|
|
297
|
+
) -> Optional[User]:
|
|
298
|
+
"""Authenticate user."""
|
|
299
|
+
user = await self.repository.get_by_email(db, email)
|
|
300
|
+
if not user:
|
|
301
|
+
return None
|
|
302
|
+
if not verify_password(password, user.hashed_password):
|
|
303
|
+
return None
|
|
304
|
+
return user
|
|
305
|
+
|
|
306
|
+
async def update_user(
|
|
307
|
+
self,
|
|
308
|
+
db: AsyncSession,
|
|
309
|
+
user_id: int,
|
|
310
|
+
user_in: UserUpdate
|
|
311
|
+
) -> Optional[User]:
|
|
312
|
+
"""Update user."""
|
|
313
|
+
user = await self.repository.get(db, user_id)
|
|
314
|
+
if not user:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
if user_in.password:
|
|
318
|
+
user_in_dict = user_in.dict(exclude_unset=True)
|
|
319
|
+
user_in_dict["hashed_password"] = get_password_hash(
|
|
320
|
+
user_in_dict.pop("password")
|
|
321
|
+
)
|
|
322
|
+
user_in = UserUpdate(**user_in_dict)
|
|
323
|
+
|
|
324
|
+
return await self.repository.update(db, user, user_in)
|
|
325
|
+
|
|
326
|
+
user_service = UserService()
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Pattern 4: API Endpoints with Dependencies
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
# api/v1/endpoints/users.py
|
|
333
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
334
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
335
|
+
from typing import List
|
|
336
|
+
|
|
337
|
+
from app.core.database import get_db
|
|
338
|
+
from app.schemas.user import User, UserCreate, UserUpdate
|
|
339
|
+
from app.services.user_service import user_service
|
|
340
|
+
from app.api.dependencies import get_current_user
|
|
341
|
+
|
|
342
|
+
router = APIRouter()
|
|
343
|
+
|
|
344
|
+
@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
|
|
345
|
+
async def create_user(
|
|
346
|
+
user_in: UserCreate,
|
|
347
|
+
db: AsyncSession = Depends(get_db)
|
|
348
|
+
):
|
|
349
|
+
"""Create new user."""
|
|
350
|
+
try:
|
|
351
|
+
user = await user_service.create_user(db, user_in)
|
|
352
|
+
return user
|
|
353
|
+
except ValueError as e:
|
|
354
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
355
|
+
|
|
356
|
+
@router.get("/me", response_model=User)
|
|
357
|
+
async def read_current_user(
|
|
358
|
+
current_user: User = Depends(get_current_user)
|
|
359
|
+
):
|
|
360
|
+
"""Get current user."""
|
|
361
|
+
return current_user
|
|
362
|
+
|
|
363
|
+
@router.get("/{user_id}", response_model=User)
|
|
364
|
+
async def read_user(
|
|
365
|
+
user_id: int,
|
|
366
|
+
db: AsyncSession = Depends(get_db),
|
|
367
|
+
current_user: User = Depends(get_current_user)
|
|
368
|
+
):
|
|
369
|
+
"""Get user by ID."""
|
|
370
|
+
user = await user_service.repository.get(db, user_id)
|
|
371
|
+
if not user:
|
|
372
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
373
|
+
return user
|
|
374
|
+
|
|
375
|
+
@router.patch("/{user_id}", response_model=User)
|
|
376
|
+
async def update_user(
|
|
377
|
+
user_id: int,
|
|
378
|
+
user_in: UserUpdate,
|
|
379
|
+
db: AsyncSession = Depends(get_db),
|
|
380
|
+
current_user: User = Depends(get_current_user)
|
|
381
|
+
):
|
|
382
|
+
"""Update user."""
|
|
383
|
+
if current_user.id != user_id:
|
|
384
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
385
|
+
|
|
386
|
+
user = await user_service.update_user(db, user_id, user_in)
|
|
387
|
+
if not user:
|
|
388
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
389
|
+
return user
|
|
390
|
+
|
|
391
|
+
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
392
|
+
async def delete_user(
|
|
393
|
+
user_id: int,
|
|
394
|
+
db: AsyncSession = Depends(get_db),
|
|
395
|
+
current_user: User = Depends(get_current_user)
|
|
396
|
+
):
|
|
397
|
+
"""Delete user."""
|
|
398
|
+
if current_user.id != user_id:
|
|
399
|
+
raise HTTPException(status_code=403, detail="Not authorized")
|
|
400
|
+
|
|
401
|
+
deleted = await user_service.repository.delete(db, user_id)
|
|
402
|
+
if not deleted:
|
|
403
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Pattern 5: Authentication & Authorization
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
# core/security.py
|
|
410
|
+
from datetime import datetime, timedelta
|
|
411
|
+
from typing import Optional
|
|
412
|
+
from jose import JWTError, jwt
|
|
413
|
+
from passlib.context import CryptContext
|
|
414
|
+
from app.core.config import get_settings
|
|
415
|
+
|
|
416
|
+
settings = get_settings()
|
|
417
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
418
|
+
|
|
419
|
+
ALGORITHM = "HS256"
|
|
420
|
+
|
|
421
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
422
|
+
"""Create JWT access token."""
|
|
423
|
+
to_encode = data.copy()
|
|
424
|
+
if expires_delta:
|
|
425
|
+
expire = datetime.utcnow() + expires_delta
|
|
426
|
+
else:
|
|
427
|
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
|
428
|
+
to_encode.update({"exp": expire})
|
|
429
|
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
|
430
|
+
return encoded_jwt
|
|
431
|
+
|
|
432
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
433
|
+
"""Verify password against hash."""
|
|
434
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
435
|
+
|
|
436
|
+
def get_password_hash(password: str) -> str:
|
|
437
|
+
"""Hash password."""
|
|
438
|
+
return pwd_context.hash(password)
|
|
439
|
+
|
|
440
|
+
# api/dependencies.py
|
|
441
|
+
from fastapi import Depends, HTTPException, status
|
|
442
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
443
|
+
from jose import JWTError, jwt
|
|
444
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
445
|
+
|
|
446
|
+
from app.core.database import get_db
|
|
447
|
+
from app.core.security import ALGORITHM
|
|
448
|
+
from app.core.config import get_settings
|
|
449
|
+
from app.repositories.user_repository import user_repository
|
|
450
|
+
|
|
451
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
|
|
452
|
+
|
|
453
|
+
async def get_current_user(
|
|
454
|
+
db: AsyncSession = Depends(get_db),
|
|
455
|
+
token: str = Depends(oauth2_scheme)
|
|
456
|
+
):
|
|
457
|
+
"""Get current authenticated user."""
|
|
458
|
+
credentials_exception = HTTPException(
|
|
459
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
460
|
+
detail="Could not validate credentials",
|
|
461
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
|
466
|
+
user_id: int = payload.get("sub")
|
|
467
|
+
if user_id is None:
|
|
468
|
+
raise credentials_exception
|
|
469
|
+
except JWTError:
|
|
470
|
+
raise credentials_exception
|
|
471
|
+
|
|
472
|
+
user = await user_repository.get(db, user_id)
|
|
473
|
+
if user is None:
|
|
474
|
+
raise credentials_exception
|
|
475
|
+
|
|
476
|
+
return user
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Testing
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
# tests/conftest.py
|
|
483
|
+
import pytest
|
|
484
|
+
import asyncio
|
|
485
|
+
from httpx import AsyncClient
|
|
486
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
487
|
+
from sqlalchemy.orm import sessionmaker
|
|
488
|
+
|
|
489
|
+
from app.main import app
|
|
490
|
+
from app.core.database import get_db, Base
|
|
491
|
+
|
|
492
|
+
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
|
493
|
+
|
|
494
|
+
@pytest.fixture(scope="session")
|
|
495
|
+
def event_loop():
|
|
496
|
+
loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
497
|
+
yield loop
|
|
498
|
+
loop.close()
|
|
499
|
+
|
|
500
|
+
@pytest.fixture
|
|
501
|
+
async def db_session():
|
|
502
|
+
engine = create_async_engine(TEST_DATABASE_URL, echo=True)
|
|
503
|
+
async with engine.begin() as conn:
|
|
504
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
505
|
+
|
|
506
|
+
AsyncSessionLocal = sessionmaker(
|
|
507
|
+
engine, class_=AsyncSession, expire_on_commit=False
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
async with AsyncSessionLocal() as session:
|
|
511
|
+
yield session
|
|
512
|
+
|
|
513
|
+
@pytest.fixture
|
|
514
|
+
async def client(db_session):
|
|
515
|
+
async def override_get_db():
|
|
516
|
+
yield db_session
|
|
517
|
+
|
|
518
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
519
|
+
|
|
520
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
521
|
+
yield client
|
|
522
|
+
|
|
523
|
+
# tests/test_users.py
|
|
524
|
+
import pytest
|
|
525
|
+
|
|
526
|
+
@pytest.mark.asyncio
|
|
527
|
+
async def test_create_user(client):
|
|
528
|
+
response = await client.post(
|
|
529
|
+
"/api/v1/users/",
|
|
530
|
+
json={
|
|
531
|
+
"email": "test@example.com",
|
|
532
|
+
"password": "testpass123",
|
|
533
|
+
"name": "Test User"
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
assert response.status_code == 201
|
|
537
|
+
data = response.json()
|
|
538
|
+
assert data["email"] == "test@example.com"
|
|
539
|
+
assert "id" in data
|
|
540
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit
|
|
3
|
+
description: 'Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping'
|
|
4
|
+
license: MIT
|
|
5
|
+
allowed-tools: Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Git Commit with Conventional Commits
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Create standardized, semantic git commits using the Conventional Commits specification. Analyze the actual diff to determine appropriate type, scope, and message.
|
|
13
|
+
|
|
14
|
+
## Conventional Commit Format
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<type>[optional scope]: <description>
|
|
18
|
+
|
|
19
|
+
[optional body]
|
|
20
|
+
|
|
21
|
+
[optional footer(s)]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commit Types
|
|
25
|
+
|
|
26
|
+
| Type | Purpose |
|
|
27
|
+
| ---------- | ------------------------------ |
|
|
28
|
+
| `feat` | New feature |
|
|
29
|
+
| `fix` | Bug fix |
|
|
30
|
+
| `docs` | Documentation only |
|
|
31
|
+
| `style` | Formatting/style (no logic) |
|
|
32
|
+
| `refactor` | Code refactor (no feature/fix) |
|
|
33
|
+
| `perf` | Performance improvement |
|
|
34
|
+
| `test` | Add/update tests |
|
|
35
|
+
| `build` | Build system/dependencies |
|
|
36
|
+
| `ci` | CI/config changes |
|
|
37
|
+
| `chore` | Maintenance/misc |
|
|
38
|
+
| `revert` | Revert commit |
|
|
39
|
+
|
|
40
|
+
## Breaking Changes
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
# Exclamation mark after type/scope
|
|
44
|
+
feat!: remove deprecated endpoint
|
|
45
|
+
|
|
46
|
+
# BREAKING CHANGE footer
|
|
47
|
+
feat: allow config to extend other configs
|
|
48
|
+
|
|
49
|
+
BREAKING CHANGE: `extends` key behavior changed
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Workflow
|
|
53
|
+
|
|
54
|
+
### 1. Analyze Diff
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# If files are staged, use staged diff
|
|
58
|
+
git diff --staged
|
|
59
|
+
|
|
60
|
+
# If nothing staged, use working tree diff
|
|
61
|
+
git diff
|
|
62
|
+
|
|
63
|
+
# Also check status
|
|
64
|
+
git status --porcelain
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Stage Files (if needed)
|
|
68
|
+
|
|
69
|
+
If nothing is staged or you want to group changes differently:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Stage specific files
|
|
73
|
+
git add path/to/file1 path/to/file2
|
|
74
|
+
|
|
75
|
+
# Stage by pattern
|
|
76
|
+
git add *.test.*
|
|
77
|
+
git add src/components/*
|
|
78
|
+
|
|
79
|
+
# Interactive staging
|
|
80
|
+
git add -p
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Never commit secrets** (.env, credentials.json, private keys).
|
|
84
|
+
|
|
85
|
+
### 3. Generate Commit Message
|
|
86
|
+
|
|
87
|
+
Analyze the diff to determine:
|
|
88
|
+
|
|
89
|
+
- **Type**: What kind of change is this?
|
|
90
|
+
- **Scope**: What area/module is affected?
|
|
91
|
+
- **Description**: One-line summary of what changed (present tense, imperative mood, <72 chars)
|
|
92
|
+
|
|
93
|
+
### 4. Execute Commit
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Single line
|
|
97
|
+
git commit -m "<type>[scope]: <description>"
|
|
98
|
+
|
|
99
|
+
# Multi-line with body/footer
|
|
100
|
+
git commit -m "$(cat <<'EOF'
|
|
101
|
+
<type>[scope]: <description>
|
|
102
|
+
|
|
103
|
+
<optional body>
|
|
104
|
+
|
|
105
|
+
<optional footer>
|
|
106
|
+
EOF
|
|
107
|
+
)"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Best Practices
|
|
111
|
+
|
|
112
|
+
- One logical change per commit
|
|
113
|
+
- Present tense: "add" not "added"
|
|
114
|
+
- Imperative mood: "fix bug" not "fixes bug"
|
|
115
|
+
- Reference issues: `Closes #123`, `Refs #456`
|
|
116
|
+
- Keep description under 72 characters
|
|
117
|
+
|
|
118
|
+
## Git Safety Protocol
|
|
119
|
+
|
|
120
|
+
- NEVER update git config
|
|
121
|
+
- NEVER run destructive commands (--force, hard reset) without explicit request
|
|
122
|
+
- NEVER skip hooks (--no-verify) unless user asks
|
|
123
|
+
- NEVER force push to main/master
|
|
124
|
+
- If commit fails due to hooks, fix and create NEW commit (don't amend)
|
package/bin/cli.js
CHANGED
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AXIOM Coding Agent Setup CLI
|
|
5
|
-
*
|
|
6
|
-
* Downloads AGENTS.md and .agents/ folder (core docs, templates, skills)
|
|
7
|
-
* from the GitHub repository into the current project directory.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const https = require('https');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
const REPO_OWNER = 'mcikalmerdeka';
|
|
15
|
-
const REPO_NAME = 'axiom-coding-agent-setup';
|
|
16
|
-
const BRANCH = 'main';
|
|
17
|
-
|
|
18
|
-
const FILES_TO_DOWNLOAD = [
|
|
19
|
-
// Main instructions
|
|
20
|
-
'AGENTS.md',
|
|
21
|
-
// Core agent documents
|
|
22
|
-
'.agents/engineering.md',
|
|
23
|
-
'.agents/stack.md',
|
|
24
|
-
'.agents/workflow.md',
|
|
25
|
-
// Templates (project-type conventions)
|
|
26
|
-
'.agents/templates/ai-engineering-python.md',
|
|
27
|
-
'.agents/templates/fullstack-ai-nextjs.md',
|
|
28
|
-
// Skills (domain-specific guides)
|
|
29
|
-
'.agents/skills/mcp-builder/SKILL.md',
|
|
30
|
-
'.agents/skills/n8n-patterns/SKILL.md',
|
|
31
|
-
'.agents/skills/ai-integration/SKILL.md',
|
|
32
|
-
'.agents/skills/deployment-patterns/SKILL.md'
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
36
|
-
|
|
37
|
-
// ANSI color codes for terminal output
|
|
38
|
-
const colors = {
|
|
39
|
-
reset: '\x1b[0m',
|
|
40
|
-
green: '\x1b[32m',
|
|
41
|
-
yellow: '\x1b[33m',
|
|
42
|
-
red: '\x1b[31m',
|
|
43
|
-
cyan: '\x1b[36m',
|
|
44
|
-
bold: '\x1b[1m'
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function log(message, color = 'reset') {
|
|
48
|
-
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function downloadFile(filePath) {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const url = `${GITHUB_RAW_URL}/${filePath}`;
|
|
54
|
-
const localPath = path.join(process.cwd(), filePath);
|
|
55
|
-
const dir = path.dirname(localPath);
|
|
56
|
-
|
|
57
|
-
// Create directory if it doesn't exist
|
|
58
|
-
if (!fs.existsSync(dir)) {
|
|
59
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const file = fs.createWriteStream(localPath);
|
|
63
|
-
|
|
64
|
-
https.get(url, (response) => {
|
|
65
|
-
if (response.statusCode === 200) {
|
|
66
|
-
response.pipe(file);
|
|
67
|
-
file.on('finish', () => {
|
|
68
|
-
file.close();
|
|
69
|
-
resolve(filePath);
|
|
70
|
-
});
|
|
71
|
-
} else if (response.statusCode === 301 || response.statusCode === 302) {
|
|
72
|
-
// Handle redirects
|
|
73
|
-
https.get(response.headers.location, (redirectResponse) => {
|
|
74
|
-
if (redirectResponse.statusCode === 200) {
|
|
75
|
-
redirectResponse.pipe(file);
|
|
76
|
-
file.on('finish', () => {
|
|
77
|
-
file.close();
|
|
78
|
-
resolve(filePath);
|
|
79
|
-
});
|
|
80
|
-
} else {
|
|
81
|
-
fs.unlinkSync(localPath);
|
|
82
|
-
reject(new Error(`Failed to download ${filePath}: ${redirectResponse.statusCode}`));
|
|
83
|
-
}
|
|
84
|
-
}).on('error', reject);
|
|
85
|
-
} else {
|
|
86
|
-
file.close();
|
|
87
|
-
fs.unlinkSync(localPath);
|
|
88
|
-
reject(new Error(`Failed to download ${filePath}: ${response.statusCode}`));
|
|
89
|
-
}
|
|
90
|
-
}).on('error', (err) => {
|
|
91
|
-
fs.unlinkSync(localPath);
|
|
92
|
-
reject(err);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function main() {
|
|
98
|
-
log('\n' + '='.repeat(60), 'cyan');
|
|
99
|
-
log('AXIOM Coding Agent Setup', 'bold');
|
|
100
|
-
log('='.repeat(60) + '\n', 'cyan');
|
|
101
|
-
|
|
102
|
-
log(`Target directory: ${process.cwd()}\n`, 'yellow');
|
|
103
|
-
|
|
104
|
-
let successCount = 0;
|
|
105
|
-
let failCount = 0;
|
|
106
|
-
|
|
107
|
-
for (const file of FILES_TO_DOWNLOAD) {
|
|
108
|
-
try {
|
|
109
|
-
process.stdout.write(`Downloading ${file}... `);
|
|
110
|
-
await downloadFile(file);
|
|
111
|
-
log('✓', 'green');
|
|
112
|
-
successCount++;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
log(`✗ (${error.message})`, 'red');
|
|
115
|
-
failCount++;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
log('\n' + '='.repeat(60), 'cyan');
|
|
120
|
-
log(`Setup complete! ${successCount} files downloaded, ${failCount} failed.`, successCount > 0 ? 'green' : 'red');
|
|
121
|
-
log('='.repeat(60) + '\n', 'cyan');
|
|
122
|
-
|
|
123
|
-
if (successCount > 0) {
|
|
124
|
-
log('Your project now has AXIOM coding agent instructions:', 'bold');
|
|
125
|
-
log(' - AGENTS.md → Main agent instructions', 'cyan');
|
|
126
|
-
log(' - .agents/engineering.md → Engineering principles', 'cyan');
|
|
127
|
-
log(' - .agents/stack.md → Tech stack knowledge', 'cyan');
|
|
128
|
-
log(' - .agents/workflow.md → Workflow guidelines', 'cyan');
|
|
129
|
-
log(' - .agents/templates/ → Project-type conventions', 'cyan');
|
|
130
|
-
log(' - .agents/skills/ → Domain-specific skills\n', 'cyan');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
process.exit(failCount > 0 ? 1 : 0);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
main().catch((error) => {
|
|
137
|
-
log(`\nUnexpected error: ${error.message}`, 'red');
|
|
138
|
-
process.exit(1);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AXIOM Coding Agent Setup CLI
|
|
5
|
+
*
|
|
6
|
+
* Downloads AGENTS.md and .agents/ folder (core docs, templates, skills)
|
|
7
|
+
* from the GitHub repository into the current project directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const https = require('https');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const REPO_OWNER = 'mcikalmerdeka';
|
|
15
|
+
const REPO_NAME = 'axiom-coding-agent-setup';
|
|
16
|
+
const BRANCH = 'main';
|
|
17
|
+
|
|
18
|
+
const FILES_TO_DOWNLOAD = [
|
|
19
|
+
// Main instructions
|
|
20
|
+
'AGENTS.md',
|
|
21
|
+
// Core agent documents
|
|
22
|
+
'.agents/engineering.md',
|
|
23
|
+
'.agents/stack.md',
|
|
24
|
+
'.agents/workflow.md',
|
|
25
|
+
// Templates (project-type conventions)
|
|
26
|
+
'.agents/templates/ai-engineering-python.md',
|
|
27
|
+
'.agents/templates/fullstack-ai-nextjs.md',
|
|
28
|
+
// Skills (domain-specific guides)
|
|
29
|
+
'.agents/skills/mcp-builder/SKILL.md',
|
|
30
|
+
'.agents/skills/n8n-patterns/SKILL.md',
|
|
31
|
+
'.agents/skills/ai-integration/SKILL.md',
|
|
32
|
+
'.agents/skills/deployment-patterns/SKILL.md'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
36
|
+
|
|
37
|
+
// ANSI color codes for terminal output
|
|
38
|
+
const colors = {
|
|
39
|
+
reset: '\x1b[0m',
|
|
40
|
+
green: '\x1b[32m',
|
|
41
|
+
yellow: '\x1b[33m',
|
|
42
|
+
red: '\x1b[31m',
|
|
43
|
+
cyan: '\x1b[36m',
|
|
44
|
+
bold: '\x1b[1m'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function log(message, color = 'reset') {
|
|
48
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function downloadFile(filePath) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const url = `${GITHUB_RAW_URL}/${filePath}`;
|
|
54
|
+
const localPath = path.join(process.cwd(), filePath);
|
|
55
|
+
const dir = path.dirname(localPath);
|
|
56
|
+
|
|
57
|
+
// Create directory if it doesn't exist
|
|
58
|
+
if (!fs.existsSync(dir)) {
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const file = fs.createWriteStream(localPath);
|
|
63
|
+
|
|
64
|
+
https.get(url, (response) => {
|
|
65
|
+
if (response.statusCode === 200) {
|
|
66
|
+
response.pipe(file);
|
|
67
|
+
file.on('finish', () => {
|
|
68
|
+
file.close();
|
|
69
|
+
resolve(filePath);
|
|
70
|
+
});
|
|
71
|
+
} else if (response.statusCode === 301 || response.statusCode === 302) {
|
|
72
|
+
// Handle redirects
|
|
73
|
+
https.get(response.headers.location, (redirectResponse) => {
|
|
74
|
+
if (redirectResponse.statusCode === 200) {
|
|
75
|
+
redirectResponse.pipe(file);
|
|
76
|
+
file.on('finish', () => {
|
|
77
|
+
file.close();
|
|
78
|
+
resolve(filePath);
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
fs.unlinkSync(localPath);
|
|
82
|
+
reject(new Error(`Failed to download ${filePath}: ${redirectResponse.statusCode}`));
|
|
83
|
+
}
|
|
84
|
+
}).on('error', reject);
|
|
85
|
+
} else {
|
|
86
|
+
file.close();
|
|
87
|
+
fs.unlinkSync(localPath);
|
|
88
|
+
reject(new Error(`Failed to download ${filePath}: ${response.statusCode}`));
|
|
89
|
+
}
|
|
90
|
+
}).on('error', (err) => {
|
|
91
|
+
fs.unlinkSync(localPath);
|
|
92
|
+
reject(err);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
log('\n' + '='.repeat(60), 'cyan');
|
|
99
|
+
log('AXIOM Coding Agent Setup', 'bold');
|
|
100
|
+
log('='.repeat(60) + '\n', 'cyan');
|
|
101
|
+
|
|
102
|
+
log(`Target directory: ${process.cwd()}\n`, 'yellow');
|
|
103
|
+
|
|
104
|
+
let successCount = 0;
|
|
105
|
+
let failCount = 0;
|
|
106
|
+
|
|
107
|
+
for (const file of FILES_TO_DOWNLOAD) {
|
|
108
|
+
try {
|
|
109
|
+
process.stdout.write(`Downloading ${file}... `);
|
|
110
|
+
await downloadFile(file);
|
|
111
|
+
log('✓', 'green');
|
|
112
|
+
successCount++;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
log(`✗ (${error.message})`, 'red');
|
|
115
|
+
failCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log('\n' + '='.repeat(60), 'cyan');
|
|
120
|
+
log(`Setup complete! ${successCount} files downloaded, ${failCount} failed.`, successCount > 0 ? 'green' : 'red');
|
|
121
|
+
log('='.repeat(60) + '\n', 'cyan');
|
|
122
|
+
|
|
123
|
+
if (successCount > 0) {
|
|
124
|
+
log('Your project now has AXIOM coding agent instructions:', 'bold');
|
|
125
|
+
log(' - AGENTS.md → Main agent instructions', 'cyan');
|
|
126
|
+
log(' - .agents/engineering.md → Engineering principles', 'cyan');
|
|
127
|
+
log(' - .agents/stack.md → Tech stack knowledge', 'cyan');
|
|
128
|
+
log(' - .agents/workflow.md → Workflow guidelines', 'cyan');
|
|
129
|
+
log(' - .agents/templates/ → Project-type conventions', 'cyan');
|
|
130
|
+
log(' - .agents/skills/ → Domain-specific skills\n', 'cyan');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.exit(failCount > 0 ? 1 : 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch((error) => {
|
|
137
|
+
log(`\nUnexpected error: ${error.message}`, 'red');
|
|
138
|
+
process.exit(1);
|
|
139
139
|
});
|
package/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "axiom-coding-agent-setup",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "CLI tool to download AXIOM coding agent setup files into your project",
|
|
5
|
-
"main": "bin/cli.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"axiom-coding-agent-setup": "bin/cli.js",
|
|
8
|
-
"axiom-setup": "bin/cli.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"axiom",
|
|
15
|
-
"coding-agent",
|
|
16
|
-
"setup",
|
|
17
|
-
"claude",
|
|
18
|
-
"ai-agent"
|
|
19
|
-
],
|
|
20
|
-
"author": "mcikalmerdeka",
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "https://github.com/mcikalmerdeka/axiom-coding-agent-setup.git"
|
|
25
|
-
},
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=14.0.0"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "axiom-coding-agent-setup",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "CLI tool to download AXIOM coding agent setup files into your project",
|
|
5
|
+
"main": "bin/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"axiom-coding-agent-setup": "bin/cli.js",
|
|
8
|
+
"axiom-setup": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"axiom",
|
|
15
|
+
"coding-agent",
|
|
16
|
+
"setup",
|
|
17
|
+
"claude",
|
|
18
|
+
"ai-agent"
|
|
19
|
+
],
|
|
20
|
+
"author": "mcikalmerdeka",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/mcikalmerdeka/axiom-coding-agent-setup.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/skills-lock.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"skills": {
|
|
4
|
+
"fastapi-templates": {
|
|
5
|
+
"source": "wshobson/agents",
|
|
6
|
+
"sourceType": "github",
|
|
7
|
+
"computedHash": "1a5fe66bd2683afd1db9afdf37a25cd8b9195c369c98489333badf0406fc91b3"
|
|
8
|
+
},
|
|
9
|
+
"git-commit": {
|
|
10
|
+
"source": "github/awesome-copilot",
|
|
11
|
+
"sourceType": "github",
|
|
12
|
+
"computedHash": "2607fc60629b82b257136dd2a7a373f0a4466c0b49df7746d845d59313c99b21"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
# npx CLI Package Setup Reference
|
|
2
|
-
|
|
3
|
-
Quick reference for creating and publishing an npx CLI package that downloads files from a GitHub repository.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
This pattern creates a CLI tool that can be run via `npx your-package-name` to download specific files from a GitHub repo into the current project directory.
|
|
10
|
-
|
|
11
|
-
**Use case**: Distributing coding agent instructions, project templates, config files, etc.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Step-by-Step Setup
|
|
16
|
-
|
|
17
|
-
### 1. Create Project Structure
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
your-project/
|
|
21
|
-
├── package.json # npm package configuration
|
|
22
|
-
├── bin/
|
|
23
|
-
│ └── cli.js # CLI entry point (Node.js script)
|
|
24
|
-
└── README.md # Documentation
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 2. Configure package.json
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
{
|
|
31
|
-
"name": "your-package-name",
|
|
32
|
-
"version": "1.0.0",
|
|
33
|
-
"description": "Description of what this CLI does",
|
|
34
|
-
"main": "bin/cli.js",
|
|
35
|
-
"bin": {
|
|
36
|
-
"your-package-name": "bin/cli.js",
|
|
37
|
-
"short-alias": "bin/cli.js"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
41
|
-
},
|
|
42
|
-
"keywords": ["cli", "setup", "scaffold"],
|
|
43
|
-
"author": "your-github-username",
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "https://github.com/username/repo-name.git"
|
|
48
|
-
},
|
|
49
|
-
"engines": {
|
|
50
|
-
"node": ">=14.0.0"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Key fields:**
|
|
56
|
-
- `name`: Must be unique on npm (check availability first)
|
|
57
|
-
- `bin`: Maps command names to the CLI script
|
|
58
|
-
- `repository`: Links to your GitHub repo
|
|
59
|
-
|
|
60
|
-
### 3. Create the CLI Script (bin/cli.js)
|
|
61
|
-
|
|
62
|
-
Template for downloading files from GitHub raw content:
|
|
63
|
-
|
|
64
|
-
```javascript
|
|
65
|
-
#!/usr/bin/env node
|
|
66
|
-
|
|
67
|
-
const https = require('https');
|
|
68
|
-
const fs = require('fs');
|
|
69
|
-
const path = require('path');
|
|
70
|
-
|
|
71
|
-
const REPO_OWNER = 'your-github-username';
|
|
72
|
-
const REPO_NAME = 'your-repo-name';
|
|
73
|
-
const BRANCH = 'main';
|
|
74
|
-
|
|
75
|
-
const FILES_TO_DOWNLOAD = [
|
|
76
|
-
'file1.md',
|
|
77
|
-
'folder/file2.md',
|
|
78
|
-
'folder/file3.md'
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${BRANCH}`;
|
|
82
|
-
|
|
83
|
-
function downloadFile(filePath) {
|
|
84
|
-
return new Promise((resolve, reject) => {
|
|
85
|
-
const url = `${GITHUB_RAW_URL}/${filePath}`;
|
|
86
|
-
const localPath = path.join(process.cwd(), filePath);
|
|
87
|
-
const dir = path.dirname(localPath);
|
|
88
|
-
|
|
89
|
-
// Create directory if it doesn't exist
|
|
90
|
-
if (!fs.existsSync(dir)) {
|
|
91
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const file = fs.createWriteStream(localPath);
|
|
95
|
-
|
|
96
|
-
https.get(url, (response) => {
|
|
97
|
-
if (response.statusCode === 200) {
|
|
98
|
-
response.pipe(file);
|
|
99
|
-
file.on('finish', () => {
|
|
100
|
-
file.close();
|
|
101
|
-
resolve(filePath);
|
|
102
|
-
});
|
|
103
|
-
} else {
|
|
104
|
-
file.close();
|
|
105
|
-
if (fs.existsSync(localPath)) fs.unlinkSync(localPath);
|
|
106
|
-
reject(new Error(`Failed to download ${filePath}: ${response.statusCode}`));
|
|
107
|
-
}
|
|
108
|
-
}).on('error', (err) => {
|
|
109
|
-
if (fs.existsSync(localPath)) fs.unlinkSync(localPath);
|
|
110
|
-
reject(err);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function main() {
|
|
116
|
-
console.log('Starting download...\n');
|
|
117
|
-
|
|
118
|
-
for (const file of FILES_TO_DOWNLOAD) {
|
|
119
|
-
try {
|
|
120
|
-
process.stdout.write(`Downloading ${file}... `);
|
|
121
|
-
await downloadFile(file);
|
|
122
|
-
console.log('✓');
|
|
123
|
-
} catch (error) {
|
|
124
|
-
console.log(`✗ (${error.message})`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
console.log('\nDone!');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
main().catch(console.error);
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### 4. Test Locally
|
|
135
|
-
|
|
136
|
-
Before publishing, test the CLI locally:
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
node bin/cli.js
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Or link it locally:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
npm link
|
|
146
|
-
your-package-name # Test the command
|
|
147
|
-
npm unlink # Remove local link when done
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### 5. Publish to npm
|
|
151
|
-
|
|
152
|
-
#### First-time setup:
|
|
153
|
-
|
|
154
|
-
1. **Create npm account**: https://www.npmjs.com/signup
|
|
155
|
-
|
|
156
|
-
2. **Login from terminal**:
|
|
157
|
-
```bash
|
|
158
|
-
npm login
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
3. **Publish**:
|
|
162
|
-
```bash
|
|
163
|
-
npm publish
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
#### Check if published successfully:
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
npm view your-package-name
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Or visit: `https://www.npmjs.com/package/your-package-name`
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Usage After Publishing
|
|
177
|
-
|
|
178
|
-
Anyone can now use your CLI (no installation required):
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
npx your-package-name
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Or install globally:
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
npm install -g your-package-name
|
|
188
|
-
your-package-name # Run directly
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
---
|
|
192
|
-
|
|
193
|
-
## Updating Your Package
|
|
194
|
-
|
|
195
|
-
### Updating Content Files (the downloaded files)
|
|
196
|
-
|
|
197
|
-
Since files are downloaded directly from GitHub:
|
|
198
|
-
|
|
199
|
-
1. Edit files locally
|
|
200
|
-
2. Commit and push:
|
|
201
|
-
```bash
|
|
202
|
-
git add .
|
|
203
|
-
git commit -m "Update instructions"
|
|
204
|
-
git push origin main
|
|
205
|
-
```
|
|
206
|
-
3. Done! Changes are live immediately (no npm publish needed)
|
|
207
|
-
|
|
208
|
-
### Updating the CLI Script Itself
|
|
209
|
-
|
|
210
|
-
If you modify `bin/cli.js` or `package.json`:
|
|
211
|
-
|
|
212
|
-
1. Update version in `package.json`:
|
|
213
|
-
```json
|
|
214
|
-
"version": "1.0.1" // Increment version
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
2. Commit and push to GitHub:
|
|
218
|
-
```bash
|
|
219
|
-
git add .
|
|
220
|
-
git commit -m "Fix download logic"
|
|
221
|
-
git push origin main
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
3. Republish to npm:
|
|
225
|
-
```bash
|
|
226
|
-
npm publish
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
---
|
|
230
|
-
|
|
231
|
-
## Version Management
|
|
232
|
-
|
|
233
|
-
npm follows semantic versioning:
|
|
234
|
-
|
|
235
|
-
| Version change | When to use |
|
|
236
|
-
|----------------|-------------|
|
|
237
|
-
| `1.0.0` → `1.0.1` | Bug fixes (patch) |
|
|
238
|
-
| `1.0.0` → `1.1.0` | New features (minor) |
|
|
239
|
-
| `1.0.0` → `2.0.0` | Breaking changes (major) |
|
|
240
|
-
|
|
241
|
-
Update manually in `package.json` before republishing.
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Troubleshooting
|
|
246
|
-
|
|
247
|
-
### 403 Forbidden when publishing
|
|
248
|
-
- Package name might already exist on npm
|
|
249
|
-
- Try a more unique name (e.g., `@username/package-name` for scoped packages)
|
|
250
|
-
|
|
251
|
-
### 404 when downloading files
|
|
252
|
-
- Check GitHub repo is public
|
|
253
|
-
- Verify `REPO_OWNER` and `REPO_NAME` match your GitHub URL
|
|
254
|
-
- Ensure files exist on the `main` branch
|
|
255
|
-
|
|
256
|
-
### Command not found after publishing
|
|
257
|
-
- Wait a few minutes for npm registry to propagate
|
|
258
|
-
- Try `npx your-package-name@latest`
|
|
259
|
-
|
|
260
|
-
### Local changes not reflected
|
|
261
|
-
- Clear npx cache: `npx clear-npx-cache`
|
|
262
|
-
- Or specify version: `npx your-package-name@1.0.1`
|
|
263
|
-
|
|
264
|
-
---
|
|
265
|
-
|
|
266
|
-
## Scoped Packages (Optional)
|
|
267
|
-
|
|
268
|
-
If the package name is taken, use your username as a scope:
|
|
269
|
-
|
|
270
|
-
```json
|
|
271
|
-
{
|
|
272
|
-
"name": "@username/package-name"
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Publish with:
|
|
277
|
-
```bash
|
|
278
|
-
npm publish --access public
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Use with:
|
|
282
|
-
```bash
|
|
283
|
-
npx @username/package-name
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
---
|
|
287
|
-
|
|
288
|
-
## Quick Checklist
|
|
289
|
-
|
|
290
|
-
- [ ] Create `package.json` with `bin` entry
|
|
291
|
-
- [ ] Create `bin/cli.js` with shebang (`#!/usr/bin/env node`)
|
|
292
|
-
- [ ] Set correct `REPO_OWNER`, `REPO_NAME`, and `FILES_TO_DOWNLOAD`
|
|
293
|
-
- [ ] Test locally with `node bin/cli.js`
|
|
294
|
-
- [ ] Create npm account
|
|
295
|
-
- [ ] Run `npm login`
|
|
296
|
-
- [ ] Run `npm publish`
|
|
297
|
-
- [ ] Test with `npx your-package-name`
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
## Example Commands Reference
|
|
302
|
-
|
|
303
|
-
```bash
|
|
304
|
-
# Setup
|
|
305
|
-
npm login # Login to npm
|
|
306
|
-
npm publish # Publish package
|
|
307
|
-
npm version patch # Bump version (1.0.0 → 1.0.1)
|
|
308
|
-
|
|
309
|
-
# Maintenance
|
|
310
|
-
npm view your-package-name # Check package info
|
|
311
|
-
npm unpublish your-package-name@version # Remove specific version
|
|
312
|
-
npm deprecate your-package-name@version # Deprecate version
|
|
313
|
-
|
|
314
|
-
# Usage
|
|
315
|
-
npx your-package-name # Run via npx
|
|
316
|
-
npx your-package-name@latest # Force latest version
|
|
317
|
-
npx clear-npx-cache # Clear npx cache
|
|
318
|
-
```
|