@beignet/cli 0.0.37 → 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 (70) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +18 -5
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/inspect.js +15 -15
  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.js +3 -3
  37. package/dist/provider-audit.js.map +1 -1
  38. package/dist/schedule.d.ts.map +1 -1
  39. package/dist/schedule.js +28 -1
  40. package/dist/schedule.js.map +1 -1
  41. package/dist/task.d.ts.map +1 -1
  42. package/dist/task.js +37 -4
  43. package/dist/task.js.map +1 -1
  44. package/dist/templates/index.js +2 -2
  45. package/dist/templates/index.js.map +1 -1
  46. package/dist/templates/server.d.ts +1 -1
  47. package/dist/templates/server.d.ts.map +1 -1
  48. package/dist/templates/server.js +5 -5
  49. package/dist/templates/server.js.map +1 -1
  50. package/dist/templates/testing.js +1 -1
  51. package/dist/templates/todos.js +6 -6
  52. package/package.json +4 -4
  53. package/src/config.ts +2 -2
  54. package/src/inspect.ts +15 -17
  55. package/src/lint.ts +35 -4
  56. package/src/make/inbox.ts +7 -7
  57. package/src/make/payments.ts +23 -15
  58. package/src/make/shared.ts +56 -40
  59. package/src/make/tenancy.ts +8 -8
  60. package/src/make.ts +30 -16
  61. package/src/operational-error-reporting.ts +60 -0
  62. package/src/outbox.ts +77 -0
  63. package/src/provider-add.ts +21 -17
  64. package/src/provider-audit.ts +3 -3
  65. package/src/schedule.ts +32 -1
  66. package/src/task.ts +41 -4
  67. package/src/templates/index.ts +2 -2
  68. package/src/templates/server.ts +5 -5
  69. package/src/templates/testing.ts +1 -1
  70. package/src/templates/todos.ts +6 -6
