@beignet/cli 0.0.41 → 0.0.43

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 (100) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +96 -25
  3. package/dist/analysis/source-index.d.ts +1 -1
  4. package/dist/analysis/source-index.d.ts.map +1 -1
  5. package/dist/analysis/source-index.js +4 -2
  6. package/dist/analysis/source-index.js.map +1 -1
  7. package/dist/analysis/workspace.d.ts +3 -2
  8. package/dist/analysis/workspace.d.ts.map +1 -1
  9. package/dist/analysis/workspace.js +51 -39
  10. package/dist/analysis/workspace.js.map +1 -1
  11. package/dist/app-map.d.ts.map +1 -1
  12. package/dist/app-map.js +65 -4
  13. package/dist/app-map.js.map +1 -1
  14. package/dist/choices.d.ts +18 -0
  15. package/dist/choices.d.ts.map +1 -1
  16. package/dist/choices.js +35 -0
  17. package/dist/choices.js.map +1 -1
  18. package/dist/db.d.ts +18 -7
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +20 -7
  21. package/dist/db.js.map +1 -1
  22. package/dist/doctor-fixes.d.ts +64 -0
  23. package/dist/doctor-fixes.d.ts.map +1 -0
  24. package/dist/doctor-fixes.js +142 -0
  25. package/dist/doctor-fixes.js.map +1 -0
  26. package/dist/explain.d.ts +3 -1
  27. package/dist/explain.d.ts.map +1 -1
  28. package/dist/explain.js +136 -42
  29. package/dist/explain.js.map +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +92 -25
  32. package/dist/index.js.map +1 -1
  33. package/dist/inspect.d.ts +38 -9
  34. package/dist/inspect.d.ts.map +1 -1
  35. package/dist/inspect.js +385 -251
  36. package/dist/inspect.js.map +1 -1
  37. package/dist/lib.d.ts +6 -2
  38. package/dist/lib.d.ts.map +1 -1
  39. package/dist/lib.js +3 -2
  40. package/dist/lib.js.map +1 -1
  41. package/dist/make/inbox.js +4 -4
  42. package/dist/make/payments.js +6 -6
  43. package/dist/make/shared.js +3 -3
  44. package/dist/make/shared.js.map +1 -1
  45. package/dist/make/tenancy.d.ts.map +1 -1
  46. package/dist/make/tenancy.js +12 -14
  47. package/dist/make/tenancy.js.map +1 -1
  48. package/dist/make.d.ts.map +1 -1
  49. package/dist/make.js +33 -23
  50. package/dist/make.js.map +1 -1
  51. package/dist/mcp.d.ts.map +1 -1
  52. package/dist/mcp.js +121 -13
  53. package/dist/mcp.js.map +1 -1
  54. package/dist/registry-edits.d.ts +7 -1
  55. package/dist/registry-edits.d.ts.map +1 -1
  56. package/dist/registry-edits.js +61 -43
  57. package/dist/registry-edits.js.map +1 -1
  58. package/dist/templates/agents.d.ts.map +1 -1
  59. package/dist/templates/agents.js +34 -11
  60. package/dist/templates/agents.js.map +1 -1
  61. package/dist/templates/base.d.ts.map +1 -1
  62. package/dist/templates/base.js +5 -5
  63. package/dist/templates/base.js.map +1 -1
  64. package/dist/templates/db/mysql.js +3 -3
  65. package/dist/templates/db/postgres.js +3 -3
  66. package/dist/templates/db/sqlite.js +3 -3
  67. package/dist/templates/server.js +4 -4
  68. package/dist/templates/server.js.map +1 -1
  69. package/dist/templates/shared.d.ts +2 -1
  70. package/dist/templates/shared.d.ts.map +1 -1
  71. package/dist/templates/shared.js +7 -4
  72. package/dist/templates/shared.js.map +1 -1
  73. package/dist/templates/todos.js +2 -2
  74. package/package.json +3 -2
  75. package/skills/app-structure/SKILL.md +43 -10
  76. package/src/analysis/source-index.ts +12 -3
  77. package/src/analysis/workspace.ts +94 -52
  78. package/src/app-map.ts +82 -4
  79. package/src/choices.ts +57 -0
  80. package/src/db.ts +45 -15
  81. package/src/doctor-fixes.ts +252 -0
  82. package/src/explain.ts +151 -43
  83. package/src/index.ts +130 -35
  84. package/src/inspect.ts +537 -295
  85. package/src/lib.ts +28 -1
  86. package/src/make/inbox.ts +4 -4
  87. package/src/make/payments.ts +6 -6
  88. package/src/make/shared.ts +3 -3
  89. package/src/make/tenancy.ts +12 -14
  90. package/src/make.ts +36 -24
  91. package/src/mcp.ts +187 -13
  92. package/src/registry-edits.ts +81 -39
  93. package/src/templates/agents.ts +34 -11
  94. package/src/templates/base.ts +5 -4
  95. package/src/templates/db/mysql.ts +3 -3
  96. package/src/templates/db/postgres.ts +3 -3
  97. package/src/templates/db/sqlite.ts +3 -3
  98. package/src/templates/server.ts +4 -4
  99. package/src/templates/shared.ts +14 -6
  100. package/src/templates/todos.ts +2 -2
