@beignet/cli 0.0.47 → 0.0.48

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 +12 -0
  2. package/README.md +50 -21
  3. package/dist/check.d.ts +8 -1
  4. package/dist/check.d.ts.map +1 -1
  5. package/dist/check.js +18 -6
  6. package/dist/check.js.map +1 -1
  7. package/dist/db.d.ts +38 -0
  8. package/dist/db.d.ts.map +1 -1
  9. package/dist/db.js +52 -0
  10. package/dist/db.js.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +73 -6
  13. package/dist/index.js.map +1 -1
  14. package/dist/inspect.d.ts.map +1 -1
  15. package/dist/inspect.js +39 -2
  16. package/dist/inspect.js.map +1 -1
  17. package/dist/lib.d.ts +2 -2
  18. package/dist/lib.d.ts.map +1 -1
  19. package/dist/lib.js +1 -1
  20. package/dist/lib.js.map +1 -1
  21. package/dist/make/inbox.js +1 -1
  22. package/dist/make/inbox.js.map +1 -1
  23. package/dist/make/payments.js +1 -1
  24. package/dist/make/payments.js.map +1 -1
  25. package/dist/make/tenancy.js +1 -1
  26. package/dist/make/tenancy.js.map +1 -1
  27. package/dist/mcp.d.ts.map +1 -1
  28. package/dist/mcp.js +39 -2
  29. package/dist/mcp.js.map +1 -1
  30. package/dist/preflight.d.ts +11 -3
  31. package/dist/preflight.d.ts.map +1 -1
  32. package/dist/preflight.js +139 -16
  33. package/dist/preflight.js.map +1 -1
  34. package/dist/provider-add.d.ts.map +1 -1
  35. package/dist/provider-add.js +7 -2
  36. package/dist/provider-add.js.map +1 -1
  37. package/dist/provider-audit.d.ts +13 -0
  38. package/dist/provider-audit.d.ts.map +1 -1
  39. package/dist/provider-audit.js +122 -8
  40. package/dist/provider-audit.js.map +1 -1
  41. package/dist/templates/agents.d.ts.map +1 -1
  42. package/dist/templates/agents.js +13 -7
  43. package/dist/templates/agents.js.map +1 -1
  44. package/dist/templates/base.d.ts +1 -0
  45. package/dist/templates/base.d.ts.map +1 -1
  46. package/dist/templates/base.js +11 -5
  47. package/dist/templates/base.js.map +1 -1
  48. package/dist/templates/db/index.d.ts +1 -0
  49. package/dist/templates/db/index.d.ts.map +1 -1
  50. package/dist/templates/db/index.js.map +1 -1
  51. package/dist/templates/db/migration-status.d.ts +9 -0
  52. package/dist/templates/db/migration-status.d.ts.map +1 -0
  53. package/dist/templates/db/migration-status.js +117 -0
  54. package/dist/templates/db/migration-status.js.map +1 -0
  55. package/dist/templates/db/mysql.d.ts.map +1 -1
  56. package/dist/templates/db/mysql.js +23 -0
  57. package/dist/templates/db/mysql.js.map +1 -1
  58. package/dist/templates/db/postgres.d.ts.map +1 -1
  59. package/dist/templates/db/postgres.js +21 -0
  60. package/dist/templates/db/postgres.js.map +1 -1
  61. package/dist/templates/db/sqlite.d.ts.map +1 -1
  62. package/dist/templates/db/sqlite.js +53 -0
  63. package/dist/templates/db/sqlite.js.map +1 -1
  64. package/dist/templates/index.d.ts.map +1 -1
  65. package/dist/templates/index.js +5 -0
  66. package/dist/templates/index.js.map +1 -1
  67. package/dist/templates/shared.d.ts.map +1 -1
  68. package/dist/templates/shared.js +1 -0
  69. package/dist/templates/shared.js.map +1 -1
  70. package/package.json +2 -2
  71. package/skills/app-structure/SKILL.md +11 -7
  72. package/src/check.ts +35 -7
  73. package/src/db.ts +97 -0
  74. package/src/index.ts +108 -7
  75. package/src/inspect.ts +76 -1
  76. package/src/lib.ts +9 -1
  77. package/src/make/inbox.ts +1 -1
  78. package/src/make/payments.ts +1 -1
  79. package/src/make/tenancy.ts +1 -1
  80. package/src/mcp.ts +56 -1
  81. package/src/preflight.ts +195 -16
  82. package/src/provider-add.ts +7 -2
  83. package/src/provider-audit.ts +210 -18
  84. package/src/templates/agents.ts +13 -7
  85. package/src/templates/base.ts +12 -3
  86. package/src/templates/db/index.ts +1 -0
  87. package/src/templates/db/migration-status.ts +126 -0
  88. package/src/templates/db/mysql.ts +23 -0
  89. package/src/templates/db/postgres.ts +21 -0
  90. package/src/templates/db/sqlite.ts +53 -0
  91. package/src/templates/index.ts +5 -0
  92. package/src/templates/shared.ts +1 -0
