@crossdelta/platform-sdk 0.19.0 → 0.19.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.
Files changed (46) hide show
  1. package/README.md +27 -5
  2. package/bin/chunk-634PL24Z.mjs +20 -0
  3. package/bin/cli.mjs +604 -0
  4. package/bin/config-CKQHYOF4.mjs +2 -0
  5. package/bin/docs/generators/code-style.md +79 -0
  6. package/bin/docs/generators/natural-language.md +117 -0
  7. package/bin/docs/generators/service.md +129 -60
  8. package/bin/templates/hono-microservice/Dockerfile.hbs +3 -1
  9. package/bin/templates/hono-microservice/src/config/env.ts.hbs +3 -0
  10. package/bin/templates/nest-microservice/Dockerfile.hbs +6 -2
  11. package/bin/templates/nest-microservice/src/config/env.ts.hbs +17 -0
  12. package/bin/templates/nest-microservice/src/main.ts.hbs +2 -1
  13. package/bin/templates/workspace/.github/actions/prepare-build-context/action.yml +58 -6
  14. package/bin/templates/workspace/.github/workflows/build-and-deploy.yml.hbs +25 -3
  15. package/bin/templates/workspace/.github/workflows/publish-packages.yml +6 -8
  16. package/bin/templates/workspace/biome.json.hbs +4 -1
  17. package/bin/templates/workspace/infra/package.json.hbs +2 -2
  18. package/bin/templates/workspace/package.json.hbs +1 -0
  19. package/bin/templates/workspace/packages/contracts/README.md.hbs +5 -5
  20. package/bin/templates/workspace/packages/contracts/package.json.hbs +15 -6
  21. package/bin/templates/workspace/packages/contracts/src/index.ts +1 -1
  22. package/bin/templates/workspace/packages/contracts/tsconfig.json.hbs +6 -1
  23. package/bin/templates/workspace/turbo.json +8 -11
  24. package/bin/templates/workspace/turbo.json.hbs +6 -5
  25. package/dist/facade.d.mts +840 -0
  26. package/dist/facade.d.ts +840 -0
  27. package/dist/facade.js +2294 -0
  28. package/dist/facade.js.map +1 -0
  29. package/dist/facade.mjs +2221 -0
  30. package/dist/facade.mjs.map +1 -0
  31. package/dist/plugin-types-DQOv97Zh.d.mts +180 -0
  32. package/dist/plugin-types-DQOv97Zh.d.ts +180 -0
  33. package/dist/plugin-types.d.mts +1 -0
  34. package/dist/plugin-types.d.ts +1 -0
  35. package/dist/plugin-types.js +19 -0
  36. package/dist/plugin-types.js.map +1 -0
  37. package/dist/plugin-types.mjs +1 -0
  38. package/dist/plugin-types.mjs.map +1 -0
  39. package/dist/plugin.d.mts +31 -0
  40. package/dist/plugin.d.ts +31 -0
  41. package/dist/plugin.js +105 -0
  42. package/dist/plugin.js.map +1 -0
  43. package/dist/plugin.mjs +75 -0
  44. package/dist/plugin.mjs.map +1 -0
  45. package/package.json +118 -99
  46. package/bin/cli.js +0 -540
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{A as g,B as h,C as i,D as j,E as k,F as l,G as m,H as n,u as a,v as b,w as c,x as d,y as e,z as f}from"./chunk-634PL24Z.mjs";export{n as discoverAvailableServices,b as findWorkspaceRoot,f as getContractsConfig,m as getGeneratorConfig,h as getPackageJsonField,e as getPfConfig,k as getPkgJson,g as getRootPackageScope,c as getWorkspacePackageJson,d as getWorkspacePathsConfig,a as isInWorkspace,l as pkgJson,j as readPackageConfig,i as updatePackageJsonField};
@@ -94,3 +94,82 @@ z.string().datetime()
94
94
  // ❌ Deprecated
95
95
  z.string().email('Invalid')
