@beignet/cli 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +70 -17
  3. package/dist/check.d.ts +2 -0
  4. package/dist/check.d.ts.map +1 -1
  5. package/dist/check.js +16 -0
  6. package/dist/check.js.map +1 -1
  7. package/dist/choices.d.ts +22 -2
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +62 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +2 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/create-prompts.d.ts +6 -2
  16. package/dist/create-prompts.d.ts.map +1 -1
  17. package/dist/create-prompts.js +1 -1
  18. package/dist/create-prompts.js.map +1 -1
  19. package/dist/db.d.ts +5 -5
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +5 -5
  22. package/dist/db.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +386 -27
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +5 -0
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +805 -62
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/make/inbox.d.ts.map +1 -1
  31. package/dist/make/inbox.js +5 -3
  32. package/dist/make/inbox.js.map +1 -1
  33. package/dist/make/payments.d.ts.map +1 -1
  34. package/dist/make/payments.js +58 -29
  35. package/dist/make/payments.js.map +1 -1
  36. package/dist/make/shared.d.ts +12 -3
  37. package/dist/make/shared.d.ts.map +1 -1
  38. package/dist/make/shared.js +52 -11
  39. package/dist/make/shared.js.map +1 -1
  40. package/dist/make/tenancy.d.ts.map +1 -1
  41. package/dist/make/tenancy.js +6 -4
  42. package/dist/make/tenancy.js.map +1 -1
  43. package/dist/make.d.ts.map +1 -1
  44. package/dist/make.js +136 -65
  45. package/dist/make.js.map +1 -1
  46. package/dist/outbox.d.ts +114 -1
  47. package/dist/outbox.d.ts.map +1 -1
  48. package/dist/outbox.js +190 -0
  49. package/dist/outbox.js.map +1 -1
  50. package/dist/preflight.d.ts +90 -0
  51. package/dist/preflight.d.ts.map +1 -0
  52. package/dist/preflight.js +423 -0
  53. package/dist/preflight.js.map +1 -0
  54. package/dist/provider-add.d.ts +10 -1
  55. package/dist/provider-add.d.ts.map +1 -1
  56. package/dist/provider-add.js +255 -16
  57. package/dist/provider-add.js.map +1 -1
  58. package/dist/task.d.ts.map +1 -1
  59. package/dist/task.js +2 -3
  60. package/dist/task.js.map +1 -1
  61. package/dist/templates/agents.d.ts.map +1 -1
  62. package/dist/templates/agents.js +6 -3
  63. package/dist/templates/agents.js.map +1 -1
  64. package/dist/templates/base.d.ts.map +1 -1
  65. package/dist/templates/base.js +5 -11
  66. package/dist/templates/base.js.map +1 -1
  67. package/dist/templates/index.d.ts.map +1 -1
  68. package/dist/templates/index.js +2 -0
  69. package/dist/templates/index.js.map +1 -1
  70. package/dist/templates/server.d.ts.map +1 -1
  71. package/dist/templates/server.js +10 -5
  72. package/dist/templates/server.js.map +1 -1
  73. package/dist/templates/shared.d.ts +5 -1
  74. package/dist/templates/shared.d.ts.map +1 -1
  75. package/dist/templates/shared.js +13 -1
  76. package/dist/templates/shared.js.map +1 -1
  77. package/dist/templates/testing.d.ts +5 -0
  78. package/dist/templates/testing.d.ts.map +1 -0
  79. package/dist/templates/testing.js +542 -0
  80. package/dist/templates/testing.js.map +1 -0
  81. package/dist/templates/todos.js +2 -2
  82. package/package.json +3 -2
  83. package/skills/app-structure/SKILL.md +12 -4
  84. package/src/check.ts +23 -0
  85. package/src/choices.ts +84 -0
  86. package/src/config.ts +4 -0
  87. package/src/create-prompts.ts +8 -2
  88. package/src/db.ts +12 -12
  89. package/src/index.ts +592 -37
  90. package/src/inspect.ts +1253 -156
  91. package/src/make/inbox.ts +5 -3
  92. package/src/make/payments.ts +58 -29
  93. package/src/make/shared.ts +87 -13
  94. package/src/make/tenancy.ts +6 -4
  95. package/src/make.ts +179 -65
  96. package/src/outbox.ts +363 -0
  97. package/src/preflight.ts +596 -0
  98. package/src/provider-add.ts +270 -16
  99. package/src/task.ts +2 -3
  100. package/src/templates/agents.ts +6 -3
  101. package/src/templates/base.ts +5 -13
  102. package/src/templates/index.ts +2 -0
  103. package/src/templates/server.ts +10 -5
  104. package/src/templates/shared.ts +15 -1
  105. package/src/templates/testing.ts +545 -0
  106. package/src/templates/todos.ts +2 -2
  107. package/src/test-helpers/generated-app.ts +10 -6
