@beignet/cli 0.0.30 → 0.0.32

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 (72) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +49 -6
  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 +60 -0
  10. package/dist/choices.js.map +1 -1
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +1 -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 +4 -4
  22. package/dist/db.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +107 -26
  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 +8 -4
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/make/shared.d.ts +1 -1
  31. package/dist/make/shared.d.ts.map +1 -1
  32. package/dist/make/shared.js +8 -1
  33. package/dist/make/shared.js.map +1 -1
  34. package/dist/make.d.ts.map +1 -1
  35. package/dist/make.js +7 -10
  36. package/dist/make.js.map +1 -1
  37. package/dist/preflight.d.ts +90 -0
  38. package/dist/preflight.d.ts.map +1 -0
  39. package/dist/preflight.js +423 -0
  40. package/dist/preflight.js.map +1 -0
  41. package/dist/provider-add.d.ts +10 -1
  42. package/dist/provider-add.d.ts.map +1 -1
  43. package/dist/provider-add.js +209 -16
  44. package/dist/provider-add.js.map +1 -1
  45. package/dist/templates/agents.d.ts.map +1 -1
  46. package/dist/templates/agents.js +2 -1
  47. package/dist/templates/agents.js.map +1 -1
  48. package/dist/templates/base.d.ts.map +1 -1
  49. package/dist/templates/base.js +2 -8
  50. package/dist/templates/base.js.map +1 -1
  51. package/dist/templates/server.js +4 -4
  52. package/dist/templates/shared.d.ts +2 -0
  53. package/dist/templates/shared.d.ts.map +1 -1
  54. package/dist/templates/shared.js +2 -0
  55. package/dist/templates/shared.js.map +1 -1
  56. package/package.json +2 -2
  57. package/skills/app-structure/SKILL.md +1 -1
  58. package/src/check.ts +23 -0
  59. package/src/choices.ts +81 -0
  60. package/src/config.ts +2 -0
  61. package/src/create-prompts.ts +8 -2
  62. package/src/db.ts +11 -11
  63. package/src/index.ts +155 -36
  64. package/src/inspect.ts +8 -4
  65. package/src/make/shared.ts +9 -2
  66. package/src/make.ts +7 -11
  67. package/src/preflight.ts +596 -0
  68. package/src/provider-add.ts +223 -16
  69. package/src/templates/agents.ts +2 -1
  70. package/src/templates/base.ts +2 -10
  71. package/src/templates/server.ts +4 -4
  72. package/src/templates/shared.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beignet/cli",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "type": "module",
5
5
  "description": "CLI for creating and maintaining Beignet apps.",
6
6
  "main": "./dist/lib.js",
@@ -68,7 +68,7 @@
68
68
  "typescript": "^5.3.0"
69
69
  },
