@beignet/cli 0.0.26 → 0.0.28

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 (97) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +59 -15
  3. package/dist/check.d.ts +61 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +164 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/choices.d.ts +8 -0
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +14 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/db.d.ts +8 -0
  12. package/dist/db.d.ts.map +1 -1
  13. package/dist/db.js +4 -4
  14. package/dist/db.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +156 -21
  17. package/dist/index.js.map +1 -1
  18. package/dist/inspect.d.ts.map +1 -1
  19. package/dist/inspect.js +121 -3
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/make/inbox.d.ts +63 -0
  22. package/dist/make/inbox.d.ts.map +1 -0
  23. package/dist/make/inbox.js +1747 -0
  24. package/dist/make/inbox.js.map +1 -0
  25. package/dist/make/payments.d.ts +61 -0
  26. package/dist/make/payments.d.ts.map +1 -0
  27. package/dist/make/payments.js +1900 -0
  28. package/dist/make/payments.js.map +1 -0
  29. package/dist/make/shared.d.ts +592 -0
  30. package/dist/make/shared.d.ts.map +1 -0
  31. package/dist/make/shared.js +2233 -0
  32. package/dist/make/shared.js.map +1 -0
  33. package/dist/make/tenancy.d.ts +101 -0
  34. package/dist/make/tenancy.d.ts.map +1 -0
  35. package/dist/make/tenancy.js +4409 -0
  36. package/dist/make/tenancy.js.map +1 -0
  37. package/dist/make.d.ts +8 -44
  38. package/dist/make.d.ts.map +1 -1
  39. package/dist/make.js +57 -3689
  40. package/dist/make.js.map +1 -1
  41. package/dist/mcp.d.ts.map +1 -1
  42. package/dist/mcp.js +45 -4
  43. package/dist/mcp.js.map +1 -1
  44. package/dist/provider-add.d.ts +19 -0
  45. package/dist/provider-add.d.ts.map +1 -0
  46. package/dist/provider-add.js +691 -0
  47. package/dist/provider-add.js.map +1 -0
  48. package/dist/provider-audit.d.ts.map +1 -1
  49. package/dist/provider-audit.js +77 -1
  50. package/dist/provider-audit.js.map +1 -1
  51. package/dist/registry-edits.d.ts +9 -0
  52. package/dist/registry-edits.d.ts.map +1 -1
  53. package/dist/registry-edits.js +32 -0
  54. package/dist/registry-edits.js.map +1 -1
  55. package/dist/task.d.ts +2 -0
  56. package/dist/task.d.ts.map +1 -1
  57. package/dist/task.js +2 -0
  58. package/dist/task.js.map +1 -1
  59. package/dist/templates/agents.d.ts.map +1 -1
  60. package/dist/templates/agents.js +29 -11
  61. package/dist/templates/agents.js.map +1 -1
  62. package/dist/templates/base.d.ts.map +1 -1
  63. package/dist/templates/base.js +42 -12
  64. package/dist/templates/base.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +7 -1
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/server.d.ts.map +1 -1
  69. package/dist/templates/server.js +44 -37
  70. package/dist/templates/server.js.map +1 -1
  71. package/dist/templates/shared.d.ts +5 -0
  72. package/dist/templates/shared.d.ts.map +1 -1
  73. package/dist/templates/shared.js +5 -0
  74. package/dist/templates/shared.js.map +1 -1
  75. package/package.json +2 -2
  76. package/skills/app-structure/SKILL.md +1 -1
  77. package/src/check.ts +246 -0
  78. package/src/choices.ts +28 -0
  79. package/src/db.ts +4 -4
  80. package/src/index.ts +232 -21
  81. package/src/inspect.ts +168 -2
  82. package/src/make/inbox.ts +2015 -0
  83. package/src/make/payments.ts +2182 -0
  84. package/src/make/shared.ts +3638 -0
  85. package/src/make/tenancy.ts +4809 -0
  86. package/src/make.ts +388 -5355
  87. package/src/mcp.ts +66 -3
  88. package/src/provider-add.ts +926 -0
  89. package/src/provider-audit.ts +95 -3
  90. package/src/registry-edits.ts +47 -0
  91. package/src/task.ts +8 -10
  92. package/src/templates/agents.ts +29 -11
  93. package/src/templates/base.ts +43 -12
  94. package/src/templates/db/sqlite.ts +7 -1
  95. package/src/templates/server.ts +44 -37
  96. package/src/templates/shared.ts +5 -0
  97. package/src/test-helpers/generated-app.ts +168 -0
