@lousy-agents/cli 1.0.2 → 1.0.6
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/package.json +9 -7
- package/ui/copilot-with-react/.devcontainer/devcontainer.json +90 -0
- package/ui/copilot-with-react/.editorconfig +16 -0
- package/ui/copilot-with-react/.github/ISSUE_TEMPLATE/feature-to-spec.yml +54 -0
- package/ui/copilot-with-react/.github/copilot-instructions.md +281 -0
- package/ui/copilot-with-react/.github/instructions/pipeline.instructions.md +46 -0
- package/ui/copilot-with-react/.github/instructions/software-architecture.instructions.md +401 -0
- package/ui/copilot-with-react/.github/instructions/spec.instructions.md +411 -0
- package/ui/copilot-with-react/.github/instructions/test.instructions.md +119 -0
- package/ui/copilot-with-react/.github/specs/README.md +84 -0
- package/ui/copilot-with-react/.github/workflows/assign-copilot.yml +59 -0
- package/ui/copilot-with-react/.github/workflows/copilot-setup-steps.yml +37 -0
- package/ui/copilot-with-react/.nvmrc +1 -0
- package/ui/copilot-with-react/.vscode/extensions.json +15 -0
- package/ui/copilot-with-react/.vscode/launch.json +19 -0
- package/ui/copilot-with-react/.yamllint +18 -0
- package/ui/copilot-with-react/biome.json +31 -0
- package/ui/copilot-with-react/next.config.ts +16 -0
- package/ui/copilot-with-react/package.json +41 -0
- package/ui/copilot-with-react/tsconfig.json +28 -0
- package/ui/copilot-with-react/vitest.config.ts +17 -0
- package/ui/copilot-with-react/vitest.setup.ts +2 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/**/*.{ts,tsx}"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Clean Architecture Instructions for SPA
|
|
6
|
+
|
|
7
|
+
## The Dependency Rule
|
|
8
|
+
|
|
9
|
+
Dependencies point inward only. Outer layers depend on inner layers, never the reverse.
|
|
10
|
+
|
|
11
|
+
**Layers (innermost to outermost):**
|
|
12
|
+
1. Entities — Enterprise business rules
|
|
13
|
+
2. Use Cases — Application business rules
|
|
14
|
+
3. Adapters — Interface converters (gateways, components)
|
|
15
|
+
4. Infrastructure — Frameworks, drivers, composition root
|
|
16
|
+
|
|
17
|
+
## Directory Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
├── entities/ # Layer 1: Business domain entities
|
|
22
|
+
├── use-cases/ # Layer 2: Application business rules
|
|
23
|
+
├── gateways/ # Layer 3: Backend-for-Frontend (BFF) API adapters
|
|
24
|
+
├── app/ # Layer 4: Next.js App Router (pages, layouts)
|
|
25
|
+
│ ├── api/ # BFF API routes (proxy to backend services)
|
|
26
|
+
│ ├── (routes)/ # Page routes
|
|
27
|
+
│ └── layout.tsx # Root layout
|
|
28
|
+
├── components/ # Layer 3: React components (UI adapters)
|
|
29
|
+
│ ├── ui/ # Primitive UI components
|
|
30
|
+
│ └── features/ # Feature-specific components
|
|
31
|
+
├── hooks/ # Layer 3: React hooks for data fetching
|
|
32
|
+
└── lib/ # Layer 3: Configuration and utilities
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Layer 1: Entities
|
|
36
|
+
|
|
37
|
+
**Location:** `src/entities/`
|
|
38
|
+
|
|
39
|
+
- MUST NOT import from any other layer
|
|
40
|
+
- MUST NOT depend on frameworks or infrastructure
|
|
41
|
+
- MUST NOT use non-deterministic or side-effect-producing global APIs (e.g., `crypto.randomUUID()`, `Date.now()`, `Math.random()`)
|
|
42
|
+
- MAY use pure, deterministic global APIs (e.g., `Intl.NumberFormat`, `parseInt()`, `JSON.parse()`)
|
|
43
|
+
- MUST be plain TypeScript objects/classes with business logic
|
|
44
|
+
- MAY contain validation and business rules
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// src/entities/product.ts
|
|
48
|
+
export interface Product {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly price: number;
|
|
52
|
+
readonly inStock: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isAvailableForPurchase(product: Product): boolean {
|
|
56
|
+
return product.inStock && product.price > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatPrice(product: Product): string {
|
|
60
|
+
return new Intl.NumberFormat('en-US', {
|
|
61
|
+
style: 'currency',
|
|
62
|
+
currency: 'USD',
|
|
63
|
+
}).format(product.price);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Violations:**
|
|
68
|
+
- Importing React, Next.js, or any framework
|
|
69
|
+
- Importing from `src/use-cases/`, `src/gateways/`, `src/components/`, or `src/lib/`
|
|
70
|
+
- HTTP calls or API operations
|
|
71
|
+
- Using non-deterministic global APIs like `crypto.randomUUID()`, `Date.now()`, or `Math.random()`
|
|
72
|
+
|
|
73
|
+
## Layer 2: Use Cases
|
|
74
|
+
|
|
75
|
+
**Location:** `src/use-cases/`
|
|
76
|
+
|
|
77
|
+
- MUST only import from entities and ports (interfaces)
|
|
78
|
+
- MUST define input/output DTOs
|
|
79
|
+
- MUST define ports for external dependencies
|
|
80
|
+
- MUST NOT import concrete implementations
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// src/use-cases/get-products.ts
|
|
84
|
+
import type { Product } from '../entities/product';
|
|
85
|
+
|
|
86
|
+
export interface GetProductsInput {
|
|
87
|
+
category?: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface GetProductsOutput {
|
|
92
|
+
products: Product[];
|
|
93
|
+
total: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Port - interface for the API gateway
|
|
97
|
+
export interface ProductApiGateway {
|
|
98
|
+
fetchProducts(category?: string, limit?: number): Promise<{ products: Product[]; total: number }>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class GetProductsUseCase {
|
|
102
|
+
constructor(private readonly productApi: ProductApiGateway) {}
|
|
103
|
+
|
|
104
|
+
async execute(input: GetProductsInput): Promise<GetProductsOutput> {
|
|
105
|
+
const { products, total } = await this.productApi.fetchProducts(
|
|
106
|
+
input.category,
|
|
107
|
+
input.limit
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { products, total };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Violations:**
|
|
116
|
+
- Importing React, Next.js, or any framework
|
|
117
|
+
- Importing from `gateways/`, `components/`, or `lib/`
|
|
118
|
+
- Making HTTP calls directly
|
|
119
|
+
|
|
120
|
+
## Layer 3: Adapters
|
|
121
|
+
|
|
122
|
+
**Location:** `src/gateways/`, `src/components/`, `src/hooks/`, and `src/lib/`
|
|
123
|
+
|
|
124
|
+
### Gateways (Backend-for-Frontend API Adapters)
|
|
125
|
+
|
|
126
|
+
Gateways act as the BFF layer, calling your backend API routes and transforming data for the frontend.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// src/gateways/product-api-gateway.ts
|
|
130
|
+
import { z } from 'zod';
|
|
131
|
+
import type { Product } from '@/entities/product';
|
|
132
|
+
import type { ProductApiGateway } from '@/use-cases/get-products';
|
|
133
|
+
|
|
134
|
+
// Schema for runtime validation of API responses
|
|
135
|
+
const ProductSchema = z.object({
|
|
136
|
+
id: z.string(),
|
|
137
|
+
name: z.string(),
|
|
138
|
+
price: z.number(),
|
|
139
|
+
inStock: z.boolean(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const ProductsResponseSchema = z.object({
|
|
143
|
+
products: z.array(ProductSchema),
|
|
144
|
+
total: z.number(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export function createProductApiGateway(baseUrl: string): ProductApiGateway {
|
|
148
|
+
return {
|
|
149
|
+
async fetchProducts(
|
|
150
|
+
category?: string,
|
|
151
|
+
limit?: number
|
|
152
|
+
): Promise<{ products: Product[]; total: number }> {
|
|
153
|
+
const params = new URLSearchParams();
|
|
154
|
+
if (category) params.set('category', category);
|
|
155
|
+
if (limit) params.set('limit', String(limit));
|
|
156
|
+
|
|
157
|
+
const response = await fetch(`${baseUrl}/api/products?${params}`);
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`Failed to fetch products: ${response.status}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const data: unknown = await response.json();
|
|
164
|
+
return ProductsResponseSchema.parse(data);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### React Hooks (Data Fetching Adapters)
|
|
171
|
+
|
|
172
|
+
Hooks wire use cases to React components and manage loading/error states. Use factory functions to enable testing with different implementations.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// src/hooks/use-products.ts
|
|
176
|
+
'use client';
|
|
177
|
+
|
|
178
|
+
import { useState, useEffect } from 'react';
|
|
179
|
+
import type { Product } from '@/entities/product';
|
|
180
|
+
import type { GetProductsUseCase } from '@/use-cases/get-products';
|
|
181
|
+
|
|
182
|
+
interface UseProductsDeps {
|
|
183
|
+
getProductsUseCase: GetProductsUseCase;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Factory to create a hook with injected dependencies
|
|
187
|
+
export function createUseProductsHook({ getProductsUseCase }: UseProductsDeps) {
|
|
188
|
+
return function useProducts(category?: string) {
|
|
189
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
190
|
+
const [loading, setLoading] = useState(true);
|
|
191
|
+
const [error, setError] = useState<Error | null>(null);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
setLoading(true);
|
|
195
|
+
getProductsUseCase
|
|
196
|
+
.execute({ category })
|
|
197
|
+
.then(({ products }) => setProducts(products))
|
|
198
|
+
.catch(setError)
|
|
199
|
+
.finally(() => setLoading(false));
|
|
200
|
+
}, [category]);
|
|
201
|
+
|
|
202
|
+
return { products, loading, error };
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/hooks/index.ts - Composition root for hooks
|
|
207
|
+
import { GetProductsUseCase } from '@/use-cases/get-products';
|
|
208
|
+
import { createProductApiGateway } from '@/gateways/product-api-gateway';
|
|
209
|
+
import { createUseProductsHook } from './use-products';
|
|
210
|
+
|
|
211
|
+
const productApi = createProductApiGateway('');
|
|
212
|
+
const getProductsUseCase = new GetProductsUseCase(productApi);
|
|
213
|
+
|
|
214
|
+
// Export the fully-wired hook for use in components
|
|
215
|
+
export const useProducts = createUseProductsHook({ getProductsUseCase });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### React Components (UI Adapters)
|
|
219
|
+
|
|
220
|
+
Components receive data as props and focus purely on presentation.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// src/components/features/product-list.tsx
|
|
224
|
+
'use client';
|
|
225
|
+
|
|
226
|
+
import type { Product } from '@/entities/product';
|
|
227
|
+
import { formatPrice, isAvailableForPurchase } from '@/entities/product';
|
|
228
|
+
|
|
229
|
+
interface ProductListProps {
|
|
230
|
+
products: Product[];
|
|
231
|
+
onAddToCart: (product: Product) => void;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function ProductList({ products, onAddToCart }: ProductListProps) {
|
|
235
|
+
return (
|
|
236
|
+
<ul>
|
|
237
|
+
{products.map((product) => (
|
|
238
|
+
<li key={product.id}>
|
|
239
|
+
<span>{product.name}</span>
|
|
240
|
+
<span>{formatPrice(product)}</span>
|
|
241
|
+
<button
|
|
242
|
+
onClick={() => onAddToCart(product)}
|
|
243
|
+
disabled={!isAvailableForPurchase(product)}
|
|
244
|
+
>
|
|
245
|
+
Add to Cart
|
|
246
|
+
</button>
|
|
247
|
+
</li>
|
|
248
|
+
))}
|
|
249
|
+
</ul>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Violations:**
|
|
255
|
+
- Business logic (validation rules, pricing calculations)
|
|
256
|
+
- Domain decisions that should be in entities or use cases
|
|
257
|
+
- Direct API calls in components (use hooks instead)
|
|
258
|
+
|
|
259
|
+
## Layer 4: Infrastructure
|
|
260
|
+
|
|
261
|
+
**Location:** `src/app/` (Next.js App Router)
|
|
262
|
+
|
|
263
|
+
The BFF API routes act as a proxy layer between your SPA and backend services.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// src/app/api/products/route.ts
|
|
267
|
+
import { NextResponse } from 'next/server';
|
|
268
|
+
|
|
269
|
+
const BACKEND_API_URL = process.env.BACKEND_API_URL || 'https://api.example.com';
|
|
270
|
+
|
|
271
|
+
export async function GET(request: Request) {
|
|
272
|
+
const { searchParams } = new URL(request.url);
|
|
273
|
+
const category = searchParams.get('category');
|
|
274
|
+
const limit = searchParams.get('limit');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Proxy to backend service
|
|
278
|
+
const backendUrl = new URL('/v1/products', BACKEND_API_URL);
|
|
279
|
+
if (category) backendUrl.searchParams.set('category', category);
|
|
280
|
+
if (limit) backendUrl.searchParams.set('limit', limit);
|
|
281
|
+
|
|
282
|
+
const response = await fetch(backendUrl.toString(), {
|
|
283
|
+
headers: {
|
|
284
|
+
'Authorization': `Bearer ${process.env.BACKEND_API_KEY}`,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
throw new Error(`Backend error: ${response.status}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const data = await response.json();
|
|
293
|
+
return NextResponse.json(data);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return NextResponse.json(
|
|
296
|
+
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
|
297
|
+
{ status: 500 }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Page Components (Composition Root)
|
|
304
|
+
|
|
305
|
+
Pages wire together hooks and components.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// src/app/products/page.tsx
|
|
309
|
+
'use client';
|
|
310
|
+
|
|
311
|
+
import { useProducts } from '@/hooks/use-products';
|
|
312
|
+
import { ProductList } from '@/components/features/product-list';
|
|
313
|
+
import type { Product } from '@/entities/product';
|
|
314
|
+
|
|
315
|
+
export default function ProductsPage() {
|
|
316
|
+
const { products, loading, error } = useProducts();
|
|
317
|
+
|
|
318
|
+
const handleAddToCart = (product: Product) => {
|
|
319
|
+
// Handle add to cart action
|
|
320
|
+
console.log('Added to cart:', product.name);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (loading) return <div>Loading...</div>;
|
|
324
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
325
|
+
|
|
326
|
+
return <ProductList products={products} onAddToCart={handleAddToCart} />;
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Dependency Injection Patterns
|
|
331
|
+
|
|
332
|
+
### Factory Functions (Preferred for SPA)
|
|
333
|
+
|
|
334
|
+
Factory functions create gateway instances with injected configuration.
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// ✅ Good - Factory function with dependency injection
|
|
338
|
+
export function createProductApiGateway(baseUrl: string): ProductApiGateway {
|
|
339
|
+
return {
|
|
340
|
+
async fetchProducts(category?: string): Promise<{ products: Product[] }> {
|
|
341
|
+
const response = await fetch(`${baseUrl}/api/products`);
|
|
342
|
+
return response.json();
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Usage in tests
|
|
348
|
+
const mockGateway = createProductApiGateway('http://mock-api');
|
|
349
|
+
|
|
350
|
+
// ❌ Bad - Hardcoded URL (hard to test)
|
|
351
|
+
export const productApiGateway: ProductApiGateway = {
|
|
352
|
+
async fetchProducts(): Promise<{ products: Product[] }> {
|
|
353
|
+
const response = await fetch('/api/products'); // Hardcoded
|
|
354
|
+
return response.json();
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Constructor Injection for Classes
|
|
360
|
+
|
|
361
|
+
Use constructor injection when classes are preferred.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// ✅ Good - Constructor injection
|
|
365
|
+
export class GetProductsUseCase {
|
|
366
|
+
constructor(private readonly productApi: ProductApiGateway) {}
|
|
367
|
+
|
|
368
|
+
async execute(input: GetProductsInput): Promise<GetProductsOutput> {
|
|
369
|
+
return this.productApi.fetchProducts(input.category);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Import Rules Summary
|
|
375
|
+
|
|
376
|
+
| From | Entities | Use Cases | Gateways/Hooks/Components | App (Infrastructure) |
|
|
377
|
+
|------|----------|-----------|---------------------------|---------------------|
|
|
378
|
+
| Entities | ✓ | ✗ | ✗ | ✗ |
|
|
379
|
+
| Use Cases | ✓ | ✓ | ✗ | ✗ |
|
|
380
|
+
| Gateways/Hooks/Components | ✓ | ✓ | ✓ | ✗ |
|
|
381
|
+
| App (Infrastructure) | ✓ | ✓ | ✓ | ✓ |
|
|
382
|
+
|
|
383
|
+
## Anti-Patterns
|
|
384
|
+
|
|
385
|
+
**Anemic Domain Model:** Entities as data-only containers with logic in services. Put business rules in entities.
|
|
386
|
+
|
|
387
|
+
**Leaky Abstractions:** Gateways exposing fetch Response objects. Return domain types only.
|
|
388
|
+
|
|
389
|
+
**Business Logic in Components:** Authorization checks or validation in React components. Move to entities/use cases.
|
|
390
|
+
|
|
391
|
+
**Direct API Calls in Components:** Components making fetch calls directly. Use hooks or gateways.
|
|
392
|
+
|
|
393
|
+
## Code Review Checklist
|
|
394
|
+
|
|
395
|
+
- Entities have zero imports from other layers
|
|
396
|
+
- Use cases define ports for all external dependencies
|
|
397
|
+
- Gateways implement ports and handle API communication
|
|
398
|
+
- Hooks wire use cases to React lifecycle
|
|
399
|
+
- Components receive data as props, focus on presentation
|
|
400
|
+
- API routes act as BFF proxy layer
|
|
401
|
+
- Use cases testable with simple mocks (no HTTP)
|