@beignet/cli 0.0.47 → 0.0.49

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 (144) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +126 -42
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +10 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +66 -6
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map.d.ts.map +1 -1
  24. package/dist/app-map.js +311 -92
  25. package/dist/app-map.js.map +1 -1
  26. package/dist/check.d.ts +8 -1
  27. package/dist/check.d.ts.map +1 -1
  28. package/dist/check.js +55 -9
  29. package/dist/check.js.map +1 -1
  30. package/dist/db.d.ts +38 -0
  31. package/dist/db.d.ts.map +1 -1
  32. package/dist/db.js +52 -0
  33. package/dist/db.js.map +1 -1
  34. package/dist/explain.js +4 -4
  35. package/dist/explain.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +73 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts.map +1 -1
  40. package/dist/inspect.js +454 -170
  41. package/dist/inspect.js.map +1 -1
  42. package/dist/lib.d.ts +2 -2
  43. package/dist/lib.d.ts.map +1 -1
  44. package/dist/lib.js +1 -1
  45. package/dist/lib.js.map +1 -1
  46. package/dist/lint.d.ts.map +1 -1
  47. package/dist/lint.js +309 -487
  48. package/dist/lint.js.map +1 -1
  49. package/dist/make/inbox.d.ts.map +1 -1
  50. package/dist/make/inbox.js +12 -5
  51. package/dist/make/inbox.js.map +1 -1
  52. package/dist/make/payments.js +3 -3
  53. package/dist/make/payments.js.map +1 -1
  54. package/dist/make/tenancy.js +1 -1
  55. package/dist/make/tenancy.js.map +1 -1
  56. package/dist/make.d.ts.map +1 -1
  57. package/dist/make.js +144 -8
  58. package/dist/make.js.map +1 -1
  59. package/dist/mcp.d.ts +1 -1
  60. package/dist/mcp.d.ts.map +1 -1
  61. package/dist/mcp.js +56 -19
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/preflight.d.ts +11 -3
  64. package/dist/preflight.d.ts.map +1 -1
  65. package/dist/preflight.js +139 -16
  66. package/dist/preflight.js.map +1 -1
  67. package/dist/provider-add.d.ts.map +1 -1
  68. package/dist/provider-add.js +149 -9
  69. package/dist/provider-add.js.map +1 -1
  70. package/dist/provider-audit.d.ts +13 -0
  71. package/dist/provider-audit.d.ts.map +1 -1
  72. package/dist/provider-audit.js +122 -8
  73. package/dist/provider-audit.js.map +1 -1
  74. package/dist/registry-edits.d.ts +7 -0
  75. package/dist/registry-edits.d.ts.map +1 -1
  76. package/dist/registry-edits.js +237 -53
  77. package/dist/registry-edits.js.map +1 -1
  78. package/dist/templates/agents.d.ts.map +1 -1
  79. package/dist/templates/agents.js +29 -7
  80. package/dist/templates/agents.js.map +1 -1
  81. package/dist/templates/base.d.ts +1 -0
  82. package/dist/templates/base.d.ts.map +1 -1
  83. package/dist/templates/base.js +20 -6
  84. package/dist/templates/base.js.map +1 -1
  85. package/dist/templates/db/index.d.ts +1 -0
  86. package/dist/templates/db/index.d.ts.map +1 -1
  87. package/dist/templates/db/index.js.map +1 -1
  88. package/dist/templates/db/migration-status.d.ts +9 -0
  89. package/dist/templates/db/migration-status.d.ts.map +1 -0
  90. package/dist/templates/db/migration-status.js +117 -0
  91. package/dist/templates/db/migration-status.js.map +1 -0
  92. package/dist/templates/db/mysql.d.ts.map +1 -1
  93. package/dist/templates/db/mysql.js +23 -0
  94. package/dist/templates/db/mysql.js.map +1 -1
  95. package/dist/templates/db/postgres.d.ts.map +1 -1
  96. package/dist/templates/db/postgres.js +21 -0
  97. package/dist/templates/db/postgres.js.map +1 -1
  98. package/dist/templates/db/sqlite.d.ts.map +1 -1
  99. package/dist/templates/db/sqlite.js +53 -0
  100. package/dist/templates/db/sqlite.js.map +1 -1
  101. package/dist/templates/index.d.ts.map +1 -1
  102. package/dist/templates/index.js +5 -0
  103. package/dist/templates/index.js.map +1 -1
  104. package/dist/templates/server.d.ts.map +1 -1
  105. package/dist/templates/server.js +2 -0
  106. package/dist/templates/server.js.map +1 -1
  107. package/dist/templates/shared.d.ts +1 -0
  108. package/dist/templates/shared.d.ts.map +1 -1
  109. package/dist/templates/shared.js +4 -2
  110. package/dist/templates/shared.js.map +1 -1
  111. package/package.json +7 -6
  112. package/skills/app-structure/SKILL.md +56 -11
  113. package/src/analysis/contract-factories.ts +256 -0
  114. package/src/analysis/layers.ts +266 -0
  115. package/src/analysis/port-wiring.ts +506 -0
  116. package/src/analysis/source-index.ts +225 -57
  117. package/src/analysis/workspace.ts +115 -6
  118. package/src/app-map.ts +418 -109
  119. package/src/check.ts +86 -10
  120. package/src/db.ts +97 -0
  121. package/src/explain.ts +4 -4
  122. package/src/index.ts +108 -7
  123. package/src/inspect.ts +659 -216
  124. package/src/lib.ts +9 -1
  125. package/src/lint.ts +393 -643
  126. package/src/make/inbox.ts +12 -5
  127. package/src/make/payments.ts +3 -3
  128. package/src/make/tenancy.ts +1 -1
  129. package/src/make.ts +225 -8
  130. package/src/mcp.ts +73 -21
  131. package/src/preflight.ts +195 -16
  132. package/src/provider-add.ts +193 -9
  133. package/src/provider-audit.ts +210 -18
  134. package/src/registry-edits.ts +313 -63
  135. package/src/templates/agents.ts +29 -7
  136. package/src/templates/base.ts +21 -4
  137. package/src/templates/db/index.ts +1 -0
  138. package/src/templates/db/migration-status.ts +126 -0
  139. package/src/templates/db/mysql.ts +23 -0
  140. package/src/templates/db/postgres.ts +21 -0
  141. package/src/templates/db/sqlite.ts +53 -0
  142. package/src/templates/index.ts +5 -0
  143. package/src/templates/server.ts +2 -0
  144. package/src/templates/shared.ts +4 -2
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.",
@@ -518,9 +518,9 @@ export const inboxNotificationSchema = z.object({
518
518
  });