package/src/index.ts CHANGED
@@ -26,12 +26,15 @@ import {
26
26
  makeFeatureAddonChoices,
27
27
  makeFeatureRecipeChoices,
28
28
  type PackageManager,
29
+ type ProviderPresetName,
29
30
  packageManagerChoices,
31
+ providerPresetChoices,
30
32
  type TemplateName,
31
33
  templateChoices,
32
34
  } from "./choices.js";
33
35
  // Type-only: the runtime module stays behind the create command's lazy loader.
34
36
  import type { CreateSelections } from "./create-prompts.js";
37
+ import type { AddProviderPresetResult } from "./provider-add.js";
35
38
  import { getCliVersion } from "./version.js";
36
39
 
37
40
  type CliContext = CommandContext & {
@@ -102,6 +105,12 @@ type LintFlags = {
102
105
  format?: OutputFormat;
103
106
  };
104
107
 
108
+ type CheckFlags = {
109
+ json?: boolean;
110
+ fix?: boolean;
111
+ cwd?: string;
112
+ };
113
+
105
114
  type DbFlags = {
106
115
  json?: boolean;
107
116
  dryRun?: boolean;
@@ -131,6 +140,7 @@ type DbSchemaGenerateFlags = DbFlags & {
131
140
  type TaskRunFlags = {
132
141
  json?: boolean;
133
142
  input?: string;
143
+ tenant?: string;
134
144
  module?: string;
135
145
  };
136
146
 
@@ -168,7 +178,24 @@ type ProviderAuditFlags = {
168
178
  cwd?: string;
169
179
  };
170
180
 
181
+ type ProviderAddFlags = {
182
+ json?: boolean;
183
+ dryRun?: boolean;
184
+ cwd?: string;
185
+ };
186
+
171
187
  const parseString = (input: string): string => input;
188
+ const parseProviderPreset = (input: string): ProviderPresetName => {
189
+ if ((providerPresetChoices as readonly string[]).includes(input)) {
190
+ return input as ProviderPresetName;
191
+ }
192
+
193
+ throw new Error(
194
+ `Unknown provider preset "${input}". Available presets: ${providerPresetChoices.join(
195
+ ", ",
196
+ )}.`,
197
+ );
198
+ };
172
199
  const parsePositiveInteger = (input: string): number => {
173
200
  const value = Number(input);
174
201
  if (!Number.isInteger(value) || value <= 0) {
@@ -240,6 +267,12 @@ const providerAuditFlagParameters = {
240
267
  cwd: cwdFlag,
241
268
  } satisfies FlagParametersForType<ProviderAuditFlags, CliContext>;
242
269
 
270
+ const providerAddFlagParameters = {
271
+ json: jsonFlag,
272
+ dryRun: dryRunFlag,
273
+ cwd: cwdFlag,
274
+ } satisfies FlagParametersForType<ProviderAddFlags, CliContext>;
275
+
243
276
  const dbFlagParameters = {
244
277
  json: jsonFlag,
245
278
  dryRun: dryRunFlag,
@@ -270,6 +303,9 @@ const dbSchemaGenerateFlagParameters = {
270
303
  const taskRunFlagParameters = {
271
304
  json: jsonFlag,
272
305
  input: parsedStringFlag("JSON input passed to the task schema."),
306
+ tenant: parsedStringFlag(
307
+ "Tenant id or slug passed to the app's createTaskContext, separate from task input.",
308
+ ),
273
309
  module: parsedStringFlag(
274
310
  "Task registry module. Defaults to server/tasks.ts.",
275
311
  ),
@@ -317,6 +353,19 @@ const namePositional = {
317
353
  ],
318
354
  } as const;
319
355
 
356
+ const providerPresetPositional = {
357
+ kind: "tuple",
358
+ parameters: [
359
+ {
360
+ parse: parseProviderPreset,
361
+ placeholder: "preset",
362
+ brief: `Provider preset to add. Available: ${providerPresetChoices.join(
363
+ ", ",
364
+ )}.`,
365
+ },
366
+ ],
367
+ } as const;
368
+
320
369
  function resolveOutputFormat(flags: {
321
370
  json?: boolean;
322
371
  format?: OutputFormat;
@@ -558,6 +607,55 @@ const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
558
607
  },
559
608
  });
560
609
 
610
+ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
611
+ docs: {
612
+ brief: "Run the full app validation loop as one command.",
613
+ fullDescription:
614
+ "Runs beignet lint, beignet doctor --strict, and the app's lint, typecheck, and test package scripts through the detected package manager. Every step runs even when an earlier one fails, so one run reports everything. Missing package scripts are skipped, not failed.",
615
+ },
616
+ parameters: {
617
+ flags: {
618
+ json: jsonFlag,
619
+ fix: {
620
+ kind: "boolean",
621
+ optional: true,
622
+ withNegated: false,
623
+ brief: "Apply low-risk doctor fixes before checking.",
624
+ },
625
+ cwd: cwdFlag,
626
+ } satisfies FlagParametersForType<CheckFlags, CliContext>,
627
+ },
628
+ loader: async () => {
629
+ const { checkApp, formatCheckStep, formatCheckSummary } = await import(
630
+ "./check.js"
631
+ );
632
+
633
+ return async function runCheck(this: CliContext, flags: CheckFlags) {
634
+ const color = useColor();
635
+ const result = await checkApp({
636
+ cwd: flags.cwd,
637
+ fix: flags.fix,
638
+ color,
639
+ ...(flags.json
640
+ ? {}
641
+ : {
642
+ onStep: (step) =>
643
+ writeOutput(this, formatCheckStep(step, { color })),
644
+ }),
645
+ });
646
+ writeOutput(
647
+ this,
648
+ flags.json
649
+ ? JSON.stringify(result, null, 2)
650
+ : `\n${formatCheckSummary(result, { color })}`,
651
+ );
652
+ if (!result.ok) {
653
+ this.process.exitCode = 1;
654
+ }
655
+ };
656
+ },
657
+ });
658
+
561
659
  const lintCommand = buildCommand<LintFlags, [], CliContext>({
562
660
  docs: {
563
661
  brief: "Check Beignet dependency direction conventions.",
@@ -639,11 +737,50 @@ const providerAuditCommand = buildCommand<ProviderAuditFlags, [], CliContext>({
639
737
  },
640
738
  });
641
739
 
740
+ const providerAddCommand = buildCommand<
741
+ ProviderAddFlags,
742
+ [ProviderPresetName],
743
+ CliContext
744
+ >({
745
+ docs: {
746
+ brief: "Add a provider setup preset to a Beignet app.",
747
+ fullDescription: `Updates an existing Beignet app with provider package dependencies, server/provider registration, app port wiring, .env.example entries, and local setup notes.
748
+
749
+ Available presets: ${providerPresetChoices.join(", ")}`,
750
+ },
751
+ parameters: {
752
+ flags: providerAddFlagParameters,
753
+ positional: providerPresetPositional,
754
+ },
755
+ loader: async () => {
756
+ const { addProviderPreset } = await import("./provider-add.js");
757
+
758
+ return async function runProviderAdd(
759
+ this: CliContext,
760
+ flags: ProviderAddFlags,
761
+ name: ProviderPresetName,
762
+ ) {
763
+ const result = await addProviderPreset({
764
+ name,
765
+ cwd: flags.cwd,
766
+ dryRun: Boolean(flags.dryRun),
767
+ });
768
+ writeOutput(
769
+ this,
770
+ flags.json
771
+ ? JSON.stringify(result, null, 2)
772
+ : providerAddNextSteps(result),
773
+ );
774
+ };
775
+ },
776
+ });
777
+
642
778
  const providerRoutes = buildRouteMap({
643
779
  docs: {
644
- brief: "Inspect Beignet provider setup.",
780
+ brief: "Manage Beignet provider setup.",
645
781
  },
646
782
  routes: {
783
+ add: providerAddCommand,
647
784
  audit: providerAuditCommand,
648
785
  },
649
786
  });
@@ -760,6 +897,7 @@ const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
760
897
  const result = await runAppTask({
761
898
  name,
762
899
  input: flags.input,
900
+ tenant: flags.tenant,
763
901
  modulePath: flags.module,
764
902
  });
765
903
 
@@ -1215,7 +1353,9 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
1215
1353
  flags: makeFlagParameters,
1216
1354
  },
1217
1355
  loader: async () => {
1218
- const { makePayments } = await import("./make.js");
1356
+ const { makePayments, makePaymentsNextStepLines } = await import(
1357
+ "./make.js"
1358
+ );
1219
1359
 
1220
1360
  return async function runMakePayments(this: CliContext, flags: MakeFlags) {
1221
1361
  const result = await makePayments({
@@ -1226,7 +1366,69 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
1226
1366
  this,
1227
1367
  flags.json
1228
1368
  ? JSON.stringify(result, null, 2)
1229
- : makePaymentsNextSteps(result),
1369
+ : makeNextSteps(
1370
+ result,
1371
+ "billing slice",
1372
+ makePaymentsNextStepLines(result),
1373
+ ),
1374
+ );
1375
+ };
1376
+ },
1377
+ });
1378
+
1379
+ const makeTenancyCommand = buildCommand<MakeFlags, [], CliContext>({
1380
+ docs: {
1381
+ brief: "Generate a workspace tenancy slice.",
1382
+ },
1383
+ parameters: {
1384
+ flags: makeFlagParameters,
1385
+ },
1386
+ loader: async () => {
1387
+ const { makeTenancy, makeTenancyNextStepLines } = await import("./make.js");
1388
+
1389
+ return async function runMakeTenancy(this: CliContext, flags: MakeFlags) {
1390
+ const result = await makeTenancy({
1391
+ force: Boolean(flags.force),
1392
+ dryRun: Boolean(flags.dryRun),
1393
+ });
1394
+ writeOutput(
1395
+ this,
1396
+ flags.json
1397
+ ? JSON.stringify(result, null, 2)
1398
+ : makeNextSteps(
1399
+ result,
1400
+ "workspace slice",
1401
+ makeTenancyNextStepLines(),
1402
+ ),
1403
+ );
1404
+ };
1405
+ },
1406
+ });
1407
+
1408
+ const makeInboxCommand = buildCommand<MakeFlags, [], CliContext>({
1409
+ docs: {
1410
+ brief: "Generate an in-app inbox notifications slice.",
1411
+ },
1412
+ parameters: {
1413
+ flags: makeFlagParameters,
1414
+ },
1415
+ loader: async () => {
1416
+ const { makeInbox, makeInboxNextStepLines } = await import("./make.js");
1417
+
1418
+ return async function runMakeInbox(this: CliContext, flags: MakeFlags) {
1419
+ const result = await makeInbox({
1420
+ force: Boolean(flags.force),
1421
+ dryRun: Boolean(flags.dryRun),
1422
+ });
1423
+ writeOutput(
1424
+ this,
1425
+ flags.json
1426
+ ? JSON.stringify(result, null, 2)
1427
+ : makeNextSteps(
1428
+ result,
1429
+ "notifications slice",
1430
+ makeInboxNextStepLines(),
1431
+ ),
1230
1432
  );
1231
1433
  };
1232
1434
  },
@@ -1283,6 +1485,7 @@ const makeRoutes = buildRouteMap({
1283
1485
  makeFactoryNextSteps,
1284
1486
  ),
1285
1487
  feature: makeFeatureCommand,
1488
+ inbox: makeInboxCommand,
1286
1489
  job: simpleMakeCommand(
1287
1490
  "makeJob",
1288
1491
  "Generate a feature job.",
@@ -1318,6 +1521,7 @@ const makeRoutes = buildRouteMap({
1318
1521
  "Generate a feature operational task.",
1319
1522
  makeTaskNextSteps,
1320
1523
  ),
1524
+ tenancy: makeTenancyCommand,
1321
1525
  test: simpleMakeCommand(
1322
1526
  "makeTest",
1323
1527
  "Generate a use case test.",
@@ -1344,6 +1548,7 @@ const rootRoutes = buildRouteMap({
1344
1548
  Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
1345
1549
  },
1346
1550
  routes: {
1551
+ check: checkCommand,
1347
1552
  completion: completionRoutes,
1348
1553
  create: createCommand,
1349
1554
  db: dbRoutes,
@@ -1357,6 +1562,10 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
1357
1562
  schedule: scheduleRoutes,
1358
1563
  task: taskRoutes,
1359
1564
  },
1565
+ aliases: {
1566
+ // The singular form is a common near-miss for the providers commands.
1567
+ provider: "providers",
1568
+ },
1360
1569
  });
1361
1570
 
1362
1571
  const cli = buildApplication(rootRoutes, {
@@ -1432,6 +1641,7 @@ type DatabaseSchemaGenerateNextStepsResult = {
1432
1641
  type AppTaskRunNextStepsResult = {
1433
1642
  name: string;
1434
1643
  durationMs: number;
1644
+ tenant?: string;
1435
1645
  output: unknown;
1436
1646
  };
1437
1647
 
@@ -1463,7 +1673,9 @@ function appTaskRunNextSteps(result: AppTaskRunNextStepsResult): string {
1463
1673
  ? "undefined"
1464
1674
  : JSON.stringify(result.output, null, 2);
1465
1675
 
1466
- return `Ran task ${result.name} in ${result.durationMs}ms
1676
+ const tenant = result.tenant ? `\nTenant: ${result.tenant}` : "";
1677
+
1678
+ return `Ran task ${result.name} in ${result.durationMs}ms${tenant}
1467
1679
 
1468
1680
  Output:
1469
1681
  ${output}`;
@@ -1545,19 +1757,13 @@ ${openStep}
1545
1757
 
1546
1758
  Inspect the app:
1547
1759
  ${cli} routes
1548
- ${run} lint
1549
- ${cli} lint
1550
- ${cli} doctor
1760
+ ${cli} check
1551
1761
 
1552
1762
  Generate a feature:
1553
1763
  ${cli} make feature projects
1554
1764
  ${cli} db generate
1555
1765
  ${cli} db migrate
1556
- ${run} test
1557
- ${run} lint
1558
- ${run} typecheck
1559
- ${cli} lint
1560
- ${cli} doctor`;
1766
+ ${cli} check`;
1561
1767
  }
1562
1768
 
1563
1769
  function packageRunnerFromPackageManager(pm: PackageManager): string {
@@ -1617,6 +1823,20 @@ Next steps:
1617
1823
  ${steps.map((step) => ` ${step}`).join("\n")}`;
1618
1824
  }
1619
1825
 
1826
+ function providerAddNextSteps(result: AddProviderPresetResult): string {
1827
+ const { changedFiles, skippedFiles } = changedFileLines(result);
1828
+ const prefix = result.dryRun ? "Would add" : "Added";
1829
+
1830
+ return `${prefix} ${result.name} provider preset in ${result.targetDir}
1831
+
1832
+ Changed files:
1833
+ ${changedFiles || " none"}
1834
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1835
+
1836
+ Next steps:
1837
+ ${result.nextSteps.map((step) => ` ${step}`).join("\n")}`;
1838
+ }
1839
+
1620
1840
  function makeResourceNextSteps(result: MakeNextStepsResult): string {
1621
1841
  return makeNextSteps(result, "resource", [
1622
1842
  "Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
@@ -1754,15 +1974,6 @@ function makeNotificationNextSteps(result: MakeNextStepsResult): string {
1754
1974
  ]);
1755
1975
  }
1756
1976
 
1757
- function makePaymentsNextSteps(result: MakeNextStepsResult): string {
1758
- return makeNextSteps(result, "billing slice", [
1759
- "Set BILLING_TEAM_PRICE_ID in .env.local to the provider price ID for your team plan.",
1760
- "Run beignet db generate and beignet db migrate so billing_accounts exists before handling webhooks.",
1761
- "Use createMemoryPaymentsProvider() for local development, then swap server/providers.ts to stripePaymentsProvider when STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are configured.",
1762
- "Run your app's test, lint, and typecheck commands, then beignet lint and beignet doctor.",
1763
- ]);
1764
- }
1765
-
1766
1977
  function makeOutboxNextSteps(result: MakeNextStepsResult): string {
1767
1978
  return makeNextSteps(result, "outbox runtime", [
1768
1979
  "Set CRON_SECRET in your deployment and call /api/cron/outbox/drain from a scheduler with Authorization: Bearer $CRON_SECRET.",
package/src/inspect.ts CHANGED
@@ -19,6 +19,10 @@ import {
19
19
  formatGithubAnnotation,
20
20
  type GithubAnnotationSeverity,
21
21
  } from "./github-annotations.js";
22
+ import {
23
+ providersFilePath,
24
+ wireListenersProviderSource,
25
+ } from "./make/shared.js";
22
26
  import {
23
27
  detectedProviderVariants,
24
28
  diagnosticPackageJsonFile,
@@ -269,6 +273,14 @@ export async function applyDoctorFixes(
269
273
  ...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
270
274
  );
271
275
 
276
+ const listenerFix = await fixUnregisteredListeners(
277
+ targetDir,
278
+ files,
279
+ drift,
280
+ config,
281
+ );
282
+ if (listenerFix) fixes.push(listenerFix);
283
+
272
284
  const openApiFix = await fixDirectOpenApiArrayDrift(
273
285
  targetDir,
274
286
  files,
@@ -986,6 +998,24 @@ function parseRouteExports(
986
998
  const imports = parseNamedImports(source, config);
987
999
  const exportRegex =
988
1000
  /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
1001
+ const apiRouteExportRegex =
1002
+ /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
1003
+
1004
+ for (const match of source.matchAll(apiRouteExportRegex)) {
1005
+ for (const member of match[1].split(",")) {
1006
+ const parts = member.split(":");
1007
+ const method = (parts[1] ?? parts[0])?.trim();
1008
+ if (!method || !isHttpMethod(method)) continue;
1009
+
1010
+ exports.push({
1011
+ method,
1012
+ handlerFile,
1013
+ contractRef: routePath,
1014
+ catchAllPrefix: catchAllRoutePrefix(routePath),
1015
+ source: "next-route",
1016
+ });
1017
+ }
1018
+ }
989
1019
 
990
1020
  for (const match of source.matchAll(exportRegex)) {
991
1021
  const method = match[1] as HttpMethod;
@@ -1384,7 +1414,7 @@ async function inspectDiagnostics(
1384
1414
  convention,
1385
1415
  contracts,
1386
1416
  )),
1387
- ...(await inspectServerlessFootguns(targetDir, files)),
1417
+ ...(await inspectServerlessFootguns(targetDir, files, config, convention)),
1388
1418
  ...(await inspectProductionReadiness(targetDir, files, config, convention)),
1389
1419
  ...(await inspectFeatureArtifactDrift(
1390
1420
  targetDir,
@@ -3773,6 +3803,8 @@ function isWorkflowTestFile(file: string): boolean {
3773
3803
  async function inspectServerlessFootguns(
3774
3804
  targetDir: string,
3775
3805
  files: string[],
3806
+ config: ResolvedBeignetConfig,
3807
+ convention: InspectConvention,
3776
3808
  ): Promise<InspectDiagnostic[]> {
3777
3809
  const diagnostics: InspectDiagnostic[] = [];
3778
3810
 
@@ -3792,6 +3824,19 @@ async function inspectServerlessFootguns(
3792
3824
  /(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
3793
3825
  const source = await readFile(path.join(targetDir, file), "utf8");
3794
3826
 
3827
+ if (
3828
+ convention.resourceGenerator &&
3829
+ isNextRouteFile(file, config) &&
3830
+ importsEagerServer(source, file, files, config)
3831
+ ) {
3832
+ diagnostics.push({
3833
+ severity: "warning",
3834
+ code: "BEIGNET_NEXT_ROUTE_EAGER_SERVER_IMPORT",
3835
+ file,
3836
+ message: `${file} imports the eager server singleton. Next.js imports route modules during production builds, so use createNextServerLoader(...) in ${config.paths.server} and pass getServer to createApiRoute(...), createHealthRoute(...), or the focused route helper instead.`,
3837
+ });
3838
+ }
3839
+
3795
3840
  if (
3796
3841
  isInfraOrServerFile &&
3797
3842
  isProviderFile &&
@@ -3850,6 +3895,37 @@ async function inspectServerlessFootguns(
3850
3895
  return diagnostics;
3851
3896
  }
3852
3897
 
3898
+ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
3899
+ const routesPath = directoryPath(config.paths.routes);
3900
+
3901
+ return (
3902
+ file.startsWith(`${routesPath}/`) &&
3903
+ file.endsWith("/route.ts") &&
3904
+ !file.endsWith(".test.ts")
3905
+ );
3906
+ }
3907
+
3908
+ function importsEagerServer(
3909
+ source: string,
3910
+ importerFile: string,
3911
+ files: string[],
3912
+ config: ResolvedBeignetConfig,
3913
+ ): boolean {
3914
+ const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
3915
+
3916
+ for (const match of source.matchAll(importRegex)) {
3917
+ const members = match[1]
3918
+ .split(",")
3919
+ .map((member) => parseImportMember(member));
3920
+ if (!members.some((member) => member?.importedName === "server")) continue;
3921
+
3922
+ const sourceFile = sourceFileFromImport(match[2], importerFile, files);
3923
+ if (sourceFile === config.paths.server) return true;
3924
+ }
3925
+
3926
+ return false;
3927
+ }
3928
+
3853
3929
  function hasOutboxRegistry(files: string[]): boolean {
3854
3930
  return files.some(isOutboxRegistryFile);
3855
3931
  }
@@ -4076,7 +4152,9 @@ async function inspectDatabaseLifecycleDrift(
4076
4152
  {
4077
4153
  script: "db:seed",
4078
4154
  command: "seed",
4079
- file: `${infrastructurePath}/db/seed.ts`,
4155
+ // The seed entrypoint lives beside the server runtime because it boots
4156
+ // the app through getServer(), which infra/ must not import.
4157
+ file: `${directoryPath(path.dirname(config.paths.server))}/seed.ts`,
4080
4158
  code: "BEIGNET_DB_SEED_ENTRYPOINT_MISSING",
4081
4159
  },
4082
4160
  {
@@ -5396,6 +5474,94 @@ async function fixUnregisteredTasks(
5396
5474
  });
5397
5475
  }
5398
5476
 
5477
+ /**
5478
+ * Wire fully unregistered feature listener registries into the providers
5479
+ * file with the same `<registry>Provider` block `beignet make listener`
5480
+ * generates, so both write paths stay byte-compatible.
5481
+ *
5482
+ * Bails without writing when the providers file or its exported array is
5483
+ * missing or customized, when the app has no eventBus port to register
5484
+ * against, or when the registry name is already imported from another file.
5485
+ * The diagnostic stays in every bail case.
5486
+ */
5487
+ async function fixUnregisteredListeners(
5488
+ targetDir: string,
5489
+ files: string[],
5490
+ drift: WorkflowRegistrationDrift,
5491
+ config: ResolvedBeignetConfig,
5492
+ ): Promise<InspectFix | undefined> {
5493
+ const candidates = drift.listeners.unregistered.filter(
5494
+ (entry) => entry.fullyUnregistered,
5495
+ );
5496
+ if (candidates.length === 0) return undefined;
5497
+
5498
+ const providersFile = providersFilePath(config);
5499
+ const contextFile = normalizePath(
5500
+ path.join(path.dirname(config.paths.server), "context.ts"),
5501
+ );
5502
+ const wiringDependencies = [
5503
+ providersFile,
5504
+ contextFile,
5505
+ config.paths.appContext,
5506
+ config.paths.ports,
5507
+ ];
5508
+ if (wiringDependencies.some((file) => !files.includes(file))) {
5509
+ return undefined;
5510
+ }
5511
+
5512
+ // The generated wiring registers listeners on ports.eventBus; without the
5513
+ // port the fix would produce code that cannot typecheck.
5514
+ const portsSource = await readFile(
5515
+ path.join(targetDir, config.paths.ports),
5516
+ "utf8",
5517
+ );
5518
+ if (!/\beventBus\s*[:?]/.test(portsSource)) return undefined;
5519
+
5520
+ const providersPath = path.join(targetDir, providersFile);
5521
+ const original = await readFile(providersPath, "utf8");
5522
+ let next = original;
5523
+ const registeredNames: string[] = [];
5524
+
5525
+ for (const { registry } of candidates) {
5526
+ const imported = parseNamedImportSources(next).get(registry.registryName);
5527
+ if (imported) {
5528
+ const importedFile = sourceFileFromImport(
5529
+ imported.sourcePath,
5530
+ providersFile,
5531
+ files,
5532
+ );
5533
+ if (importedFile !== registry.indexFile) return undefined;
5534
+ }
5535
+
5536
+ const result = wireListenersProviderSource(next, {
5537
+ registryName: registry.registryName,
5538
+ featureKebab: listenerFeatureKebab(registry.indexFile),
5539
+ listenerModule: `@/${modulePath(registry.indexFile)}`,
5540
+ config,
5541
+ });
5542
+ if (result.kind === "missing") return undefined;
5543
+ if (result.kind === "updated") {
5544
+ next = result.source;
5545
+ registeredNames.push(registry.registryName);
5546
+ }
5547
+ }
5548
+
5549
+ if (next === original || registeredNames.length === 0) return undefined;
5550
+
5551
+ await writeFile(providersPath, next);
5552
+ return {
5553
+ code: "BEIGNET_LISTENER_UNREGISTERED",
5554
+ file: providersFile,
5555
+ message: `Registered ${registeredNames.join(", ")} with registerListeners(...) in ${providersFile}.`,
5556
+ };
5557
+ }
5558
+
5559
+ function listenerFeatureKebab(indexFile: string): string {
5560
+ return path.posix.basename(
5561
+ path.posix.dirname(path.posix.dirname(normalizePath(indexFile))),
5562
+ );
5563
+ }
5564
+
5399
5565
  async function fixUnregisteredOutboxEntries(
5400
5566
  targetDir: string,
5401
5567
  files: string[],