@crossdelta/platform-sdk 0.4.0 → 0.4.1

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.
@@ -0,0 +1,234 @@
1
+ # AI Generation Rules for CLI Code Output
2
+
3
+ These rules define how AI-generated scaffolded services must be structured when produced by CLI tools.
4
+
5
+ ---
6
+
7
+ # 1. Commands Block (REQUIRED - MUST BE FIRST)
8
+
9
+ **EVERY GENERATION MUST START WITH A COMMANDS BLOCK:**
10
+
11
+ ```commands
12
+ pf new hono-micro services/my-service -y
13
+ ```
14
+
15
+ Rules:
16
+ - **ALWAYS** include this commands block as the FIRST thing in your output
17
+ - Use exactly the path provided by the user
18
+ - Do not alter naming or prepend paths
19
+ - The `-y` flag is REQUIRED to skip prompts
20
+
21
+ ---
22
+
23
+ # 2. Dependencies Block
24
+
25
+ ```dependencies
26
+ zod
27
+ @pusher/push-notifications-server
28
+ ```
29
+
30
+ Rules:
31
+ - One package per line.
32
+ - No versions.
33
+ - Only packages not included in the scaffold.
34
+
35
+ ---
36
+
37
+ # 3. Required Source Files
38
+
39
+ Generated services must include:
40
+
41
+ - `src/index.ts`
42
+ - `src/handlers/*.event.ts`
43
+ - `src/use-cases/*.use-case.ts`
44
+ - `src/use-cases/*.test.ts`
45
+ - `README.md`
46
+
47
+ Rules:
48
+ - Paths must be relative to the service root.
49
+ - Do NOT generate controller or module folders unless requested.
50
+
51
+ ---
52
+
53
+ # 4. Code Style
54
+
55
+ - Biome-compatible formatting.
56
+ - Strict TS.
57
+ - Arrow functions only.
58
+ - No semicolons.
59
+ - Minimal comments.
60
+ - Alphabetized imports.
61
+
62
+ ---
63
+
64
+ # 5. Service Structure
65
+
66
+ ### index.ts
67
+ **IMPORTANT:** Telemetry MUST be the first import!
68
+
69
+ ```ts
70
+ import '@crossdelta/telemetry'
71
+
72
+ import { consumeJetStreamEvents } from '@crossdelta/cloudevents/transports/nats'
73
+ import { Hono } from 'hono'
74
+
75
+ const port = Number(process.env.PORT || process.env.MY_SERVICE_PORT) || 4003
76
+ const app = new Hono()
77
+
78
+ app.get('/health', (c) => c.json({ status: 'ok' }))
79
+
80
+ // Start NATS consumer - handlers are auto-discovered
81
+ consumeJetStreamEvents({
82
+ stream: 'ORDERS',
83
+ subjects: ['orders.>'],
84
+ consumer: 'my-service',
85
+ discover: './src/handlers/**/*.event.ts',
86
+ })
87
+
88
+ Bun.serve({ port, fetch: app.fetch })
89
+ ```
90
+
91
+ Contains only:
92
+ - Telemetry import (MUST be first)
93
+ - Server setup (Hono)
94
+ - Health endpoint
95
+ - Event consumer registration (if consuming events)
96
+ - Port configuration from env vars
97
+
98
+ ---
99
+
100
+ # 6. Use-Case Rules
101
+ - All domain logic lives in `use-cases`.
102
+ - Pure functions preferred.
103
+ - No framework imports.
104
+ - Use inferred schema types.
105
+
106
+ ---
107
+
108
+ # 7. Event Handler Rules
109
+
110
+ Handlers MUST:
111
+ - Live under `src/handlers/*.event.ts`
112
+ - Use Zod for validation (NO `type` field in schema!)
113
+ - Export inferred types for use in use-cases
114
+ - Delegate ALL logic to use-cases
115
+ - Contain NO business logic
116
+
117
+ **CRITICAL:** Schema validates ONLY the event data payload (without `type` field). Event type is declared in the options object.
118
+
119
+ Example:
120
+ ```ts
121
+ import { handleEvent } from '@crossdelta/cloudevents'
122
+ import { z } from 'zod'
123
+ import { processOrder } from '../use-cases/process-order.use-case'
124
+
125
+ const OrderCreatedSchema = z.object({
126
+ orderId: z.string(),
127
+ customerId: z.string(),
128
+ total: z.number(),
129
+ })
130
+
131
+ // Export type for use in use-cases
132
+ export type OrderCreatedEvent = z.infer<typeof OrderCreatedSchema>
133
+
134
+ export default handleEvent(
135
+ {
136
+ schema: OrderCreatedSchema,
137
+ type: 'orders.created', // Event type here, NOT in schema
138
+ },
139
+ async (data) => {
140
+ await processOrder(data)
141
+ },
142
+ )
143
+ ```
144
+
145
+ **Naming conventions:**
146
+ - Schema constants: PascalCase with `Schema` suffix (e.g., `OrderCreatedSchema`)
147
+ - Exported types: PascalCase with `Event` suffix (e.g., `OrderCreatedEvent`)
148
+
149
+ Forbidden in handlers:
150
+ - `type: z.literal('...')` in schema (redundant and causes errors)
151
+ - DB access
152
+ - External API calls
153
+ - Multi-step logic
154
+ - Env var parsing
155
+
156
+ ---
157
+
158
+ # 8. Testing Rules
159
+
160
+ **CRITICAL:**
161
+ - Test ONLY use-cases (NOT event handlers)
162
+ - Use Bun's native test runner: `import { describe, expect, it, beforeEach, afterEach } from 'bun:test'`
163
+ - **NO mocking available** - Bun does NOT have `vi`, `mock`, `jest.fn()`, or module mocking
164
+ - Focus on validation (input params, env vars) and error handling
165
+ - Keep tests simple - avoid complex scenarios
166
+
167
+ **What to test:**
168
+ - ✅ Input validation (missing/empty parameters)
169
+ - ✅ Environment variable validation
170
+ - ✅ Error messages and error types
171
+ - ❌ External API integrations (Pusher, AWS, etc.)
172
+ - ❌ Complex mocking scenarios
173
+
174
+ Example:
175
+ ```ts
176
+ import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
177
+ import { sendNotification } from './send-notification.use-case'
178
+
179
+ describe('Send Notification Use Case', () => {
180
+ const originalEnv = process.env
181
+
182
+ beforeEach(() => {
183
+ process.env = { ...originalEnv }
184
+ })
185
+
186
+ afterEach(() => {
187
+ process.env = originalEnv
188
+ })
189
+
190
+ it('should throw error if orderId is missing', async () => {
191
+ await expect(
192
+ sendNotification({ orderId: '', customerId: 'cust-1' })
193
+ ).rejects.toThrow('Missing orderId')
194
+ })
195
+
196
+ it('should throw error if credentials not set', async () => {
197
+ delete process.env.PUSHER_BEAMS_INSTANCE_ID
198
+
199
+ await expect(
200
+ sendNotification({ orderId: '123', customerId: 'cust-1' })
201
+ ).rejects.toThrow('Missing Pusher Beams credentials')
202
+ })
203
+ })
204
+ ```
205
+
206
+ ---
207
+
208
+ # 9. README Requirements
209
+
210
+ MUST include:
211
+ - Description
212
+ - Environment variable table
213
+ - Published/consumed events
214
+ - API endpoints
215
+ - Dev commands
216
+
217
+ ---
218
+
219
+ # 10. Absolute Rules
220
+
221
+ DO NOT:
222
+ - Generate full rewrites
223
+ - Add new architecture concepts
224
+ - Use raw NATS clients
225
+ - Put logic in handlers or index.ts
226
+ - Insert semicolons
227
+ - Add unused folders/files
228
+
229
+ DO:
230
+ - Produce minimal, clean code
231
+ - Follow structure and conventions strictly
232
+ - Keep output compact and correct
233
+
234
+ ---
@@ -195,30 +195,180 @@ export const ordersCollectionDefinition = defineCollection({
195
195
 
196
196
  ## Infrastructure (Pulumi + Kubernetes)
197
197
 
198
- Service configs in `infra/services/<name>.ts`:
198
+ ### Modern Port Configuration (Fluent API)
199
+
200
+ **Use the fluent `ports()` builder** for defining service ports:
199
201
 
200
202
  ```ts
203
+ import { ports } from '@crossdelta/infrastructure'
204
+ import type { K8sServiceConfig } from '@crossdelta/infrastructure'
205
+
206
+ // Simple internal HTTP service
201
207
  const config: K8sServiceConfig = {
202
208
  name: 'orders',
203
- containerPort: 4001,
209
+ ports: ports().http(4001).build(),
204
210
  replicas: 1,
205
- healthCheck: { port: 4001 },
206
- resources: { requests: { cpu: '50m', memory: '64Mi' }, limits: { cpu: '150m', memory: '128Mi' } },
211
+ healthCheck: { httpPath: '/health' }, // HTTP health check endpoint
212
+ resources: {
213
+ requests: { cpu: '50m', memory: '64Mi' },
214
+ limits: { cpu: '150m', memory: '128Mi' }
215
+ },
207
216
  env: { PUBLIC_SUPABASE_URL: supabaseUrl },
208
217
  secrets: { SUPABASE_SERVICE_ROLE_KEY: supabaseServiceRoleKey },
209
218
  }
219
+
220
+ // Public HTTP service
221
+ const publicConfig: K8sServiceConfig = {
222
+ name: 'api-gateway',
223
+ ports: ports().http(4000).public().build(),
224
+ ingress: { path: '/api', host: 'api.example.com' },
225
+ }
226
+
227
+ // Service with multiple ports
228
+ const natsConfig: K8sServiceConfig = {
229
+ name: 'nats',
230
+ ports: ports()
231
+ .primary(4222, 'client')
232
+ .addHttp(8222, 'monitoring').public()
233
+ .add(6222, 'routing')
234
+ .build(),
235
+ }
210
236
  ```
211
237
 
238
+ **Port Builder Methods:**
239
+ - `.http(port)` - HTTP primary port
240
+ - `.https(port)` - HTTPS primary port
241
+ - `.grpc(port)` - gRPC primary port
242
+ - `.primary(port, name?)` - Custom primary port
243
+ - `.add(port, name?, protocol?)` - Add additional port
244
+ - `.addHttp(port, name?)` - Add HTTP additional port
245
+ - `.addGrpc(port, name?)` - Add gRPC additional port
246
+ - `.public()` - Mark last port as public (exposed via ingress)
247
+ - `.protocol(type)` - Set protocol for last port
248
+ - `.build()` - Build final config
249
+
250
+ **Legacy `containerPort` is deprecated** - use the fluent API instead.
251
+
252
+ ### Health Checks
253
+
254
+ Services should expose health check endpoints:
255
+
256
+ ```ts
257
+ // HTTP health check (recommended)
258
+ healthCheck: {
259
+ httpPath: '/health', // GET endpoint
260
+ initialDelaySeconds: 10, // Wait before first check
261
+ periodSeconds: 10, // Check interval
262
+ }
263
+
264
+ // For services without HTTP endpoints, Kubernetes will use TCP checks automatically
265
+ // based on the primary port
266
+ ```
267
+
268
+ Health check endpoint implementation:
269
+
270
+ ```ts
271
+ app.get('/health', (c) => c.json({ status: 'ok' }))
272
+ ```
273
+
274
+ ### Smart Service Discovery
275
+
276
+ **Use `discoverServiceConfigs()` for automatic service discovery:**
277
+
278
+ ```ts
279
+ import { discoverServiceConfigs, discoverServiceConfigsWithOptions } from '@crossdelta/infrastructure'
280
+
281
+ // Simple discovery (backward compatible)
282
+ const configs = discoverServiceConfigs('services')
283
+
284
+ // Advanced discovery with filtering
285
+ const result = discoverServiceConfigsWithOptions({
286
+ servicesDir: 'services',
287
+ filter: /^api-/, // Pattern matching
288
+ exclude: ['test-service'], // Exclude services
289
+ env: { // Inject env vars
290
+ NODE_ENV: 'production',
291
+ NATS_URL: natsUrl,
292
+ },
293
+ validate: true, // Port conflict checks
294
+ })
295
+ ```
296
+
297
+ **Discovery Options:**
298
+ - `filter` - Regex or string pattern to match service names
299
+ - `include` - Array of service names to include
300
+ - `exclude` - Array of service names to exclude
301
+ - `tags` - Filter by service tags
302
+ - `env` - Inject environment variables into all services
303
+ - `validate` - Enable validation (port conflicts, missing fields)
304
+ - `sort` - Sort services by name (default: true)
305
+ - `registry` - Custom registry for auto-generated images
306
+
212
307
  Follow existing configs in `infra/services/`. Don't invent new providers or patterns.
213
308
 
214
309
  ## Code Style & Conventions
215
310
 
216
- - **Biome:** Single quotes, no semicolons, 2-space indent, 120 char width
311
+ ### Biome Configuration
312
+
313
+ **CRITICAL:** All code MUST follow the Biome rules configured in the root `biome.json`:
314
+
315
+ - **Formatting:** Single quotes, no semicolons, 2-space indent, 120 char width, trailing commas
316
+ - **Import Organization:** Imports and exports MUST be sorted alphabetically (use `organizeImports: "on"`)
317
+ - **Unused Imports:** Remove all unused imports (lint error)
318
+ - **Code Quality:** Follow all Biome linter rules (security, style, complexity)
319
+
320
+ **Always run before committing:**
321
+ ```bash
322
+ bun lint # Check for issues
323
+ bun format # Auto-fix formatting and organize imports
324
+ ```
325
+
326
+ **In your editor:** Enable Biome's "Organize Imports on Save" for automatic sorting.
327
+
328
+ ### General Conventions
329
+
330
+ - **Functions:** Prefer **arrow functions** and **functional programming patterns** (map, filter, reduce) over imperative loops
331
+ - **Immutability:** Use `const` over `let`, avoid mutations, prefer spread operators and array methods
332
+ - **Composition:** Small, composable functions over large classes
217
333
  - **Ports:** Always `process.env.PORT || process.env.SERVICE_PORT || default`
218
334
  - **Health:** All services expose `GET /health → { status: 'ok' }`
219
335
  - **Commits:** Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`)
220
336
  - **Tests:** Use `bun:test` — see `packages/cloudevents/test/*.test.ts` for patterns
221
337
 
338
+ ### Functional Programming Examples
339
+
340
+ **Prefer:**
341
+ ```ts
342
+ // Arrow functions
343
+ const add = (a: number, b: number) => a + b
344
+
345
+ // Map/filter/reduce over loops
346
+ const activeUsers = users.filter(user => user.active)
347
+ const userNames = users.map(user => user.name)
348
+ const totalAge = users.reduce((sum, user) => sum + user.age, 0)
349
+
350
+ // Composition
351
+ const processData = (data: Data[]) =>
352
+ data
353
+ .filter(isValid)
354
+ .map(transform)
355
+ .reduce(aggregate, initialValue)
356
+ ```
357
+
358
+ **Avoid:**
359
+ ```ts
360
+ // Function declarations (unless needed for hoisting)
361
+ function add(a: number, b: number) { return a + b }
362
+
363
+ // Imperative loops
364
+ const activeUsers = []
365
+ for (let i = 0; i < users.length; i++) {
366
+ if (users[i].active) {
367
+ activeUsers.push(users[i])
368
+ }
369
+ }
370
+ ```
371
+
222
372
  ## Key Files Reference
223
373
 
224
374
  | Pattern | Example |
@@ -104,18 +104,11 @@ jobs:
104
104
  registry-url: https://registry.npmjs.org
105
105
 
106
106
  - name: Install dependencies
107
- env:
108
- NPM_TOKEN: ${{ secrets.NPMJS_TOKEN }}
109
- run: |
110
- # Copy package to isolated directory to avoid workspace resolution
111
- mkdir -p /tmp/publish-pkg
112
- cp -r ${{ matrix.package.dir }}/* /tmp/publish-pkg/
113
- cd /tmp/publish-pkg
114
- bun install
107
+ run: bun install
115
108
 
116
109
  - name: Bump version (only if already published)
117
110
  id: bump
118
- working-directory: /tmp/publish-pkg
111
+ working-directory: ${{ matrix.package.dir }}
119
112
  run: |
120
113
  CURRENT=$(jq -r '.version' package.json)
121
114
  PUBLISHED=$(npm view "${{ matrix.package.name }}" version 2>/dev/null || echo "0.0.0")
@@ -134,15 +127,15 @@ jobs:
134
127
  fi
135
128
 
136
129
  - name: Build
137
- working-directory: /tmp/publish-pkg
130
+ working-directory: ${{ matrix.package.dir }}
138
131
  run: bun run --if-present build
139
132
 
140
133
  - name: Test
141
- working-directory: /tmp/publish-pkg
134
+ working-directory: ${{ matrix.package.dir }}
142
135
  run: bun run --if-present test
143
136
 
144
137
  - name: Publish
145
- working-directory: /tmp/publish-pkg
138
+ working-directory: ${{ matrix.package.dir }}
146
139
  env:
147
140
  NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }}
148
141
  run: npm publish --access=public
@@ -152,8 +145,6 @@ jobs:
152
145
  env:
153
146
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154
147
  run: |
155
- # Copy bumped version back to repo
156
- cp /tmp/publish-pkg/package.json ${{ matrix.package.dir }}/package.json
157
148
  git config user.name "github-actions[bot]"
158
149
  git config user.email "github-actions[bot]@users.noreply.github.com"
159
150
  git add "${{ matrix.package.dir }}/package.json"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossdelta/platform-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI toolkit for scaffolding Turborepo workspaces with Pulumi infrastructure and Hono/NestJS microservices",
5
5
  "keywords": [
6
6
  "cli",
@@ -27,6 +27,7 @@
27
27
  "files": [
28
28
  "bin/**/*.js",
29
29
  "bin/**/templates/**",
30
+ "bin/**/instructions/**",
30
31
  "bin/**/*.json",
31
32
  "!bin/**/*.map",
32
33
  "dist/",
@@ -46,7 +47,7 @@
46
47
  "scripts": {
47
48
  "start:dev": "node esbuild.config.mjs --watch",
48
49
  "build:cli": "node esbuild.config.mjs",
49
- "build:cli:copy": "cp cli/integration.collection.json bin/integration.collection.json && rm -rf bin/templates && mkdir -p bin/templates && cp -r cli/src/commands/create/workspace/templates bin/templates/workspace && cp -r cli/src/commands/create/hono-microservice/templates bin/templates/hono-microservice && cp -r cli/src/commands/create/nest-microservice/templates bin/templates/nest-microservice",
50
+ "build:cli:copy": "cp cli/integration.collection.json bin/integration.collection.json && rm -rf bin/templates && mkdir -p bin/templates && cp -r cli/src/commands/create/workspace/templates bin/templates/workspace && cp -r cli/src/commands/create/hono-microservice/templates bin/templates/hono-microservice && cp -r cli/src/commands/create/nest-microservice/templates bin/templates/nest-microservice && mkdir -p bin/services/ai/instructions && cp cli/src/services/ai/instructions/ai-instructions.md bin/services/ai/instructions/",
50
51
  "build:schematics:transpile": "tsc --project tsconfig.schematics.json",
51
52
  "build:schematics:copy": "cp -R schematics/* dist/schematics && find dist/schematics -name '*.ts' -delete",
52
53
  "build:schematics": "npm run build:schematics:transpile && npm run build:schematics:copy",
@@ -103,7 +104,7 @@
103
104
  "zx": "^8.5.3"
104
105
  },
105
106
  "peerDependencies": {
106
- "@crossdelta/infrastructure": "^0.2.2",
107
+ "@crossdelta/infrastructure": "workspace:*",
107
108
  "@nestjs/schematics": "^11.0.5",
108
109
  "turbo": "^2.0.0"
109
110
  },