@beignet/cli 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +70 -17
  3. package/dist/check.d.ts +2 -0
  4. package/dist/check.d.ts.map +1 -1
  5. package/dist/check.js +16 -0
  6. package/dist/check.js.map +1 -1
  7. package/dist/choices.d.ts +22 -2
  8. package/dist/choices.d.ts.map +1 -1
  9. package/dist/choices.js +62 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +2 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/create-prompts.d.ts +6 -2
  16. package/dist/create-prompts.d.ts.map +1 -1
  17. package/dist/create-prompts.js +1 -1
  18. package/dist/create-prompts.js.map +1 -1
  19. package/dist/db.d.ts +5 -5
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +5 -5
  22. package/dist/db.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +386 -27
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +5 -0
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +805 -62
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/make/inbox.d.ts.map +1 -1
  31. package/dist/make/inbox.js +5 -3
  32. package/dist/make/inbox.js.map +1 -1
  33. package/dist/make/payments.d.ts.map +1 -1
  34. package/dist/make/payments.js +58 -29
  35. package/dist/make/payments.js.map +1 -1
  36. package/dist/make/shared.d.ts +12 -3
  37. package/dist/make/shared.d.ts.map +1 -1
  38. package/dist/make/shared.js +52 -11
  39. package/dist/make/shared.js.map +1 -1
  40. package/dist/make/tenancy.d.ts.map +1 -1
  41. package/dist/make/tenancy.js +6 -4
  42. package/dist/make/tenancy.js.map +1 -1
  43. package/dist/make.d.ts.map +1 -1
  44. package/dist/make.js +136 -65
  45. package/dist/make.js.map +1 -1
  46. package/dist/outbox.d.ts +114 -1
  47. package/dist/outbox.d.ts.map +1 -1
  48. package/dist/outbox.js +190 -0
  49. package/dist/outbox.js.map +1 -1
  50. package/dist/preflight.d.ts +90 -0
  51. package/dist/preflight.d.ts.map +1 -0
  52. package/dist/preflight.js +423 -0
  53. package/dist/preflight.js.map +1 -0
  54. package/dist/provider-add.d.ts +10 -1
  55. package/dist/provider-add.d.ts.map +1 -1
  56. package/dist/provider-add.js +255 -16
  57. package/dist/provider-add.js.map +1 -1
  58. package/dist/task.d.ts.map +1 -1
  59. package/dist/task.js +2 -3
  60. package/dist/task.js.map +1 -1
  61. package/dist/templates/agents.d.ts.map +1 -1
  62. package/dist/templates/agents.js +6 -3
  63. package/dist/templates/agents.js.map +1 -1
  64. package/dist/templates/base.d.ts.map +1 -1
  65. package/dist/templates/base.js +5 -11
  66. package/dist/templates/base.js.map +1 -1
  67. package/dist/templates/index.d.ts.map +1 -1
  68. package/dist/templates/index.js +2 -0
  69. package/dist/templates/index.js.map +1 -1
  70. package/dist/templates/server.d.ts.map +1 -1
  71. package/dist/templates/server.js +10 -5
  72. package/dist/templates/server.js.map +1 -1
  73. package/dist/templates/shared.d.ts +5 -1
  74. package/dist/templates/shared.d.ts.map +1 -1
  75. package/dist/templates/shared.js +13 -1
  76. package/dist/templates/shared.js.map +1 -1
  77. package/dist/templates/testing.d.ts +5 -0
  78. package/dist/templates/testing.d.ts.map +1 -0
  79. package/dist/templates/testing.js +542 -0
  80. package/dist/templates/testing.js.map +1 -0
  81. package/dist/templates/todos.js +2 -2
  82. package/package.json +3 -2
  83. package/skills/app-structure/SKILL.md +12 -4
  84. package/src/check.ts +23 -0
  85. package/src/choices.ts +84 -0
  86. package/src/config.ts +4 -0
  87. package/src/create-prompts.ts +8 -2
  88. package/src/db.ts +12 -12
  89. package/src/index.ts +592 -37
  90. package/src/inspect.ts +1253 -156
  91. package/src/make/inbox.ts +5 -3
  92. package/src/make/payments.ts +58 -29
  93. package/src/make/shared.ts +87 -13
  94. package/src/make/tenancy.ts +6 -4
  95. package/src/make.ts +179 -65
  96. package/src/outbox.ts +363 -0
  97. package/src/preflight.ts +596 -0
  98. package/src/provider-add.ts +270 -16
  99. package/src/task.ts +2 -3
  100. package/src/templates/agents.ts +6 -3
  101. package/src/templates/base.ts +5 -13
  102. package/src/templates/index.ts +2 -0
  103. package/src/templates/server.ts +10 -5
  104. package/src/templates/shared.ts +15 -1
  105. package/src/templates/testing.ts +545 -0
  106. package/src/templates/todos.ts +2 -2
  107. package/src/test-helpers/generated-app.ts +10 -6
