@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/index.ts CHANGED
@@ -15,12 +15,13 @@ import {
15
15
  } from "@stricli/core";
16
16
  import {
17
17
  type CompletionShell,
18
+ type CreateIntegrationName,
18
19
  completionShellChoices,
20
+ createIntegrationChoices,
19
21
  type DatabaseName,
20
22
  databaseChoices,
21
23
  databaseStartCommand,
22
24
  type IntegrationName,
23
- integrationChoices,
24
25
  type MakeFeatureAddon,
25
26
  type MakeFeatureRecipe,
26
27
  makeFeatureAddonChoices,
@@ -29,6 +30,7 @@ import {
29
30
  type ProviderPresetName,
30
31
  packageManagerChoices,
31
32
  providerPresetChoices,
33
+ splitCreateIntegrations,
32
34
  type TemplateName,
33
35
  templateChoices,
34
36
  } from "./choices.js";
@@ -54,7 +56,7 @@ type CreateFlags = {
54
56
  api?: boolean;
55
57
  db?: DatabaseName;
56
58
  packageManager?: PackageManager;
57
- integrations?: readonly IntegrationName[];
59
+ integrations?: readonly CreateIntegrationName[];
58
60
  yes?: boolean;
59
61
  force?: boolean;
60
62
  dryRun?: boolean;
@@ -108,6 +110,16 @@ type LintFlags = {
108
110
  type CheckFlags = {
109
111
  json?: boolean;
110
112
  fix?: boolean;
113
+ preflight?: boolean;
114
+ cwd?: string;
115
+ };
116
+
117
+ type PreflightFlags = {
118
+ json?: boolean;
119
+ envFile?: string;
120
+ envModule?: string;
121
+ connect?: boolean;
122
+ serverModule?: string;
111
123
  cwd?: string;
112
124
  };
113
125
 
@@ -131,7 +143,7 @@ const databaseSchemaTableChoices = [
131
143
  "outbox",
132
144
  ] as const satisfies readonly DatabaseSchemaTable[];
133
145
 
134
- type DbSchemaGenerateFlags = DbFlags & {
146
+ type DbSchemaSyncFlags = DbFlags & {
135
147
  dialect?: DatabaseSchemaDialect;
136
148
  tables?: readonly DatabaseSchemaTable[];
137
149
  output?: string;
@@ -150,6 +162,60 @@ type OutboxDrainFlags = {
150
162
  batchSize?: number;
151
163
  };
152
164
 
165
+ type OutboxMessageStatus = "pending" | "claimed" | "delivered" | "deadLettered";
166
+
167
+ type OutboxMessageKind = "event" | "job";
168
+
169
+ const outboxMessageStatusChoices = [
170
+ "pending",
171
+ "claimed",
172
+ "delivered",
173
+ "deadLettered",
174
+ ] as const satisfies readonly OutboxMessageStatus[];
175
+
176
+ const outboxMessageKindChoices = [
177
+ "event",
178
+ "job",
179
+ ] as const satisfies readonly OutboxMessageKind[];
180
+
181
+ type OutboxListFlags = {
182
+ json?: boolean;
183
+ module?: string;
184
+ status?: OutboxMessageStatus;
185
+ kind?: OutboxMessageKind;
186
+ name?: string;
187
+ limit?: number;
188
+ };
189
+
190
+ type OutboxShowFlags = {
191
+ json?: boolean;
192
+ module?: string;
193
+ };
194
+
195
+ type OutboxRequeueFlags = {
196
+ json?: boolean;
197
+ module?: string;
198
+ availableAt?: Date;
199
+ resetAttempts?: boolean;
200
+ };
201
+
202
+ type OutboxPurgeFlags = {
203
+ json?: boolean;
204
+ module?: string;
205
+ before?: Date;
206
+ all?: boolean;
207
+ limit?: number;
208
+ dryRun?: boolean;
209
+ };
210
+
211
+ type OutboxPruneFlags = {
212
+ json?: boolean;
213
+ module?: string;
214
+ before?: Date;
215
+ limit?: number;
216
+ dryRun?: boolean;
217
+ };
218
+
153
219
  type ScheduleRunFlags = {
154
220
  json?: boolean;
155
221
  module?: string;
@@ -203,6 +269,13 @@ const parsePositiveInteger = (input: string): number => {
203
269
  }
204
270
  return value;
205
271
  };
272
+ const parseDate = (input: string): Date => {
273
+ const value = new Date(input);
274
+ if (Number.isNaN(value.getTime())) {
275
+ throw new Error("Expected a valid date.");
276
+ }
277
+ return value;
278
+ };
206
279
 
207
280
  const forceFlag = {
208
281
  kind: "boolean",
@@ -218,6 +291,13 @@ const dryRunFlag = {
218
291
  brief: "Preview generated changes without writing files.",
219
292
  } as const;
220
293
 
294
+ const operationDryRunFlag = {
295
+ kind: "boolean",
296
+ optional: true,
297
+ withNegated: false,
298
+ brief: "Report matching records without deleting them.",
299
+ } as const;
300
+
221
301
  const jsonFlag = {
222
302
  kind: "boolean",
223
303
  optional: true,
@@ -233,6 +313,14 @@ const parsedStringFlag = (brief: string) =>
233
313
  brief,
234
314
  }) as const;
235
315
 
316
+ const parsedDateFlag = (brief: string) =>
317
+ ({
318
+ kind: "parsed",
319
+ parse: parseDate,
320
+ optional: true,
321
+ brief,
322
+ }) as const;
323
+
236
324
  const cwdFlag = parsedStringFlag(
237
325
  "App directory to inspect. Defaults to the current working directory.",
238
326
  );
@@ -278,14 +366,14 @@ const dbFlagParameters = {
278
366
  dryRun: dryRunFlag,
279
367
  } satisfies FlagParametersForType<DbFlags, CliContext>;
280
368
 
281
- const dbSchemaGenerateFlagParameters = {
369
+ const dbSchemaSyncFlagParameters = {
282
370
  ...dbFlagParameters,
283
371
  dialect: {
284
372
  kind: "enum",
285
373
  values: databaseSchemaDialectChoices,
286
374
  optional: true,
287
375
  brief:
288
- "Drizzle dialect to generate for. Inferred from server/providers.ts when omitted.",
376
+ "Drizzle dialect to sync. Inferred from server/providers.ts when omitted.",
289
377
  },
290
378
  tables: {
291
379
  kind: "enum",
@@ -293,12 +381,12 @@ const dbSchemaGenerateFlagParameters = {
293
381
  optional: true,
294
382
  variadic: ",",
295
383
  brief:
296
- "Provider tables to generate as a comma-separated list. Defaults to audit,idempotency,outbox.",
384
+ "Provider tables to sync as a comma-separated list. Defaults to audit,idempotency,outbox.",
297
385
  },
298
386
  output: parsedStringFlag(
299
- "Generated schema file. Defaults to infra/db/schema/beignet.ts.",
387
+ "Synced schema file. Defaults to infra/db/schema/beignet.ts.",
300
388
  ),
301
- } satisfies FlagParametersForType<DbSchemaGenerateFlags, CliContext>;
389
+ } satisfies FlagParametersForType<DbSchemaSyncFlags, CliContext>;
302
390
 
303
391
  const taskRunFlagParameters = {
304
392
  json: jsonFlag,
@@ -324,6 +412,95 @@ const outboxDrainFlagParameters = {
324
412
  },
325
413
  } satisfies FlagParametersForType<OutboxDrainFlags, CliContext>;
326
414
 
415
+ const outboxListFlagParameters = {
416
+ json: jsonFlag,
417
+ module: parsedStringFlag(
418
+ "Outbox registry module. Defaults to server/outbox.ts.",
419
+ ),
420
+ status: {
421
+ kind: "enum",
422
+ values: outboxMessageStatusChoices,
423
+ optional: true,
424
+ brief: "Filter by outbox message status.",
425
+ },
426
+ kind: {
427
+ kind: "enum",
428
+ values: outboxMessageKindChoices,
429
+ optional: true,
430
+ brief: "Filter by outbox message kind.",
431
+ },
432
+ name: parsedStringFlag("Filter by event or job name."),
433
+ limit: {
434
+ kind: "parsed",
435
+ parse: parsePositiveInteger,
436
+ optional: true,
437
+ brief: "Maximum messages to return. Defaults to 50.",
438
+ },
439
+ } satisfies FlagParametersForType<OutboxListFlags, CliContext>;
440
+
441
+ const outboxShowFlagParameters = {
442
+ json: jsonFlag,
443
+ module: parsedStringFlag(
444
+ "Outbox registry module. Defaults to server/outbox.ts.",
445
+ ),
446
+ } satisfies FlagParametersForType<OutboxShowFlags, CliContext>;
447
+
448
+ const outboxRequeueFlagParameters = {
449
+ json: jsonFlag,
450
+ module: parsedStringFlag(
451
+ "Outbox registry module. Defaults to server/outbox.ts.",
452
+ ),
453
+ availableAt: parsedDateFlag(
454
+ "Earliest timestamp the requeued message may be claimed. Defaults to now.",
455
+ ),
456
+ resetAttempts: {
457
+ kind: "boolean",
458
+ optional: true,
459
+ withNegated: false,
460
+ brief: "Reset attempts to zero before requeueing.",
461
+ },
462
+ } satisfies FlagParametersForType<OutboxRequeueFlags, CliContext>;
463
+
464
+ const outboxPurgeFlagParameters = {
465
+ json: jsonFlag,
466
+ module: parsedStringFlag(
467
+ "Outbox registry module. Defaults to server/outbox.ts.",
468
+ ),
469
+ before: parsedDateFlag(
470
+ "Only purge dead-lettered messages last updated before this timestamp.",
471
+ ),
472
+ all: {
473
+ kind: "boolean",
474
+ optional: true,
475
+ withNegated: false,
476
+ brief: "Purge every dead-lettered message when --before is omitted.",
477
+ },
478
+ limit: {
479
+ kind: "parsed",
480
+ parse: parsePositiveInteger,
481
+ optional: true,
482
+ brief: "Maximum messages to purge.",
483
+ },
484
+ dryRun: operationDryRunFlag,
485
+ } satisfies FlagParametersForType<OutboxPurgeFlags, CliContext>;
486
+
487
+ const outboxPruneFlagParameters = {
488
+ json: jsonFlag,
489
+ module: parsedStringFlag(
490
+ "Outbox registry module. Defaults to server/outbox.ts.",
491
+ ),
492
+ before: parsedDateFlag(
493
+ "Prune delivered messages delivered before this timestamp.",
494
+ ),
495
+ limit: {
496
+ kind: "parsed",
497
+ parse: parsePositiveInteger,
498
+ optional: true,
499
+ brief: "Maximum messages to prune.",
500
+ },
501
+ dryRun: operationDryRunFlag,
502
+ } satisfies FlagParametersForType<OutboxPruneFlags, CliContext>;
503
+
327
504
  const scheduleRunFlagParameters = {
328
505
  json: jsonFlag,
329
506
  module: parsedStringFlag(
@@ -353,6 +530,17 @@ const namePositional = {
353
530
  ],
354
531
  } as const;
355
532
 
533
+ const idPositional = {
534
+ kind: "tuple",
535
+ parameters: [
536
+ {
537
+ parse: parseString,
538
+ placeholder: "id",
539
+ brief: "Outbox message ID.",
540
+ },
541
+ ],
542
+ } as const;
543
+
356
544
  const providerPresetPositional = {
357
545
  kind: "tuple",
358
546
  parameters: [
@@ -400,7 +588,11 @@ and a shadcn UI shell. Pass --api for an API-only scaffold without the
400
588
  UI shell, and --db to choose the database.
401
589
 
402
590
  Available databases: ${databaseChoices.join(", ")}
403
- Available integrations: ${integrationChoices.join(", ")}
591
+ Available integrations: ${createIntegrationChoices.join(", ")}
592
+
593
+ --integrations accepts the starter template integrations plus the full
594
+ beignet providers add preset catalog; presets are applied to the scaffold
595
+ with the same machinery as beignet providers add.
404
596
 
405
597
  Running create on an interactive terminal without selection flags opens a
406
598
  prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
@@ -433,10 +625,11 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
433
625
  },
434
626
  integrations: {
435
627
  kind: "enum",
436
- values: integrationChoices,
628
+ values: createIntegrationChoices,
437
629
  optional: true,
438
630
  variadic: ",",
439
- brief: "Add first-party integrations as a comma-separated list.",
631
+ brief:
632
+ "Add first-party integrations or provider presets as a comma-separated list.",
440
633
  },
441
634
  yes: {
442
635
  kind: "boolean",
@@ -504,7 +697,18 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
504
697
 
505
698
  const api = selections.api ?? false;
506
699
  const database = selections.db ?? "sqlite";
507
- const integrations = uniqueValues([...(selections.integrations ?? [])]);
700
+ const { integrations, presets } = splitCreateIntegrations(
701
+ uniqueValues([...(selections.integrations ?? [])]),
702
+ );
703
+
704
+ let presetsNeedEnv = false;
705
+ if (presets.length > 0) {
706
+ const { assertCompatibleCreatePresets, providerPresetNeedsEnv } =
707
+ await import("./provider-add.js");
708
+ assertCompatibleCreatePresets(presets, integrations);
709
+ presetsNeedEnv = presets.some(providerPresetNeedsEnv);
710
+ }
711
+
508
712
  const result = await createProject({
509
713
  name: selections.directory,
510
714
  template: flags.template,
@@ -516,11 +720,24 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
516
720
  dryRun: Boolean(flags.dryRun),
517
721
  });
518
722
 
723
+ if (!flags.dryRun && presets.length > 0) {
724
+ const { addProviderPreset } = await import("./provider-add.js");
725
+ for (const preset of presets) {
726
+ await addProviderPreset({ name: preset, cwd: result.targetDir });
727
+ }
728
+ }
729
+
519
730
  writeOutput(
520
731
  this,
521
732
  flags.json
522
- ? JSON.stringify(result, null, 2)
523
- : nextSteps(result, { api, integrations, database }),
733
+ ? JSON.stringify({ ...result, providerPresets: presets }, null, 2)
734
+ : nextSteps(result, {
735
+ api,
736
+ integrations,
737
+ database,
738
+ presets,
739
+ presetsNeedEnv,
740
+ }),
524
741
  );
525
742
  };
526
743
  },
@@ -622,6 +839,12 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
622
839
  withNegated: false,
623
840
  brief: "Apply low-risk doctor fixes before checking.",
624
841
  },
842
+ preflight: {
843
+ kind: "boolean",
844
+ optional: true,
845
+ withNegated: false,
846
+ brief: "Append a beignet preflight step after doctor.",
847
+ },
625
848
  cwd: cwdFlag,
626
849
  } satisfies FlagParametersForType<CheckFlags, CliContext>,
627
850
  },
@@ -635,6 +858,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
635
858
  const result = await checkApp({
636
859
  cwd: flags.cwd,
637
860
  fix: flags.fix,
861
+ preflight: flags.preflight,
638
862
  color,
639
863
  ...(flags.json
640
864
  ? {}
@@ -656,6 +880,64 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
656
880
  },
657
881
  });
658
882
 
883
+ const preflightCommand = buildCommand<PreflightFlags, [], CliContext>({
884
+ docs: {
885
+ brief: "Validate the deploy environment before shipping.",
886
+ fullDescription:
887
+ "Runtime production gate, unlike the static doctor checks: reads the actual environment the process runs with, verifies every env var installed provider manifests mark as required, flags values still matching .env.example or common placeholders on secret-like keys, and validates the app env schema by importing lib/env.ts. Run it in the deploy pipeline where production configuration is present. Exits 1 on any error finding.",
888
+ },
889
+ parameters: {
890
+ flags: {
891
+ json: jsonFlag,
892
+ envFile: parsedStringFlag(
893
+ "Dotenv-style file merged under the environment (existing env keys win) for local rehearsal.",
894
+ ),
895
+ envModule: parsedStringFlag(
896
+ "App env module validated by importing it. Defaults to lib/env.ts.",
897
+ ),
898
+ connect: {
899
+ kind: "boolean",
900
+ optional: true,
901
+ withNegated: false,
902
+ brief:
903
+ "Boot the app server and run every port's checkHealth(). Needs network and real credentials.",
904
+ },
905
+ serverModule: parsedStringFlag(
906
+ "Module exporting getServer, used by --connect. Defaults to server/index.ts.",
907
+ ),
908
+ cwd: cwdFlag,
909
+ } satisfies FlagParametersForType<PreflightFlags, CliContext>,
910
+ },
911
+ loader: async () => {
912
+ const { formatPreflight, runPreflight } = await import("./preflight.js");
913
+ const path = await import("node:path");
914
+
915
+ return async function runPreflightCommand(
916
+ this: CliContext,
917
+ flags: PreflightFlags,
918
+ ) {
919
+ const cwd = path.resolve(flags.cwd ?? process.cwd());
920
+ const result = await runPreflight({
921
+ cwd,
922
+ envFile: flags.envFile,
923
+ envModule: flags.envModule,
924
+ connect: flags.connect,
925
+ serverModule: flags.serverModule,
926
+ });
927
+
928
+ writeOutput(
929
+ this,
930
+ flags.json
931
+ ? JSON.stringify({ schemaVersion: 1, cwd, ...result }, null, 2)
932
+ : formatPreflight(result, cwd),
933
+ );
934
+ if (result.findings.some((finding) => finding.severity === "error")) {
935
+ this.process.exitCode = 1;
936
+ }
937
+ };
938
+ },
939
+ });
940
+
659
941
  const lintCommand = buildCommand<LintFlags, [], CliContext>({
660
942
  docs: {
661
943
  brief: "Check Beignet dependency direction conventions.",
@@ -819,27 +1101,23 @@ function databaseCommand(command: DatabaseCommandName) {
819
1101
  });
820
1102
  }
821
1103
 
822
- const dbSchemaGenerateCommand = buildCommand<
823
- DbSchemaGenerateFlags,
824
- [],
825
- CliContext
826
- >({
1104
+ const dbSchemaSyncCommand = buildCommand<DbSchemaSyncFlags, [], CliContext>({
827
1105
  docs: {
828
- brief: "Generate provider-owned Drizzle schema exports.",
1106
+ brief: "Sync app-owned re-exports of Beignet provider tables.",
829
1107
  fullDescription:
830
- "Writes an app-owned schema file that re-exports Beignet provider tables, then updates the schema index. Run beignet db generate afterward to let the app's Drizzle Kit script create migrations.",
1108
+ "Idempotently brings the app-owned schema file that re-exports Beignet provider tables in sync with the installed providers, then updates the schema index. Run beignet db generate afterward to let the app's Drizzle Kit script create migrations.",
831
1109
  },
832
1110
  parameters: {
833
- flags: dbSchemaGenerateFlagParameters,
1111
+ flags: dbSchemaSyncFlagParameters,
834
1112
  },
835
1113
  loader: async () => {
836
- const { generateDatabaseSchema } = await import("./db.js");
1114
+ const { syncDatabaseSchema } = await import("./db.js");
837
1115
 
838
- return async function runDbSchemaGenerate(
1116
+ return async function runDbSchemaSync(
839
1117
  this: CliContext,
840
- flags: DbSchemaGenerateFlags,
1118
+ flags: DbSchemaSyncFlags,
841
1119
  ) {
842
- const result = await generateDatabaseSchema({
1120
+ const result = await syncDatabaseSchema({
843
1121
  dialect: flags.dialect,
844
1122
  tables: flags.tables,
845
1123
  output: flags.output,
@@ -850,7 +1128,7 @@ const dbSchemaGenerateCommand = buildCommand<
850
1128
  this,
851
1129
  flags.json
852
1130
  ? JSON.stringify(result, null, 2)
853
- : databaseSchemaGenerateNextSteps(result),
1131
+ : databaseSchemaSyncNextSteps(result),
854
1132
  );
855
1133
  };
856
1134
  },
@@ -858,10 +1136,10 @@ const dbSchemaGenerateCommand = buildCommand<
858
1136
 
859
1137
  const dbSchemaRoutes = buildRouteMap({
860
1138
  docs: {
861
- brief: "Generate Beignet database schema helpers.",
1139
+ brief: "Sync app-owned Beignet provider schema exports.",
862
1140
  },
863
1141
  routes: {
864
- generate: dbSchemaGenerateCommand,
1142
+ sync: dbSchemaSyncCommand,
865
1143
  },
866
1144
  });
867
1145
 
@@ -946,12 +1224,178 @@ const outboxDrainCommand = buildCommand<OutboxDrainFlags, [], CliContext>({
946
1224
  },
947
1225
  });
948
1226
 
1227
+ const outboxListCommand = buildCommand<OutboxListFlags, [], CliContext>({
1228
+ docs: {
1229
+ brief: "List outbox messages for operational inspection.",
1230
+ },
1231
+ parameters: {
1232
+ flags: outboxListFlagParameters,
1233
+ },
1234
+ loader: async () => {
1235
+ const { runOutboxList } = await import("./outbox.js");
1236
+
1237
+ return async function runList(this: CliContext, flags: OutboxListFlags) {
1238
+ const result = await runOutboxList({
1239
+ modulePath: flags.module,
1240
+ status: flags.status,
1241
+ kind: flags.kind,
1242
+ name: flags.name,
1243
+ limit: flags.limit,
1244
+ });
1245
+
1246
+ writeOutput(
1247
+ this,
1248
+ flags.json
1249
+ ? JSON.stringify(result, null, 2)
1250
+ : outboxListNextSteps(result),
1251
+ );
1252
+ };
1253
+ },
1254
+ });
1255
+
1256
+ const outboxShowCommand = buildCommand<OutboxShowFlags, [string], CliContext>({
1257
+ docs: {
1258
+ brief: "Show one outbox message.",
1259
+ },
1260
+ parameters: {
1261
+ flags: outboxShowFlagParameters,
1262
+ positional: idPositional,
1263
+ },
1264
+ loader: async () => {
1265
+ const { runOutboxShow } = await import("./outbox.js");
1266
+
1267
+ return async function runShow(
1268
+ this: CliContext,
1269
+ flags: OutboxShowFlags,
1270
+ id: string,
1271
+ ) {
1272
+ const result = await runOutboxShow({
1273
+ modulePath: flags.module,
1274
+ id,
1275
+ });
1276
+
1277
+ writeOutput(
1278
+ this,
1279
+ flags.json
1280
+ ? JSON.stringify(result, null, 2)
1281
+ : outboxShowNextSteps(result),
1282
+ );
1283
+ };
1284
+ },
1285
+ });
1286
+
1287
+ const outboxRequeueCommand = buildCommand<
1288
+ OutboxRequeueFlags,
1289
+ [string],
1290
+ CliContext
1291
+ >({
1292
+ docs: {
1293
+ brief: "Return one dead-lettered outbox message to pending state.",
1294
+ },
1295
+ parameters: {
1296
+ flags: outboxRequeueFlagParameters,
1297
+ positional: idPositional,
1298
+ },
1299
+ loader: async () => {
1300
+ const { runOutboxRequeue } = await import("./outbox.js");
1301
+
1302
+ return async function runRequeue(
1303
+ this: CliContext,
1304
+ flags: OutboxRequeueFlags,
1305
+ id: string,
1306
+ ) {
1307
+ const result = await runOutboxRequeue({
1308
+ modulePath: flags.module,
1309
+ id,
1310
+ availableAt: flags.availableAt,
1311
+ resetAttempts: Boolean(flags.resetAttempts),
1312
+ });
1313
+
1314
+ writeOutput(
1315
+ this,
1316
+ flags.json
1317
+ ? JSON.stringify(result, null, 2)
1318
+ : outboxRequeueNextSteps(result),
1319
+ );
1320
+ };
1321
+ },
1322
+ });
1323
+
1324
+ const outboxPurgeCommand = buildCommand<OutboxPurgeFlags, [], CliContext>({
1325
+ docs: {
1326
+ brief: "Delete dead-lettered outbox messages.",
1327
+ },
1328
+ parameters: {
1329
+ flags: outboxPurgeFlagParameters,
1330
+ },
1331
+ loader: async () => {
1332
+ const { runOutboxPurge } = await import("./outbox.js");
1333
+
1334
+ return async function runPurge(this: CliContext, flags: OutboxPurgeFlags) {
1335
+ const result = await runOutboxPurge({
1336
+ modulePath: flags.module,
1337
+ before: flags.before,
1338
+ all: Boolean(flags.all),
1339
+ limit: flags.limit,
1340
+ dryRun: Boolean(flags.dryRun),
1341
+ });
1342
+
1343
+ writeOutput(
1344
+ this,
1345
+ flags.json
1346
+ ? JSON.stringify(result, null, 2)
1347
+ : outboxDeleteNextSteps(
1348
+ "Purged dead-lettered outbox messages",
1349
+ result,
1350
+ ),
1351
+ );
1352
+ };
1353
+ },
1354
+ });
1355
+
1356
+ const outboxPruneCommand = buildCommand<OutboxPruneFlags, [], CliContext>({
1357
+ docs: {
1358
+ brief: "Delete delivered outbox messages older than a retention cutoff.",
1359
+ },
1360
+ parameters: {
1361
+ flags: outboxPruneFlagParameters,
1362
+ },
1363
+ loader: async () => {
1364
+ const { runOutboxPrune } = await import("./outbox.js");
1365
+
1366
+ return async function runPrune(this: CliContext, flags: OutboxPruneFlags) {
1367
+ if (!flags.before) {
1368
+ throw new Error("Pass --before <date> to prune delivered messages.");
1369
+ }
1370
+
1371
+ const result = await runOutboxPrune({
1372
+ modulePath: flags.module,
1373
+ before: flags.before,
1374
+ limit: flags.limit,
1375
+ dryRun: Boolean(flags.dryRun),
1376
+ });
1377
+
1378
+ writeOutput(
1379
+ this,
1380
+ flags.json
1381
+ ? JSON.stringify(result, null, 2)
1382
+ : outboxDeleteNextSteps("Pruned delivered outbox messages", result),
1383
+ );
1384
+ };
1385
+ },
1386
+ });
1387
+
949
1388
  const outboxRoutes = buildRouteMap({
950
1389
  docs: {
951
1390
  brief: "Run Beignet outbox operations.",
952
1391
  },
953
1392
  routes: {
954
1393
  drain: outboxDrainCommand,
1394
+ list: outboxListCommand,
1395
+ show: outboxShowCommand,
1396
+ requeue: outboxRequeueCommand,
1397
+ purge: outboxPurgeCommand,
1398
+ prune: outboxPruneCommand,
955
1399
  },
956
1400
  });
957
1401
 
@@ -1557,6 +2001,7 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
1557
2001
  make: makeRoutes,
1558
2002
  mcp: mcpCommand,
1559
2003
  outbox: outboxRoutes,
2004
+ preflight: preflightCommand,
1560
2005
  providers: providerRoutes,
1561
2006
  routes: routesCommand,
1562
2007
  schedule: scheduleRoutes,
@@ -1626,7 +2071,7 @@ type DatabaseCommandNextStepsResult = {
1626
2071
  dryRun: boolean;
1627
2072
  };
1628
2073
 
1629
- type DatabaseSchemaGenerateNextStepsResult = {
2074
+ type DatabaseSchemaSyncNextStepsResult = {
1630
2075
  cwd: string;
1631
2076
  dialect: DatabaseSchemaDialect;
1632
2077
  tables: DatabaseSchemaTable[];
@@ -1655,6 +2100,37 @@ type OutboxDrainNextStepsResult = {
1655
2100
  };
1656
2101
  };
1657
2102
 
2103
+ type OutboxAdminMessage = {
2104
+ id: string;
2105
+ kind: string;
2106
+ name: string;
2107
+ status: string;
2108
+ attempts: number;
2109
+ maxAttempts: number;
2110
+ availableAt: Date | string;
2111
+ deliveredAt: Date | string | null;
2112
+ updatedAt: Date | string;
2113
+ lastError: { name?: string; message: string } | null;
2114
+ };
2115
+
2116
+ type OutboxListNextStepsResult = {
2117
+ durationMs: number;
2118
+ messages: readonly OutboxAdminMessage[];
2119
+ count: number;
2120
+ };
2121
+
2122
+ type OutboxMessageNextStepsResult = {
2123
+ durationMs: number;
2124
+ message: OutboxAdminMessage;
2125
+ };
2126
+
2127
+ type OutboxDeleteNextStepsResult = {
2128
+ durationMs: number;
2129
+ dryRun: boolean;
2130
+ matched: number;
2131
+ deleted: number;
2132
+ };
2133
+
1658
2134
  type ScheduleRunNextStepsResult = {
1659
2135
  name: string;
1660
2136
  durationMs: number;
@@ -1691,6 +2167,69 @@ Result:
1691
2167
  deadLettered: ${result.result.deadLettered}`;
1692
2168
  }
1693
2169
 
2170
+ function formatOutboxDate(value: Date | string | null | undefined): string {
2171
+ if (!value) return "none";
2172
+ return value instanceof Date ? value.toISOString() : value;
2173
+ }
2174
+
2175
+ function outboxMessageLine(message: OutboxAdminMessage): string {
2176
+ return `${message.id} ${message.status} ${message.kind} ${message.name} attempts ${message.attempts}/${message.maxAttempts} updated ${formatOutboxDate(message.updatedAt)}`;
2177
+ }
2178
+
2179
+ function outboxListNextSteps(result: OutboxListNextStepsResult): string {
2180
+ const messages =
2181
+ result.messages.length === 0
2182
+ ? " none"
2183
+ : result.messages
2184
+ .map((message) => ` ${outboxMessageLine(message)}`)
2185
+ .join("\n");
2186
+
2187
+ return `Listed ${result.messages.length} of ${result.count} matching outbox messages in ${result.durationMs}ms
2188
+
2189
+ Messages:
2190
+ ${messages}`;
2191
+ }
2192
+
2193
+ function outboxShowNextSteps(result: OutboxMessageNextStepsResult): string {
2194
+ const message = result.message;
2195
+ const error = message.lastError
2196
+ ? `${message.lastError.name ?? "Error"}: ${message.lastError.message}`
2197
+ : "none";
2198
+
2199
+ return `Outbox message ${message.id} (${message.status}) in ${result.durationMs}ms
2200
+
2201
+ Message:
2202
+ kind: ${message.kind}
2203
+ name: ${message.name}
2204
+ attempts: ${message.attempts}/${message.maxAttempts}
2205
+ availableAt: ${formatOutboxDate(message.availableAt)}
2206
+ deliveredAt: ${formatOutboxDate(message.deliveredAt)}
2207
+ updatedAt: ${formatOutboxDate(message.updatedAt)}
2208
+ lastError: ${error}`;
2209
+ }
2210
+
2211
+ function outboxRequeueNextSteps(result: OutboxMessageNextStepsResult): string {
2212
+ return `Requeued outbox message ${result.message.id} in ${result.durationMs}ms
2213
+
2214
+ Message:
2215
+ status: ${result.message.status}
2216
+ attempts: ${result.message.attempts}/${result.message.maxAttempts}
2217
+ availableAt: ${formatOutboxDate(result.message.availableAt)}`;
2218
+ }
2219
+
2220
+ function outboxDeleteNextSteps(
2221
+ label: string,
2222
+ result: OutboxDeleteNextStepsResult,
2223
+ ): string {
2224
+ const action = result.dryRun ? "Would delete" : "Deleted";
2225
+
2226
+ return `${label} in ${result.durationMs}ms
2227
+
2228
+ Result:
2229
+ matched: ${result.matched}
2230
+ ${action}: ${result.deleted}`;
2231
+ }
2232
+
1694
2233
  function scheduleRunNextSteps(result: ScheduleRunNextStepsResult): string {
1695
2234
  return `Ran schedule ${result.name} in ${result.durationMs}ms
1696
2235
 
@@ -1708,15 +2247,23 @@ function nextSteps(
1708
2247
  api?: boolean;
1709
2248
  integrations?: readonly IntegrationName[];
1710
2249
  database?: DatabaseName;
2250
+ presets?: readonly ProviderPresetName[];
2251
+ presetsNeedEnv?: boolean;
1711
2252
  } = {},
1712
2253
  ): string {
1713
2254
  if (result.dryRun) {
1714
2255
  const plannedFiles = result.files.map((file) => ` ${file}`).join("\n");
2256
+ const plannedPresets =
2257
+ options.presets && options.presets.length > 0
2258
+ ? `\n\nProvider presets applied after scaffold (skipped in dry run):\n${options.presets
2259
+ .map((preset) => ` ${preset}`)
2260
+ .join("\n")}`
2261
+ : "";
1715
2262
 
1716
2263
  return `Would create ${result.name} at ${result.targetDir}
1717
2264
 
1718
2265
  Planned files:
1719
- ${plannedFiles}`;
2266
+ ${plannedFiles}${plannedPresets}`;
1720
2267
  }
1721
2268
 
1722
2269
  const pm = result.packageManager;
@@ -1725,9 +2272,17 @@ ${plannedFiles}`;
1725
2272
  const envRequiredIntegrations =
1726
2273
  options.integrations?.filter(isEnvRequiredProviderIntegration) ?? [];
1727
2274
  const envStep =
1728
- envRequiredIntegrations.length > 0
2275
+ envRequiredIntegrations.length > 0 || options.presetsNeedEnv
1729
2276
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
1730
2277
  : "";
2278
+ const presetSection =
2279
+ options.presets && options.presets.length > 0
2280
+ ? `\nProvider presets applied:\n${options.presets
2281
+ .map((preset) => ` ${preset}`)
2282
+ .join(
2283
+ "\n",
2284
+ )}\n See docs/integrations.md for setup notes and follow-up steps.\n`
2285
+ : "";
1731
2286
  const openStep = options.api
1732
2287
  ? " open http://localhost:3000 for the API landing page"
1733
2288
  : " open http://localhost:3000/sign-up";
@@ -1742,7 +2297,7 @@ ${plannedFiles}`;
1742
2297
  : "";
1743
2298
 
1744
2299
  return `Created ${result.name} at ${result.targetDir}
1745
-
2300
+ ${presetSection}
1746
2301
  Next steps:
1747
2302
  cd ${result.targetDir}
1748
2303
  ${pm} install
@@ -1858,15 +2413,15 @@ Command:
1858
2413
  ${command}`;
1859
2414
  }
1860
2415
 
1861
- function databaseSchemaGenerateNextSteps(
1862
- result: DatabaseSchemaGenerateNextStepsResult,
2416
+ function databaseSchemaSyncNextSteps(
2417
+ result: DatabaseSchemaSyncNextStepsResult,
1863
2418
  ): string {
1864
2419
  const { changedFiles, skippedFiles } = changedFileLines({
1865
2420
  createdFiles: result.createdFiles,
1866
2421
  updatedFiles: result.updatedFiles,
1867
2422
  skippedFiles: result.skippedFiles,
1868
2423
  });
1869
- const prefix = result.dryRun ? "Would generate" : "Generated";
2424
+ const prefix = result.dryRun ? "Would sync" : "Synced";
1870
2425
  const tables = result.tables.join(", ");
1871
2426
 
1872
2427
  return `${prefix} Beignet ${result.dialect} database schema (${tables}) in ${result.cwd}
@@ -1984,7 +2539,7 @@ function makeOutboxNextSteps(result: MakeNextStepsResult): string {
1984
2539
  function makeListenerNextSteps(result: MakeNextStepsResult): string {
1985
2540
  return makeNextSteps(result, "listener", [
1986
2541
  "Replace the starter handler with the event reaction.",
1987
- "Register the listener collection from infrastructure startup.",
2542
+ "Register the listener collection from server provider wiring.",
1988
2543
  ]);
1989
2544
  }
1990
2545