@beignet/cli 0.0.38 → 0.0.40

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 (92) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +154 -102
  3. package/dist/choices.d.ts +17 -21
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +21 -67
  6. package/dist/choices.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/create-prompts.d.ts +5 -6
  12. package/dist/create-prompts.d.ts.map +1 -1
  13. package/dist/create-prompts.js +15 -15
  14. package/dist/create-prompts.js.map +1 -1
  15. package/dist/create.d.ts +11 -2
  16. package/dist/create.d.ts.map +1 -1
  17. package/dist/create.js +38 -28
  18. package/dist/create.js.map +1 -1
  19. package/dist/framework.d.ts +3 -0
  20. package/dist/framework.d.ts.map +1 -0
  21. package/dist/framework.js +6 -0
  22. package/dist/framework.js.map +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +149 -47
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +4 -2
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +174 -34
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/lib.d.ts +1 -1
  31. package/dist/lib.d.ts.map +1 -1
  32. package/dist/make/inbox.js +3 -3
  33. package/dist/make/inbox.js.map +1 -1
  34. package/dist/make/payments.d.ts.map +1 -1
  35. package/dist/make/payments.js +5 -3
  36. package/dist/make/payments.js.map +1 -1
  37. package/dist/make/shared.d.ts +10 -3
  38. package/dist/make/shared.d.ts.map +1 -1
  39. package/dist/make/shared.js +7 -8
  40. package/dist/make/shared.js.map +1 -1
  41. package/dist/make.d.ts +3 -2
  42. package/dist/make.d.ts.map +1 -1
  43. package/dist/make.js +549 -186
  44. package/dist/make.js.map +1 -1
  45. package/dist/mcp.d.ts.map +1 -1
  46. package/dist/mcp.js +15 -11
  47. package/dist/mcp.js.map +1 -1
  48. package/dist/provider-add.d.ts +21 -3
  49. package/dist/provider-add.d.ts.map +1 -1
  50. package/dist/provider-add.js +101 -65
  51. package/dist/provider-add.js.map +1 -1
  52. package/dist/task.js +1 -1
  53. package/dist/task.js.map +1 -1
  54. package/dist/templates/base.d.ts +1 -1
  55. package/dist/templates/base.d.ts.map +1 -1
  56. package/dist/templates/base.js +15 -15
  57. package/dist/templates/base.js.map +1 -1
  58. package/dist/templates/index.d.ts +2 -2
  59. package/dist/templates/index.d.ts.map +1 -1
  60. package/dist/templates/index.js +5 -5
  61. package/dist/templates/index.js.map +1 -1
  62. package/dist/templates/server.d.ts.map +1 -1
  63. package/dist/templates/server.js +17 -13
  64. package/dist/templates/server.js.map +1 -1
  65. package/dist/templates/shadcn.js +1 -1
  66. package/dist/templates/shared.d.ts +4 -4
  67. package/dist/templates/shared.d.ts.map +1 -1
  68. package/dist/templates/shared.js +6 -6
  69. package/dist/templates/shared.js.map +1 -1
  70. package/package.json +2 -2
  71. package/skills/app-structure/SKILL.md +29 -9
  72. package/src/choices.ts +38 -88
  73. package/src/config.ts +7 -3
  74. package/src/create-prompts.ts +20 -21
  75. package/src/create.ts +54 -36
  76. package/src/framework.ts +13 -0
  77. package/src/index.ts +193 -65
  78. package/src/inspect.ts +290 -39
  79. package/src/lib.ts +1 -1
  80. package/src/make/inbox.ts +3 -3
  81. package/src/make/payments.ts +10 -3
  82. package/src/make/shared.ts +16 -10
  83. package/src/make.ts +720 -176
  84. package/src/mcp.ts +20 -13
  85. package/src/provider-add.ts +159 -79
  86. package/src/task.ts +1 -1
  87. package/src/templates/base.ts +16 -16
  88. package/src/templates/index.ts +6 -6
  89. package/src/templates/server.ts +17 -13
  90. package/src/templates/shadcn.ts +1 -1
  91. package/src/templates/shared.ts +10 -10
  92. package/src/test-helpers/generated-app.ts +2 -1