@@ -330,7 +330,7 @@ export async function updateTenancyWiring(
330
330
  if (
331
331
  await updateWorkspaceInfrastructurePorts(targetDir, config, editOptions)
332
332
  ) {
333
- updated.add(config.paths.infrastructurePorts);
333
+ updated.add(config.paths.portWiring);
334
334
  }
335
335
  if (
336
336
  await updateDrizzleSchemaIndexExports(
@@ -524,7 +524,7 @@ export async function updateWorkspaceInfrastructurePorts(
524
524
  config: ResolvedBeignetConfig,
525
525
  options: { dryRun: boolean },
526
526
  ): Promise<boolean> {
527
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
527
+ const filePath = path.join(targetDir, config.paths.portWiring);
528
528
  const original = await readFile(filePath, "utf8");
529
529
  let next = original;
530
530
 
@@ -532,21 +532,21 @@ export async function updateWorkspaceInfrastructurePorts(
532
532
  const appended = appendDeferredPortKey(next, portKey);
533
533
  if (appended === next && !new RegExp(`["']${portKey}["']`).test(next)) {
534
534
  throw new Error(
535
- `Could not find the deferred port list in ${config.paths.infrastructurePorts}. Add "${portKey}" manually, or restore the generated infrastructure ports file before running make tenancy.`,
535
+ `Could not find the deferred port list in ${config.paths.portWiring}. Add "${portKey}" manually, or restore the generated port wiring file before running make tenancy.`,
536
536
  );
537
537
  }
538
538
  next = appended;
539
539
  }
540
540
 
541
541
  const policyModule = relativeModule(
542
- config.paths.infrastructurePorts,
542
+ config.paths.portWiring,
543
543
  path.join(config.paths.features, "workspaces/policy.ts"),
544
544
  );
545
545
  next = addNamedImport(next, "workspacePolicy", policyModule);
546
546
  next = appendCreateGatePolicy(
547
547
  next,
548
548
  "workspacePolicy",
549
- `Could not find createGate({ policies: [...] }) in ${config.paths.infrastructurePorts}. Add workspacePolicy to the gate policies manually, or restore the generated infrastructure ports file before running make tenancy.`,
549
+ `Could not find createGate({ policies: [...] }) in ${config.paths.portWiring}. Add workspacePolicy to the gate policies manually, or restore the generated port wiring file before running make tenancy.`,
550
550
  );
551
551
 
552
552
  if (next === original) return false;
@@ -2274,7 +2274,7 @@ import type {
2274
2274
  \tWorkspaceMemberWithUser,
2275
2275
  \tWorkspaceRepository,
2276
2276
  } from "@/features/workspaces/ports";
2277
- import { appPorts } from "@/infra/app-ports";
2277
+ import { initialPorts } from "@/infra/port-wiring";
2278
2278
  import type { AppTransactionPorts } from "@/ports";
2279
2279
 
2280
2280
  export type MemoryWorkspacePorts = {
@@ -2468,9 +2468,9 @@ export type WorkspaceTestFixture = TestPortsFixture<
2468
2468
  export function createWorkspaceFixture(): WorkspaceTestFixture {
2469
2469
  \tconst repositories = createMemoryWorkspacePorts();
2470
2470
  \tconst fixture = createTestPorts<AppContext["ports"], AppTransactionPorts>({
2471
- \t\tbase: appPorts,
2471
+ \t\tbase: initialPorts,
2472
2472
  \t\toverrides: {
2473
- \t\t\tgate: appPorts.gate,
2473
+ \t\t\tgate: initialPorts.gate,
2474
2474
  \t\t\t...repositories,
2475
2475
  \t\t},
2476
2476
  \t});
package/src/make.ts CHANGED
@@ -911,7 +911,7 @@ export async function makePort(
911
911
  dryRun: Boolean(options.dryRun),
912
912
  })
913
913
  ) {
914
- updatedFiles.push(config.paths.infrastructurePorts);
914
+ updatedFiles.push(config.paths.portWiring);
915
915
  }
916
916
 
917
917
  return {
@@ -957,7 +957,7 @@ export async function makeAdapter(
957
957
  dryRun: Boolean(options.dryRun),
958
958
  })
959
959
  ) {
960
- updatedFiles.push(config.paths.infrastructurePorts);
960
+ updatedFiles.push(config.paths.portWiring);
961
961
  }
962
962
 
963
963
  return {
@@ -1034,6 +1034,9 @@ export async function makeEvent(
1034
1034
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1035
1035
  force: Boolean(options.force),
1036
1036
  });
1037
+ await updateOutboxRegistry(targetDir, names, "events", config, {
1038
+ dryRun: true,
1039
+ });
1037
1040
 
1038
1041
  const result = await makeFeatureArtifact({
1039
1042
  targetDir,
@@ -1078,6 +1081,9 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1078
1081
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1079
1082
  force: Boolean(options.force),
1080
1083
  });
1084
+ await updateOutboxRegistry(targetDir, names, "jobs", config, {
1085
+ dryRun: true,
1086
+ });
1081
1087
 
1082
1088
  const result = await makeFeatureArtifact({
1083
1089
  targetDir,
@@ -1697,7 +1703,7 @@ async function updateOutboxPortWiring(
1697
1703
  changes.updatedFiles.push(config.paths.ports);
1698
1704
  }
1699
1705
  if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
1700
- changes.updatedFiles.push(config.paths.infrastructurePorts);
1706
+ changes.updatedFiles.push(config.paths.portWiring);
1701
1707
  }
1702
1708
  if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
1703
1709
  changes.updatedFiles.push(
@@ -1805,7 +1811,7 @@ async function updateOutboxInfrastructurePorts(
1805
1811
  config: ResolvedBeignetConfig,
1806
1812
  options: { dryRun: boolean },
1807
1813
  ): Promise<boolean> {
1808
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1814
+ const filePath = path.join(targetDir, config.paths.portWiring);
1809
1815
  const original = await readFile(filePath, "utf8");
1810
1816
  let next = appendDeferredPortKey(original, "outbox");
1811
1817
  next = appendDeferredPortKey(next, "outboxAdmin");
@@ -2166,7 +2172,7 @@ export async function makeOutbox(
2166
2172
  ...cronSecretPlan.files.map((file) => file.path),
2167
2173
  ...cronSecretPlan.skippedFiles,
2168
2174
  config.paths.ports,
2169
- config.paths.infrastructurePorts,
2175
+ config.paths.portWiring,
2170
2176
  path.join(infrastructureDir(config), "db/provider.ts"),
2171
2177
  drizzleRepositoriesPath(config),
2172
2178
  outboxDrizzleSchemaFilePath(config),
@@ -2361,19 +2367,19 @@ async function updateResourceWiring(
2361
2367
  persistence === "memory" &&
2362
2368
  (await updateInfrastructurePorts(targetDir, names, config, options))
2363
2369
  ) {
2364
- updated.add(config.paths.infrastructurePorts);
2370
+ updated.add(config.paths.portWiring);
2365
2371
  }
2366
2372
  if (
2367
2373
  generationOptions.auth &&
2368
2374
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))
2369
2375
  ) {
2370
- updated.add(config.paths.infrastructurePorts);
2376
+ updated.add(config.paths.portWiring);
2371
2377
  }
2372
2378
  if (
2373
2379
  persistence === "drizzle" &&
2374
2380
  (await updateInfrastructureDeferredPorts(targetDir, names, config, options))
2375
2381
  ) {
2376
- updated.add(config.paths.infrastructurePorts);
2382
+ updated.add(config.paths.portWiring);
2377
2383
  }
2378
2384
  if (
2379
2385
  persistence === "drizzle" &&
@@ -3381,7 +3387,13 @@ async function updateOutboxRegistry(
3381
3387
  importLine,
3382
3388
  );
3383
3389
 
3384
- if (appended.kind === "missing" || appended.kind === "unchanged") {
3390
+ if (appended.kind === "missing") {
3391
+ throw new Error(
3392
+ `Could not find the ${kind} array in ${config.paths.outbox}. Register ${registryName} manually, or restore the generated outbox file before running this command.`,
3393
+ );
3394
+ }
3395
+
3396
+ if (appended.kind === "unchanged") {
3385
3397
  return "skipped";
3386
3398
  }
3387
3399
 
@@ -3988,6 +4000,7 @@ export const outboxMessages = mysqlTable(
3988
4000
  \t\tkind: varchar("kind", { length: 32 }).notNull(),
3989
4001
  \t\tname: varchar("name", { length: 255 }).notNull(),
3990
4002
  \t\tpayloadJson: longtext("payload_json").notNull(),
4003
+ \t\ttraceContextJson: longtext("trace_context_json"),
3991
4004
  \t\tstatus: varchar("status", { length: 32 }).notNull(),
3992
4005
  \t\tattempts: int("attempts").notNull().default(0),
3993
4006
  \t\tmaxAttempts: int("max_attempts").notNull().default(3),
@@ -4023,6 +4036,7 @@ export const outboxMessages = ${dialect.tableFunction}(
4023
4036
  \t\tkind: text("kind").notNull(),
4024
4037
  \t\tname: text("name").notNull(),
4025
4038
  \t\tpayloadJson: text("payload_json").notNull(),
4039
+ \t\ttraceContextJson: text("trace_context_json"),
4026
4040
  \t\tstatus: text("status").notNull(),
4027
4041
  \t\tattempts: integer("attempts").notNull().default(0),
4028
4042
  \t\tmaxAttempts: integer("max_attempts").notNull().default(3),
@@ -4503,15 +4517,15 @@ import { createInMemoryDevtools } from "@beignet/devtools";
4503
4517
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
4504
4518
  import { createTestAnonymousActor } from "@beignet/core/testing";
4505
4519
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
4506
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
4520
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
4507
4521
  import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
4508
4522
 
4509
4523
  describe("${names.exportName}", () => {
4510
4524
  it("runs ${names.action.camel}", async () => {
4511
4525
  const testFixture = createTestPorts<AppContext["ports"]>({
4512
- base: appPorts,
4526
+ base: initialPorts,
4513
4527
  overrides: {
4514
- gate: appPorts.gate,
4528
+ gate: initialPorts.gate,
4515
4529
  devtools: createInMemoryDevtools(),
4516
4530
  },
4517
4531
  });
@@ -5276,7 +5290,7 @@ function testFile(
5276
5290
  options: ResourceGenerationOptions,
5277
5291
  ): string {
5278
5292
  const repositoryPath = path.join(
5279
- path.dirname(config.paths.infrastructurePorts),
5293
+ path.dirname(config.paths.portWiring),
5280
5294
  names.pluralKebab,
5281
5295
  `in-memory-${names.singularKebab}-repository.ts`,
5282
5296
  );
@@ -5294,7 +5308,7 @@ import { createInMemoryDevtools } from "@beignet/devtools";
5294
5308
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
5295
5309
  import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
5296
5310
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5297
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
5311
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
5298
5312
  import { appContext } from "${aliasModule(serverContextPath)}";
5299
5313
  import {
5300
5314
  create${names.singularPascal},
@@ -5334,7 +5348,7 @@ ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${
5334
5348
  ${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
5335
5349
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
5336
5350
  const testFixture = createTestPorts<AppContext["ports"]>({
5337
- base: appPorts,
5351
+ base: initialPorts,
5338
5352
  overrides: {
5339
5353
  ${names.pluralCamel},
5340
5354
  ${
@@ -5345,7 +5359,7 @@ ${
5345
5359
  ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
5346
5360
  }),
5347
5361
  },
5348
- ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
5362
+ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5349
5363
  `
5350
5364
  : `\t\t\t\tauth: {
5351
5365
  getSession: async () => null,
@@ -0,0 +1,60 @@
1
+ import type {
2
+ ErrorReporterPort,
3
+ ErrorReportOptions,
4
+ } from "@beignet/core/error-reporting";
5
+
6
+ export type OperationalErrorReportingContext = {
7
+ requestId?: string;
8
+ traceId?: string;
9
+ ports?: {
10
+ errorReporter?: ErrorReporterPort;
11
+ };
12
+ };
13
+
14
+ export async function reportOperationalFailure(args: {
15
+ ctx: OperationalErrorReportingContext;
16
+ error: unknown;
17
+ reportOptions: ErrorReportOptions;
18
+ }): Promise<void> {
19
+ const reporter = args.ctx.ports?.errorReporter;
20
+ if (!reporter) return;
21
+
22
+ try {
23
+ const { tryReportException } = await import(
24
+ "@beignet/core/error-reporting"
25
+ );
26
+ await tryReportException({
27
+ reporter,
28
+ error: args.error,
29
+ reportOptions: {
30
+ requestId: args.ctx.requestId,
31
+ traceId: args.ctx.traceId,
32
+ ...args.reportOptions,
33
+ },
34
+ });
35
+ } catch {
36
+ // Reporting must not replace the operational command failure.
37
+ }
38
+ }
39
+
40
+ export async function flushOperationalErrorReporter(
41
+ ctx: OperationalErrorReportingContext,
42
+ timeoutMs = 1000,
43
+ ): Promise<void> {
44
+ const reporter = ctx.ports?.errorReporter;
45
+ if (!reporter) return;
46
+
47
+ let timeout: ReturnType<typeof setTimeout> | undefined;
48
+ try {
49
+ await Promise.race([
50
+ reporter.flush({ timeoutMs }),
51
+ new Promise<boolean>((resolve) => {
52
+ timeout = setTimeout(() => resolve(false), timeoutMs);
53
+ }),
54
+ ]);
55
+ } catch {
56
+ // Reporting shutdown must not replace the operational command result.
57
+ } finally {
58
+ if (timeout !== undefined) clearTimeout(timeout);
59
+ }
60
+ }
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
 
@@ -1679,13 +1679,13 @@ function isInfraOrServerFile(
1679
1679
  ): boolean {
1680
1680
  const serverDir = directoryPath(path.dirname(config.paths.server));
1681
1681
  const infrastructureDir = directoryPath(
1682
- path.dirname(config.paths.infrastructurePorts),
1682
+ path.dirname(config.paths.portWiring),
1683
1683
  );
1684
1684
 
1685
1685
  return (
1686
1686
  file === config.paths.server ||
1687
1687
  file.startsWith(`${serverDir}/`) ||
1688
- file === config.paths.infrastructurePorts ||
1688
+ file === config.paths.portWiring ||
1689
1689
  file.startsWith(`${infrastructureDir}/`)
1690
1690
  );
1691
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
  }