@beignet/cli 0.0.40 → 0.0.42

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 (111) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +139 -17
  3. package/dist/analysis/source-index.d.ts +38 -0
  4. package/dist/analysis/source-index.d.ts.map +1 -0
  5. package/dist/analysis/source-index.js +271 -0
  6. package/dist/analysis/source-index.js.map +1 -0
  7. package/dist/analysis/workspace.d.ts +16 -0
  8. package/dist/analysis/workspace.d.ts.map +1 -0
  9. package/dist/analysis/workspace.js +134 -0
  10. package/dist/analysis/workspace.js.map +1 -0
  11. package/dist/app-map-schema.d.ts +5 -0
  12. package/dist/app-map-schema.d.ts.map +1 -0
  13. package/dist/app-map-schema.js +26 -0
  14. package/dist/app-map-schema.js.map +1 -0
  15. package/dist/app-map.d.ts +96 -0
  16. package/dist/app-map.d.ts.map +1 -0
  17. package/dist/app-map.js +1141 -0
  18. package/dist/app-map.js.map +1 -0
  19. package/dist/check.d.ts +7 -0
  20. package/dist/check.d.ts.map +1 -1
  21. package/dist/check.js +20 -5
  22. package/dist/check.js.map +1 -1
  23. package/dist/choices.d.ts +18 -0
  24. package/dist/choices.d.ts.map +1 -1
  25. package/dist/choices.js +35 -0
  26. package/dist/choices.js.map +1 -1
  27. package/dist/db.d.ts +49 -13
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +202 -21
  30. package/dist/db.js.map +1 -1
  31. package/dist/doctor-fixes.d.ts +64 -0
  32. package/dist/doctor-fixes.d.ts.map +1 -0
  33. package/dist/doctor-fixes.js +142 -0
  34. package/dist/doctor-fixes.js.map +1 -0
  35. package/dist/explain.d.ts +100 -0
  36. package/dist/explain.d.ts.map +1 -0
  37. package/dist/explain.js +606 -0
  38. package/dist/explain.js.map +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +165 -21
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspect.d.ts +33 -9
  43. package/dist/inspect.d.ts.map +1 -1
  44. package/dist/inspect.js +406 -119
  45. package/dist/inspect.js.map +1 -1
  46. package/dist/lib.d.ts +10 -2
  47. package/dist/lib.d.ts.map +1 -1
  48. package/dist/lib.js +5 -2
  49. package/dist/lib.js.map +1 -1
  50. package/dist/make/shared.js +3 -3
  51. package/dist/make/shared.js.map +1 -1
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +63 -2
  54. package/dist/make.js.map +1 -1
  55. package/dist/mcp.d.ts +1 -1
  56. package/dist/mcp.d.ts.map +1 -1
  57. package/dist/mcp.js +188 -12
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/preflight.d.ts.map +1 -1
  60. package/dist/preflight.js +10 -0
  61. package/dist/preflight.js.map +1 -1
  62. package/dist/provider-add.d.ts.map +1 -1
  63. package/dist/provider-add.js +167 -13
  64. package/dist/provider-add.js.map +1 -1
  65. package/dist/provider-audit.d.ts +6 -0
  66. package/dist/provider-audit.d.ts.map +1 -1
  67. package/dist/provider-audit.js +63 -15
  68. package/dist/provider-audit.js.map +1 -1
  69. package/dist/templates/agents.d.ts.map +1 -1
  70. package/dist/templates/agents.js +35 -6
  71. package/dist/templates/agents.js.map +1 -1
  72. package/dist/templates/base.d.ts.map +1 -1
  73. package/dist/templates/base.js +13 -7
  74. package/dist/templates/base.js.map +1 -1
  75. package/dist/templates/index.d.ts +1 -1
  76. package/dist/templates/index.d.ts.map +1 -1
  77. package/dist/templates/index.js +8 -1
  78. package/dist/templates/index.js.map +1 -1
  79. package/dist/templates/server.d.ts +3 -0
  80. package/dist/templates/server.d.ts.map +1 -1
  81. package/dist/templates/server.js +55 -1
  82. package/dist/templates/server.js.map +1 -1
  83. package/dist/templates/shared.d.ts +2 -1
  84. package/dist/templates/shared.d.ts.map +1 -1
  85. package/dist/templates/shared.js +8 -5
  86. package/dist/templates/shared.js.map +1 -1
  87. package/package.json +3 -2
  88. package/skills/app-structure/SKILL.md +48 -6
  89. package/src/analysis/source-index.ts +395 -0
  90. package/src/analysis/workspace.ts +180 -0
  91. package/src/app-map-schema.ts +28 -0
  92. package/src/app-map.ts +1705 -0
  93. package/src/check.ts +27 -4
  94. package/src/choices.ts +57 -0
  95. package/src/db.ts +276 -28
  96. package/src/doctor-fixes.ts +252 -0
  97. package/src/explain.ts +894 -0
  98. package/src/index.ts +237 -31
  99. package/src/inspect.ts +567 -147
  100. package/src/lib.ts +64 -1
  101. package/src/make/shared.ts +3 -3
  102. package/src/make.ts +90 -2
  103. package/src/mcp.ts +292 -12
  104. package/src/preflight.ts +14 -0
  105. package/src/provider-add.ts +211 -12
  106. package/src/provider-audit.ts +127 -22
  107. package/src/templates/agents.ts +35 -6
  108. package/src/templates/base.ts +25 -6
  109. package/src/templates/index.ts +18 -1
  110. package/src/templates/server.ts +58 -1
  111. package/src/templates/shared.ts +15 -7