70
70
  "dependencies": {
71
- "@beignet/core": "^0.0.30",
71
+ "@beignet/core": "^0.0.32",
72
72
  "@clack/prompts": "^1.5.1",
73
73
  "@modelcontextprotocol/sdk": "^1.29.0",
74
74
  "@stricli/core": "^1.2.7",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: app-structure
3
- description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema generation, beignet.config paths, routes inspection, lint dependency boundaries, doctor drift checks and fixes, MCP tools, generated AGENTS.md guidance, Intent package skills, and generated-app validation. Use when scaffolding, generating, fixing, or reviewing a Beignet app's structure."
3
+ description: "Maintain Beignet app structure with @beignet/cli: create starter output, make generators, full-slice feature recipes, db schema sync, beignet.config paths, routes inspection, lint dependency boundaries, doctor drift checks and fixes, MCP tools, generated AGENTS.md guidance, Intent package skills, and generated-app validation. Use when scaffolding, generating, fixing, or reviewing a Beignet app's structure."
4
4
  ---
5
5
 
6
6
  # Beignet CLI App Structure
package/src/check.ts CHANGED
@@ -38,6 +38,8 @@ export type CheckAppOptions = {
38
38
  cwd?: string;
39
39
  fix?: boolean;
40
40
  color?: boolean;
41
+ /** Append a `beignet preflight` step after the doctor step. */
42
+ preflight?: boolean;
41
43
  /** Called after each step completes, for incremental progress output. */
42
44
  onStep?: (step: CheckStep) => void;
43
45
  };
@@ -69,6 +71,10 @@ export async function checkApp(
69
71
  const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
70
72
  record(doctorStep);
71
73
 
74
+ if (options.preflight) {
75
+ record(await preflightStep(targetDir));
76
+ }
77
+
72
78
  const scripts = await readPackageScripts(targetDir);
73
79
  for (const script of scriptStepNames) {
74
80
  record(await scriptStep(targetDir, runner, script, scripts));
@@ -244,3 +250,20 @@ async function readPackageScripts(
244
250
  };
245
251
  return parsed.scripts ?? {};
246
252
  }
253
+
254
+ async function preflightStep(targetDir: string): Promise<CheckStep> {
255
+ const startedAt = Date.now();
256
+ const { formatPreflight, runPreflight } = await import("./preflight.js");
257
+ const result = await runPreflight({ cwd: targetDir });
258
+ const failed = result.findings.some(
259
+ (finding) => finding.severity === "error",
260
+ );
261
+
262
+ return {
263
+ name: "beignet preflight",
264
+ command: "beignet preflight",
265
+ status: failed ? "failed" : "passed",
266
+ ...(failed ? { output: formatPreflight(result, targetDir) } : {}),
267
+ durationMs: Date.now() - startedAt,
268
+ };
269
+ }
package/src/choices.ts CHANGED
@@ -29,8 +29,11 @@ export type DatabaseName = "sqlite" | "postgres" | "mysql";
29
29
  */
30
30
  export type ProviderPresetName =
31
31
  | "flags-openfeature"
32
+ | "jobs-bullmq"
33
+ | "jobs-inngest"
32
34
  | "mail-resend"
33
35
  | "mail-smtp"
36
+ | "payments-stripe"
34
37
  | "search-meilisearch"
35
38
  | "sentry"
36
39
  | "upstash-rate-limit"
@@ -79,8 +82,11 @@ export const databaseChoices = [
79
82
  */
80
83
  export const providerPresetChoices = [
81
84
  "flags-openfeature",
85
+ "jobs-bullmq",
86
+ "jobs-inngest",
82
87
  "mail-resend",
83
88
  "mail-smtp",
89
+ "payments-stripe",
84
90
  "search-meilisearch",
85
91
  "sentry",
86
92
  "upstash-rate-limit",
@@ -90,6 +96,81 @@ export const providerPresetChoices = [
90
96
  "vercel-blob-storage",
91
97
  ] as const satisfies readonly ProviderPresetName[];
92
98
 
99
+ /**
100
+ * Values accepted by `beignet create --integrations`: the starter template
101
+ * integrations plus the full `beignet providers add` preset catalog.
102
+ */
103
+ export type CreateIntegrationName = IntegrationName | ProviderPresetName;
104
+
105
+ /**
106
+ * Supported `beignet create --integrations` choices. Template integrations
107
+ * come first; the rest of the provider preset catalog follows.
108
+ */
109
+ export const createIntegrationChoices = [
110
+ "inngest",
111
+ "resend",
112
+ "upstash-rate-limit",
113
+ "flags-openfeature",
114
+ "jobs-bullmq",
115
+ "jobs-inngest",
116
+ "mail-resend",
117
+ "mail-smtp",
118
+ "payments-stripe",
119
+ "redis-cache",
120
+ "redis-locks",
121
+ "s3-storage",
122
+ "search-meilisearch",
123
+ "sentry",
124
+ "vercel-blob-storage",
125
+ ] as const satisfies readonly CreateIntegrationName[];
126
+
127
+ /**
128
+ * Provider presets whose create-time wiring already ships as a starter
129
+ * template integration. Selecting the preset name at create time scaffolds
130
+ * the equivalent template integration, so preset and integration stay
131
+ * consistent and `beignet providers add` remains a no-op afterwards.
132
+ */
133
+ const presetTemplateIntegrationAliases: Partial<
134
+ Record<ProviderPresetName, IntegrationName>
135
+ > = {
136
+ "jobs-inngest": "inngest",
137
+ "mail-resend": "resend",
138
+ "upstash-rate-limit": "upstash-rate-limit",
139
+ };
140
+
141
+ /**
142
+ * Split `--integrations` selections into starter template integrations and
143
+ * provider presets applied with the `beignet providers add` machinery after
144
+ * the scaffold is written. Preset names with a template equivalent are
145
+ * normalized to the template integration; duplicates collapse.
146
+ */
147
+ export function splitCreateIntegrations(
148
+ values: readonly CreateIntegrationName[],
149
+ ): {
150
+ integrations: IntegrationName[];
151
+ presets: ProviderPresetName[];
152
+ } {
153
+ const integrations = new Set<IntegrationName>();
154
+ const presets = new Set<ProviderPresetName>();
155
+
156
+ for (const value of values) {
157
+ if ((integrationChoices as readonly string[]).includes(value)) {
158
+ integrations.add(value as IntegrationName);
159
+ continue;
160
+ }
161
+
162
+ const preset = value as ProviderPresetName;
163
+ const alias = presetTemplateIntegrationAliases[preset];
164
+ if (alias) {
165
+ integrations.add(alias);
166
+ } else {
167
+ presets.add(preset);
168
+ }
169
+ }
170
+
171
+ return { integrations: [...integrations], presets: [...presets] };
172
+ }
173
+
93
174
  /**
94
175
  * Local database name derived from the app name.
95
176
  *
package/src/config.ts CHANGED
@@ -32,6 +32,7 @@ export type BeignetPaths = {
32
32
  schedulesBuilder: string;
33
33
  notificationsBuilder: string;
34
34
  tasksBuilder: string;
35
+ uploadsBuilder: string;
35
36
  tests: string;
36
37
  };
37
38
 
@@ -100,6 +101,7 @@ export const defaultBeignetConfig: ResolvedBeignetConfig = {
100
101
  schedulesBuilder: "lib/schedules.ts",
101
102
  notificationsBuilder: "lib/notifications.ts",
102
103
  tasksBuilder: "lib/tasks.ts",
104
+ uploadsBuilder: "lib/uploads.ts",
103
105
  tests: "tests",
104
106
  },
105
107
  database: {
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import {
10
+ type CreateIntegrationName,
10
11
  type DatabaseName,
11
12
  databaseChoices,
12
13
  type IntegrationName,
@@ -16,12 +17,16 @@ import {
16
17
 
17
18
  /**
18
19
  * Selections shared by create flags and interactive prompt answers.
20
+ *
21
+ * `integrations` accepts the starter template integrations plus the full
22
+ * `beignet providers add` preset catalog; the interactive prompt shows the
23
+ * curated template list and points at the wider catalog.
19
24
  */
20
25
  export type CreateSelections = {
21
26
  directory?: string;
22
27
  api?: boolean;
23
28
  db?: DatabaseName;
24
- integrations?: readonly IntegrationName[];
29
+ integrations?: readonly CreateIntegrationName[];
25
30
  packageManager?: PackageManager;
26
31
  };
27
32
 
@@ -114,7 +119,8 @@ export async function promptCreateSelections(
114
119
  if (prompts.isCancel(db)) return undefined;
115
120
 
116
121
  const integrations = await prompts.multiselect<IntegrationName>({
117
- message: "Add provider integrations? (press enter to skip)",
122
+ message:
123
+ "Add provider integrations? (press enter to skip; the full preset catalog is available via --integrations or beignet providers add)",
118
124
  options: integrationChoices.map((integration) => ({
119
125
  value: integration,
120
126
  label: integration,
package/src/db.ts CHANGED
@@ -36,7 +36,7 @@ export type RunDatabaseCommandResult = {
36
36
  export type DatabaseSchemaDialect = "sqlite" | "postgres" | "mysql";
37
37
  export type DatabaseSchemaTable = "audit" | "idempotency" | "outbox";
38
38
 
39
- export type GenerateDatabaseSchemaOptions = {
39
+ export type SyncDatabaseSchemaOptions = {
40
40
  cwd?: string;
41
41
  dialect?: DatabaseSchemaDialect;
42
42
  tables?: readonly DatabaseSchemaTable[];
@@ -44,9 +44,9 @@ export type GenerateDatabaseSchemaOptions = {
44
44
  dryRun?: boolean;
45
45
  };
46
46
 
47
- export type GenerateDatabaseSchemaResult = {
47
+ export type SyncDatabaseSchemaResult = {
48
48
  schemaVersion: 1;
49
- command: "schema:generate";
49
+ command: "schema:sync";
50
50
  cwd: string;
51
51
  dialect: DatabaseSchemaDialect;
52
52
  tables: DatabaseSchemaTable[];
@@ -132,15 +132,15 @@ export async function runDatabaseCommand(
132
132
  }
133
133
 
134
134
  /**
135
- * Generate the app-owned Drizzle schema entrypoint for Beignet provider tables.
135
+ * Sync the app-owned Drizzle schema entrypoint for Beignet provider tables.
136
136
  *
137
137
  * This only writes schema source. The app still owns SQL migration generation
138
138
  * through `beignet db generate` and migration application through
139
139
  * `beignet db migrate`.
140
140
  */
141
- export async function generateDatabaseSchema(
142
- options: GenerateDatabaseSchemaOptions = {},
143
- ): Promise<GenerateDatabaseSchemaResult> {
141
+ export async function syncDatabaseSchema(
142
+ options: SyncDatabaseSchemaOptions = {},
143
+ ): Promise<SyncDatabaseSchemaResult> {
144
144
  const cwd = path.resolve(options.cwd ?? process.cwd());
145
145
  const dialect = await resolveDatabaseSchemaDialect(cwd, options.dialect);
146
146
  const tables = uniqueSchemaTables(options.tables ?? allDatabaseSchemaTables);
@@ -159,9 +159,9 @@ export async function generateDatabaseSchema(
159
159
  throw new Error("Schema output must not be the schema index file.");
160
160
  }
161
161
 
162
- const result: GenerateDatabaseSchemaResult = {
162
+ const result: SyncDatabaseSchemaResult = {
163
163
  schemaVersion: 1,
164
- command: "schema:generate",
164
+ command: "schema:sync",
165
165
  cwd,
166
166
  dialect,
167
167
  tables,
@@ -345,7 +345,7 @@ function renderDatabaseSchema(
345
345
  const tableNames = selectedTables.map((table) => table.tableName);
346
346
  const exportNames = selectedTables.map((table) => table.exportName).sort();
347
347
 
348
- return `// Generated by beignet db schema generate.
348
+ return `// Managed by beignet db schema sync.
349
349
  // Required tables: ${tableNames.join(", ")}.
350
350
  // Re-run this command after upgrading @beignet/provider-db-drizzle.
351
351
 
@@ -437,7 +437,7 @@ async function readTextIfExists(file: string): Promise<string | undefined> {
437
437
  }
438
438
 
439
439
  function recordFileStatus(
440
- result: GenerateDatabaseSchemaResult,
440
+ result: SyncDatabaseSchemaResult,
441
441
  file: string,
442
442
  status: FileWriteStatus,
443
443
  ): void {
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;
@@ -278,14 +290,14 @@ const dbFlagParameters = {
278
290
  dryRun: dryRunFlag,
279
291
  } satisfies FlagParametersForType<DbFlags, CliContext>;
280
292
 
281
- const dbSchemaGenerateFlagParameters = {
293
+ const dbSchemaSyncFlagParameters = {
282
294
  ...dbFlagParameters,
283
295
  dialect: {
284
296
  kind: "enum",
285
297
  values: databaseSchemaDialectChoices,
286
298
  optional: true,
287
299
  brief:
288
- "Drizzle dialect to generate for. Inferred from server/providers.ts when omitted.",
300
+ "Drizzle dialect to sync. Inferred from server/providers.ts when omitted.",
289
301
  },
290
302
  tables: {
291
303
  kind: "enum",
@@ -293,12 +305,12 @@ const dbSchemaGenerateFlagParameters = {
293
305
  optional: true,
294
306
  variadic: ",",
295
307
  brief:
296
- "Provider tables to generate as a comma-separated list. Defaults to audit,idempotency,outbox.",
308
+ "Provider tables to sync as a comma-separated list. Defaults to audit,idempotency,outbox.",
297
309
  },
298
310
  output: parsedStringFlag(
299
- "Generated schema file. Defaults to infra/db/schema/beignet.ts.",
311
+ "Synced schema file. Defaults to infra/db/schema/beignet.ts.",
300
312
  ),
301
- } satisfies FlagParametersForType<DbSchemaGenerateFlags, CliContext>;
313
+ } satisfies FlagParametersForType<DbSchemaSyncFlags, CliContext>;
302
314
 
303
315
  const taskRunFlagParameters = {
304
316
  json: jsonFlag,
@@ -400,7 +412,11 @@ and a shadcn UI shell. Pass --api for an API-only scaffold without the
400
412
  UI shell, and --db to choose the database.
401
413
 
402
414
  Available databases: ${databaseChoices.join(", ")}
403
- Available integrations: ${integrationChoices.join(", ")}
415
+ Available integrations: ${createIntegrationChoices.join(", ")}
416
+
417
+ --integrations accepts the starter template integrations plus the full
418
+ beignet providers add preset catalog; presets are applied to the scaffold
419
+ with the same machinery as beignet providers add.
404
420
 
405
421
  Running create on an interactive terminal without selection flags opens a
406
422
  prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
@@ -433,10 +449,11 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
433
449
  },
434
450
  integrations: {
435
451
  kind: "enum",
436
- values: integrationChoices,
452
+ values: createIntegrationChoices,
437
453
  optional: true,
438
454
  variadic: ",",
439
- brief: "Add first-party integrations as a comma-separated list.",
455
+ brief:
456
+ "Add first-party integrations or provider presets as a comma-separated list.",
440
457
  },
441
458
  yes: {
442
459
  kind: "boolean",
@@ -504,7 +521,18 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
504
521
 
505
522
  const api = selections.api ?? false;
506
523
  const database = selections.db ?? "sqlite";
507
- const integrations = uniqueValues([...(selections.integrations ?? [])]);
524
+ const { integrations, presets } = splitCreateIntegrations(
525
+ uniqueValues([...(selections.integrations ?? [])]),
526
+ );
527
+
528
+ let presetsNeedEnv = false;
529
+ if (presets.length > 0) {
530
+ const { assertCompatibleCreatePresets, providerPresetNeedsEnv } =
531
+ await import("./provider-add.js");
532
+ assertCompatibleCreatePresets(presets, integrations);
533
+ presetsNeedEnv = presets.some(providerPresetNeedsEnv);
534
+ }
535
+
508
536
  const result = await createProject({
509
537
  name: selections.directory,
510
538
  template: flags.template,
@@ -516,11 +544,24 @@ prompt-based setup. Pass --yes or any selection flag to skip the prompts.`,
516
544
  dryRun: Boolean(flags.dryRun),
517
545
  });
518
546
 
547
+ if (!flags.dryRun && presets.length > 0) {
548
+ const { addProviderPreset } = await import("./provider-add.js");
549
+ for (const preset of presets) {
550
+ await addProviderPreset({ name: preset, cwd: result.targetDir });
551
+ }
552
+ }
553
+
519
554
  writeOutput(
520
555
  this,
521
556
  flags.json
522
- ? JSON.stringify(result, null, 2)
523
- : nextSteps(result, { api, integrations, database }),
557
+ ? JSON.stringify({ ...result, providerPresets: presets }, null, 2)
558
+ : nextSteps(result, {
559
+ api,
560
+ integrations,
561
+ database,
562
+ presets,
563
+ presetsNeedEnv,
564
+ }),
524
565
  );
525
566
  };
526
567
  },
@@ -622,6 +663,12 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
622
663
  withNegated: false,
623
664
  brief: "Apply low-risk doctor fixes before checking.",
624
665
  },
666
+ preflight: {
667
+ kind: "boolean",
668
+ optional: true,
669
+ withNegated: false,
670
+ brief: "Append a beignet preflight step after doctor.",
671
+ },
625
672
  cwd: cwdFlag,
626
673
  } satisfies FlagParametersForType<CheckFlags, CliContext>,
627
674
  },
@@ -635,6 +682,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
635
682
  const result = await checkApp({
636
683
  cwd: flags.cwd,
637
684
  fix: flags.fix,
685
+ preflight: flags.preflight,
638
686
  color,
639
687
  ...(flags.json
640
688
  ? {}
@@ -656,6 +704,64 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
656
704
  },
657
705
  });
658
706
 
707
+ const preflightCommand = buildCommand<PreflightFlags, [], CliContext>({
708
+ docs: {
709
+ brief: "Validate the deploy environment before shipping.",
710
+ fullDescription:
711
+ "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.",
712
+ },
713
+ parameters: {
714
+ flags: {
715
+ json: jsonFlag,
716
+ envFile: parsedStringFlag(
717
+ "Dotenv-style file merged under the environment (existing env keys win) for local rehearsal.",
718
+ ),
719
+ envModule: parsedStringFlag(
720
+ "App env module validated by importing it. Defaults to lib/env.ts.",
721
+ ),
722
+ connect: {
723
+ kind: "boolean",
724
+ optional: true,
725
+ withNegated: false,
726
+ brief:
727
+ "Boot the app server and run every port's checkHealth(). Needs network and real credentials.",
728
+ },
729
+ serverModule: parsedStringFlag(
730
+ "Module exporting getServer, used by --connect. Defaults to server/index.ts.",
731
+ ),
732
+ cwd: cwdFlag,
733
+ } satisfies FlagParametersForType<PreflightFlags, CliContext>,
734
+ },
735
+ loader: async () => {
736
+ const { formatPreflight, runPreflight } = await import("./preflight.js");
737
+ const path = await import("node:path");
738
+
739
+ return async function runPreflightCommand(
740
+ this: CliContext,
741
+ flags: PreflightFlags,
742
+ ) {
743
+ const cwd = path.resolve(flags.cwd ?? process.cwd());
744
+ const result = await runPreflight({
745
+ cwd,
746
+ envFile: flags.envFile,
747
+ envModule: flags.envModule,
748
+ connect: flags.connect,
749
+ serverModule: flags.serverModule,
750
+ });
751
+
752
+ writeOutput(
753
+ this,
754
+ flags.json
755
+ ? JSON.stringify({ schemaVersion: 1, cwd, ...result }, null, 2)
756
+ : formatPreflight(result, cwd),
757
+ );
758
+ if (result.findings.some((finding) => finding.severity === "error")) {
759
+ this.process.exitCode = 1;
760
+ }
761
+ };
762
+ },
763
+ });
764
+
659
765
  const lintCommand = buildCommand<LintFlags, [], CliContext>({
660
766
  docs: {
661
767
  brief: "Check Beignet dependency direction conventions.",
@@ -819,27 +925,23 @@ function databaseCommand(command: DatabaseCommandName) {
819
925
  });
820
926
  }
821
927
 
822
- const dbSchemaGenerateCommand = buildCommand<
823
- DbSchemaGenerateFlags,
824
- [],
825
- CliContext
826
- >({
928
+ const dbSchemaSyncCommand = buildCommand<DbSchemaSyncFlags, [], CliContext>({
827
929
  docs: {
828
- brief: "Generate provider-owned Drizzle schema exports.",
930
+ brief: "Sync app-owned re-exports of Beignet provider tables.",
829
931
  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.",
932
+ "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
933
  },
832
934
  parameters: {
833
- flags: dbSchemaGenerateFlagParameters,
935
+ flags: dbSchemaSyncFlagParameters,
834
936
  },
835
937
  loader: async () => {
836
- const { generateDatabaseSchema } = await import("./db.js");
938
+ const { syncDatabaseSchema } = await import("./db.js");
837
939
 
838
- return async function runDbSchemaGenerate(
940
+ return async function runDbSchemaSync(
839
941
  this: CliContext,
840
- flags: DbSchemaGenerateFlags,
942
+ flags: DbSchemaSyncFlags,
841
943
  ) {
842
- const result = await generateDatabaseSchema({
944
+ const result = await syncDatabaseSchema({
843
945
  dialect: flags.dialect,
844
946
  tables: flags.tables,
845
947
  output: flags.output,
@@ -850,7 +952,7 @@ const dbSchemaGenerateCommand = buildCommand<
850
952
  this,
851
953
  flags.json
852
954
  ? JSON.stringify(result, null, 2)
853
- : databaseSchemaGenerateNextSteps(result),
955
+ : databaseSchemaSyncNextSteps(result),
854
956
  );
855
957
  };
856
958
  },
@@ -858,10 +960,10 @@ const dbSchemaGenerateCommand = buildCommand<
858
960
 
859
961
  const dbSchemaRoutes = buildRouteMap({
860
962
  docs: {
861
- brief: "Generate Beignet database schema helpers.",
963
+ brief: "Sync app-owned Beignet provider schema exports.",
862
964
  },
863
965
  routes: {
864
- generate: dbSchemaGenerateCommand,
966
+ sync: dbSchemaSyncCommand,
865
967
  },
866
968
  });
867
969
 
@@ -1557,6 +1659,7 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
1557
1659
  make: makeRoutes,
1558
1660
  mcp: mcpCommand,
1559
1661
  outbox: outboxRoutes,
1662
+ preflight: preflightCommand,
1560
1663
  providers: providerRoutes,
1561
1664
  routes: routesCommand,
1562
1665
  schedule: scheduleRoutes,
@@ -1626,7 +1729,7 @@ type DatabaseCommandNextStepsResult = {
1626
1729
  dryRun: boolean;
1627
1730
  };
1628
1731
 
1629
- type DatabaseSchemaGenerateNextStepsResult = {
1732
+ type DatabaseSchemaSyncNextStepsResult = {
1630
1733
  cwd: string;
1631
1734
  dialect: DatabaseSchemaDialect;
1632
1735
  tables: DatabaseSchemaTable[];
@@ -1708,15 +1811,23 @@ function nextSteps(
1708
1811
  api?: boolean;
1709
1812
  integrations?: readonly IntegrationName[];
1710
1813
  database?: DatabaseName;
1814
+ presets?: readonly ProviderPresetName[];
1815
+ presetsNeedEnv?: boolean;
1711
1816
  } = {},
1712
1817
  ): string {
1713
1818
  if (result.dryRun) {
1714
1819
  const plannedFiles = result.files.map((file) => ` ${file}`).join("\n");
1820
+ const plannedPresets =
1821
+ options.presets && options.presets.length > 0
1822
+ ? `\n\nProvider presets applied after scaffold (skipped in dry run):\n${options.presets
1823
+ .map((preset) => ` ${preset}`)
1824
+ .join("\n")}`
1825
+ : "";
1715
1826
 
1716
1827
  return `Would create ${result.name} at ${result.targetDir}
1717
1828
 
1718
1829
  Planned files:
1719
- ${plannedFiles}`;
1830
+ ${plannedFiles}${plannedPresets}`;
1720
1831
  }
1721
1832
 
1722
1833
  const pm = result.packageManager;
@@ -1725,9 +1836,17 @@ ${plannedFiles}`;
1725
1836
  const envRequiredIntegrations =
1726
1837
  options.integrations?.filter(isEnvRequiredProviderIntegration) ?? [];
1727
1838
  const envStep =
1728
- envRequiredIntegrations.length > 0
1839
+ envRequiredIntegrations.length > 0 || options.presetsNeedEnv
1729
1840
  ? " Fill .env.local for selected provider integrations before starting the app.\n"
1730
1841
  : "";
1842
+ const presetSection =
1843
+ options.presets && options.presets.length > 0
1844
+ ? `\nProvider presets applied:\n${options.presets
1845
+ .map((preset) => ` ${preset}`)
1846
+ .join(
1847
+ "\n",
1848
+ )}\n See docs/integrations.md for setup notes and follow-up steps.\n`
1849
+ : "";
1731
1850
  const openStep = options.api
1732
1851
  ? " open http://localhost:3000 for the API landing page"
1733
1852
  : " open http://localhost:3000/sign-up";
@@ -1742,7 +1861,7 @@ ${plannedFiles}`;
1742
1861
  : "";
1743
1862
 
1744
1863
  return `Created ${result.name} at ${result.targetDir}
1745
-
1864
+ ${presetSection}
1746
1865
  Next steps:
1747
1866
  cd ${result.targetDir}
1748
1867
  ${pm} install
@@ -1858,15 +1977,15 @@ Command:
1858
1977
  ${command}`;
1859
1978
  }
1860
1979
 
1861
- function databaseSchemaGenerateNextSteps(
1862
- result: DatabaseSchemaGenerateNextStepsResult,
1980
+ function databaseSchemaSyncNextSteps(
1981
+ result: DatabaseSchemaSyncNextStepsResult,
1863
1982
  ): string {
1864
1983
  const { changedFiles, skippedFiles } = changedFileLines({
1865
1984
  createdFiles: result.createdFiles,
1866
1985
  updatedFiles: result.updatedFiles,
1867
1986
  skippedFiles: result.skippedFiles,
1868
1987
  });
1869
- const prefix = result.dryRun ? "Would generate" : "Generated";
1988
+ const prefix = result.dryRun ? "Would sync" : "Synced";
1870
1989
  const tables = result.tables.join(", ");
1871
1990
 
1872
1991
  return `${prefix} Beignet ${result.dialect} database schema (${tables}) in ${result.cwd}