@@ -64,6 +64,7 @@ changing `infra/db/schema/`, run the app's db lifecycle commands:
64
64
  ```bash
65
65
  beignet db generate
66
66
  beignet db migrate
67
+ beignet db status
67
68
  ```
68
69
 
69
70
  ## Explicit Registration
@@ -195,7 +196,7 @@ Generated apps register:
195
196
  }
196
197
  ```
197
198
 
198
- The MCP server exposes `app_map`, `explain`, `check`, `db`,
199
+ The MCP server exposes `app_map`, `explain`, `check`, `db`, `db_status`,
199
200
  `db_schema_sync`, `task_run`, `schedule_run`, `outbox_inspect`, `outbox_run`,
200
201
  `routes`, `doctor`, `doctor_fix_plan`, `doctor_fix`, `lint`, `make`, and
201
202
  `provider_add`. It also publishes `beignet://app/guidance` and the
@@ -210,10 +211,11 @@ concept or diagnostic and returns the same deterministic result as
210
211
  `beignet explain <kind> <target> --json`. Explain accepts every app-map node
211
212
  kind plus `diagnostic`; targets may use stable IDs, runtime or declaration
212
213
  names, source selectors, and applicable aliases. `check` accepts optional
213
- `{ preflight?: boolean, timeoutMs?: number }`, bounds failure output, cancels
214
- the active script with the MCP request, and returns the same versioned result
215
- as `beignet check --json`. It does not apply Beignet fixes, but app package
216
- scripts retain their normal side effects. `db` accepts
214
+ `{ preflight?: boolean, preflightConnect?: boolean, connectTimeoutMs?: number, timeoutMs?: number }`.
215
+ Connected preflight adds migration status and dependency health checks. Check
216
+ bounds failure output, cancels the active script with the MCP request, and
217
+ returns the same versioned result as `beignet check --json`. It does not apply
218
+ Beignet fixes, but app package scripts retain their normal side effects. `db` accepts
217
219
  `{ command: "generate" | "migrate" | "seed" | "reset", dryRun?, timeoutMs? }`
218
220
  and returns the matching versioned `beignet db <command> --json` report with
219
221
  bounded output, cancellation, and process-tree timeouts. `db_schema_sync`
@@ -221,7 +223,9 @@ accepts `{ dialect?, tables?, output?, dryRun? }` and returns the matching
221
223
  `beignet db schema sync --json` report. For lifecycle commands, `dryRun`
222
224
  validates and reports the app-owned script without executing it; it does not
223
225
  simulate SQL or data changes. `reset` remains environment-sensitive even
224
- though the generated entrypoint has a production guard. `task_run` and
226
+ though the generated entrypoint has a production guard. `db_status` runs the
227
+ app-owned read-only migration inspection: current exits 0, pending exits 2,
228
+ and inspection failures use another non-zero exit. `task_run` and
225
229
  `schedule_run` execute registered app workflows. Use `outbox_inspect` for
226
230
  read-only `list` and `show`, and `outbox_run` for state-changing `drain`,
227
231
  `requeue`, `purge`, and `prune`; purge and prune support `dryRun`. Operational
@@ -255,7 +259,7 @@ When MCP is available, call `check` after edits to run the full validation loop
255
259
  through the app-local CLI without reconstructing these commands individually.
256
260
  For a generated persistent resource, the complete structured workflow is
257
261
  `app_map` → `explain` → `make` → `db_schema_sync` when provider tables changed
258
- → `db` generate → `db` migrate → the relevant `task_run`, `schedule_run`, or
262
+ → `db` generate → `db` migrate → `db_status` → the relevant `task_run`, `schedule_run`, or
259
263
  outbox tool when operational work is required → `check`.
260
264
 
261
265
  For generated app template changes inside the Beignet monorepo, also run:
