@crossdelta/platform-sdk 0.7.7 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -11
- package/bin/cli.js +24 -24
- package/bin/services/ai/instructions/ai-instructions.md +220 -152
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,13 +162,21 @@ When you create a workspace with `pf`, you get a **Turborepo monorepo** with thi
|
|
|
162
162
|
```
|
|
163
163
|
my-platform/
|
|
164
164
|
├── services/ # Microservices (Hono, NestJS)
|
|
165
|
-
│ ├── orders/ # Example: Order processing service
|
|
166
|
-
│ ├── notifications/# Example: Notification service
|
|
165
|
+
│ ├── orders/ # Example: Order processing service (Event Publisher)
|
|
166
|
+
│ ├── notifications/# Example: Notification service (Event Consumer)
|
|
167
167
|
│ └── nats/ # NATS message broker (auto-scaffolded)
|
|
168
168
|
├── apps/ # Frontend apps (optional: Qwik, Next.js, etc.)
|
|
169
169
|
├── packages/ # Shared libraries
|
|
170
|
+
│ ├── contracts/ # Event contracts (Schema Registry)
|
|
171
|
+
│ │ └── src/
|
|
172
|
+
│ │ ├── events/ # Event schemas with Zod validation
|
|
173
|
+
│ │ │ ├── order-created.ts
|
|
174
|
+
│ │ │ ├── order-created.mock.json
|
|
175
|
+
│ │ │ └── payment-processed.ts
|
|
176
|
+
│ │ └── index.ts # Export all contracts
|
|
170
177
|
│ ├── cloudevents/ # Event publishing/consuming toolkit
|
|
171
|
-
│
|
|
178
|
+
│ ├── telemetry/ # OpenTelemetry setup
|
|
179
|
+
│ └── infrastructure/ # Pulumi utilities and K8s builders
|
|
172
180
|
├── infra/ # Pulumi Infrastructure-as-Code
|
|
173
181
|
│ ├── index.ts # Main Pulumi program
|
|
174
182
|
│ └── services/ # Per-service K8s configs
|
|
@@ -183,21 +191,47 @@ my-platform/
|
|
|
183
191
|
### Key Architectural Decisions
|
|
184
192
|
|
|
185
193
|
1. **NATS + JetStream baseline** — Event-driven communication is built-in, not bolted on
|
|
186
|
-
2. **
|
|
187
|
-
3. **
|
|
188
|
-
4. **
|
|
189
|
-
5. **
|
|
194
|
+
2. **Schema Registry Pattern** — All event schemas live in `packages/contracts` as single source of truth
|
|
195
|
+
3. **Infrastructure-as-Code by default** — Every service has a matching `infra/services/<name>.ts` config
|
|
196
|
+
4. **Auto-wiring everywhere** — Ports, env vars, NATS subjects, and event contracts are derived automatically
|
|
197
|
+
5. **AI-assisted generation** — Services generate with correct event schemas from natural language descriptions
|
|
198
|
+
6. **Opinionated conventions** — Event handlers in `src/events/*.event.ts`, business logic in `src/use-cases/*.use-case.ts`, schemas in `src/types/*.ts`
|
|
199
|
+
7. **Bun-first DX** — Ultra-fast installs, tests, and dev server with fallback to npm/yarn
|
|
190
200
|
|
|
191
201
|
### Event-Driven Mental Model
|
|
192
202
|
|
|
193
|
-
Services communicate via **CloudEvents** over **NATS JetStream**:
|
|
203
|
+
Services communicate via **CloudEvents** over **NATS JetStream** with **type-safe contracts**:
|
|
194
204
|
|
|
195
205
|
```typescript
|
|
196
|
-
//
|
|
197
|
-
|
|
206
|
+
// packages/contracts/src/events/order-created.ts (Schema Registry)
|
|
207
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
208
|
+
import { z } from 'zod'
|
|
209
|
+
|
|
210
|
+
export const OrderCreatedContract = createContract({
|
|
211
|
+
type: 'orders.created',
|
|
212
|
+
schema: z.object({
|
|
213
|
+
orderId: z.string(),
|
|
214
|
+
customerId: z.string(),
|
|
215
|
+
total: z.number(),
|
|
216
|
+
}),
|
|
217
|
+
})
|
|
198
218
|
|
|
199
|
-
|
|
219
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
220
|
+
|
|
221
|
+
// Service A publishes an event (Event Publisher)
|
|
222
|
+
import { publish } from '@crossdelta/cloudevents'
|
|
223
|
+
import { OrderCreatedContract } from '@my-platform/contracts'
|
|
224
|
+
|
|
225
|
+
await publish(OrderCreatedContract, {
|
|
226
|
+
orderId: '123',
|
|
227
|
+
customerId: 'cust-456',
|
|
228
|
+
total: 99.99
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Service B auto-discovers and handles it (Event Consumer)
|
|
200
232
|
// File: services/notifications/src/events/order-created.event.ts
|
|
233
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
234
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@my-platform/contracts'
|
|
201
235
|
export default handleEvent(
|
|
202
236
|
{ schema: OrderCreatedSchema, type: 'orders.created' },
|
|
203
237
|
async (data) => { await sendNotification(data) }
|
package/bin/cli.js
CHANGED
|
@@ -93,7 +93,7 @@ Expecting one of '${s.join("', '")}'`);let i=`${e}Help`;return this.on(i,n=>{let
|
|
|
93
93
|
`),m="";return c&&(m=u.style.error(c)),[`${h} ${d} ${o}`,[p,m].filter(Boolean).join(`
|
|
94
94
|
`)]});var Wu=ne((r,e)=>{let{validate:t=()=>!0}=r,s=X(r.theme),[i,n]=P("idle"),[o,a]=P(),[c,l]=P(""),u=se({status:i,theme:s});ie(async(m,b)=>{if(i==="idle")if(re(m)){let x=c;n("loading");let g=await t(x);g===!0?(l(x),n("done"),e(x)):(b.write(c),a(g||"You must provide a valid value"),n("idle"))}else l(b.line),a(void 0)});let h=s.style.message(r.message,i),f="",d;r.mask?f=(typeof r.mask=="string"?r.mask:"*").repeat(c.length):i!=="done"&&(d=`${s.style.help("[input is masked]")}${gs}`),i==="done"&&(f=s.style.answer(f));let p="";return o&&(p=s.style.error(o)),[[u,h,r.mask?f:d].join(" "),p]});var Gr=E(cr(),1);var T8={icon:{cursor:Xe.pointer},style:{disabled:r=>Gr.default.dim(`- ${r}`),searchTerm:r=>Gr.default.cyan(r),description:r=>Gr.default.cyan(r),keysHelpTip:r=>r.map(([e,t])=>`${Gr.default.bold(e)} ${Gr.default.dim(t)}`).join(Gr.default.dim(" \u2022 "))},helpMode:"always"};function Uu(r){return!L.isSeparator(r)&&!r.disabled}function P8(r){return r.map(e=>{if(L.isSeparator(e))return e;if(typeof e=="string")return{value:e,name:e,short:e,disabled:!1};let t=e.name??String(e.value),s={value:e.value,name:t,short:e.short??t,disabled:e.disabled??!1};return e.description&&(s.description=e.description),s})}var O8=ne((r,e)=>{let{pageSize:t=7,validate:s=()=>!0}=r,i=X(T8,r.theme),[n,o]=P("loading"),[a,c]=P(""),[l,u]=P([]),[h,f]=P(),d=se({status:n,theme:i}),p=$e(()=>{let C=l.findIndex(Uu),T=l.findLastIndex(Uu);return{first:C,last:T}},[l]),[m=p.first,b]=P();Ue(()=>{let C=new AbortController;return o("loading"),f(void 0),(async()=>{try{let M=await r.source(a||void 0,{signal:C.signal});C.signal.aborted||(b(void 0),f(void 0),u(P8(M)),o("idle"))}catch(M){!C.signal.aborted&&M instanceof Error&&f(M.message)}})(),()=>{C.abort()}},[a]);let x=l[m];ie(async(C,T)=>{if(re(C))if(x){o("loading");let M=await s(x.value);o("idle"),M===!0?(o("done"),e(x.value)):x.name===a?f(M||"You must provide a valid value"):(T.write(x.name),c(x.name))}else T.write(a);else if(Ht(C)&&x)T.clearLine(0),T.write(x.name),c(x.name);else if(n!=="loading"&&(be(C)||Me(C))){if(T.clearLine(0),be(C)&&m!==p.first||Me(C)&&m!==p.last){let M=be(C)?-1:1,K=m;do K=(K+M+l.length)%l.length;while(!Uu(l[K]));b(K)}}else c(T.line)});let g=i.style.message(r.message,n),y;if(i.helpMode!=="never")if(r.instructions){let{pager:C,navigation:T}=r.instructions;y=i.style.help(l.length>t?C:T)}else y=i.style.keysHelpTip([["\u2191\u2193","navigate"],["\u23CE","select"]]);let w=Nr({items:l,active:m,renderItem({item:C,isActive:T}){if(L.isSeparator(C))return` ${C.separator}`;if(C.disabled){let Be=typeof C.disabled=="string"?C.disabled:"(disabled)";return i.style.disabled(`${C.name} ${Be}`)}let M=T?i.style.highlight:Be=>Be,K=T?i.icon.cursor:" ";return M(`${K} ${C.name}`)},pageSize:t,loop:!1}),v;h?v=i.style.error(h):l.length===0&&a!==""&&n==="idle"&&(v=i.style.error("No results found"));let k;if(n==="done"&&x)return[d,g,i.style.answer(x.short)].filter(Boolean).join(" ").trimEnd();k=i.style.searchTerm(a);let _=x?.description,O=[d,g,k].filter(Boolean).join(" ").trimEnd(),A=[v??w," ",_?i.style.description(_):"",y].filter(Boolean).join(`
|
|
95
95
|
`).trimEnd();return[O,A]});var Cs=E(cr(),1);var I8={icon:{cursor:Xe.pointer},style:{disabled:r=>Cs.default.dim(`- ${r}`),description:r=>Cs.default.cyan(r),keysHelpTip:r=>r.map(([e,t])=>`${Cs.default.bold(e)} ${Cs.default.dim(t)}`).join(Cs.default.dim(" \u2022 "))},helpMode:"always",indexMode:"hidden",keybindings:[]};function vs(r){return!L.isSeparator(r)&&!r.disabled}function B8(r){return r.map(e=>{if(L.isSeparator(e))return e;if(typeof e=="string")return{value:e,name:e,short:e,disabled:!1};let t=e.name??String(e.value),s={value:e.value,name:t,short:e.short??t,disabled:e.disabled??!1};return e.description&&(s.description=e.description),s})}var xo=ne((r,e)=>{let{loop:t=!0,pageSize:s=7}=r,i=X(I8,r.theme),{keybindings:n}=i,[o,a]=P("idle"),c=se({status:o,theme:i}),l=$r(),u=!n.includes("vim"),h=$e(()=>B8(r.choices),[r.choices]),f=$e(()=>{let _=h.findIndex(vs),O=h.findLastIndex(vs);if(_===-1)throw new St("[select prompt] No selectable choices. All choices are disabled.");return{first:_,last:O}},[h]),d=$e(()=>"default"in r?h.findIndex(_=>vs(_)&&_.value===r.default):-1,[r.default,h]),[p,m]=P(d===-1?f.first:d),b=h[p];ie((_,O)=>{if(clearTimeout(l.current),re(_))a("done"),e(b.value);else if(be(_,n)||Me(_,n)){if(O.clearLine(0),t||be(_,n)&&p!==f.first||Me(_,n)&&p!==f.last){let A=be(_,n)?-1:1,C=p;do C=(C+A+h.length)%h.length;while(!vs(h[C]));m(C)}}else if(wi(_)&&!Number.isNaN(Number(O.line))){let A=Number(O.line)-1,C=-1,T=h.findIndex(K=>L.isSeparator(K)?!1:(C++,C===A)),M=h[T];M!=null&&vs(M)&&m(T),l.current=setTimeout(()=>{O.clearLine(0)},700)}else if(Br(_))O.clearLine(0);else if(u){let A=O.line.toLowerCase(),C=h.findIndex(T=>L.isSeparator(T)||!vs(T)?!1:T.name.toLowerCase().startsWith(A));C!==-1&&m(C),l.current=setTimeout(()=>{O.clearLine(0)},700)}}),Ue(()=>()=>{clearTimeout(l.current)},[]);let x=i.style.message(r.message,o),g;if(i.helpMode!=="never")if(r.instructions){let{pager:_,navigation:O}=r.instructions;g=i.style.help(h.length>s?_:O)}else g=i.style.keysHelpTip([["\u2191\u2193","navigate"],["\u23CE","select"]]);let y=0,w=Nr({items:h,active:p,renderItem({item:_,isActive:O,index:A}){if(L.isSeparator(_))return y++,` ${_.separator}`;let C=i.indexMode==="number"?`${A+1-y}. `:"";if(_.disabled){let K=typeof _.disabled=="string"?_.disabled:"(disabled)";return i.style.disabled(`${C}${_.name} ${K}`)}let T=O?i.style.highlight:K=>K,M=O?i.icon.cursor:" ";return T(`${M} ${C}${_.name}`)},pageSize:s,loop:t});if(o==="done")return[c,x,i.style.answer(b.short)].filter(Boolean).join(" ");let{description:v}=b;return`${[[c,x].filter(Boolean).join(" "),w," ",v?i.style.description(v):"",g].filter(Boolean).join(`
|
|
96
|
-
`).trimEnd()}${gs}`});var
|
|
96
|
+
`).trimEnd()}${gs}`});var G=E(require("chalk")),v2=E(require("ora"));var H=require("node:fs"),Se=require("node:path"),Lg=require("@anatine/zod-mock");var Bi=E(require("chalk"));function qu(r,e){if(r.length===0)return"No entries to display.";let t=e?.title||"Benchmark Results",s=e?.footer||"",i=e?.maxBarWidth||50,n=e?.unit||"",o=Math.max(...r.map(c=>c.value)),a=[];a.push(""),a.push(t),a.push("");for(let c of r){let l=R8(c.barColor??e?.barColor),u=M8(c,o,i,l,n,e?.labelColor);a.push(u)}return s&&(a.push(""),a.push(s)),a.join(`
|
|
97
97
|
`)}function R8(r){return r?Bi.default[r]("\u2587"):"\u2587"}function M8(r,e,t,s,i,n){let o=Math.round(r.value/e*t),a=r.barColor?Bi.default[r.barColor](s.repeat(o)):s.repeat(o),c=r.color??n;return`${Bi.default.bold(c?Bi.default[c](r.label.padEnd(12)):r.label.padEnd(12))} ${a} ${r.value.toLocaleString()}${i?` ${i}`:""}`}var go=E(require("chalk"));var Hu=require("listr2");var Ri=E(require("chalk")),S={logs:[],breakLine:()=>(console.log(),S),success:(r,...e)=>(console.log(Ri.default.green(`\u2714 ${r}`),...e),S),info:(r,...e)=>(console.log(Ri.default.cyan(`\u{1F6C8} ${r}`),...e),S),warn:(r,...e)=>(console.warn(Ri.default.yellow(`\u26A0\uFE0E ${r}`),...e),S),error:(r,...e)=>(console.error(Ri.default.red(`\u2716 ${r}`),...e),S),log:(r,...e)=>(console.log(r,...e),S.logs.push({message:r,context:e.join()}),S),getStoredLogs:r=>r?S.logs.filter(e=>e.context?.includes(r)):S.logs,storeLog:(r,e)=>S.logs.push({message:r,context:e})};function hr(r){let e=new ce(r.name).description(go.default.bold(r.description)).showHelpAfterError();return r.arguments?.map(([t,s])=>e.argument(t,s)),r.options?.map(([t,s])=>e.option(t,s)),r.exampleUsage&&e.addHelpText("after",()=>`${go.default.cyan.bold(`
|
|
98
98
|
Example:`)}
|
|
99
99
|
${go.default.bold(r.exampleUsage)}
|
|
@@ -110,7 +110,7 @@ To create a new workspace, run:
|
|
|
110
110
|
|
|
111
111
|
\x1B[36mpf new workspace my-platform\x1B[0m
|
|
112
112
|
`);r=e}return r}function E0(){let r=ae(),e=(0,Ge.join)(r,"package.json"),s=JSON.parse((0,It.readFileSync)(e,"utf-8")).name;return s.startsWith("@")?s.split("/")[0]:`@${s}`}function Jr(r,e,t=process.cwd()){let s=(0,Ge.join)(t,"package.json"),i=(0,Ls.readJsonSync)(s);z4(i,r,e),(0,Ls.writeJsonSync)(s,i,{spaces:2,EOL:`
|
|
113
|
-
`,encoding:"utf-8"})}function z4(r,e,t){let s=e.split("."),i=r;for(let n=0;n<s.length-1;n++){let o=s[n];(!(o in i)||typeof i[o]!="object"||i[o]===null)&&(i[o]={}),i=i[o]}i[s[s.length-1]]=t}var Ws=r=>{let e=Rg();return(0,Ls.readJSONSync)((0,Ge.resolve)(e,r),{encoding:"utf-8"})},wr=Ws("package.json");var K4={firstname:"firstName",lastname:"lastName",fullname:"firstName",street:"streetAddress",address:"streetAddress",city:"city",state:"state",country:"country",phone:"phoneNumber",company:"companyName",organization:"companyName",productname:"productName",product:"productName",item:"productName",productdescription:"productDescription",description:"productDescription",password:"password",jobtitle:"string",job:"string",title:"string",sku:"string",code:"string",price:"number",amount:"number",total:"number",qty:"number",quantity:"number",count:"number",status:"string",date:"date",createdat:"date",updatedat:"date",birthday:"date",birthdate:"date"},Y4={email:"email",id:"uuid",createdat:"date",updatedat:"date",firstname:"firstName",lastname:"lastName"},J4=(r,e)=>{let t=r.toLowerCase();if(e.includes(".email()")||t.includes("email"))return"email";if(e.includes(".uuid()")||t.includes("id")&&!t.includes("email"))return"uuid";if(e.includes(".url()")||t.includes("url")||t.includes("website"))return"url";if(t==="name"&&!t.includes("username"))return"productName";if(t.includes("zipcode")||t.includes("zip")||t.includes("postal"))return"zipCode";for(let[s,i]of Object.entries(K4))if(t.includes(s))return i;return null},Z4=r=>{let e=r.match(/type:\s*['"]([^'"]+)['"]/s);if(!e)return null;let t=r.match(/const\s+(\w+Contract)\s*=\s*\{[\s\S]*?type:\s*['"]([^'"]+)['"][\s\S]*?schema:\s*z\.object\s*\(/s);if(t){let f=t[1],d=r.indexOf(`const ${f}`),p=r.indexOf("z.object(",d),m=r.indexOf("{",p),b=1,x=m+1;for(;x<r.length&&b>0;)r[x]==="{"&&b++,r[x]==="}"&&b--,x++;let g=r.indexOf(")",x),y=`const ${f}Schema = z.object(${r.substring(m,g+1)}`;return{eventType:e[1],schemaName:`${f}Schema`,schemaCode:y}}let s=r.match(/import\s+\{[^}]*?(\w+Schema)[^}]*?\}\s+from\s+['"]\.\.\/types\//);if(s)return{eventType:e[1],schemaName:s[1],schemaCode:""};let i=r.match(/const\s+(\w+Schema)\s*=\s*z\.object\(/s);if(!i)return null;let n=r.indexOf(`const ${i[1]}`),o=r.indexOf("z.object(",n),a=r.indexOf("{",o),c=1,l=a+1;for(;l<r.length&&c>0;)r[l]==="{"&&c++,r[l]==="}"&&c--,l++;let u=r.indexOf(")",l),h=r.substring(n,u+1);return{eventType:e[1],schemaName:i[1],schemaCode:h}},X4=(r,e,t)=>r==="email"?"user@example.com":r==="uuid"?"550e8400-e29b-41d4-a716-446655440000":r==="firstName"?"John":r==="lastName"?"Doe":e==="string"?`sample-${t}`:e==="number"?100:e==="boolean"?!0:e==="array"?[]:null,Q4=async(r,e,t)=>{let s={};try{let{createJiti:i}=await import("jiti"),o=i(process.cwd(),{interopDefault:!0})(t),a=Object.entries(o).find(([c])=>c.endsWith("Schema")||c==="schema");if(a?.[1]){let c=a[1],l=(0,Lg.generateMock)(c);if(l&&typeof l=="object"&&Object.keys(l).length>0)return S0(l,s,""),{data:l,faker:s}}}catch{}return Wg(r)},S0=(r,e,t)=>{!r||typeof r!="object"||Object.entries(r).forEach(([s,i])=>{let n=t?`${t}.${s}`:s,o=s.toLowerCase(),a=Object.entries(Y4).find(([c])=>o.includes(c));a&&(a[0]!=="id"||typeof i=="string")&&(e[n]=a[1]),typeof i=="object"&&!Array.isArray(i)?S0(i,e,n):Array.isArray(i)&&i.length>0&&S0(i[0],e,`${n}[0]`)})},Wg=r=>{let e={},t={},s=r.match(/z\.object\(\s*\{([\s\S]*)\}\s*\)/m);if(!s)return{data:e,faker:t};let i=s[1];return k0(i,e,t,""),{data:e,faker:t}},k0=(r,e,t,s)=>{let i=/^\s*(\w+):\s*z\.(\w+)\(/gm;Array.from(r.matchAll(i)).filter(a=>a.index!==void 0).map(a=>{let c=a[1],l=a[2],u=a.index,h=1,f=u+a[0].length;for(;f<r.length&&h>0;){let g=r[f];g==="("&&h++,g===")"&&h--,f++}let d=r.substring(u,f),p=r.substring(0,u),m=(p.match(/\{/g)||[]).length,b=(p.match(/\}/g)||[]).length,x=m-b;return{name:c,zodType:l,def:d,nestingLevel:x}}).filter(a=>a.nestingLevel===0).forEach(a=>{e5(a.name,a.zodType,a.def,e,t,s)})},e5=(r,e,t,s,i,n)=>{let o=n?`${n}.${r}`:r,a=t.includes(".optional()");e==="array"?t5(t,s,r,o,i):e==="object"?r5(t,s,r,o,i):s5(r,e,a,o,s,i)},t5=(r,e,t,s,i)=>{if(r.includes("z.object")){let n=r.match(/z\.object\(\s*\{([\s\S]*?)\}\s*\)/);if(n){let o={};k0(n[1],o,i,`${s}[0]`),e[t]=[o];return}}e[t]=[]},r5=(r,e,t,s,i)=>{let n=r.match(/z\.object\(\s*\{([\s\S]*?)\}\s*\)/);n&&(e[t]={},k0(n[1],e[t],i,s))},s5=(r,e,t,s,i,n)=>{let o=J4(r,`z.${e}()${t?".optional()":""}`),a=X4(o,e,r);o&&(n[s]=o),(!t||a!==null)&&(i[r]=a)},Ug=r=>r.replace(/\./g,"-"),i5=async(r,e,t)=>{let s=ae(),{eventsPath:i,indexPath:n,packagePath:o}=Bt(),a=t.outputDir??i;if(!(0,
|
|
113
|
+
`,encoding:"utf-8"})}function z4(r,e,t){let s=e.split("."),i=r;for(let n=0;n<s.length-1;n++){let o=s[n];(!(o in i)||typeof i[o]!="object"||i[o]===null)&&(i[o]={}),i=i[o]}i[s[s.length-1]]=t}var Ws=r=>{let e=Rg();return(0,Ls.readJSONSync)((0,Ge.resolve)(e,r),{encoding:"utf-8"})},wr=Ws("package.json");var K4={firstname:"firstName",lastname:"lastName",fullname:"firstName",street:"streetAddress",address:"streetAddress",city:"city",state:"state",country:"country",phone:"phoneNumber",company:"companyName",organization:"companyName",productname:"productName",product:"productName",item:"productName",productdescription:"productDescription",description:"productDescription",password:"password",jobtitle:"string",job:"string",title:"string",sku:"string",code:"string",price:"number",amount:"number",total:"number",qty:"number",quantity:"number",count:"number",status:"string",date:"date",createdat:"date",updatedat:"date",birthday:"date",birthdate:"date"},Y4={email:"email",id:"uuid",createdat:"date",updatedat:"date",firstname:"firstName",lastname:"lastName"},J4=(r,e)=>{let t=r.toLowerCase();if(e.includes(".email()")||t.includes("email"))return"email";if(e.includes(".uuid()")||t.includes("id")&&!t.includes("email"))return"uuid";if(e.includes(".url()")||t.includes("url")||t.includes("website"))return"url";if(t==="name"&&!t.includes("username"))return"productName";if(t.includes("zipcode")||t.includes("zip")||t.includes("postal"))return"zipCode";for(let[s,i]of Object.entries(K4))if(t.includes(s))return i;return null},Z4=r=>{let e=r.match(/type:\s*['"]([^'"]+)['"]/s);if(!e)return null;let t=r.match(/const\s+(\w+Contract)\s*=\s*\{[\s\S]*?type:\s*['"]([^'"]+)['"][\s\S]*?schema:\s*z\.object\s*\(/s);if(t){let f=t[1],d=r.indexOf(`const ${f}`),p=r.indexOf("z.object(",d),m=r.indexOf("{",p),b=1,x=m+1;for(;x<r.length&&b>0;)r[x]==="{"&&b++,r[x]==="}"&&b--,x++;let g=r.indexOf(")",x),y=`const ${f}Schema = z.object(${r.substring(m,g+1)}`;return{eventType:e[1],schemaName:`${f}Schema`,schemaCode:y}}let s=r.match(/import\s+\{[^}]*?(\w+Schema)[^}]*?\}\s+from\s+['"]\.\.\/types\//);if(s)return{eventType:e[1],schemaName:s[1],schemaCode:""};let i=r.match(/const\s+(\w+Schema)\s*=\s*z\.object\(/s);if(!i)return null;let n=r.indexOf(`const ${i[1]}`),o=r.indexOf("z.object(",n),a=r.indexOf("{",o),c=1,l=a+1;for(;l<r.length&&c>0;)r[l]==="{"&&c++,r[l]==="}"&&c--,l++;let u=r.indexOf(")",l),h=r.substring(n,u+1);return{eventType:e[1],schemaName:i[1],schemaCode:h}},X4=(r,e,t)=>r==="email"?"user@example.com":r==="uuid"?"550e8400-e29b-41d4-a716-446655440000":r==="firstName"?"John":r==="lastName"?"Doe":e==="string"?`sample-${t}`:e==="number"?100:e==="boolean"?!0:e==="array"?[]:null,Q4=async(r,e,t)=>{let s={};try{let{createJiti:i}=await import("jiti"),o=i(process.cwd(),{interopDefault:!0})(t),a=Object.entries(o).find(([c])=>c.endsWith("Schema")||c==="schema");if(a?.[1]){let c=a[1],l=(0,Lg.generateMock)(c);if(l&&typeof l=="object"&&Object.keys(l).length>0)return S0(l,s,""),{data:l,faker:s}}}catch{}return Wg(r)},S0=(r,e,t)=>{!r||typeof r!="object"||Object.entries(r).forEach(([s,i])=>{let n=t?`${t}.${s}`:s,o=s.toLowerCase(),a=Object.entries(Y4).find(([c])=>o.includes(c));a&&(a[0]!=="id"||typeof i=="string")&&(e[n]=a[1]),typeof i=="object"&&!Array.isArray(i)?S0(i,e,n):Array.isArray(i)&&i.length>0&&S0(i[0],e,`${n}[0]`)})},Wg=r=>{let e={},t={},s=r.match(/z\.object\(\s*\{([\s\S]*)\}\s*\)/m);if(!s)return{data:e,faker:t};let i=s[1];return k0(i,e,t,""),{data:e,faker:t}},k0=(r,e,t,s)=>{let i=/^\s*(\w+):\s*z\.(\w+)\(/gm;Array.from(r.matchAll(i)).filter(a=>a.index!==void 0).map(a=>{let c=a[1],l=a[2],u=a.index,h=1,f=u+a[0].length;for(;f<r.length&&h>0;){let g=r[f];g==="("&&h++,g===")"&&h--,f++}let d=r.substring(u,f),p=r.substring(0,u),m=(p.match(/\{/g)||[]).length,b=(p.match(/\}/g)||[]).length,x=m-b;return{name:c,zodType:l,def:d,nestingLevel:x}}).filter(a=>a.nestingLevel===0).forEach(a=>{e5(a.name,a.zodType,a.def,e,t,s)})},e5=(r,e,t,s,i,n)=>{let o=n?`${n}.${r}`:r,a=t.includes(".optional()");e==="array"?t5(t,s,r,o,i):e==="object"?r5(t,s,r,o,i):s5(r,e,a,o,s,i)},t5=(r,e,t,s,i)=>{if(r.includes("z.object")){let n=r.match(/z\.object\(\s*\{([\s\S]*?)\}\s*\)/);if(n){let o={};k0(n[1],o,i,`${s}[0]`),e[t]=[o];return}}e[t]=[]},r5=(r,e,t,s,i)=>{let n=r.match(/z\.object\(\s*\{([\s\S]*?)\}\s*\)/);n&&(e[t]={},k0(n[1],e[t],i,s))},s5=(r,e,t,s,i,n)=>{let o=J4(r,`z.${e}()${t?".optional()":""}`),a=X4(o,e,r);o&&(n[s]=o),(!t||a!==null)&&(i[r]=a)},Ug=r=>r.replace(/\./g,"-"),i5=async(r,e,t)=>{let s=ae(),{eventsPath:i,indexPath:n,packagePath:o}=Bt(),a=t.outputDir??i;if(!(0,H.existsSync)(a))return S.error(`Contracts events directory not found: ${a}`),{mockPath:null};let c=e.match(/import\s+\{[^}]*?(\w+Contract)[^}]*?\}\s+from\s+['"]([^'"]+)\/contracts['"]/);if(!c)return{mockPath:null,skipped:!0,reason:"Advanced Mode handler (uses contracts)"};let l=c[1],u=c[2];if(!e.match(/handleEvent\s*\(\s*(\w+Contract)/))return{mockPath:null,skipped:!0,reason:"Could not parse handleEvent call"};let f=l.replace("Contract","").replace(/([A-Z])/g,(_,O,A)=>(A>0?"-":"")+_.toLowerCase()).toLowerCase(),d=`${f}.ts`,p=(0,Se.join)(a,d);if((0,H.existsSync)(p)){let _=(0,H.readFileSync)(p,"utf-8"),O=_.match(/export const (\w+)\s*=\s*z\.object\(\s*\{([\s\S]*?)\}\s*\)/m);if(O){let A=O[2],{data:C,faker:T}=Wg(`z.object({${A}})`),M=_.match(/type:\s*['"]([^'"]+)['"]/),K=M?M[1]:f.replace(/-/g,"."),Be=`${f}.mock.json`,Lt=(0,Se.join)(a,Be);if((0,H.existsSync)(Lt)&&!t.overwrite)return{mockPath:null,skipped:!0,reason:"Mock already exists"};let Sh={eventName:K,description:`Mock data for ${K} event`,data:C};return Object.keys(T).length>0&&(Sh.faker=T),(0,H.writeFileSync)(Lt,JSON.stringify(Sh,null,2),"utf-8"),{mockPath:Lt,skipped:!1,reason:void 0}}}let m=f.replace(/-/g,"."),b=l.replace("Contract","Data"),x=`import { createContract } from '@crossdelta/cloudevents'
|
|
114
114
|
import { z } from 'zod'
|
|
115
115
|
|
|
116
116
|
// TODO: Define your event schema
|
|
@@ -126,9 +126,9 @@ export const ${l} = createContract({
|
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
export type ${b} = z.infer<typeof ${l}.schema>
|
|
129
|
-
`;(0,
|
|
129
|
+
`;(0,H.existsSync)(p)||(0,H.writeFileSync)(p,x,"utf-8");let g=(0,H.readFileSync)(n,"utf-8"),y=`export * from './events/${f}'`;if(!g.includes(y)){let _=g.split(`
|
|
130
130
|
`),O=_.findLastIndex(A=>A.startsWith("export"));O>=0?_.splice(O+1,0,y):_.push("",y),g=_.join(`
|
|
131
|
-
`),(0,
|
|
131
|
+
`),(0,H.writeFileSync)(n,g,"utf-8")}let w=`${f}.mock.json`,v=(0,Se.join)(a,w),k={eventName:m,description:`Mock data for ${m} event`,data:{id:"550e8400-e29b-41d4-a716-446655440000",createdAt:new Date().toISOString()},faker:{id:"uuid",createdAt:"date"}};return(0,H.writeFileSync)(v,JSON.stringify(k,null,2),"utf-8"),{mockPath:v,skipped:!1,reason:void 0}},n5=async(r,e,t,s,i,n,o,a)=>{let{eventsPath:c,indexPath:l,packagePath:u}=Bt(),h=a??c;if(!(0,H.existsSync)(h)){S.error(`Contracts events directory not found: ${h}`);return}let f=(0,Se.join)(u,"package.json"),p=JSON.parse((0,H.readFileSync)(f,"utf-8")).name.split("/")[0],m=Ug(t),b=`${m}.ts`,x=(0,Se.join)(h,b),g=m.split("-").map(M=>M.charAt(0).toUpperCase()+M.slice(1)).join("")+"Contract",y=g.replace("Contract","Data"),w=`import { createContract } from '@crossdelta/cloudevents'
|
|
132
132
|
import { z } from 'zod'
|
|
133
133
|
|
|
134
134
|
${s}
|
|
@@ -139,9 +139,9 @@ export const ${g} = createContract({
|
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
export type ${y} = z.infer<typeof ${g}.schema>
|
|
142
|
-
`;(0,
|
|
142
|
+
`;(0,H.writeFileSync)(x,w,"utf-8");let v=(0,H.readFileSync)(l,"utf-8"),k=`export * from './events/${m}'`;if(!v.includes(k)){let M=v.split(`
|
|
143
143
|
`),K=M.findLastIndex(Be=>Be.startsWith("export"));K>=0?M.splice(K+1,0,k):M.push("",k),v=M.join(`
|
|
144
|
-
`),(0,
|
|
144
|
+
`),(0,H.writeFileSync)(l,v,"utf-8")}let O=(0,H.readFileSync)(r,"utf-8").replace(/import\s+\{[^}]*\}\s+from\s+['"]\.\.\/types\/[^'"]+['"]/,`import { ${g}, type ${y} } from '${p}/contracts'`).replace(/handleEvent\s*\(\s*\{[\s\S]*?type:\s*['"]([^'"]+)['"][\s\S]*?schema:\s*\w+Schema[\s\S]*?\}\s*,/,`handleEvent(${g},`).replace(new RegExp(`\\b${i.replace("Schema","Event")}\\b`,"g"),y);(0,H.writeFileSync)(r,O,"utf-8");let A=`${m}.mock.json`,C=(0,Se.join)(c,A),T={eventName:t,description:`Mock data for ${t} event`,data:n};Object.keys(o).length>0&&(T.faker=o),(0,H.writeFileSync)(C,JSON.stringify(T,null,2),"utf-8")},o5=async(r,e={})=>{let{outputDir:t,overwrite:s=!1}=e;if(!(0,H.existsSync)(r))return S.error(`Event handler not found: ${r}`),{mockPath:null};let i=(0,H.readFileSync)(r,"utf-8");if(i.includes("Contract")&&(i.includes("contracts'")||i.includes('contracts"')))return await i5(r,i,e);let n=Z4(i);if(!n)return S.error(`Could not parse event handler: ${r}`),{mockPath:null};let{eventType:o,schemaCode:a,schemaName:c}=n,l=a;if(!a){let y=(0,Se.dirname)(r),w=(0,Se.dirname)(y),v=(0,Se.join)(w,"types","events.ts");if((0,H.existsSync)(v)){let k=(0,H.readFileSync)(v,"utf-8"),_=k.indexOf(`export const ${c}`);if(_>=0){let O=k.indexOf("z.object(",_),A=k.indexOf("{",O),C=1,T=A+1;for(;T<k.length&&C>0;)k[T]==="{"&&C++,k[T]==="}"&&C--,T++;let M=k.indexOf(")",T);M>=0&&(l=k.substring(_,M+1).replace(/^export\s+/,""))}}if(!l)return S.error(`Could not load schema ${c} from types file`),{mockPath:null}}let{data:u,faker:h}=await Q4(l,c,r),f=ae();if((i.includes("from '../types/")||i.includes('from "../types/'))&&!t)return await n5(r,f,o,l,c,u,h),{mockPath:null,skipped:!0,reason:"Migrated to Advanced Mode (contracts)"};let p=t??Bt().eventsPath;if(!(0,H.existsSync)(p))return S.error(`Contracts events directory not found: ${p}`),{mockPath:null};let b=`${Ug(o)}.mock.json`,x=(0,Se.join)(p,b);if((0,H.existsSync)(x)&&!s)return S.warn(`Mock file already exists: ${x}`),{mockPath:null};let g={eventName:o,description:`Mock data for ${o} event`,data:u};return Object.keys(h).length>0&&(g.faker=h),(0,H.writeFileSync)(x,JSON.stringify(g,null,2),"utf-8"),{mockPath:x}},Jo=async(r,e={})=>{let t=(0,Se.join)(r,"src","events");if(!(0,H.existsSync)(t))return{mockPaths:[],skippedHandlers:[]};let s=(0,H.readdirSync)(t).filter(a=>a.endsWith(".event.ts")).map(a=>(0,Se.join)(t,a)),i=await Promise.all(s.map(a=>o5(a,e))),n=i.filter(a=>a.mockPath!==null).map(a=>a.mockPath),o=i.filter(a=>a.skipped).map((a,c)=>`${(0,Se.basename)(s[c])} (${a.reason})`);return{mockPaths:n,skippedHandlers:o}};var Zr=require("node:fs"),i2=require("node:path"),O0=require("node:process"),n2=require("execa");var T0=require("node:child_process"),e2=require("node:path"),t2=require("execa");var Jg=require("node:fs"),Zg=require("node:path"),Xg=E(Yg()),F0=(r,e)=>{let t=(0,Zg.resolve)(e,r);return(0,Jg.existsSync)(t)?(0,Xg.config)({path:t,processEnv:{}}).parsed||{}:{}},Qg=(r,e)=>{let t=F0(r,e),s=[];for(let[i,n]of Object.entries(t))if(i.endsWith("_PORT")&&n){let o=Number.parseInt(n,10);!Number.isNaN(o)&&o>0&&s.push(o)}return s};var E5={...process.env,NODE_NO_WARNINGS:"1"};async function Dr(r,e,{cwd:t=process.cwd(),task:s,shell:i,context:n=r,quiet:o=!1,nonInteractive:a=!1,env:c}={}){try{e.length===0&&(e=r.split(" ").slice(1));let l=o?"pipe":s||a?["ignore","pipe","pipe"]:"inherit",u={...E5};c?u={...u,...c}:(s||a)&&(u={...u,CI:"true"});let h=(0,t2.execa)(r,e,{cwd:t,stdio:l,all:s||a?!0:void 0,shell:i??!0,env:u});h.all&&h.all.on("data",f=>{let d=f.toString().trim();s?(s.output=d,d.length&&S.storeLog(d,n)):a&&d.length&&console.log(d)}),await h}catch(l){let u=l instanceof Error?l.message:String(l),h=S5(u);throw new Error(h)}}var S5=r=>{let e=r.match(/error: (.+)/);return e?e[1].trim():r};function r2(r,e,t={}){let{cwd:s=process.cwd(),envFile:i,detached:n=!0,pipeStdout:o=!1,onStdout:a,onStderr:c,onExit:l}=t,u=i?F0(i,s):{},h=(0,e2.resolve)(s,"node_modules",".bin"),f=process.env.PATH||"",d=`${h}:${f}`,p={...process.env,...u,PATH:d,FORCE_COLOR:"1"},m=(0,T0.spawn)(r,e,{cwd:s,env:p,stdio:["inherit",o?"pipe":"inherit","pipe"],detached:n});return a&&o&&m.stdout?.on("data",a),c&&m.stderr?.on("data",c),l&&m.on("exit",l),m}function P0(r,e,t={}){let{cwd:s=process.cwd()}=t;(0,T0.spawn)(r,e,{cwd:s,stdio:"inherit",shell:!0}).on("exit",n=>process.exit(n||0))}async function s2(r){let{execSync:e}=await import("node:child_process");for(let t of r)try{let i=e(`lsof -ti:${t}`,{encoding:"utf8",stdio:"pipe"}).trim().split(`
|
|
145
145
|
`).filter(Boolean);for(let n of i)try{process.kill(Number.parseInt(n,10),"SIGKILL")}catch{}}catch{}}var Xo=r=>Mi(r);function o2(r){let e=(0,i2.join)(nn(),"node_modules",r);return(0,Zr.existsSync)(e)}var a2=r=>Object.keys(wr.scripts??{}).includes(r);function I0(){if((0,Zr.existsSync)("bun.lock")||(0,Zr.existsSync)("bun.lockb"))return"bun";if((0,Zr.existsSync)("pnpm-lock.yaml"))return"pnpm";if((0,Zr.existsSync)("yarn.lock"))return"yarn";if((0,Zr.existsSync)("package-lock.json"))return"npm"}function Le(){return I0()??B0()}function B0(){return Xo("bun")?"bun":Xo("pnpm")?"pnpm":Xo("yarn")?"yarn":"npm"}function c2(){return["bun","pnpm","yarn","npm"].filter(Xo)}async function vr(r,e){let t=k5(r,e),{mergedOptions:s,packages:i}=t,{cwd:n=process.cwd(),flags:o=[],task:a}=s,c=s.packageManager??Le(),l=[],u=Array.isArray(i)?i:[i];c==="bun"&&(u=u.map(f=>f.replace("git+ssh://",""))),c==="pnpm"&&(l.push("--config.engine-strict=false"),l.push("--no-frozen-lockfile")),["yarn","npm"].includes(c)&&l.push("--ignore-engines");let h=u.length?c==="npm"?["install",...o,...l,...u]:["add",...o,...l,...u]:["install",...l];await Dr(c,h,{cwd:n,task:a})}async function l2(r){let e=["-g"],t=[...r.flags??[],...e];await vr({...r,flags:t,packageManager:r.packageManager??Le()})}function u2(r,e,t){let{args:s,mergedOptions:i}=f2(e??[],t),n=i.manager??Le(),{command:o,args:a}=h2(n);(0,n2.execaSync)(o,[...a,r,...s],{cwd:i.cwd??(0,O0.cwd)(),stdio:"inherit",preferLocal:!0})}async function Rt(r,e,t){let{args:s,mergedOptions:i}=f2(e??[],t),n=i.manager??Le(),{command:o,args:a}=h2(n);await Dr(o,[...a,r,...s],i)}async function Qo(r,e){let{script:t,mergedOptions:s}=_5(r,e),i=s.packageManager??Le();await Dr(i,["run",t,...s.args??[]],{cwd:s.cwd??(0,O0.cwd)(),task:s.task})}function h2(r){let t={bun:"bunx",pnpm:"npx",yarn:"npx",npm:"npx"}[r];if(!t)throw new Error(`No executor found for the detected package manager: ${r}`);let[s,...i]=t.split(" ");return{command:s,args:i}}function k5(r,e){return Array.isArray(r)?{packages:r,mergedOptions:{...e,packages:r}}:{packages:r.packages??[],mergedOptions:r}}function f2(r,e){return Array.isArray(r)?{args:r,mergedOptions:e??{}}:{args:[],mergedOptions:r}}function _5(r,e){return typeof r=="string"?{script:r,mergedOptions:e??{}}:{script:r.script,mergedOptions:r}}function A5(r){let e=r.split(/\s+/),t=e[0],s=e.slice(1);if(t==="pf"){t=process.argv[1];let i=s.filter(o=>o.startsWith("-"));s=[...s.filter(o=>!o.startsWith("-")),...i]}return{executable:t,args:s}}async function F5(r,e){try{let{executable:t,args:s}=A5(r.command);return await Dr(t,s,{cwd:e,shell:!1}),{command:r.command,success:!0}}catch(t){return{command:r.command,success:!1,error:t instanceof Error?t.message:String(t)}}}async function d2(r,e){let t=[];for(let s of r){let i=await F5(s,e);if(t.push(i),!i.success)break}return t}function p2(r){let e=[/^pf\s+new\s+/,/^pf\s+add\s+/,/^bun\s+pf\s+new\s+/,/^bun\s+pf\s+add\s+/];return r.filter(t=>e.some(s=>s.test(t.command)))}function m2(r){for(let e of r){let t=e.command.match(/(?:bun\s+)?pf\s+new\s+\S+\s+(\S+)/);if(t)return t[1]}return null}var qs=require("node:fs"),R0=require("node:path"),x2=r=>{let e=(0,R0.join)(r,"packages","contracts","src","schemas");if(!(0,qs.existsSync)(e))return[];let t=[];try{let s=(0,qs.readdirSync)(e).filter(i=>i.endsWith(".schema.ts"));for(let i of s){let n=(0,R0.join)(e,i),o=(0,qs.readFileSync)(n,"utf-8"),a=T5(o,n);a&&t.push(a)}}catch(s){return console.warn("Warning: Could not scan contracts package:",s),[]}return t},T5=(r,e)=>{try{let t=r.match(/export const (\w+Contract) = createContract/);if(!t)return null;let s=t[1],i=r.match(/export type (\w+(?:Data|Event)) = z\.infer/);if(!i)return null;let n=i[1],o=r.match(/type:\s*['"]([^'"]+)['"]/);if(!o)return null;let a=o[1],c=P5(r);return{name:s,typeName:n,eventType:a,fields:c,filePath:e}}catch{return null}},P5=r=>{let e=[],t=r.match(/z\.object\({([^}]+)\}/);if(!t)return e;let s=t[1],i=/(\w+):\s*z\.(\w+)\([^)]*\)/g,n;for(;(n=i.exec(s))!==null;){let[,o,a]=n,l={string:"string",number:"number",boolean:"boolean",array:"array",object:"object",date:"date"}[a]||a;e.push(`${o}: ${l}`)}if(r.includes("z.array(")){let o=/(\w+):\s*z\.array\(/g;for(;(n=o.exec(r))!==null;){let a=n[1];e.some(c=>c.startsWith(a))||e.push(`${a}: array`)}}return e},g2=(r,e)=>{if(r.length===0)return`
|
|
146
146
|
**Available Contracts:**
|
|
147
147
|
|
|
@@ -163,7 +163,7 @@ The following event contracts are available in \`${e}/contracts\`:
|
|
|
163
163
|
${t}
|
|
164
164
|
|
|
165
165
|
If the event type your service consumes matches one of these, **you MUST use Advanced Mode**.
|
|
166
|
-
`.trim()};var Xr=require("node:fs"),ea=require("node:path");function b2(r,e){let t={written:[],skipped:[],failed:[]};for(let s of r){let
|
|
166
|
+
`.trim()};var Xr=require("node:fs"),ea=require("node:path");function b2(r,e){let t={written:[],skipped:[],failed:[]};for(let s of r){let n=s.path.startsWith("packages/")&&e.workspaceRoot?e.workspaceRoot:e.baseDir,o=(0,ea.join)(n,s.path);try{if((0,Xr.existsSync)(o)&&!e.overwrite){t.skipped.push(s.path);continue}if(e.dryRun){t.written.push(s.path);continue}let a=(0,ea.dirname)(o);(0,Xr.existsSync)(a)||(0,Xr.mkdirSync)(a,{recursive:!0}),(0,Xr.writeFileSync)(o,s.content,"utf-8"),t.written.push(s.path)}catch(a){t.failed.push({path:s.path,error:a.message})}}return t}var ta=require("node:fs"),y2=require("node:path");var O5=[/####\s+`?([^\s`\n]+\.[a-z]+)`?\s*\n+```(\w+)?\n([\s\S]*?)```/gi,/\*\*(?:File|Datei):?\*\*\s*`?([^\s`\n]+\.[a-z]+)`?\s*\n+```(\w+)?\n([\s\S]*?)```/gi,/(?:File|Datei):?\s*`?([^\s`\n]+\.[a-z]+)`?\s*\n+```(\w+)?\n([\s\S]*?)```/gi,/`([^\s`]+\.[a-z]+)`[:\s]*\n+```(\w+)?\n([\s\S]*?)```/gi],I5=/```(\w+)?\n([\s\S]*?)```/g;function B5(r,e,t){let s=[[/new Hono\(\)/i,"src/index.ts"],[/@Module\(/i,"src/app.module.ts"],[/@Controller\(/i,"src/app.controller.ts"],[/@Injectable\(/i,"src/app.service.ts"],[/NestFactory\.create/i,"src/main.ts"],[/"name":\s*"[^"]+"/i,"package.json"],[/^FROM\s+/im,"Dockerfile"],[/"compilerOptions"/i,"tsconfig.json"],[/^[A-Z_]+=.+$/m,".env"]];for(let[o,a]of s)if(o.test(r))return a;let n={typescript:"ts",ts:"ts",javascript:"js",js:"js",json:"json",toml:"toml",yaml:"yaml",yml:"yml",plaintext:"txt",env:"env",dockerfile:""}[e?.toLowerCase()||"ts"]??"ts";return n?`src/file-${t+1}.${n}`:null}function R5(r){let e=[],t=/```commands?\n([\s\S]*?)```/gi,s=[...r.matchAll(t)];for(let i of s){let o=i[1].trim().split(`
|
|
167
167
|
`).filter(a=>a.trim());for(let a of o){let c=a.trim();c&&!c.startsWith("#")&&e.push({command:c,isPfCommand:c.startsWith("pf ")||c.includes(" pf ")})}}return e}function M5(r){let e=[],t=/```(?:bash|shell|sh)\n([\s\S]*?)```/gi,s=[...r.matchAll(t)];for(let i of s){let o=i[1].trim().split(`
|
|
168
168
|
`).filter(a=>a.trim());for(let a of o){let c=a.trim();c&&(c.startsWith("pf ")||c.startsWith("bun pf "))&&e.push({command:c.replace(/^bun\s+/,""),isPfCommand:!0})}}return e}function $5(r){let e=R5(r);return e.length>0?e:M5(r)}function N5(){try{let r=ae(),e=(0,y2.join)(r,"package.json");if(!(0,ta.existsSync)(e))return null;let s=JSON.parse((0,ta.readFileSync)(e,"utf-8")).name;return s?s.startsWith("@")?s.slice(1).split("/")[0]:s:null}catch{return null}}function j5(r){let t=/```dependencies?\n([\s\S]*?)```/gi.exec(r);if(!t)return[];let n=t[1].trim().split(`
|
|
169
169
|
`).filter(a=>a.trim()).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#")),o=N5();return n.map(a=>o&&a.match(new RegExp(`^@${o}/[\\w-]+$`))?`${a}@workspace:*`:a.match(/^@crossdelta\/[\w-]+$/)?`${a}@workspace:*`:a)}function L5(r,e){let t=[`${e}/`,`services/${e}/`,"./"];for(let s of t)if(r.startsWith(s))return r.slice(s.length);return r}function W5(r,e,t){let s=[];for(let i of O5){let n=[...r.matchAll(i)];for(let o of n){let[,a,c="typescript",l]=o,u=L5(a.trim(),e);t.has(u)||(t.add(u),s.push({path:u,content:l.trim(),language:c||"typescript"}))}}return s}function U5(r,e){let t=[],s=[...r.matchAll(I5)],i=0;for(let n of s){let[,o="typescript",a]=n,c=B5(a,o,i);c&&!e.has(c)&&(e.add(c),t.push({path:c,content:a.trim(),language:o||"typescript"})),i++}return t}function q5(r,e){let t=new Set,s=W5(r,e,t);return s.length>0?s:U5(r,t)}function w2(r,e){return{commands:$5(r),files:q5(r,e),dependencies:j5(r)}}function D2(r){let e=[];r.length===0&&e.push("No code files could be extracted from the AI response");let t=r.some(s=>s.path==="src/index.ts"||s.path==="src/main.ts"||s.path==="index.ts");r.length>0&&!t&&e.push("No entry point file (src/index.ts or src/main.ts) found");for(let s of r)(!s.content||s.content.trim().length===0)&&e.push(`File ${s.path} is empty`);return{valid:e.length===0,errors:e}}var H5=["docs/ai-guidelines.md"],G5=`You are an expert code generator. Generate clean, production-ready code.
|
|
@@ -190,8 +190,8 @@ Format your code blocks with the file path on the line before the code block:
|
|
|
190
190
|
|
|
191
191
|
Follow these project-specific conventions and patterns when generating code:
|
|
192
192
|
|
|
193
|
-
${e}`},Z5=r=>`${
|
|
194
|
-
`,X5=r=>
|
|
193
|
+
${e}`},Z5=r=>`${G.default.dim(" \u2022 ")+G.default.cyan(r)}
|
|
194
|
+
`,X5=r=>G.default.dim(` \u2728 Generated ${G.default.white.bold(r)} tokens
|
|
195
195
|
`),Q5=()=>{let r="",e=0,t=Date.now(),s=()=>{let n=r.match(/####\s+`([^`]+)`/);if(!n)return!1;process.stdout.write(Z5(n[1]));let o=r.indexOf(n[0])+n[0].length;return r=r.slice(o),!0},i=n=>{r+=n,e++,s(),r.length>500&&(r=r.slice(-200))};return i.getStats=()=>{let n=((Date.now()-t)/1e3).toFixed(2),o=(e/Number.parseFloat(n)).toFixed(0);return{tokenCount:e,duration:n,tokensPerSec:o}},i},eC=(r,e,t)=>`Generate source code for a ${t==="hono"?"Hono":"NestJS"} microservice at path "${r}".
|
|
196
196
|
|
|
197
197
|
Use the patterns and conventions from the Project Guidelines above.
|
|
@@ -224,25 +224,25 @@ FORMATTING RULES:
|
|
|
224
224
|
- Format each file with the path header (e.g., #### \`src/index.ts\`)
|
|
225
225
|
- Do NOT include the service path in file headers (use \`src/index.ts\`, NOT \`${r}/src/index.ts\`)
|
|
226
226
|
- Ensure code blocks are properly closed
|
|
227
|
-
`,tC=(r,e)=>{let t=(0,Mt.join)(e.workspaceRoot,"packages/contracts");if(!(0,Xt.existsSync)(t))return[];let s=/import\s+(?:type\s+)?{([^}]+)}\s+from\s+['"]@\w+\/contracts['"]/g,i=new Set;for(let n of r){let o;for(;(o=s.exec(n.content))!==null;)o[1].split(",").map(c=>c.trim()).map(c=>c.replace(/^type\s+/,"").replace(/\s+as\s+.+$/,"").trim()).filter(c=>c.endsWith("Contract")).forEach(c=>i.add(c))}return Array.from(i).sort()},rC=(r,e)=>{r.length!==0&&(console.log(
|
|
228
|
-
`)),r.forEach(t=>{console.log(
|
|
227
|
+
`,tC=(r,e)=>{let t=(0,Mt.join)(e.workspaceRoot,"packages/contracts");if(!(0,Xt.existsSync)(t))return[];let s=/import\s+(?:type\s+)?{([^}]+)}\s+from\s+['"]@\w+\/contracts['"]/g,i=new Set;for(let n of r){let o;for(;(o=s.exec(n.content))!==null;)o[1].split(",").map(c=>c.trim()).map(c=>c.replace(/^type\s+/,"").replace(/\s+as\s+.+$/,"").trim()).filter(c=>c.endsWith("Contract")).forEach(c=>i.add(c))}return Array.from(i).sort()},rC=(r,e)=>{r.length!==0&&(console.log(G.default.cyan(`\u{1F4E6} Using existing contracts:
|
|
228
|
+
`)),r.forEach(t=>{console.log(G.default.dim(` ${e}/contracts \u2192 ${t}`))}),console.log())},sC=async r=>{console.log(G.default.cyan(`
|
|
229
229
|
\u25B6 Scaffolding service structure...
|
|
230
|
-
`));let t=(await d2(r,process.cwd())).find(s=>!s.success);if(t)throw console.log(
|
|
231
|
-
\u2717 Scaffolding failed: ${t.command}`)),t.error&&console.log(
|
|
232
|
-
`))},iC=async(r,e,t)=>{t.stop(),console.log(
|
|
230
|
+
`));let t=(await d2(r,process.cwd())).find(s=>!s.success);if(t)throw console.log(G.default.red(`
|
|
231
|
+
\u2717 Scaffolding failed: ${t.command}`)),t.error&&console.log(G.default.red(` Error: ${t.error}`)),new Error(`Command failed: ${t.command}${t.error?` - ${t.error}`:""}`);console.log(G.default.green(`\u2713 Service scaffolding complete
|
|
232
|
+
`))},iC=async(r,e,t)=>{t.stop(),console.log(G.default.cyan(`
|
|
233
233
|
\u25B6 Installing ${r.length} additional ${r.length===1?"package":"packages"}...
|
|
234
|
-
`));try{await vr({packages:r,cwd:e,flags:["--silent"]}),console.log(
|
|
235
|
-
`))}catch(s){throw console.log(
|
|
236
|
-
`)),s}},nC=async(r,e)=>{e.start("Applying code formatting...");try{await Rt("biome",["check","--fix","--unsafe",r],{cwd:process.cwd(),quiet:!0}),e.succeed("Code formatted!")}catch{e.warn("Code formatting completed with warnings")}},oC=async(r,e)=>{e.start("Scanning for event handlers...");let{mockPaths:t,skippedHandlers:s}=await Jo(r);t.length>0?(e.succeed(`Generated ${t.length} event ${t.length===1?"mock":"mocks"}`),console.log(),t.forEach(i=>{let n=i.replace(r+"/","");console.log(
|
|
237
|
-
`)+
|
|
238
|
-
`)};try{return{valid:!0,config:In()}}catch(r){return{valid:!1,error:
|
|
239
|
-
`)}}},ra=async r=>r||Ii({message:"\u{1F4DD} Describe what this service should do:",validate:e=>e.trim().length>0||"Please provide a description for the AI to generate code"}),sa=async(r,e)=>{let{servicePath:t,description:s="",serviceType:i}=r,n=t.split("/").pop()||t,o=V5();console.log(
|
|
234
|
+
`));try{await vr({packages:r,cwd:e,flags:["--silent"]}),console.log(G.default.green(`\u2713 Installed ${r.length} ${r.length===1?"package":"packages"}
|
|
235
|
+
`))}catch(s){throw console.log(G.default.red(`\u2717 Package installation failed
|
|
236
|
+
`)),s}},nC=async(r,e)=>{e.start("Applying code formatting...");try{await Rt("biome",["check","--fix","--unsafe",r],{cwd:process.cwd(),quiet:!0}),e.succeed("Code formatted!")}catch{e.warn("Code formatting completed with warnings")}},oC=async(r,e)=>{e.start("Scanning for event handlers...");let{mockPaths:t,skippedHandlers:s}=await Jo(r);t.length>0?(e.succeed(`Generated ${t.length} event ${t.length===1?"mock":"mocks"}`),console.log(),t.forEach(i=>{let n=i.replace(r+"/","");console.log(G.default.dim(` ${n}`))}),console.log()):e.info("No event handlers found"),s.length>0&&(s.forEach(i=>{console.log(G.default.dim(` \u2139 Skipped: ${i}`))}),console.log())},aC=async(r,e,t,s)=>{s.start("Writing generated files...");let i=b2(r,{baseDir:e,workspaceRoot:t.workspaceRoot,overwrite:!0});i.written.length>0?(s.succeed(`Created ${i.written.length} files`),console.log(),i.written.forEach(o=>console.log(G.default.dim(` ${o}`))),console.log()):s.succeed("No files to write");let n=tC(r,t);rC(n,t.scope),i.written.length>0&&(await nC(e,s),await oC(e,s))},Hs=()=>{if(!yi())return{valid:!1,error:G.default.red.bold(`\u274C AI configuration not found.
|
|
237
|
+
`)+G.default.yellow("Please run ")+G.default.cyan.bold("pf setup --ai")+G.default.yellow(` first to configure your AI provider.
|
|
238
|
+
`)};try{return{valid:!0,config:In()}}catch(r){return{valid:!1,error:G.default.red.bold(`\u274C ${r.message}
|
|
239
|
+
`)}}},ra=async r=>r||Ii({message:"\u{1F4DD} Describe what this service should do:",validate:e=>e.trim().length>0||"Please provide a description for the AI to generate code"}),sa=async(r,e)=>{let{servicePath:t,description:s="",serviceType:i}=r,n=t.split("/").pop()||t,o=V5();console.log(G.default.cyan.bold(`
|
|
240
240
|
\u{1F916} AI Generation
|
|
241
|
-
`));let a=eC(t,s,i),c=J5(o),l=Q5(),u=await ep(e,a,{system:c,maxTokens:8192,temperature:.7,onToken:l}),h=l.getStats();console.log(X5(h.tokenCount)),console.log();let f=(0,v2.default)({text:"Processing AI response...",color:"cyan"}).start(),d=w2(u,n),{commands:p,files:m,dependencies:b}=d,x=p2(p);f.text="Validating extracted files...";let g=D2(m);if(!g.valid)return f.warn("Could not extract structured files from AI response"),console.log(
|
|
242
|
-
Warnings:`)),g.errors.forEach(k=>console.log(
|
|
243
|
-
`))),await aC(y,v,o,f),b.length>0&&await iC(b,v,f),!0},ia=(r,e,t)=>{console.log(
|
|
241
|
+
`));let a=eC(t,s,i),c=J5(o),l=Q5(),u=await ep(e,a,{system:c,maxTokens:8192,temperature:.7,onToken:l}),h=l.getStats();console.log(X5(h.tokenCount)),console.log();let f=(0,v2.default)({text:"Processing AI response...",color:"cyan"}).start(),d=w2(u,n),{commands:p,files:m,dependencies:b}=d,x=p2(p);f.text="Validating extracted files...";let g=D2(m);if(!g.valid)return f.warn("Could not extract structured files from AI response"),console.log(G.default.yellow(`
|
|
242
|
+
Warnings:`)),g.errors.forEach(k=>console.log(G.default.yellow(` \u2022 ${k}`))),!1;f.succeed(`Extracted ${m.length} files`);let y=m.filter(k=>!["package.json","tsconfig.json","Dockerfile"].includes(k.path)),w=m2(x);w&&!w.includes("/")&&!w.startsWith(".")&&(w=`${Jt().services}/${w}`);let v=w?(0,Mt.resolve)(o.cwd,w):(0,Mt.resolve)(o.cwd,t);return x.length>0?await sC(x):((0,Xt.mkdirSync)(v,{recursive:!0}),console.log(G.default.yellow(`\u26A0 No scaffolding command found - creating empty directory
|
|
243
|
+
`))),await aC(y,v,o,f),b.length>0&&await iC(b,v,f),!0},ia=(r,e,t)=>{console.log(G.default.cyan.bold(`
|
|
244
244
|
\u{1F916} AI-Powered Generation
|
|
245
|
-
`)),console.log(
|
|
245
|
+
`)),console.log(G.default.dim(`Service: ${r.split("/").pop()}`)),console.log(G.default.dim(`Path: ${r}`)),console.log(G.default.dim(`Model: ${e.model}`)),console.log(G.default.dim(`Description: ${t}
|
|
246
246
|
`))};var na=require("node:fs"),an=E(require("node:path")),M0=E(require("chalk"));var E2=async r=>{await Dr("bash",["-c",`
|
|
247
247
|
curl -fsSL https://bun.sh/install | bash && if [ -f "$HOME/.bun/bin/bun" ]; then export PATH="$HOME/.bun/bin:$PATH"; fi
|
|
248
248
|
`],{task:r,shell:!0}),await cC()},$0=()=>Mi("bun"),S2=()=>qu([{label:"Bun",value:59026,color:"magenta",barColor:"magenta"},{label:"Deno",value:25335},{label:"Node.js",value:19039}],{title:'Express.js "hello world" HTTP requests per second (Linux x64)',barColor:"blackBright"}),k2=()=>qu([{label:"Bun",value:.36,color:"magenta",barColor:"magenta"},{label:"pnpm",value:6.44},{label:"npm",value:10.58},{label:"yarn",value:12.08}],{title:"Bun is an npm-compatible package manager.",unit:"s",footer:M0.default.blackBright("* Installing dependencies from cache for a Remix app."),barColor:"blackBright"}),cC=async()=>{let r='export PATH="$HOME/.bun/bin:$PATH"',e=process.env.HOME;if(!e){S.error("Could not detect $HOME environment variable. Cannot update shell config.");return}let t=[an.default.join(e,".zshrc"),an.default.join(e,".bashrc"),an.default.join(e,".bash_profile")];for(let s of t)try{if(await na.promises.stat(s).then(()=>!0).catch(()=>!1)){(await na.promises.readFile(s,"utf8")).includes(r)||(await na.promises.appendFile(s,`
|
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
These rules define how AI-generated scaffolded services must be structured when produced by CLI tools.
|
|
4
4
|
|
|
5
|
+
## 🚨 EMERGENCY RULES - READ FIRST 🚨
|
|
6
|
+
|
|
7
|
+
**FOR EVENT CONSUMER SERVICES (services that "listen to", "consume", "react to" events):**
|
|
8
|
+
|
|
9
|
+
**CRITICAL:** Contract schema fields MUST match event handler usage!
|
|
10
|
+
|
|
11
|
+
**MANDATORY 4-STEP WORKFLOW:**
|
|
12
|
+
|
|
13
|
+
1. **Analyze service description** → Extract required fields
|
|
14
|
+
Example: "needs orderId, customerId, total" → Fields: `orderId`, `customerId`, `total`
|
|
15
|
+
|
|
16
|
+
2. **CREATE contract FIRST** (`packages/contracts/src/events/<event-name>.ts`):
|
|
17
|
+
```ts
|
|
18
|
+
export const OrderCreatedContract = createContract({
|
|
19
|
+
type: 'order.created',
|
|
20
|
+
schema: z.object({
|
|
21
|
+
orderId: z.string(), // ✅ From step 1
|
|
22
|
+
customerId: z.string(), // ✅ From step 1
|
|
23
|
+
total: z.number(), // ✅ From step 1
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. **Export contract** (`packages/contracts/src/index.ts`):
|
|
29
|
+
```ts
|
|
30
|
+
export * from './events/order-created'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. **Create event handler** that uses SAME fields from contract:
|
|
34
|
+
```ts
|
|
35
|
+
export default handleEvent(OrderCreatedContract, async (data) => {
|
|
36
|
+
await sendNotification({
|
|
37
|
+
orderId: data.orderId, // ✅ Exists in contract
|
|
38
|
+
customerId: data.customerId, // ✅ Exists in contract
|
|
39
|
+
total: data.total, // ✅ Exists in contract
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**DO NOT create contracts with generic fields like `id`, `createdAt` - use the actual fields from the service description!**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
5
48
|
## ⚠️ CRITICAL: Code Quality Guidelines
|
|
6
49
|
|
|
7
50
|
**NEVER invent APIs, package names, or TypeScript types:**
|
|
@@ -100,15 +143,50 @@ Rules:
|
|
|
100
143
|
|
|
101
144
|
Generated services must include:
|
|
102
145
|
|
|
103
|
-
|
|
146
|
+
**For Event Consumers:**
|
|
147
|
+
- `src/index.ts` (with NATS setup)
|
|
104
148
|
- `src/events/*.event.ts` (event handlers - NOT `src/handlers/`)
|
|
105
149
|
- `src/use-cases/*.use-case.ts`
|
|
106
150
|
- `src/use-cases/*.test.ts`
|
|
151
|
+
- `src/types/*.ts` (Zod schemas and types)
|
|
152
|
+
- `packages/contracts/src/events/*.ts` (event contracts)
|
|
153
|
+
- `packages/contracts/src/index.ts` (export contract)
|
|
154
|
+
- `README.md`
|
|
155
|
+
|
|
156
|
+
**For Event Publishers:**
|
|
157
|
+
- `src/index.ts` (REST API)
|
|
158
|
+
- `src/use-cases/*.use-case.ts`
|
|
159
|
+
- `src/use-cases/*.test.ts`
|
|
160
|
+
- `src/types/*.ts` (Zod schemas and types)
|
|
161
|
+
- `packages/contracts/src/events/*.ts` (RECOMMENDED: contracts for published events)
|
|
162
|
+
- `packages/contracts/src/index.ts` (export contract)
|
|
107
163
|
- `README.md`
|
|
108
164
|
|
|
165
|
+
**Example for Event Consumer:**
|
|
166
|
+
```
|
|
167
|
+
services/order-notifications/
|
|
168
|
+
├── src/
|
|
169
|
+
│ ├── index.ts # NATS consumer setup
|
|
170
|
+
│ ├── events/
|
|
171
|
+
│ │ └── order-created.event.ts # Event handler
|
|
172
|
+
│ ├── use-cases/
|
|
173
|
+
│ │ ├── send-notification.use-case.ts
|
|
174
|
+
│ │ └── send-notification.test.ts
|
|
175
|
+
│ └── types/
|
|
176
|
+
│ └── notifications.ts # Zod schemas/types
|
|
177
|
+
└── README.md
|
|
178
|
+
|
|
179
|
+
packages/contracts/
|
|
180
|
+
├── src/
|
|
181
|
+
│ ├── events/
|
|
182
|
+
│ │ └── order-created.ts # Contract definition
|
|
183
|
+
│ └── index.ts # Export contract
|
|
184
|
+
```
|
|
185
|
+
|
|
109
186
|
Rules:
|
|
110
187
|
- Paths must be relative to the service root.
|
|
111
188
|
- Event handlers MUST go in `src/events/`, NOT `src/handlers/`.
|
|
189
|
+
- Contracts MUST go in `packages/contracts/src/events/`, NOT in service directory.
|
|
112
190
|
- Do NOT generate controller or module folders unless requested.
|
|
113
191
|
|
|
114
192
|
---
|
|
@@ -200,6 +278,55 @@ Bun.serve({ port, fetch: app.fetch })
|
|
|
200
278
|
|
|
201
279
|
**Example:** "Notification service that sends emails when orders are created. Consumes order.created events."
|
|
202
280
|
|
|
281
|
+
**⚠️⚠️⚠️ MANDATORY WORKFLOW FOR EVENT CONSUMERS ⚠️⚠️⚠️**
|
|
282
|
+
|
|
283
|
+
**YOU MUST FOLLOW THIS EXACT ORDER:**
|
|
284
|
+
|
|
285
|
+
**STEP 1: Analyze Service Description**
|
|
286
|
+
Extract the required event data fields from the description.
|
|
287
|
+
|
|
288
|
+
Example: "sends push notifications when orders are created. Needs orderId, customerId, and total"
|
|
289
|
+
→ Required fields: `orderId`, `customerId`, `total`
|
|
290
|
+
|
|
291
|
+
**STEP 2: CREATE Contract FIRST (packages/contracts/src/events/<event-name>.ts)**
|
|
292
|
+
```ts
|
|
293
|
+
import { createContract } from '@crossdelta/cloudevents'
|
|
294
|
+
import { z } from 'zod'
|
|
295
|
+
|
|
296
|
+
export const OrderCreatedContract = createContract({
|
|
297
|
+
type: 'order.created',
|
|
298
|
+
schema: z.object({
|
|
299
|
+
orderId: z.string(), // ✅ From Step 1
|
|
300
|
+
customerId: z.string(), // ✅ From Step 1
|
|
301
|
+
total: z.number(), // ✅ From Step 1
|
|
302
|
+
}),
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**STEP 3: Export Contract (packages/contracts/src/index.ts)**
|
|
309
|
+
```ts
|
|
310
|
+
export * from './events/order-created'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**STEP 4: Create Event Handler (src/events/order-created.event.ts)**
|
|
314
|
+
```ts
|
|
315
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
316
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
317
|
+
import { sendNotification } from '../use-cases/send-notification.use-case'
|
|
318
|
+
|
|
319
|
+
export default handleEvent(OrderCreatedContract, async (data: OrderCreatedData) => {
|
|
320
|
+
await sendNotification({
|
|
321
|
+
orderId: data.orderId, // ✅ Field exists in contract
|
|
322
|
+
customerId: data.customerId, // ✅ Field exists in contract
|
|
323
|
+
total: data.total, // ✅ Field exists in contract
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**CRITICAL:** The contract schema fields (Step 2) MUST match the event handler usage (Step 4)!
|
|
329
|
+
|
|
203
330
|
**Structure:**
|
|
204
331
|
- ✅ Event handlers in `src/events/*.event.ts`
|
|
205
332
|
- ✅ Use `consumeJetStreamEvents()` in index.ts
|
|
@@ -536,140 +663,138 @@ The platform supports **Advanced Mode** (contracts) with automatic creation for
|
|
|
536
663
|
|
|
537
664
|
### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
|
|
538
665
|
|
|
539
|
-
**Always use Advanced Mode (contracts) -
|
|
666
|
+
**Always use Advanced Mode (contracts) - You MUST create complete contracts!**
|
|
540
667
|
|
|
541
668
|
When generating an Event Consumer service:
|
|
542
669
|
|
|
543
|
-
1. **
|
|
544
|
-
2. **
|
|
545
|
-
3. **
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
670
|
+
1. **CREATE the contract** in `packages/contracts/src/events/` with **complete schema** based on the service description
|
|
671
|
+
2. **Use the contract** in the event handler (`import { OrderCreatedContract } from '{{scope}}/contracts'`)
|
|
672
|
+
3. **Export the contract** in `packages/contracts/src/index.ts`
|
|
673
|
+
|
|
674
|
+
**⚠️ CRITICAL: Contract Schema Must Match Handler Usage!**
|
|
675
|
+
|
|
676
|
+
If the event handler uses fields like `orderId`, `customerId`, `total`, the contract schema MUST include those exact fields!
|
|
550
677
|
|
|
551
678
|
**Example Flow:**
|
|
552
679
|
```
|
|
553
|
-
User prompt: "
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
680
|
+
User prompt: "Notification service that sends push notifications when orders are created"
|
|
681
|
+
|
|
682
|
+
Step 1: Analyze what data the service needs:
|
|
683
|
+
- orderId (to identify the order)
|
|
684
|
+
- customerId (to send notification to customer)
|
|
685
|
+
- total (to show order amount)
|
|
686
|
+
|
|
687
|
+
Step 2: CREATE contract with those fields:
|
|
688
|
+
packages/contracts/src/events/order-created.ts:
|
|
689
|
+
schema: z.object({
|
|
690
|
+
orderId: z.string(),
|
|
691
|
+
customerId: z.string(),
|
|
692
|
+
total: z.number(),
|
|
693
|
+
createdAt: z.string().optional(), // Add optional context fields
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
Step 3: CREATE event handler that uses those fields:
|
|
697
|
+
services/order-notifications/src/events/order-created.event.ts:
|
|
698
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
699
|
+
export default handleEvent(OrderCreatedContract, async (data) => {
|
|
700
|
+
await sendNotification({
|
|
701
|
+
orderId: data.orderId, // ✅ Field exists in contract
|
|
702
|
+
customerId: data.customerId, // ✅ Field exists in contract
|
|
703
|
+
total: data.total, // ✅ Field exists in contract
|
|
704
|
+
})
|
|
705
|
+
})
|
|
559
706
|
```
|
|
560
707
|
|
|
561
|
-
**Why
|
|
562
|
-
- ✅
|
|
563
|
-
- ✅
|
|
564
|
-
- ✅
|
|
565
|
-
- ✅
|
|
708
|
+
**Why Create Contracts Immediately?**
|
|
709
|
+
- ✅ Schema matches handler requirements from the start
|
|
710
|
+
- ✅ No schema mismatch errors at runtime
|
|
711
|
+
- ✅ Type-safety between contract and handler
|
|
712
|
+
- ✅ Single source of truth for event structure
|
|
566
713
|
|
|
567
|
-
**
|
|
714
|
+
**MANDATORY for Event Consumers:**
|
|
715
|
+
- Always create `packages/contracts/src/events/<event-name>.ts` file
|
|
716
|
+
- Schema must include ALL fields used by the event handler
|
|
717
|
+
- Export contract in `packages/contracts/src/index.ts`
|
|
718
|
+
- Include `pf event:generate` in post-commands to create mock JSON
|
|
568
719
|
|
|
569
720
|
### 🟢 ADVANCED MODE (Default - Recommended)
|
|
570
721
|
|
|
571
|
-
**Use Advanced Mode for Event Consumers** -
|
|
572
|
-
|
|
573
|
-
1. **Add dependency** `{{scope}}/contracts` in dependencies block
|
|
574
|
-
2. **Import contract** in event handler
|
|
575
|
-
3. **Use with `handleEvent`** for type-safe event handling
|
|
576
|
-
|
|
577
|
-
**Example Event Handler:**
|
|
578
|
-
```ts
|
|
579
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
580
|
-
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
581
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
582
|
-
|
|
583
|
-
export default handleEvent(OrderCreatedContract,
|
|
584
|
-
async (data: OrderCreatedData) => {
|
|
585
|
-
await processOrder(data)
|
|
586
|
-
},
|
|
587
|
-
)
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
**Why Contracts as Default?**
|
|
591
|
-
- ✅ Strong type-safety between Publisher and Consumer
|
|
592
|
-
- ✅ Single source of truth for event schemas
|
|
593
|
-
- ✅ Schema evolution and versioning
|
|
594
|
-
- ✅ Automatic mock generation
|
|
595
|
-
- ✅ Better documentation
|
|
722
|
+
**Use Advanced Mode for Event Consumers** - you MUST create contracts with complete schemas:
|
|
596
723
|
|
|
597
|
-
|
|
724
|
+
**⚠️ CRITICAL CONTRACT CREATION RULES:**
|
|
598
725
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
**
|
|
604
|
-
|
|
605
|
-
1. **Check if contract exists** in `packages/contracts/src/events/`
|
|
606
|
-
2. **If exists:** Import and use the contract
|
|
607
|
-
3. **If NOT exists:** Create contract first in `packages/contracts`, then use it
|
|
608
|
-
|
|
609
|
-
**Advanced Mode Example:**
|
|
726
|
+
1. **Analyze service description** to determine required event fields
|
|
727
|
+
2. **CREATE contract file** in `packages/contracts/src/events/<event-name>.ts`
|
|
728
|
+
3. **Include ALL fields** that the event handler will use
|
|
729
|
+
4. **Export in index** - add to `packages/contracts/src/index.ts`
|
|
730
|
+
5. **Add dependency** `{{scope}}/contracts` to service's dependencies block
|
|
610
731
|
|
|
732
|
+
**Contract File Template:**
|
|
611
733
|
```ts
|
|
612
|
-
// packages/contracts/src/events
|
|
734
|
+
// packages/contracts/src/events/<event-name>.ts
|
|
613
735
|
import { createContract } from '@crossdelta/cloudevents'
|
|
614
736
|
import { z } from 'zod'
|
|
615
737
|
|
|
616
738
|
export const OrderCreatedContract = createContract({
|
|
617
|
-
type: 'order.created',
|
|
739
|
+
type: 'order.created', // Exact event type string
|
|
618
740
|
schema: z.object({
|
|
741
|
+
// REQUIRED: Fields used by the event handler
|
|
619
742
|
orderId: z.string(),
|
|
620
743
|
customerId: z.string(),
|
|
744
|
+
total: z.number(),
|
|
745
|
+
|
|
746
|
+
// OPTIONAL: Additional context fields
|
|
621
747
|
items: z.array(z.object({
|
|
622
748
|
productId: z.string(),
|
|
623
749
|
quantity: z.number(),
|
|
624
750
|
price: z.number(),
|
|
625
|
-
})),
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
createdAt: z.string(),
|
|
751
|
+
})).optional(),
|
|
752
|
+
status: z.enum(['created', 'processing', 'completed']).optional(),
|
|
753
|
+
createdAt: z.string().optional(),
|
|
629
754
|
}),
|
|
630
755
|
})
|
|
631
756
|
|
|
632
757
|
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
633
758
|
```
|
|
634
759
|
|
|
760
|
+
**Export in Index:**
|
|
635
761
|
```ts
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
640
|
-
|
|
641
|
-
export default handleEvent(
|
|
642
|
-
OrderCreatedContract, // Type-safe contract
|
|
643
|
-
async (data: OrderCreatedData) => {
|
|
644
|
-
await processOrder(data)
|
|
645
|
-
},
|
|
646
|
-
)
|
|
762
|
+
// packages/contracts/src/index.ts
|
|
763
|
+
export * from './events/order-created'
|
|
764
|
+
// ... other exports
|
|
647
765
|
```
|
|
648
766
|
|
|
767
|
+
**Event Handler Using Contract:**
|
|
649
768
|
```ts
|
|
650
|
-
// services/
|
|
651
|
-
import
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
769
|
+
// services/order-notifications/src/events/order-created.event.ts
|
|
770
|
+
import { handleEvent } from '@crossdelta/cloudevents'
|
|
771
|
+
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
772
|
+
import { sendNotification } from '../use-cases/send-notification.use-case'
|
|
773
|
+
|
|
774
|
+
export default handleEvent(OrderCreatedContract, async (data: OrderCreatedData) => {
|
|
775
|
+
// data has all fields from contract schema
|
|
776
|
+
await sendNotification({
|
|
777
|
+
orderId: data.orderId, // ✅ Exists in contract
|
|
778
|
+
customerId: data.customerId, // ✅ Exists in contract
|
|
779
|
+
total: data.total, // ✅ Exists in contract
|
|
780
|
+
})
|
|
781
|
+
})
|
|
656
782
|
```
|
|
657
783
|
|
|
658
|
-
**
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
```
|
|
784
|
+
**Why Contracts as Default?**
|
|
785
|
+
- ✅ Strong type-safety between Publisher and Consumer
|
|
786
|
+
- ✅ Single source of truth for event schemas
|
|
787
|
+
- ✅ Schema evolution and versioning
|
|
788
|
+
- ✅ Automatic mock generation
|
|
789
|
+
- ✅ Better documentation
|
|
667
790
|
|
|
668
|
-
|
|
791
|
+
**IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always create complete contracts for Event Consumers.
|
|
792
|
+
|
|
793
|
+
### 📋 DEPRECATED: Basic Mode
|
|
669
794
|
|
|
670
795
|
**Basic Mode is deprecated and no longer recommended.**
|
|
671
796
|
|
|
672
|
-
|
|
797
|
+
Always use Advanced Mode (contracts) for Event Consumers.
|
|
673
798
|
|
|
674
799
|
**Previous Basic Mode approach:**
|
|
675
800
|
- ❌ Local schemas in `src/types/events.ts`
|
|
@@ -677,69 +802,12 @@ The platform automatically creates missing contracts when you run `pf event:gene
|
|
|
677
802
|
- ❌ No type-safety between services
|
|
678
803
|
|
|
679
804
|
**Current approach (Advanced Mode only):**
|
|
680
|
-
- ✅ Always
|
|
681
|
-
- ✅
|
|
805
|
+
- ✅ Always create contracts in `packages/contracts/src/events/`
|
|
806
|
+
- ✅ Complete schemas with all required fields
|
|
682
807
|
- ✅ Type-safety across all services
|
|
683
808
|
- ✅ Single source of truth
|
|
684
809
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
export const processOrder = async (data: OrderCreatedData) => {
|
|
688
|
-
// Use data.orderId, data.customerId, etc.
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
{{AVAILABLE_CONTRACTS}}
|
|
693
|
-
|
|
694
|
-
**DO NOT:**
|
|
695
|
-
- ❌ Redefine schemas that exist in contracts
|
|
696
|
-
- ❌ Create inline schemas for existing event types
|
|
697
|
-
- ❌ Duplicate event type definitions
|
|
698
|
-
|
|
699
|
-
**Advanced Mode Example:**
|
|
700
|
-
```ts
|
|
701
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
702
|
-
import { OrderCreatedContract, type OrderCreatedData } from '{{scope}}/contracts'
|
|
703
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
704
|
-
|
|
705
|
-
export default handleEvent(
|
|
706
|
-
OrderCreatedContract, // Import from contracts package
|
|
707
|
-
async (data: OrderCreatedData) => {
|
|
708
|
-
await processOrder(data)
|
|
709
|
-
},
|
|
710
|
-
)
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
**If you need only a subset of fields**, extract them in the handler:
|
|
714
|
-
```ts
|
|
715
|
-
export default handleEvent(
|
|
716
|
-
OrderCreatedContract,
|
|
717
|
-
async (data: OrderCreatedData) => {
|
|
718
|
-
// Extract only what you need
|
|
719
|
-
const { orderId, items } = data
|
|
720
|
-
await updateInventory({ orderId, items })
|
|
721
|
-
},
|
|
722
|
-
)
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
**Contract Definition** (in `packages/contracts/src/schemas/order-created.schema.ts`):
|
|
726
|
-
```ts
|
|
727
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
728
|
-
import { z } from 'zod'
|
|
729
|
-
|
|
730
|
-
export const OrderCreatedSchema = z.object({
|
|
731
|
-
orderId: z.string(),
|
|
732
|
-
customerId: z.string(),
|
|
733
|
-
total: z.number(),
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
737
|
-
|
|
738
|
-
export const OrderCreatedContract = createContract({
|
|
739
|
-
type: 'orders.created',
|
|
740
|
-
schema: OrderCreatedSchema,
|
|
741
|
-
})
|
|
742
|
-
```
|
|
810
|
+
---
|
|
743
811
|
|
|
744
812
|
## General Event Handler Requirements
|
|
745
813
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossdelta/platform-sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Your AI-powered platform engineer. Scaffold complete Turborepo workspaces, generate microservice boilerplate with natural language, and deploy to the cloud — all from one CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|