@beignet/cli 0.0.14 → 0.0.16

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 (49) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +30 -22
  3. package/dist/choices.d.ts +1 -1
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +8 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +29 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/inspect.js +12 -3
  11. package/dist/inspect.js.map +1 -1
  12. package/dist/make.d.ts +14 -0
  13. package/dist/make.d.ts.map +1 -1
  14. package/dist/make.js +622 -17
  15. package/dist/make.js.map +1 -1
  16. package/dist/mcp.d.ts.map +1 -1
  17. package/dist/mcp.js +12 -4
  18. package/dist/mcp.js.map +1 -1
  19. package/dist/templates/agents.d.ts +2 -2
  20. package/dist/templates/agents.d.ts.map +1 -1
  21. package/dist/templates/agents.js +18 -15
  22. package/dist/templates/agents.js.map +1 -1
  23. package/dist/templates/base.js +1 -1
  24. package/dist/templates/base.js.map +1 -1
  25. package/dist/templates/server.d.ts.map +1 -1
  26. package/dist/templates/server.js +13 -6
  27. package/dist/templates/server.js.map +1 -1
  28. package/dist/templates/shared.js +3 -3
  29. package/dist/templates/shared.js.map +1 -1
  30. package/dist/templates/shell.d.ts.map +1 -1
  31. package/dist/templates/shell.js +2 -2
  32. package/dist/templates/shell.js.map +1 -1
  33. package/dist/templates/todos.js +1 -1
  34. package/package.json +2 -2
  35. package/src/choices.ts +8 -0
  36. package/src/index.ts +35 -2
  37. package/src/inspect.ts +15 -3
  38. package/src/make.ts +876 -18
  39. package/src/mcp.ts +12 -3
  40. package/src/templates/agents.ts +18 -16
  41. package/src/templates/base.ts +3 -3
  42. package/src/templates/server.ts +13 -6
  43. package/src/templates/shared.ts +3 -3
  44. package/src/templates/shell.ts +2 -1
  45. package/src/templates/todos.ts +1 -1
  46. package/dist/templates/db.d.ts +0 -15
  47. package/dist/templates/db.d.ts.map +0 -1
  48. package/dist/templates/db.js +0 -1001
  49. package/dist/templates/db.js.map +0 -1
