rex-claude 6.1.0 → 6.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/dist/index.js CHANGED
@@ -638,7 +638,7 @@ ${COLORS.bold}${projects.length} projects found${COLORS.reset}
638
638
  break;
639
639
  }
640
640
  case "preload": {
641
- const { preload } = await import("./preload-I3MYBVNU.js");
641
+ const { preload } = await import("./preload-OJJO66IG.js");
642
642
  const cwd = process.argv[3] || process.cwd();
643
643
  const context = await preload(cwd);
644
644
  if (context) console.log(context);
@@ -14,9 +14,50 @@ import "./chunk-PDX44BCA.js";
14
14
  // src/preload.ts
15
15
  import Database from "better-sqlite3";
16
16
  import * as sqliteVec from "sqlite-vec";
17
- import { existsSync } from "fs";
17
+ import { existsSync, readFileSync } from "fs";
18
+ import { join } from "path";
18
19
  var log = createLogger("preload");
19
- var MAX_TOKENS = 200;
20
+ var MAX_TOKENS = 300;
21
+ var SKILL_RULES = [
22
+ // Frontend frameworks → UI/UX skills
23
+ { deps: ["next", "react", "vue", "nuxt", "@angular/core"], skills: ["ux-flow", "ui-craft", "ui-review"] },
24
+ // API layers → API design
25
+ { deps: ["express", "fastify", "hono", "koa", "@hapi/hapi"], skills: ["api-design", "error-handling"] },
26
+ // Database ORMs → DB design
27
+ { deps: ["drizzle-orm", "prisma", "typeorm", "mongoose", "sequelize", "knex"], skills: ["db-design"] },
28
+ // Auth libraries → auth patterns
29
+ { deps: ["next-auth", "lucia", "passport", "jose", "jsonwebtoken", "@auth/core"], skills: ["auth-patterns"] },
30
+ // i18n
31
+ { deps: ["next-intl", "i18next", "react-i18next", "vue-i18n"], skills: ["i18n"] },
32
+ // Testing
33
+ { deps: ["vitest", "jest", "@testing-library/react", "playwright", "cypress"], skills: ["test-strategy"] },
34
+ // Next.js specifically → SEO worth mentioning
35
+ { deps: ["next"], skills: ["seo", "perf"] },
36
+ // Any project → performance
37
+ { deps: ["react", "vue", "angular"], skills: ["perf"] }
38
+ ];
39
+ function detectRelevantSkills(projectRoot) {
40
+ const pkgPath = join(projectRoot, "package.json");
41
+ if (!existsSync(pkgPath)) return [];
42
+ let pkg = {};
43
+ try {
44
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
45
+ } catch {
46
+ return [];
47
+ }
48
+ const allDeps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
49
+ const suggested = /* @__PURE__ */ new Set();
50
+ for (const rule of SKILL_RULES) {
51
+ if (rule.deps && rule.deps.some((d) => allDeps.includes(d))) {
52
+ rule.skills.forEach((s) => suggested.add(s));
53
+ }
54
+ if (rule.files) {
55
+ const matched = rule.files.some((f) => existsSync(join(projectRoot, f)));
56
+ if (matched) rule.skills.forEach((s) => suggested.add(s));
57
+ }
58
+ }
59
+ return [...suggested];
60
+ }
20
61
  async function preload(cwd) {
21
62
  if (!existsSync(MEMORY_DB_PATH)) {
22
63
  log.debug("No memory DB found, skipping preload");
@@ -66,6 +107,12 @@ async function preload(cwd) {
66
107
  } finally {
67
108
  db.close();
68
109
  }
110
+ if (project?.path) {
111
+ const skills = detectRelevantSkills(project.path);
112
+ if (skills.length > 0) {
113
+ sections.push(`Skills: ${skills.join(", ")}`);
114
+ }
115
+ }
69
116
  const output = sections.join("\n");
70
117
  log.info(`Preloaded ${sections.length} sections for ${project?.name || cwd} (${output.length} chars)`);
71
118
  if (output.length > MAX_TOKENS * 4) {
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: api-design
3
+ description: REST API design. Enforces consistent endpoints, response envelopes, pagination, error codes, and versioning. Use before building any new endpoint or reviewing an existing API surface.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # API Design
8
+
9
+ Consistency is the only thing that matters in an API. One inconsistent endpoint breaks trust in all of them.
10
+
11
+ ## URL conventions
12
+
13
+ ```
14
+ GET /api/v1/users # list (always paginated)
15
+ GET /api/v1/users/:id # single resource
16
+ POST /api/v1/users # create
17
+ PATCH /api/v1/users/:id # partial update
18
+ DELETE /api/v1/users/:id # delete
19
+
20
+ # Nested resources (max 2 levels deep)
21
+ GET /api/v1/users/:id/orders
22
+ POST /api/v1/users/:id/orders
23
+
24
+ # Actions (when REST doesn't fit)
25
+ POST /api/v1/users/:id/activate
26
+ POST /api/v1/invoices/:id/send
27
+ ```
28
+
29
+ - Always plural nouns, never verbs in URL
30
+ - kebab-case for multi-word: `/order-items` not `/orderItems`
31
+ - Version prefix: `/api/v1/` — bump to v2 only for breaking changes
32
+
33
+ ## Response envelope
34
+
35
+ **Every response** uses this shape:
36
+
37
+ ```typescript
38
+ // Success
39
+ {
40
+ "data": { ... } | [...],
41
+ "meta": {
42
+ "total": 150, // always on lists
43
+ "limit": 20,
44
+ "offset": 0
45
+ }
46
+ }
47
+
48
+ // Error
49
+ {
50
+ "data": null,
51
+ "error": {
52
+ "code": "VALIDATION_ERROR", // machine-readable, SCREAMING_SNAKE
53
+ "message": "Email is required", // human-readable, end-user safe
54
+ "field": "email" // optional, for field-level errors
55
+ }
56
+ }
57
+ ```
58
+
59
+ Never return raw arrays at the top level. Never return different shapes for the same endpoint.
60
+
61
+ ## Pagination (mandatory on all lists)
62
+
63
+ ```
64
+ GET /api/v1/orders?limit=20&offset=0
65
+ GET /api/v1/orders?limit=20&offset=20
66
+
67
+ // Response
68
+ {
69
+ "data": [...],
70
+ "meta": { "total": 847, "limit": 20, "offset": 0 }
71
+ }
72
+ ```
73
+
74
+ - Default limit: 20. Max limit: 100 (enforce server-side).
75
+ - Never return unbounded lists. Always paginate.
76
+ - Frontend shows `total` from meta, not `data.length`.
77
+
78
+ ## HTTP status codes
79
+
80
+ | Code | When |
81
+ |------|------|
82
+ | 200 | Success (GET, PATCH) |
83
+ | 201 | Created (POST) — include `Location` header |
84
+ | 204 | Deleted (DELETE) — no body |
85
+ | 400 | Bad request (malformed JSON, invalid params) |
86
+ | 401 | Not authenticated (missing/expired token) |
87
+ | 403 | Authenticated but not authorized |
88
+ | 404 | Resource not found |
89
+ | 409 | Conflict (duplicate email, concurrent update) |
90
+ | 422 | Validation error (valid JSON but business rule violated) |
91
+ | 429 | Rate limited — include `Retry-After` header |
92
+ | 500 | Server error — log internally, never expose stack trace |
93
+
94
+ ## Error codes
95
+
96
+ Use machine-readable `code` values the frontend can switch on:
97
+
98
+ ```
99
+ VALIDATION_ERROR — field validation failed
100
+ NOT_FOUND — resource doesn't exist
101
+ UNAUTHORIZED — not logged in
102
+ FORBIDDEN — logged in but no permission
103
+ DUPLICATE — unique constraint violation
104
+ RATE_LIMITED — too many requests
105
+ INTERNAL_ERROR — catch-all for 500s
106
+ ```
107
+
108
+ ## Validation
109
+
110
+ - Validate at the boundary — never trust client data inside business logic
111
+ - Return ALL validation errors at once, not one by one
112
+ - Use `422` + `field` in error for form validation, `400` for structural issues
113
+
114
+ ## Rate limiting headers
115
+
116
+ Always include on authenticated endpoints:
117
+
118
+ ```
119
+ X-RateLimit-Limit: 100
120
+ X-RateLimit-Remaining: 42
121
+ X-RateLimit-Reset: 1735689600 # Unix timestamp
122
+ Retry-After: 30 # seconds, on 429
123
+ ```
124
+
125
+ ## Checklist before shipping an endpoint
126
+
127
+ - [ ] URL follows conventions (plural, kebab, versioned)
128
+ - [ ] Response uses the standard envelope
129
+ - [ ] List endpoint is paginated (limit+offset+total)
130
+ - [ ] All error cases return correct status + error code
131
+ - [ ] Validation errors return field-level details
132
+ - [ ] No secrets or internal paths in responses
133
+ - [ ] Auth required where needed (don't forget!)
134
+ - [ ] Rate limiting on sensitive endpoints (auth, email send)
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: auth-patterns
3
+ description: Authentication and authorization patterns. JWT, sessions, OAuth, RBAC, route protection. Use when implementing login, protected routes, or permission systems.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Auth Patterns
8
+
9
+ Auth bugs are security bugs. Every decision here has a security consequence.
10
+
11
+ ## Token storage
12
+
13
+ | Method | XSS safe | CSRF safe | Use for |
14
+ |--------|----------|-----------|---------|
15
+ | `httpOnly` cookie | ✅ | ❌ (need CSRF token) | Sessions, refresh tokens |
16
+ | Memory (React state) | ✅ | ✅ | Access tokens (short-lived) |
17
+ | `localStorage` | ❌ | ✅ | Never for auth tokens |
18
+ | `sessionStorage` | ❌ | ✅ | Never for auth tokens |
19
+
20
+ **Rule:** Access token in memory. Refresh token in `httpOnly` `Secure` `SameSite=Strict` cookie.
21
+
22
+ ## JWT structure
23
+
24
+ ```ts
25
+ // Access token: short-lived, stateless
26
+ const accessToken = jwt.sign(
27
+ { sub: user.id, role: user.role, email: user.email },
28
+ ACCESS_TOKEN_SECRET,
29
+ { expiresIn: '15m' } // never more than 1h
30
+ )
31
+
32
+ // Refresh token: long-lived, stored in DB for revocation
33
+ const refreshToken = jwt.sign(
34
+ { sub: user.id, jti: crypto.randomUUID() }, // jti = unique ID for revocation
35
+ REFRESH_TOKEN_SECRET,
36
+ { expiresIn: '30d' }
37
+ )
38
+ // Store hashed refresh token in DB
39
+ await db.refreshTokens.insert({ userId: user.id, tokenHash: hash(refreshToken) })
40
+ ```
41
+
42
+ **Never:** embed sensitive data in JWT (passwords, full profile, card numbers).
43
+ **Always:** verify on every request, don't trust payload without signature check.
44
+
45
+ ## Route protection (Next.js App Router)
46
+
47
+ ```ts
48
+ // middleware.ts — runs on every request before page render
49
+ import { NextResponse } from 'next/server'
50
+ import { verifyToken } from '@/lib/auth'
51
+
52
+ export async function middleware(request: NextRequest) {
53
+ const token = request.cookies.get('access_token')?.value
54
+
55
+ if (!token) {
56
+ return NextResponse.redirect(new URL('/login', request.url))
57
+ }
58
+
59
+ const payload = await verifyToken(token)
60
+ if (!payload) {
61
+ return NextResponse.redirect(new URL('/login', request.url))
62
+ }
63
+
64
+ return NextResponse.next()
65
+ }
66
+
67
+ export const config = {
68
+ matcher: ['/dashboard/:path*', '/settings/:path*', '/api/v1/:path*'],
69
+ }
70
+ ```
71
+
72
+ **Never** protect routes only client-side — always enforce on server/middleware.
73
+
74
+ ## RBAC (Role-Based Access Control)
75
+
76
+ ```ts
77
+ // Define roles and permissions clearly
78
+ const PERMISSIONS = {
79
+ 'admin': ['read', 'write', 'delete', 'manage_users'],
80
+ 'editor': ['read', 'write'],
81
+ 'viewer': ['read'],
82
+ } as const
83
+
84
+ // Check permission, not role (more flexible)
85
+ function can(user: User, action: string): boolean {
86
+ return PERMISSIONS[user.role]?.includes(action) ?? false
87
+ }
88
+
89
+ // Usage
90
+ if (!can(user, 'delete')) {
91
+ return res.status(403).json({ error: { code: 'FORBIDDEN', message: 'Insufficient permissions' } })
92
+ }
93
+ ```
94
+
95
+ **Never:** check `user.role === 'admin'` inline everywhere. Centralize permission logic.
96
+
97
+ ## Password handling
98
+
99
+ ```ts
100
+ import bcrypt from 'bcrypt' // or argon2
101
+
102
+ // Hash on registration (never store plain text)
103
+ const hash = await bcrypt.hash(password, 12) // cost factor 12
104
+
105
+ // Verify on login
106
+ const valid = await bcrypt.compare(password, user.passwordHash)
107
+
108
+ // Timing-safe comparison for tokens
109
+ import { timingSafeEqual } from 'node:crypto'
110
+ const match = timingSafeEqual(Buffer.from(token), Buffer.from(expectedToken))
111
+ ```
112
+
113
+ ## OAuth (social login)
114
+
115
+ ```ts
116
+ // Always use a library — don't implement OAuth yourself
117
+ // next-auth / auth.js for Next.js
118
+ // passport.js for Express
119
+
120
+ // Validate state parameter to prevent CSRF
121
+ // Validate redirect_uri server-side
122
+ // Store minimal profile data (don't keep what you don't need)
123
+ ```
124
+
125
+ ## Rate limiting on auth endpoints
126
+
127
+ ```ts
128
+ // Login: 5 attempts per 15min per IP
129
+ // Password reset: 3 per hour per email
130
+ // Token refresh: 10 per minute per token
131
+ // Registration: 3 per hour per IP
132
+ ```
133
+
134
+ ## Checklist
135
+
136
+ - [ ] Access tokens expire in ≤1h
137
+ - [ ] Refresh tokens stored as hash in DB (revocable)
138
+ - [ ] Tokens in `httpOnly` cookies, never `localStorage`
139
+ - [ ] Passwords hashed with bcrypt/argon2 (cost ≥12)
140
+ - [ ] CSRF protection on all cookie-based state changes
141
+ - [ ] Rate limiting on login, register, reset endpoints
142
+ - [ ] All protected routes verified server-side (not just client guard)
143
+ - [ ] `HTTPS` only in production (`Secure` cookie flag)
144
+ - [ ] Logout invalidates refresh token in DB
145
+ - [ ] No sensitive data in JWT payload
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: db-design
3
+ description: Database schema design, indexing strategy, and migration patterns. Prevents N+1, missing indexes, unsafe migrations, and schema drift. Use when designing tables, adding columns, or writing complex queries.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # DB Design
8
+
9
+ Bad schema is the most expensive technical debt. You can refactor code in hours; migrating 10M rows takes days.
10
+
11
+ ## Schema design principles
12
+
13
+ ### Naming
14
+ - Tables: `snake_case`, plural (`users`, `order_items`, `refresh_tokens`)
15
+ - Columns: `snake_case` (`created_at`, `user_id`, `is_active`)
16
+ - Foreign keys: `{table_singular}_id` (`user_id`, `order_id`)
17
+ - Booleans: prefix `is_` or `has_` (`is_active`, `has_verified_email`)
18
+ - Timestamps: always `created_at` + `updated_at` on every table
19
+
20
+ ### Standard columns every table needs
21
+ ```sql
22
+ id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY -- or UUID
23
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
24
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
25
+ ```
26
+
27
+ ### Soft delete pattern (prefer over hard delete)
28
+ ```sql
29
+ deleted_at TIMESTAMPTZ -- NULL = active, non-NULL = deleted
30
+ -- Query: WHERE deleted_at IS NULL
31
+ ```
32
+
33
+ ## Indexing strategy
34
+
35
+ **Index everything you filter, sort, or join on.**
36
+
37
+ ```sql
38
+ -- Always index foreign keys
39
+ CREATE INDEX idx_orders_user_id ON orders(user_id);
40
+
41
+ -- Composite index for common query patterns (leftmost prefix rule)
42
+ CREATE INDEX idx_orders_user_status ON orders(user_id, status);
43
+ -- This covers: WHERE user_id = ?
44
+ -- And: WHERE user_id = ? AND status = ?
45
+ -- But NOT: WHERE status = ? alone
46
+
47
+ -- Partial index for filtered queries
48
+ CREATE INDEX idx_orders_pending ON orders(created_at)
49
+ WHERE status = 'pending';
50
+
51
+ -- Text search
52
+ CREATE INDEX idx_products_name_search ON products USING gin(to_tsvector('english', name));
53
+ ```
54
+
55
+ **Check for missing indexes:**
56
+ ```sql
57
+ EXPLAIN ANALYZE SELECT ... -- look for "Seq Scan" on large tables
58
+ -- Seq Scan on 1000 rows = fine. On 1M rows = add an index.
59
+ ```
60
+
61
+ **Over-indexing costs:** Every index slows down INSERT/UPDATE/DELETE. Don't index columns you never filter on.
62
+
63
+ ## N+1 pattern (the most common DB bug)
64
+
65
+ ```ts
66
+ // BAD — N+1: 1 query for orders + N queries for each user
67
+ const orders = await db.orders.findMany()
68
+ for (const order of orders) {
69
+ order.user = await db.users.findById(order.userId) // N queries!
70
+ }
71
+
72
+ // GOOD — 1 query with join or eager load
73
+ const orders = await db.orders.findMany({
74
+ include: { user: true } // Prisma
75
+ })
76
+
77
+ // Or raw SQL with JOIN
78
+ SELECT o.*, u.name, u.email
79
+ FROM orders o
80
+ JOIN users u ON u.id = o.user_id
81
+ WHERE o.status = 'pending'
82
+ ```
83
+
84
+ ## Safe migration patterns
85
+
86
+ ```sql
87
+ -- SAFE: adding nullable column (instant, no lock)
88
+ ALTER TABLE users ADD COLUMN avatar_url TEXT;
89
+
90
+ -- SAFE: adding column with default (Postgres 11+, instant)
91
+ ALTER TABLE users ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT false;
92
+
93
+ -- DANGEROUS: adding NOT NULL without default (locks table, blocks writes)
94
+ -- Do it in 3 steps:
95
+ -- 1. Add nullable
96
+ ALTER TABLE users ADD COLUMN phone TEXT;
97
+ -- 2. Backfill
98
+ UPDATE users SET phone = '' WHERE phone IS NULL;
99
+ -- 3. Add constraint
100
+ ALTER TABLE users ALTER COLUMN phone SET NOT NULL;
101
+
102
+ -- DANGEROUS: dropping column (data loss, code must be deployed first)
103
+ -- 1. Deploy code that no longer uses the column
104
+ -- 2. Then drop in next migration
105
+ ALTER TABLE users DROP COLUMN legacy_field;
106
+
107
+ -- DANGEROUS: renaming column/table (breaks existing queries)
108
+ -- Use a 2-phase rename: add new column, migrate data, remove old
109
+ ```
110
+
111
+ ## Transaction patterns
112
+
113
+ ```ts
114
+ // Always use transactions for multi-step operations
115
+ await db.transaction(async (tx) => {
116
+ const order = await tx.orders.create({ data: { userId, total } })
117
+ await tx.inventory.decrement({ productId, quantity })
118
+ await tx.payments.create({ data: { orderId: order.id, amount: total } })
119
+ // If any step fails, all are rolled back
120
+ })
121
+ ```
122
+
123
+ ## Query hygiene
124
+
125
+ - Never `SELECT *` in production — select only what you need
126
+ - Parameterized queries always (no string interpolation)
127
+ - `LIMIT` on all queries that could return unbounded results
128
+ - Avoid `OFFSET` for large pagination → use cursor-based (`WHERE id > lastId`)
129
+
130
+ ## Checklist
131
+
132
+ - [ ] Every table has `id`, `created_at`, `updated_at`
133
+ - [ ] All foreign keys have indexes
134
+ - [ ] Columns filtered/sorted in queries have indexes
135
+ - [ ] `EXPLAIN ANALYZE` run on queries touching >10k rows
136
+ - [ ] Migrations are reversible (or have a rollback plan)
137
+ - [ ] No `ALTER TABLE` without reviewing lock implications
138
+ - [ ] Multi-step operations wrapped in transactions
139
+ - [ ] No `SELECT *`, no string-interpolated queries
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: error-handling
3
+ description: Error handling strategy. Error boundaries, logging patterns, user-facing messages, monitoring setup, and async error flows. Use when building any feature that can fail, or auditing error handling in existing code.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Error Handling
8
+
9
+ Every error falls into two buckets: expected (handle gracefully) and unexpected (log, alert, recover).
10
+ Never show users a stack trace. Never silently swallow an error.
11
+
12
+ ## Error hierarchy
13
+
14
+ ```
15
+ Expected errors (handle in code)
16
+ ├── Validation errors → show to user, guide to fix
17
+ ├── Not found → 404, redirect or empty state
18
+ ├── Auth errors → redirect to login
19
+ ├── Business rule violations → explain and offer alternative
20
+ └── External API failures → retry or fallback
21
+
22
+ Unexpected errors (log + alert + recover)
23
+ ├── Unhandled promise rejections
24
+ ├── Database connection failures
25
+ ├── Third-party SDK crashes
26
+ └── Memory/timeout errors
27
+ ```
28
+
29
+ ## Frontend: React Error Boundaries
30
+
31
+ ```tsx
32
+ // components/ErrorBoundary.tsx
33
+ 'use client'
34
+ import { Component, ReactNode } from 'react'
35
+
36
+ interface Props { children: ReactNode; fallback?: ReactNode }
37
+ interface State { hasError: boolean; error?: Error }
38
+
39
+ export class ErrorBoundary extends Component<Props, State> {
40
+ state: State = { hasError: false }
41
+
42
+ static getDerivedStateFromError(error: Error): State {
43
+ return { hasError: true, error }
44
+ }
45
+
46
+ componentDidCatch(error: Error, info: { componentStack: string }) {
47
+ // Log to your monitoring service
48
+ console.error('[ErrorBoundary]', error, info)
49
+ reportError(error, { context: info.componentStack })
50
+ }
51
+
52
+ render() {
53
+ if (this.state.hasError) {
54
+ return this.props.fallback ?? <DefaultErrorFallback error={this.state.error} />
55
+ }
56
+ return this.props.children
57
+ }
58
+ }
59
+
60
+ // Wrap at route level and around risky components
61
+ <ErrorBoundary fallback={<ErrorPage />}>
62
+ <Dashboard />
63
+ </ErrorBoundary>
64
+ ```
65
+
66
+ ## Frontend: async error patterns
67
+
68
+ ```tsx
69
+ // NEVER: unhandled promise
70
+ useEffect(() => {
71
+ fetchData() // if this throws, silent failure
72
+ }, [])
73
+
74
+ // GOOD: handle errors explicitly
75
+ const [error, setError] = useState<string | null>(null)
76
+
77
+ useEffect(() => {
78
+ fetchData()
79
+ .catch(err => {
80
+ setError(getErrorMessage(err))
81
+ reportError(err)
82
+ })
83
+ }, [])
84
+
85
+ // GOOD: React Query handles this automatically
86
+ const { data, error, isError } = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
87
+ if (isError) return <ErrorState message={getErrorMessage(error)} />
88
+ ```
89
+
90
+ ## Next.js App Router error files
91
+
92
+ ```tsx
93
+ // app/error.tsx — catches runtime errors in route segment
94
+ 'use client'
95
+ export default function Error({ error, reset }: { error: Error; reset: () => void }) {
96
+ useEffect(() => { reportError(error) }, [error])
97
+
98
+ return (
99
+ <div>
100
+ <h2>Something went wrong</h2>
101
+ <button onClick={reset}>Try again</button>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ // app/not-found.tsx — 404 handler
107
+ export default function NotFound() {
108
+ return <div>Page not found</div>
109
+ }
110
+
111
+ // app/global-error.tsx — catches errors in root layout
112
+ 'use client'
113
+ export default function GlobalError({ error, reset }) { ... }
114
+ ```
115
+
116
+ ## Backend: error response pattern
117
+
118
+ ```ts
119
+ // Never expose internal errors to clients
120
+ class AppError extends Error {
121
+ constructor(
122
+ public code: string, // machine-readable
123
+ public message: string, // end-user safe
124
+ public status: number, // HTTP status
125
+ public details?: unknown // optional context (logged, not returned)
126
+ ) { super(message) }
127
+ }
128
+
129
+ // Express global error handler
130
+ app.use((err: Error, req, res, next) => {
131
+ if (err instanceof AppError) {
132
+ return res.status(err.status).json({
133
+ data: null,
134
+ error: { code: err.code, message: err.message }
135
+ })
136
+ }
137
+
138
+ // Unexpected error — log full details, return generic message
139
+ logger.error({ err, req: { method: req.method, url: req.url } })
140
+ reportError(err)
141
+
142
+ res.status(500).json({
143
+ data: null,
144
+ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' }
145
+ })
146
+ })
147
+ ```
148
+
149
+ ## Logging levels
150
+
151
+ | Level | When | Example |
152
+ |-------|------|---------|
153
+ | `error` | Unexpected failures, alerts needed | DB crash, unhandled exception |
154
+ | `warn` | Expected failures worth monitoring | Retry attempts, rate limit hit |
155
+ | `info` | Key business events | User registered, payment processed |
156
+ | `debug` | Dev only, never in production | Query params, response body |
157
+
158
+ ```ts
159
+ // Always log with context, not just the message
160
+ logger.error({ err, userId: req.user?.id, endpoint: req.url }, 'Payment failed')
161
+ // Not: logger.error('Payment failed')
162
+ ```
163
+
164
+ ## Async error rules
165
+
166
+ - Every `async` function must have a `try/catch` or be called with `.catch()`
167
+ - Never `await` inside `forEach` — use `Promise.all` or `for...of`
168
+ - Unhandled promise rejections crash Node.js 15+ — always handle
169
+
170
+ ```ts
171
+ // GOOD
172
+ const results = await Promise.all(items.map(item => processItem(item)))
173
+
174
+ // BAD (forEach ignores returned promise)
175
+ items.forEach(async (item) => await processItem(item))
176
+
177
+ // For sequential with error isolation:
178
+ for (const item of items) {
179
+ try {
180
+ await processItem(item)
181
+ } catch (err) {
182
+ logger.warn({ err, itemId: item.id }, 'Failed to process item, continuing')
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Checklist
188
+
189
+ - [ ] Error boundaries around route segments and risky components
190
+ - [ ] Every `fetch`/`async` call has error handling
191
+ - [ ] User sees human-readable message, never stack trace or raw error
192
+ - [ ] Unexpected errors logged with full context (user, endpoint, stack)
193
+ - [ ] `app/error.tsx` and `app/not-found.tsx` exist and are styled
194
+ - [ ] Global error handler in API (Express/Hono/Next route handler)
195
+ - [ ] `AppError` class for expected errors with machine-readable codes
196
+ - [ ] Error monitoring service wired up (see monitoring patterns)
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: i18n
3
+ description: Internationalization patterns for Next.js. next-intl setup, message extraction, locale routing, date/number formatting, and pluralization. Use when adding multi-language support or auditing an existing i18n setup.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # i18n (Next.js + next-intl)
8
+
9
+ i18n added after launch is painful. i18n from day one is just a folder structure.
10
+
11
+ ## Setup (next-intl)
12
+
13
+ ```bash
14
+ npm install next-intl
15
+ ```
16
+
17
+ ```
18
+ messages/
19
+ ├── en.json
20
+ ├── fr.json
21
+ └── es.json
22
+
23
+ app/
24
+ ├── [locale]/
25
+ │ ├── layout.tsx
26
+ │ └── page.tsx
27
+ ├── i18n/
28
+ │ ├── routing.ts
29
+ │ └── request.ts
30
+ └── middleware.ts
31
+ ```
32
+
33
+ ```ts
34
+ // i18n/routing.ts
35
+ import { defineRouting } from 'next-intl/routing'
36
+
37
+ export const routing = defineRouting({
38
+ locales: ['en', 'fr', 'es'],
39
+ defaultLocale: 'en',
40
+ })
41
+
42
+ // middleware.ts
43
+ import createMiddleware from 'next-intl/middleware'
44
+ import { routing } from './i18n/routing'
45
+
46
+ export default createMiddleware(routing)
47
+
48
+ export const config = {
49
+ matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
50
+ }
51
+ ```
52
+
53
+ ## Message files
54
+
55
+ ```json
56
+ // messages/en.json
57
+ {
58
+ "nav": {
59
+ "home": "Home",
60
+ "pricing": "Pricing",
61
+ "signin": "Sign in"
62
+ },
63
+ "dashboard": {
64
+ "welcome": "Welcome back, {name}!",
65
+ "items": "{count, plural, =0 {No items} one {# item} other {# items}}",
66
+ "lastSeen": "Last seen {date, relativetime}"
67
+ },
68
+ "errors": {
69
+ "required": "{field} is required",
70
+ "notFound": "Page not found"
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## Usage in components
76
+
77
+ ```tsx
78
+ // Server component
79
+ import { getTranslations } from 'next-intl/server'
80
+
81
+ export default async function Page() {
82
+ const t = await getTranslations('dashboard')
83
+ return <h1>{t('welcome', { name: 'Kevin' })}</h1>
84
+ }
85
+
86
+ // Client component
87
+ 'use client'
88
+ import { useTranslations } from 'next-intl'
89
+
90
+ export function NavBar() {
91
+ const t = useTranslations('nav')
92
+ return <nav><a>{t('home')}</a></nav>
93
+ }
94
+
95
+ // Pluralization
96
+ t('items', { count: 0 }) // "No items"
97
+ t('items', { count: 1 }) // "1 item"
98
+ t('items', { count: 42 }) // "42 items"
99
+ ```
100
+
101
+ ## Date, number, and currency formatting
102
+
103
+ ```tsx
104
+ import { useFormatter } from 'next-intl'
105
+
106
+ export function PricingCard({ price, date }: { price: number; date: Date }) {
107
+ const format = useFormatter()
108
+
109
+ return (
110
+ <div>
111
+ {/* Currency — auto-formats per locale */}
112
+ <p>{format.number(price, { style: 'currency', currency: 'EUR' })}</p>
113
+ {/* fr: 29,99 € en: €29.99 */}
114
+
115
+ {/* Relative time */}
116
+ <time>{format.relativeTime(date)}</time>
117
+ {/* "3 hours ago" / "il y a 3 heures" */}
118
+
119
+ {/* Absolute date */}
120
+ <span>{format.dateTime(date, { dateStyle: 'long' })}</span>
121
+ {/* "March 7, 2026" / "7 mars 2026" */}
122
+ </div>
123
+ )
124
+ }
125
+ ```
126
+
127
+ ## Locale-aware routing
128
+
129
+ ```tsx
130
+ import { Link } from '@/i18n/routing' // next-intl's Link (adds locale prefix)
131
+
132
+ // /en/dashboard → /fr/dashboard automatically
133
+ <Link href="/dashboard">Dashboard</Link>
134
+
135
+ // Redirect to locale-specific path
136
+ import { redirect } from 'next-intl/navigation'
137
+ redirect({ href: '/login', locale: 'fr' })
138
+ ```
139
+
140
+ ## Common mistakes to avoid
141
+
142
+ | Don't | Do instead |
143
+ |-------|-----------|
144
+ | Hardcode strings in JSX | Extract to message files from day 1 |
145
+ | Use `new Date().toLocaleDateString()` | Use `format.dateTime()` |
146
+ | Build plural strings manually (`${count} item(s)`) | Use ICU plural syntax in messages |
147
+ | Store locale in state | Use URL-based routing (SEO + shareable) |
148
+ | Forget RTL support | Add `dir={locale === 'ar' ? 'rtl' : 'ltr'}` to `<html>` |
149
+
150
+ ## Checklist
151
+
152
+ - [ ] Locale detected from URL, not browser/cookie (SEO friendly)
153
+ - [ ] All user-facing strings in message files (grep for hardcoded text)
154
+ - [ ] Dates formatted with `format.dateTime()`, numbers with `format.number()`
155
+ - [ ] Plurals use ICU syntax, not manual string building
156
+ - [ ] `<Link>` from next-intl (not next/link) for locale-aware navigation
157
+ - [ ] `<html lang={locale}>` set in root layout
158
+ - [ ] Fallback to `defaultLocale` when translation missing
159
+ - [ ] OG metadata localized per language
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: perf
3
+ description: Performance audit. Identifies bottlenecks in frontend (Core Web Vitals, bundle, rendering), backend (slow queries, N+1, missing indexes), and infrastructure (caching, CDN). Reports with file:line and concrete fixes. Use when the app feels slow or before a launch.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Performance
8
+
9
+ Measure first. Never guess where the bottleneck is.
10
+
11
+ ## Frontend
12
+
13
+ ### Core Web Vitals targets
14
+ | Metric | Good | Needs work |
15
+ |--------|------|-----------|
16
+ | LCP (largest content) | <2.5s | >4s |
17
+ | INP (interaction) | <200ms | >500ms |
18
+ | CLS (layout shift) | <0.1 | >0.25 |
19
+
20
+ ### Bundle audit
21
+ ```bash
22
+ # Next.js
23
+ ANALYZE=true next build # needs @next/bundle-analyzer
24
+
25
+ # Check for duplicate deps
26
+ npx depcheck
27
+ npx bundlephobia <package> # before adding anything
28
+ ```
29
+
30
+ **Red flags:**
31
+ - `moment.js` (replace with `date-fns` or `dayjs`)
32
+ - Full `lodash` import (use `lodash-es` + tree-shaking)
33
+ - Unoptimized images (no `next/image`, missing `width`/`height`)
34
+ - No code splitting (all JS in one chunk)
35
+ - `useEffect` on every render with no dep array
36
+
37
+ ### Rendering bottlenecks
38
+
39
+ ```tsx
40
+ // BAD: expensive filter on every render
41
+ const filtered = items.filter(...)
42
+
43
+ // GOOD: memoized
44
+ const filtered = useMemo(() => items.filter(...), [items, query])
45
+
46
+ // BAD: new function reference breaks child memo
47
+ <Child onClick={() => doSomething()} />
48
+
49
+ // GOOD
50
+ const handleClick = useCallback(() => doSomething(), [])
51
+ <Child onClick={handleClick} />
52
+ ```
53
+
54
+ **Check:**
55
+ - [ ] Lists with 100+ items: use virtualization (`react-window` or `@tanstack/virtual`)
56
+ - [ ] Heavy components: lazy load with `dynamic(() => import(...), { ssr: false })`
57
+ - [ ] Images: `next/image` with `priority` on above-the-fold, lazy below
58
+ - [ ] Fonts: `next/font` only, never `<link>` to Google Fonts
59
+
60
+ ## Backend / API
61
+
62
+ ### Query audit
63
+
64
+ ```sql
65
+ -- Spot slow queries (Postgres)
66
+ SELECT query, mean_exec_time, calls
67
+ FROM pg_stat_statements
68
+ ORDER BY mean_exec_time DESC LIMIT 20;
69
+
70
+ -- Missing indexes
71
+ EXPLAIN ANALYZE SELECT ...; -- look for "Seq Scan" on large tables
72
+ ```
73
+
74
+ **Red flags:**
75
+ - `SELECT *` on wide tables
76
+ - Missing indexes on foreign keys and filtered columns
77
+ - N+1: loop calling DB inside `.map()` — batch with `WHERE id IN (...)` instead
78
+ - No pagination on list endpoints
79
+ - Sorting on non-indexed column
80
+
81
+ ### Caching strategy
82
+
83
+ | Layer | Tool | TTL |
84
+ |-------|------|-----|
85
+ | Static data | CDN edge cache | 1h–24h |
86
+ | API responses | Redis / Cloudflare KV | 1min–1h |
87
+ | DB queries | In-memory LRU | 30s |
88
+ | Images | `Cache-Control: public, max-age=31536000` | 1 year |
89
+
90
+ ## Audit output format
91
+
92
+ ```
93
+ ## Perf Audit — [app/page]
94
+
95
+ ### Critical (>1s impact)
96
+ - [file:line] Description + fix
97
+
98
+ ### High (100ms–1s)
99
+ - ...
100
+
101
+ ### Quick wins (<1h to fix)
102
+ - ...
103
+
104
+ ### Metrics baseline
105
+ - Bundle: Xkb (main), Ykb (page)
106
+ - TTFB: Xms
107
+ - LCP: Xs
108
+ ```
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: seo
3
+ description: SEO implementation for Next.js. Metadata, OG tags, sitemap, robots.txt, structured data, and Core Web Vitals. Use when building landing pages, blog, or any public-facing page.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # SEO (Next.js App Router)
8
+
9
+ SEO is plumbing. Do it right once and forget it. Do it wrong and it's invisible for 6 months.
10
+
11
+ ## Metadata API (App Router)
12
+
13
+ ```tsx
14
+ // app/layout.tsx — site-wide defaults
15
+ export const metadata: Metadata = {
16
+ metadataBase: new URL('https://yourdomain.com'),
17
+ title: {
18
+ default: 'Your App Name',
19
+ template: '%s | Your App Name', // page title → "Page | Your App Name"
20
+ },
21
+ description: 'Clear, 150-char max description of what the app does.',
22
+ openGraph: {
23
+ type: 'website',
24
+ locale: 'en_US',
25
+ url: 'https://yourdomain.com',
26
+ siteName: 'Your App Name',
27
+ images: [{ url: '/og-default.png', width: 1200, height: 630 }],
28
+ },
29
+ twitter: {
30
+ card: 'summary_large_image',
31
+ creator: '@yourhandle',
32
+ },
33
+ robots: { index: true, follow: true },
34
+ }
35
+
36
+ // app/blog/[slug]/page.tsx — dynamic per-page metadata
37
+ export async function generateMetadata({ params }): Promise<Metadata> {
38
+ const post = await getPost(params.slug)
39
+ return {
40
+ title: post.title,
41
+ description: post.excerpt,
42
+ openGraph: {
43
+ type: 'article',
44
+ publishedTime: post.publishedAt,
45
+ images: [{ url: post.ogImage, width: 1200, height: 630 }],
46
+ },
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## OG Image generation
52
+
53
+ ```tsx
54
+ // app/og/route.tsx — dynamic OG images (Vercel OG)
55
+ import { ImageResponse } from 'next/og'
56
+
57
+ export async function GET(request: Request) {
58
+ const { searchParams } = new URL(request.url)
59
+ const title = searchParams.get('title') || 'Default Title'
60
+
61
+ return new ImageResponse(
62
+ <div style={{ display: 'flex', width: '100%', height: '100%', background: '#000' }}>
63
+ <h1 style={{ color: '#fff', fontSize: 60 }}>{title}</h1>
64
+ </div>,
65
+ { width: 1200, height: 630 }
66
+ )
67
+ }
68
+
69
+ // Use in metadata:
70
+ // images: [{ url: `/og?title=${encodeURIComponent(post.title)}` }]
71
+ ```
72
+
73
+ ## Sitemap
74
+
75
+ ```tsx
76
+ // app/sitemap.ts
77
+ import { MetadataRoute } from 'next'
78
+
79
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
80
+ const posts = await getAllPosts()
81
+
82
+ return [
83
+ { url: 'https://yourdomain.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
84
+ { url: 'https://yourdomain.com/pricing', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
85
+ ...posts.map(post => ({
86
+ url: `https://yourdomain.com/blog/${post.slug}`,
87
+ lastModified: post.updatedAt,
88
+ changeFrequency: 'monthly' as const,
89
+ priority: 0.6,
90
+ })),
91
+ ]
92
+ }
93
+ ```
94
+
95
+ ## Robots.txt
96
+
97
+ ```tsx
98
+ // app/robots.ts
99
+ import { MetadataRoute } from 'next'
100
+
101
+ export default function robots(): MetadataRoute.Robots {
102
+ return {
103
+ rules: [
104
+ { userAgent: '*', allow: '/', disallow: ['/api/', '/dashboard/', '/admin/'] },
105
+ ],
106
+ sitemap: 'https://yourdomain.com/sitemap.xml',
107
+ }
108
+ }
109
+ ```
110
+
111
+ ## Structured data (JSON-LD)
112
+
113
+ ```tsx
114
+ // app/blog/[slug]/page.tsx
115
+ export default function BlogPost({ post }) {
116
+ const jsonLd = {
117
+ '@context': 'https://schema.org',
118
+ '@type': 'Article',
119
+ headline: post.title,
120
+ datePublished: post.publishedAt,
121
+ dateModified: post.updatedAt,
122
+ author: { '@type': 'Person', name: post.author },
123
+ description: post.excerpt,
124
+ }
125
+
126
+ return (
127
+ <>
128
+ <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
129
+ {/* page content */}
130
+ </>
131
+ )
132
+ }
133
+ ```
134
+
135
+ Common schemas: `Article`, `Product`, `Organization`, `BreadcrumbList`, `FAQPage`, `LocalBusiness`
136
+
137
+ ## Technical SEO checklist
138
+
139
+ - [ ] `metadataBase` set in root layout (required for absolute OG URLs)
140
+ - [ ] `title.template` set for consistent page titles
141
+ - [ ] Description 50–160 chars on every page
142
+ - [ ] OG image 1200×630px on every page (static or generated)
143
+ - [ ] `sitemap.ts` includes all public pages
144
+ - [ ] `robots.ts` blocks `/api/`, `/dashboard/`, `/admin/`
145
+ - [ ] Canonical URL set on duplicate content pages
146
+ - [ ] Structured data on blog posts, products, FAQ
147
+ - [ ] No `noindex` accidentally on important pages
148
+ - [ ] Images have descriptive `alt` text
149
+ - [ ] Headings hierarchy: one `h1` per page, logical `h2`/`h3` structure
150
+ - [ ] Core Web Vitals passing (LCP <2.5s, CLS <0.1, INP <200ms)
151
+
152
+ ## Quick wins (do these first)
153
+
154
+ 1. Add `metadataBase` + default `title.template` (20 min)
155
+ 2. Add OG image to every public page (1h)
156
+ 3. Generate sitemap (30 min)
157
+ 4. Add `robots.ts` (15 min)
158
+ 5. Add JSON-LD to blog/product pages (1h)
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: test-strategy
3
+ description: Testing strategy. Defines what to test, at what level, and how to mock. Prevents over-testing, under-testing, and tests that slow down development. Use before writing tests for a new feature or when a test suite grows painful.
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Test Strategy
8
+
9
+ Test the behavior, not the implementation. A test that breaks when you rename a variable is worthless.
10
+
11
+ ## The pyramid
12
+
13
+ ```
14
+ /‾‾‾‾‾‾\
15
+ / E2E (5%) \ — Happy paths only. Slow, brittle, expensive.
16
+ /────────────\
17
+ / Integration \ — API endpoints, DB queries, service boundaries.
18
+ / (25–35%) \
19
+ /──────────────────\
20
+ / Unit (60–70%) \ — Business logic, transformations, edge cases.
21
+ /──────────────────────\
22
+ ```
23
+
24
+ If your pyramid is inverted (more E2E than unit), your tests are slow and fragile.
25
+
26
+ ## What to test at each level
27
+
28
+ ### Unit tests
29
+ Test pure functions, business logic, edge cases:
30
+ - Calculations, transformations, validations
31
+ - Error conditions and boundary values
32
+ - Utility functions
33
+ - State machines
34
+
35
+ ```ts
36
+ // GOOD — tests behavior
37
+ test('calculates discount correctly', () => {
38
+ expect(applyDiscount(100, 0.2)).toBe(80)
39
+ expect(applyDiscount(100, 0)).toBe(100)
40
+ expect(applyDiscount(0, 0.5)).toBe(0)
41
+ })
42
+
43
+ // BAD — tests implementation, breaks on refactor
44
+ test('calls Math.floor once', () => { ... })
45
+ ```
46
+
47
+ ### Integration tests
48
+ Test your API endpoints end-to-end (request → response, with real DB in test mode):
49
+
50
+ ```ts
51
+ test('POST /api/v1/users creates user', async () => {
52
+ const res = await request(app)
53
+ .post('/api/v1/users')
54
+ .send({ email: 'test@example.com', name: 'Test' })
55
+
56
+ expect(res.status).toBe(201)
57
+ expect(res.body.data.email).toBe('test@example.com')
58
+ // verify it's actually in the DB
59
+ const user = await db.users.findByEmail('test@example.com')
60
+ expect(user).toBeTruthy()
61
+ })
62
+ ```
63
+
64
+ ### E2E tests
65
+ Reserve for critical user paths only:
66
+ - Authentication (login, logout, forgot password)
67
+ - Checkout / payment flow
68
+ - Core value action of the product (publish, send, submit)
69
+
70
+ Never E2E test: form validation, UI states, error messages, edge cases.
71
+
72
+ ## Mocking rules
73
+
74
+ | Mock this | Don't mock this |
75
+ |-----------|----------------|
76
+ | External APIs (Stripe, SendGrid, S3) | Internal business logic |
77
+ | Email/SMS sending | Database in unit tests (use in-memory or test DB) |
78
+ | Time (`Date.now()`, `new Date()`) | Your own service layer |
79
+ | Random values (`Math.random()`) | Framework code |
80
+ | File system in unit tests | |
81
+
82
+ ```ts
83
+ // GOOD — mock external, test logic
84
+ jest.mock('./email-service')
85
+ test('sends welcome email on registration', async () => {
86
+ await registerUser({ email: 'test@example.com' })
87
+ expect(sendWelcomeEmail).toHaveBeenCalledWith('test@example.com')
88
+ })
89
+
90
+ // BAD — mocking what you're testing
91
+ jest.mock('./user-service')
92
+ test('user service creates user', () => { ... }) // what are we even testing?
93
+ ```
94
+
95
+ ## Coverage as a signal, not a goal
96
+
97
+ - 80% coverage is fine. 100% is usually waste.
98
+ - Coverage doesn't measure quality — a test that does `expect(true).toBe(true)` counts.
99
+ - Focus on: critical paths, error branches, edge cases.
100
+ - Skip: trivial getters/setters, framework boilerplate, generated code.
101
+
102
+ ## Red flags in test suites
103
+
104
+ - Tests that test implementation details (internals, private methods)
105
+ - `setTimeout` or `sleep` in tests (use fake timers or conditions)
106
+ - Tests that depend on execution order (each test must be independent)
107
+ - Snapshots for everything (become maintenance burden — use for UI components only)
108
+ - Mocking your own database service (test against a real test DB instead)
109
+
110
+ ## Before writing tests
111
+
112
+ 1. Identify the **behavior** you're testing (not the code)
113
+ 2. Write the test name as a sentence: `"should return 404 when user doesn't exist"`
114
+ 3. Arrange → Act → Assert — three clear sections, no more
115
+ 4. One assertion per test (or one logical group)
116
+ 5. Make the test fail first — if it passes without code, it's testing nothing
117
+
118
+ ## Stack conventions
119
+
120
+ ```ts
121
+ // Vitest (preferred for Vite/Next projects)
122
+ import { describe, test, expect, vi } from 'vitest'
123
+
124
+ // Jest (legacy, Node projects)
125
+ import { describe, test, expect, jest } from '@jest/globals'
126
+
127
+ // React Testing Library — test from user perspective
128
+ import { render, screen, userEvent } from '@testing-library/react'
129
+ test('shows error when email invalid', async () => {
130
+ render(<LoginForm />)
131
+ await userEvent.type(screen.getByLabelText('Email'), 'not-an-email')
132
+ await userEvent.click(screen.getByRole('button', { name: 'Login' }))
133
+ expect(screen.getByText('Invalid email')).toBeInTheDocument()
134
+ })
135
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rex-claude",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "description": "REX — dev assistant with Telegram gateway, memory RAG, guards, model switching, and macOS native app",
5
5
  "type": "module",
6
6
  "bin": {