@@ -1,6 +1,10 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { type ProviderPresetName, providerPresetChoices } from "./choices.js";
3
+ import {
4
+ type IntegrationName,
5
+ type ProviderPresetName,
6
+ providerPresetChoices,
7
+ } from "./choices.js";
4
8
  import {
5
9
  directoryPath,
6
10
  loadBeignetConfig,
@@ -104,12 +108,105 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
104
108
  "Run beignet providers audit and beignet doctor --strict.",
105
109
  ],
106
110
  },
111
+ "jobs-bullmq": {
112
+ name: "jobs-bullmq",
113
+ displayName: "BullMQ jobs",
114
+ importName: "bullMQJobsProvider",
115
+ importFrom: "@beignet/provider-jobs-bullmq",
116
+ providerEntry: "bullMQJobsProvider",
117
+ dependencies: (packageJson) => ({
118
+ "@beignet/provider-jobs-bullmq": beignetDependencyVersion(packageJson),
119
+ bullmq: externalVersions.bullmq,
120
+ }),
121
+ appPort: {
122
+ key: "jobs",
123
+ typeName: "JobDispatcherPort",
124
+ importName: "JobDispatcherPort",
125
+ importFrom: "@beignet/core/ports",
126
+ },
127
+ envVars: ["BULLMQ_REDIS_URL"],
128
+ envExample: [
129
+ "# BullMQ jobs provider",
130
+ "BULLMQ_REDIS_URL=redis://localhost:6379",
131
+ "# BULLMQ_QUEUE_NAME=beignet-jobs",
132
+ "# BULLMQ_PREFIX=beignet",
133
+ "# BULLMQ_CONCURRENCY=5",
134
+ "",
135
+ ].join("\n"),
136
+ docs: [
137
+ "## BullMQ jobs",
138
+ "",
139
+ "- Package: `@beignet/provider-jobs-bullmq`",
140
+ "- Peer dependency: `bullmq`",
141
+ "- The preset wires `bullMQJobsProvider` in `server/providers.ts` and defers `jobs` in `infra/app-ports.ts`.",
142
+ "- Set `BULLMQ_REDIS_URL` before booting the app; the default points at local Redis.",
143
+ "- Dispatching enqueues jobs, but nothing executes them until a worker entrypoint runs `createBullMQJobWorker(...)` in a worker process. See the jobs docs page.",
144
+ "- Use `ctx.ports.jobs.dispatch(...)` in app code; `ctx.ports.bullMQJobs` is available as an escape hatch.",
145
+ "",
146
+ ].join("\n"),
147
+ nextSteps: [
148
+ "Run your package manager install command.",
149
+ "Set BULLMQ_REDIS_URL in .env.local or your deployment environment.",
150
+ "Add a worker entrypoint that runs createBullMQJobWorker(...) in a worker process; dispatched jobs do not execute without one. See the jobs docs page.",
151
+ "Run beignet providers audit and beignet doctor --strict.",
152
+ ],
153
+ conflictingProviderEntries: [
154
+ "inngestJobsProvider",
155
+ "createInngestJobsProvider",
156
+ "createBullMQJobsProvider",
157
+ ],
158
+ },
159
+ "jobs-inngest": {
160
+ name: "jobs-inngest",
161
+ displayName: "Inngest jobs",
162
+ importName: "inngestJobsProvider",
163
+ importFrom: "@beignet/provider-jobs-inngest",
164
+ providerEntry: "inngestJobsProvider",
165
+ dependencies: (packageJson) => ({
166
+ "@beignet/provider-jobs-inngest": beignetDependencyVersion(packageJson),
167
+ inngest: externalVersions.inngest,
168
+ }),
169
+ appPort: {
170
+ key: "jobs",
171
+ typeName: "JobDispatcherPort",
172
+ importName: "JobDispatcherPort",
173
+ importFrom: "@beignet/core/ports",
174
+ },
175
+ envVars: ["INNGEST_APP_NAME", "INNGEST_EVENT_KEY"],
176
+ envExample: [
177
+ "# Inngest jobs provider",
178
+ "INNGEST_APP_NAME=beignet-app",
179
+ "# INNGEST_EVENT_KEY=",
180
+ "",
181
+ ].join("\n"),
182
+ docs: [
183
+ "## Inngest jobs",
184
+ "",
185
+ "- Package: `@beignet/provider-jobs-inngest`",
186
+ "- Peer dependency: `inngest`",
187
+ "- The preset wires `inngestJobsProvider` in `server/providers.ts` and defers `jobs` in `infra/app-ports.ts`.",
188
+ "- Define jobs with `defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.",
189
+ "- Execution needs an Inngest serve route built from `createInngestJobFunction(...)` and `serve(...)`. See the jobs docs page.",
190
+ "- Use `ctx.ports.inngest` only as an escape hatch for Inngest-specific workflow APIs; prefer `ctx.ports.jobs.dispatch(...)` for Beignet jobs.",
191
+ "",
192
+ ].join("\n"),
193
+ nextSteps: [
194
+ "Run your package manager install command.",
195
+ "Set INNGEST_APP_NAME and, for Inngest cloud, INNGEST_EVENT_KEY in .env.local or your deployment environment.",
196
+ "Expose an Inngest serve route with createInngestJobFunction(...) so dispatched jobs execute. See the jobs docs page.",
197
+ "Run beignet providers audit and beignet doctor --strict.",
198
+ ],
199
+ conflictingProviderEntries: [
200
+ "bullMQJobsProvider",
201
+ "createBullMQJobsProvider",
202
+ ],
203
+ },
107
204
  "mail-resend": {
108
205
  name: "mail-resend",
109
206
  displayName: "Resend mail",
110
- importName: "mailResendProvider",
207
+ importName: "resendMailProvider",
111
208
  importFrom: "@beignet/provider-mail-resend",
112
- providerEntry: "mailResendProvider",
209
+ providerEntry: "resendMailProvider",
113
210
  dependencies: (packageJson) => ({
114
211
  "@beignet/provider-mail-resend": beignetDependencyVersion(packageJson),
115
212
  resend: externalVersions.resend,
@@ -132,7 +229,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
132
229
  "",
133
230
  "- Package: `@beignet/provider-mail-resend`",
134
231
  "- Peer dependency: `resend`",
135
- "- The preset wires `mailResendProvider` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
232
+ "- The preset wires `resendMailProvider` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
136
233
  "- `.env.example` ships dev placeholders so the app boots; set real `RESEND_API_KEY` and `RESEND_FROM` values before sending mail.",
137
234
  "- Use `ctx.ports.mailer` in app code; `ctx.ports.resend` is available as an escape hatch.",
138
235
  "",
@@ -143,16 +240,16 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
143
240
  "Run beignet providers audit and beignet doctor --strict.",
144
241
  ],
145
242
  conflictingProviderEntries: [
146
- "mailSmtpProvider",
243
+ "smtpMailProvider",
147
244
  "createMemoryMailerProvider",
148
245
  ],
149
246
  },
150
247
  "mail-smtp": {
151
248
  name: "mail-smtp",
152
249
  displayName: "SMTP mail",
153
- importName: "mailSmtpProvider",
250
+ importName: "smtpMailProvider",
154
251
  importFrom: "@beignet/provider-mail-smtp",
155
- providerEntry: "mailSmtpProvider",
252
+ providerEntry: "smtpMailProvider",
156
253
  dependencies: (packageJson) => ({
157
254
  "@beignet/provider-mail-smtp": beignetDependencyVersion(packageJson),
158
255
  nodemailer: externalVersions.nodemailer,
@@ -178,7 +275,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
178
275
  "",
179
276
  "- Package: `@beignet/provider-mail-smtp`",
180
277
  "- Peer dependency: `nodemailer`",
181
- "- The preset wires `mailSmtpProvider` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
278
+ "- The preset wires `smtpMailProvider` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
182
279
  "- `.env.example` ships dev placeholders so the app boots; set real `MAIL_*` values before sending mail.",
183
280
  "- Use `ctx.ports.mailer` in app code; `ctx.ports.smtp` is available as an escape hatch.",
184
281
  "",
@@ -189,10 +286,61 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
189
286
  "Run beignet providers audit and beignet doctor --strict.",
190
287
  ],
191
288
  conflictingProviderEntries: [
192
- "mailResendProvider",
289
+ "resendMailProvider",
193
290
  "createMemoryMailerProvider",
194
291
  ],
195
292
  },
293
+ "payments-stripe": {
294
+ name: "payments-stripe",
295
+ displayName: "Stripe payments",
296
+ importName: "stripePaymentsProvider",
297
+ importFrom: "@beignet/provider-payments-stripe",
298
+ providerEntry: "stripePaymentsProvider",
299
+ dependencies: (packageJson) => ({
300
+ "@beignet/provider-payments-stripe":
301
+ beignetDependencyVersion(packageJson),
302
+ stripe: externalVersions.stripe,
303
+ }),
304
+ appPort: {
305
+ key: "payments",
306
+ typeName: "PaymentsPort",
307
+ importName: "PaymentsPort",
308
+ importFrom: "@beignet/core/payments",
309
+ },
310
+ envVars: [
311
+ "STRIPE_SECRET_KEY",
312
+ "STRIPE_WEBHOOK_SECRET",
313
+ "STRIPE_PUBLISHABLE_KEY",
314
+ ],
315
+ envExample: [
316
+ "# Stripe payments provider",
317
+ "STRIPE_SECRET_KEY=sk_test_dev-placeholder-change-me",
318
+ "STRIPE_WEBHOOK_SECRET=whsec_dev-placeholder-change-me",
319
+ "# STRIPE_PUBLISHABLE_KEY=",
320
+ "",
321
+ ].join("\n"),
322
+ docs: [
323
+ "## Stripe payments",
324
+ "",
325
+ "- Package: `@beignet/provider-payments-stripe`",
326
+ "- Peer dependency: `stripe`",
327
+ "- The preset wires `stripePaymentsProvider` in `server/providers.ts` and defers `payments` in `infra/app-ports.ts`.",
328
+ "- `.env.example` ships dev placeholders so the app boots; set real `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` values before taking payments.",
329
+ "- Stripe requires a webhook route: run `beignet make payments` for the full billing slice, including a `createPaymentWebhookRoute(...)` route.",
330
+ "- Use `ctx.ports.payments` in app code; `ctx.ports.stripe` is available as an escape hatch.",
331
+ "",
332
+ ].join("\n"),
333
+ nextSteps: [
334
+ "Run your package manager install command.",
335
+ "Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in .env.local or your deployment environment.",
336
+ "Run beignet make payments for the full billing slice; Stripe needs its webhook route before subscription state can sync.",
337
+ "Run beignet providers audit and beignet doctor --strict.",
338
+ ],
339
+ conflictingProviderEntries: [
340
+ "createMemoryPaymentsProvider",
341
+ "createStripePaymentsProvider",
342
+ ],
343
+ },
196
344
  "search-meilisearch": {
197
345
  name: "search-meilisearch",
198
346
  displayName: "Meilisearch search",
@@ -271,11 +419,11 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
271
419
  "redis-cache": {
272
420
  name: "redis-cache",
273
421
  displayName: "Redis cache",
274
- importName: "redisProvider",
275
- importFrom: "@beignet/provider-redis",
276
- providerEntry: "redisProvider",
422
+ importName: "redisCacheProvider",
423
+ importFrom: "@beignet/provider-cache-redis",
424
+ providerEntry: "redisCacheProvider",
277
425
  dependencies: (packageJson) => ({
278
- "@beignet/provider-redis": beignetDependencyVersion(packageJson),
426
+ "@beignet/provider-cache-redis": beignetDependencyVersion(packageJson),
279
427
  ioredis: externalVersions.ioredis,
280
428
  }),
281
429
  appPort: {
@@ -294,9 +442,9 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
294
442
  docs: [
295
443
  "## Redis cache",
296
444
  "",
297
- "- Package: `@beignet/provider-redis`",
445
+ "- Package: `@beignet/provider-cache-redis`",
298
446
  "- Peer dependency: `ioredis`",
299
- "- The preset wires `redisProvider` in `server/providers.ts` and defers `cache` in `infra/app-ports.ts`.",
447
+ "- The preset wires `redisCacheProvider` in `server/providers.ts` and defers `cache` in `infra/app-ports.ts`.",
300
448
  "- Set `REDIS_URL` before booting the app.",
301
449
  "- Use `ctx.ports.cache` in app code; `ctx.ports.redis` is available as an escape hatch.",
302
450
  "",
@@ -307,6 +455,53 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
307
455
  "Run beignet providers audit and beignet doctor --strict.",
308
456
  ],
309
457
  },
458
+ "event-bus-redis": {
459
+ name: "event-bus-redis",
460
+ displayName: "Redis event bus",
461
+ importName: "redisEventBusProvider",
462
+ importFrom: "@beignet/provider-event-bus-redis",
463
+ providerEntry: "redisEventBusProvider",
464
+ dependencies: (packageJson) => ({
465
+ "@beignet/provider-event-bus-redis":
466
+ beignetDependencyVersion(packageJson),
467
+ ioredis: externalVersions.ioredis,
468
+ }),
469
+ appPort: {
470
+ key: "eventBus",
471
+ typeName: "EventBusPort",
472
+ importName: "EventBusPort",
473
+ importFrom: "@beignet/core/ports",
474
+ },
475
+ envVars: ["REDIS_EVENT_BUS_URL"],
476
+ envExample: [
477
+ "# Redis event bus provider",
478
+ "REDIS_EVENT_BUS_URL=redis://localhost:6379",
479
+ "# REDIS_EVENT_BUS_DB=0",
480
+ "# REDIS_EVENT_BUS_CHANNEL_PREFIX=beignet:event-bus:",
481
+ "",
482
+ ].join("\n"),
483
+ docs: [
484
+ "## Redis event bus",
485
+ "",
486
+ "- Package: `@beignet/provider-event-bus-redis`",
487
+ "- Peer dependency: `ioredis`",
488
+ "- The preset wires `redisEventBusProvider` in `server/providers.ts` and defers `eventBus` in `infra/app-ports.ts`.",
489
+ "- Set `REDIS_EVENT_BUS_URL` before booting the app.",
490
+ "- Redis Pub/Sub delivery is best-effort and cross-process, but not durable: use outbox/jobs for side effects that need retry, replay, or dead-letter handling.",
491
+ "- Use `ctx.ports.eventBus` in app code; `ctx.ports.redisEventBus` is available as an escape hatch.",
492
+ "",
493
+ ].join("\n"),
494
+ nextSteps: [
495
+ "Run your package manager install command.",
496
+ "Set REDIS_EVENT_BUS_URL in .env.local or your deployment environment.",
497
+ "Keep durable side effects on the outbox/jobs path; Redis Pub/Sub does not replay missed events.",
498
+ "Run beignet providers audit and beignet doctor --strict.",
499
+ ],
500
+ conflictingProviderEntries: [
501
+ "createInMemoryEventBusProvider",
502
+ "createRedisEventBusProvider",
503
+ ],
504
+ },
310
505
  "redis-locks": {
311
506
  name: "redis-locks",
312
507
  displayName: "Redis locks",
@@ -364,7 +559,19 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
364
559
  importName: "StoragePort",
365
560
  importFrom: "@beignet/core/storage",
366
561
  },
367
- envVars: ["STORAGE_S3_BUCKET"],
562
+ envVars: [
563
+ "STORAGE_S3_BUCKET",
564
+ "STORAGE_S3_REGION",
565
+ "STORAGE_S3_ENDPOINT",
566
+ "STORAGE_S3_PUBLIC_BASE_URL",
567
+ "STORAGE_S3_FORCE_PATH_STYLE",
568
+ "STORAGE_S3_KEY_PREFIX",
569
+ "STORAGE_S3_MAX_ATTEMPTS",
570
+ "STORAGE_S3_RETRY_MODE",
571
+ "STORAGE_S3_ACCESS_KEY_ID",
572
+ "STORAGE_S3_SECRET_ACCESS_KEY",
573
+ "STORAGE_S3_SESSION_TOKEN",
574
+ ],
368
575
  envExample: [
369
576
  "# S3-compatible storage provider",
370
577
  "STORAGE_S3_BUCKET=beignet-assets",
@@ -373,6 +580,8 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
373
580
  "# STORAGE_S3_PUBLIC_BASE_URL=",
374
581
  "# STORAGE_S3_FORCE_PATH_STYLE=false",
375
582
  "# STORAGE_S3_KEY_PREFIX=",
583
+ "# STORAGE_S3_MAX_ATTEMPTS=3",
584
+ "# STORAGE_S3_RETRY_MODE=standard",
376
585
  "# STORAGE_S3_ACCESS_KEY_ID=",
377
586
  "# STORAGE_S3_SECRET_ACCESS_KEY=",
378
587
  "# STORAGE_S3_SESSION_TOKEN=",
@@ -496,6 +705,51 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
496
705
 
497
706
  const providerPresetSet = new Set<string>(providerPresetChoices);
498
707
 
708
+ /**
709
+ * Provider entries the starter template wires for each create-time
710
+ * integration, used to pre-validate preset selections at create time.
711
+ */
712
+ const integrationProviderEntries: Record<IntegrationName, string> = {
713
+ inngest: "inngestJobsProvider",
714
+ resend: "resendMailProvider",
715
+ "upstash-rate-limit": "upstashRateLimitProvider",
716
+ };
717
+
718
+ /**
719
+ * Whether a preset ships `.env.example` entries the user must fill in.
720
+ */
721
+ export function providerPresetNeedsEnv(name: ProviderPresetName): boolean {
722
+ return presets[name].envVars.length > 0;
723
+ }
724
+
725
+ /**
726
+ * Fail fast when a create-time selection combines presets or template
727
+ * integrations that fill the same app port, before any files are written.
728
+ */
729
+ export function assertCompatibleCreatePresets(
730
+ presetNames: readonly ProviderPresetName[],
731
+ integrations: readonly IntegrationName[],
732
+ ): void {
733
+ const selectedEntries = new Map<string, string>();
734
+ for (const integration of integrations) {
735
+ selectedEntries.set(integrationProviderEntries[integration], integration);
736
+ }
737
+ for (const name of presetNames) {
738
+ selectedEntries.set(presets[name].providerEntry, name);
739
+ }
740
+
741
+ for (const name of presetNames) {
742
+ for (const conflict of presets[name].conflictingProviderEntries ?? []) {
743
+ const other = selectedEntries.get(conflict);
744
+ if (other !== undefined && other !== name) {
745
+ throw new Error(
746
+ `--integrations selections "${name}" and "${other}" fill the same app port. Pick one of them.`,
747
+ );
748
+ }
749
+ }
750
+ }
751
+ }
752
+
499
753
  export async function addProviderPreset(
500
754
  options: AddProviderPresetOptions,
501
755
  ): Promise<AddProviderPresetResult> {
package/src/task.ts CHANGED
@@ -50,7 +50,7 @@ export async function runAppTask(
50
50
  const task = findTask(taskModule.tasks, options.name, modulePath);
51
51
  // Loaded lazily so CLI commands that never touch the app runtime, such as
52
52
  // `beignet lint`, work without a built @beignet/core.
53
- const { parseTaskInput } = await import("@beignet/core/tasks");
53
+ const { parseTaskInput, runTask } = await import("@beignet/core/tasks");
54
54
  const parsedInput = await parseTaskInput(task, rawInput);
55
55
  const contextArgs: TaskRunContextArgs = {
56
56
  task,
@@ -63,8 +63,7 @@ export async function runAppTask(
63
63
  : {};
64
64
 
65
65
  try {
66
- const output = await task.handle({
67
- task,
66
+ const output = await runTask(task, {
68
67
  input: parsedInput,
69
68
  ctx,
70
69
  });
@@ -27,7 +27,9 @@ export function agentsMd(ctx: TemplateContext): string {
27
27
  const intent = intentRunner(ctx);
28
28
  const capabilityClientRow = ctx.api
29
29
  ? ""
30
- : "\n| Query cache keys and invalidation | `rq(contract).invalidate(queryClient, params?)`, `.key(...)`, `.filter(...)` from `client/` (`@beignet/react-query`). Thin named wrappers in `features/<feature>/client/` are the convention — hand-built key arrays are not. |";
30
+ : "\n| Query cache keys and invalidation | `rq(contract).invalidate(queryClient, params?)`, `.key(...)`, `.filter(...)` from `client/` (`@beignet/react-query`). Thin named wrappers in `features/<feature>/client/` are the convention — hand-built key arrays are not. |" +
31
+ "\n| Server Component request context | Use an app-owned server-only helper such as `lib/server-context.ts` that wraps `server.createContextFromNext()` in React `cache(...)`. Layouts may read `ctx.auth`, `ctx.tenant`, and request metadata for redirects and shell state; feature data still belongs behind use cases. |" +
32
+ "\n| Server Component React Query prefetch | Use an app-owned helper such as `lib/server-react-query.ts` on top of the request context: keep the `rq(contract).queryOptions(...)` key, replace only the server `queryFn` with the direct use case call, and do not create a parallel `rqServer` abstraction. |";
31
33
  const specificSkills = ctx.api
32
34
  ? "`@beignet/next#routes-server`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`"
33
35
  : "`@beignet/next#routes-server`, `@beignet/react-query#client`, `@beignet/react-hook-form#forms`, `@beignet/provider-db-drizzle#database-provider`, `@beignet/provider-auth-better-auth#auth-provider`, or `@beignet/cli#app-structure`";
@@ -50,8 +52,8 @@ is a silent failure — the file exists but never runs:
50
52
  - Tasks must be added to \`defineTasks([...])\` in \`server/tasks.ts\`.
51
53
  - Outbox events and jobs must be registered in \`defineOutboxRegistry({...})\`
52
54
  in \`server/outbox.ts\`.
53
- - Listeners must be wired through a \`registerListeners(...)\` call in infra
54
- wiring.
55
+ - Listeners must be added to the \`listeners\` array in \`server/listeners.ts\`
56
+ and wired through a \`registerListeners(...)\` call in server provider wiring.
55
57
 
56
58
  The starter ships no workflow registries — generators create them on first
57
59
  use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
@@ -81,6 +83,7 @@ apps have reimplemented by accident:
81
83
  | Ports outside a request — auth callbacks, module-level helpers | \`const { ports } = await getServer()\` (dynamic \`import("@/server")\` breaks module cycles). Do not construct parallel provider clients or fall back to \`console.*\` when \`ports.logger\` exists. |
82
84
  | Routes that cannot be contracts — webhooks, third-party callbacks, streaming | \`createWebhookRoute\`, \`createPaymentWebhookRoute\`, \`createScheduleRoute\`, \`createOutboxDrainRoute\` from \`@beignet/next\`; \`server.rawRoute(...)\` for anything else. All run the hooks pipeline — never hand-enforce rate limits in a route body. |
83
85
  | Rate limiting or idempotency on a route | Declare \`metadata.rateLimit\` / \`metadata.idempotency\` on the contract (or the \`pipeline\` option on raw routes); hooks enforce it. |
86
+ | Tenant-owned repository access | Pass \`TenantScope\` through app-facing repository methods and unwrap it with \`tenantScopeId(scope)\` in adapters. Raw provider-correlation lookups such as webhook customer IDs are not tenant authorization. |
84
87
  | Route-level tests that exercise real hooks | \`createTestApp\` / \`createTestRequester\` from \`@beignet/web/testing\`. Bind the rate-limit and idempotency ports in the test app when asserting 429s or replay. |
85
88
  | Environment configuration | \`lib/env.ts\` (\`createEnv\`), never ad-hoc \`process.env\` reads in app code. |
86
89
  | The same lookup runs several times in one request — context, policy, use case | Wrap the repository read in \`createMemo(...)\` from \`@beignet/core/memo\` where the adapter is wired in infra. The cache lives for exactly one request; pair mutations with \`.invalidate(...)\`. Never hand-roll per-request caches. |
@@ -67,11 +67,11 @@ export function packageJson(ctx: TemplateContext): string {
67
67
  "@beignet/cli": ctx.beignetVersion,
68
68
  "@beignet/web": ctx.beignetVersion,
69
69
  "@biomejs/biome": externalVersions.biome,
70
- "@types/bun": externalVersions.typesBun,
71
70
  "@types/node": externalVersions.typesNode,
72
71
  "@types/react": externalVersions.typesReact,
73
72
  "@types/react-dom": externalVersions.typesReactDom,
74
73
  "drizzle-kit": externalVersions.drizzleKit,
74
+ tsx: externalVersions.tsx,
75
75
  typescript: externalVersions.typescript,
76
76
  };
77
77
 
@@ -97,11 +97,11 @@ export function packageJson(ctx: TemplateContext): string {
97
97
  start: "next start",
98
98
  lint: "biome lint .",
99
99
  format: "biome format --write .",
100
- test: "bun test",
100
+ test: "tsx lib/beignet-test-runner.ts",
101
101
  typecheck: "tsc --noEmit",
102
102
  "db:generate": "drizzle-kit generate",
103
103
  "db:migrate": "drizzle-kit migrate",
104
- "db:reset": "bun infra/db/reset.ts",
104
+ "db:reset": "tsx infra/db/reset.ts",
105
105
  };
106
106
 
107
107
  return json({
@@ -159,18 +159,10 @@ export function readme(ctx: TemplateContext): string {
159
159
  ctx.packageManager === "npm"
160
160
  ? "npm run test"
161
161
  : `${ctx.packageManager} run test`;
162
- const lint =
163
- ctx.packageManager === "npm"
164
- ? "npm run lint"
165
- : `${ctx.packageManager} run lint`;
166
162
  const format =
167
163
  ctx.packageManager === "npm"
168
164
  ? "npm run format"
169
165
  : `${ctx.packageManager} run format`;
170
- const typecheck =
171
- ctx.packageManager === "npm"
172
- ? "npm run typecheck"
173
- : `${ctx.packageManager} run typecheck`;
174
166
  const cli = packageRunner(ctx);
175
167
 
176
168
  const firstOpen = ctx.api
@@ -393,7 +385,7 @@ export function integrationsDoc(ctx: TemplateContext): string {
393
385
  for (const integration of ctx.integrations) {
394
386
  if (integration === "inngest") {
395
387
  lines.push(
396
- "## Inngest",
388
+ "## Inngest jobs",
397
389
  "",
398
390
  "- Package: `@beignet/core` (`@beignet/core/events`, `@beignet/core/jobs`)",
399
391
  "- Package: `@beignet/provider-jobs-inngest`",
@@ -411,7 +403,7 @@ export function integrationsDoc(ctx: TemplateContext): string {
411
403
  "",
412
404
  "- Package: `@beignet/provider-mail-resend`",
413
405
  "- Peer dependency: `resend`",
414
- "- The starter wires `mailResendProvider` in `server/providers.ts`.",
406
+ "- The starter wires `resendMailProvider` in `server/providers.ts`.",
415
407
  "- `.env.example` ships dev placeholders so the app boots; set real `RESEND_API_KEY` and `RESEND_FROM` values before sending mail.",
416
408
  "",
417
409
  );
@@ -28,6 +28,7 @@ import {
28
28
  import { shadcnTemplateFiles } from "./shadcn.js";
29
29
  import type { TemplateContext, TemplateFile } from "./shared.js";
30
30
  import { shellTemplateFiles } from "./shell.js";
31
+ import { testSupportTemplateFiles } from "./testing.js";
31
32
  import { todosFiles } from "./todos.js";
32
33
 
33
34
  /**
@@ -133,6 +134,7 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
133
134
  { path: "lib/auth.ts", content: serverFiles.authHelpers },
134
135
  { path: "lib/better-auth.ts", content: betterAuth(ctx) },
135
136
  { path: "lib/tenant.ts", content: serverFiles.tenantHelper },
137
+ ...testSupportTemplateFiles,
136
138
  { path: "lib/use-case.ts", content: serverFiles.useCaseBuilder },
137
139
  { path: "server/index.ts", content: server(ctx) },
138
140
  { path: "server/context.ts", content: serverFiles.serverContext },
@@ -15,9 +15,9 @@ export function serverProviders(ctx: TemplateContext): string {
15
15
  hasIntegration(ctx, "inngest")
16
16
  ? 'import { inngestJobsProvider } from "@beignet/provider-jobs-inngest";'
17
17
  : undefined,
18
- 'import { loggerPinoProvider } from "@beignet/provider-logger-pino";',
18
+ 'import { pinoLoggerProvider } from "@beignet/provider-logger-pino";',
19
19
  hasIntegration(ctx, "resend")
20
- ? 'import { mailResendProvider } from "@beignet/provider-mail-resend";'
20
+ ? 'import { resendMailProvider } from "@beignet/provider-mail-resend";'
21
21
  : undefined,
22
22
  hasIntegration(ctx, "upstash-rate-limit")
23
23
  ? 'import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";'
@@ -31,11 +31,11 @@ export function serverProviders(ctx: TemplateContext): string {
31
31
  const entries = [
32
32
  "\tcreateDevtoolsProvider(),",
33
33
  "\tcreateAuthBetterAuthProvider<AuthUser, AuthSessionMetadata>(auth),",
34
- "\tloggerPinoProvider,",
34
+ "\tpinoLoggerProvider,",
35
35
  `\t${db.providerInstance},`,
36
36
  "\tstarterDatabaseProvider,",
37
37
  hasIntegration(ctx, "inngest") ? "\tinngestJobsProvider," : undefined,
38
- hasIntegration(ctx, "resend") ? "\tmailResendProvider," : undefined,
38
+ hasIntegration(ctx, "resend") ? "\tresendMailProvider," : undefined,
39
39
  hasIntegration(ctx, "upstash-rate-limit")
40
40
  ? "\tupstashRateLimitProvider,"
41
41
  : undefined,
@@ -297,7 +297,11 @@ export function server(ctx: TemplateContext): string {
297
297
 
298
298
  return `import "@beignet/core/server-only";
299
299
  import { createNextServer, createNextServerLoader } from "@beignet/next";
300
- import { createErrorReportingHooks, createIdempotencyHooks } from "@beignet/core/server";
300
+ import {
301
+ createErrorReportingHooks,
302
+ createIdempotencyHooks,
303
+ createSecurityHeadersHooks,
304
+ } from "@beignet/core/server";
301
305
  import type { AppContext } from "@/app-context";
302
306
  import { appPorts } from "@/infra/app-ports";
303
307
  import { env } from "@/lib/env";
@@ -314,6 +318,7 @@ export const getServer = createNextServerLoader(async () => {
314
318
  ${providerConfigEntries}
315
319
  },
316
320
  hooks: [
321
+ createSecurityHeadersHooks<AppContext>(),
317
322
  createErrorReportingHooks<AppContext>(),
318
323
  createIdempotencyHooks<AppContext>(),
319
324
  ],
@@ -35,8 +35,8 @@ export const externalVersions = {
35
35
  next: "^16.2.4",
36
36
  react: "^19.2.5",
37
37
  reactDom: "^19.2.5",
38
+ tsx: "^4.22.4",
38
39
  typescript: "^5.3.0",
39
- typesBun: "^1.3.13",
40
40
  typesNode: "^20.10.0",
41
41
  typesReact: "^19.0.0",
42
42
  typesReactDom: "^19.0.0",
@@ -45,6 +45,7 @@ export const externalVersions = {
45
45
  hookformResolvers: "^5.0.0",
46
46
  reactHookForm: "^7.74.0",
47
47
  betterAuth: "1.6.20",
48
+ bullmq: "^5.0.0",
48
49
  drizzleKit: "^0.31.10",
49
50
  drizzleOrm: "^0.45.2",
50
51
  inngest: "^3.0.0",
@@ -56,6 +57,7 @@ export const externalVersions = {
56
57
  pino: "^9.7.0",
57
58
  resend: "^6.14.0",
58
59
  sentryNode: "^10.58.0",
60
+ stripe: "^22.2.1",
59
61
  nodemailer: "^8.0.5",
60
62
  openFeatureServerSdk: "^1.22.0",
61
63
  typesPg: "^8.11.0",
@@ -65,6 +67,18 @@ export const externalVersions = {
65
67
  upstashRedis: "^1.0.0",
66
68
  };
67
69
 
70
+ export const legacyGeneratedPackageScripts: Record<string, string> = {
71
+ "db:reset": "bun infra/db/reset.ts",
72
+ "db:seed": "bun server/seed.ts",
73
+ test: "bun test",
74
+ };
75
+
76
+ export const currentGeneratedPackageScripts: Record<string, string> = {
77
+ "db:reset": "tsx infra/db/reset.ts",
78
+ "db:seed": "tsx server/seed.ts",
79
+ test: "tsx lib/beignet-test-runner.ts",
80
+ };
81
+
68
82
  /**
69
83
  * Dialect-specific names used when rendering database-aware templates.
70
84
  */