package/src/make/inbox.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  resolveConfig,
8
8
  } from "../config.js";
9
9
  import { addNamedValueImport, appendToNamedArray } from "../registry-edits.js";
10
+ import { testSupportTemplateFiles } from "../templates/testing.js";
10
11
  import {
11
12
  aliasModule,
12
13
  appendSharedErrorEntries,
@@ -160,7 +161,7 @@ export async function makeInbox(
160
161
  const files = generatedFiles.map((file) => file.path);
161
162
  const dryRun = Boolean(options.dryRun);
162
163
  const inboxSeedNames = seedNames("inbox/demo-inbox");
163
- const seedScript = { "db:seed": `bun ${seedEntrypointPath(config)}` };
164
+ const seedScript = { "db:seed": `tsx ${seedEntrypointPath(config)}` };
164
165
 
165
166
  const recordSeedEntrypoint = (result: "created" | "updated" | "skipped") => {
166
167
  const entrypoint = seedEntrypointPath(config);
@@ -399,6 +400,7 @@ export function inboxFiles(
399
400
  path.join(config.paths.features, "inbox", file);
400
401
 
401
402
  const files: GeneratedFile[] = [
403
+ ...testSupportTemplateFiles,
402
404
  { path: featureDir("schemas.ts"), content: inboxSchemasFile() },
403
405
  { path: featureDir("ports.ts"), content: inboxPortsFile() },
404
406
  { path: featureDir("channel.ts"), content: inboxChannelFile(config) },
@@ -1109,7 +1111,7 @@ export function createMemoryInboxRepository(
1109
1111
  }
1110
1112
 
1111
1113
  export function inboxTestFile(config: ResolvedBeignetConfig): string {
1112
- return `import { describe, expect, it } from "bun:test";
1114
+ return `import { describe, expect, it } from "@/lib/beignet-test";
1113
1115
  import { createUseCaseTester } from "@beignet/core/application";
1114
1116
  import { createAnonymousActor } from "@beignet/core/ports";
1115
1117
  import { createTestUserActor } from "@beignet/core/ports/testing";
@@ -1292,7 +1294,7 @@ export function inboxChannelTestFile(config: ResolvedBeignetConfig): string {
1292
1294
  path.join(config.paths.features, "inbox/channel.ts"),
1293
1295
  );
1294
1296
 
1295
- return `import { describe, expect, it } from "bun:test";
1297
+ return `import { describe, expect, it } from "@/lib/beignet-test";
1296
1298
  import { createTestSystemActor } from "@beignet/core/ports/testing";
1297
1299
  import {
1298
1300
  \tcreateTestContextFactory,
@@ -7,6 +7,7 @@ import {
7
7
  resolveConfig,
8
8
  } from "../config.js";
9
9
  import { appendToNamedArray, insertAfterImports } from "../registry-edits.js";
10
+ import { testSupportTemplateFiles } from "../templates/testing.js";
10
11
  import {
11
12
  addNamedTypeImport,
12
13
  aliasModule,
@@ -200,9 +201,11 @@ export function makePaymentsNextStepLines(
200
201
  'Gate paid actions once usage passes FREE_PLAN_LIMITS: await requireEntitlement(ctx, { entitlement: "todos.create", subject: { type: "tenant", id: tenantId } });',
201
202
  ...(hasPlanUi
202
203
  ? [
203
- "Users manage their plan at /settings/plan; run bun run db:seed to create the demo billing account.",
204
+ "Users manage their plan at /settings/plan; run your db:seed package script to create the demo billing account.",
204
205
  ]
205
- : ["Run bun run db:seed to create the demo billing account."]),
206
+ : [
207
+ "Run your db:seed package script to create the demo billing account.",
208
+ ]),
206
209
  "Use createMemoryPaymentsProvider() for local development, then swap server/providers.ts to stripePaymentsProvider when STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are configured.",
207
210
  "Billing accounts fall back to the user id today; after beignet make tenancy they become workspace-scoped automatically.",
208
211
  "Run your app's test, lint, and typecheck commands, then beignet lint and beignet doctor.",
@@ -223,7 +226,7 @@ export async function updatePaymentsWiring(
223
226
  if (
224
227
  await updatePackageScripts(
225
228
  targetDir,
226
- { "db:seed": `bun ${seedEntrypointPath(config)}` },
229
+ { "db:seed": `tsx ${seedEntrypointPath(config)}` },
227
230
  { dryRun: options.dryRun },
228
231
  )
229
232
  ) {
@@ -590,6 +593,7 @@ export function paymentsFiles(
590
593
  : [];
591
594
 
592
595
  return [
596
+ ...testSupportTemplateFiles,
593
597
  {
594
598
  path: path.join(config.paths.features, "billing/schemas.ts"),
595
599
  content: billingSchemasFile(),
@@ -755,6 +759,8 @@ export function billingEntitlementsFile(): string {
755
759
  \tcreateEntitlements,
756
760
  \tdenyEntitlement,
757
761
  } from "@beignet/core/entitlements";
762
+ import { createTenant } from "@beignet/core/ports";
763
+ import { createTenantScope } from "@beignet/core/tenancy";
758
764
  import { isBillingAccountActive, type BillingRepository } from "./ports";
759
765
  import { billingPlanIncludesEntitlement } from "./pricing";
760
766
  import type { BillingPlan } from "./schemas";
@@ -777,7 +783,9 @@ export function createBillingEntitlements(billing: BillingRepository) {
777
783
  \t\t\t\t});
778
784
  \t\t\t}
779
785
 
780
- \t\t\tconst account = await billing.findByTenantId(input.subject.id);
786
+ \t\t\tconst account = await billing.findByTenantScope(
787
+ \t\t\t\tcreateTenantScope(createTenant(input.subject.id)),
788
+ \t\t\t);
781
789
  \t\t\tconst plan: BillingPlan = isBillingAccountActive(account)
782
790
  \t\t\t\t? account.plan
783
791
  \t\t\t\t: "free";
@@ -803,7 +811,8 @@ export function createBillingEntitlements(billing: BillingRepository) {
803
811
  }
804
812
 
805
813
  export function billingPortsFile(): string {
806
- return `import type { BillingPlan, BillingStatus } from "./schemas";
814
+ return `import type { TenantScope } from "@beignet/core/tenancy";
815
+ import type { BillingPlan, BillingStatus } from "./schemas";
807
816
 
808
817
  export type BillingAccount = {
809
818
  \ttenantId: string;
@@ -822,7 +831,7 @@ export type BillingAccount = {
822
831
  export type SaveBillingAccountInput = BillingAccount;
823
832
 
824
833
  export interface BillingRepository {
825
- \tfindByTenantId(tenantId: string): Promise<BillingAccount | null>;
834
+ \tfindByTenantScope(scope: TenantScope): Promise<BillingAccount | null>;
826
835
  \tfindByCustomerId(
827
836
  \t\tcustomerId: string,
828
837
  \t\tprovider: string,
@@ -917,7 +926,12 @@ export function billingUseCasesFile(config: ResolvedBeignetConfig): string {
917
926
  \trunIdempotently,
918
927
  } from "@beignet/core/idempotency";
919
928
  import type { PaymentWebhookEvent } from "@beignet/core/payments";
920
- import { requireUser } from "@beignet/core/ports";
929
+ import { createTenant, requireUser } from "@beignet/core/ports";
930
+ import {
931
+ createTenantScope,
932
+ type TenantScope,
933
+ tenantScopeId,
934
+ } from "@beignet/core/tenancy";
921
935
  import { z } from "zod";
922
936
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
923
937
  import { appError } from "@/features/shared/errors";
@@ -1029,9 +1043,9 @@ function cancelAtPeriodEndFromPayload(
1029
1043
  \t\t: undefined;
1030
1044
  }
1031
1045
 
1032
- function billingTenantId(ctx: AppContext): string {
1046
+ function billingTenantScope(ctx: AppContext): TenantScope {
1033
1047
  \tconst user = requireUser(ctx);
1034
- \treturn ctx.tenant?.id ?? user.id;
1048
+ \treturn createTenantScope(createTenant(ctx.tenant?.id ?? user.id));
1035
1049
  }
1036
1050
 
1037
1051
  function billingStatusOutput(tenantId: string, account: BillingAccount | null) {
@@ -1100,13 +1114,15 @@ async function saveBillingAccount(args: {
1100
1114
  async function applyCheckoutCompleted(
1101
1115
  \tevent: PaymentWebhookEvent,
1102
1116
  \tsave: (account: BillingAccount) => Promise<BillingAccount>,
1103
- \tfindByTenantId: (tenantId: string) => Promise<BillingAccount | null>,
1117
+ \tfindByTenantScope: (scope: TenantScope) => Promise<BillingAccount | null>,
1104
1118
  ) {
1105
1119
  \tconst data = record(event.data);
1106
1120
  \tconst tenantId = tenantIdFromPayload(data);
1107
1121
  \tif (!tenantId) return null;
1108
1122
 
1109
- \tconst existing = await findByTenantId(tenantId);
1123
+ \tconst existing = await findByTenantScope(
1124
+ \t\tcreateTenantScope(createTenant(tenantId)),
1125
+ \t);
1110
1126
  \treturn saveBillingAccount({
1111
1127
  \t\texisting,
1112
1128
  \t\ttenantId,
@@ -1128,13 +1144,13 @@ async function applySubscriptionEvent(
1128
1144
  \t\tcustomerId: string,
1129
1145
  \t\tprovider: string,
1130
1146
  \t) => Promise<BillingAccount | null>,
1131
- \tfindByTenantId: (tenantId: string) => Promise<BillingAccount | null>,
1147
+ \tfindByTenantScope: (scope: TenantScope) => Promise<BillingAccount | null>,
1132
1148
  ) {
1133
1149
  \tconst data = record(event.data);
1134
1150
  \tconst customerId = providerId(data.customer);
1135
1151
  \tconst explicitTenantId = tenantIdFromPayload(data);
1136
1152
  \tconst existing = explicitTenantId
1137
- \t\t? await findByTenantId(explicitTenantId)
1153
+ \t\t? await findByTenantScope(createTenantScope(createTenant(explicitTenantId)))
1138
1154
  \t\t: customerId
1139
1155
  \t\t\t? await findByCustomerId(customerId, event.provider)
1140
1156
  \t\t\t: null;
@@ -1224,8 +1240,9 @@ export const getBillingStatusUseCase = useCase
1224
1240
  \t.input(z.object({}))
1225
1241
  \t.output(billingStatusOutputSchema)
1226
1242
  \t.run(async ({ ctx }) => {
1227
- \t\tconst tenantId = billingTenantId(ctx);
1228
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
1243
+ \t\tconst scope = billingTenantScope(ctx);
1244
+ \t\tconst tenantId = tenantScopeId(scope);
1245
+ \t\tconst account = await ctx.ports.billing.findByTenantScope(scope);
1229
1246
  \t\treturn billingStatusOutput(tenantId, account);
1230
1247
  \t});
1231
1248
 
@@ -1234,8 +1251,9 @@ export const createCheckoutSessionUseCase = useCase
1234
1251
  \t.input(createCheckoutSessionInputSchema)
1235
1252
  \t.output(createCheckoutSessionOutputSchema)
1236
1253
  \t.run(async ({ ctx, input }) => {
1237
- \t\tconst tenantId = billingTenantId(ctx);
1238
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
1254
+ \t\tconst scope = billingTenantScope(ctx);
1255
+ \t\tconst tenantId = tenantScopeId(scope);
1256
+ \t\tconst account = await ctx.ports.billing.findByTenantScope(scope);
1239
1257
  \t\tconst plan = getBillingPlan(input.plan);
1240
1258
  \t\tconst session = await ctx.ports.payments.createCheckoutSession({
1241
1259
  \t\t\tmode: plan.mode,
@@ -1281,8 +1299,9 @@ export const createBillingPortalSessionUseCase = useCase
1281
1299
  \t.input(z.object({}))
1282
1300
  \t.output(createBillingPortalSessionOutputSchema)
1283
1301
  \t.run(async ({ ctx }) => {
1284
- \t\tconst tenantId = billingTenantId(ctx);
1285
- \t\tconst account = await ctx.ports.billing.findByTenantId(tenantId);
1302
+ \t\tconst scope = billingTenantScope(ctx);
1303
+ \t\tconst tenantId = tenantScopeId(scope);
1304
+ \t\tconst account = await ctx.ports.billing.findByTenantScope(scope);
1286
1305
 
1287
1306
  \t\tif (!account?.customerId) {
1288
1307
  \t\t\tthrow appError("BillingAccountNotFound", {
@@ -1322,14 +1341,14 @@ export const handlePaymentWebhookUseCase = useCase
1322
1341
  \t\t\t\t\t\taccount = await applyCheckoutCompleted(
1323
1342
  \t\t\t\t\t\t\tinput,
1324
1343
  \t\t\t\t\t\t\ttx.billing.save,
1325
- \t\t\t\t\t\t\ttx.billing.findByTenantId,
1344
+ \t\t\t\t\t\t\ttx.billing.findByTenantScope,
1326
1345
  \t\t\t\t\t\t);
1327
1346
  \t\t\t\t\t} else if (input.type.startsWith("customer.subscription.")) {
1328
1347
  \t\t\t\t\t\taccount = await applySubscriptionEvent(
1329
1348
  \t\t\t\t\t\t\tinput,
1330
1349
  \t\t\t\t\t\t\ttx.billing.save,
1331
1350
  \t\t\t\t\t\t\ttx.billing.findByCustomerId,
1332
- \t\t\t\t\t\t\ttx.billing.findByTenantId,
1351
+ \t\t\t\t\t\t\ttx.billing.findByTenantScope,
1333
1352
  \t\t\t\t\t\t);
1334
1353
  \t\t\t\t\t} else if (input.type === "invoice.payment_succeeded") {
1335
1354
  \t\t\t\t\t\taccount = await applyInvoiceSucceeded(
@@ -1384,11 +1403,12 @@ export const billingRoutes = defineRouteGroup<AppContext>()({
1384
1403
  }
1385
1404
 
1386
1405
  export function billingTestFile(config: ResolvedBeignetConfig): string {
1387
- return `import { describe, expect, it } from "bun:test";
1406
+ return `import { describe, expect, it } from "@/lib/beignet-test";
1388
1407
  import { createUseCaseTester } from "@beignet/core/application";
1389
1408
  import { requireEntitlement } from "@beignet/core/entitlements";
1390
- import { createAnonymousActor } from "@beignet/core/ports";
1409
+ import { createAnonymousActor, createTenant } from "@beignet/core/ports";
1391
1410
  import { createTestUserActor } from "@beignet/core/ports/testing";
1411
+ import { createTenantScope, tenantScopeId } from "@beignet/core/tenancy";
1392
1412
  import {
1393
1413
  \tcreateTestContextFactory,
1394
1414
  \tcreateTestPorts,
@@ -1409,7 +1429,8 @@ function createMemoryBillingRepository(): BillingRepository {
1409
1429
  \tconst accounts = new Map<string, BillingAccount>();
1410
1430
 
1411
1431
  \treturn {
1412
- \t\tasync findByTenantId(tenantId) {
1432
+ \t\tasync findByTenantScope(scope) {
1433
+ \t\t\tconst tenantId = tenantScopeId(scope);
1413
1434
  \t\t\treturn accounts.get(tenantId) ?? null;
1414
1435
  \t\t},
1415
1436
 
@@ -1498,7 +1519,9 @@ describe("billing use cases", () => {
1498
1519
  \t\t\tharness.payments.checkoutSessions[0]?.input.idempotencyKey,
1499
1520
  \t\t).toBe(undefined);
1500
1521
  \t\tawait expect(
1501
- \t\t\tharness.billing.findByTenantId("tenant_example"),
1522
+ \t\t\tharness.billing.findByTenantScope(
1523
+ \t\t\t\tcreateTenantScope(createTenant("tenant_example")),
1524
+ \t\t\t),
1502
1525
  \t\t).resolves.toMatchObject({
1503
1526
  \t\t\tcheckoutSessionId: checkout.sessionId,
1504
1527
  \t\t\tplan: "pro",
@@ -1690,7 +1713,9 @@ describe("billing use cases", () => {
1690
1713
  \t\t\t}),
1691
1714
  \t\t).resolves.toEqual({ received: true, accountUpdated: true });
1692
1715
  \t\tawait expect(
1693
- \t\t\tharness.billing.findByTenantId("tenant_example"),
1716
+ \t\t\tharness.billing.findByTenantScope(
1717
+ \t\t\t\tcreateTenantScope(createTenant("tenant_example")),
1718
+ \t\t\t),
1694
1719
  \t\t).resolves.toMatchObject({
1695
1720
  \t\t\tstatus: "past_due",
1696
1721
  \t\t\tlastEventId: "evt_invoice_failed",
@@ -1722,7 +1747,9 @@ describe("billing use cases", () => {
1722
1747
  \t\t\t}),
1723
1748
  \t\t).resolves.toEqual({ received: true, accountUpdated: true });
1724
1749
  \t\tawait expect(
1725
- \t\t\tharness.billing.findByTenantId("tenant_example"),
1750
+ \t\t\tharness.billing.findByTenantScope(
1751
+ \t\t\t\tcreateTenantScope(createTenant("tenant_example")),
1752
+ \t\t\t),
1726
1753
  \t\t).resolves.toMatchObject({
1727
1754
  \t\t\tstatus: "active",
1728
1755
  \t\t\tcancelAtPeriodEnd: true,
@@ -1814,7 +1841,8 @@ export function billingDrizzleRepositoryFile(
1814
1841
 
1815
1842
  \t\t\treturn toBillingAccount(row);`;
1816
1843
 
1817
- return `import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
1844
+ return `import { tenantScopeId } from "@beignet/core/tenancy";
1845
+ import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
1818
1846
  import { and, eq } from "drizzle-orm";
1819
1847
  import type {
1820
1848
  \tBillingAccount,
@@ -1870,7 +1898,8 @@ export function createDrizzleBillingRepository(
1870
1898
  \tdb: ${dialect.dbTypeName}<typeof schema>,
1871
1899
  ): BillingRepository {
1872
1900
  \treturn {
1873
- \t\tasync findByTenantId(tenantId) {
1901
+ \t\tasync findByTenantScope(scope) {
1902
+ \t\t\tconst tenantId = tenantScopeId(scope);
1874
1903
  \t\t\tconst [row] = await db
1875
1904
  \t\t\t\t.select()
1876
1905
  \t\t\t\t.from(schema.billingAccounts)
@@ -15,6 +15,11 @@ import {
15
15
  insertAfterImports,
16
16
  matchingDelimiterIndex,
17
17
  } from "../registry-edits.js";
18
+ import {
19
+ currentGeneratedPackageScripts,
20
+ externalVersions,
21
+ legacyGeneratedPackageScripts,
22
+ } from "../templates/shared.js";
18
23
 
19
24
  export type MakeResult = {
20
25
  schemaVersion: 1;
@@ -350,10 +355,43 @@ export function providersFilePath(config: ResolvedBeignetConfig): string {
350
355
  return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
351
356
  }
352
357
 
358
+ export function listenersFilePath(config: ResolvedBeignetConfig): string {
359
+ return (
360
+ config.paths.listeners ??
361
+ `${directoryPath(path.dirname(config.paths.server))}/listeners.ts`
362
+ );
363
+ }
364
+
365
+ export function createListenersRegistrySource(options: {
366
+ registryName: string;
367
+ listenerModule: string;
368
+ }): string {
369
+ return `import { ${options.registryName} } from "${options.listenerModule}";
370
+
371
+ export const listeners = [...${options.registryName}] as const;
372
+ `;
373
+ }
374
+
375
+ export function updateListenersRegistrySource(
376
+ source: string,
377
+ options: {
378
+ registryName: string;
379
+ listenerModule: string;
380
+ },
381
+ ): AppendResult {
382
+ const importLine = `import { ${options.registryName} } from "${options.listenerModule}";`;
383
+ return appendToNamedArray(
384
+ source,
385
+ "listeners",
386
+ `...${options.registryName}`,
387
+ importLine,
388
+ );
389
+ }
390
+
353
391
  /**
354
392
  * Wire a feature listener registry into the generated providers file by
355
- * adding a `<registry>Provider` lifecycle provider whose setup calls
356
- * `registerListeners(...)` and registering it in the providers array.
393
+ * registering the central `server/listeners.ts` registry with
394
+ * `registerListeners(...)`.
357
395
  *
358
396
  * Shared by `beignet make listener` and `beignet doctor --fix` so both write
359
397
  * paths produce byte-identical wiring. Returns `missing` when the exported
@@ -369,8 +407,8 @@ export function wireListenersProviderSource(
369
407
  config: ResolvedBeignetConfig;
370
408
  },
371
409
  ): AppendResult {
372
- const { registryName, featureKebab, listenerModule, config } = options;
373
- const providerName = `${registryName}Provider`;
410
+ const { config } = options;
411
+ const providerName = "listenersProvider";
374
412
  let next = source;
375
413
  next = addNamedImport(next, "registerListeners", "@beignet/core/events");
376
414
  next = addNamedImport(next, "createServiceActor", "@beignet/core/ports");
@@ -387,7 +425,10 @@ export function wireListenersProviderSource(
387
425
  );
388
426
  next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
389
427
 
390
- const listenerImport = `import { ${registryName} } from "${listenerModule}";`;
428
+ const listenerImport = `import { listeners } from "${relativeModule(
429
+ providersFilePath(config),
430
+ listenersFilePath(config),
431
+ )}";`;
391
432
  if (!next.includes(listenerImport)) {
392
433
  next = insertAfterImports(next, listenerImport);
393
434
  }
@@ -398,9 +439,9 @@ export function wireListenersProviderSource(
398
439
  \tAppContext,
399
440
  \tAppServiceContextInput
400
441
  >()({
401
- \tname: "${featureKebab}-listeners",
442
+ \tname: "app-listeners",
402
443
  \tsetup({ ports, createServiceContext }) {
403
- \t\tconst unregister = registerListeners(ports.eventBus, ${registryName}, {
444
+ \t\tconst unregister = registerListeners(ports.eventBus, listeners, {
404
445
  \t\t\tctx: () =>
405
446
  \t\t\t\tcreateServiceContext({
406
447
  \t\t\t\t\tactor: createServiceActor("beignet-listener"),
@@ -913,7 +954,8 @@ export type CapabilityBuilder =
913
954
  | "jobs"
914
955
  | "schedules"
915
956
  | "notifications"
916
- | "tasks";
957
+ | "tasks"
958
+ | "uploads";
917
959
 
918
960
  export const capabilityBuilders: Record<
919
961
  CapabilityBuilder,
@@ -944,6 +986,11 @@ export const capabilityBuilders: Record<
944
986
  defineName: "defineTask",
945
987
  module: "@beignet/core/tasks",
946
988
  },
989
+ uploads: {
990
+ factoryName: "createUploads",
991
+ defineName: "defineUpload",
992
+ module: "@beignet/core/uploads",
993
+ },
947
994
  };
948
995
 
949
996
  export function capabilityBuilderPath(
@@ -954,7 +1001,8 @@ export function capabilityBuilderPath(
954
1001
  if (capability === "jobs") return config.paths.jobsBuilder;
955
1002
  if (capability === "schedules") return config.paths.schedulesBuilder;
956
1003
  if (capability === "notifications") return config.paths.notificationsBuilder;
957
- return config.paths.tasksBuilder;
1004
+ if (capability === "tasks") return config.paths.tasksBuilder;
1005
+ return config.paths.uploadsBuilder;
958
1006
  }
959
1007
 
960
1008
  export function capabilityBuilderFile(
@@ -1020,9 +1068,22 @@ export async function updatePackageScripts(
1020
1068
  const original = await readFile(filePath, "utf8");
1021
1069
  const packageJson = JSON.parse(original) as PackageJsonLike;
1022
1070
  packageJson.scripts = packageJson.scripts ?? {};
1071
+ let needsTsx = false;
1023
1072
 
1024
1073
  for (const [name, command] of Object.entries(scripts)) {
1025
- packageJson.scripts[name] ??= command;
1074
+ if (
1075
+ !packageJson.scripts[name] ||
1076
+ packageJson.scripts[name] === legacyGeneratedPackageScripts[name]
1077
+ ) {
1078
+ packageJson.scripts[name] = command;
1079
+ }
1080
+ needsTsx ||=
1081
+ command.startsWith("tsx ") && packageJson.scripts[name] === command;
1082
+ }
1083
+
1084
+ if (needsTsx) {
1085
+ packageJson.devDependencies = packageJson.devDependencies ?? {};
1086
+ packageJson.devDependencies.tsx ??= externalVersions.tsx;
1026
1087
  }
1027
1088
 
1028
1089
  const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
@@ -1057,11 +1118,11 @@ export async function updatePackageJson(
1057
1118
  devDependencies?: Record<string, string>;
1058
1119
  };
1059
1120
  packageJson.scripts = packageJson.scripts ?? {};
1060
-
1061
- packageJson.scripts.test ??= "bun test";
1121
+ updateGeneratedPackageScripts(packageJson.scripts);
1122
+ packageJson.devDependencies = packageJson.devDependencies ?? {};
1123
+ packageJson.devDependencies.tsx ??= externalVersions.tsx;
1062
1124
 
1063
1125
  if (options.webTestingDependency) {
1064
- packageJson.devDependencies = packageJson.devDependencies ?? {};
1065
1126
  packageJson.devDependencies["@beignet/web"] ??=
1066
1127
  packageJson.dependencies?.["@beignet/core"] ??
1067
1128
  packageJson.dependencies?.["@beignet/next"] ??
@@ -1073,6 +1134,19 @@ export async function updatePackageJson(
1073
1134
  return next !== original;
1074
1135
  }
1075
1136
 
1137
+ function updateGeneratedPackageScripts(scripts: Record<string, string>): void {
1138
+ for (const [name, command] of Object.entries(
1139
+ currentGeneratedPackageScripts,
1140
+ )) {
1141
+ if (
1142
+ scripts[name] === legacyGeneratedPackageScripts[name] ||
1143
+ (name === "test" && !scripts[name])
1144
+ ) {
1145
+ scripts[name] = command;
1146
+ }
1147
+ }
1148
+ }
1149
+
1076
1150
  export async function updatePortsIndex(
1077
1151
  targetDir: string,
1078
1152
  names: ResourceNames,
@@ -11,6 +11,7 @@ import {
11
11
  arrayInitializerInfo,
12
12
  } from "../registry-edits.js";
13
13
  import { serverFiles } from "../templates/server.js";
14
+ import { testSupportTemplateFiles } from "../templates/testing.js";
14
15
  import {
15
16
  addNamedImport,
16
17
  addNamedTypeImport,
@@ -428,7 +429,7 @@ async function registerTenancySeeds(
428
429
  if (
429
430
  await updatePackageScripts(
430
431
  targetDir,
431
- { "db:seed": `bun ${seedEntrypointPath(config)}` },
432
+ { "db:seed": `tsx ${seedEntrypointPath(config)}` },
432
433
  options,
433
434
  )
434
435
  ) {
@@ -741,6 +742,7 @@ export function tenancyFiles(
741
742
  path.join(config.paths.features, "workspaces", file);
742
743
 
743
744
  return [
745
+ ...testSupportTemplateFiles,
744
746
  { path: featureDir("schemas.ts"), content: tenancySchemasFile() },
745
747
  { path: featureDir("contracts.ts"), content: tenancyContractsFile() },
746
748
  { path: featureDir("policy.ts"), content: tenancyPolicyFile() },
@@ -2621,7 +2623,7 @@ export const workspaceInviteFactory = createFactory<
2621
2623
  export function membershipTestFile(config: ResolvedBeignetConfig): string {
2622
2624
  const tenantModule = aliasModule(tenantHelperPath(config));
2623
2625
 
2624
- return `import { describe, expect, test } from "bun:test";
2626
+ return `import { describe, expect, test } from "@/lib/beignet-test";
2625
2627
  import type { AuthSession } from "@/ports/auth";
2626
2628
  import {
2627
2629
  \tcreateWorkspaceUseCase,
@@ -2920,7 +2922,7 @@ describe("member management", () => {
2920
2922
  }
2921
2923
 
2922
2924
  export function invitesTestFile(): string {
2923
- return `import { describe, expect, test } from "bun:test";
2925
+ return `import { describe, expect, test } from "@/lib/beignet-test";
2924
2926
  import { createInvite } from "@/features/workspaces/contracts";
2925
2927
  import {
2926
2928
  \tacceptInviteUseCase,
@@ -3131,7 +3133,7 @@ describe("revokeInviteUseCase", () => {
3131
3133
  }
3132
3134
 
3133
3135
  export function policyTestFile(): string {
3134
- return `import { describe, expect, it } from "bun:test";
3136
+ return `import { describe, expect, it } from "@/lib/beignet-test";
3135
3137
  import { createPolicyTester } from "@beignet/core/ports/testing";
3136
3138
  import { workspacePolicy } from "@/features/workspaces/policy";
3137
3139
  import { workspaceFactory } from "./factories/workspace";