package/src/lib.ts CHANGED
@@ -27,7 +27,25 @@ export {
27
27
  } from "./app-map.js";
28
28
  export type { CreateOptions } from "./create.js";
29
29
  export { createProject } from "./create.js";
30
- export { runDatabaseCommand } from "./db.js";
30
+ export type {
31
+ DatabaseCommand,
32
+ DatabaseSchemaDialect,
33
+ DatabaseSchemaTable,
34
+ RunDatabaseCommandOptions,
35
+ RunDatabaseCommandResult,
36
+ SyncDatabaseSchemaOptions,
37
+ SyncDatabaseSchemaResult,
38
+ } from "./db.js";
39
+ export { runDatabaseCommand, syncDatabaseSchema } from "./db.js";
40
+ export type {
41
+ DoctorFixApplyResult,
42
+ DoctorFixFileChange,
43
+ DoctorFixOperation,
44
+ DoctorFixOperationId,
45
+ DoctorFixPlan,
46
+ InspectFix,
47
+ } from "./doctor-fixes.js";
48
+ export { doctorFixOperationIds } from "./doctor-fixes.js";
31
49
  export type {
32
50
  ExplainAppOptions,
33
51
  ExplainAppResult,
@@ -46,12 +64,21 @@ export {
46
64
  formatExplain,
47
65
  } from "./explain.js";
48
66
  export { main } from "./index.js";
67
+ export type {
68
+ ApplyDoctorFixPlanOptions,
69
+ DoctorFixInspectionResult,
70
+ DoctorFixPlanResult,
71
+ } from "./inspect.js";
49
72
  export {
50
73
  applyDoctorFixes,
74
+ applyDoctorFixPlan,
51
75
  formatDoctor,
76
+ formatDoctorFixPlan,
77
+ formatDoctorFixPlanGithub,
52
78
  formatDoctorGithub,
53
79
  formatRoutes,
54
80
  inspectApp,
81
+ planDoctorFixes,
55
82
  } from "./inspect.js";
56
83
  export { formatLint, formatLintGithub, lintApp } from "./lint.js";
57
84
  export type { MakeFeatureAddon, MakeFeatureRecipe } from "./make.js";
package/src/make/inbox.ts CHANGED
@@ -1437,17 +1437,17 @@ export const inboxNotifications = ${dialect.tableFunction}(
1437
1437
  \t\treadAt: ${column("read_at")},
1438
1438
  \t\tcreatedAt: ${column("created_at")}.notNull(),
1439
1439
  \t},
1440
- \t(table) => ({
1441
- \t\tunreadIdx: index("inbox_notifications_unread_idx").on(
1440
+ \t(table) => [
1441
+ \t\tindex("inbox_notifications_unread_idx").on(
1442
1442
  \t\t\ttable.userId,
1443
1443
  \t\t\ttable.readAt,
1444
1444
  \t\t),
1445
- \t\tcursorIdx: index("inbox_notifications_cursor_idx").on(
1445
+ \t\tindex("inbox_notifications_cursor_idx").on(
1446
1446
  \t\t\ttable.userId,
1447
1447
  \t\t\ttable.createdAt,
1448
1448
  \t\t\ttable.id,
1449
1449
  \t\t),
1450
- \t}),
1450
+ \t],
1451
1451
  );
1452
1452
  `;
1453
1453
  }
@@ -680,11 +680,11 @@ export const createCheckoutSessionInputSchema = z.object({
680
680
 
681
681
  export const createCheckoutSessionOutputSchema = z.object({
682
682
  \tsessionId: z.string(),
683
- \turl: z.string().url(),
683
+ \turl: z.url(),
684
684
  });
685
685
 
686
686
  export const createBillingPortalSessionOutputSchema = z.object({
687
- \turl: z.string().url(),
687
+ \turl: z.url(),
688
688
  });
689
689
 
690
690
  export const billingStatusOutputSchema = z.object({
@@ -1800,14 +1800,14 @@ export const billingAccounts = ${dialect.tableFunction}(
1800
1800
  \t\tlastEventId: ${idColumn("last_event_id")},
1801
1801
  \t\tupdatedAt: ${idColumn("updated_at")}.notNull(),
1802
1802
  \t},
1803
- \t(table) => ({
1804
- \t\tproviderCustomerUnique: uniqueIndex(
1803
+ \t(table) => [
1804
+ \t\tuniqueIndex(
1805
1805
  \t\t\t"billing_accounts_provider_customer_unique",
1806
1806
  \t\t).on(table.provider, table.customerId),
1807
- \t\tproviderSubscriptionUnique: uniqueIndex(
1807
+ \t\tuniqueIndex(
1808
1808
  \t\t\t"billing_accounts_provider_subscription_unique",
1809
1809
  \t\t).on(table.provider, table.subscriptionId),
1810
- \t}),
1810
+ \t],
1811
1811
  );
1812
1812
  `;
1813
1813
  }
@@ -18,7 +18,7 @@ import {
18
18
  import {
19
19
  currentGeneratedPackageScripts,
20
20
  externalVersions,
21
- legacyGeneratedPackageScripts,
21
+ isLegacyGeneratedPackageScript,
22
22
  } from "../templates/shared.js";
23
23
 
24
24
  export type MakeResult = {
@@ -1096,7 +1096,7 @@ export async function updatePackageScripts(
1096
1096
  for (const [name, command] of Object.entries(scripts)) {
1097
1097
  if (
1098
1098
  !packageJson.scripts[name] ||
1099
- packageJson.scripts[name] === legacyGeneratedPackageScripts[name]
1099
+ isLegacyGeneratedPackageScript(name, packageJson.scripts[name])
1100
1100
  ) {
1101
1101
  packageJson.scripts[name] = command;
1102
1102
  }
@@ -1162,7 +1162,7 @@ function updateGeneratedPackageScripts(scripts: Record<string, string>): void {
1162
1162
  currentGeneratedPackageScripts,
1163
1163
  )) {
1164
1164
  if (
1165
- scripts[name] === legacyGeneratedPackageScripts[name] ||
1165
+ isLegacyGeneratedPackageScript(name, scripts[name]) ||
1166
1166
  (name === "test" && !scripts[name])
1167
1167
  ) {
1168
1168
  scripts[name] = command;
@@ -952,7 +952,7 @@ export const workspaceMemberWithUserSchema = workspaceMemberSchema.extend({
952
952
  export const workspaceInviteSchema = z.object({
953
953
  \tid: z.string().min(1),
954
954
  \tworkspaceId: z.string().min(1),
955
- \temail: z.string().email(),
955
+ \temail: z.email(),
956
956
  \trole: workspaceRoleSchema,
957
957
  \ttoken: z.string().min(1),
958
958
  \tinvitedByUserId: z.string().min(1),
@@ -1019,7 +1019,7 @@ export const listInvitesOutputSchema = z.object({
1019
1019
  });
1020
1020
 
1021
1021
  export const createInviteInputSchema = z.object({
1022
- \temail: z.string().email(),
1022
+ \temail: z.email(),
1023
1023
  \trole: workspaceRoleSchema.default("member"),
1024
1024
  });
1025
1025
 
@@ -2005,7 +2005,7 @@ const invitePayloadSchema = z.object({
2005
2005
  \tinviteId: z.string().min(1),
2006
2006
  \tworkspaceId: z.string().min(1),
2007
2007
  \tworkspaceName: z.string().min(1),
2008
- \temail: z.string().email(),
2008
+ \temail: z.email(),
2009
2009
  \trole: z.enum(["admin", "member"]),
2010
2010
  \ttoken: z.string().min(1),
2011
2011
  \tinvitedByName: z.string().optional(),
@@ -3242,9 +3242,7 @@ export const workspaces = ${dialect.tableFunction}(
3242
3242
  \t\tcreatedAt: ${column("created_at")}.notNull(),
3243
3243
  \t\tupdatedAt: ${column("updated_at")}.notNull(),
3244
3244
  \t},
3245
- \t(table) => ({
3246
- \t\tslugUnique: uniqueIndex("workspaces_slug_unique").on(table.slug),
3247
- \t}),
3245
+ \t(table) => [uniqueIndex("workspaces_slug_unique").on(table.slug)],
3248
3246
  );
3249
3247
 
3250
3248
  export const workspaceMembers = ${dialect.tableFunction}(
@@ -3256,12 +3254,12 @@ export const workspaceMembers = ${dialect.tableFunction}(
3256
3254
  \t\trole: ${column("role")}.$type<WorkspaceRole>().notNull(),
3257
3255
  \t\tjoinedAt: ${column("joined_at")}.notNull(),
3258
3256
  \t},
3259
- \t(table) => ({
3260
- \t\tworkspaceUserUnique: uniqueIndex(
3257
+ \t(table) => [
3258
+ \t\tuniqueIndex(
3261
3259
  \t\t\t"workspace_members_workspace_user_unique",
3262
3260
  \t\t).on(table.workspaceId, table.userId),
3263
- \t\tuserIdx: index("workspace_members_user_idx").on(table.userId),
3264
- \t}),
3261
+ \t\tindex("workspace_members_user_idx").on(table.userId),
3262
+ \t],
3265
3263
  );
3266
3264
 
3267
3265
  export const workspaceInvites = ${dialect.tableFunction}(
@@ -3278,15 +3276,15 @@ export const workspaceInvites = ${dialect.tableFunction}(
3278
3276
  \t\tacceptedAt: ${column("accepted_at")},
3279
3277
  \t\tcreatedAt: ${column("created_at")}.notNull(),
3280
3278
  \t},
3281
- \t(table) => ({
3282
- \t\ttokenUnique: uniqueIndex("workspace_invites_token_unique").on(
3279
+ \t(table) => [
3280
+ \t\tuniqueIndex("workspace_invites_token_unique").on(
3283
3281
  \t\t\ttable.token,
3284
3282
  \t\t),
3285
- \t\tworkspaceStatusIdx: index("workspace_invites_workspace_status_idx").on(
3283
+ \t\tindex("workspace_invites_workspace_status_idx").on(
3286
3284
  \t\t\ttable.workspaceId,
3287
3285
  \t\t\ttable.status,
3288
3286
  \t\t),
3289
- \t}),
3287
+ \t],
3290
3288
  );
3291
3289
  `;
3292
3290
  }
package/src/make.ts CHANGED
@@ -1575,7 +1575,8 @@ const cronSecretEnvEntry = `\t\tCRON_SECRET: z.string().min(1).optional(),`;
1575
1575
 
1576
1576
  // The literal APP_URL line every generated lib/env.ts ships with. CRON_SECRET
1577
1577
  // slots in directly below it, matching the documented env shape.
1578
- const cronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1578
+ const cronSecretEnvAnchor = `\t\tAPP_URL: z.url().default("http://localhost:3000"),`;
1579
+ const legacyCronSecretEnvAnchor = `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`;
1579
1580
 
1580
1581
  type CronSecretWiringPlan = {
1581
1582
  files: Array<GeneratedFile & { result: "created" | "updated" }>;
@@ -1596,10 +1597,15 @@ async function planCronSecretWiring(
1596
1597
  `Could not find the generated env file ${cronSecretEnvFile}. ${manualInstruction}`,
1597
1598
  );
1598
1599
  }
1600
+ const envAnchor = envSource.includes(cronSecretEnvAnchor)
1601
+ ? cronSecretEnvAnchor
1602
+ : envSource.includes(legacyCronSecretEnvAnchor)
1603
+ ? legacyCronSecretEnvAnchor
1604
+ : undefined;
1599
1605
 
1600
1606
  if (/\bCRON_SECRET\b/.test(envSource)) {
1601
1607
  plan.skippedFiles.push(cronSecretEnvFile);
1602
- } else if (!envSource.includes(cronSecretEnvAnchor)) {
1608
+ } else if (envAnchor === undefined) {
1603
1609
  throw new Error(
1604
1610
  `Could not find the generated APP_URL server entry in ${cronSecretEnvFile}. ${manualInstruction}`,
1605
1611
  );
@@ -1607,8 +1613,8 @@ async function planCronSecretWiring(
1607
1613
  plan.files.push({
1608
1614
  path: cronSecretEnvFile,
1609
1615
  content: envSource.replace(
1610
- cronSecretEnvAnchor,
1611
- `${cronSecretEnvAnchor}\n${cronSecretEnvEntry}`,
1616
+ envAnchor,
1617
+ `${envAnchor}\n${cronSecretEnvEntry}`,
1612
1618
  ),
1613
1619
  result: "updated",
1614
1620
  });
@@ -2218,6 +2224,10 @@ export async function makeUpload(
2218
2224
  dryRun: true,
2219
2225
  },
2220
2226
  );
2227
+ // Validate central registration before generating feature files or applying
2228
+ // provider wiring. Customized routes require an explicit manual edit rather
2229
+ // than a successful-looking partial generation.
2230
+ await updateUploadRoute(targetDir, names, config, { dryRun: true });
2221
2231
 
2222
2232
  const result = await makeFeatureArtifact({
2223
2233
  targetDir,
@@ -3716,7 +3726,9 @@ async function updateUploadRoute(
3716
3726
 
3717
3727
  const registry = defineUploadsInitializerInfo(next, "uploadRegistry");
3718
3728
  if (!registry) {
3719
- return "skipped";
3729
+ throw new Error(
3730
+ `Cannot register ${registryName} in ${file}: expected an uploadRegistry initialized with defineUploads({ ... }). Restore that registry shape or add ${registryKey}: ${registryName} manually before rerunning beignet make upload.`,
3731
+ );
3720
3732
  }
3721
3733
  if (!new RegExp(`\\b${registryKey}\\s*:`).test(registry.text)) {
3722
3734
  next = `${next.slice(0, registry.end)}\t${registryKey}: ${registryName},\n${next.slice(
@@ -3912,7 +3924,7 @@ function schemasFile(
3912
3924
  return `import { z } from "zod";
3913
3925
 
3914
3926
  export const ${names.singularPascal}Schema = z.object({
3915
- id: z.string().uuid(),
3927
+ id: z.uuid(),
3916
3928
  ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3917
3929
  version: z.number().int().min(1),
3918
3930
  createdAt: z.string().datetime(),
@@ -3926,7 +3938,7 @@ const ${names.singularPascal}CursorPayloadSchema = z.object({
3926
3938
  sortBy: ${names.singularPascal}SortBySchema,
3927
3939
  sortDirection: ${names.singularPascal}SortDirectionSchema,
3928
3940
  sortValue: z.string(),
3929
- id: z.string().uuid(),
3941
+ id: z.uuid(),
3930
3942
  });
3931
3943
 
3932
3944
  export type ${names.singularPascal}SortBy = z.infer<
@@ -4019,7 +4031,7 @@ ${
4019
4031
  mode === "resource"
4020
4032
  ? `
4021
4033
  export const ${names.singularPascal}IdInputSchema = z.object({
4022
- id: z.string().uuid(),
4034
+ id: z.uuid(),
4023
4035
  });
4024
4036
 
4025
4037
  export const Update${names.singularPascal}BodySchema = z.object({
@@ -4479,16 +4491,16 @@ export const outboxMessages = mysqlTable(
4479
4491
  \t\tcreatedAt: varchar("created_at", { length: 32 }).notNull(),
4480
4492
  \t\tupdatedAt: varchar("updated_at", { length: 32 }).notNull(),
4481
4493
  \t},
4482
- \t(table) => ({
4483
- \t\tavailableIdx: index("outbox_messages_available_idx").on(
4494
+ \t(table) => [
4495
+ \t\tindex("outbox_messages_available_idx").on(
4484
4496
  \t\t\ttable.status,
4485
4497
  \t\t\ttable.availableAt,
4486
4498
  \t\t),
4487
- \t\tlockedIdx: index("outbox_messages_locked_idx").on(
4499
+ \t\tindex("outbox_messages_locked_idx").on(
4488
4500
  \t\t\ttable.status,
4489
4501
  \t\t\ttable.lockedUntil,
4490
4502
  \t\t),
4491
- \t}),
4503
+ \t],
4492
4504
  );
4493
4505
  `;
4494
4506
  }
@@ -4515,16 +4527,16 @@ export const outboxMessages = ${dialect.tableFunction}(
4515
4527
  \t\tcreatedAt: text("created_at").notNull(),
4516
4528
  \t\tupdatedAt: text("updated_at").notNull(),
4517
4529
  \t},
4518
- \t(table) => ({
4519
- \t\tavailableIdx: index("outbox_messages_available_idx").on(
4530
+ \t(table) => [
4531
+ \t\tindex("outbox_messages_available_idx").on(
4520
4532
  \t\t\ttable.status,
4521
4533
  \t\t\ttable.availableAt,
4522
4534
  \t\t),
4523
- \t\tlockedIdx: index("outbox_messages_locked_idx").on(
4535
+ \t\tindex("outbox_messages_locked_idx").on(
4524
4536
  \t\t\ttable.status,
4525
4537
  \t\t\ttable.lockedUntil,
4526
4538
  \t\t),
4527
- \t}),
4539
+ \t],
4528
4540
  );
4529
4541
  `;
4530
4542
  }
@@ -4912,7 +4924,7 @@ const ${names.pluralCamel} = defineContractGroup()
4912
4924
  });
4913
4925
 
4914
4926
  export const ${names.singularCamel}Schema = z.object({
4915
- id: z.string().uuid(),
4927
+ id: z.uuid(),
4916
4928
  name: z.string().min(1),
4917
4929
  });
4918
4930
 
@@ -5121,19 +5133,19 @@ import { z } from "zod";
5121
5133
 
5122
5134
  export const ${names.singularPascal}Created = defineEvent("${names.pluralCamel}.created", {
5123
5135
  payload: z.object({
5124
- id: z.string().uuid(),
5136
+ id: z.uuid(),
5125
5137
  }),
5126
5138
  });
5127
5139
 
5128
5140
  export const ${names.singularPascal}Updated = defineEvent("${names.pluralCamel}.updated", {
5129
5141
  payload: z.object({
5130
- id: z.string().uuid(),
5142
+ id: z.uuid(),
5131
5143
  }),
5132
5144
  });
5133
5145
 
5134
5146
  export const ${names.singularPascal}Deleted = defineEvent("${names.pluralCamel}.deleted", {
5135
5147
  payload: z.object({
5136
- id: z.string().uuid(),
5148
+ id: z.uuid(),
5137
5149
  }),
5138
5150
  });
5139
5151
 
@@ -5209,7 +5221,7 @@ function eventFile(names: EventNames): string {
5209
5221
  import { z } from "zod";
5210
5222
 
5211
5223
  export const ${names.payloadSchemaName} = z.object({
5212
- \tid: z.string().uuid(),
5224
+ \tid: z.uuid(),
5213
5225
  });
5214
5226
 
5215
5227
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
@@ -5226,7 +5238,7 @@ import { z } from "zod";
5226
5238
  import { defineJob } from "${aliasModule(config.paths.jobsBuilder)}";
5227
5239
 
5228
5240
  export const ${names.payloadSchemaName} = z.object({
5229
- \tid: z.string().uuid(),
5241
+ \tid: z.uuid(),
5230
5242
  });
5231
5243
 
5232
5244
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
@@ -5317,9 +5329,9 @@ import { z } from "zod";
5317
5329
  import { defineNotification } from "${aliasModule(config.paths.notificationsBuilder)}";
5318
5330
 
5319
5331
  export const ${names.payloadSchemaName} = z.object({
5320
- \tid: z.string().uuid(),
5332
+ \tid: z.uuid(),
5321
5333
  \tsubject: z.string(),
5322
- \trecipientEmail: z.string().email(),
5334
+ \trecipientEmail: z.email(),
5323
5335
  });
5324
5336
 
5325
5337
  export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
package/src/mcp.ts CHANGED
@@ -5,9 +5,28 @@ import { z } from "zod";
5
5
  import { mapApp, projectAppMap } from "./app-map.js";
6
6
  import { appMapNodeKinds } from "./app-map-schema.js";
7
7
  import { checkApp } from "./check.js";
8
- import { type ProviderPresetName, providerPresetChoices } from "./choices.js";
8
+ import {
9
+ databaseCommandChoices,
10
+ databaseSchemaDialectChoices,
11
+ databaseSchemaTableChoices,
12
+ doctorFixOperationIds,
13
+ type ProviderPresetName,
14
+ providerPresetChoices,
15
+ } from "./choices.js";
16
+ import {
17
+ defaultDatabaseCommandMaxOutputBytes,
18
+ defaultDatabaseCommandTimeoutMs,
19
+ runDatabaseCommand,
20
+ syncDatabaseSchema,
21
+ } from "./db.js";
9
22
  import { explainApp, explainTargetKinds } from "./explain.js";
10
- import { applyDoctorFixes, inspectApp } from "./inspect.js";
23
+ import {
24
+ applyDoctorFixesWithResult,
25
+ applyDoctorFixPlan,
26
+ createDoctorFixInspectionResult,
27
+ inspectApp,
28
+ planDoctorFixes,
29
+ } from "./inspect.js";
11
30
  import { lintApp } from "./lint.js";
12
31
  import {
13
32
  type MakeResourceResult,
@@ -379,14 +398,14 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
379
398
  "explain",
380
399
  {
381
400
  description:
382
- "Explain one mapped Beignet feature, route, provider, or diagnostic using deterministic source evidence. Returns relationships, relevant conventions, findings, suggested files, and follow-up commands without changing the app.",
401
+ "Explain one mapped Beignet concept or diagnostic using deterministic source evidence. Supports every app-map node kind and returns relationships, relevant conventions, findings, suggested files, and follow-up commands without changing the app.",
383
402
  inputSchema: {
384
403
  kind: z.enum(explainTargetKinds).describe("Mapped concept to explain."),
385
404
  target: z
386
405
  .string()
387
406
  .min(1)
388
407
  .describe(
389
- "Feature name, METHOD /path route, provider package or port name, or diagnostic code.",
408
+ "Stable ID, runtime name, declaration name, source selector, applicable concept alias, or diagnostic code.",
390
409
  ),
391
410
  },
392
411
  annotations: { readOnlyHint: true },
@@ -441,6 +460,107 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
441
460
  ),
442
461
  );
443
462
 
463
+ server.registerTool(
464
+ "db",
465
+ {
466
+ description:
467
+ "Run one app-owned database lifecycle command and return exactly the versioned beignet db <command> --json result. Supports generate, migrate, seed, and reset; captures a bounded output tail, stops after the configured timeout, and cancels the complete child process tree with the MCP request. Dry runs validate prerequisites and report the command without executing it.",
468
+ inputSchema: {
469
+ command: z
470
+ .enum(databaseCommandChoices)
471
+ .describe("Database lifecycle command to run."),
472
+ dryRun: z
473
+ .boolean()
474
+ .optional()
475
+ .describe(
476
+ "Validate prerequisites and report the app-owned script without executing it. This does not simulate SQL or data changes.",
477
+ ),
478
+ timeoutMs: z
479
+ .number()
480
+ .int()
481
+ .min(1_000)
482
+ .max(60 * 60 * 1_000)
483
+ .optional()
484
+ .describe(
485
+ "Maximum command duration in milliseconds. Defaults to 600000 (10 minutes).",
486
+ ),
487
+ },
488
+ annotations: {
489
+ readOnlyHint: false,
490
+ destructiveHint: true,
491
+ idempotentHint: false,
492
+ openWorldHint: true,
493
+ },
494
+ },
495
+ async (input, extra) =>
496
+ safeToolResult(async () =>
497
+ jsonResult(
498
+ await runDatabaseCommand({
499
+ command: input.command,
500
+ cwd,
501
+ captureOutput: true,
502
+ maxOutputBytes: defaultDatabaseCommandMaxOutputBytes,
503
+ signal: extra.signal,
504
+ timeoutMs: input.timeoutMs ?? defaultDatabaseCommandTimeoutMs,
505
+ dryRun: input.dryRun,
506
+ }),
507
+ ),
508
+ ),
509
+ );
510
+
511
+ server.registerTool(
512
+ "db_schema_sync",
513
+ {
514
+ description:
515
+ "Idempotently sync app-owned Drizzle schema re-exports for Beignet provider tables and return exactly the versioned beignet db schema sync --json result. Supports dry-run planning and never generates or applies SQL migrations.",
516
+ inputSchema: {
517
+ dialect: z
518
+ .enum(databaseSchemaDialectChoices)
519
+ .optional()
520
+ .describe(
521
+ "Drizzle dialect. Inferred from registered providers or drizzle.config.* when omitted.",
522
+ ),
523
+ tables: z
524
+ .array(z.enum(databaseSchemaTableChoices))
525
+ .min(1)
526
+ .optional()
527
+ .describe(
528
+ "Beignet provider tables to re-export. Defaults to audit, idempotency, and outbox.",
529
+ ),
530
+ output: z
531
+ .string()
532
+ .min(1)
533
+ .optional()
534
+ .describe(
535
+ "App-relative schema output. Defaults to infra/db/schema/beignet.ts.",
536
+ ),
537
+ dryRun: z
538
+ .boolean()
539
+ .optional()
540
+ .describe("Preview schema source changes without writing files."),
541
+ },
542
+ annotations: {
543
+ readOnlyHint: false,
544
+ destructiveHint: true,
545
+ idempotentHint: true,
546
+ openWorldHint: false,
547
+ },
548
+ },
549
+ async (input, extra) =>
550
+ safeToolResult(async () =>
551
+ jsonResult(
552
+ await syncDatabaseSchema({
553
+ cwd,
554
+ dialect: input.dialect,
555
+ tables: input.tables,
556
+ output: input.output,
557
+ dryRun: input.dryRun,
558
+ signal: extra.signal,
559
+ }),
560
+ ),
561
+ ),
562
+ );
563
+
444
564
  server.registerTool(
445
565
  "routes",
446
566
  {
@@ -469,7 +589,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
469
589
  "doctor",
470
590
  {
471
591
  description:
472
- "Check app wiring and Beignet conventions, returning diagnostics, the detected convention, and applied fixes as JSON. Strict mode (the default) is the CI bar and includes CI-oriented warnings. Read-only; use doctor_fix to repair registration drift.",
592
+ "Check app wiring and Beignet conventions, returning diagnostics and the detected convention as JSON. Strict mode (the default) is the CI bar and includes CI-oriented warnings. Read-only; use doctor_fix_plan before a guarded doctor_fix repair.",
473
593
  inputSchema: {
474
594
  strict: z
475
595
  .boolean()
@@ -489,28 +609,82 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
489
609
  }),
490
610
  );
491
611
 
612
+ server.registerTool(
613
+ "doctor_fix_plan",
614
+ {
615
+ description:
616
+ "Plan every currently available low-risk doctor repair without changing files. Returns stable operation IDs, exact unified patches, file hashes, and a plan ID for guarded doctor_fix application.",
617
+ inputSchema: {
618
+ strict: z
619
+ .boolean()
620
+ .optional()
621
+ .describe("Include CI-oriented warnings. Defaults to true."),
622
+ },
623
+ annotations: {
624
+ readOnlyHint: true,
625
+ destructiveHint: false,
626
+ idempotentHint: true,
627
+ openWorldHint: false,
628
+ },
629
+ },
630
+ async (input) =>
631
+ safeToolResult(async () => {
632
+ return jsonResult(
633
+ await planDoctorFixes({ cwd, strict: input.strict ?? true }),
634
+ );
635
+ }),
636
+ );
637
+
492
638
  server.registerTool(
493
639
  "doctor_fix",
494
640
  {
495
641
  description:
496
- "Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, outbox, and listener registration drift. Returns applied fixes and remaining diagnostics as JSON.",
642
+ "Apply low-risk doctor repairs, then re-check the app. With no planId, preserves the legacy apply-all behavior. Pass the planId returned by doctor_fix_plan for stale-file protection, optionally with fixIds to select repair operations.",
497
643
  inputSchema: {
498
644
  strict: z
499
645
  .boolean()
500
646
  .optional()
501
647
  .describe("Include CI-oriented warnings. Defaults to true."),
648
+ planId: z
649
+ .string()
650
+ .min(1)
651
+ .optional()
652
+ .describe("Plan ID returned by doctor_fix_plan."),
653
+ fixIds: z
654
+ .array(z.enum(doctorFixOperationIds))
655
+ .min(1)
656
+ .optional()
657
+ .describe("Repair operation IDs to apply. Requires planId."),
658
+ },
659
+ annotations: {
660
+ readOnlyHint: false,
661
+ destructiveHint: true,
662
+ idempotentHint: true,
663
+ openWorldHint: false,
502
664
  },
503
665
  },
504
666
  async (input) =>
505
667
  safeToolResult(async () => {
506
668
  const strict = input.strict ?? true;
507
- const fixes = await applyDoctorFixes({ cwd, strict });
508
- const result = await inspectApp({ cwd, strict });
509
- return jsonResult({
510
- diagnostics: result.diagnostics,
511
- convention: result.convention,
512
- fixes,
513
- });
669
+ if (input.fixIds && !input.planId) {
670
+ throw new Error(
671
+ "doctor_fix fixIds require planId from doctor_fix_plan.",
672
+ );
673
+ }
674
+ const applied = input.planId
675
+ ? await applyDoctorFixPlan({
676
+ cwd,
677
+ strict,
678
+ planId: input.planId,
679
+ ...(input.fixIds ? { fixIds: input.fixIds } : {}),
680
+ })
681
+ : await applyDoctorFixesWithResult({ cwd, strict });
682
+ return jsonResult(
683
+ createDoctorFixInspectionResult(
684
+ await inspectApp({ cwd, strict }),
685
+ applied,
686
+ ),
687
+ );
514
688
  }),
515
689
  );
516
690