519
519
 
520
520
  export const listInboxQuerySchema = z.object({
521
- \tlimit: z.coerce.number().int().min(1).max(50).default(20),
521
+ \tlimit: z.number().int().min(1).max(50).default(20),
522
522
  \tcursor: z.string().min(1).optional(),
523
- \tunread: z.coerce.boolean().default(false),
523
+ \tunread: z.boolean().default(false),
524
524
  });
525
525
 
526
526
  export const listInboxOutputSchema = z.object({
@@ -659,7 +659,7 @@ export function inboxContractsFile(config: ResolvedBeignetConfig): string {
659
659
  );
660
660
  const errorsModule = aliasModule(resourceSharedErrorsPath(config));
661
661
 
662
- return `import { defineContractGroup } from "@beignet/core/contracts";
662
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
663
663
  import {
664
664
  \tinboxNotificationSchema,
665
665
  \tlistInboxOutputSchema,
@@ -674,7 +674,14 @@ const inbox = defineContractGroup().namespace("inbox").prefix("/api/inbox");
674
674
 
675
675
  export const listInbox = inbox
676
676
  \t.get("/")
677
- \t.query(listInboxQuerySchema)
677
+ \t.query(
678
+ \t\tlistInboxQuerySchema,
679
+ \t\tdefineQueryTransport({
680
+ \t\t\tlimit: query.integer(),
681
+ \t\t\tcursor: query.string(),
682
+ \t\t\tunread: query.boolean(),
683
+ \t\t}),
684
+ \t)
678
685
  \t.meta({ auth: "required" })
679
686
  \t.errors({ Unauthorized: errors.Unauthorized })
680
687
  \t.responses({
@@ -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
  ? [
@@ -862,7 +862,7 @@ export function isBillingAccountActive(
862
862
  export function billingContractsFile(
863
863
  idempotencyScope: "actor" | "actor-tenant" = "actor",
864
864
  ): string {
865
- return `import { defineContractGroup } from "@beignet/core/contracts";
865
+ return `import { defineContractGroup, defineQueryTransport } from "@beignet/core/contracts";
866
866
  import { z } from "zod";
867
867
  import {
868
868
  \tbillingStatusOutputSchema,
@@ -882,7 +882,7 @@ const checkoutBilling = billing.headers(checkoutHeadersSchema);
882
882
 
883
883
  export const getBillingStatus = billing
884
884
  \t.get("/")
885
- \t.query(z.object({}))
885
+ \t.query(z.object({}), defineQueryTransport({}))
886
886
  \t.meta({ auth: "required" })
887
887
  \t.errors({ Unauthorized: errors.Unauthorized })
888
888
  \t.responses({
@@ -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.",
package/src/make.ts CHANGED
@@ -2,6 +2,8 @@ import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import ts from "typescript";
5
+ import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
6
+ import { listProjectFiles } from "./analysis/workspace.js";
5
7
  import type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
6
8
  import {
7
9
  type BeignetConfig,
@@ -1084,6 +1086,10 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1084
1086
  await assertFeatureArtifactApp(targetDir, config, "job");
1085
1087
  await assertServerRuntimeApp(targetDir, config, "job");
1086
1088
  await updateOutboxPortWiring(targetDir, config, { dryRun: true });
1089
+ const jobDispatcherPlan = await planInlineJobDispatcherWiring(
1090
+ targetDir,
1091
+ config,
1092
+ );
1087
1093
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1088
1094
  force: Boolean(options.force),
1089
1095
  });
@@ -1114,6 +1120,9 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1114
1120
  await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
1115
1121
  dryRun: Boolean(options.dryRun),
1116
1122
  });
1123
+ await applyInlineJobDispatcherWiring(result, targetDir, jobDispatcherPlan, {
1124
+ dryRun: Boolean(options.dryRun),
1125
+ });
1117
1126
  await applyOutboxPortWiring(result, targetDir, config, {
1118
1127
  dryRun: Boolean(options.dryRun),
1119
1128
  });
@@ -1124,6 +1133,201 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1124
1133
  return result;
1125
1134
  }
1126
1135
 
1136
+ type InlineJobDispatcherWiringPlan = {
1137
+ changes: { path: string; before: string; after: string }[];
1138
+ };
1139
+
1140
+ async function planInlineJobDispatcherWiring(
1141
+ targetDir: string,
1142
+ config: ResolvedBeignetConfig,
1143
+ ): Promise<InlineJobDispatcherWiringPlan> {
1144
+ const portsFile = config.paths.ports;
1145
+ const portWiringFile = config.paths.portWiring;
1146
+ const providersFile = providersFilePath(config);
1147
+ const ports = await readFile(path.join(targetDir, portsFile), "utf8");
1148
+ const portWiring = await readFile(
1149
+ path.join(targetDir, portWiringFile),
1150
+ "utf8",
1151
+ );
1152
+ const analysis = await analyzePortWiringFiles({
1153
+ targetDir,
1154
+ files: await listProjectFiles(targetDir),
1155
+ config,
1156
+ });
1157
+ const declaresJobs = analysis.declared.has("jobs");
1158
+ const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
1159
+
1160
+ if (declaresJobs && wiresJobs) {
1161
+ return { changes: [] };
1162
+ }
1163
+ if (!declaresJobs && analysis.declarationIndeterminate) {
1164
+ throw new Error(
1165
+ `Could not prove whether AppPorts in ${portsFile} declares the jobs port. Preserve the app's custom port type and add jobs: JobDispatcherPort manually before running make job.`,
1166
+ );
1167
+ }
1168
+ if (!wiresJobs && analysis.indeterminate) {
1169
+ throw new Error(
1170
+ `Could not prove how the jobs port is wired in ${portWiringFile}. Preserve the app's custom dispatcher by binding or deferring "jobs" in a statically inspectable definePorts(...) object before running make job.`,
1171
+ );
1172
+ }
1173
+
1174
+ let nextPorts = ports;
1175
+ if (!declaresJobs && !analysis.appPortsInferredFromWiring) {
1176
+ nextPorts = addNamedTypeImport(
1177
+ nextPorts,
1178
+ "JobDispatcherPort",
1179
+ "@beignet/core/ports",
1180
+ );
1181
+ nextPorts = insertTypeProperty(
1182
+ nextPorts,
1183
+ "AppPorts",
1184
+ "jobs: JobDispatcherPort;",
1185
+ `Could not find AppPorts in ${portsFile}. Add jobs: JobDispatcherPort manually and wire a dispatcher, or restore the generated ports file before running make job.`,
1186
+ );
1187
+ }
1188
+
1189
+ if (wiresJobs) {
1190
+ return {
1191
+ changes:
1192
+ ports === nextPorts
1193
+ ? []
1194
+ : [{ path: portsFile, before: ports, after: nextPorts }],
1195
+ };
1196
+ }
1197
+
1198
+ const nextPortWiring = appendDeferredPortKey(portWiring, "jobs");
1199
+ if (nextPortWiring === portWiring) {
1200
+ throw new Error(
1201
+ `Could not find the deferred port list in ${portWiringFile}. Add "jobs" manually and wire a dispatcher, or restore the generated port wiring file before running make job.`,
1202
+ );
1203
+ }
1204
+
1205
+ const providersPath = path.join(targetDir, providersFile);
1206
+ const providers = await readOptionalFile(providersPath);
1207
+ if (providers === undefined) {
1208
+ throw new Error(
1209
+ `Could not find ${providersFile}. Register an inline or provider-backed jobs dispatcher manually, or restore the generated providers file before running make job.`,
1210
+ );
1211
+ }
1212
+ let nextProviders = addNamedImport(
1213
+ providers,
1214
+ "createInlineJobDispatcher",
1215
+ "@beignet/core/jobs",
1216
+ );
1217
+ nextProviders = addNamedImport(
1218
+ nextProviders,
1219
+ "createProvider",
1220
+ "@beignet/core/providers",
1221
+ );
1222
+ nextProviders = addNamedTypeImport(
1223
+ nextProviders,
1224
+ "AppContext",
1225
+ aliasModule(config.paths.appContext),
1226
+ );
1227
+ nextProviders = addNamedTypeImport(
1228
+ nextProviders,
1229
+ "AppPorts",
1230
+ aliasModule(config.paths.ports),
1231
+ );
1232
+ nextProviders = addNamedTypeImport(
1233
+ nextProviders,
1234
+ "AppServiceContextInput",
1235
+ aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
1236
+ );
1237
+
1238
+ const providerName = "inlineJobsProvider";
1239
+ const generatedProviderMarker = "// beignet:generated-inline-jobs-provider";
1240
+ const providersSourceFile = ts.createSourceFile(
1241
+ providersFile,
1242
+ nextProviders,
1243
+ ts.ScriptTarget.Latest,
1244
+ true,
1245
+ ts.ScriptKind.TS,
1246
+ );
1247
+ const providerDeclaration = providersSourceFile.statements
1248
+ .filter(ts.isVariableStatement)
1249
+ .flatMap((statement) => [...statement.declarationList.declarations])
1250
+ .find(
1251
+ (declaration) =>
1252
+ ts.isIdentifier(declaration.name) &&
1253
+ declaration.name.text === providerName,
1254
+ );
1255
+ const providerDeclarationExists = providerDeclaration !== undefined;
1256
+ const providerIsGenerated = providerDeclaration
1257
+ ? nextProviders
1258
+ .slice(0, providerDeclaration.getStart(providersSourceFile))
1259
+ .trimEnd()
1260
+ .endsWith(generatedProviderMarker)
1261
+ : false;
1262
+ if (providerDeclarationExists && !providerIsGenerated) {
1263
+ throw new Error(
1264
+ `Could not install the generated inline jobs dispatcher because ${providerName} already exists in ${providersFile}. Rename the app-owned declaration or wire its jobs port explicitly before running make job.`,
1265
+ );
1266
+ }
1267
+ if (!providerDeclarationExists) {
1268
+ const definition = `// beignet:generated-inline-jobs-provider
1269
+ const ${providerName} = createProvider<
1270
+ {},
1271
+ AppContext,
1272
+ AppServiceContextInput
1273
+ >()({
1274
+ name: "inline-jobs",
1275
+ setup({ createServiceContext }): { ports: Pick<AppPorts, "jobs"> } {
1276
+ return {
1277
+ ports: {
1278
+ jobs: createInlineJobDispatcher<AppContext>({
1279
+ ctx: () => createServiceContext(undefined),
1280
+ }),
1281
+ },
1282
+ };
1283
+ },
1284
+ });
1285
+ `;
1286
+ const inserted = nextProviders.replace(
1287
+ /\nexport const providers = \[/,
1288
+ `\n${definition}\nexport const providers = [`,
1289
+ );
1290
+ if (inserted === nextProviders) {
1291
+ throw new Error(
1292
+ `Could not find the exported providers array in ${providersFile}. Register an inline or provider-backed jobs dispatcher manually, or restore the generated providers file before running make job.`,
1293
+ );
1294
+ }
1295
+ nextProviders = inserted;
1296
+ }
1297
+ const appended = appendToNamedArray(nextProviders, "providers", providerName);
1298
+ if (appended.kind === "missing") {
1299
+ throw new Error(
1300
+ `Could not find the exported providers array in ${providersFile}. Register ${providerName} manually, or restore the generated providers file before running make job.`,
1301
+ );
1302
+ }
1303
+ if (appended.kind === "updated") nextProviders = appended.source;
1304
+
1305
+ return {
1306
+ changes: [
1307
+ { path: portsFile, before: ports, after: nextPorts },
1308
+ { path: portWiringFile, before: portWiring, after: nextPortWiring },
1309
+ { path: providersFile, before: providers, after: nextProviders },
1310
+ ].filter((change) => change.before !== change.after),
1311
+ };
1312
+ }
1313
+
1314
+ async function applyInlineJobDispatcherWiring(
1315
+ result: MakeResult,
1316
+ targetDir: string,
1317
+ plan: InlineJobDispatcherWiringPlan,
1318
+ options: { dryRun: boolean },
1319
+ ): Promise<void> {
1320
+ for (const change of plan.changes) {
1321
+ if (!options.dryRun) {
1322
+ await writeFile(path.join(targetDir, change.path), change.after);
1323
+ }
1324
+ if (!result.updatedFiles.includes(change.path)) {
1325
+ result.updatedFiles.push(change.path);
1326
+ }
1327
+ if (!result.files.includes(change.path)) result.files.push(change.path);
1328
+ }
1329
+ }
1330
+
1127
1331
  export async function makeTask(
1128
1332
  options: MakeTaskOptions,
1129
1333
  ): Promise<MakeTaskResult> {
@@ -3983,8 +4187,8 @@ export function decode${names.singularPascal}Cursor(cursor: string): ${names.sin
3983
4187
 
3984
4188
  export const List${names.pluralPascal}InputSchema = z
3985
4189
  .object({
3986
- limit: z.coerce.number().int().min(1).max(100).default(20),
3987
- cursor: z.string().nullable().default(null),
4190
+ limit: z.number().int().min(1).max(100).default(20),
4191
+ cursor: z.string().optional().transform((value) => value ?? null),
3988
4192
  name: z.string().trim().min(1).max(120).optional(),
3989
4193
  sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
3990
4194
  sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
@@ -4840,7 +5044,7 @@ function contractFile(
4840
5044
  mode: ResourceGenerationMode,
4841
5045
  options: ResourceGenerationOptions,
4842
5046
  ): string {
4843
- return `import { defineContractGroup } from "@beignet/core/contracts";
5047
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
4844
5048
  import { z } from "zod";
4845
5049
  ${mode === "resource" ? `import { errors } from "${aliasModule(resourceSharedErrorsPath(config))}";\n` : ""}import {
4846
5050
  Create${names.singularPascal}InputSchema,
@@ -4863,7 +5067,16 @@ const ${names.pluralCamel} = defineContractGroup()
4863
5067
 
4864
5068
  export const list${names.pluralPascal} = ${names.pluralCamel}
4865
5069
  .get("/api/${names.pluralKebab}")
4866
- .query(List${names.pluralPascal}InputSchema)
5070
+ .query(
5071
+ List${names.pluralPascal}InputSchema,
5072
+ defineQueryTransport({
5073
+ limit: query.integer(),
5074
+ cursor: query.string(),
5075
+ name: query.string(),
5076
+ sortBy: query.string(),
5077
+ sortDirection: query.string(),
5078
+ }),
5079
+ )
4867
5080
  .responses({
4868
5081
  200: List${names.pluralPascal}OutputSchema,
4869
5082
  });
@@ -4908,7 +5121,7 @@ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization:
4908
5121
  }
4909
5122
 
4910
5123
  function standaloneContractFile(names: ResourceNames): string {
4911
- return `import { defineContractGroup } from "@beignet/core/contracts";
5124
+ return `import { defineContractGroup, defineQueryTransport, query } from "@beignet/core/contracts";
4912
5125
  import { z } from "zod";
4913
5126
 
4914
5127
  const ErrorResponseSchema = z.object({
@@ -4932,8 +5145,12 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
4932
5145
  .get("/api/${names.pluralKebab}")
4933
5146
  .query(
4934
5147
  z.object({
4935
- limit: z.coerce.number().int().min(1).max(100).default(20),
4936
- offset: z.coerce.number().int().min(0).default(0),
5148
+ limit: z.number().int().min(1).max(100).default(20),
5149
+ offset: z.number().int().min(0).default(0),
5150
+ }),
5151
+ defineQueryTransport({
5152
+ limit: query.integer(),
5153
+ offset: query.integer(),
4937
5154
  }),
4938
5155
  )
4939
5156
  .responses({
@@ -6038,7 +6255,7 @@ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
6038
6255
  );
6039
6256
  const secondPage = await tester.run(
6040
6257
  list${names.pluralPascal}UseCase,
6041
- { limit: 2, cursor: firstPage.page.nextCursor },
6258
+ { limit: 2, cursor: firstPage.page.nextCursor! },
6042
6259
  { ctx },
6043
6260
  );
6044
6261
  const nameAscendingPage = await tester.run(
package/src/mcp.ts CHANGED
@@ -1,11 +1,8 @@
1
1
  import { open, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import {
4
- McpServer,
5
- ResourceTemplate,
6
- } from "@modelcontextprotocol/sdk/server/mcp.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import { z } from "zod";
3
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
5
+ import { z } from "zod/v4";
9
6
  import { mapApp, projectAppMap } from "./app-map.js";
10
7
  import { appMapNodeKinds } from "./app-map-schema.js";
11
8
  import { checkApp } from "./check.js";
@@ -23,6 +20,7 @@ import {
23
20
  defaultDatabaseCommandMaxOutputBytes,
24
21
  defaultDatabaseCommandTimeoutMs,
25
22
  runDatabaseCommand,
23
+ runDatabaseStatus,
26
24
  syncDatabaseSchema,
27
25
  } from "./db.js";
28
26
  import { explainApp, explainTargetKinds } from "./explain.js";
@@ -639,7 +637,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
639
637
  "check",
640
638
  {
641
639
  description:
642
- "Run the full Beignet app validation loop: Beignet lint, strict doctor, and the app's lint, typecheck, and test scripts. Returns the same versioned JSON as beignet check --json, including every step, bounded failure output, durations, and overall ok status. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
640
+ "Run the full Beignet app validation loop: Beignet lint, strict doctor, optional disconnected or connected preflight, and the app's lint, typecheck, and test scripts. Returns the same versioned JSON as beignet check --json, including every step, bounded failure output, durations, and overall ok status. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
643
641
  inputSchema: {
644
642
  preflight: z
645
643
  .boolean()
@@ -647,6 +645,21 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
647
645
  .describe(
648
646
  "Include the environment preflight check before app package scripts. Defaults to false.",
649
647
  ),
648
+ preflightConnect: z
649
+ .boolean()
650
+ .optional()
651
+ .describe(
652
+ "Include connected preflight with migration status and dependency health checks. Implies preflight and defaults to false.",
653
+ ),
654
+ connectTimeoutMs: z
655
+ .number()
656
+ .int()
657
+ .min(1)
658
+ .max(60 * 60 * 1_000)
659
+ .optional()
660
+ .describe(
661
+ "Maximum duration for migration status and each dependency health check in milliseconds. Defaults to 5000.",
662
+ ),
650
663
  timeoutMs: z
651
664
  .number()
652
665
  .int()
@@ -664,14 +677,16 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
664
677
  openWorldHint: true,
665
678
  },
666
679
  },
667
- async (input, extra) =>
680
+ async (input, ctx) =>
668
681
  safeToolResult(async () =>
669
682
  jsonResult(
670
683
  await checkApp({
671
684
  cwd,
672
685
  preflight: input.preflight ?? false,
686
+ preflightConnect: input.preflightConnect ?? false,
687
+ connectTimeoutMs: input.connectTimeoutMs,
673
688
  scriptTimeoutMs: input.timeoutMs ?? defaultMcpCheckTimeoutMs,
674
- signal: extra.signal,
689
+ signal: ctx.mcpReq.signal,
675
690
  }),
676
691
  ),
677
692
  ),
@@ -709,7 +724,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
709
724
  openWorldHint: true,
710
725
  },
711
726
  },
712
- async (input, extra) =>
727
+ async (input, ctx) =>
713
728
  safeToolResult(async () =>
714
729
  jsonResult(
715
730
  await runDatabaseCommand({
@@ -717,7 +732,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
717
732
  cwd,
718
733
  captureOutput: true,
719
734
  maxOutputBytes: defaultDatabaseCommandMaxOutputBytes,
720
- signal: extra.signal,
735
+ signal: ctx.mcpReq.signal,
721
736
  timeoutMs: input.timeoutMs ?? defaultDatabaseCommandTimeoutMs,
722
737
  dryRun: input.dryRun,
723
738
  }),
@@ -725,6 +740,43 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
725
740
  ),
726
741
  );
727
742
 
743
+ server.registerTool(
744
+ "db_status",
745
+ {
746
+ description:
747
+ "Inspect app-owned database migrations and return exactly the versioned beignet db status --json result. Exit state distinguishes current, pending, and failed inspection without mutating the database. Output is bounded, the complete child process tree is cancelled with the MCP request, and execution stops after the configured timeout.",
748
+ inputSchema: {
749
+ timeoutMs: z
750
+ .number()
751
+ .int()
752
+ .min(1_000)
753
+ .max(60 * 60 * 1_000)
754
+ .optional()
755
+ .describe(
756
+ "Maximum status duration in milliseconds. Defaults to 600000 (10 minutes).",
757
+ ),
758
+ },
759
+ annotations: {
760
+ readOnlyHint: true,
761
+ destructiveHint: false,
762
+ idempotentHint: true,
763
+ openWorldHint: true,
764
+ },
765
+ },
766
+ async (input, ctx) =>
767
+ safeToolResult(async () =>
768
+ jsonResult(
769
+ await runDatabaseStatus({
770
+ cwd,
771
+ captureOutput: true,
772
+ maxOutputBytes: defaultDatabaseCommandMaxOutputBytes,
773
+ signal: ctx.mcpReq.signal,
774
+ timeoutMs: input.timeoutMs ?? defaultDatabaseCommandTimeoutMs,
775
+ }),
776
+ ),
777
+ ),
778
+ );
779
+
728
780
  server.registerTool(
729
781
  "db_schema_sync",
730
782
  {
@@ -763,7 +815,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
763
815
  openWorldHint: false,
764
816
  },
765
817
  },
766
- async (input, extra) =>
818
+ async (input, ctx) =>
767
819
  safeToolResult(async () =>
768
820
  jsonResult(
769
821
  await syncDatabaseSchema({
@@ -772,7 +824,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
772
824
  tables: input.tables,
773
825
  output: input.output,
774
826
  dryRun: input.dryRun,
775
- signal: extra.signal,
827
+ signal: ctx.mcpReq.signal,
776
828
  }),
777
829
  ),
778
830
  ),
@@ -791,7 +843,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
791
843
  openWorldHint: true,
792
844
  },
793
845
  },
794
- async (input, extra) =>
846
+ async (input, ctx) =>
795
847
  safeToolResult(async () =>
796
848
  operationalJsonResult(
797
849
  await runIsolatedOperationalCommand({
@@ -803,7 +855,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
803
855
  tenant: input.tenant,
804
856
  modulePath: input.module,
805
857
  },
806
- signal: extra.signal,
858
+ signal: ctx.mcpReq.signal,
807
859
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
808
860
  }),
809
861
  ),
@@ -823,7 +875,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
823
875
  openWorldHint: true,
824
876
  },
825
877
  },
826
- async (input, extra) =>
878
+ async (input, ctx) =>
827
879
  safeToolResult(async () =>
828
880
  operationalJsonResult(
829
881
  await runIsolatedOperationalCommand({
@@ -839,7 +891,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
839
891
  source: input.source,
840
892
  modulePath: input.module,
841
893
  },
842
- signal: extra.signal,
894
+ signal: ctx.mcpReq.signal,
843
895
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
844
896
  }),
845
897
  ),
@@ -859,7 +911,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
859
911
  openWorldHint: true,
860
912
  },
861
913
  },
862
- async (input, extra) =>
914
+ async (input, ctx) =>
863
915
  safeToolResult(async () =>
864
916
  operationalJsonResult(
865
917
  await runIsolatedOperationalCommand({
@@ -879,7 +931,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
879
931
  id: input.id,
880
932
  modulePath: input.module,
881
933
  },
882
- signal: extra.signal,
934
+ signal: ctx.mcpReq.signal,
883
935
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
884
936
  }),
885
937
  ),
@@ -899,7 +951,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
899
951
  openWorldHint: true,
900
952
  },
901
953
  },
902
- async (input, extra) =>
954
+ async (input, ctx) =>
903
955
  safeToolResult(async () => {
904
956
  const request =
905
957
  input.operation === "drain"
@@ -936,7 +988,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
936
988
  await runIsolatedOperationalCommand({
937
989
  cwd,
938
990
  request,
939
- signal: extra.signal,
991
+ signal: ctx.mcpReq.signal,
940
992
  timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
941
993
  }),
942
994
  );