@bradtaylorsf/alpha-loop 1.1.0 → 1.1.2
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 +10 -12
- package/dist/cli.js +1 -1
- package/dist/commands/sync.js +9 -1
- package/dist/commands/sync.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/reviewer.md +1 -1
- package/templates/skills/api-contracts/SKILL.md +676 -0
- package/templates/skills/api-patterns/SKILL.md +346 -0
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +293 -0
- package/templates/skills/api-patterns/templates/express-router-template.ts +294 -0
- package/templates/skills/docs-sync/SKILL.md +42 -0
- package/templates/skills/jest-mock-patterns/SKILL.md +397 -0
- package/templates/skills/playwright-testing/SKILL.md +124 -0
- package/templates/skills/sqlite-patterns/SKILL.md +229 -0
- package/templates/skills/test-caching/SKILL.md +99 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sqlite-patterns
|
|
3
|
+
description: SQLite database patterns using better-sqlite3. Synchronous API, file-based storage, great for single-user apps and development.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SQLite Patterns Skill
|
|
7
|
+
|
|
8
|
+
Patterns for SQLite database operations using better-sqlite3.
|
|
9
|
+
|
|
10
|
+
## When to Use SQLite
|
|
11
|
+
|
|
12
|
+
- Single-user applications
|
|
13
|
+
- Development/prototyping
|
|
14
|
+
- File-based storage needs
|
|
15
|
+
- Simple applications without concurrency
|
|
16
|
+
- Embedded databases
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import Database from 'better-sqlite3';
|
|
22
|
+
|
|
23
|
+
// Production
|
|
24
|
+
const db = new Database('./data/app.db');
|
|
25
|
+
|
|
26
|
+
// Testing (in-memory)
|
|
27
|
+
const testDb = new Database(':memory:');
|
|
28
|
+
|
|
29
|
+
// Enable WAL mode for better performance
|
|
30
|
+
db.pragma('journal_mode = WAL');
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Basic Patterns
|
|
34
|
+
|
|
35
|
+
### Query Single Row
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
function getUserById(id: number): User | null {
|
|
39
|
+
const result = db.prepare(`
|
|
40
|
+
SELECT id, email, name, created_at as createdAt
|
|
41
|
+
FROM users
|
|
42
|
+
WHERE id = ?
|
|
43
|
+
`).get(id);
|
|
44
|
+
|
|
45
|
+
return result ?? null; // better-sqlite3 returns undefined, not null
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Query Multiple Rows
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
function getAllUsers(): User[] {
|
|
53
|
+
return db.prepare(`
|
|
54
|
+
SELECT id, email, name, created_at as createdAt
|
|
55
|
+
FROM users
|
|
56
|
+
ORDER BY created_at DESC
|
|
57
|
+
`).all() as User[];
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Insert and Get ID
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
function createUser(data: CreateUserDto): User {
|
|
65
|
+
const stmt = db.prepare(`
|
|
66
|
+
INSERT INTO users (email, password_hash, name)
|
|
67
|
+
VALUES (?, ?, ?)
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
const info = stmt.run(data.email, passwordHash, data.name);
|
|
71
|
+
const id = info.lastInsertRowid;
|
|
72
|
+
|
|
73
|
+
return getUserById(Number(id))!;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Update
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
function updateUser(id: number, updates: UpdateUserDto): User | null {
|
|
81
|
+
const stmt = db.prepare(`
|
|
82
|
+
UPDATE users
|
|
83
|
+
SET email = COALESCE(?, email),
|
|
84
|
+
name = COALESCE(?, name),
|
|
85
|
+
updated_at = datetime('now')
|
|
86
|
+
WHERE id = ?
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
const info = stmt.run(updates.email, updates.name, id);
|
|
90
|
+
|
|
91
|
+
if (info.changes === 0) return null;
|
|
92
|
+
return getUserById(id);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Delete
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
function deleteUser(id: number): boolean {
|
|
100
|
+
const info = db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
|
101
|
+
return info.changes > 0;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Transactions
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
function transferFunds(fromId: number, toId: number, amount: number): void {
|
|
109
|
+
const transfer = db.transaction(() => {
|
|
110
|
+
db.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
|
|
111
|
+
.run(amount, fromId);
|
|
112
|
+
|
|
113
|
+
db.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
|
|
114
|
+
.run(amount, toId);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
transfer(); // Automatically rolls back on error
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## JSON Fields
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Store JSON
|
|
125
|
+
function saveSettings(userId: number, settings: Settings): void {
|
|
126
|
+
db.prepare(`
|
|
127
|
+
UPDATE users SET settings = ? WHERE id = ?
|
|
128
|
+
`).run(JSON.stringify(settings), userId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse JSON
|
|
132
|
+
function getSettings(userId: number): Settings {
|
|
133
|
+
const result = db.prepare(`
|
|
134
|
+
SELECT settings FROM users WHERE id = ?
|
|
135
|
+
`).get(userId) as { settings: string } | undefined;
|
|
136
|
+
|
|
137
|
+
return result ? JSON.parse(result.settings) : {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Helper for safe JSON parsing
|
|
141
|
+
function parseJsonField<T>(value: string | null | undefined, fallback: T): T {
|
|
142
|
+
if (!value) return fallback;
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(value) as T;
|
|
145
|
+
} catch {
|
|
146
|
+
return fallback;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Migrations
|
|
152
|
+
|
|
153
|
+
```sql
|
|
154
|
+
-- migrations/001_create_users.sql
|
|
155
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
156
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
157
|
+
email TEXT UNIQUE NOT NULL,
|
|
158
|
+
password_hash TEXT NOT NULL,
|
|
159
|
+
name TEXT NOT NULL,
|
|
160
|
+
settings TEXT DEFAULT '{}',
|
|
161
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
162
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE INDEX idx_users_email ON users(email);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Run migrations
|
|
170
|
+
function runMigrations(db: Database.Database): void {
|
|
171
|
+
const migrations = fs.readdirSync('./migrations')
|
|
172
|
+
.filter(f => f.endsWith('.sql'))
|
|
173
|
+
.sort();
|
|
174
|
+
|
|
175
|
+
for (const file of migrations) {
|
|
176
|
+
const sql = fs.readFileSync(`./migrations/${file}`, 'utf-8');
|
|
177
|
+
db.exec(sql);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Error Handling
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
function createUser(data: CreateUserDto): User {
|
|
186
|
+
try {
|
|
187
|
+
// ... insert logic
|
|
188
|
+
} catch (error: unknown) {
|
|
189
|
+
if (error instanceof Error && 'code' in error) {
|
|
190
|
+
const sqliteError = error as { code: string };
|
|
191
|
+
if (sqliteError.code === 'SQLITE_CONSTRAINT') {
|
|
192
|
+
throw new AppError('Email already exists', 400);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Timestamps
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// SQLite datetime format
|
|
204
|
+
const now = new Date().toISOString(); // '2024-01-15T10:30:00.000Z'
|
|
205
|
+
|
|
206
|
+
// Or use SQLite's built-in
|
|
207
|
+
db.prepare(`
|
|
208
|
+
INSERT INTO logs (message, created_at)
|
|
209
|
+
VALUES (?, datetime('now'))
|
|
210
|
+
`).run(message);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Best Practices
|
|
214
|
+
|
|
215
|
+
1. **Use prepared statements** - Prevents SQL injection
|
|
216
|
+
2. **Use transactions** - For multiple related operations
|
|
217
|
+
3. **Enable WAL mode** - Better concurrent read performance
|
|
218
|
+
4. **Use in-memory for tests** - Fast, isolated testing
|
|
219
|
+
5. **Handle undefined** - better-sqlite3 returns undefined, not null
|
|
220
|
+
6. **Index foreign keys** - Improve JOIN performance
|
|
221
|
+
7. **Use COALESCE for updates** - Partial updates without overwriting
|
|
222
|
+
|
|
223
|
+
## Common Gotchas
|
|
224
|
+
|
|
225
|
+
- Returns `undefined` not `null` for missing rows
|
|
226
|
+
- `lastInsertRowid` is a BigInt (convert with Number())
|
|
227
|
+
- JSON must be stringified before storage
|
|
228
|
+
- Datetime stored as TEXT (ISO format recommended)
|
|
229
|
+
- No native boolean type (use INTEGER 0/1)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-caching
|
|
3
|
+
description: API response caching for expensive AI API calls in tests. Use when writing tests that call Claude, OpenAI, or other AI APIs.
|
|
4
|
+
auto_load: false
|
|
5
|
+
priority: medium
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Test Caching Skill
|
|
9
|
+
|
|
10
|
+
## Trigger
|
|
11
|
+
When writing tests that make HTTP calls to AI/LLM APIs (Claude, OpenAI, etc.) or any expensive external API.
|
|
12
|
+
|
|
13
|
+
## How It Works
|
|
14
|
+
|
|
15
|
+
The `mockExpensiveAPI()` helper intercepts HTTP requests matching a URL pattern.
|
|
16
|
+
|
|
17
|
+
- **Default mode** (`pnpm test`): Replays responses from fixture files in `tests/fixtures/`. No network calls.
|
|
18
|
+
- **Record mode** (`RECORD_FIXTURES=true` / `pnpm test:full`): Lets real requests through, captures responses to fixture files.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { mockExpensiveAPI } from '../../src/testing/cache';
|
|
24
|
+
|
|
25
|
+
describe('my AI feature', () => {
|
|
26
|
+
let mock: ReturnType<typeof mockExpensiveAPI>;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mock = mockExpensiveAPI({
|
|
30
|
+
name: 'my-feature-openai', // fixture filename
|
|
31
|
+
pattern: 'https://api.openai.com', // URL prefix to intercept
|
|
32
|
+
service: 'openai', // metadata
|
|
33
|
+
estimatedCostUSD: 0.02, // metadata
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
mock.restore(); // always restore HTTP patches
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('calls the AI API', async () => {
|
|
42
|
+
// In replay mode, this returns the cached response
|
|
43
|
+
// In record mode, this hits the real API and saves the response
|
|
44
|
+
const result = await callMyAIFeature();
|
|
45
|
+
expect(result).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
| Option | Required | Description |
|
|
53
|
+
|--------|----------|-------------|
|
|
54
|
+
| `name` | Yes | Unique fixture name (becomes `<name>.fixture.json`) |
|
|
55
|
+
| `pattern` | Yes | URL prefix (string) or RegExp to intercept |
|
|
56
|
+
| `service` | No | Service name for metadata (default: `"unknown"`) |
|
|
57
|
+
| `estimatedCostUSD` | No | Cost per call for metadata (default: `0`) |
|
|
58
|
+
| `fixturesDir` | No | Override fixture directory (default: `tests/fixtures/`) |
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `pnpm test` | Run tests with cached API responses (default) |
|
|
65
|
+
| `pnpm test:full` | Run tests with real API calls, re-record fixtures |
|
|
66
|
+
|
|
67
|
+
## Fixture Format
|
|
68
|
+
|
|
69
|
+
Fixtures are stored as JSON arrays in `tests/fixtures/<name>.fixture.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
[
|
|
73
|
+
{
|
|
74
|
+
"request": { "url": "https://api.openai.com/v1/chat", "method": "POST", "body": { "model": "gpt-4" } },
|
|
75
|
+
"response": { "status": 200, "headers": {}, "body": { "choices": [] } },
|
|
76
|
+
"metadata": { "recordedAt": "2025-01-15T10:00:00.000Z", "service": "openai", "estimatedCostUSD": 0.02 }
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Cache Invalidation
|
|
82
|
+
|
|
83
|
+
Fixtures older than 30 days produce a warning at test startup. Re-record with `RECORD_FIXTURES=true` to refresh.
|
|
84
|
+
|
|
85
|
+
## In the Loop
|
|
86
|
+
|
|
87
|
+
`scripts/loop.sh` uses cached mode by default. Pass `--run-full` to bypass cache:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bash scripts/loop.sh --run-full # real API calls
|
|
91
|
+
bash scripts/loop.sh # cached (default)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Rules
|
|
95
|
+
|
|
96
|
+
1. Always call `mock.restore()` in `afterEach` to undo HTTP patches
|
|
97
|
+
2. Commit fixture files to git so CI can replay without API keys
|
|
98
|
+
3. Use descriptive `name` values — one fixture per test scenario
|
|
99
|
+
4. Check `mock.warnings` for staleness alerts in your test setup
|