package/src/lib.ts CHANGED
@@ -6,16 +6,79 @@
6
6
  * loads command modules lazily.
7
7
  */
8
8
 
9
+ export type {
10
+ AppMapEdge,
11
+ AppMapEdgeKind,
12
+ AppMapNode,
13
+ AppMapNodeKind,
14
+ AppMapNodeStatus,
15
+ AppMapOptions,
16
+ AppMapProjectionOptions,
17
+ AppMapResult,
18
+ AppMapSource,
19
+ AppMapSummary,
20
+ AppMapUnresolvedReference,
21
+ } from "./app-map.js";
22
+ export {
23
+ appMapNodeKinds,
24
+ formatAppMap,
25
+ mapApp,
26
+ projectAppMap,
27
+ } from "./app-map.js";
9
28
  export type { CreateOptions } from "./create.js";
10
29
  export { createProject } from "./create.js";
11
- 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";
49
+ export type {
50
+ ExplainAppOptions,
51
+ ExplainAppResult,
52
+ ExplainEvidence,
53
+ ExplainFinding,
54
+ ExplainRelatedNode,
55
+ ExplainRelationship,
56
+ ExplainSuggestedAction,
57
+ ExplainSuggestedFile,
58
+ ExplainTarget,
59
+ ExplainTargetKind,
60
+ } from "./explain.js";
61
+ export {
62
+ explainApp,
63
+ explainTargetKinds,
64
+ formatExplain,
65
+ } from "./explain.js";
12
66
  export { main } from "./index.js";
67
+ export type {
68
+ ApplyDoctorFixPlanOptions,
69
+ DoctorFixInspectionResult,
70
+ DoctorFixPlanResult,
71
+ } from "./inspect.js";
13
72
  export {
14
73
  applyDoctorFixes,
74
+ applyDoctorFixPlan,
15
75
  formatDoctor,
76
+ formatDoctorFixPlan,
77
+ formatDoctorFixPlanGithub,
16
78
  formatDoctorGithub,
17
79
  formatRoutes,
18
80
  inspectApp,
81
+ planDoctorFixes,
19
82
  } from "./inspect.js";
20
83
  export { formatLint, formatLintGithub, lintApp } from "./lint.js";
21
84
  export type { MakeFeatureAddon, MakeFeatureRecipe } from "./make.js";
@@ -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;
package/src/make.ts CHANGED
@@ -1090,6 +1090,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1090
1090
  await updateOutboxRegistry(targetDir, names, "jobs", config, {
1091
1091
  dryRun: true,
1092
1092
  });
