@beignet/cli 0.0.36 → 0.0.38

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 (95) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +36 -5
  3. package/dist/config.d.ts +15 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +30 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/inspect.js +21 -17
  8. package/dist/inspect.js.map +1 -1
  9. package/dist/lint.d.ts.map +1 -1
  10. package/dist/lint.js +27 -6
  11. package/dist/lint.js.map +1 -1
  12. package/dist/make/inbox.js +7 -7
  13. package/dist/make/inbox.js.map +1 -1
  14. package/dist/make/payments.d.ts +2 -1
  15. package/dist/make/payments.d.ts.map +1 -1
  16. package/dist/make/payments.js +15 -11
  17. package/dist/make/payments.js.map +1 -1
  18. package/dist/make/shared.d.ts +1 -1
  19. package/dist/make/shared.d.ts.map +1 -1
  20. package/dist/make/shared.js +49 -37
  21. package/dist/make/shared.js.map +1 -1
  22. package/dist/make/tenancy.js +8 -8
  23. package/dist/make/tenancy.js.map +1 -1
  24. package/dist/make.d.ts.map +1 -1
  25. package/dist/make.js +27 -16
  26. package/dist/make.js.map +1 -1
  27. package/dist/operational-error-reporting.d.ts +15 -0
  28. package/dist/operational-error-reporting.d.ts.map +1 -0
  29. package/dist/operational-error-reporting.js +42 -0
  30. package/dist/operational-error-reporting.js.map +1 -0
  31. package/dist/outbox.d.ts.map +1 -1
  32. package/dist/outbox.js +73 -0
  33. package/dist/outbox.js.map +1 -1
  34. package/dist/provider-add.js +17 -17
  35. package/dist/provider-add.js.map +1 -1
  36. package/dist/provider-audit.d.ts.map +1 -1
  37. package/dist/provider-audit.js +11 -6
  38. package/dist/provider-audit.js.map +1 -1
  39. package/dist/schedule.d.ts.map +1 -1
  40. package/dist/schedule.js +28 -1
  41. package/dist/schedule.js.map +1 -1
  42. package/dist/task.d.ts.map +1 -1
  43. package/dist/task.js +37 -4
  44. package/dist/task.js.map +1 -1
  45. package/dist/templates/base.d.ts.map +1 -1
  46. package/dist/templates/base.js +6 -1
  47. package/dist/templates/base.js.map +1 -1
  48. package/dist/templates/db/index.d.ts +1 -0
  49. package/dist/templates/db/index.d.ts.map +1 -1
  50. package/dist/templates/db/index.js.map +1 -1
  51. package/dist/templates/db/mysql.d.ts.map +1 -1
  52. package/dist/templates/db/mysql.js +16 -0
  53. package/dist/templates/db/mysql.js.map +1 -1
  54. package/dist/templates/db/postgres.d.ts.map +1 -1
  55. package/dist/templates/db/postgres.js +16 -0
  56. package/dist/templates/db/postgres.js.map +1 -1
  57. package/dist/templates/db/sqlite.d.ts.map +1 -1
  58. package/dist/templates/db/sqlite.js +16 -0
  59. package/dist/templates/db/sqlite.js.map +1 -1
  60. package/dist/templates/index.d.ts.map +1 -1
  61. package/dist/templates/index.js +4 -3
  62. package/dist/templates/index.js.map +1 -1
  63. package/dist/templates/server.d.ts +2 -2
  64. package/dist/templates/server.d.ts.map +1 -1
  65. package/dist/templates/server.js +56 -40
  66. package/dist/templates/server.js.map +1 -1
  67. package/dist/templates/shared.js +3 -3
  68. package/dist/templates/shared.js.map +1 -1
  69. package/dist/templates/testing.js +1 -1
  70. package/dist/templates/todos.js +6 -6
  71. package/package.json +4 -4
  72. package/src/config.ts +66 -2
  73. package/src/inspect.ts +31 -19
  74. package/src/lint.ts +35 -4
  75. package/src/make/inbox.ts +7 -7
  76. package/src/make/payments.ts +23 -15
  77. package/src/make/shared.ts +56 -40
  78. package/src/make/tenancy.ts +8 -8
  79. package/src/make.ts +30 -16
  80. package/src/operational-error-reporting.ts +60 -0
  81. package/src/outbox.ts +77 -0
  82. package/src/provider-add.ts +21 -17
  83. package/src/provider-audit.ts +28 -11
  84. package/src/schedule.ts +32 -1
  85. package/src/task.ts +41 -4
  86. package/src/templates/base.ts +6 -1
  87. package/src/templates/db/index.ts +1 -0
  88. package/src/templates/db/mysql.ts +16 -0
  89. package/src/templates/db/postgres.ts +16 -0
  90. package/src/templates/db/sqlite.ts +16 -0
  91. package/src/templates/index.ts +4 -3
  92. package/src/templates/server.ts +56 -42
  93. package/src/templates/shared.ts +3 -3
  94. package/src/templates/testing.ts +1 -1
  95. package/src/templates/todos.ts +6 -6