package/src/mcp.ts CHANGED
@@ -78,13 +78,13 @@ const makeInputSchema = {
78
78
  .string()
79
79
  .optional()
80
80
  .describe(
81
- "Artifact name. Required except for inbox, outbox, payments, and tenancy. Feature-owned artifacts use feature-scoped paths such as posts/backfill.",
81
+ "Artifact name. Required except for inbox, outbox, payments, and tenancy. Feature-owned artifacts use identities such as posts.backfill.",
82
82
  ),
83
83
  with: z
84
84
  .array(z.enum(makeFeatureAddonChoices))
85
85
  .optional()
86
86
  .describe(
87
- "feature only: add feature-owned artifacts such as policy, factories, seeds, events, listeners, jobs, notifications, schedules, tasks, ui, and uploads.",
87
+ "feature only: add feature-owned artifacts such as policy, factory, seed, event, listener, job, notification, schedule, task, ui, and upload.",
88
88
  ),
89
89
  recipe: z
90
90
  .enum(makeFeatureRecipeChoices)
@@ -94,7 +94,7 @@ const makeInputSchema = {
94
94
  .string()
95
95
  .optional()
96
96
  .describe(
97
- "listener only (required): event to listen to, for example posts/published.",
97
+ "listener only (required): event to listen to, for example posts.published.",
98
98
  ),
99
99
  cron: z
100
100
  .string()
@@ -108,13 +108,19 @@ const makeInputSchema = {
108
108
  .boolean()
109
109
  .optional()
110
110
  .describe("schedule only: generate a Next.js cron route for the schedule."),
111
- auth: z
111
+ ui: z
112
+ .boolean()
113
+ .optional()
114
+ .describe(
115
+ "upload only: generate a typed React upload client and component.",
116
+ ),
117
+ authorization: z
112
118
  .boolean()
113
119
  .optional()
114
120
  .describe(
115
121
  "resource only: generate policy metadata and use-case authorization checks for mutating routes.",
116
122
  ),
117
- tenant: z
123
+ tenantScoped: z
118
124
  .boolean()
119
125
  .optional()
120
126
  .describe(
@@ -163,8 +169,9 @@ type MakeToolInput = {
163
169
  cron?: string;
164
170
  timezone?: string;
165
171
  route?: boolean;
166
- auth?: boolean;
167
- tenant?: boolean;
172
+ ui?: boolean;
173
+ authorization?: boolean;
174
+ tenantScoped?: boolean;
168
175
  events?: boolean;
169
176
  softDelete?: boolean;
170
177
  force?: boolean;
@@ -268,7 +275,7 @@ async function runMakeTool(
268
275
  case "listener":
269
276
  if (!input.event) {
270
277
  throw new Error(
271
- "make listener requires an event, for example event: posts/published.",
278
+ "make listener requires an event, for example event: posts.published.",
272
279
  );
273
280
  }
274
281
  return makeListener({ ...base, event: input.event });
@@ -281,8 +288,8 @@ async function runMakeTool(
281
288
  case "resource":
282
289
  return makeResource({
283
290
  ...base,
284
- auth: input.auth,
285
- tenant: input.tenant,
291
+ authorization: input.authorization,
292
+ tenantScoped: input.tenantScoped,
286
293
  events: input.events,
287
294
  softDelete: input.softDelete,
288
295
  });
@@ -300,7 +307,7 @@ async function runMakeTool(
300
307
  case "test":
301
308
  return makeTest(base);
302
309
  case "upload":
303
- return makeUpload(base);
310
+ return makeUpload({ ...base, ui: input.ui });
304
311
  case "use-case":
305
312
  return makeUseCase(base);
306
313
  }
@@ -418,7 +425,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
418
425
  "make",
419
426
  {
420
427
  description:
421
- "Generate Beignet app files for one artifact: adapter, contract, event, factory, feature, inbox, job, listener, notification, outbox, payments, policy, port, resource, schedule, seed, task, tenancy, test, upload, or use-case. Generators auto-register artifacts, so doctor stays clean. Options by artifact: feature accepts with (addons); resource accepts auth, tenant, events, softDelete; listener requires event; schedule accepts cron, timezone, route. All artifacts accept force and dryRun. Returns created, updated, and skipped files as JSON.",
428
+ "Generate Beignet app files for one artifact: adapter, contract, event, factory, feature, inbox, job, listener, notification, outbox, payments, policy, port, resource, schedule, seed, task, tenancy, test, upload, or use-case. Generators auto-register artifacts, so doctor stays clean. Options by artifact: feature accepts with (addons); resource accepts authorization, tenantScoped, events, softDelete; listener requires event; schedule accepts cron, timezone, route; upload accepts ui. All artifacts accept force and dryRun. Returns created, updated, and skipped files as JSON.",
422
429
  inputSchema: makeInputSchema,
423
430
  },
424
431
  async (input) =>
@@ -430,7 +437,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
430
437
  server.registerTool(
431
438
  "provider_add",
432
439
  {
433
- description: `Add a Beignet provider setup preset to this app. Updates dependencies, provider registration, app port wiring, .env.example, and docs/integrations.md. Presets: ${providerPresetDescription}. Supports dryRun.`,
440
+ description: `Add a Beignet provider setup preset to this app. Updates dependencies, provider registration, app port wiring, .env.example, and docs/providers.md. Presets: ${providerPresetDescription}. Supports dryRun.`,
434
441
  inputSchema: providerAddInputSchema,
435
442
  },
436
443
  async (input) =>
@@ -1,9 +1,9 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import {
4
- type IntegrationName,
5
4
  type ProviderPresetName,
6
5
  providerPresetChoices,
6
+ type StarterProviderName,
7
7
  } from "./choices.js";
8
8
  import {
9
9
  directoryPath,
@@ -72,6 +72,18 @@ type PlannedFile = {
72
72
  result: "created" | "updated" | "skipped";
73
73
  };
74
74
 
75
+ type ProviderPlanFile = {
76
+ path: string;
77
+ content: string;
78
+ };
79
+
80
+ type ProviderSourceReader = (filePath: string) => Promise<string | undefined>;
81
+
82
+ export type CreateProviderPresetPlan = {
83
+ files: ProviderPlanFile[];
84
+ presets: AddProviderPresetResult[];
85
+ };
86
+
75
87
  const presets: Record<ProviderPresetName, ProviderPreset> = {
76
88
  "flags-openfeature": {
77
89
  name: "flags-openfeature",
@@ -105,7 +117,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
105
117
  nextSteps: [
106
118
  "Run your package manager install command.",
107
119
  "Configure an OpenFeature provider before relying on non-default flag values.",
108
- "Run beignet providers audit and beignet doctor --strict.",
120
+ "Run beignet provider audit and beignet doctor --strict.",
109
121
  ],
110
122
  },
111
123
  "jobs-bullmq": {
@@ -148,7 +160,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
148
160
  "Run your package manager install command.",
149
161
  "Set BULLMQ_REDIS_URL in .env.local or your deployment environment.",
150
162
  "Add a worker entrypoint that runs createBullMQJobWorker(...) in a worker process; dispatched jobs do not execute without one. See the jobs docs page.",
151
- "Run beignet providers audit and beignet doctor --strict.",
163
+ "Run beignet provider audit and beignet doctor --strict.",
152
164
  ],
153
165
  conflictingProviderEntries: ["createInngestJobsProvider"],
154
166
  },
@@ -190,7 +202,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
190
202
  "Run your package manager install command.",
191
203
  "Set INNGEST_APP_NAME and, for Inngest cloud, INNGEST_EVENT_KEY in .env.local or your deployment environment.",
192
204
  "Expose an Inngest serve route with createInngestJobFunction(...) so dispatched jobs execute. See the jobs docs page.",
193
- "Run beignet providers audit and beignet doctor --strict.",
205
+ "Run beignet provider audit and beignet doctor --strict.",
194
206
  ],
195
207
  conflictingProviderEntries: ["createBullMQJobsProvider"],
196
208
  },
@@ -230,7 +242,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
230
242
  nextSteps: [
231
243
  "Run your package manager install command.",
232
244
  "Set RESEND_API_KEY and RESEND_FROM in .env.local or your deployment environment.",
233
- "Run beignet providers audit and beignet doctor --strict.",
245
+ "Run beignet provider audit and beignet doctor --strict.",
234
246
  ],
235
247
  conflictingProviderEntries: [
236
248
  "createSmtpMailProvider",
@@ -276,7 +288,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
276
288
  nextSteps: [
277
289
  "Run your package manager install command.",
278
290
  "Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment.",
279
- "Run beignet providers audit and beignet doctor --strict.",
291
+ "Run beignet provider audit and beignet doctor --strict.",
280
292
  ],
281
293
  conflictingProviderEntries: [
282
294
  "createResendMailProvider",
@@ -327,7 +339,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
327
339
  "Run your package manager install command.",
328
340
  "Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in .env.local or your deployment environment.",
329
341
  "Run beignet make payments for the full billing slice; Stripe needs its webhook route before subscription state can sync.",
330
- "Run beignet providers audit and beignet doctor --strict.",
342
+ "Run beignet provider audit and beignet doctor --strict.",
331
343
  ],
332
344
  conflictingProviderEntries: ["createMemoryPaymentsProvider"],
333
345
  },
@@ -368,11 +380,11 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
368
380
  nextSteps: [
369
381
  "Run your package manager install command.",
370
382
  "Set MEILISEARCH_HOST and any needed MEILISEARCH_* values in .env.local or your deployment environment.",
371
- "Run beignet providers audit and beignet doctor --strict.",
383
+ "Run beignet provider audit and beignet doctor --strict.",
372
384
  ],
373
385
  },
374
- sentry: {
375
- name: "sentry",
386
+ "error-reporting-sentry": {
387
+ name: "error-reporting-sentry",
376
388
  displayName: "Sentry error reporting",
377
389
  importName: "createSentryErrorReportingProvider",
378
390
  importFrom: "@beignet/provider-error-reporting-sentry",
@@ -403,11 +415,11 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
403
415
  nextSteps: [
404
416
  "Run your package manager install command.",
405
417
  "Set SENTRY_DSN in .env.local or your deployment environment.",
406
- "Run beignet providers audit and beignet doctor --strict.",
418
+ "Run beignet provider audit and beignet doctor --strict.",
407
419
  ],
408
420
  },
409
- "redis-cache": {
410
- name: "redis-cache",
421
+ "cache-redis": {
422
+ name: "cache-redis",
411
423
  displayName: "Redis cache",
412
424
  importName: "createRedisCacheProvider",
413
425
  importFrom: "@beignet/provider-cache-redis",
@@ -442,7 +454,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
442
454
  nextSteps: [
443
455
  "Run your package manager install command.",
444
456
  "Set REDIS_URL in .env.local or your deployment environment.",
445
- "Run beignet providers audit and beignet doctor --strict.",
457
+ "Run beignet provider audit and beignet doctor --strict.",
446
458
  ],
447
459
  },
448
460
  "event-bus-redis": {
@@ -485,12 +497,12 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
485
497
  "Run your package manager install command.",
486
498
  "Set REDIS_EVENT_BUS_URL in .env.local or your deployment environment.",
487
499
  "Keep durable side effects on the outbox/jobs path; Redis Pub/Sub does not replay missed events.",
488
- "Run beignet providers audit and beignet doctor --strict.",
500
+ "Run beignet provider audit and beignet doctor --strict.",
489
501
  ],
490
502
  conflictingProviderEntries: ["createMemoryEventBusProvider"],
491
503
  },
492
- "redis-locks": {
493
- name: "redis-locks",
504
+ "locks-redis": {
505
+ name: "locks-redis",
494
506
  displayName: "Redis locks",
495
507
  importName: "createRedisLocksProvider",
496
508
  importFrom: "@beignet/provider-locks-redis",
@@ -526,11 +538,11 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
526
538
  nextSteps: [
527
539
  "Run your package manager install command.",
528
540
  "Set REDIS_LOCKS_URL in .env.local or your deployment environment.",
529
- "Run beignet providers audit and beignet doctor --strict.",
541
+ "Run beignet provider audit and beignet doctor --strict.",
530
542
  ],
531
543
  },
532
- "s3-storage": {
533
- name: "s3-storage",
544
+ "storage-s3": {
545
+ name: "storage-s3",
534
546
  displayName: "S3-compatible storage",
535
547
  importName: "createS3StorageProvider",
536
548
  importFrom: "@beignet/provider-storage-s3",
@@ -587,15 +599,15 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
587
599
  nextSteps: [
588
600
  "Run your package manager install command.",
589
601
  "Set STORAGE_S3_BUCKET and any needed STORAGE_S3_* credentials in .env.local or your deployment environment.",
590
- "Run beignet providers audit and beignet doctor --strict.",
602
+ "Run beignet provider audit and beignet doctor --strict.",
591
603
  ],
592
604
  conflictingProviderEntries: [
593
605
  "createLocalStorageProvider",
594
606
  "createVercelBlobStorageProvider",
595
607
  ],
596
608
  },
597
- "vercel-blob-storage": {
598
- name: "vercel-blob-storage",
609
+ "storage-vercel-blob": {
610
+ name: "storage-vercel-blob",
599
611
  displayName: "Vercel Blob storage",
600
612
  importName: "createVercelBlobStorageProvider",
601
613
  importFrom: "@beignet/provider-storage-vercel-blob",
@@ -634,15 +646,15 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
634
646
  nextSteps: [
635
647
  "Run your package manager install command.",
636
648
  "Connect a Vercel Blob store to the project (or set BLOB_READ_WRITE_TOKEN manually).",
637
- "Run beignet providers audit and beignet doctor --strict.",
649
+ "Run beignet provider audit and beignet doctor --strict.",
638
650
  ],
639
651
  conflictingProviderEntries: [
640
652
  "createLocalStorageProvider",
641
653
  "createS3StorageProvider",
642
654
  ],
643
655
  },
644
- "upstash-rate-limit": {
645
- name: "upstash-rate-limit",
656
+ "rate-limit-upstash": {
657
+ name: "rate-limit-upstash",
646
658
  displayName: "Upstash rate limit",
647
659
  importName: "createUpstashRateLimitProvider",
648
660
  importFrom: "@beignet/provider-rate-limit-upstash",
@@ -681,7 +693,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
681
693
  nextSteps: [
682
694
  "Run your package manager install command.",
683
695
  "Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in .env.local or your deployment environment.",
684
- "Run beignet providers audit and beignet doctor --strict.",
696
+ "Run beignet provider audit and beignet doctor --strict.",
685
697
  ],
686
698
  },
687
699
  };
@@ -690,12 +702,12 @@ const providerPresetSet = new Set<string>(providerPresetChoices);
690
702
 
691
703
  /**
692
704
  * Provider entries the starter template wires for each create-time
693
- * integration, used to pre-validate preset selections at create time.
705
+ * provider, used to pre-validate preset selections at create time.
694
706
  */
695
- const integrationProviderEntries: Record<IntegrationName, string> = {
696
- inngest: "createInngestJobsProvider",
697
- resend: "createResendMailProvider",
698
- "upstash-rate-limit": "createUpstashRateLimitProvider",
707
+ const starterProviderEntries: Record<StarterProviderName, string> = {
708
+ "jobs-inngest": "createInngestJobsProvider",
709
+ "mail-resend": "createResendMailProvider",
710
+ "rate-limit-upstash": "createUpstashRateLimitProvider",
699
711
  };
700
712
 
701
713
  /**
@@ -707,15 +719,18 @@ export function providerPresetNeedsEnv(name: ProviderPresetName): boolean {
707
719
 
708
720
  /**
709
721
  * Fail fast when a create-time selection combines presets or template
710
- * integrations that fill the same app port, before any files are written.
722
+ * starter providers that fill the same app port, before any files are written.
711
723
  */
712
724
  export function assertCompatibleCreatePresets(
713
725
  presetNames: readonly ProviderPresetName[],
714
- integrations: readonly IntegrationName[],
726
+ starterProviders: readonly StarterProviderName[],
715
727
  ): void {
716
728
  const selectedEntries = new Map<string, string>();
717
- for (const integration of integrations) {
718
- selectedEntries.set(integrationProviderEntries[integration], integration);
729
+ for (const starterProvider of starterProviders) {
730
+ selectedEntries.set(
731
+ starterProviderEntries[starterProvider],
732
+ starterProvider,
733
+ );
719
734
  }
720
735
  for (const name of presetNames) {
721
736
  selectedEntries.set(presets[name].importName, name);
@@ -726,7 +741,7 @@ export function assertCompatibleCreatePresets(
726
741
  const other = selectedEntries.get(conflict);
727
742
  if (other !== undefined && other !== name) {
728
743
  throw new Error(
729
- `--integrations selections "${name}" and "${other}" fill the same app port. Pick one of them.`,
744
+ `--providers selections "${name}" and "${other}" fill the same app port. Pick one of them.`,
730
745
  );
731
746
  }
732
747
  }
@@ -747,18 +762,85 @@ export async function addProviderPreset(
747
762
  const preset = presets[options.name as ProviderPresetName];
748
763
  const targetDir = path.resolve(options.cwd ?? process.cwd());
749
764
  const config = await loadBeignetConfig(targetDir);
750
- const packageJsonPath = "package.json";
751
- const providersPath = serverProvidersPath(config);
752
- const integrationsPath = "docs/integrations.md";
765
+ const readSource: ProviderSourceReader = (filePath) =>
766
+ readOptionalAppFile(targetDir, filePath);
767
+ const filesToPlan = await providerPresetFiles(preset, config, readSource);
768
+ const plannedFiles = await planFiles(filesToPlan, readSource);
753
769
 
754
- const packageJsonSource = await readRequiredAppFile(
770
+ if (!options.dryRun) {
771
+ for (const file of plannedFiles) {
772
+ await writePlannedFile(targetDir, file);
773
+ }
774
+ }
775
+
776
+ return providerPresetResult(
777
+ preset,
755
778
  targetDir,
779
+ Boolean(options.dryRun),
780
+ plannedFiles,
781
+ );
782
+ }
783
+
784
+ /**
785
+ * Apply provider presets to an in-memory fresh scaffold. Each preset reads the
786
+ * previous preset's output so dry runs model the real sequential create path.
787
+ */
788
+ export async function planProviderPresetsForCreate(options: {
789
+ targetDir: string;
790
+ files: readonly ProviderPlanFile[];
791
+ names: readonly ProviderPresetName[];
792
+ }): Promise<CreateProviderPresetPlan> {
793
+ const sources = new Map(
794
+ options.files.map((file) => [file.path, file.content] as const),
795
+ );
796
+ const config = await loadBeignetConfig(options.targetDir);
797
+ const presetResults: AddProviderPresetResult[] = [];
798
+ const readSource: ProviderSourceReader = async (filePath) => {
799
+ const generatedSource = sources.get(filePath);
800
+ return generatedSource ?? readOptionalAppFile(options.targetDir, filePath);
801
+ };
802
+
803
+ for (const name of options.names) {
804
+ const preset = presets[name];
805
+ const filesToPlan = await providerPresetFiles(preset, config, readSource);
806
+ const plannedFiles = await planFiles(filesToPlan, readSource);
807
+
808
+ for (const file of plannedFiles) {
809
+ sources.set(file.path, file.content);
810
+ }
811
+
812
+ presetResults.push(
813
+ providerPresetResult(preset, options.targetDir, true, plannedFiles),
814
+ );
815
+ }
816
+
817
+ return {
818
+ files: [...sources].map(([filePath, content]) => ({
819
+ path: filePath,
820
+ content,
821
+ })),
822
+ presets: presetResults,
823
+ };
824
+ }
825
+
826
+ async function providerPresetFiles(
827
+ preset: ProviderPreset,
828
+ config: ResolvedBeignetConfig,
829
+ readSource: ProviderSourceReader,
830
+ ): Promise<ProviderPlanFile[]> {
831
+ const packageJsonPath = "package.json";
832
+ const providersPath = serverProvidersPath(config);
833
+ const providersDocsPath = "docs/providers.md";
834
+ const packageJsonSource = await readRequiredSource(
835
+ readSource,
756
836
  packageJsonPath,
757
837
  );
758
838
  const packageJson = parsePackageJson(packageJsonSource);
759
- const envExampleSource = await readOptionalAppFile(targetDir, ".env.example");
760
- const envExampleContent = updateEnvExample(envExampleSource, preset);
761
- const filesToPlan = [
839
+ const envExampleContent = updateEnvExample(
840
+ await readSource(".env.example"),
841
+ preset,
842
+ );
843
+ const files = [
762
844
  {
763
845
  path: packageJsonPath,
764
846
  content: updatePackageJson(packageJsonSource, packageJson, preset),
@@ -766,53 +848,51 @@ export async function addProviderPreset(
766
848
  {
767
849
  path: providersPath,
768
850
  content: updateServerProviders(
769
- await readRequiredAppFile(targetDir, providersPath),
851
+ await readRequiredSource(readSource, providersPath),
770
852
  preset,
771
853
  ),
772
854
  },
773
855
  {
774
856
  path: config.paths.ports,
775
857
  content: updatePortsFile(
776
- await readRequiredAppFile(targetDir, config.paths.ports),
858
+ await readRequiredSource(readSource, config.paths.ports),
777
859
  preset.appPort,
778
860
  ),
779
861
  },
780
862
  {
781
863
  path: config.paths.portWiring,
782
864
  content: updateInfrastructurePortsFile(
783
- await readRequiredAppFile(targetDir, config.paths.portWiring),
865
+ await readRequiredSource(readSource, config.paths.portWiring),
784
866
  preset.appPort.key,
785
867
  ),
786
868
  },
787
869
  {
788
- path: integrationsPath,
789
- content: updateIntegrationsDoc(
790
- await readOptionalAppFile(targetDir, integrationsPath),
791
- preset,
792
- ),
870
+ path: providersDocsPath,
871
+ content: updateProvidersDoc(await readSource(providersDocsPath), preset),
793
872
  },
794
873
  ];
795
874
 
796
875
  if (envExampleContent !== undefined) {
797
- filesToPlan.splice(filesToPlan.length - 1, 0, {
876
+ files.splice(files.length - 1, 0, {
798
877
  path: ".env.example",
799
878
  content: envExampleContent,
800
879
  });
801
880
  }
802
881
 
803
- const plannedFiles = await planFiles(targetDir, filesToPlan);
804
-
805
- if (!options.dryRun) {
806
- for (const file of plannedFiles) {
807
- await writePlannedFile(targetDir, file);
808
- }
809
- }
882
+ return files;
883
+ }
810
884
 
885
+ function providerPresetResult(
886
+ preset: ProviderPreset,
887
+ targetDir: string,
888
+ dryRun: boolean,
889
+ plannedFiles: readonly PlannedFile[],
890
+ ): AddProviderPresetResult {
811
891
  return {
812
892
  schemaVersion: 1,
813
893
  name: preset.name,
814
894
  targetDir,
815
- dryRun: Boolean(options.dryRun),
895
+ dryRun,
816
896
  files: plannedFiles.map((file) => file.path),
817
897
  createdFiles: plannedFiles
818
898
  .filter((file) => file.result === "created")
@@ -827,6 +907,19 @@ export async function addProviderPreset(
827
907
  };
828
908
  }
829
909
 
910
+ async function readRequiredSource(
911
+ readSource: ProviderSourceReader,
912
+ filePath: string,
913
+ ): Promise<string> {
914
+ const content = await readSource(filePath);
915
+ if (content === undefined) {
916
+ throw new Error(
917
+ `Could not find ${filePath}. Run this command in a Beignet app.`,
918
+ );
919
+ }
920
+ return content;
921
+ }
922
+
830
923
  function beignetDependencyVersion(packageJson: PackageJson): string {
831
924
  return (
832
925
  packageJson.dependencies?.["@beignet/core"] ??
@@ -959,14 +1052,14 @@ function updateEnvExample(
959
1052
  return appendSection(current, preset.envExample);
960
1053
  }
961
1054
 
962
- function updateIntegrationsDoc(
1055
+ function updateProvidersDoc(
963
1056
  source: string | undefined,
964
1057
  preset: ProviderPreset,
965
1058
  ): string {
966
1059
  const current =
967
1060
  source ??
968
1061
  [
969
- "# Integrations",
1062
+ "# Providers",
970
1063
  "",
971
1064
  "This file tracks app-level provider setup added after the initial scaffold.",
972
1065
  "",
@@ -1144,13 +1237,13 @@ function sortRecord(record: Record<string, string>): Record<string, string> {
1144
1237
  }
1145
1238
 
1146
1239
  async function planFiles(
1147
- targetDir: string,
1148
- files: Array<{ path: string; content: string }>,
1240
+ files: ProviderPlanFile[],
1241
+ readSource: ProviderSourceReader,
1149
1242
  ): Promise<PlannedFile[]> {
1150
1243
  const planned: PlannedFile[] = [];
1151
1244
 
1152
1245
  for (const file of files) {
1153
- const existing = await readOptionalAppFile(targetDir, file.path);
1246
+ const existing = await readSource(file.path);
1154
1247
  planned.push({
1155
1248
  ...file,
1156
1249
  result:
@@ -1176,19 +1269,6 @@ async function writePlannedFile(
1176
1269
  await writeFile(destination, file.content);
1177
1270
  }
1178
1271
 
1179
- async function readRequiredAppFile(
1180
- targetDir: string,
1181
- filePath: string,
1182
- ): Promise<string> {
1183
- const content = await readOptionalAppFile(targetDir, filePath);
1184
- if (content === undefined) {
1185
- throw new Error(
1186
- `Could not find ${filePath}. Run this command in a Beignet app.`,
1187
- );
1188
- }
1189
- return content;
1190
- }
1191
-
1192
1272
  async function readOptionalAppFile(
1193
1273
  targetDir: string,
1194
1274
  filePath: string,
package/src/task.ts CHANGED
@@ -142,7 +142,7 @@ async function loadTaskModule(
142
142
  await stat(absolutePath);
143
143
  } catch {
144
144
  throw new Error(
145
- `Could not find app task registry at ${modulePath}. Create it with beignet make task <feature>/<name> or configure paths.tasks in beignet.config.ts.`,
145
+ `Could not find app task registry at ${modulePath}. Create it with beignet make task <feature.name> or configure paths.tasks in beignet.config.ts.`,
146
146
  );
147
147
  }
148
148