package/dist/make.js CHANGED
@@ -218,6 +218,13 @@ async function makeFeatureAddon(addon, feature, options) {
218
218
  if (addon === "policy") {
219
219
  return makePolicy({ ...shared, name: feature });
220
220
  }
221
+ if (addon === "factory") {
222
+ return makeFactory({
223
+ ...shared,
224
+ name: `${feature}/${singularize(feature)}`,
225
+ skipPersistenceAssert: true,
226
+ });
227
+ }
221
228
  if (addon === "task") {
222
229
  return makeTask({ ...shared, name: `${feature}/backfill` });
223
230
  }
@@ -227,9 +234,27 @@ async function makeFeatureAddon(addon, feature, options) {
227
234
  if (addon === "job") {
228
235
  return makeJob({ ...shared, name: `${feature}/process` });
229
236
  }
237
+ if (addon === "listener") {
238
+ return makeListener({
239
+ ...shared,
240
+ name: `${feature}/handle-created`,
241
+ event: `${feature}/created`,
242
+ skipEventAssert: true,
243
+ });
244
+ }
230
245
  if (addon === "notification") {
231
246
  return makeNotification({ ...shared, name: `${feature}/created` });
232
247
  }
248
+ if (addon === "schedule") {
249
+ return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
250
+ }
251
+ if (addon === "seed") {
252
+ return makeSeed({
253
+ ...shared,
254
+ name: `${feature}/demo-${feature}`,
255
+ skipPersistenceAssert: true,
256
+ });
257
+ }
233
258
  if (addon === "ui") {
234
259
  return makeFeatureUi({ ...shared, name: feature });
235
260
  }
@@ -238,16 +263,30 @@ async function makeFeatureAddon(addon, feature, options) {
238
263
  function normalizeMakeFeatureAddons(addons) {
239
264
  const normalized = new Set();
240
265
  for (const addon of addons) {
241
- if (addon === "events")
266
+ if (addon === "factories")
267
+ normalized.add("factory");
268
+ else if (addon === "events")
269
+ normalized.add("event");
270
+ else if (addon === "listeners") {
242
271
  normalized.add("event");
272
+ normalized.add("listener");
273
+ }
243
274
  else if (addon === "tasks")
244
275
  normalized.add("task");
245
276
  else if (addon === "jobs")
246
277
  normalized.add("job");
247
278
  else if (addon === "notifications")
248
279
  normalized.add("notification");
280
+ else if (addon === "schedules")
281
+ normalized.add("schedule");
282
+ else if (addon === "seeds")
283
+ normalized.add("seed");
249
284
  else if (addon === "uploads")
250
285
  normalized.add("upload");
286
+ else if (addon === "listener") {
287
+ normalized.add("event");
288
+ normalized.add("listener");
289
+ }
251
290
  else
252
291
  normalized.add(addon);
253
292
  }
@@ -255,10 +294,14 @@ function normalizeMakeFeatureAddons(addons) {
255
294
  }
256
295
  const featureAddonOrder = [
257
296
  "policy",
297
+ "factory",
258
298
  "task",
259
299
  "event",
260
300
  "job",
301
+ "listener",
261
302
  "notification",
303
+ "schedule",
304
+ "seed",
262
305
  "ui",
263
306
  "upload",
264
307
  ];
@@ -271,6 +314,21 @@ function mergeMakeResults(left, right) {
271
314
  skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
272
315
  };
273
316
  }
317
+ function mergeMakeResultInto(target, source) {
318
+ target.files = uniqueStrings([...target.files, ...source.files]);
319
+ target.createdFiles = uniqueStrings([
320
+ ...target.createdFiles,
321
+ ...source.createdFiles,
322
+ ]);
323
+ target.updatedFiles = uniqueStrings([
324
+ ...target.updatedFiles,
325
+ ...source.updatedFiles,
326
+ ]);
327
+ target.skippedFiles = uniqueStrings([
328
+ ...target.skippedFiles,
329
+ ...source.skippedFiles,
330
+ ]);
331
+ }
274
332
  function uniqueStrings(values) {
275
333
  return [...new Set(values)];
276
334
  }
@@ -522,6 +580,8 @@ export async function makeEvent(options) {
522
580
  ? resolveConfig(options.config)
523
581
  : await loadBeignetConfig(targetDir);
524
582
  await assertFeatureArtifactApp(targetDir, config, "event");
583
+ await assertServerRuntimeApp(targetDir, config, "event");
584
+ await updateOutboxPortWiring(targetDir, config, { dryRun: true });
525
585
  // Events are published through ctx.ports.eventBus, so the generator wires
526
586
  // the port and an in-memory dev provider when the app has no bus yet. Plan
527
587
  // the wiring before writing any files so an anchor miss aborts the
@@ -530,6 +590,9 @@ export async function makeEvent(options) {
530
590
  await wirePortProviders(targetDir, config, eventBusWirings, {
531
591
  dryRun: true,
532
592
  });
593
+ const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
594
+ force: Boolean(options.force),
595
+ });
533
596
  const result = await makeFeatureArtifact({
534
597
  targetDir,
535
598
  config,
@@ -545,6 +608,12 @@ export async function makeEvent(options) {
545
608
  await applyOutboxRegistryUpdate(result, targetDir, names, "events", config, {
546
609
  dryRun: Boolean(options.dryRun),
547
610
  });
611
+ await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
612
+ dryRun: Boolean(options.dryRun),
613
+ });
614
+ await applyOutboxPortWiring(result, targetDir, config, {
615
+ dryRun: Boolean(options.dryRun),
616
+ });
548
617
  await applyPortProviderWiring(result, targetDir, config, eventBusWirings, {
549
618
  dryRun: Boolean(options.dryRun),
550
619
  });
@@ -557,6 +626,11 @@ export async function makeJob(options) {
557
626
  ? resolveConfig(options.config)
558
627
  : await loadBeignetConfig(targetDir);
559
628
  await assertFeatureArtifactApp(targetDir, config, "job");
629
+ await assertServerRuntimeApp(targetDir, config, "job");
630
+ await updateOutboxPortWiring(targetDir, config, { dryRun: true });
631
+ const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
632
+ force: Boolean(options.force),
633
+ });
560
634
  const result = await makeFeatureArtifact({
561
635
  targetDir,
562
636
  config,
@@ -575,6 +649,12 @@ export async function makeJob(options) {
575
649
  await applyOutboxRegistryUpdate(result, targetDir, names, "jobs", config, {
576
650
  dryRun: Boolean(options.dryRun),
577
651
  });
652
+ await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
653
+ dryRun: Boolean(options.dryRun),
654
+ });
655
+ await applyOutboxPortWiring(result, targetDir, config, {
656
+ dryRun: Boolean(options.dryRun),
657
+ });
578
658
  return result;
579
659
  }
580
660
  export async function makeTask(options) {
@@ -619,7 +699,9 @@ export async function makeFactory(options) {
619
699
  const config = options.config
620
700
  ? resolveConfig(options.config)
621
701
  : await loadBeignetConfig(targetDir);
622
- await assertPersistenceArtifactApp(targetDir, names, config, "factory");
702
+ if (!options.skipPersistenceAssert) {
703
+ await assertPersistenceArtifactApp(targetDir, names, config, "factory");
704
+ }
623
705
  return makeFeatureArtifact({
624
706
  targetDir,
625
707
  config,
@@ -639,8 +721,10 @@ export async function makeSeed(options) {
639
721
  const config = options.config
640
722
  ? resolveConfig(options.config)
641
723
  : await loadBeignetConfig(targetDir);
642
- await assertPersistenceArtifactApp(targetDir, names, config, "seed");
643
- return makeFeatureArtifact({
724
+ if (!options.skipPersistenceAssert) {
725
+ await assertPersistenceArtifactApp(targetDir, names, config, "seed");
726
+ }
727
+ const result = await makeFeatureArtifact({
644
728
  targetDir,
645
729
  config,
646
730
  force: Boolean(options.force),
@@ -652,6 +736,38 @@ export async function makeSeed(options) {
652
736
  "@beignet/core": beignetDependencyVersion,
653
737
  },
654
738
  });
739
+ const factoryNamesForSeed = defaultFactoryNamesForSeed(names);
740
+ if (!(await fileExists(path.join(targetDir, factoryFilePath(factoryNamesForSeed, config))))) {
741
+ const factoryResult = await makeFeatureArtifact({
742
+ targetDir,
743
+ config,
744
+ force: Boolean(options.force),
745
+ dryRun: Boolean(options.dryRun),
746
+ name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
747
+ files: factoryFiles(factoryNamesForSeed, config),
748
+ index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
749
+ dependencies: {
750
+ "@beignet/core": beignetDependencyVersion,
751
+ },
752
+ });
753
+ mergeMakeResultInto(result, factoryResult);
754
+ }
755
+ const seedEntrypointResult = await updateSeedEntrypoint(targetDir, names, config, { dryRun: Boolean(options.dryRun) });
756
+ if (seedEntrypointResult === "created")
757
+ result.createdFiles.push(seedEntrypointPath(config));
758
+ if (seedEntrypointResult === "updated")
759
+ result.updatedFiles.push(seedEntrypointPath(config));
760
+ if (seedEntrypointResult === "skipped")
761
+ result.skippedFiles.push(seedEntrypointPath(config));
762
+ if (!result.files.includes(seedEntrypointPath(config))) {
763
+ result.files.push(seedEntrypointPath(config));
764
+ }
765
+ if (await updatePackageScripts(targetDir, { "db:seed": `bun ${seedEntrypointPath(config)}` }, { dryRun: Boolean(options.dryRun) })) {
766
+ result.updatedFiles.push("package.json");
767
+ if (!result.files.includes("package.json"))
768
+ result.files.push("package.json");
769
+ }
770
+ return result;
655
771
  }
656
772
  export async function makeNotification(options) {
657
773
  const targetDir = path.resolve(options.cwd ?? process.cwd());
@@ -696,7 +812,9 @@ export async function makeListener(options) {
696
812
  ? resolveConfig(options.config)
697
813
  : await loadBeignetConfig(targetDir);
698
814
  await assertFeatureArtifactApp(targetDir, config, "listener");
699
- await assertListenerEventExists(targetDir, names.event, config);
815
+ if (!options.skipEventAssert) {
816
+ await assertListenerEventExists(targetDir, names.event, config);
817
+ }
700
818
  return makeFeatureArtifact({
701
819
  targetDir,
702
820
  config,
@@ -850,6 +968,164 @@ async function applyCronSecretWiring(targetDir, plan, options) {
850
968
  skippedFiles: plan.skippedFiles,
851
969
  };
852
970
  }
971
+ async function planOutboxDrainWiring(targetDir, config, options) {
972
+ if (await fileExists(path.join(targetDir, outboxDrainRoutePath(config)))) {
973
+ return undefined;
974
+ }
975
+ const route = await planGeneratedFile(targetDir, {
976
+ path: outboxDrainRoutePath(config),
977
+ content: outboxDrainRouteFile(config),
978
+ }, { force: options.force });
979
+ return {
980
+ route,
981
+ cronSecret: await planCronSecretWiring(targetDir),
982
+ };
983
+ }
984
+ async function updateOutboxPortWiring(targetDir, config, options) {
985
+ const updated = new Set();
986
+ if (await updateOutboxPorts(targetDir, config, options)) {
987
+ updated.add(config.paths.ports);
988
+ }
989
+ if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
990
+ updated.add(config.paths.infrastructurePorts);
991
+ }
992
+ if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
993
+ updated.add(path.join(infrastructureDir(config), "db/provider.ts"));
994
+ }
995
+ if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
996
+ updated.add(drizzleRepositoriesPath(config));
997
+ }
998
+ return [...updated];
999
+ }
1000
+ async function applyOutboxPortWiring(result, targetDir, config, options) {
1001
+ const updatedFiles = await updateOutboxPortWiring(targetDir, config, options);
1002
+ for (const file of updatedFiles) {
1003
+ if (!result.updatedFiles.includes(file))
1004
+ result.updatedFiles.push(file);
1005
+ if (!result.files.includes(file))
1006
+ result.files.push(file);
1007
+ }
1008
+ }
1009
+ async function updateOutboxPorts(targetDir, config, options) {
1010
+ const filePath = path.join(targetDir, config.paths.ports);
1011
+ const original = await readFile(filePath, "utf8");
1012
+ let next = addNamedTypeImport(original, "OutboxPort", "@beignet/core/outbox");
1013
+ if (!typeHasTopLevelProperty(next, "AppPorts", "outbox")) {
1014
+ next = insertTypeProperty(next, "AppPorts", "outbox: OutboxPort;", `Could not find AppPorts in ${config.paths.ports}. Add outbox: OutboxPort; manually, or restore the generated ports file before running make outbox.`);
1015
+ }
1016
+ if (!typeHasTopLevelProperty(next, "AppTransactionPorts", "outbox")) {
1017
+ next = insertTypeProperty(next, "AppTransactionPorts", "outbox: OutboxPort;", `Could not find AppTransactionPorts in ${config.paths.ports}. Add outbox: OutboxPort; manually, or restore the generated ports file before running make outbox.`);
1018
+ }
1019
+ if (next === original)
1020
+ return false;
1021
+ if (!options.dryRun)
1022
+ await writeFile(filePath, next);
1023
+ return true;
1024
+ }
1025
+ async function updateOutboxInfrastructurePorts(targetDir, config, options) {
1026
+ const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1027
+ const original = await readFile(filePath, "utf8");
1028
+ const next = appendDeferredPortKey(original, "outbox");
1029
+ if (next === original)
1030
+ return false;
1031
+ if (!options.dryRun)
1032
+ await writeFile(filePath, next);
1033
+ return true;
1034
+ }
1035
+ async function updateOutboxDatabaseProvider(targetDir, config, options) {
1036
+ const file = path.join(infrastructureDir(config), "db/provider.ts");
1037
+ const filePath = path.join(targetDir, file);
1038
+ if (!(await fileExists(filePath)))
1039
+ return false;
1040
+ const database = await detectResourceDatabase(targetDir, config);
1041
+ const factoryName = drizzleOutboxPortFactoryName(database);
1042
+ const original = await readFile(filePath, "utf8");
1043
+ let next = addNamedImport(original, factoryName, `@beignet/provider-db-drizzle/${database}`);
1044
+ next = appendPickStringLiteralMember(next, "AppPorts", "outbox");
1045
+ if (!/\bconst\s+outbox\s*=/.test(next)) {
1046
+ const beforeOutboxConst = next;
1047
+ next = beforeOutboxConst.replace(/(\n\s*const idempotency = createDrizzle[A-Za-z]+IdempotencyPort\(dbPort\.db\);)/, `$1\n\t\tconst outbox = ${factoryName}(dbPort.db);`);
1048
+ if (next === beforeOutboxConst) {
1049
+ throw new Error(`Could not find the idempotency port in ${file}. Add const outbox = ${factoryName}(dbPort.db) manually, or restore the generated database provider before running make outbox.`);
1050
+ }
1051
+ }
1052
+ if (!/\boutbox\s*,/.test(next)) {
1053
+ const withOutboxPort = next.replace(/(\n\s*idempotency,\n)/, `$1\t\t\toutbox,\n`);
1054
+ if (withOutboxPort === next) {
1055
+ throw new Error(`Could not find idempotency in providedPorts in ${file}. Add outbox to providedPorts manually, or restore the generated database provider before running make outbox.`);
1056
+ }
1057
+ next = withOutboxPort;
1058
+ }
1059
+ if (!new RegExp(`outbox:\\s*${factoryName}\\(tx\\)`).test(next)) {
1060
+ const withTransactionOutbox = next.replace(/(\n\s*idempotency: createDrizzle[A-Za-z]+IdempotencyPort\(tx\),\n)/, `$1\t\t\t\t\toutbox: ${factoryName}(tx),\n`);
1061
+ if (withTransactionOutbox === next) {
1062
+ throw new Error(`Could not find transaction idempotency wiring in ${file}. Add outbox: ${factoryName}(tx) manually, or restore the generated database provider before running make outbox.`);
1063
+ }
1064
+ next = withTransactionOutbox;
1065
+ }
1066
+ if (next === original)
1067
+ return false;
1068
+ if (!options.dryRun)
1069
+ await writeFile(filePath, next);
1070
+ return true;
1071
+ }
1072
+ async function updateOutboxRepositoriesReturnType(targetDir, config, options) {
1073
+ const file = drizzleRepositoriesPath(config);
1074
+ const filePath = path.join(targetDir, file);
1075
+ if (!(await fileExists(filePath)))
1076
+ return false;
1077
+ const original = await readFile(filePath, "utf8");
1078
+ const next = original.replace(/Omit<AppTransactionPorts,\s*"idempotency"\s*>/g, 'Omit<AppTransactionPorts, "idempotency" | "outbox">');
1079
+ if (next === original)
1080
+ return false;
1081
+ if (!options.dryRun)
1082
+ await writeFile(filePath, next);
1083
+ return true;
1084
+ }
1085
+ function drizzleOutboxPortFactoryName(database) {
1086
+ if (database === "postgres")
1087
+ return "createDrizzlePostgresOutboxPort";
1088
+ if (database === "mysql")
1089
+ return "createDrizzleMysqlOutboxPort";
1090
+ return "createDrizzleSqliteOutboxPort";
1091
+ }
1092
+ async function applyOutboxDrainWiring(result, targetDir, plan, options) {
1093
+ if (!plan)
1094
+ return;
1095
+ if (!options.dryRun) {
1096
+ await writePlannedGeneratedFile(targetDir, plan.route);
1097
+ }
1098
+ if (plan.route.result === "created") {
1099
+ result.createdFiles.push(plan.route.path);
1100
+ }
1101
+ if (plan.route.result === "updated") {
1102
+ result.updatedFiles.push(plan.route.path);
1103
+ }
1104
+ if (plan.route.result === "skipped") {
1105
+ result.skippedFiles.push(plan.route.path);
1106
+ }
1107
+ if (!result.files.includes(plan.route.path)) {
1108
+ result.files.push(plan.route.path);
1109
+ }
1110
+ const cronSecretResult = await applyCronSecretWiring(targetDir, plan.cronSecret, options);
1111
+ result.createdFiles = uniqueStrings([
1112
+ ...result.createdFiles,
1113
+ ...cronSecretResult.createdFiles,
1114
+ ]);
1115
+ result.updatedFiles = uniqueStrings([
1116
+ ...result.updatedFiles,
1117
+ ...cronSecretResult.updatedFiles,
1118
+ ]);
1119
+ result.skippedFiles = uniqueStrings([
1120
+ ...result.skippedFiles,
1121
+ ...cronSecretResult.skippedFiles,
1122
+ ]);
1123
+ result.files = uniqueStrings([
1124
+ ...result.files,
1125
+ ...plan.cronSecret.files.map((file) => file.path),
1126
+ ...plan.cronSecret.skippedFiles,
1127
+ ]);
1128
+ }
853
1129
  function eventBusPortWiring(command) {
854
1130
  return {
855
1131
  command,
@@ -891,6 +1167,17 @@ function paymentsPortWiring(command) {
891
1167
  providerModule: "@beignet/core/payments",
892
1168
  };
893
1169
  }
1170
+ function storagePortWiring(command) {
1171
+ return {
1172
+ command,
1173
+ portKey: "storage",
1174
+ portType: "StoragePort",
1175
+ portTypeModule: "@beignet/core/ports",
1176
+ providerFactory: "createLocalStorageProvider",
1177
+ providerModule: "@beignet/provider-storage-local",
1178
+ dependency: "@beignet/provider-storage-local",
1179
+ };
1180
+ }
894
1181
  function providersFilePath(config) {
895
1182
  return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
896
1183
  }
@@ -1011,7 +1298,10 @@ export async function makeUpload(options) {
1011
1298
  ? resolveConfig(options.config)
1012
1299
  : await loadBeignetConfig(targetDir);
1013
1300
  await assertFeatureArtifactApp(targetDir, config, "upload");
1014
- return makeFeatureArtifact({
1301
+ await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
1302
+ dryRun: true,
1303
+ });
1304
+ const result = await makeFeatureArtifact({
1015
1305
  targetDir,
1016
1306
  config,
1017
1307
  force: Boolean(options.force),
@@ -1023,6 +1313,72 @@ export async function makeUpload(options) {
1023
1313
  "@beignet/core": beignetDependencyVersion,
1024
1314
  },
1025
1315
  });
1316
+ await applyPortProviderWiring(result, targetDir, config, [storagePortWiring("make upload")], { dryRun: Boolean(options.dryRun) });
1317
+ const uploadRouteResult = await updateUploadRoute(targetDir, names, config, {
1318
+ dryRun: Boolean(options.dryRun),
1319
+ });
1320
+ if (uploadRouteResult === "created")
1321
+ result.createdFiles.push(uploadRoutePath(config));
1322
+ if (uploadRouteResult === "updated")
1323
+ result.updatedFiles.push(uploadRoutePath(config));
1324
+ if (uploadRouteResult === "skipped")
1325
+ result.skippedFiles.push(uploadRoutePath(config));
1326
+ if (!result.files.includes(uploadRoutePath(config))) {
1327
+ result.files.push(uploadRoutePath(config));
1328
+ }
1329
+ return result;
1330
+ }
1331
+ export async function makeOutbox(options = {}) {
1332
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
1333
+ const config = options.config
1334
+ ? resolveConfig(options.config)
1335
+ : await loadBeignetConfig(targetDir);
1336
+ await assertServerRuntimeApp(targetDir, config, "outbox");
1337
+ await updateOutboxPortWiring(targetDir, config, { dryRun: true });
1338
+ const cronSecretPlan = await planCronSecretWiring(targetDir);
1339
+ const files = outboxFiles(config);
1340
+ const plannedFiles = await planGeneratedFiles(targetDir, files, {
1341
+ force: Boolean(options.force),
1342
+ });
1343
+ const createdFiles = plannedFiles
1344
+ .filter((file) => file.result === "created")
1345
+ .map((file) => file.path);
1346
+ const updatedFiles = plannedFiles
1347
+ .filter((file) => file.result === "updated")
1348
+ .map((file) => file.path);
1349
+ const skippedFiles = plannedFiles
1350
+ .filter((file) => file.result === "skipped")
1351
+ .map((file) => file.path);
1352
+ if (!options.dryRun) {
1353
+ for (const file of plannedFiles) {
1354
+ await writePlannedGeneratedFile(targetDir, file);
1355
+ }
1356
+ }
1357
+ const cronSecretResult = await applyCronSecretWiring(targetDir, cronSecretPlan, { dryRun: Boolean(options.dryRun) });
1358
+ createdFiles.push(...cronSecretResult.createdFiles);
1359
+ updatedFiles.push(...cronSecretResult.updatedFiles);
1360
+ skippedFiles.push(...cronSecretResult.skippedFiles);
1361
+ updatedFiles.push(...(await updateOutboxPortWiring(targetDir, config, {
1362
+ dryRun: Boolean(options.dryRun),
1363
+ })));
1364
+ return {
1365
+ schemaVersion: 1,
1366
+ name: "outbox",
1367
+ targetDir,
1368
+ dryRun: Boolean(options.dryRun),
1369
+ files: [
1370
+ ...files.map((file) => file.path),
1371
+ ...cronSecretPlan.files.map((file) => file.path),
1372
+ ...cronSecretPlan.skippedFiles,
1373
+ config.paths.ports,
1374
+ config.paths.infrastructurePorts,
1375
+ path.join(infrastructureDir(config), "db/provider.ts"),
1376
+ drizzleRepositoriesPath(config),
1377
+ ],
1378
+ createdFiles: uniqueStrings(createdFiles),
1379
+ updatedFiles: uniqueStrings(updatedFiles),
1380
+ skippedFiles: uniqueStrings(skippedFiles),
1381
+ };
1026
1382
  }
1027
1383
  async function makeFeatureUi(options) {
1028
1384
  const targetDir = path.resolve(options.cwd ?? process.cwd());
@@ -1180,6 +1536,21 @@ async function assertFeatureUiApp(targetDir, config) {
1180
1536
  }
1181
1537
  }
1182
1538
  }
1539
+ async function assertServerRuntimeApp(targetDir, config, artifact) {
1540
+ const requiredFiles = [
1541
+ config.paths.appContext,
1542
+ config.paths.server,
1543
+ config.paths.infrastructurePorts,
1544
+ ];
1545
+ for (const file of requiredFiles) {
1546
+ try {
1547
+ await stat(path.join(targetDir, file));
1548
+ }
1549
+ catch {
1550
+ throw new Error(`beignet make ${artifact} expects a standard Beignet app with a server runtime. Missing ${file}.`);
1551
+ }
1552
+ }
1553
+ }
1183
1554
  async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
1184
1555
  const requiredFiles = [
1185
1556
  config.paths.appContext,
@@ -1358,6 +1729,21 @@ async function updatePackageDependencies(targetDir, dependencies, options) {
1358
1729
  await writeFile(filePath, next);
1359
1730
  return true;
1360
1731
  }
1732
+ async function updatePackageScripts(targetDir, scripts, options) {
1733
+ const filePath = path.join(targetDir, "package.json");
1734
+ const original = await readFile(filePath, "utf8");
1735
+ const packageJson = JSON.parse(original);
1736
+ packageJson.scripts = packageJson.scripts ?? {};
1737
+ for (const [name, command] of Object.entries(scripts)) {
1738
+ packageJson.scripts[name] ??= command;
1739
+ }
1740
+ const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
1741
+ if (next === original)
1742
+ return false;
1743
+ if (!options.dryRun)
1744
+ await writeFile(filePath, next);
1745
+ return true;
1746
+ }
1361
1747
  function beignetDependencyVersion(packageJson) {
1362
1748
  return (packageJson.dependencies?.["@beignet/core"] ??
1363
1749
  packageJson.dependencies?.["@beignet/next"] ??
@@ -2239,6 +2625,10 @@ function typeBodySource(source, typeName) {
2239
2625
  return undefined;
2240
2626
  return source.slice(openBrace + 1, closeBrace);
2241
2627
  }
2628
+ function typeHasTopLevelProperty(source, typeName, propertyName) {
2629
+ const body = typeBodySource(source, typeName);
2630
+ return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
2631
+ }
2242
2632
  function appendUnionTypeMember(source, typeName, member) {
2243
2633
  const match = new RegExp(`(export\\s+type\\s+${typeName}\\s*=)([\\s\\S]*?);`).exec(source);
2244
2634
  if (!match)
@@ -2429,6 +2819,30 @@ function addNamedTypeImport(source, name, moduleName) {
2429
2819
  : `import type { ${names.join(", ")} } from "${moduleName}";`;
2430
2820
  return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
2431
2821
  }
2822
+ function addNamedImport(source, name, moduleName) {
2823
+ const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2824
+ const importPattern = new RegExp(`import \\{([^}]*)\\} from "${escapedModule}";`);
2825
+ const match = importPattern.exec(source);
2826
+ if (!match) {
2827
+ return insertAfterImports(source, `import { ${name} } from "${moduleName}";`);
2828
+ }
2829
+ const names = match[1]
2830
+ .split(",")
2831
+ .map((entry) => entry.trim())
2832
+ .filter(Boolean);
2833
+ if (names.includes(name))
2834
+ return source;
2835
+ const insertIndex = names.findIndex((existing) => existing.localeCompare(name) > 0);
2836
+ if (insertIndex === -1)
2837
+ names.push(name);
2838
+ else
2839
+ names.splice(insertIndex, 0, name);
2840
+ const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
2841
+ const replacement = match[1].includes("\n")
2842
+ ? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
2843
+ : `import { ${names.join(", ")} } from "${moduleName}";`;
2844
+ return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
2845
+ }
2432
2846
  function topLevelBoundObjectRange(source, openBrace, closeBrace) {
2433
2847
  const body = source.slice(openBrace + 1, closeBrace);
2434
2848
  if (!hasTopLevelObjectProperty(body, "bound"))
@@ -2798,6 +3212,9 @@ function seedNames(input) {
2798
3212
  seedExportName,
2799
3213
  };
2800
3214
  }
3215
+ function defaultFactoryNamesForSeed(names) {
3216
+ return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
3217
+ }
2801
3218
  function notificationNames(input) {
2802
3219
  const names = featureArtifactNames(input, "Notification");
2803
3220
  const notificationExportName = `${names.featureSingularPascal}${names.artifact.pascal}Notification`;
@@ -3266,6 +3683,18 @@ function uploadFiles(names, config) {
3266
3683
  },
3267
3684
  ];
3268
3685
  }
3686
+ function outboxFiles(config) {
3687
+ return [
3688
+ {
3689
+ path: config.paths.outbox,
3690
+ content: outboxRegistryFile(config),
3691
+ },
3692
+ {
3693
+ path: outboxDrainRoutePath(config),
3694
+ content: outboxDrainRouteFile(config),
3695
+ },
3696
+ ];
3697
+ }
3269
3698
  function portFilePath(names, config) {
3270
3699
  return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
3271
3700
  }
@@ -3450,6 +3879,15 @@ function scheduleFilePath(names, config) {
3450
3879
  function uploadFilePath(names, config) {
3451
3880
  return path.join(featureArtifactDir(names, "uploads", config), `${names.artifact.kebab}.ts`);
3452
3881
  }
3882
+ function seedEntrypointPath(config) {
3883
+ return path.join(infrastructureDir(config), "db", "seed.ts");
3884
+ }
3885
+ function uploadRoutePath(config) {
3886
+ return path.join(config.paths.routes, "uploads", "[uploadName]", "[action]", "route.ts");
3887
+ }
3888
+ function outboxDrainRoutePath(config) {
3889
+ return path.join(config.paths.routes, "cron", "outbox", "drain", "route.ts");
3890
+ }
3453
3891
  function scheduleRouteFilePath(names, config) {
3454
3892
  return path.join(config.paths.routes, "cron", names.feature.kebab, names.artifact.kebab, "route.ts");
3455
3893
  }
@@ -3755,19 +4193,87 @@ export async function stopScheduleContext(): Promise<void> {
3755
4193
  await writeFile(destination, next);
3756
4194
  return "updated";
3757
4195
  }
4196
+ async function updateSeedEntrypoint(targetDir, names, config, options) {
4197
+ const file = seedEntrypointPath(config);
4198
+ const destination = path.join(targetDir, file);
4199
+ const existing = await readOptionalFile(destination);
4200
+ const registryName = `${names.featureSingularCamel}Seeds`;
4201
+ const importLine = `import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";`;
4202
+ const registryEntry = `...${registryName}`;
4203
+ if (existing === undefined) {
4204
+ const content = seedEntrypointFile(names, config);
4205
+ if (!options.dryRun) {
4206
+ await mkdir(path.dirname(destination), { recursive: true });
4207
+ await writeFile(destination, content);
4208
+ }
4209
+ return "created";
4210
+ }
4211
+ let next = existing;
4212
+ if (!next.includes(importLine)) {
4213
+ next = insertAfterImports(next, importLine);
4214
+ }
4215
+ const registry = arrayInitializerInfo(next, "seeds");
4216
+ if (!registry) {
4217
+ next = `${next.trimEnd()}\n\nconst seeds = [${registryEntry}] as const;\n`;
4218
+ }
4219
+ else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
4220
+ const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
4221
+ next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
4222
+ }
4223
+ if (next === existing)
4224
+ return "skipped";
4225
+ if (!options.dryRun)
4226
+ await writeFile(destination, next);
4227
+ return "updated";
4228
+ }
4229
+ async function updateUploadRoute(targetDir, names, config, options) {
4230
+ const file = uploadRoutePath(config);
4231
+ const destination = path.join(targetDir, file);
4232
+ const existing = await readOptionalFile(destination);
4233
+ const registryName = `${names.featureSingularCamel}Uploads`;
4234
+ const registryKey = names.featureSingularCamel;
4235
+ const importLine = `import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "uploads", config), "index.ts"))}";`;
4236
+ if (existing === undefined) {
4237
+ const content = uploadRouteFile(names, config);
4238
+ if (!options.dryRun) {
4239
+ await mkdir(path.dirname(destination), { recursive: true });
4240
+ await writeFile(destination, content);
4241
+ }
4242
+ return "created";
4243
+ }
4244
+ let next = existing;
4245
+ if (!next.includes(importLine)) {
4246
+ next = insertAfterImports(next, importLine);
4247
+ }
4248
+ const registry = defineUploadsInitializerInfo(next, "uploadRegistry");
4249
+ if (!registry) {
4250
+ return "skipped";
4251
+ }
4252
+ if (!new RegExp(`\\b${registryKey}\\s*:`).test(registry.text)) {
4253
+ next = `${next.slice(0, registry.end)}\t${registryKey}: ${registryName},\n${next.slice(registry.end)}`;
4254
+ }
4255
+ if (next === existing)
4256
+ return "skipped";
4257
+ if (!options.dryRun)
4258
+ await writeFile(destination, next);
4259
+ return "updated";
4260
+ }
3758
4261
  /**
3759
4262
  * Append a feature event or job registry to `server/outbox.ts`.
3760
- *
3761
- * The outbox registry is opt-in, so this never creates the file; it returns
3762
- * `undefined` when the app has no outbox module. It also bails to "skipped"
3763
- * instead of writing when the `defineOutboxRegistry({...})` anchor is missing
3764
- * or the matching array is not editable.
4263
+ * Creates the registry when missing so event/job generators produce a complete
4264
+ * drainable path instead of leaving durable delivery as manual wiring.
3765
4265
  */
3766
4266
  async function updateOutboxRegistry(targetDir, names, kind, config, options) {
3767
4267
  const destination = path.join(targetDir, config.paths.outbox);
3768
4268
  const existing = await readOptionalFile(destination);
3769
- if (existing === undefined)
3770
- return undefined;
4269
+ if (existing === undefined) {
4270
+ const content = outboxRegistryFile(config, { names, kind });
4271
+ if (!options.dryRun) {
4272
+ await mkdir(path.dirname(destination), { recursive: true });
4273
+ await writeFile(destination, content);
4274
+ }
4275
+ return "created";
4276
+ }
3771
4277
  const featureIndexPath = path.join(featureArtifactDir(names, kind, config), "index.ts");
3772
4278
  const registryName = `${names.featureSingularCamel}${kind === "events" ? "Events" : "Jobs"}`;
3773
4279
  const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
@@ -3786,8 +4292,8 @@ async function updateOutboxRegistry(targetDir, names, kind, config, options) {
3786
4292
  */
3787
4293
  async function applyOutboxRegistryUpdate(result, targetDir, names, kind, config, options) {
3788
4294
  const registryResult = await updateOutboxRegistry(targetDir, names, kind, config, options);
3789
- if (registryResult === undefined)
3790
- return;
4295
+ if (registryResult === "created")
4296
+ result.createdFiles.push(config.paths.outbox);
3791
4297
  if (registryResult === "updated")
3792
4298
  result.updatedFiles.push(config.paths.outbox);
3793
4299
  if (registryResult === "skipped")
@@ -5144,6 +5650,105 @@ export const ${names.uploadExportName} = defineUpload<
5144
5650
  });
5145
5651
  `;
5146
5652
  }
5653
+ function seedEntrypointFile(names, config) {
5654
+ const registryName = `${names.featureSingularCamel}Seeds`;
5655
+ return `import { createServiceActor } from "@beignet/core/ports";
5656
+ import { runSeeds } from "@beignet/core/testing";
5657
+ import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";
5658
+ import { server } from "${aliasModule(config.paths.server)}";
5659
+
5660
+ const seeds = [...${registryName}] as const;
5661
+
5662
+ const ctx = await server.createServiceContext({
5663
+ \tactor: createServiceActor("beignet-seed"),
5664
+ });
5665
+
5666
+ try {
5667
+ \tawait runSeeds({ ctx, seeds });
5668
+ } finally {
5669
+ \tawait server.stop();
5670
+ }
5671
+ `;
5672
+ }
5673
+ function uploadRouteFile(names, config) {
5674
+ const registryName = `${names.featureSingularCamel}Uploads`;
5675
+ return `import {
5676
+ \tcreateUploadRouter,
5677
+ \tdefineUploads,
5678
+ \tuploadsFromRegistry,
5679
+ } from "@beignet/core/uploads";
5680
+ import { createUploadRoute } from "@beignet/next";
5681
+ import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "uploads", config), "index.ts"))}";
5682
+ import { server } from "${aliasModule(config.paths.server)}";
5683
+
5684
+ const uploadRegistry = defineUploads({
5685
+ \t${names.featureSingularCamel}: ${registryName},
5686
+ });
5687
+
5688
+ const uploadRouter = createUploadRouter({
5689
+ \tuploads: uploadsFromRegistry(uploadRegistry),
5690
+ \tctx: () => server.createContextFromNext(),
5691
+ \tstorage: server.ports.storage,
5692
+ });
5693
+
5694
+ export const { POST } = createUploadRoute(uploadRouter);
5695
+ `;
5696
+ }
5697
+ function outboxRegistryFile(config, registration) {
5698
+ const featureRegistry = registration
5699
+ ? outboxFeatureRegistry(registration.names, registration.kind, config)
5700
+ : undefined;
5701
+ const eventEntry = featureRegistry?.kind === "events" ? `...${featureRegistry.name}` : "";
5702
+ const jobEntry = featureRegistry?.kind === "jobs" ? `...${featureRegistry.name}` : "";
5703
+ const featureImport = featureRegistry
5704
+ ? `${featureRegistry.importLine}\n`
5705
+ : "";
5706
+ return `import { defineOutboxRegistry } from "@beignet/core/outbox";
5707
+ import { createServiceActor } from "@beignet/core/ports";
5708
+ import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5709
+ ${featureImport}import { server } from "${relativeModule(config.paths.outbox, config.paths.server)}";
5710
+
5711
+ export const outboxRegistry = defineOutboxRegistry({
5712
+ \tevents: [${eventEntry}],
5713
+ \tjobs: [${jobEntry}],
5714
+ });
5715
+
5716
+ export async function createOutboxDrainContext(): Promise<AppContext> {
5717
+ \treturn server.createServiceContext({
5718
+ \t\tactor: createServiceActor("beignet-outbox"),
5719
+ \t});
5720
+ }
5721
+
5722
+ export async function stopOutboxDrainContext(): Promise<void> {
5723
+ \tawait server.stop();
5724
+ }
5725
+ `;
5726
+ }
5727
+ function outboxDrainRouteFile(config) {
5728
+ return `import { createOutboxDrainRoute } from "@beignet/next";
5729
+ import { env } from "@/lib/env";
5730
+ import { server } from "${aliasModule(config.paths.server)}";
5731
+ import { outboxRegistry } from "${aliasModule(config.paths.outbox)}";
5732
+
5733
+ export const runtime = "nodejs";
5734
+
5735
+ export const { GET, POST } = createOutboxDrainRoute({
5736
+ \tserver,
5737
+ \tregistry: outboxRegistry,
5738
+ \tsecret: env.CRON_SECRET,
5739
+ \tbatchSize: 100,
5740
+ });
5741
+ `;
5742
+ }
5743
+ function outboxFeatureRegistry(names, kind, config) {
5744
+ const featureIndexPath = path.join(featureArtifactDir(names, kind, config), "index.ts");
5745
+ const name = `${names.featureSingularCamel}${kind === "events" ? "Events" : "Jobs"}`;
5746
+ return {
5747
+ kind,
5748
+ name,
5749
+ importLine: `import { ${name} } from "${aliasModule(featureIndexPath)}";`,
5750
+ };
5751
+ }
5147
5752
  function billingSchemasFile() {
5148
5753
  return `import { z } from "zod";
5149
5754
 
@@ -5832,7 +6437,7 @@ import type { AppContext } from "${relativeModule(routeFilePath, config.paths.ap
5832
6437
  import * as billingContracts from "@/features/billing/contracts";
5833
6438
  import * as billingUseCases from "@/features/billing/use-cases";
5834
6439
 
5835
- export const billingRoutes = defineRouteGroup<AppContext>({
6440
+ export const billingRoutes = defineRouteGroup<AppContext>()({
5836
6441
  \tname: "billing",
5837
6442
  \troutes: [
5838
6443
  \t\t{
@@ -6480,7 +7085,7 @@ import {
6480
7085
  ${mode === "resource" ? ` delete${names.singularPascal}UseCase,\n get${names.singularPascal}UseCase,\n` : ""} list${names.pluralPascal}UseCase,
6481
7086
  ${mode === "resource" ? ` update${names.singularPascal}UseCase,\n` : ""}} from "${relativeModule(routeFilePath, useCasesPath)}";
6482
7087
 
6483
- export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>({
7088
+ export const ${names.singularCamel}Routes = defineRouteGroup<AppContext>()({
6484
7089
  name: "${names.pluralKebab}",
6485
7090
  routes: [
6486
7091
  { contract: list${names.pluralPascal}, useCase: list${names.pluralPascal}UseCase },