package/src/outbox.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import type { ErrorReporterPort } from "@beignet/core/error-reporting";
3
4
  import type {
4
5
  DrainOutboxOptions,
5
6
  DrainOutboxResult,
@@ -11,6 +12,10 @@ import type {
11
12
  } from "@beignet/core/outbox";
12
13
  import { createJiti } from "jiti";
13
14
  import { loadBeignetConfig, normalizePath } from "./config.js";
15
+ import {
16
+ flushOperationalErrorReporter,
17
+ reportOperationalFailure,
18
+ } from "./operational-error-reporting.js";
14
19
 
15
20
  /**
16
21
  * Options for running one bounded outbox drain pass.
@@ -159,6 +164,7 @@ type OutboxDrainPorts = {
159
164
  logger?: OutboxDrainLogger;
160
165
  devtools?: OutboxDrainInstrumentation;
161
166
  instrumentation?: OutboxDrainInstrumentation;
167
+ errorReporter?: ErrorReporterPort;
162
168
  };
163
169
 
164
170
  type OutboxDrainContext = {
@@ -240,6 +246,60 @@ export async function runOutboxDrain(
240
246
  attempts: message.attempts,
241
247
  });
242
248
  },
249
+ onDeadLetter(error, message) {
250
+ return reportOperationalFailure({
251
+ ctx,
252
+ error,
253
+ reportOptions: {
254
+ level: "error",
255
+ mechanism: "beignet.outbox.cli",
256
+ handled: false,
257
+ tags: {
258
+ "beignet.kind": "outbox",
259
+ "beignet.outbox.kind": message.kind,
260
+ "beignet.outbox.name": message.name,
261
+ "beignet.outbox.outcome": "deadLettered",
262
+ },
263
+ contexts: {
264
+ outbox: {
265
+ messageId: message.id,
266
+ kind: message.kind,
267
+ name: message.name,
268
+ attempts: message.attempts,
269
+ maxAttempts: message.maxAttempts,
270
+ outcome: "deadLettered",
271
+ },
272
+ },
273
+ },
274
+ });
275
+ },
276
+ onSettlementError(settlementError, message) {
277
+ return reportOperationalFailure({
278
+ ctx,
279
+ error: settlementError,
280
+ reportOptions: {
281
+ level: "error",
282
+ mechanism: "beignet.outbox.cli",
283
+ handled: false,
284
+ tags: {
285
+ "beignet.kind": "outbox",
286
+ "beignet.outbox.kind": message.kind,
287
+ "beignet.outbox.name": message.name,
288
+ "beignet.outbox.outcome": "settlementFailed",
289
+ },
290
+ contexts: {
291
+ outbox: {
292
+ messageId: message.id,
293
+ kind: message.kind,
294
+ name: message.name,
295
+ attempts: message.attempts,
296
+ maxAttempts: message.maxAttempts,
297
+ outcome: "settlementFailed",
298
+ },
299
+ },
300
+ },
301
+ });
302
+ },
243
303
  });
244
304
 
245
305
  await recordOutboxDrain(ctx, result);
@@ -251,7 +311,24 @@ export async function runOutboxDrain(
251
311
  result,
252
312
  durationMs: Math.round(performance.now() - startedAt),
253
313
  };
314
+ } catch (error) {
315
+ await reportOperationalFailure({
316
+ ctx,
317
+ error,
318
+ reportOptions: {
319
+ level: "error",
320
+ mechanism: "beignet.outbox.cli",
321
+ handled: false,
322
+ tags: {
323
+ "beignet.kind": "outbox",
324
+ "beignet.outbox.outcome": "drainFailed",
325
+ },
326
+ contexts: { outbox: { outcome: "drainFailed" } },
327
+ },
328
+ });
329
+ throw error;
254
330
  } finally {
331
+ await flushOperationalErrorReporter(ctx);
255
332
  await outboxModule.stopOutboxDrainContext?.(ctx, contextArgs);
256
333
  }
257
334
  }
@@ -97,7 +97,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
97
97
  "",
98
98
  "- Package: `@beignet/provider-flags-openfeature`",
99
99
  "- Peer dependency: `@openfeature/server-sdk`",
100
- "- The preset wires `createOpenFeatureFlagsProvider()` in `server/providers.ts` and defers `flags` in `infra/app-ports.ts`.",
100
+ "- The preset wires `createOpenFeatureFlagsProvider()` in `server/providers.ts` and defers `flags` in `infra/port-wiring.ts`.",
101
101
  "- Configure the OpenFeature provider globally or replace this preset with `createOpenFeatureFlagsProvider(...)` when the app needs provider-specific setup.",
102
102
  "- Use `ctx.ports.flags` in app code; `ctx.ports.openFeature` is available as an escape hatch.",
103
103
  "",
@@ -138,7 +138,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
138
138
  "",
139
139
  "- Package: `@beignet/provider-jobs-bullmq`",
140
140
  "- Peer dependency: `bullmq`",
141
- "- The preset wires `createBullMQJobsProvider()` in `server/providers.ts` and defers `jobs` in `infra/app-ports.ts`.",
141
+ "- The preset wires `createBullMQJobsProvider()` in `server/providers.ts` and defers `jobs` in `infra/port-wiring.ts`.",
142
142
  "- Set `BULLMQ_REDIS_URL` before booting the app; the default points at local Redis.",
143
143
  "- Dispatching enqueues jobs, but nothing executes them until a worker entrypoint runs `createBullMQJobWorker(...)` in a worker process. See the jobs docs page.",
144
144
  "- Use `ctx.ports.jobs.dispatch(...)` in app code; `ctx.ports.bullMQJobs` is available as an escape hatch.",
@@ -180,7 +180,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
180
180
  "",
181
181
  "- Package: `@beignet/provider-jobs-inngest`",
182
182
  "- Peer dependency: `inngest`",
183
- "- The preset wires `createInngestJobsProvider()` in `server/providers.ts` and defers `jobs` in `infra/app-ports.ts`.",
183
+ "- The preset wires `createInngestJobsProvider()` in `server/providers.ts` and defers `jobs` in `infra/port-wiring.ts`.",
184
184
  "- Define jobs with `defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.",
185
185
  "- Execution needs an Inngest serve route built from `createInngestJobFunction(...)` and `serve(...)`. See the jobs docs page.",
186
186
  "- Use `ctx.ports.inngest` only as an escape hatch for Inngest-specific workflow APIs; prefer `ctx.ports.jobs.dispatch(...)` for Beignet jobs.",
@@ -222,7 +222,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
222
222
  "",
223
223
  "- Package: `@beignet/provider-mail-resend`",
224
224
  "- Peer dependency: `resend`",
225
- "- The preset wires `createResendMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
225
+ "- The preset wires `createResendMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/port-wiring.ts`.",
226
226
  "- `.env.example` ships dev placeholders so the app boots; set real `RESEND_API_KEY` and `RESEND_FROM` values before sending mail.",
227
227
  "- Use `ctx.ports.mailer` in app code; `ctx.ports.resend` is available as an escape hatch.",
228
228
  "",
@@ -268,7 +268,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
268
268
  "",
269
269
  "- Package: `@beignet/provider-mail-smtp`",
270
270
  "- Peer dependency: `nodemailer`",
271
- "- The preset wires `createSmtpMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/app-ports.ts`.",
271
+ "- The preset wires `createSmtpMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/port-wiring.ts`.",
272
272
  "- `.env.example` ships dev placeholders so the app boots; set real `MAIL_*` values before sending mail.",
273
273
  "- Use `ctx.ports.mailer` in app code; `ctx.ports.smtp` is available as an escape hatch.",
274
274
  "",
@@ -317,7 +317,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
317
317
  "",
318
318
  "- Package: `@beignet/provider-payments-stripe`",
319
319
  "- Peer dependency: `stripe`",
320
- "- The preset wires `createStripePaymentsProvider()` in `server/providers.ts` and defers `payments` in `infra/app-ports.ts`.",
320
+ "- The preset wires `createStripePaymentsProvider()` in `server/providers.ts` and defers `payments` in `infra/port-wiring.ts`.",
321
321
  "- `.env.example` ships dev placeholders so the app boots; set real `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` values before taking payments.",
322
322
  "- Stripe requires a webhook route: run `beignet make payments` for the full billing slice, including a `createPaymentWebhookRoute(...)` route.",
323
323
  "- Use `ctx.ports.payments` in app code; `ctx.ports.stripe` is available as an escape hatch.",
@@ -360,7 +360,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
360
360
  "## Meilisearch search",
361
361
  "",
362
362
  "- Package: `@beignet/provider-search-meilisearch`",
363
- "- The preset wires `createMeilisearchSearchProvider()` in `server/providers.ts` and defers `search` in `infra/app-ports.ts`.",
363
+ "- The preset wires `createMeilisearchSearchProvider()` in `server/providers.ts` and defers `search` in `infra/port-wiring.ts`.",
364
364
  "- Set `MEILISEARCH_HOST` before booting the app; set `MEILISEARCH_API_KEY` when your Meilisearch server requires one.",
365
365
  "- Use `ctx.ports.search` in app code; `ctx.ports.meilisearch` is available as an escape hatch.",
366
366
  "",
@@ -434,7 +434,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
434
434
  "",
435
435
  "- Package: `@beignet/provider-cache-redis`",
436
436
  "- Peer dependency: `ioredis`",
437
- "- The preset wires `createRedisCacheProvider()` in `server/providers.ts` and defers `cache` in `infra/app-ports.ts`.",
437
+ "- The preset wires `createRedisCacheProvider()` in `server/providers.ts` and defers `cache` in `infra/port-wiring.ts`.",
438
438
  "- Set `REDIS_URL` before booting the app.",
439
439
  "- Use `ctx.ports.cache` in app code; `ctx.ports.redis` is available as an escape hatch.",
440
440
  "",
@@ -475,7 +475,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
475
475
  "",
476
476
  "- Package: `@beignet/provider-event-bus-redis`",
477
477
  "- Peer dependency: `ioredis`",
478
- "- The preset wires `createRedisEventBusProvider()` in `server/providers.ts` and defers `eventBus` in `infra/app-ports.ts`.",
478
+ "- The preset wires `createRedisEventBusProvider()` in `server/providers.ts` and defers `eventBus` in `infra/port-wiring.ts`.",
479
479
  "- Set `REDIS_EVENT_BUS_URL` before booting the app.",
480
480
  "- 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.",
481
481
  "- Use `ctx.ports.eventBus` in app code; `ctx.ports.redisEventBus` is available as an escape hatch.",
@@ -518,7 +518,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
518
518
  "",
519
519
  "- Package: `@beignet/provider-locks-redis`",
520
520
  "- Peer dependency: `ioredis`",
521
- "- The preset wires `createRedisLocksProvider()` in `server/providers.ts` and defers `locks` in `infra/app-ports.ts`.",
521
+ "- The preset wires `createRedisLocksProvider()` in `server/providers.ts` and defers `locks` in `infra/port-wiring.ts`.",
522
522
  "- Set `REDIS_LOCKS_URL` before booting the app.",
523
523
  "- Use `ctx.ports.locks` in app code; `ctx.ports.redisLocks` is available as an escape hatch.",
524
524
  "",
@@ -579,7 +579,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
579
579
  "",
580
580
  "- Package: `@beignet/provider-storage-s3`",
581
581
  "- Peer dependencies: `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`",
582
- "- The preset wires `createS3StorageProvider()` in `server/providers.ts` and defers `storage` in `infra/app-ports.ts`.",
582
+ "- The preset wires `createS3StorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
583
583
  "- Set `STORAGE_S3_BUCKET` before booting the app; set endpoint and credentials when using an S3-compatible host outside AWS defaults.",
584
584
  "- Use `ctx.ports.storage` in app code; `ctx.ports.s3Storage` is available as an escape hatch.",
585
585
  "",
@@ -625,7 +625,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
625
625
  "",
626
626
  "- Package: `@beignet/provider-storage-vercel-blob`",
627
627
  "- Peer dependency: `@vercel/blob`",
628
- "- The preset wires `createVercelBlobStorageProvider()` in `server/providers.ts` and defers `storage` in `infra/app-ports.ts`.",
628
+ "- The preset wires `createVercelBlobStorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
629
629
  "- On Vercel, connect a Blob store to the project and `BLOB_READ_WRITE_TOKEN` is set automatically; set it manually elsewhere.",
630
630
  "- The store is uniform-visibility (`BLOB_ACCESS`, default `private`): Vercel Blob does not report per-object access back, so mixed visibility needs a second store.",
631
631
  "- Use `ctx.ports.storage` in app code; `ctx.ports.vercelBlob` is available as an escape hatch.",
@@ -673,7 +673,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
673
673
  "",
674
674
  "- Package: `@beignet/provider-rate-limit-upstash`",
675
675
  "- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
676
- "- The preset wires `createUpstashRateLimitProvider()` in `server/providers.ts` and defers `rateLimit` in `infra/app-ports.ts`.",
676
+ "- The preset wires `createUpstashRateLimitProvider()` in `server/providers.ts` and defers `rateLimit` in `infra/port-wiring.ts`.",
677
677
  "- `.env.example` ships dev placeholders so the app boots; set real `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` values before rate limiting requests.",
678
678
  "- Use `ctx.ports.rateLimit` in app code; `ctx.ports.upstash` is available as an escape hatch.",
679
679
  "",
@@ -778,9 +778,9 @@ export async function addProviderPreset(
778
778
  ),
779
779
  },
780
780
  {
781
- path: config.paths.infrastructurePorts,
781
+ path: config.paths.portWiring,
782
782
  content: updateInfrastructurePortsFile(
783
- await readRequiredAppFile(targetDir, config.paths.infrastructurePorts),
783
+ await readRequiredAppFile(targetDir, config.paths.portWiring),
784
784
  preset.appPort.key,
785
785
  ),
786
786
  },
@@ -1056,13 +1056,17 @@ function ensureTypeProperty(
1056
1056
  function ensureDeferredPort(source: string, portKey: string): string {
1057
1057
  const match = /\bdeferred\s*:\s*\[/.exec(source);
1058
1058
  if (!match) {
1059
- throw new Error("Could not find `deferred: [...]` in infra/app-ports.ts.");
1059
+ throw new Error(
1060
+ "Could not find `deferred: [...]` in infra/port-wiring.ts.",
1061
+ );
1060
1062
  }
1061
1063
 
1062
1064
  const openBracket = source.indexOf("[", match.index);
1063
1065
  const closeBracket = matchingDelimiterIndex(source, openBracket, "[", "]");
1064
1066
  if (closeBracket === -1) {
1065
- throw new Error("Could not parse `deferred` array in infra/app-ports.ts.");
1067
+ throw new Error(
1068
+ "Could not parse `deferred` array in infra/port-wiring.ts.",
1069
+ );
1066
1070
  }
1067
1071
 
1068
1072
  const arrayText = source.slice(openBracket, closeBracket + 1);
@@ -149,7 +149,7 @@ export async function auditProviders(
149
149
  ? await readCachedSource(targetDir, config.paths.ports, sourceCache)
150
150
  : "";
151
151
  const infrastructurePath = directoryPath(
152
- path.dirname(config.paths.infrastructurePorts),
152
+ path.dirname(config.paths.portWiring),
153
153
  );
154
154
  const schemaDir = `${infrastructurePath}/db/schema`;
155
155
 
@@ -1015,6 +1015,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
1015
1015
  return typeof value === "object" && value !== null && !Array.isArray(value);
1016
1016
  }
1017
1017
 
1018
+ function configuredProviderRequiredEnv(
1019
+ envVars: readonly string[],
1020
+ config: ResolvedBeignetConfig,
1021
+ ): string[] {
1022
+ const ignored = new Set(config.providerAudit.ignoreRequiredEnv);
1023
+ return envVars.filter((envVar) => !ignored.has(envVar));
1024
+ }
1025
+
1018
1026
  function configuredProviderRequiredTables(
1019
1027
  tableNames: readonly string[],
1020
1028
  config: ResolvedBeignetConfig,
@@ -1140,13 +1148,18 @@ async function providerAuditEntryForRule(options: {
1140
1148
  const requiredEnv =
1141
1149
  options.rule.variants !== undefined
1142
1150
  ? aggregateVariantRequirement(variants, "requiredEnv")
1143
- : await requirementAudit(options.rule.requiredEnv ?? [], (envVar) =>
1144
- providerRequiredEnvExists(
1145
- options.targetDir,
1146
- options.configFiles,
1147
- envVar,
1148
- options.sourceCache,
1151
+ : await requirementAudit(
1152
+ configuredProviderRequiredEnv(
1153
+ options.rule.requiredEnv ?? [],
1154
+ options.config,
1149
1155
  ),
1156
+ (envVar) =>
1157
+ providerRequiredEnvExists(
1158
+ options.targetDir,
1159
+ options.configFiles,
1160
+ envVar,
1161
+ options.sourceCache,
1162
+ ),
1150
1163
  );
1151
1164
 
1152
1165
  const requiredTables =
@@ -1222,12 +1235,16 @@ async function providerVariantAudit(options: {
1222
1235
  variant: ProviderDoctorVariantRule;
1223
1236
  registered: boolean;
1224
1237
  }): Promise<ProviderVariantAudit> {
1238
+ const requiredEnv = configuredProviderRequiredEnv(
1239
+ options.variant.requiredEnv,
1240
+ options.config,
1241
+ );
1225
1242
  if (!options.registered) {
1226
1243
  return {
1227
1244
  name: options.variant.name,
1228
1245
  displayName: options.variant.displayName,
1229
1246
  registration: "inactive",
1230
- requiredEnv: inactiveRequirement(options.variant.requiredEnv),
1247
+ requiredEnv: inactiveRequirement(requiredEnv),
1231
1248
  requiredTables: inactiveRequirement(options.variant.requiredTables),
1232
1249
  };
1233
1250
  }
@@ -1240,7 +1257,7 @@ async function providerVariantAudit(options: {
1240
1257
  options.variant.registrationSeverity,
1241
1258
  options.providerEntries,
1242
1259
  ),
1243
- requiredEnv: await requirementAudit(options.variant.requiredEnv, (envVar) =>
1260
+ requiredEnv: await requirementAudit(requiredEnv, (envVar) =>
1244
1261
  providerRequiredEnvExists(
1245
1262
  options.targetDir,
1246
1263
  options.configFiles,
@@ -1662,13 +1679,13 @@ function isInfraOrServerFile(
1662
1679
  ): boolean {
1663
1680
  const serverDir = directoryPath(path.dirname(config.paths.server));
1664
1681
  const infrastructureDir = directoryPath(
1665
- path.dirname(config.paths.infrastructurePorts),
1682
+ path.dirname(config.paths.portWiring),
1666
1683
  );
1667
1684
 
1668
1685
  return (
1669
1686
  file === config.paths.server ||
1670
1687
  file.startsWith(`${serverDir}/`) ||
1671
- file === config.paths.infrastructurePorts ||
1688
+ file === config.paths.portWiring ||
1672
1689
  file.startsWith(`${infrastructureDir}/`)
1673
1690
  );
1674
1691
  }
package/src/schedule.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import type { ErrorReporterPort } from "@beignet/core/error-reporting";
3
4
  import type { ProviderInstrumentationPort } from "@beignet/core/providers";
4
5
  import type {
5
6
  ScheduleDef,
@@ -8,6 +9,10 @@ import type {
8
9
  } from "@beignet/core/schedules";
9
10
  import { createJiti } from "jiti";
10
11
  import { loadBeignetConfig, normalizePath } from "./config.js";
12
+ import {
13
+ flushOperationalErrorReporter,
14
+ reportOperationalFailure,
15
+ } from "./operational-error-reporting.js";
11
16
 
12
17
  /**
13
18
  * Options for running one app-owned schedule.
@@ -56,6 +61,7 @@ type ScheduleContext = {
56
61
  logger?: ScheduleLogger;
57
62
  devtools?: ProviderInstrumentationPort;
58
63
  instrumentation?: ProviderInstrumentationPort;
64
+ errorReporter?: ErrorReporterPort;
59
65
  };
60
66
  };
61
67
 
@@ -131,7 +137,31 @@ export async function runAppSchedule(
131
137
  },
132
138
  });
133
139
 
134
- await runner.run(schedule, runOptions);
140
+ try {
141
+ await runner.run(schedule, runOptions);
142
+ } catch (error) {
143
+ await reportOperationalFailure({
144
+ ctx: scheduleContext,
145
+ error,
146
+ reportOptions: {
147
+ level: "error",
148
+ mechanism: "beignet.schedule.cli",
149
+ handled: false,
150
+ tags: {
151
+ "beignet.kind": "schedule",
152
+ "beignet.schedule": schedule.name,
153
+ },
154
+ contexts: {
155
+ schedule: {
156
+ name: schedule.name,
157
+ source: runOptions.source ?? "beignet-cli",
158
+ attempt: runOptions.attempt ?? null,
159
+ },
160
+ },
161
+ },
162
+ });
163
+ throw error;
164
+ }
135
165
 
136
166
  return {
137
167
  schemaVersion: 1,
@@ -148,6 +178,7 @@ export async function runAppSchedule(
148
178
  durationMs: Math.round(performance.now() - startedAt),
149
179
  };
150
180
  } finally {
181
+ await flushOperationalErrorReporter(scheduleContext);
151
182
  await scheduleModule.stopScheduleContext?.(ctx, contextArgs);
152
183
  }
153
184
  }
package/src/task.ts CHANGED
@@ -3,6 +3,11 @@ import path from "node:path";
3
3
  import type { TaskDef, TaskRunContextArgs } from "@beignet/core/tasks";
4
4
  import { createJiti } from "jiti";
5
5
  import { loadBeignetConfig, normalizePath } from "./config.js";
6
+ import {
7
+ flushOperationalErrorReporter,
8
+ type OperationalErrorReportingContext,
9
+ reportOperationalFailure,
10
+ } from "./operational-error-reporting.js";
6
11
 
7
12
  /**
8
13
  * Options for running an app-owned operational task.
@@ -63,10 +68,35 @@ export async function runAppTask(
63
68
  : {};
64
69
 
65
70
  try {
66
- const output = await runTask(task, {
67
- input: parsedInput,
68
- ctx,
69
- });
71
+ let output: unknown;
72
+ try {
73
+ output = await runTask(task, {
74
+ input: parsedInput,
75
+ ctx,
76
+ });
77
+ } catch (error) {
78
+ await reportOperationalFailure({
79
+ ctx: toOperationalContext(ctx),
80
+ error,
81
+ reportOptions: {
82
+ level: "error",
83
+ mechanism: "beignet.task.cli",
84
+ handled: false,
85
+ tags: {
86
+ "beignet.kind": "task",
87
+ "beignet.task": task.name,
88
+ },
89
+ contexts: {
90
+ task: {
91
+ name: task.name,
92
+ tenant: options.tenant ?? null,
93
+ source: "beignet-cli",
94
+ },
95
+ },
96
+ },
97
+ });
98
+ throw error;
99
+ }
70
100
 
71
101
  return {
72
102
  schemaVersion: 1,
@@ -79,10 +109,17 @@ export async function runAppTask(
79
109
  durationMs: Math.round(performance.now() - startedAt),
80
110
  };
81
111
  } finally {
112
+ await flushOperationalErrorReporter(toOperationalContext(ctx));
82
113
  await taskModule.stopTaskContext?.(ctx, contextArgs);
83
114
  }
84
115
  }
85
116
 
117
+ function toOperationalContext(ctx: unknown): OperationalErrorReportingContext {
118
+ return typeof ctx === "object" && ctx !== null
119
+ ? (ctx as OperationalErrorReportingContext)
120
+ : {};
121
+ }
122
+
86
123
  function parseTaskInputFlag(input: RunAppTaskOptions["input"]): unknown {
87
124
  if (input === undefined) return {};
88
125
  if (typeof input !== "string") return input;
@@ -314,7 +314,7 @@ Use \`${cli} make feature projects --recipe full-slice\` when you want a richer
314
314
  - \`features/shared/errors.ts\` keeps application errors and route-owned error schemas together.
315
315
  - \`ports/\` defines app-owned dependencies.
316
316
  - \`infra/\` implements ports for the selected runtime.
317
- - \`infra/db/schema/\` contains the Drizzle schema, \`drizzle/\` contains the checked-in migrations, and \`infra/todos/\` contains the durable todo repository adapter.
317
+ - \`infra/db/client.ts\` owns the process-wide database client shared by Better Auth and the Beignet provider; the public server \`stop()\` closes it after provider shutdown. \`infra/db/schema/\` contains the Drizzle schema, \`drizzle/\` contains the checked-in migrations, and \`infra/todos/\` contains the durable todo repository adapter.
318
318
  - \`server/routes.ts\` keeps the central route registry and OpenAPI contract list.
319
319
  - \`server/context.ts\` declares the context blueprint shared by the server and route tests.
320
320
  - \`server/providers.ts\` wires devtools, Better Auth, pino, ${
@@ -342,6 +342,7 @@ ${optionalAuthRedirects}
342
342
  cli +
343
343
  " db migrate` against it before starting the app."
344
344
  }
345
+ - Size the process-wide database client for the deployment. Postgres and MySQL use the generated pool-limit environment variable; count web, worker, preview, and task processes against the database connection limit, and keep writable SQLite files on one host.
345
346
  - Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.
346
347
  - Run \`${cli} db reset\` to rebuild a local ${
347
348
  ctx.database === "sqlite"
@@ -438,11 +439,15 @@ export function envExample(ctx: TemplateContext): string {
438
439
  "# Local Postgres matching the docker one-liner below; point at your own server in production.",
439
440
  `# ${databaseStartCommand("postgres", ctx.name)}`,
440
441
  `POSTGRES_DB_URL=${databaseLocalUrl("postgres", ctx.name)}`,
442
+ "# Maximum connections opened by each running app process.",
443
+ "POSTGRES_DB_POOL_MAX=5",
441
444
  ]
442
445
  : [
443
446
  "# Local MySQL matching the docker one-liner below; point at your own server in production.",
444
447
  `# ${databaseStartCommand("mysql", ctx.name)}`,
445
448
  `MYSQL_DB_URL=${databaseLocalUrl("mysql", ctx.name)}`,
449
+ "# Maximum connections opened by each running app process.",
450
+ "MYSQL_DB_POOL_MAX=5",
446
451
  ];
447
452
 
448
453
  const lines = [
@@ -10,6 +10,7 @@ import { sqliteDbFiles } from "./sqlite.js";
10
10
  */
11
11
  export type DbTemplateFiles = {
12
12
  drizzleConfig: string;
13
+ dbClient: string;
13
14
  dbSchema: string;
14
15
  drizzleTodoRepository: string;
15
16
  dbRepositories: string;
@@ -36,6 +36,22 @@ export function mysqlDbFiles(ctx: TemplateContext): DbTemplateFiles {
36
36
  url: process.env.MYSQL_DB_URL ?? "${databaseLocalUrl("mysql", ctx.name)}",
37
37
  },
38
38
  };
39
+ `,
40
+ dbClient: `import "@beignet/core/server-only";
41
+ import mysql from "mysql2/promise";
42
+ import { env } from "@/lib/env";
43
+
44
+ export const databaseClient = mysql.createPool({
45
+ uri: env.MYSQL_DB_URL,
46
+ connectionLimit: env.MYSQL_DB_POOL_MAX,
47
+ });
48
+
49
+ let closePromise: Promise<void> | undefined;
50
+
51
+ export function closeDatabaseClient(): Promise<void> {
52
+ closePromise ??= Promise.resolve().then(() => databaseClient.end());
53
+ return closePromise;
54
+ }
39
55
  `,
40
56
  dbSchema: `import {
41
57
  boolean,
@@ -33,6 +33,22 @@ export function postgresDbFiles(ctx: TemplateContext): DbTemplateFiles {
33
33
  url: process.env.POSTGRES_DB_URL ?? "${databaseLocalUrl("postgres", ctx.name)}",
34
34
  },
35
35
  };
36
+ `,
37
+ dbClient: `import "@beignet/core/server-only";
38
+ import pg from "pg";
39
+ import { env } from "@/lib/env";
40
+
41
+ export const databaseClient = new pg.Pool({
42
+ connectionString: env.POSTGRES_DB_URL,
43
+ max: env.POSTGRES_DB_POOL_MAX,
44
+ });
45
+
46
+ let closePromise: Promise<void> | undefined;
47
+
48
+ export function closeDatabaseClient(): Promise<void> {
49
+ closePromise ??= Promise.resolve().then(() => databaseClient.end());
50
+ return closePromise;
51
+ }
36
52
  `,
37
53
  dbSchema: `import { boolean, index, pgTable, text, timestamp } from "drizzle-orm/pg-core";
38
54
 
@@ -24,6 +24,22 @@ const files = {
24
24
  authToken: process.env.SQLITE_DB_AUTH_TOKEN,
25
25
  },
26
26
  };