package/src/check.ts CHANGED
@@ -39,8 +39,15 @@ export type CheckAppOptions = {
39
39
  cwd?: string;
40
40
  fix?: boolean;
41
41
  color?: boolean;
42
- /** Append a `beignet preflight` step after the doctor step. */
42
+ /** Append a disconnected `beignet preflight` step after the doctor step. */
43
43
  preflight?: boolean;
44
+ /**
45
+ * Append a connected `beignet preflight --connect` step after doctor.
46
+ * Implies `preflight`.
47
+ */
48
+ preflightConnect?: boolean;
49
+ /** Per migration-status or dependency health check timeout in milliseconds. */
50
+ connectTimeoutMs?: number;
44
51
  /** Keep only the last N bytes from each package-script output stream. */
45
52
  maxOutputBytes?: number;
46
53
  /** Cancel the active package script with the owning request. */
@@ -81,9 +88,15 @@ export async function checkApp(
81
88
  const { step: doctorStep, fixes } = await runDoctorStep(targetDir, options);
82
89
  record(doctorStep);
83
90
 
84
- if (options.preflight) {
91
+ if (options.preflight || options.preflightConnect) {
85
92
  options.signal?.throwIfAborted();
86
- record(await preflightStep(targetDir));
93
+ record(
94
+ await preflightStep(targetDir, {
95
+ connect: Boolean(options.preflightConnect),
96
+ connectTimeoutMs: options.connectTimeoutMs,
97
+ signal: options.signal,
98
+ }),
99
+ );
87
100
  }
88
101
 
89
102
  const scripts = await readPackageScripts(targetDir);
@@ -274,17 +287,32 @@ async function readPackageScripts(
274
287
  return parsed.scripts ?? {};
275
288
  }
276
289
 
277
- async function preflightStep(targetDir: string): Promise<CheckStep> {
290
+ async function preflightStep(
291
+ targetDir: string,
292
+ options: {
293
+ connect: boolean;
294
+ connectTimeoutMs?: number;
295
+ signal?: AbortSignal;
296
+ },
297
+ ): Promise<CheckStep> {
278
298
  const startedAt = Date.now();
279
299
  const { formatPreflight, runPreflight } = await import("./preflight.js");
280
- const result = await runPreflight({ cwd: targetDir });
300
+ const result = await runPreflight({
301
+ cwd: targetDir,
302
+ connect: options.connect,
303
+ connectTimeoutMs: options.connectTimeoutMs,
304
+ signal: options.signal,
305
+ });
281
306
  const failed = result.findings.some(
282
307
  (finding) => finding.severity === "error",
283
308
  );
309
+ const command = options.connect
310
+ ? "beignet preflight --connect"
311
+ : "beignet preflight";
284
312
 
285
313
  return {
286
- name: "beignet preflight",
287
- command: "beignet preflight",
314
+ name: command,
315
+ command,
288
316
  status: failed ? "failed" : "passed",
289
317
  ...(failed ? { output: formatPreflight(result, targetDir) } : {}),
290
318
  durationMs: Date.now() - startedAt,
package/src/db.ts CHANGED
@@ -18,6 +18,8 @@ export type {
18
18
  export const defaultDatabaseCommandMaxOutputBytes = 64 * 1024;
19
19
  /** Default timeout used by request-controlled database command callers. */
20
20
  export const defaultDatabaseCommandTimeoutMs = 10 * 60 * 1_000;
21
+ /** Exit code reserved for an app-owned status script that found pending migrations. */
22
+ export const databaseMigrationsPendingExitCode = 2;
21
23
 
22
24
  /**
23
25
  * Options for running a database lifecycle command.
@@ -53,6 +55,37 @@ export type RunDatabaseCommandResult = {
53
55
  exitCode: number;
54
56
  };
55
57
 
58
+ /** Migration state reported by an app-owned database status script. */
59
+ export type DatabaseMigrationStatus = "current" | "pending" | "failed";
60
+
61
+ /** Options for inspecting app-owned database migration status. */
62
+ export type RunDatabaseStatusOptions = {
63
+ cwd?: string;
64
+ captureOutput?: boolean;
65
+ /** Keep only the last N bytes from each output stream. */
66
+ maxOutputBytes?: number;
67
+ /** Cancel the app-owned status script when the owning request aborts. */
68
+ signal?: AbortSignal;
69
+ /** Stop the app-owned status script after this many milliseconds. */
70
+ timeoutMs?: number;
71
+ };
72
+
73
+ /** Versioned result returned by `beignet db status`. */
74
+ export type RunDatabaseStatusResult = {
75
+ schemaVersion: 1;
76
+ command: "status";
77
+ status: DatabaseMigrationStatus;
78
+ cwd: string;
79
+ script: "db:status";
80
+ runner: string;
81
+ args: string[];
82
+ stdout?: string;
83
+ stderr?: string;
84
+ outputTruncated?: boolean;
85
+ timedOut?: boolean;
86
+ exitCode: number;
87
+ };
88
+
56
89
  export type SpawnCommandOptions = {
57
90
  captureOutput: boolean;
58
91
  /** Keep only the last N bytes from each output stream. Unbounded when omitted. */
@@ -176,6 +209,70 @@ export async function runDatabaseCommand(
176
209
  };
177
210
  }
178
211
 
212
+ /**
213
+ * Inspect migrations through the app-owned `db:status` script.
214
+ *
215
+ * Exit 0 means current, exit 2 means pending, and every other non-zero exit
216
+ * means the inspection failed. The app owns database-specific comparison
217
+ * logic while Beignet owns the stable report and automation contract.
218
+ */
219
+ export async function runDatabaseStatus(
220
+ options: RunDatabaseStatusOptions = {},
221
+ ): Promise<RunDatabaseStatusResult> {
222
+ options.signal?.throwIfAborted();
223
+ const cwd = path.resolve(options.cwd ?? process.cwd());
224
+ const script = "db:status" as const;
225
+ const packageJson = await readPackageJson(cwd);
226
+ const scriptCommand = packageJson.scripts?.[script];
227
+
228
+ if (!scriptCommand) {
229
+ throw new Error(
230
+ 'Missing package.json script "db:status". Add an app-owned read-only migration status script so beignet db status can inspect the target database.',
231
+ );
232
+ }
233
+
234
+ if (
235
+ scriptCommand.includes("infra/db/migration-status.ts") &&
236
+ !(await exists(path.join(cwd, "infra/db/migration-status.ts")))
237
+ ) {
238
+ throw new Error(
239
+ 'Missing database status entrypoint infra/db/migration-status.ts. beignet db status runs the "db:status" script, so restore the entrypoint or update package.json.',
240
+ );
241
+ }
242
+
243
+ options.signal?.throwIfAborted();
244
+ const runner = await detectPackageManager(cwd);
245
+ const args = ["run", script];
246
+ const commandResult = await spawnCommand(runner, args, cwd, {
247
+ captureOutput: Boolean(options.captureOutput),
248
+ maxOutputBytes: options.maxOutputBytes,
249
+ signal: options.signal,
250
+ timeoutMs: options.timeoutMs,
251
+ });
252
+ const status: DatabaseMigrationStatus =
253
+ commandResult.exitCode === 0
254
+ ? "current"
255
+ : commandResult.exitCode === databaseMigrationsPendingExitCode
256
+ ? "pending"
257
+ : "failed";
258
+
259
+ return {
260
+ schemaVersion: 1,
261
+ command: "status",
262
+ status,
263
+ cwd,
264
+ script,
265
+ runner,
266
+ args,
267
+ exitCode: commandResult.exitCode,
268
+ ...(options.captureOutput
269
+ ? { stdout: commandResult.stdout, stderr: commandResult.stderr }
270
+ : {}),
271
+ ...(commandResult.outputTruncated ? { outputTruncated: true } : {}),
272
+ ...(commandResult.timedOut ? { timedOut: true } : {}),
273
+ };
274
+ }
275
+
179
276
  /**
180
277
  * Sync the app-owned Drizzle schema entrypoint for Beignet provider tables.
181
278
  *
package/src/index.ts CHANGED
@@ -134,6 +134,8 @@ type CheckFlags = {
134
134
  json?: boolean;
135
135
  fix?: boolean;
136
136
  preflight?: boolean;
137
+ preflightConnect?: boolean;
138
+ connectTimeoutMs?: number;
137
139
  cwd?: string;
138
140
  };
139
141
 
@@ -142,6 +144,7 @@ type PreflightFlags = {
142
144
  envFile?: string;
143
145
  envModule?: string;
144
146
  connect?: boolean;
147
+ connectTimeoutMs?: number;
145
148
  serverModule?: string;
146
149
  cwd?: string;
147
150
  };
@@ -152,6 +155,11 @@ type DbFlags = {
152
155
  cwd?: string;
153
156
  };
154
157
 
158
+ type DbStatusFlags = {
159
+ json?: boolean;
160
+ cwd?: string;
161
+ };
162
+
155
163
  type DbSchemaSyncFlags = DbFlags & {
156
164
  dialect?: DatabaseSchemaDialect;
157
165
  tables?: readonly DatabaseSchemaTable[];
@@ -349,6 +357,14 @@ const parsedDateFlag = (brief: string) =>
349
357
  brief,
350
358
  }) as const;
351
359
 
360
+ const connectTimeoutMsFlag = {
361
+ kind: "parsed",
362
+ parse: parsePositiveInteger,
363
+ optional: true,
364
+ brief:
365
+ "Per migration-status or dependency health check timeout in milliseconds. Defaults to 5000.",
366
+ } as const;
367
+
352
368
  const cwdFlag = parsedStringFlag(
353
369
  "App directory. Defaults to the current working directory.",
354
370
  );
@@ -411,6 +427,11 @@ const dbFlagParameters = {
411
427
  cwd: cwdFlag,
412
428
  } satisfies FlagParametersForType<DbFlags, CliContext>;
413
429
 
430
+ const dbStatusFlagParameters = {
431
+ json: jsonFlag,
432
+ cwd: cwdFlag,
433
+ } satisfies FlagParametersForType<DbStatusFlags, CliContext>;
434
+
414
435
  const dbSchemaSyncFlagParameters = {
415
436
  ...dbFlagParameters,
416
437
  dialect: {
@@ -1097,7 +1118,7 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1097
1118
  docs: {
1098
1119
  brief: "Run the full app validation loop as one command.",
1099
1120
  fullDescription:
1100
- "Runs beignet lint, beignet doctor --strict, and the app's lint, typecheck, and test package scripts through the detected package manager. Every step runs even when an earlier one fails, so one run reports everything. Missing package scripts are skipped, not failed.",
1121
+ "Runs beignet lint, beignet doctor --strict, optional disconnected or connected preflight, and the app's lint, typecheck, and test package scripts through the detected package manager. Every step runs even when an earlier one fails, so one run reports everything. Missing package scripts are skipped, not failed.",
1101
1122
  },
1102
1123
  parameters: {
1103
1124
  flags: {
@@ -1112,8 +1133,17 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1112
1133
  kind: "boolean",
1113
1134
  optional: true,
1114
1135
  withNegated: false,
1115
- brief: "Append a beignet preflight step after doctor.",
1136
+ brief:
1137
+ "Append a disconnected beignet preflight environment step after doctor.",
1138
+ },
1139
+ preflightConnect: {
1140
+ kind: "boolean",
1141
+ optional: true,
1142
+ withNegated: false,
1143
+ brief:
1144
+ "Append beignet preflight --connect after doctor. Implies --preflight.",
1116
1145
  },
1146
+ connectTimeoutMs: connectTimeoutMsFlag,
1117
1147
  cwd: cwdFlag,
1118
1148
  } satisfies FlagParametersForType<CheckFlags, CliContext>,
1119
1149
  },
@@ -1128,6 +1158,8 @@ const checkCommand = buildCommand<CheckFlags, [], CliContext>({
1128
1158
  cwd: flags.cwd,
1129
1159
  fix: flags.fix,
1130
1160
  preflight: flags.preflight,
1161
+ preflightConnect: flags.preflightConnect,
1162
+ connectTimeoutMs: flags.connectTimeoutMs,
1131
1163
  color,
1132
1164
  ...(flags.json
1133
1165
  ? {}
@@ -1153,7 +1185,7 @@ const preflightCommand = buildCommand<PreflightFlags, [], CliContext>({
1153
1185
  docs: {
1154
1186
  brief: "Validate the deploy environment before shipping.",
1155
1187
  fullDescription:
1156
- "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.",
1188
+ "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. With --connect, it checks migration status before booting the server and running port health checks. Run it in the deploy pipeline where production configuration is present. Exits 1 on any error finding.",
1157
1189
  },
1158
1190
  parameters: {
1159
1191
  flags: {
@@ -1169,8 +1201,9 @@ const preflightCommand = buildCommand<PreflightFlags, [], CliContext>({
1169
1201
  optional: true,
1170
1202
  withNegated: false,
1171
1203
  brief:
1172
- "Boot the app server and run every port's checkHealth(). Needs network and real credentials.",
1204
+ "Check migration status, then boot the app server and run every port's checkHealth(). Needs network and real credentials.",
1173
1205
  },
1206
+ connectTimeoutMs: connectTimeoutMsFlag,
1174
1207
  serverModule: parsedStringFlag(
1175
1208
  "Module exporting getServer, used by --connect. Defaults to server/index.ts.",
1176
1209
  ),
@@ -1191,6 +1224,7 @@ const preflightCommand = buildCommand<PreflightFlags, [], CliContext>({
1191
1224
  envFile: flags.envFile,
1192
1225
  envModule: flags.envModule,
1193
1226
  connect: flags.connect,
1227
+ connectTimeoutMs: flags.connectTimeoutMs,
1194
1228
  serverModule: flags.serverModule,
1195
1229
  });
1196
1230
 
@@ -1374,6 +1408,42 @@ function databaseCommand(command: DatabaseCommand) {
1374
1408
  });
1375
1409
  }
1376
1410
 
1411
+ const databaseStatusCommand = buildCommand<DbStatusFlags, [], CliContext>({
1412
+ docs: {
1413
+ brief: "Inspect whether app-owned database migrations are current.",
1414
+ fullDescription:
1415
+ "Runs the app's read-only db:status script. Exit 0 means current, exit 2 means pending migrations, and any other non-zero exit means inspection failed.",
1416
+ },
1417
+ parameters: {
1418
+ flags: dbStatusFlagParameters,
1419
+ },
1420
+ loader: async () => {
1421
+ const { defaultDatabaseCommandMaxOutputBytes, runDatabaseStatus } =
1422
+ await import("./db.js");
1423
+
1424
+ return async function runDbStatus(this: CliContext, flags: DbStatusFlags) {
1425
+ const captureOutput = Boolean(flags.json);
1426
+ const result = await runDatabaseStatus({
1427
+ cwd: flags.cwd,
1428
+ captureOutput,
1429
+ maxOutputBytes: captureOutput
1430
+ ? defaultDatabaseCommandMaxOutputBytes
1431
+ : undefined,
1432
+ });
1433
+
1434
+ writeOutput(
1435
+ this,
1436
+ flags.json
1437
+ ? JSON.stringify(result, null, 2)
1438
+ : databaseStatusNextSteps(result),
1439
+ );
1440
+ if (result.exitCode !== 0) {
1441
+ this.process.exitCode = result.exitCode;
1442
+ }
1443
+ };
1444
+ },
1445
+ });
1446
+
1377
1447
  const dbSchemaSyncCommand = buildCommand<DbSchemaSyncFlags, [], CliContext>({
1378
1448
  docs: {
1379
1449
  brief: "Sync app-owned re-exports of Beignet provider tables.",
@@ -1427,7 +1497,8 @@ const dbRoutes = buildRouteMap({
1427
1497
  reset: databaseCommand("reset"),
1428
1498
  schema: dbSchemaRoutes,
1429
1499
  seed: databaseCommand("seed"),
1430
- } satisfies Record<DatabaseCommand | "schema", unknown>,
1500
+ status: databaseStatusCommand,
1501
+ } satisfies Record<DatabaseCommand | "schema" | "status", unknown>,
1431
1502
  });
1432
1503
 
1433
1504
  const taskRunCommand = buildCommand<TaskRunFlags, [string], CliContext>({
@@ -2398,6 +2469,14 @@ type DatabaseCommandNextStepsResult = {
2398
2469
  dryRun: boolean;
2399
2470
  };
2400
2471
 
2472
+ type DatabaseStatusNextStepsResult = {
2473
+ status: "current" | "pending" | "failed";
2474
+ cwd: string;
2475
+ script: string;
2476
+ runner: string;
2477
+ args: string[];
2478
+ };
2479
+
2401
2480
  type DatabaseSchemaSyncNextStepsResult = {
2402
2481
  cwd: string;
2403
2482
  dialect: DatabaseSchemaDialect;
@@ -2632,6 +2711,7 @@ Next steps:
2632
2711
  ${envStep}
2633
2712
  ${startDatabaseStep}Prepare the database:
2634
2713
  ${cli} db migrate
2714
+ ${cli} db status
2635
2715
 
2636
2716
  Start the app:
2637
2717
  ${run} dev
@@ -2646,6 +2726,7 @@ Generate a feature:
2646
2726
  ${cli} make feature projects
2647
2727
  ${cli} db generate
2648
2728
  ${cli} db migrate
2729
+ ${cli} db status
2649
2730
  ${cli} check`;
2650
2731
  }
2651
2732
 
@@ -2722,7 +2803,7 @@ function makeResourceNextSteps(result: MakeNextStepsResult): string {
2722
2803
  return makeNextSteps(result, "resource", [
2723
2804
  "Use make resource when the feature maps cleanly to a REST resource with repository-backed persistence.",
2724
2805
  "Review the generated schemas and repository fields.",
2725
- "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
2806
+ "If this app uses Drizzle, run beignet db generate, beignet db migrate, and beignet db status so the new schema is ready and verified.",
2726
2807
  "Run your app's lint, test, and typecheck commands, then beignet lint and beignet doctor.",
2727
2808
  ]);
2728
2809
  }
@@ -2739,6 +2820,25 @@ Command:
2739
2820
  ${command}`;
2740
2821
  }
2741
2822
 
2823
+ function databaseStatusNextSteps(
2824
+ result: DatabaseStatusNextStepsResult,
2825
+ ): string {
2826
+ const command = [result.runner, ...result.args].join(" ");
2827
+ const summary =
2828
+ result.status === "current"
2829
+ ? "Database migrations are current."
2830
+ : result.status === "pending"
2831
+ ? "Database migrations are pending."
2832
+ : "Database migration status could not be determined.";
2833
+
2834
+ return `${summary}
2835
+
2836
+ Ran ${result.script} in ${result.cwd}
2837
+
2838
+ Command:
2839
+ ${command}`;
2840
+ }
2841
+
2742
2842
  function databaseSchemaSyncNextSteps(
2743
2843
  result: DatabaseSchemaSyncNextStepsResult,
2744
2844
  ): string {
@@ -2759,6 +2859,7 @@ ${skippedFiles ? `\nSkipped identical files:\n${skippedFiles}` : ""}
2759
2859
  Next steps:
2760
2860
  Run beignet db generate to produce app-owned Drizzle migrations.
2761
2861
  Run beignet db migrate to apply the migration.
2862
+ Run beignet db status to verify the target database.
2762
2863
  Run beignet doctor --strict to check provider setup.`;
2763
2864
  }
2764
2865
 
@@ -2766,7 +2867,7 @@ function makeFeatureNextSteps(result: MakeNextStepsResult): string {
2766
2867
  return makeNextSteps(result, "feature slice", [
2767
2868
  "Use make feature for product capabilities and workflows. Use make resource when the concept is mostly CRUD-shaped.",
2768
2869
  "Treat the generated name field as a placeholder and shape the contracts, use cases, and repository around the feature's real workflow.",
2769
- "If this app uses Drizzle, run beignet db generate and beignet db migrate so the new schema is ready.",
2870
+ "If this app uses Drizzle, run beignet db generate, beignet db migrate, and beignet db status so the new schema is ready and verified.",
2770
2871
  "Run your app's lint, test, and typecheck commands, then beignet lint and beignet doctor.",
2771
2872
  ]);
2772
2873
  }
package/src/inspect.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  import {
44
44
  detectedProviderVariants,
45
45
  diagnosticPackageJsonFile,
46
+ formatProviderEnvAlternatives,
46
47
  formatProviderMetadataIssues,
47
48
  installedPackageNames,
48
49
  providerDoctorRulesForInstalledPackages,
@@ -2619,6 +2620,27 @@ async function inspectProductionReadiness(
2619
2620
  provider.variants,
2620
2621
  providerEntries,
2621
2622
  )) {
2623
+ const envAlternatives = configuredProviderRequiredEnvAlternatives(
2624
+ variant.requiredEnvAlternatives,
2625
+ config,
2626
+ );
2627
+ if (
2628
+ envAlternatives.length > 0 &&
2629
+ !(await providerRequiredEnvAlternativeExists(
2630
+ targetDir,
2631
+ configFiles,
2632
+ envAlternatives,
2633
+ sourceCache,
2634
+ ))
2635
+ ) {
2636
+ diagnostics.push({
2637
+ severity: "warning",
2638
+ code: "BEIGNET_PROVIDER_ENV_MISSING",
2639
+ file: "package.json",
2640
+ message: `${provider.packageName} is installed and ${variant.displayName} is registered, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the provider registration.`,
2641
+ });
2642
+ }
2643
+
2622
2644
  for (const envVar of configuredProviderRequiredEnv(
2623
2645
  variant.requiredEnv,
2624
2646
  config,
@@ -2669,6 +2691,27 @@ async function inspectProductionReadiness(
2669
2691
  continue;
2670
2692
  }
2671
2693
 
2694
+ const envAlternatives = configuredProviderRequiredEnvAlternatives(
2695
+ provider.requiredEnvAlternatives ?? [],
2696
+ config,
2697
+ );
2698
+ if (
2699
+ envAlternatives.length > 0 &&
2700
+ !(await providerRequiredEnvAlternativeExists(
2701
+ targetDir,
2702
+ configFiles,
2703
+ envAlternatives,
2704
+ sourceCache,
2705
+ ))
2706
+ ) {
2707
+ diagnostics.push({
2708
+ severity: "warning",
2709
+ code: "BEIGNET_PROVIDER_ENV_MISSING",
2710
+ file: "package.json",
2711
+ message: `${provider.packageName} is installed, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the unused provider package.`,
2712
+ });
2713
+ }
2714
+
2672
2715
  for (const envVar of configuredProviderRequiredEnv(
2673
2716
  provider.requiredEnv ?? [],
2674
2717
  config,
@@ -5175,7 +5218,7 @@ async function inspectDatabaseLifecycleDrift(
5175
5218
  }
5176
5219
 
5177
5220
  if (hasDrizzleArtifacts) {
5178
- for (const script of ["db:generate", "db:migrate"] as const) {
5221
+ for (const script of ["db:generate", "db:migrate", "db:status"] as const) {
5179
5222
  if (packageScripts[script]) continue;
5180
5223
  diagnostics.push({
5181
5224
  severity: "warning",
@@ -5257,6 +5300,12 @@ async function inspectDatabaseLifecycleDrift(
5257
5300
  file: `${infrastructurePath}/db/reset.ts`,
5258
5301
  code: "BEIGNET_DB_RESET_ENTRYPOINT_MISSING",
5259
5302
  },
5303
+ {
5304
+ script: "db:status",
5305
+ command: "status",
5306
+ file: `${infrastructurePath}/db/migration-status.ts`,
5307
+ code: "BEIGNET_DB_STATUS_ENTRYPOINT_MISSING",
5308
+ },
5260
5309
  ] as const;
5261
5310
 
5262
5311
  for (const entrypoint of standardEntrypoints) {
@@ -6284,6 +6333,32 @@ function configuredProviderRequiredEnv(
6284
6333
  return envVars.filter((envVar) => !ignored.has(envVar));
6285
6334
  }
6286
6335
 
6336
+ function configuredProviderRequiredEnvAlternatives(
6337
+ alternatives: readonly (readonly string[])[],
6338
+ config: ResolvedBeignetConfig,
6339
+ ): string[][] {
6340
+ return alternatives.map((alternative) =>
6341
+ configuredProviderRequiredEnv(alternative, config),
6342
+ );
6343
+ }
6344
+
6345
+ async function providerRequiredEnvAlternativeExists(
6346
+ targetDir: string,
6347
+ configFiles: readonly string[],
6348
+ alternatives: readonly (readonly string[])[],
6349
+ sourceCache: Map<string, string>,
6350
+ ): Promise<boolean> {
6351
+ for (const alternative of alternatives) {
6352
+ const exists = await Promise.all(
6353
+ alternative.map((envVar) =>
6354
+ providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache),
6355
+ ),
6356
+ );
6357
+ if (exists.every(Boolean)) return true;
6358
+ }
6359
+ return false;
6360
+ }
6361
+
6287
6362
  function configuredProviderRequiredTable(
6288
6363
  tableName: string,
6289
6364
  config: ResolvedBeignetConfig,
package/src/lib.ts CHANGED
@@ -29,14 +29,22 @@ export type { CreateOptions } from "./create.js";
29
29
  export { createProject } from "./create.js";
30
30
  export type {
31
31
  DatabaseCommand,
32
+ DatabaseMigrationStatus,
32
33
  DatabaseSchemaDialect,
33
34
  DatabaseSchemaTable,
34
35
  RunDatabaseCommandOptions,
35
36
  RunDatabaseCommandResult,
37
+ RunDatabaseStatusOptions,
38
+ RunDatabaseStatusResult,
36
39
  SyncDatabaseSchemaOptions,
37
40
  SyncDatabaseSchemaResult,
38
41
  } from "./db.js";
39
- export { runDatabaseCommand, syncDatabaseSchema } from "./db.js";
42
+ export {
43
+ databaseMigrationsPendingExitCode,
44
+ runDatabaseCommand,
45
+ runDatabaseStatus,
46
+ syncDatabaseSchema,
47
+ } from "./db.js";
40
48
  export type {
41
49
  DoctorFixApplyResult,
42
50
  DoctorFixFileChange,
package/src/make/inbox.ts CHANGED
@@ -226,7 +226,7 @@ export async function makeInbox(
226
226
  */
227
227
  export function makeInboxNextStepLines(): string[] {
228
228
  return [
229
- "Run beignet db generate and beignet db migrate so inbox_notifications exists.",
229
+ "Run beignet db generate, beignet db migrate, and beignet db status so inbox_notifications exists and is verified.",
230
230
  'Add an in-app channel to any feature notification: import { defineInboxNotificationChannel } from "@/features/inbox/channel" and add inApp: defineInboxNotificationChannel(({ payload }) => ({ userId: payload.userId, type: "your-type", title: "Something happened" })) to its channels.',
231
231
  "Render the unread badge from features/inbox/components/unread-badge.tsx next to your Inbox nav link for a live unread count.",
232
232
  "Run your app's test, lint, and typecheck commands, then beignet lint and beignet doctor.",
@@ -209,7 +209,7 @@ export function makePaymentsNextStepLines(
209
209
  return [
210
210
  "The free plan works out of the box; billing accounts without an active subscription resolve to it automatically.",
211
211
  "Set BILLING_PRO_PRICE_ID in .env.local to the provider price ID for your pro plan.",
212
- "Run beignet db generate and beignet db migrate so billing_accounts exists before handling webhooks.",
212
+ "Run beignet db generate, beignet db migrate, and beignet db status so billing_accounts exists and is verified before handling webhooks.",
213
213
  'Gate paid actions once usage passes FREE_PLAN_LIMITS: await requireEntitlement(ctx, { entitlement: "todos.create", subject: { type: "tenant", id: tenantId } });',
214
214
  ...(hasPlanUi
215
215
  ? [
@@ -249,7 +249,7 @@ export async function makeTenancy(
249
249
  */
250
250
  export function makeTenancyNextStepLines(): string[] {
251
251
  return [
252
- "Run beignet db generate and beignet db migrate so the workspaces, workspace_members, and workspace_invites tables exist.",
252
+ "Run beignet db generate, beignet db migrate, and beignet db status so the workspaces, workspace_members, and workspace_invites tables exist and are verified.",
253
253
  "Requests resolve the active workspace from memberships and the beignet-workspace cookie; users without a workspace have no tenant, so point them at Settings > Workspace to create one.",
254
254
  "Run the db:seed script for a demo workspace with a sign-in-able demo admin (demo-admin@example.com).",
255
255
  "Invite emails send through the in-memory mailer in development; swap server/providers.ts to a real mail provider such as @beignet/provider-mail-resend for delivery.",