1093
+ await updateInngestJobRegistry(targetDir, names, config, { dryRun: true });
1093
1094
 
1094
1095
  const result = await makeFeatureArtifact({
1095
1096
  targetDir,
@@ -1116,6 +1117,9 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1116
1117
  await applyOutboxPortWiring(result, targetDir, config, {
1117
1118
  dryRun: Boolean(options.dryRun),
1118
1119
  });
1120
+ await applyInngestJobRegistryUpdate(result, targetDir, names, config, {
1121
+ dryRun: Boolean(options.dryRun),
1122
+ });
1119
1123
 
1120
1124
  return result;
1121
1125
  }
@@ -3814,6 +3818,92 @@ async function applyOutboxRegistryUpdate(
3814
3818
  }
3815
3819
  }
3816
3820
 
3821
+ function inngestRegistryPath(config: ResolvedBeignetConfig): string {
3822
+ return path.posix.join(path.posix.dirname(config.paths.server), "inngest.ts");
3823
+ }
3824
+
3825
+ async function appUsesInngestJobs(targetDir: string): Promise<boolean> {
3826
+ const source = await readOptionalFile(path.join(targetDir, "package.json"));
3827
+ if (source === undefined) return false;
3828
+
3829
+ try {
3830
+ const packageJson = JSON.parse(source) as PackageJsonLike;
3831
+ return Boolean(
3832
+ packageJson.dependencies?.["@beignet/provider-jobs-inngest"] ??
3833
+ packageJson.devDependencies?.["@beignet/provider-jobs-inngest"],
3834
+ );
3835
+ } catch {
3836
+ return false;
3837
+ }
3838
+ }
3839
+
3840
+ async function updateInngestJobRegistry(
3841
+ targetDir: string,
3842
+ names: JobNames,
3843
+ config: ResolvedBeignetConfig,
3844
+ options: { dryRun: boolean },
3845
+ ): Promise<WriteGeneratedFileResult | undefined> {
3846
+ if (!(await appUsesInngestJobs(targetDir))) return undefined;
3847
+
3848
+ const registryPath = inngestRegistryPath(config);
3849
+ const destination = path.join(targetDir, registryPath);
3850
+ const existing = await readOptionalFile(destination);
3851
+ if (existing === undefined) {
3852
+ throw new Error(
3853
+ `@beignet/provider-jobs-inngest is installed, but ${registryPath} is missing. Run \`beignet provider add jobs-inngest\` to create the execution registry before generating jobs.`,
3854
+ );
3855
+ }
3856
+
3857
+ const featureIndexPath = path.join(
3858
+ featureArtifactDir(names, "jobs", config),
3859
+ "index.ts",
3860
+ );
3861
+ const registryName = `${names.featureSingularCamel}Jobs`;
3862
+ const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
3863
+ let next = existing;
3864
+ if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
3865
+
3866
+ const registry = arrayInitializerInfo(next, "inngestJobs");
3867
+ if (!registry) {
3868
+ throw new Error(
3869
+ `Could not find \`export const inngestJobs = [...]\` in ${registryPath}. Restore the generated registry or register ${registryName} manually.`,
3870
+ );
3871
+ }
3872
+ if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
3873
+ const updatedArray = appendToArrayExpression(registry.text, [
3874
+ `...${registryName}`,
3875
+ ]);
3876
+ next = `${next.slice(0, registry.start)}${updatedArray}${next.slice(
3877
+ registry.end,
3878
+ )}`;
3879
+ }
3880
+
3881
+ if (next === existing) return "skipped";
3882
+ if (!options.dryRun) await writeFile(destination, next);
3883
+ return "updated";
3884
+ }
3885
+
3886
+ async function applyInngestJobRegistryUpdate(
3887
+ result: MakeJobResult,
3888
+ targetDir: string,
3889
+ names: JobNames,
3890
+ config: ResolvedBeignetConfig,
3891
+ options: { dryRun: boolean },
3892
+ ): Promise<void> {
3893
+ const registryResult = await updateInngestJobRegistry(
3894
+ targetDir,
3895
+ names,
3896
+ config,
3897
+ options,
3898
+ );
3899
+ if (registryResult === undefined) return;
3900
+
3901
+ const registryPath = inngestRegistryPath(config);
3902
+ if (registryResult === "updated") result.updatedFiles.push(registryPath);
3903
+ if (registryResult === "skipped") result.skippedFiles.push(registryPath);
3904
+ if (!result.files.includes(registryPath)) result.files.push(registryPath);
3905
+ }
3906
+
3817
3907
  function schemasFile(
3818
3908
  names: ResourceNames,
3819
3909
  mode: ResourceGenerationMode,
@@ -5145,8 +5235,6 @@ export const ${names.jobExportName} = defineJob("${names.jobName}", {
5145
5235
  \tpayload: ${names.payloadSchemaName},
5146
5236
  \tretry: retry.exponential({
5147
5237
  \t\tattempts: 3,
5148
- \t\tinitialDelay: "1s",
5149
- \t\tmaxDelay: "1m",
5150
5238
  \t}),
5151
5239
  \tasync handle({ payload, ctx }) {
5152
5240
  \t\tctx.ports.logger.info("Job handled", {
package/src/mcp.ts CHANGED
@@ -2,8 +2,31 @@ import path from "node:path";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { type ProviderPresetName, providerPresetChoices } from "./choices.js";
6
- import { applyDoctorFixes, inspectApp } from "./inspect.js";
5
+ import { mapApp, projectAppMap } from "./app-map.js";
6
+ import { appMapNodeKinds } from "./app-map-schema.js";
7
+ import { checkApp } from "./check.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";
22
+ import { explainApp, explainTargetKinds } from "./explain.js";
23
+ import {
24
+ applyDoctorFixesWithResult,
25
+ applyDoctorFixPlan,
26
+ createDoctorFixInspectionResult,
27
+ inspectApp,
28
+ planDoctorFixes,
29
+ } from "./inspect.js";
7
30
  import { lintApp } from "./lint.js";
8
31
  import {
9
32
  type MakeResourceResult,
@@ -68,6 +91,8 @@ const makeArtifactChoices = [
68
91
  "use-case",
69
92
  ] as const;
70
93
 
94
+ const defaultMcpCheckTimeoutMs = 10 * 60 * 1_000;
95
+
71
96
  type MakeArtifact = (typeof makeArtifactChoices)[number];
72
97
 
73
98
  const makeInputSchema = {
@@ -325,7 +350,7 @@ async function runProviderAddTool(
325
350
  }
326
351
 
327
352
  /**
328
- * Build the Beignet MCP server with routes, doctor, lint, and make tools.
353
+ * Build the Beignet MCP server with app-map, validation, and generator tools.
329
354
  * Every tool resolves against the app directory captured at launch.
330
355
  */
331
356
  export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
@@ -335,6 +360,207 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
335
360
  version: options.version,
336
361
  });
337
362
 
363
+ server.registerTool(
364
+ "app_map",
365
+ {
366
+ description:
367
+ "Return a deterministic graph of this Beignet app: features, contracts, routes, use cases, authorization, workflows, ports, providers, OpenAPI, tests, dependency edges, and validation findings. Project by feature or node kind to keep agent context focused.",
368
+ inputSchema: {
369
+ feature: z
370
+ .string()
371
+ .optional()
372
+ .describe("Project one feature and its direct relationships."),
373
+ kinds: z
374
+ .array(z.enum(appMapNodeKinds))
375
+ .optional()
376
+ .describe("Include only these app-map node kinds."),
377
+ includeDiagnostics: z
378
+ .boolean()
379
+ .optional()
380
+ .describe("Include doctor and lint findings. Defaults to true."),
381
+ },
382
+ annotations: { readOnlyHint: true },
383
+ },
384
+ async (input) =>
385
+ safeToolResult(async () => {
386
+ const result = await mapApp({ cwd, strict: true });
387
+ return jsonResult(
388
+ projectAppMap(result, {
389
+ feature: input.feature,
390
+ kinds: input.kinds,
391
+ includeDiagnostics: input.includeDiagnostics,
392
+ }),
393
+ );
394
+ }),
395
+ );
396
+
397
+ server.registerTool(
398
+ "explain",
399
+ {
400
+ description:
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.",
402
+ inputSchema: {
403
+ kind: z.enum(explainTargetKinds).describe("Mapped concept to explain."),
404
+ target: z
405
+ .string()
406
+ .min(1)
407
+ .describe(
408
+ "Stable ID, runtime name, declaration name, source selector, applicable concept alias, or diagnostic code.",
409
+ ),
410
+ },
411
+ annotations: { readOnlyHint: true },
412
+ },
413
+ async (input) =>
414
+ safeToolResult(async () =>
415
+ jsonResult(
416
+ await explainApp({ cwd, kind: input.kind, target: input.target }),
417
+ ),
418
+ ),
419
+ );
420
+
421
+ server.registerTool(
422
+ "check",
423
+ {
424
+ description:
425
+ "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.",
426
+ inputSchema: {
427
+ preflight: z
428
+ .boolean()
429
+ .optional()
430
+ .describe(
431
+ "Include the environment preflight check before app package scripts. Defaults to false.",
432
+ ),
433
+ timeoutMs: z
434
+ .number()
435
+ .int()
436
+ .min(1_000)
437
+ .max(60 * 60 * 1_000)
438
+ .optional()
439
+ .describe(
440
+ "Maximum duration for each app package script in milliseconds. Defaults to 600000 (10 minutes).",
441
+ ),
442
+ },
443
+ annotations: {
444
+ readOnlyHint: false,
445
+ destructiveHint: true,
446
+ idempotentHint: false,
447
+ openWorldHint: true,
448
+ },
449
+ },
450
+ async (input, extra) =>
451
+ safeToolResult(async () =>
452
+ jsonResult(
453
+ await checkApp({
454
+ cwd,
455
+ preflight: input.preflight ?? false,
456
+ scriptTimeoutMs: input.timeoutMs ?? defaultMcpCheckTimeoutMs,
457
+ signal: extra.signal,
458
+ }),
459
+ ),
460
+ ),
461
+ );
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
+
338
564
  server.registerTool(
339
565
  "routes",
340
566
  {
@@ -363,7 +589,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
363
589
  "doctor",
364
590
  {
365
591
  description:
366
- "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.",
367
593
  inputSchema: {
368
594
  strict: z
369
595
  .boolean()
@@ -383,28 +609,82 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
383
609
  }),
384
610
  );
385
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
+
386
638
  server.registerTool(
387
639
  "doctor_fix",
388
640
  {
389
641
  description:
390
- "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.",
391
643
  inputSchema: {
392
644
  strict: z
393
645
  .boolean()
394
646
  .optional()
395
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,
396
664
  },
397
665
  },
398
666
  async (input) =>
399
667
  safeToolResult(async () => {
400
668
  const strict = input.strict ?? true;
401
- const fixes = await applyDoctorFixes({ cwd, strict });
402
- const result = await inspectApp({ cwd, strict });
403
- return jsonResult({
404
- diagnostics: result.diagnostics,
405
- convention: result.convention,
406
- fixes,
407
- });
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
+ );
408
688
  }),
409
689
  );
410
690
 
package/src/preflight.ts CHANGED
@@ -245,6 +245,20 @@ export async function runPreflight(
245
245
  }
246
246
  }
247
247
 
248
+ const inngestDev = env.INNGEST_DEV?.trim().toLowerCase();
249
+ if (
250
+ env.NODE_ENV === "production" &&
251
+ inngestDev !== undefined &&
252
+ !["", "0", "false"].includes(inngestDev)
253
+ ) {
254
+ findings.push({
255
+ severity: "error",
256
+ code: "BEIGNET_PREFLIGHT_INNGEST_DEV_IN_PRODUCTION",
257
+ message:
258
+ "INNGEST_DEV enables the local Inngest dev server and must be disabled in production.",
259
+ });
260
+ }
261
+
248
262
  const exampleSource = await readFileIfExists(
249
263
  path.join(options.cwd, ".env.example"),
250
264
  );