96
96
  ```
97
+
98
+ ---
99
+
100
+ ## Type-Safe External Package Integration
101
+
102
+ **NEVER use `any` types** - always create explicit type definitions for packages without TypeScript support.
103
+
104
+ ### Guidelines
105
+
106
+ - ✅ Create type definitions based on API documentation
107
+ - ✅ Prefer `type` over `interface` for simple object shapes
108
+ - ✅ Use `const` with direct initialization
109
+ - ✅ Use type assertions (`as Type`) only after explicit types
110
+ - ❌ Never use `any`
111
+ - ❌ Never use `let` + null + conditional initialization
112
+
113
+ ### Example (Pusher Beams)
114
+
115
+ ```ts
116
+ // ❌ WRONG - Using any
117
+ let client: any = new PushNotifications({ ... })
118
+
119
+ // ❌ WRONG - let + null + conditional
120
+ let client: BeamsClient | null = null
121
+ if (!client) {
122
+ client = new PushNotifications({ ... }) as BeamsClient
123
+ }
124
+
125
+ // ✅ CORRECT - Explicit types + const + direct initialization
126
+ type BeamsClient = {
127
+ publishToUsers: (userIds: string[], request: BeamsPublishRequest) => Promise<PublishResponse>
128
+ }
129
+
130
+ type BeamsPublishRequest = {
131
+ web?: {
132
+ notification?: {
133
+ title: string
134
+ body: string
135
+ deep_link?: string
136
+ }
137
+ }
138
+ }
139
+
140
+ type PublishResponse = {
141
+ publishId: string
142
+ }
143
+
144
+ const getBeamsClient = (): BeamsClient => {
145
+ if (!env.PUSHER_INSTANCE_ID || !env.PUSHER_SECRET_KEY) {
146
+ throw new Error('Missing Pusher Beams credentials')
147
+ }
148
+
149
+ return new PushNotifications({
150
+ instanceId: env.PUSHER_INSTANCE_ID,
151
+ secretKey: env.PUSHER_SECRET_KEY,
152
+ }) as BeamsClient
153
+ }
154
+ ```
155
+
156
+ ### Why `type` over `interface`?
157
+
158
+ - More flexible (unions, intersections, mapped types)
159
+ - More concise for simple object shapes
160
+ - Better for functional programming patterns
161
+ - Standard in modern TypeScript codebases
162
+
163
+ ```ts
164
+ // ✅ CORRECT - type
165
+ type User = {
166
+ id: string
167
+ name: string
168
+ }
169
+
170
+ // ❌ AVOID - interface (only use for extensible APIs)
171
+ interface User {
172
+ id: string
173
+ name: string
174
+ }
175
+ ```
@@ -0,0 +1,117 @@
1
+ # Natural Language Service Generation
2
+
3
+ Generate services from natural language prompts using the capabilities system.
4
+
5
+ ## Quick Examples
6
+
7
+ ### 1. Freeform German
8
+ ```
9
+ Erstelle notification-service, hört auf order.created, sendet Push via Pusher Beams an interest order-updates.
10
+ ```
11
+
12
+ ### 2. Semi-Structured
13
+ ```
14
+ Service: notification-service | When: order.created | Do: push via pusher beams | Target: interest order-updates
15
+ ```
16
+
17
+ ### 3. HTTP Integration
18
+ ```
19
+ Erstelle shipping-service, hört auf order.paid, ruft HTTP POST /shipments bei https://api.example.com auf.
20
+ ```
21
+
22
+ ## How It Works
23
+
24
+ The capabilities pipeline parses your prompt:
25
+
26
+ ```
27
+ NL Prompt
28
+
29
+ ▼ parseServicePrompt()
30
+ ServiceSpec { serviceName, triggers, actions }
31
+
32
+ ▼ lowerSpecToCapabilities()
33
+ CapabilityInvocation[]
34
+
35
+ ▼ registry.planAll()
36
+ GenerationPlan (deterministic paths from policy)
37
+
38
+ ▼ generateFileContents()
39
+ Generated Files
40
+ ```
41
+
42
+ **Key:** AI extracts intent, but paths and structure come from policy—not AI.
43
+
44
+ ## Optional Defaults via package.json
45
+
46
+ Configure workspace defaults in `package.json`:
47
+
48
+ ```json
49
+ {
50
+ "pf": {
51
+ "capabilities": {
52
+ "notifier": {
53
+ "defaults": {
54
+ "push": "pusher-beams",
55
+ "email": "resend"
56
+ },
57
+ "providers": {
58
+ "push": {
59
+ "custom-provider": {
60
+ "dependencies": { "custom-sdk": "^1.0.0" },
61
+ "envVars": [
62
+ { "key": "CUSTOM_API_KEY", "description": "API key", "required": true }
63
+ ]
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ **Important:** Defaults are only applied when the prompt doesn't specify a provider. Explicit mentions in the prompt always win.
74
+
75
+ ## Supported Capabilities
76
+
77
+ | Capability | Trigger/Action | Example |
78
+ |------------|----------------|---------|
79
+ | `event-consumer` | Trigger | "listens to order.created" |
80
+ | `event-publisher` | Action | "emits order.shipped event" |
81
+ | `notifier` | Action | "sends push via pusher beams" |
82
+ | `http-endpoint` | Trigger | "POST /webhooks" |
83
+
84
+ ## Built-in Providers
85
+
86
+ ### Push Notifications
87
+ - `pusher-beams` - Pusher Beams
88
+ - `firebase` - Firebase Cloud Messaging
89
+ - `onesignal` - OneSignal
90
+ - `expo` - Expo Push Notifications
91
+
92
+ ### Email
93
+ - `resend` - Resend
94
+ - `sendgrid` - SendGrid
95
+ - `ses` - AWS SES
96
+ - `postmark` - Postmark
97
+
98
+ ### Slack
99
+ - `slack-webhook` - Slack Webhook
100
+ - `slack-api` - Slack Web API
101
+
102
+ ### SMS
103
+ - `twilio` - Twilio
104
+ - `vonage` - Vonage
105
+
106
+ ## Provider Detection
107
+
108
+ The parser detects providers from your prompt:
109
+
110
+ ```typescript
111
+ // These all detect "pusher-beams":
112
+ "send push via Pusher Beams"
113
+ "push notification using pusher-beams"
114
+ "notify with PusherBeams"
115
+ ```
116
+
117
+ If no provider is detected and no default is configured, elicitation prompts for selection.
@@ -7,6 +7,45 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## 🚨 pf MCP Flow (VERBINDLICH)
11
+
12
+ ### Architektur-Entscheidung
13
+
14
+ Der `pf` MCP-Server ist ein **Planner**, nicht ein Code-Generator. Die AI (Copilot) generiert den Code.
15
+
16
+ ### Korrekter Flow
17
+
18
+ ```
19
+ 1. AI sammelt Kontext → mcp_pf_scan_workspace()
20
+ 2. AI ruft Generate auf → mcp_pf_generate_service(dryRun: true, prompt: "...")
21
+ 3. pf liefert Plan → { files: [...], postCommands: ["pf cloudevents add ..."], ... }
22
+ 4. 🆕 MCP verifiziert NPM → Automatisch via npm registry API
23
+ 5. 🆕 Falls Korrekturen → In diagnostics: "📦 Corrected pkg: ^1.3.3 → ^1.2.7"
24
+ 6. AI führt postCommands aus → `pf cloudevents add order.created --service services/my-service`
25
+ 7. AI generiert Code → NUR für adapters/use-cases, NICHT für handlers!
26
+ 8. User OK Gate → User bestätigt Plan mit korrekten Versionen
27
+ 9. AI ruft Apply auf → mcp_pf_apply_changes(changes: [...])
28
+ ```
29
+
30
+ ### ✅ DO (korrekt)
31
+
32
+ - `mcp_pf_generate_service(dryRun: true)` → Plan holen
33
+ - **`postCommands` ausführen** → `pf cloudevents add` erstellt Handler automatisch!
34
+ - AI generiert nur adapters/use-cases basierend auf `files[].intent`
35
+ - `mcp_pf_apply_changes()` nur nach User-OK
36
+ - `expectedPlanHash` für Drift-Detection nutzen
37
+
38
+ ### ❌ DON'T (verboten)
39
+
40
+ - **KEINE Handler selbst schreiben!** → `pf cloudevents add` benutzen
41
+ - Kein `dryRun: false` ohne vorherige Preview
42
+ - Kein Code in Capabilities einbauen (nur `intent` + `inputs`)
43
+ - Kein "builtin provider catalogue" mit Templates
44
+ - Keine direkten File-Writes während Generate
45
+ - Kein Package.json direkt editieren wenn `deps:add` Effect existiert
46
+
47
+ ---
48
+
10
49
  ## 🚨 CRITICAL: Port Configuration
11
50
 
12
51
  Services MUST read their port from environment variables using this pattern:
@@ -106,10 +145,10 @@ pf new hono-micro services/order-processing -y
106
145
  ```
107
146
 
108
147
  ```post-commands
109
- pf event add order.created --service services/order-processing
148
+ pf cloudevents add order.created --service services/order-processing
110
149
  ```
111
150
 
112
- **What `pf event add` does:**
151
+ **What `pf cloudevents add` does:**
113
152
  - Creates `packages/contracts/src/events/orders/created.mock.json` (test mock)
114
153
  - Adds export to `packages/contracts/src/index.ts`
115
154
  - Skips contract creation if already exists (AI's schema is preserved!)
@@ -119,10 +158,10 @@ pf event add order.created --service services/order-processing
119
158
  **Step 1: Create contract with CLI**
120
159
  ```bash
121
160
  # Create contract with schema
122
- pf event add order.created --fields "orderId:string,total:number,customerId:string"
161
+ pf cloudevents add order.created --fields "orderId:string,total:number,customerId:string"
123
162
 
124
163
  # Or with JSON schema
125
- pf event add order.created --schema '{"orderId":"string","total":"number"}'
164
+ pf cloudevents add order.created --schema '{"orderId":"string","total":"number"}'
126
165
  ```
127
166
 
128
167
  **Step 2: Scaffold service**
@@ -132,7 +171,7 @@ pf new hono-micro services/order-processing
132
171
 
133
172
  **Step 3: Register event and create handler**
134
173
  ```bash
135
- pf event add order.created --service services/order-processing
174
+ pf cloudevents add order.created --service services/order-processing
136
175
  # Creates: src/events/order-created.handler.ts (singular!)
137
176
  ```
138
177
 
@@ -172,6 +211,57 @@ pf dev # Starts NATS + creates ephemeral streams from contracts + all services
172
211
 
173
212
  **Stream Creation:** `pf dev` scans `packages/contracts/src/index.ts` for contracts with `channel.stream` metadata and creates ephemeral streams automatically. No manual stream creation needed!
174
213
 
214
+ ---
215
+
216
+ ## 🚨 CRITICAL: Package Version Verification
217
+
218
+ **Package versions are automatically verified and corrected by the MCP server.**
219
+
220
+ ### The Problem
221
+
222
+ AI training data is outdated → may suggest versions that don't exist:
223
+
224
+ ```bash
225
+ ❌ "@pusher/push-notifications-server": "^1.3.3" # Doesn't exist!
226
+ ✅ "@pusher/push-notifications-server": "^1.2.7" # Actual latest
227
+ ```
228
+
229
+ ### The Solution
230
+
231
+ The pf MCP server automatically:
232
+ 1. Detects package.json in generated files
233
+ 2. Fetches latest versions from npm registry
234
+ 3. Corrects mismatched versions
235
+ 4. Reports corrections in diagnostics
236
+
237
+ **Example Output:**
238
+ ```
239
+ 📦 Corrected @pusher/push-notifications-server: ^1.3.3 → ^1.2.7
240
+ ```
241
+
242
+ ### Implementation
243
+
244
+ Located in `packages/pf-mcp/src/tools/services-generate/npm-verifier.ts`:
245
+
246
+ - **Pure functions**: parsePackageJson, extractNpmPackages
247
+ - **IO adapter**: fetchNpmVersion (npm registry API)
248
+ - **Higher-order**: verifyAndCorrectVersions
249
+
250
+ **Integration point**: `mode-nl.ts` after scaffold generation, before returning plan
251
+
252
+ ### What Gets Verified
253
+
254
+ - ✅ All external npm packages
255
+ - ❌ Skips `workspace:*` packages (internal monorepo)
256
+ - ❌ Skips `@crossdelta/*` packages (platform packages)
257
+ - ❌ Skips `@orderboss/*` packages (workspace packages)
258
+
259
+ ### No AI Action Required
260
+
261
+ This is handled entirely by the MCP server - AI does not need to verify packages manually.
262
+
263
+ ---
264
+
175
265
  ### 4️⃣ **Deploy to Production**
176
266
 
177
267
  ```bash
@@ -249,15 +339,19 @@ The exact command depends on the framework - see the framework-specific docs:
249
339
  **For Event Consumer services, include a `post-commands` block with ALL events:**
250
340
 
251
341
  ```post-commands
252
- pf event add order.created --service services/my-service
253
- pf event add customer.updated --service services/my-service
342
+ pf cloudevents add order.created --service services/my-service --fields "id:string"
343
+ pf cloudevents add customer.updated --service services/my-service --fields "id:string"
254
344
  ```
255
345
 
256
- **What `pf event add` creates:**
257
- - `packages/contracts/src/events/<event>.mock.json` - Test mock data
346
+ **⚠️ CRITICAL: Always include `--fields` to skip interactive prompts!**
347
+
348
+ **What `pf cloudevents add` creates:**
349
+ - `packages/contracts/src/events/<domain>/<event>.ts` - Contract with schema
350
+ - `packages/contracts/src/events/<domain>/<event>.mock.json` - Test mock data
351
+ - `services/<service>/src/events/<event>.handler.ts` - Event handler
258
352
  - Adds export to `packages/contracts/src/index.ts`
259
353
 
260
- **⚠️ IMPORTANT:** `pf event add` skips contract creation if it already exists - so YOUR contract with the correct schema is preserved!
354
+ **⚠️ IMPORTANT:** `pf cloudevents add` skips contract creation if it already exists - so YOUR contract with the correct schema is preserved!
261
355
 
262
356
  ---
263
357
 
@@ -285,49 +379,33 @@ Export named 'OrderCreatedContract' not found in module 'packages/contracts/src/
285
379
 
286
380
  1. **`packages/contracts/src/events/<domain>/<event>.ts`** - Contract with correct schema fields in domain-grouped structure (e.g., `events/orders/created.ts`)
287
381
  2. **`packages/contracts/src/index.ts`** - ⚠️ **ADD EXPORT** for the contract (CRITICAL!)
288
- 3. **`src/events/<event>.handler.ts`** - Event handler that calls the use-case
289
- 4. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
290
- 5. **`src/use-cases/*.test.ts`** - Tests for use-cases
382
+ 3. **`src/use-cases/*.use-case.ts`** - Business logic (called by handlers)
383
+ 4. **`src/use-cases/*.test.ts`** - Tests for use-cases
384
+ 5. **`src/adapters/*.adapter.ts`** - External service integrations (if needed)
291
385
  6. **`README.md`** - Documentation
292
386
 
293
- **⚠️ DO NOT generate `src/index.ts` or `src/main.ts`** - These are created by `pf new` with correct port config!
294
-
295
- ### Handler Location (CRITICAL!)
296
-
297
- **⚠️ MUST be in `src/events/*.handler.ts` - NEVER in `handlers/` subdirectory**
298
-
299
- ```
300
- ✅ CORRECT:
301
- services/my-service/src/events/
302
- ├── order-created.handler.ts
303
- └── customer-updated.handler.ts
304
-
305
- ❌ WRONG:
306
- services/my-service/src/events/handlers/ # NEVER create!
307
- ```
308
-
309
- ### Handler Export Pattern
387
+ **⚠️ DO NOT generate these files** - They are created by CLI commands:
388
+ - `src/index.ts` or `src/main.ts` - Created by `pf new`
389
+ - `src/events/*.handler.ts` - Created by `pf cloudevents add`
310
390
 
311
- ```ts
312
- // ✅ CORRECT: Default export
313
- export default handleEvent(OrdersCreatedContract, async (data) => { ... })
391
+ ### Handler Creation (CRITICAL!)
314
392
 
315
- // WRONG: Named export
316
- export const OrdersCreatedHandler = handleEvent(...)
317
- ```
393
+ **Handlers are created by `pf cloudevents add`, NOT by AI!**
318
394
 
319
- ### Handler Logging
320
-
321
- **Handlers must be thin** - use `console.log` for logging, then delegate to use-case/service:
322
-
323
- ```ts
324
- export default handleEvent(OrdersCreatedContract, async (data) => {
325
- console.log(`[order.created] Processing orderId=${data.orderId}`) // ✅ console.log
326
- await processOrder(data)
327
- })
395
+ ```bash
396
+ # This command creates the handler automatically:
397
+ pf cloudevents add order.created --service services/my-service --fields "id:string"
398
+ # → Creates: src/events/order-created.handler.ts
399
+ # → Creates: packages/contracts/src/events/orders/created.mock.json
400
+ # Updates: packages/contracts/src/index.ts (adds export)
328
401
  ```
329
402
 
330
- **Services/use-cases** can use structured logging (NestJS: `new Logger(ServiceName.name)`).
403
+ **Why `pf cloudevents add` instead of AI generation?**
404
+ - ✅ Consistent handler structure
405
+ - ✅ Correct imports from contracts
406
+ - ✅ Proper mock file generation
407
+ - ✅ Contract export management
408
+ - ✅ No hallucinated code
331
409
 
332
410
  ---
333
411
 
@@ -362,18 +440,6 @@ export const OrdersCreatedContract = createContract({
362
440
  export * from './events/orders/created'
363
441
  ```
364
442
 
365
- #### `src/events/order-created.handler.ts`
366
- ```ts
367
- import { handleEvent } from '@crossdelta/cloudevents'
368
- import { OrdersCreatedContract, type OrderCreatedData } from '{workspaceScope}/contracts'
369
- import { processOrder } from '../use-cases/process-order.use-case'
370
-
371
- export default handleEvent(OrdersCreatedContract, async (data: OrderCreatedData) => {
372
- console.log('📦 Processing order:', data.orderId)
373
- await processOrder(data)
374
- })
375
- ```
376
-
377
443
  #### `src/use-cases/process-order.use-case.ts`
378
444
  ```ts
379
445
  import type { OrderCreatedData } from '{workspaceScope}/contracts'
@@ -384,7 +450,7 @@ export const processOrder = async (data: OrderCreatedData): Promise<void> => {
384
450
  ```
385
451
 
386
452
  ```post-commands
387
- pf event add order.created --service services/my-service
453
+ pf cloudevents add order.created --service services/my-service --fields "id:string"
388
454
  ```
389
455
 
390
456
  ```dependencies
@@ -581,7 +647,7 @@ consumeJetStreams({
581
647
  | Block | Purpose |
582
648
  |-------|---------|
583
649
  | `commands` | Scaffolds service (REQUIRED FIRST) |
584
- | `post-commands` | Runs after files created (e.g., `pf event add`) |
650
+ | `post-commands` | Runs after files created (e.g., `pf cloudevents add`) |
585
651
  | `dependencies` | Extra npm packages (NOT `@crossdelta/*`) |
586
652
 
587
653
  **When adding dependencies:**
@@ -591,6 +657,7 @@ consumeJetStreams({
591
657
  - ✅ Prefer packages with TypeScript definitions (`@types/*` or built-in)
592
658
  - ✅ Check GitHub for recent activity and TypeScript support
593
659
  - ⚠️ If using an unfamiliar package, mention: "Check official docs for latest API"
660
+ - ⚠️ **For packages without TS support, see [code-style.md](code-style.md#type-safe-external-package-integration)**
594
661
 
595
662
  ---
596
663
 
@@ -637,6 +704,7 @@ packages/contracts/src/events/
637
704
  - ❌ Insert semicolons
638
705
  - ❌ Create contracts without adding exports to `packages/contracts/src/index.ts`
639
706
  - ❌ Create `use-cases/` folder in NestJS (use Services instead)
707
+ - ❌ **Use `any` types** - see [code-style.md](code-style.md#type-safe-external-package-integration)
640
708
 
641
709
  **DO:**
642
710
  - ✅ Contracts in `packages/contracts/src/events/`
@@ -649,3 +717,4 @@ packages/contracts/src/events/
649
717
  - ✅ Use current, non-deprecated APIs - check package documentation
650
718
  - ✅ Prefer TypeScript-first packages with good type definitions
651
719
  - ✅ Check npm for latest package versions and breaking changes
720
+ - ✅ **Follow [code-style.md](code-style.md) for type-safe patterns**
@@ -5,9 +5,11 @@ FROM oven/bun:${BUN_VERSION}-alpine AS production
5
5
  WORKDIR /app
6
6
 
7
7
  COPY bunfig.toml package.json ./
8
+ COPY packages ./packages
8
9
  COPY src ./src
9
10
 
10
- RUN --mount=type=secret,id=NPM_TOKEN \
11
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
12
+ --mount=type=secret,id=NPM_TOKEN \
11
13
  export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
12
14
  bun install --production --omit=optional
13
15
 
@@ -2,6 +2,9 @@ import { z } from 'zod'
2
2
 
3
3
  const envSchema = z.object({
4
4
  {{envKey}}_PORT: z.string().optional(),
5
+ {{#each envVars}}
6
+ {{key}}: z.string().min(1, '{{key}} is required'),
7
+ {{/each}}
5
8
  })
6
9
 
7
10
  const result = envSchema.safeParse(process.env)
@@ -6,9 +6,11 @@ FROM oven/bun:${BUN_VERSION}-alpine AS builder
6
6
  WORKDIR /app
7
7
 
8
8
  COPY package.json tsconfig*.json nest-cli.json ./
9
+ COPY packages ./packages
9
10
  COPY src ./src
10
11
 
11
- RUN --mount=type=secret,id=NPM_TOKEN \
12
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
13
+ --mount=type=secret,id=NPM_TOKEN \
12
14
  export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
13
15
  bun install
14
16
 
@@ -19,8 +21,10 @@ FROM oven/bun:${BUN_VERSION}-alpine AS deps
19
21
  WORKDIR /app
20
22
 
21
23
  COPY package.json ./
24
+ COPY packages ./packages
22
25
 
23
- RUN --mount=type=secret,id=NPM_TOKEN \
26
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
27
+ --mount=type=secret,id=NPM_TOKEN \
24
28
  export NPM_TOKEN="$(cat /run/secrets/NPM_TOKEN)" && \
25
29
  bun install --production --omit=optional
26
30
 
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod'
2
+
3
+ const envSchema = z.object({
4
+ {{envKey}}_PORT: z.string().optional(),
5
+ {{#each envVars}}
6
+ {{key}}: z.string().min(1, '{{key}} is required'),
7
+ {{/each}}
8
+ })
9
+
10
+ const result = envSchema.safeParse(process.env)
11
+ if (!result.success) {
12
+ console.error('❌ Environment validation failed:')
13
+ console.error(result.error.format())
14
+ process.exit(1)
15
+ }
16
+
17
+ export const env = result.data
@@ -1,4 +1,5 @@
1
- // IMPORTANT: telemetry must be imported first to patch modules before they're loaded
1
+ // IMPORTANT: env validation must be first to fail fast on missing config
2
+ import './config/env'
2
3
  import '@crossdelta/telemetry'
3
4
 
4
5
  import { env } from 'node:process'
@@ -29,7 +29,7 @@ runs:
29
29
  # Copy bun.lock from json folder
30
30
  cp "out/${{ inputs.scope-short-name }}/json/bun.lock" "$CONTEXT_DIR/"
31
31
 
32
- # Flatten: move service/app files to root of contexti
32
+ # Flatten: move service/app files to root of context
33
33
  if [ -d "$CONTEXT_DIR/$SCOPE_DIR" ]; then
34
34
  # Copy all files and directories from the scope dir to context root
35
35
  cp -r "$CONTEXT_DIR/$SCOPE_DIR"/. "$CONTEXT_DIR/"
@@ -38,12 +38,64 @@ runs:
38
38
  rm -rf "$CONTEXT_DIR/$SCOPE_DIR"
39
39
  fi
40
40
 
41
- # Remove packages and apps/services dirs (dependencies come from registry)
42
- rm -rf "$CONTEXT_DIR/packages" "$CONTEXT_DIR/apps" "$CONTEXT_DIR/services"
41
+ # Remove apps and services dirs (flattened above)
42
+ rm -rf "$CONTEXT_DIR/apps" "$CONTEXT_DIR/services"
43
43
 
44
- # Clean up root package.json workspaces
45
44
  cd "$CONTEXT_DIR"
46
- jq 'del(.workspaces)' package.json > package.json.tmp
47
- mv package.json.tmp package.json
45
+
46
+ # Replace workspace:* with npm versions for published packages
47
+ # so bun install fetches pre-built packages from registry (with dist/)
48
+ # Private packages stay as workspace deps (Bun resolves their .ts source)
49
+ if [ -d "packages" ]; then
50
+ for pkg_dir in packages/*/; do
51
+ [ -f "$pkg_dir/package.json" ] || continue
52
+
53
+ pkg_name=$(jq -r '.name' "$pkg_dir/package.json")
54
+ is_private=$(jq -r '.private // false' "$pkg_dir/package.json")
55
+ pkg_version=$(jq -r '.version // empty' "$pkg_dir/package.json")
56
+
57
+ if [ "$is_private" = "true" ] || [ -z "$pkg_version" ]; then
58
+ echo " keeping workspace dep: $pkg_name (private)"
59
+ continue
60
+ fi
61
+
62
+ echo " replacing workspace:* → ^$pkg_version for $pkg_name"
63
+
64
+ # Replace in dependencies and devDependencies
65
+ for field in dependencies devDependencies; do
66
+ if jq -e ".${field}[\"${pkg_name}\"]" package.json > /dev/null 2>&1; then
67
+ jq ".${field}[\"${pkg_name}\"] = \"^${pkg_version}\"" package.json > package.json.tmp
68
+ mv package.json.tmp package.json
69
+ fi
70
+ done
71
+
72
+ # Also update private packages that depend on this published package
73
+ for other_pkg in packages/*/; do
74
+ [ -f "$other_pkg/package.json" ] || continue
75
+ for field in dependencies devDependencies; do
76
+ if jq -e ".${field}[\"${pkg_name}\"]" "$other_pkg/package.json" > /dev/null 2>&1; then
77
+ jq ".${field}[\"${pkg_name}\"] = \"^${pkg_version}\"" "$other_pkg/package.json" > "$other_pkg/package.json.tmp"
78
+ mv "$other_pkg/package.json.tmp" "$other_pkg/package.json"
79
+ fi
80
+ done
81
+ done
82
+
83
+ # Remove published package dir (will be installed from npm)
84
+ rm -rf "$pkg_dir"
85
+ done
86
+ fi
87
+
88
+ # Set workspace config for remaining private packages, or remove it
89
+ if [ -d "packages" ] && [ -n "$(ls -A packages/ 2>/dev/null)" ]; then
90
+ jq '.workspaces = ["packages/*"]' package.json > package.json.tmp
91
+ mv package.json.tmp package.json
92
+ else
93
+ rm -rf packages
94
+ jq 'del(.workspaces)' package.json > package.json.tmp
95
+ mv package.json.tmp package.json
96
+ fi
97
+
98
+ # Ensure packages/ dir exists (Dockerfiles COPY it even when empty)
99
+ mkdir -p packages
48
100
 
49
101
  echo "context-dir=$CONTEXT_DIR" >> "$GITHUB_OUTPUT"