@crossdelta/platform-sdk 0.7.6 → 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
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,13 +278,63 @@ 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
|
|
206
333
|
- ✅ Use `handleEvent()` for each event type
|
|
207
334
|
- ❌ Usually NO REST endpoints (except /health)
|
|
208
335
|
|
|
209
|
-
|
|
336
|
+
**⚠️ CRITICAL: Event Consumer index.ts Template - COPY THIS EXACTLY:**
|
|
337
|
+
|
|
210
338
|
```ts
|
|
211
339
|
import '@crossdelta/telemetry'
|
|
212
340
|
|
|
@@ -234,12 +362,19 @@ consumeJetStreamEvents({
|
|
|
234
362
|
})
|
|
235
363
|
```
|
|
236
364
|
|
|
237
|
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
365
|
+
**⚠️ MANDATORY CHECKLIST - Event Consumer index.ts MUST INCLUDE:**
|
|
366
|
+
- [ ] `import { consumeJetStreamEvents, ensureJetStreamStream } from '@crossdelta/cloudevents'`
|
|
367
|
+
- [ ] `await ensureJetStreamStream({ stream: 'ORDERS', subjects: ['orders.>'] })`
|
|
368
|
+
- [ ] `consumeJetStreamEvents({ stream: 'ORDERS', consumer: 'service-name', discover: './src/events/**/*.event.ts' })`
|
|
369
|
+
- [ ] Top-level await (Bun supports this natively)
|
|
370
|
+
- [ ] `ensureJetStreamStream()` called BEFORE `consumeJetStreamEvents()`
|
|
371
|
+
|
|
372
|
+
**CRITICAL ERRORS TO AVOID:**
|
|
373
|
+
- ❌ Missing `ensureJetStreamStream()` → Stream won't exist, events won't be consumed
|
|
374
|
+
- ❌ Missing `consumeJetStreamEvents()` → Service won't listen to events
|
|
375
|
+
- ❌ Wrong import order → Telemetry must be first
|
|
376
|
+
- ❌ Using `filterSubjects` in `ensureJetStreamStream()` → Use `subjects` parameter
|
|
377
|
+
- ❌ Using `subjects` in `consumeJetStreamEvents()` → Use `filterSubjects` parameter if needed
|
|
243
378
|
|
|
244
379
|
### � HTTP CloudEvent Receiver (Receives CloudEvents via HTTP POST)
|
|
245
380
|
|
|
@@ -528,140 +663,138 @@ The platform supports **Advanced Mode** (contracts) with automatic creation for
|
|
|
528
663
|
|
|
529
664
|
### 🎯 DECISION RULE (CRITICAL - READ FIRST!)
|
|
530
665
|
|
|
531
|
-
**Always use Advanced Mode (contracts) -
|
|
666
|
+
**Always use Advanced Mode (contracts) - You MUST create complete contracts!**
|
|
532
667
|
|
|
533
668
|
When generating an Event Consumer service:
|
|
534
669
|
|
|
535
|
-
1. **
|
|
536
|
-
2. **
|
|
537
|
-
3. **
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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!
|
|
542
677
|
|
|
543
678
|
**Example Flow:**
|
|
544
679
|
```
|
|
545
|
-
User prompt: "
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
})
|
|
551
706
|
```
|
|
552
707
|
|
|
553
|
-
**Why
|
|
554
|
-
- ✅
|
|
555
|
-
- ✅
|
|
556
|
-
- ✅
|
|
557
|
-
- ✅
|
|
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
|
|
558
713
|
|
|
559
|
-
**
|
|
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
|
|
560
719
|
|
|
561
720
|
### 🟢 ADVANCED MODE (Default - Recommended)
|
|
562
721
|
|
|
563
|
-
**Use Advanced Mode for Event Consumers** -
|
|
564
|
-
|
|
565
|
-
1. **Add dependency** `{{scope}}/contracts` in dependencies block
|
|
566
|
-
2. **Import contract** in event handler
|
|
567
|
-
3. **Use with `handleEvent`** for type-safe event handling
|
|
568
|
-
|
|
569
|
-
**Example Event Handler:**
|
|
570
|
-
```ts
|
|
571
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
572
|
-
import { OrderCreatedContract, type OrderCreatedData } from '@scope/contracts'
|
|
573
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
574
|
-
|
|
575
|
-
export default handleEvent(OrderCreatedContract,
|
|
576
|
-
async (data: OrderCreatedData) => {
|
|
577
|
-
await processOrder(data)
|
|
578
|
-
},
|
|
579
|
-
)
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Why Contracts as Default?**
|
|
583
|
-
- ✅ Strong type-safety between Publisher and Consumer
|
|
584
|
-
- ✅ Single source of truth for event schemas
|
|
585
|
-
- ✅ Schema evolution and versioning
|
|
586
|
-
- ✅ Automatic mock generation
|
|
587
|
-
- ✅ Better documentation
|
|
588
|
-
|
|
589
|
-
**IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always use contracts from there for Event Consumers.
|
|
590
|
-
|
|
591
|
-
### � ADVANCED MODE (Shared Contracts - RECOMMENDED DEFAULT)
|
|
722
|
+
**Use Advanced Mode for Event Consumers** - you MUST create contracts with complete schemas:
|
|
592
723
|
|
|
593
|
-
|
|
724
|
+
**⚠️ CRITICAL CONTRACT CREATION RULES:**
|
|
594
725
|
|
|
595
|
-
**
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
**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
|
|
602
731
|
|
|
732
|
+
**Contract File Template:**
|
|
603
733
|
```ts
|
|
604
|
-
// packages/contracts/src/events
|
|
734
|
+
// packages/contracts/src/events/<event-name>.ts
|
|
605
735
|
import { createContract } from '@crossdelta/cloudevents'
|
|
606
736
|
import { z } from 'zod'
|
|
607
737
|
|
|
608
738
|
export const OrderCreatedContract = createContract({
|
|
609
|
-
type: 'order.created',
|
|
739
|
+
type: 'order.created', // Exact event type string
|
|
610
740
|
schema: z.object({
|
|
741
|
+
// REQUIRED: Fields used by the event handler
|
|
611
742
|
orderId: z.string(),
|
|
612
743
|
customerId: z.string(),
|
|
744
|
+
total: z.number(),
|
|
745
|
+
|
|
746
|
+
// OPTIONAL: Additional context fields
|
|
613
747
|
items: z.array(z.object({
|
|
614
748
|
productId: z.string(),
|
|
615
749
|
quantity: z.number(),
|
|
616
750
|
price: z.number(),
|
|
617
|
-
})),
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
createdAt: z.string(),
|
|
751
|
+
})).optional(),
|
|
752
|
+
status: z.enum(['created', 'processing', 'completed']).optional(),
|
|
753
|
+
createdAt: z.string().optional(),
|
|
621
754
|
}),
|
|
622
755
|
})
|
|
623
756
|
|
|
624
757
|
export type OrderCreatedData = z.infer<typeof OrderCreatedContract.schema>
|
|
625
758
|
```
|
|
626
759
|
|
|
760
|
+
**Export in Index:**
|
|
627
761
|
```ts
|
|
628
|
-
//
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
632
|
-
|
|
633
|
-
export default handleEvent(
|
|
634
|
-
OrderCreatedContract, // Type-safe contract
|
|
635
|
-
async (data: OrderCreatedData) => {
|
|
636
|
-
await processOrder(data)
|
|
637
|
-
},
|
|
638
|
-
)
|
|
762
|
+
// packages/contracts/src/index.ts
|
|
763
|
+
export * from './events/order-created'
|
|
764
|
+
// ... other exports
|
|
639
765
|
```
|
|
640
766
|
|
|
767
|
+
**Event Handler Using Contract:**
|
|
641
768
|
```ts
|
|
642
|
-
// services/
|
|
643
|
-
import
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
})
|
|
648
782
|
```
|
|
649
783
|
|
|
650
|
-
**
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
```
|
|
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
|
|
790
|
+
|
|
791
|
+
**IMPORTANT:** The `packages/contracts` package is your Schema Registry. Always create complete contracts for Event Consumers.
|
|
659
792
|
|
|
660
|
-
###
|
|
793
|
+
### 📋 DEPRECATED: Basic Mode
|
|
661
794
|
|
|
662
795
|
**Basic Mode is deprecated and no longer recommended.**
|
|
663
796
|
|
|
664
|
-
|
|
797
|
+
Always use Advanced Mode (contracts) for Event Consumers.
|
|
665
798
|
|
|
666
799
|
**Previous Basic Mode approach:**
|
|
667
800
|
- ❌ Local schemas in `src/types/events.ts`
|
|
@@ -669,69 +802,12 @@ The platform automatically creates missing contracts when you run `pf event:gene
|
|
|
669
802
|
- ❌ No type-safety between services
|
|
670
803
|
|
|
671
804
|
**Current approach (Advanced Mode only):**
|
|
672
|
-
- ✅ Always
|
|
673
|
-
- ✅
|
|
805
|
+
- ✅ Always create contracts in `packages/contracts/src/events/`
|
|
806
|
+
- ✅ Complete schemas with all required fields
|
|
674
807
|
- ✅ Type-safety across all services
|
|
675
808
|
- ✅ Single source of truth
|
|
676
809
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
export const processOrder = async (data: OrderCreatedData) => {
|
|
680
|
-
// Use data.orderId, data.customerId, etc.
|
|
681
|
-
}
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
{{AVAILABLE_CONTRACTS}}
|
|
685
|
-
|
|
686
|
-
**DO NOT:**
|
|
687
|
-
- ❌ Redefine schemas that exist in contracts
|
|
688
|
-
- ❌ Create inline schemas for existing event types
|
|
689
|
-
- ❌ Duplicate event type definitions
|
|
690
|
-
|
|
691
|
-
**Advanced Mode Example:**
|
|
692
|
-
```ts
|
|
693
|
-
import { handleEvent } from '@crossdelta/cloudevents'
|
|
694
|
-
import { OrderCreatedContract, type OrderCreatedData } from '{{scope}}/contracts'
|
|
695
|
-
import { processOrder } from '../use-cases/process-order.use-case'
|
|
696
|
-
|
|
697
|
-
export default handleEvent(
|
|
698
|
-
OrderCreatedContract, // Import from contracts package
|
|
699
|
-
async (data: OrderCreatedData) => {
|
|
700
|
-
await processOrder(data)
|
|
701
|
-
},
|
|
702
|
-
)
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
**If you need only a subset of fields**, extract them in the handler:
|
|
706
|
-
```ts
|
|
707
|
-
export default handleEvent(
|
|
708
|
-
OrderCreatedContract,
|
|
709
|
-
async (data: OrderCreatedData) => {
|
|
710
|
-
// Extract only what you need
|
|
711
|
-
const { orderId, items } = data
|
|
712
|
-
await updateInventory({ orderId, items })
|
|
713
|
-
},
|
|
714
|
-
)
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
**Contract Definition** (in `packages/contracts/src/schemas/order-created.schema.ts`):
|
|
718
|
-
```ts
|
|
719
|
-
import { createContract } from '@crossdelta/cloudevents'
|
|
720
|
-
import { z } from 'zod'
|
|
721
|
-
|
|
722
|
-
export const OrderCreatedSchema = z.object({
|
|
723
|
-
orderId: z.string(),
|
|
724
|
-
customerId: z.string(),
|
|
725
|
-
total: z.number(),
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
export type OrderCreatedData = z.infer<typeof OrderCreatedSchema>
|
|
729
|
-
|
|
730
|
-
export const OrderCreatedContract = createContract({
|
|
731
|
-
type: 'orders.created',
|
|
732
|
-
schema: OrderCreatedSchema,
|
|
733
|
-
})
|
|
734
|
-
```
|
|
810
|
+
---
|
|
735
811
|
|
|
736
812
|
## General Event Handler Requirements
|
|
737
813
|
|
|
@@ -54,3 +54,18 @@ Always generate **minimal diffs**, never full rewrites.
|
|
|
54
54
|
- Use relative paths.
|
|
55
55
|
- Keep comments minimal.
|
|
56
56
|
- Do not generate abstractions not already present.
|
|
57
|
+
|
|
58
|
+
## Service-Specific Guidelines
|
|
59
|
+
|
|
60
|
+
When working with specific service types or packages, refer to these detailed guidelines:
|
|
61
|
+
|
|
62
|
+
### Service Generation & Architecture
|
|
63
|
+
- [Service Architecture Guidelines](../docs/generators/services/architecture-guidelines.md) - General patterns for all services
|
|
64
|
+
- [Hono Microservice Guidelines](../docs/generators/services/hono-micro-guidelines.md) - Event-driven Hono patterns
|
|
65
|
+
|
|
66
|
+
### Key Packages
|
|
67
|
+
- [@crossdelta/cloudevents](../packages/cloudevents/README.md) - Event handling with NATS and CloudEvents
|
|
68
|
+
- [@crossdelta/telemetry](../packages/telemetry/README.md) - OpenTelemetry instrumentation
|
|
69
|
+
- [@crossdelta/infrastructure](../packages/infrastructure/README.md) - Pulumi/K8s configuration
|
|
70
|
+
|
|
71
|
+
**When generating services**: Always check the appropriate guidelines above to ensure correct patterns, especially for event-driven architectures.
|
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",
|