@beignet/cli 0.0.26 → 0.0.27

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 (75) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +29 -10
  3. package/dist/choices.d.ts +8 -0
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +14 -0
  6. package/dist/choices.js.map +1 -1
  7. package/dist/db.js +2 -2
  8. package/dist/db.js.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +109 -13
  11. package/dist/index.js.map +1 -1
  12. package/dist/inspect.js +51 -3
  13. package/dist/inspect.js.map +1 -1
  14. package/dist/make/inbox.d.ts +63 -0
  15. package/dist/make/inbox.d.ts.map +1 -0
  16. package/dist/make/inbox.js +1747 -0
  17. package/dist/make/inbox.js.map +1 -0
  18. package/dist/make/payments.d.ts +61 -0
  19. package/dist/make/payments.d.ts.map +1 -0
  20. package/dist/make/payments.js +1900 -0
  21. package/dist/make/payments.js.map +1 -0
  22. package/dist/make/shared.d.ts +575 -0
  23. package/dist/make/shared.d.ts.map +1 -0
  24. package/dist/make/shared.js +2166 -0
  25. package/dist/make/shared.js.map +1 -0
  26. package/dist/make/tenancy.d.ts +101 -0
  27. package/dist/make/tenancy.d.ts.map +1 -0
  28. package/dist/make/tenancy.js +4409 -0
  29. package/dist/make/tenancy.js.map +1 -0
  30. package/dist/make.d.ts +8 -44
  31. package/dist/make.d.ts.map +1 -1
  32. package/dist/make.js +71 -3659
  33. package/dist/make.js.map +1 -1
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +44 -3
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/provider-add.d.ts +19 -0
  38. package/dist/provider-add.d.ts.map +1 -0
  39. package/dist/provider-add.js +691 -0
  40. package/dist/provider-add.js.map +1 -0
  41. package/dist/registry-edits.d.ts +9 -0
  42. package/dist/registry-edits.d.ts.map +1 -1
  43. package/dist/registry-edits.js +32 -0
  44. package/dist/registry-edits.js.map +1 -1
  45. package/dist/task.d.ts +2 -0
  46. package/dist/task.d.ts.map +1 -1
  47. package/dist/task.js +2 -0
  48. package/dist/task.js.map +1 -1
  49. package/dist/templates/agents.js +2 -2
  50. package/dist/templates/server.d.ts.map +1 -1
  51. package/dist/templates/server.js +44 -37
  52. package/dist/templates/server.js.map +1 -1
  53. package/dist/templates/shared.d.ts +5 -0
  54. package/dist/templates/shared.d.ts.map +1 -1
  55. package/dist/templates/shared.js +5 -0
  56. package/dist/templates/shared.js.map +1 -1
  57. package/package.json +2 -2
  58. package/skills/app-structure/SKILL.md +1 -1
  59. package/src/choices.ts +28 -0
  60. package/src/db.ts +2 -2
  61. package/src/index.ts +170 -13
  62. package/src/inspect.ts +68 -2
  63. package/src/make/inbox.ts +2015 -0
  64. package/src/make/payments.ts +2182 -0
  65. package/src/make/shared.ts +3550 -0
  66. package/src/make/tenancy.ts +4809 -0
  67. package/src/make.ts +380 -5289
  68. package/src/mcp.ts +65 -2
  69. package/src/provider-add.ts +926 -0
  70. package/src/registry-edits.ts +47 -0
  71. package/src/task.ts +8 -10
  72. package/src/templates/agents.ts +2 -2
  73. package/src/templates/server.ts +44 -37
  74. package/src/templates/shared.ts +5 -0
  75. 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 & {
@@ -131,6 +134,7 @@ type DbSchemaGenerateFlags = DbFlags & {
131
134
  type TaskRunFlags = {
132
135
  json?: boolean;
133
136
  input?: string;
137
+ tenant?: string;
134
138
  module?: string;
135
139
  };
136
140
 
@@ -168,7 +172,24 @@ type ProviderAuditFlags = {
168
172
  cwd?: string;
169
173
  };
170
174
 
175
+ type ProviderAddFlags = {
176
+ json?: boolean;
177
+ dryRun?: boolean;
178
+ cwd?: string;
179
+ };
180
+
171
181
  const parseString = (input: string): string => input;
182
+ const parseProviderPreset = (input: string): ProviderPresetName => {
183
+ if ((providerPresetChoices as readonly string[]).includes(input)) {
184
+ return input as ProviderPresetName;
185
+ }
186
+
187
+ throw new Error(
188
+ `Unknown provider preset "${input}". Available presets: ${providerPresetChoices.join(
189
+ ", ",
190
+ )}.`,
191
+ );
192
+ };
172
193
  const parsePositiveInteger = (input: string): number => {
173
194
  const value = Number(input);
174
195
  if (!Number.isInteger(value) || value <= 0) {
@@ -240,6 +261,12 @@ const providerAuditFlagParameters = {
240
261
  cwd: cwdFlag,
241
262
  } satisfies FlagParametersForType<ProviderAuditFlags, CliContext>;
242
263
 
264
+ const providerAddFlagParameters = {
265
+ json: jsonFlag,
266
+ dryRun: dryRunFlag,
267
+ cwd: cwdFlag,
268
+ } satisfies FlagParametersForType<ProviderAddFlags, CliContext>;
269
+
243
270
  const dbFlagParameters = {
244
271
  json: jsonFlag,
245
272
  dryRun: dryRunFlag,
@@ -270,6 +297,9 @@ const dbSchemaGenerateFlagParameters = {
270
297
  const taskRunFlagParameters = {
271
298
  json: jsonFlag,
272
299
  input: parsedStringFlag("JSON input passed to the task schema."),
300
+ tenant: parsedStringFlag(
301
+ "Tenant id or slug passed to the app's createTaskContext, separate from task input.",
302
+ ),
273
303
  module: parsedStringFlag(
274
304
  "Task registry module. Defaults to server/tasks.ts.",
275
305
  ),
@@ -317,6 +347,19 @@ const namePositional = {
317
347
  ],
318
348
  } as const;
319
349
 
350
+ const providerPresetPositional = {
351
+ kind: "tuple",
352
+ parameters: [
353
+ {
354
+ parse: parseProviderPreset,
355
+ placeholder: "preset",
356
+ brief: `Provider preset to add. Available: ${providerPresetChoices.join(
357
+ ", ",
358
+ )}.`,
359
+ },
360
+ ],
361
+ } as const;
362
+
320
363
  function resolveOutputFormat(flags: {
321
364
  json?: boolean;
322
365
  format?: OutputFormat;
@@ -639,11 +682,50 @@ const providerAuditCommand = buildCommand<ProviderAuditFlags, [], CliContext>({
639
682
  },
640
683
  });
641
684
 
685
+ const providerAddCommand = buildCommand<
686
+ ProviderAddFlags,
687
+ [ProviderPresetName],
688
+ CliContext
689
+ >({
690
+ docs: {
691
+ brief: "Add a provider setup preset to a Beignet app.",
692
+ fullDescription: `Updates an existing Beignet app with provider package dependencies, server/provider registration, app port wiring, .env.example entries, and local setup notes.
693
+
694
+ Available presets: ${providerPresetChoices.join(", ")}`,
695
+ },
696
+ parameters: {
697
+ flags: providerAddFlagParameters,
698
+ positional: providerPresetPositional,
699
+ },
700
+ loader: async () => {
701
+ const { addProviderPreset } = await import("./provider-add.js");
702
+
703
+ return async function runProviderAdd(
704
+ this: CliContext,
705
+ flags: ProviderAddFlags,
706
+ name: ProviderPresetName,
707
+ ) {
708
+ const result = await addProviderPreset({
709
+ name,
710
+ cwd: flags.cwd,
711
+ dryRun: Boolean(flags.dryRun),
712
+ });
713
+ writeOutput(
714
+ this,
715
+ flags.json
716
+ ? JSON.stringify(result, null, 2)
717
+ : providerAddNextSteps(result),
718
+ );
719
+ };
720
+ },
721
+ });
722
+
642
723
  const providerRoutes = buildRouteMap({
643
724
  docs: {
644
- brief: "Inspect Beignet provider setup.",
725
+ brief: "Manage Beignet provider setup.",
645
726
  },
646
727
  routes: {
728
+ add: providerAddCommand,
647
729
  audit: providerAuditCommand,
648
730
  },
649
731
  });
@@ -760,6 +842,7 @@ const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
760
842
  const result = await runAppTask({
761
843
  name,
762
844
  input: flags.input,
845
+ tenant: flags.tenant,
763
846
  modulePath: flags.module,
764
847
  });
765
848
 
@@ -1215,7 +1298,9 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
1215
1298
  flags: makeFlagParameters,
1216
1299
  },
1217
1300
  loader: async () => {
1218
- const { makePayments } = await import("./make.js");
1301
+ const { makePayments, makePaymentsNextStepLines } = await import(
1302
+ "./make.js"
1303
+ );
1219
1304
 
1220
1305
  return async function runMakePayments(this: CliContext, flags: MakeFlags) {
1221
1306
  const result = await makePayments({
@@ -1226,7 +1311,69 @@ const makePaymentsCommand = buildCommand<MakeFlags, [], CliContext>({
1226
1311
  this,
1227
1312
  flags.json
1228
1313
  ? JSON.stringify(result, null, 2)
1229
- : makePaymentsNextSteps(result),
1314
+ : makeNextSteps(
1315
+ result,
1316
+ "billing slice",
1317
+ makePaymentsNextStepLines(result),
1318
+ ),
1319
+ );
1320
+ };
1321
+ },
1322
+ });
1323
+
1324
+ const makeTenancyCommand = buildCommand<MakeFlags, [], CliContext>({
1325
+ docs: {
1326
+ brief: "Generate a workspace tenancy slice.",
1327
+ },
1328
+ parameters: {
1329
+ flags: makeFlagParameters,
1330
+ },
1331
+ loader: async () => {
1332
+ const { makeTenancy, makeTenancyNextStepLines } = await import("./make.js");
1333
+
1334
+ return async function runMakeTenancy(this: CliContext, flags: MakeFlags) {
1335
+ const result = await makeTenancy({
1336
+ force: Boolean(flags.force),
1337
+ dryRun: Boolean(flags.dryRun),
1338
+ });
1339
+ writeOutput(
1340
+ this,
1341
+ flags.json
1342
+ ? JSON.stringify(result, null, 2)
1343
+ : makeNextSteps(
1344
+ result,
1345
+ "workspace slice",
1346
+ makeTenancyNextStepLines(),
1347
+ ),
1348
+ );
1349
+ };
1350
+ },
1351
+ });
1352
+
1353
+ const makeInboxCommand = buildCommand<MakeFlags, [], CliContext>({
1354
+ docs: {
1355
+ brief: "Generate an in-app inbox notifications slice.",
1356
+ },
1357
+ parameters: {
1358
+ flags: makeFlagParameters,
1359
+ },
1360
+ loader: async () => {
1361
+ const { makeInbox, makeInboxNextStepLines } = await import("./make.js");
1362
+
1363
+ return async function runMakeInbox(this: CliContext, flags: MakeFlags) {
1364
+ const result = await makeInbox({
1365
+ force: Boolean(flags.force),
1366
+ dryRun: Boolean(flags.dryRun),
1367
+ });
1368
+ writeOutput(
1369
+ this,
1370
+ flags.json
1371
+ ? JSON.stringify(result, null, 2)
1372
+ : makeNextSteps(
1373
+ result,
1374
+ "notifications slice",
1375
+ makeInboxNextStepLines(),
1376
+ ),
1230
1377
  );
1231
1378
  };
1232
1379
  },
@@ -1283,6 +1430,7 @@ const makeRoutes = buildRouteMap({
1283
1430
  makeFactoryNextSteps,
1284
1431
  ),
1285
1432
  feature: makeFeatureCommand,
1433
+ inbox: makeInboxCommand,
1286
1434
  job: simpleMakeCommand(
1287
1435
  "makeJob",
1288
1436
  "Generate a feature job.",
@@ -1318,6 +1466,7 @@ const makeRoutes = buildRouteMap({
1318
1466
  "Generate a feature operational task.",
1319
1467
  makeTaskNextSteps,
1320
1468
  ),
1469
+ tenancy: makeTenancyCommand,
1321
1470
  test: simpleMakeCommand(
1322
1471
  "makeTest",
1323
1472
  "Generate a use case test.",
@@ -1432,6 +1581,7 @@ type DatabaseSchemaGenerateNextStepsResult = {
1432
1581
  type AppTaskRunNextStepsResult = {
1433
1582
  name: string;
1434
1583
  durationMs: number;
1584
+ tenant?: string;
1435
1585
  output: unknown;
1436
1586
  };
1437
1587
 
@@ -1463,7 +1613,9 @@ function appTaskRunNextSteps(result: AppTaskRunNextStepsResult): string {
1463
1613
  ? "undefined"
1464
1614
  : JSON.stringify(result.output, null, 2);
1465
1615
 
1466
- return `Ran task ${result.name} in ${result.durationMs}ms
1616
+ const tenant = result.tenant ? `\nTenant: ${result.tenant}` : "";
1617
+
1618
+ return `Ran task ${result.name} in ${result.durationMs}ms${tenant}
1467
1619
 
1468
1620
  Output:
1469
1621
  ${output}`;
@@ -1617,6 +1769,20 @@ Next steps:
1617
1769
  ${steps.map((step) => ` ${step}`).join("\n")}`;
1618
1770
  }
1619
1771
 
1772
+ function providerAddNextSteps(result: AddProviderPresetResult): string {
1773
+ const { changedFiles, skippedFiles } = changedFileLines(result);
1774
+ const prefix = result.dryRun ? "Would add" : "Added";
1775
+
1776
+ return `${prefix} ${result.name} provider preset in ${result.targetDir}
1777
+
1778
+ Changed files:
1779
+ ${changedFiles || " none"}
1780
+ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
1781
+
1782
+ Next steps:
1783
+ ${result.nextSteps.map((step) => ` ${step}`).join("\n")}`;
1784
+ }
1785
+
1620
1786
  function makeResourceNextSteps(result: MakeNextStepsResult): string {
1621
1787
  return makeNextSteps(result, "resource", [
1622
1788
  "Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
@@ -1754,15 +1920,6 @@ function makeNotificationNextSteps(result: MakeNextStepsResult): string {
1754
1920
  ]);
1755
1921
  }
1756
1922
 
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
1923
  function makeOutboxNextSteps(result: MakeNextStepsResult): string {
1767
1924
  return makeNextSteps(result, "outbox runtime", [
1768
1925
  "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
@@ -986,6 +986,24 @@ function parseRouteExports(
986
986
  const imports = parseNamedImports(source, config);
987
987
  const exportRegex =
988
988
  /export const\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=\s*([^;\n]+)/g;
989
+ const apiRouteExportRegex =
990
+ /export const\s*\{([^}]+)\}\s*=\s*createApiRoute\s*\(/g;
991
+
992
+ for (const match of source.matchAll(apiRouteExportRegex)) {
993
+ for (const member of match[1].split(",")) {
994
+ const parts = member.split(":");
995
+ const method = (parts[1] ?? parts[0])?.trim();
996
+ if (!method || !isHttpMethod(method)) continue;
997
+
998
+ exports.push({
999
+ method,
1000
+ handlerFile,
1001
+ contractRef: routePath,
1002
+ catchAllPrefix: catchAllRoutePrefix(routePath),
1003
+ source: "next-route",
1004
+ });
1005
+ }
1006
+ }
989
1007
 
990
1008
  for (const match of source.matchAll(exportRegex)) {
991
1009
  const method = match[1] as HttpMethod;
@@ -1384,7 +1402,7 @@ async function inspectDiagnostics(
1384
1402
  convention,
1385
1403
  contracts,
1386
1404
  )),
1387
- ...(await inspectServerlessFootguns(targetDir, files)),
1405
+ ...(await inspectServerlessFootguns(targetDir, files, config, convention)),
1388
1406
  ...(await inspectProductionReadiness(targetDir, files, config, convention)),
1389
1407
  ...(await inspectFeatureArtifactDrift(
1390
1408
  targetDir,
@@ -3773,6 +3791,8 @@ function isWorkflowTestFile(file: string): boolean {
3773
3791
  async function inspectServerlessFootguns(
3774
3792
  targetDir: string,
3775
3793
  files: string[],
3794
+ config: ResolvedBeignetConfig,
3795
+ convention: InspectConvention,
3776
3796
  ): Promise<InspectDiagnostic[]> {
3777
3797
  const diagnostics: InspectDiagnostic[] = [];
3778
3798
 
@@ -3792,6 +3812,19 @@ async function inspectServerlessFootguns(
3792
3812
  /(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
3793
3813
  const source = await readFile(path.join(targetDir, file), "utf8");
3794
3814
 
3815
+ if (
3816
+ convention.resourceGenerator &&
3817
+ isNextRouteFile(file, config) &&
3818
+ importsEagerServer(source, file, files, config)
3819
+ ) {
3820
+ diagnostics.push({
3821
+ severity: "warning",
3822
+ code: "BEIGNET_NEXT_ROUTE_EAGER_SERVER_IMPORT",
3823
+ file,
3824
+ 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.`,
3825
+ });
3826
+ }
3827
+
3795
3828
  if (
3796
3829
  isInfraOrServerFile &&
3797
3830
  isProviderFile &&
@@ -3850,6 +3883,37 @@ async function inspectServerlessFootguns(
3850
3883
  return diagnostics;
3851
3884
  }
3852
3885
 
3886
+ function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
3887
+ const routesPath = directoryPath(config.paths.routes);
3888
+
3889
+ return (
3890
+ file.startsWith(`${routesPath}/`) &&
3891
+ file.endsWith("/route.ts") &&
3892
+ !file.endsWith(".test.ts")
3893
+ );
3894
+ }
3895
+
3896
+ function importsEagerServer(
3897
+ source: string,
3898
+ importerFile: string,
3899
+ files: string[],
3900
+ config: ResolvedBeignetConfig,
3901
+ ): boolean {
3902
+ const importRegex = /import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
3903
+
3904
+ for (const match of source.matchAll(importRegex)) {
3905
+ const members = match[1]
3906
+ .split(",")
3907
+ .map((member) => parseImportMember(member));
3908
+ if (!members.some((member) => member?.importedName === "server")) continue;
3909
+
3910
+ const sourceFile = sourceFileFromImport(match[2], importerFile, files);
3911
+ if (sourceFile === config.paths.server) return true;
3912
+ }
3913
+
3914
+ return false;
3915
+ }
3916
+
3853
3917
  function hasOutboxRegistry(files: string[]): boolean {
3854
3918
  return files.some(isOutboxRegistryFile);
3855
3919
  }
@@ -4076,7 +4140,9 @@ async function inspectDatabaseLifecycleDrift(
4076
4140
  {
4077
4141
  script: "db:seed",
4078
4142
  command: "seed",
4079
- file: `${infrastructurePath}/db/seed.ts`,
4143
+ // The seed entrypoint lives beside the server runtime because it boots
4144
+ // the app through getServer(), which infra/ must not import.
4145
+ file: `${directoryPath(path.dirname(config.paths.server))}/seed.ts`,
4080
4146
  code: "BEIGNET_DB_SEED_ENTRYPOINT_MISSING",
4081
4147
  },
4082
4148
  {