27
+ `,
28
+ dbClient: `import "@beignet/core/server-only";
29
+ import { createClient } from "@libsql/client";
30
+ import { env } from "@/lib/env";
31
+
32
+ export const databaseClient = createClient({
33
+ url: env.SQLITE_DB_URL,
34
+ authToken: env.SQLITE_DB_AUTH_TOKEN,
35
+ });
36
+
37
+ let closePromise: Promise<void> | undefined;
38
+
39
+ export function closeDatabaseClient(): Promise<void> {
40
+ closePromise ??= Promise.resolve().then(() => databaseClient.close());
41
+ return closePromise;
42
+ }
27
43
  `,
28
44
  dbSchema: `import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
29
45
 
@@ -19,8 +19,8 @@ import { dbFiles } from "./db/index.js";
19
19
  import {
20
20
  betterAuth,
21
21
  env,
22
- infrastructurePorts,
23
22
  ports,
23
+ portWiring,
24
24
  server,
25
25
  serverFiles,
26
26
  serverProviders,
@@ -109,7 +109,8 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
109
109
  },
110
110
  { path: "ports/index.ts", content: ports(ctx) },
111
111
  { path: "ports/auth.ts", content: serverFiles.authPort },
112
- { path: "infra/app-ports.ts", content: infrastructurePorts(ctx) },
112
+ { path: "infra/port-wiring.ts", content: portWiring(ctx) },
113
+ { path: "infra/db/client.ts", content: db.dbClient },
113
114
  { path: "infra/db/database-ready.ts", content: db.databaseReady },
114
115
  { path: "infra/db/provider.ts", content: db.dbProvider },
115
116
  { path: "infra/db/repositories.ts", content: db.dbRepositories },
@@ -137,7 +138,7 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
137
138
  ...testSupportTemplateFiles,
138
139
  { path: "lib/use-case.ts", content: serverFiles.useCaseBuilder },
139
140
  { path: "lib/routes.ts", content: serverFiles.routesBuilder },
140
- { path: "server/index.ts", content: server(ctx) },
141
+ { path: "server/index.ts", content: server() },
141
142
  { path: "server/context.ts", content: serverFiles.serverContext },
142
143
  { path: "server/routes.ts", content: serverFiles.serverRoutes },
143
144
  { path: "server/providers.ts", content: serverProviders(ctx) },