@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/inspect.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parseProviderPackageMetadata } from "@beignet/core/providers";
4
4
  import { createPainter } from "./ansi.js";
@@ -15,6 +15,18 @@ import {
15
15
  type ResolvedBeignetConfig,
16
16
  resolveConfig,
17
17
  } from "./config.js";
18
+ import {
19
+ applyPlannedDoctorFixes,
20
+ createDoctorFixPlan,
21
+ type DoctorFixApplyResult,
22
+ type DoctorFixOperationId,
23
+ type DoctorFixPlan,
24
+ type InspectFix,
25
+ type PlannedDoctorFixOperation,
26
+ type PlannedDoctorFixPlan,
27
+ planDoctorFixFileChange,
28
+ publicDoctorFixPlan,
29
+ } from "./doctor-fixes.js";
18
30
  import {
19
31
  formatGithubAnnotation,
20
32
  type GithubAnnotationSeverity,
@@ -55,6 +67,7 @@ import {
55
67
  import {
56
68
  currentGeneratedPackageScripts,
57
69
  externalVersions,
70
+ isLegacyGeneratedPackageScript,
58
71
  legacyGeneratedPackageScripts,
59
72
  } from "./templates/shared.js";
60
73
  import { testSupportTemplateFiles } from "./templates/testing.js";
@@ -119,14 +132,7 @@ export type InspectDiagnostic = {
119
132
  contract?: string;
120
133
  };
121
134
 
122
- /**
123
- * Automatic doctor fix that was applied.
124
- */
125
- export type InspectFix = {
126
- code: string;
127
- message: string;
128
- file: string;
129
- };
135
+ export type { InspectFix } from "./doctor-fixes.js";
130
136
 
131
137
  /**
132
138
  * Detected app convention and missing convention files.
@@ -157,6 +163,18 @@ export type InspectAppResult = {
157
163
  fixes: InspectFix[];
158
164
  };
159
165
 
166
+ /** Read-only doctor fix plan paired with the diagnostics it was derived from. */
167
+ export type DoctorFixPlanResult = DoctorFixPlan & {
168
+ convention: InspectConvention;
169
+ diagnostics: InspectDiagnostic[];
170
+ };
171
+
172
+ /** Post-apply inspection paired with the guarded plan metadata that ran. */
173
+ export type DoctorFixInspectionResult = InspectAppResult & {
174
+ planId: string;
175
+ operationIds: DoctorFixOperationId[];
176
+ };
177
+
160
178
  type RouteExport = {
161
179
  method: HttpMethod;
162
180
  handlerFile: string;
@@ -235,9 +253,66 @@ export async function inspectApp(
235
253
  };
236
254
  }
237
255
 
256
+ /** Options for guarded all-or-selected doctor fix application. */
257
+ export type ApplyDoctorFixPlanOptions = InspectAppOptions & {
258
+ planId: string;
259
+ fixIds?: readonly DoctorFixOperationId[];
260
+ };
261
+
262
+ /** Plan every currently available low-risk doctor repair without writing. */
263
+ export async function planDoctorFixes(
264
+ options: InspectAppOptions = {},
265
+ ): Promise<DoctorFixPlanResult> {
266
+ const plan = publicDoctorFixPlan(await buildDoctorFixPlan(options));
267
+ const inspected = await inspectApp(options);
268
+ return {
269
+ ...plan,
270
+ convention: inspected.convention,
271
+ diagnostics: inspected.diagnostics,
272
+ };
273
+ }
274
+
275
+ /** Apply a current doctor fix plan after verifying its plan ID. */
276
+ export async function applyDoctorFixPlan(
277
+ options: ApplyDoctorFixPlanOptions,
278
+ ): Promise<DoctorFixApplyResult> {
279
+ const plan = await buildDoctorFixPlan(options);
280
+ if (plan.planId !== options.planId) {
281
+ throw new Error(
282
+ "Doctor fix plan is stale because the available repairs changed. No files were changed; run beignet doctor --fix --dry-run again.",
283
+ );
284
+ }
285
+ return applyPlannedDoctorFixes(plan, options.fixIds);
286
+ }
287
+
238
288
  export async function applyDoctorFixes(
239
289
  options: InspectAppOptions = {},
240
290
  ): Promise<InspectFix[]> {
291
+ return (await applyDoctorFixesWithResult(options)).fixes;
292
+ }
293
+
294
+ export async function applyDoctorFixesWithResult(
295
+ options: InspectAppOptions = {},
296
+ ): Promise<DoctorFixApplyResult> {
297
+ const plan = await buildDoctorFixPlan(options);
298
+ return applyPlannedDoctorFixes(plan);
299
+ }
300
+
301
+ export function createDoctorFixInspectionResult(
302
+ inspected: InspectAppResult,
303
+ applied: DoctorFixApplyResult,
304
+ ): DoctorFixInspectionResult {
305
+ return {
306
+ ...inspected,
307
+ planId: applied.planId,
308
+ operationIds: applied.operationIds,
309
+ fixes: applied.fixes,
310
+ };
311
+ }
312
+
313
+ async function buildDoctorFixPlan(
314
+ options: InspectAppOptions = {},
315
+ ): Promise<PlannedDoctorFixPlan> {
241
316
  const targetDir = path.resolve(options.cwd ?? process.cwd());
242
317
  await assertDirectory(targetDir);
243
318
 
@@ -253,44 +328,63 @@ export async function applyDoctorFixes(
253
328
  config,
254
329
  contracts,
255
330
  );
256
- const fixes: InspectFix[] = [];
331
+ const operations: PlannedDoctorFixOperation[] = [];
257
332
 
258
- const packageFix = await fixPackageScripts(targetDir, files, convention);
259
- if (packageFix) fixes.push(packageFix);
333
+ const packageFix = await planPackageScripts(targetDir, files, convention);
334
+ if (packageFix) operations.push(packageFix);
260
335
 
261
- const routeGroupFix = await fixUnregisteredRouteGroups(
336
+ const routeGroupFix = await planUnregisteredRouteGroups(
262
337
  targetDir,
263
338
  files,
264
339
  config,
265
340
  );
266
- if (routeGroupFix) fixes.push(routeGroupFix);
341
+ if (routeGroupFix) operations.push(routeGroupFix);
267
342
 
268
343
  const drift = await workflowRegistrationDrift(targetDir, files, config);
269
344
 
270
- const scheduleFix = await fixUnregisteredSchedules(
345
+ if (config.paths.schedules === config.paths.tasks) {
346
+ const workflowFix = await planCoLocatedScheduleAndTaskRegistries(
347
+ targetDir,
348
+ files,
349
+ drift,
350
+ config,
351
+ );
352
+ if (workflowFix) operations.push(workflowFix);
353
+ } else {
354
+ const scheduleFix = await planUnregisteredSchedules(
355
+ targetDir,
356
+ files,
357
+ drift,
358
+ config,
359
+ );
360
+ if (scheduleFix) operations.push(scheduleFix);
361
+
362
+ const taskFix = await planUnregisteredTasks(
363
+ targetDir,
364
+ files,
365
+ drift,
366
+ config,
367
+ );
368
+ if (taskFix) operations.push(taskFix);
369
+ }
370
+
371
+ const outboxFix = await planUnregisteredOutboxEntries(
271
372
  targetDir,
272
373
  files,
273
374
  drift,
274
375
  config,
275
376
  );
276
- if (scheduleFix) fixes.push(scheduleFix);
377
+ if (outboxFix) operations.push(outboxFix);
277
378
 
278
- const taskFix = await fixUnregisteredTasks(targetDir, files, drift, config);
279
- if (taskFix) fixes.push(taskFix);
280
-
281
- fixes.push(
282
- ...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
283
- );
284
-
285
- const listenerFix = await fixUnregisteredListeners(
379
+ const listenerFix = await planUnregisteredListeners(
286
380
  targetDir,
287
381
  files,
288
382
  drift,
289
383
  config,
290
384
  );
291
- if (listenerFix) fixes.push(listenerFix);
385
+ if (listenerFix) operations.push(listenerFix);
292
386
 
293
- const openApiFix = await fixDirectOpenApiArrayDrift(
387
+ const openApiFix = await planDirectOpenApiArrayDrift(
294
388
  targetDir,
295
389
  files,
296
390
  config,
@@ -298,9 +392,13 @@ export async function applyDoctorFixes(
298
392
  contracts,
299
393
  matchedRoutes,
300
394
  );
301
- if (openApiFix) fixes.push(openApiFix);
395
+ if (openApiFix) operations.push(openApiFix);
302
396
 
303
- return fixes;
397
+ return createDoctorFixPlan({
398
+ targetDir,
399
+ strict: Boolean(options.strict),
400
+ operations,
401
+ });
304
402
  }
305
403
 
306
404
  /**
@@ -329,12 +427,97 @@ export function formatRoutes(result: InspectAppResult): string {
329
427
  ]);
330
428
  }
331
429
 
430
+ /**
431
+ * Format a read-only doctor fix plan with exact patches and apply commands.
432
+ */
433
+ export function formatDoctorFixPlan(
434
+ plan: DoctorFixPlanResult,
435
+ options: { color?: boolean } = {},
436
+ ): string {
437
+ const paint = createPainter(options.color);
438
+ const lines: string[] = [];
439
+ if (plan.operations.length === 0) {
440
+ lines.push(`No applicable Beignet fixes found in ${plan.targetDir}.`);
441
+ } else {
442
+ const strictFlag = plan.strict ? " --strict" : "";
443
+ lines.push(
444
+ `Planned ${plan.operations.length} Beignet fix operation${plan.operations.length === 1 ? "" : "s"} in ${plan.targetDir}:`,
445
+ "",
446
+ );
447
+ for (const operation of plan.operations) {
448
+ lines.push(
449
+ `${paint("PLAN", "cyan")} ${operation.id}`,
450
+ ...operation.fixes.map(
451
+ (fix) => ` ${fix.code} ${fix.file}\n ${fix.message}`,
452
+ ),
453
+ ...operation.changes.flatMap((change) => [
454
+ ` ${change.kind === "create" ? "Create" : "Update"} ${change.file}`,
455
+ change.patch.trimEnd(),
456
+ ]),
457
+ "",
458
+ );
459
+ }
460
+
461
+ lines.push(
462
+ `Plan ID: ${plan.planId}`,
463
+ "",
464
+ "Apply the complete plan:",
465
+ ` beignet doctor --fix${strictFlag} --plan ${plan.planId}`,
466
+ "",
467
+ "Apply selected operations:",
468
+ ` beignet doctor --fix${strictFlag} --plan ${plan.planId} --only ${plan.operations.map((operation) => operation.id).join(",")}`,
469
+ );
470
+ }
471
+
472
+ lines.push(
473
+ "",
474
+ formatDoctorResult(
475
+ {
476
+ targetDir: plan.targetDir,
477
+ strict: plan.strict,
478
+ diagnostics: plan.diagnostics,
479
+ fixes: [],
480
+ },
481
+ options,
482
+ ),
483
+ );
484
+ return lines.join("\n");
485
+ }
486
+
487
+ /**
488
+ * Format planned fixes as GitHub Actions notice annotations.
489
+ */
490
+ export function formatDoctorFixPlanGithub(plan: DoctorFixPlanResult): string {
491
+ return [
492
+ ...plan.operations.map((operation) =>
493
+ formatGithubAnnotation({
494
+ severity: "notice",
495
+ file: operation.changes[0]?.file,
496
+ message: `${operation.id}: ${operation.fixes.map((fix) => fix.message).join(" ")} Plan ${plan.planId}.`,
497
+ }),
498
+ ),
499
+ ...formatDoctorDiagnosticsGithub(plan.diagnostics),
500
+ ].join("\n");
501
+ }
502
+
503
+ type DoctorFormatResult = Pick<
504
+ InspectAppResult,
505
+ "targetDir" | "strict" | "diagnostics" | "fixes"
506
+ >;
507
+
332
508
  /**
333
509
  * Format doctor diagnostics and applied fixes for CLI output.
334
510
  */
335
511
  export function formatDoctor(
336
512
  result: InspectAppResult,
337
513
  options: { color?: boolean } = {},
514
+ ): string {
515
+ return formatDoctorResult(result, options);
516
+ }
517
+
518
+ function formatDoctorResult(
519
+ result: DoctorFormatResult,
520
+ options: { color?: boolean } = {},
338
521
  ): string {
339
522
  const paint = createPainter(options.color);
340
523
  const fixLines =
@@ -456,7 +639,7 @@ function productionHardeningChecklist(
456
639
  ];
457
640
  }
458
641
 
459
- function formatDoctorFooter(result: InspectAppResult): string {
642
+ function formatDoctorFooter(result: DoctorFormatResult): string {
460
643
  const count = (severity: InspectDiagnostic["severity"]) =>
461
644
  result.diagnostics.filter((diagnostic) => diagnostic.severity === severity)
462
645
  .length;
@@ -469,15 +652,19 @@ function formatDoctorFooter(result: InspectAppResult): string {
469
652
  * Format doctor diagnostics as GitHub Actions annotations.
470
653
  */
471
654
  export function formatDoctorGithub(result: InspectAppResult): string {
472
- return result.diagnostics
473
- .map((diagnostic) =>
474
- formatGithubAnnotation({
475
- severity: githubAnnotationSeverity(diagnostic.severity),
476
- file: diagnostic.file,
477
- message: `${diagnostic.code}: ${diagnostic.message}`,
478
- }),
479
- )
480
- .join("\n");
655
+ return formatDoctorDiagnosticsGithub(result.diagnostics).join("\n");
656
+ }
657
+
658
+ function formatDoctorDiagnosticsGithub(
659
+ diagnostics: InspectDiagnostic[],
660
+ ): string[] {
661
+ return diagnostics.map((diagnostic) =>
662
+ formatGithubAnnotation({
663
+ severity: githubAnnotationSeverity(diagnostic.severity),
664
+ file: diagnostic.file,
665
+ message: `${diagnostic.code}: ${diagnostic.message}`,
666
+ }),
667
+ );
481
668
  }
482
669
 
483
670
  function githubAnnotationSeverity(
@@ -618,7 +805,7 @@ function parseContracts(source: string, file: string): InspectedContract[] {
618
805
  for (const exportMatch of source.matchAll(exportRegex)) {
619
806
  const block = exportMatch[2];
620
807
  const methodMatch =
621
- /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["']\s*\)/.exec(
808
+ /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["'](?:\s*,\s*["'][^"']+["'])?\s*\)/.exec(
622
809
  block,
623
810
  );
624
811
  if (methodMatch) {
@@ -1521,6 +1708,7 @@ async function inspectDiagnostics(
1521
1708
  config,
1522
1709
  convention,
1523
1710
  )),
1711
+ ...(await inspectInngestProductionPath(targetDir, files, config)),
1524
1712
  ...(await inspectDatabaseLifecycleDrift(
1525
1713
  targetDir,
1526
1714
  files,
@@ -3095,6 +3283,73 @@ async function inspectProviderRegistrationDrift(
3095
3283
  return diagnostics;
3096
3284
  }
3097
3285
 
3286
+ async function inspectInngestProductionPath(
3287
+ targetDir: string,
3288
+ files: string[],
3289
+ config: ResolvedBeignetConfig,
3290
+ ): Promise<InspectDiagnostic[]> {
3291
+ const packageJson = await readPackageJson(targetDir, files);
3292
+ if (
3293
+ !installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")
3294
+ ) {
3295
+ return [];
3296
+ }
3297
+
3298
+ const diagnostics: InspectDiagnostic[] = [];
3299
+ const serverDir = directoryPath(path.dirname(config.paths.server));
3300
+ const registryFile = `${serverDir}/inngest.ts`;
3301
+ const routeFile = `${directoryPath(config.paths.routes)}/inngest/route.ts`;
3302
+
3303
+ if (!files.includes(registryFile)) {
3304
+ diagnostics.push({
3305
+ severity: "warning",
3306
+ code: "BEIGNET_INNGEST_REGISTRY_MISSING",
3307
+ file: registryFile,
3308
+ message: `@beignet/provider-jobs-inngest is installed, but ${registryFile} is missing. Run beignet provider add jobs-inngest or create a central createInngestJobFunctions(...) registry so dispatched jobs can execute.`,
3309
+ });
3310
+ } else {
3311
+ const registrySource = await readFile(
3312
+ path.join(targetDir, registryFile),
3313
+ "utf8",
3314
+ );
3315
+ const registry = arrayInitializerInfo(registrySource, "inngestJobs");
3316
+ const registeredJobs = registry
3317
+ ? identifiersFromArrayExpression(registry.text)
3318
+ : new Set<string>();
3319
+ const featureRoot = directoryPath(config.paths.features);
3320
+ const jobIndexes = files.filter((file) =>
3321
+ new RegExp(`^${escapeRegExp(featureRoot)}/[^/]+/jobs/index\\.ts$`).test(
3322
+ file,
3323
+ ),
3324
+ );
3325
+ for (const jobIndex of jobIndexes) {
3326
+ const source = await readFile(path.join(targetDir, jobIndex), "utf8");
3327
+ const match = source.match(/export const\s+([A-Za-z_$][\w$]*Jobs)\s*=/);
3328
+ const registryName = match?.[1];
3329
+ if (!registryName || registeredJobs.has(registryName)) {
3330
+ continue;
3331
+ }
3332
+ diagnostics.push({
3333
+ severity: "warning",
3334
+ code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
3335
+ file: registryFile,
3336
+ message: `${jobIndex} exports ${registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${registryName} so Inngest can execute the job.`,
3337
+ });
3338
+ }
3339
+ }
3340
+
3341
+ if (config.framework === "next" && !files.includes(routeFile)) {
3342
+ diagnostics.push({
3343
+ severity: "warning",
3344
+ code: "BEIGNET_INNGEST_ROUTE_MISSING",
3345
+ file: routeFile,
3346
+ message: `@beignet/provider-jobs-inngest is installed in a Next.js app, but ${routeFile} is missing. Expose the shared Inngest client and function registry with serve(...) so Inngest can discover and invoke jobs.`,
3347
+ });
3348
+ }
3349
+
3350
+ return diagnostics;
3351
+ }
3352
+
3098
3353
  function isFeatureUploadDefinition(
3099
3354
  file: string,
3100
3355
  config: ResolvedBeignetConfig,
@@ -4358,7 +4613,7 @@ function callReferencesCentralListenerRegistry(args: {
4358
4613
 
4359
4614
  for (const identifier of args.callIdentifiers) {
4360
4615
  const imported = args.namedImports.get(identifier);
4361
- if (!imported || imported.importedName !== "listeners") continue;
4616
+ if (imported?.importedName !== "listeners") continue;
4362
4617
 
4363
4618
  const importedFile = sourceFileFromImport(
4364
4619
  imported.sourcePath,
@@ -5952,8 +6207,7 @@ async function inspectPackageScripts(
5952
6207
  severity: "warning",
5953
6208
  code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
5954
6209
  file: "package.json",
5955
- message:
5956
- 'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
6210
+ message: `package.json does not define a test script. Add "test": "${currentGeneratedPackageScripts.test}" or run beignet doctor --fix.`,
5957
6211
  });
5958
6212
  }
5959
6213
 
@@ -5965,7 +6219,7 @@ async function inspectPackageScripts(
5965
6219
  severity: "warning",
5966
6220
  code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
5967
6221
  file: "package.json",
5968
- message: `package.json still uses legacy generated Bun scripts for ${legacyScripts.join(", ")}. Run beignet doctor --fix to migrate them to package-manager-neutral tsx scripts.`,
6222
+ message: `package.json still uses legacy generated scripts for ${legacyScripts.join(", ")}. Run beignet doctor --fix to migrate them to the current package-manager-neutral scripts.`,
5969
6223
  });
5970
6224
  }
5971
6225
 
@@ -5999,11 +6253,11 @@ async function inspectPackageScripts(
5999
6253
  return diagnostics;
6000
6254
  }
6001
6255
 
6002
- async function fixPackageScripts(
6256
+ async function planPackageScripts(
6003
6257
  targetDir: string,
6004
6258
  files: string[],
6005
6259
  convention: InspectConvention,
6006
- ): Promise<InspectFix | undefined> {
6260
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6007
6261
  if (!convention.resourceGenerator || !files.includes("package.json")) {
6008
6262
  return undefined;
6009
6263
  }
@@ -6029,10 +6283,8 @@ async function fixPackageScripts(
6029
6283
  }
6030
6284
 
6031
6285
  if (!hasDirectBunTestImports) {
6032
- for (const [name, legacyCommand] of Object.entries(
6033
- legacyGeneratedPackageScripts,
6034
- )) {
6035
- if (packageJson.scripts[name] === legacyCommand) {
6286
+ for (const name of Object.keys(legacyGeneratedPackageScripts)) {
6287
+ if (isLegacyGeneratedPackageScript(name, packageJson.scripts[name])) {
6036
6288
  packageJson.scripts[name] = currentGeneratedPackageScripts[name];
6037
6289
  if (!updatedScripts.includes(name)) updatedScripts.push(name);
6038
6290
  }
@@ -6050,29 +6302,25 @@ async function fixPackageScripts(
6050
6302
  }
6051
6303
 
6052
6304
  const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
6053
- const createdSupportFiles = await createMissingTestSupportFiles(
6054
- targetDir,
6055
- files,
6056
- packageJson.scripts.test === currentGeneratedPackageScripts.test,
6057
- );
6305
+ const createdSupportFiles =
6306
+ packageJson.scripts.test === currentGeneratedPackageScripts.test
6307
+ ? missingTestSupportFiles(files)
6308
+ : [];
6058
6309
 
6059
6310
  if (next === original && createdSupportFiles.length === 0) return undefined;
6060
6311
 
6061
- if (next !== original) await writeFile(filePath, next);
6062
-
6312
+ let fix: InspectFix | undefined;
6063
6313
  if (addedTestScript) {
6064
- return {
6314
+ fix = {
6065
6315
  code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
6066
6316
  file: "package.json",
6067
6317
  message:
6068
6318
  createdSupportFiles.length > 0
6069
- ? `Added "test": "tsx lib/beignet-test-runner.ts" and ${createdSupportFiles.join(", ")}.`
6070
- : 'Added "test": "tsx lib/beignet-test-runner.ts".',
6319
+ ? `Added "test": "${currentGeneratedPackageScripts.test}" and ${createdSupportFiles.join(", ")}.`
6320
+ : `Added "test": "${currentGeneratedPackageScripts.test}".`,
6071
6321
  };
6072
- }
6073
-
6074
- if (updatedScripts.length > 0) {
6075
- return {
6322
+ } else if (updatedScripts.length > 0) {
6323
+ fix = {
6076
6324
  code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
6077
6325
  file: "package.json",
6078
6326
  message: packageScriptFixMessage(
@@ -6081,31 +6329,48 @@ async function fixPackageScripts(
6081
6329
  addedTsx,
6082
6330
  ),
6083
6331
  };
6084
- }
6085
-
6086
- if (createdSupportFiles.length > 0) {
6087
- return {
6332
+ } else if (createdSupportFiles.length > 0) {
6333
+ fix = {
6088
6334
  code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
6089
6335
  file: "package.json",
6090
6336
  message: `Restored ${createdSupportFiles.join(", ")}.`,
6091
6337
  };
6092
- }
6093
-
6094
- if (addedTsx) {
6095
- return {
6338
+ } else if (addedTsx) {
6339
+ fix = {
6096
6340
  code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
6097
6341
  file: "package.json",
6098
6342
  message: 'Added "tsx" to devDependencies.',
6099
6343
  };
6100
6344
  }
6101
6345
 
6102
- return undefined;
6346
+ if (!fix) return undefined;
6347
+
6348
+ return {
6349
+ id: "package.repair-generated-support",
6350
+ fixes: [fix],
6351
+ changes: [
6352
+ ...(next === original
6353
+ ? []
6354
+ : [
6355
+ planDoctorFixFileChange({
6356
+ file: "package.json",
6357
+ before: original,
6358
+ after: next,
6359
+ }),
6360
+ ]),
6361
+ ...testSupportTemplateFiles
6362
+ .filter((file) => createdSupportFiles.includes(file.path))
6363
+ .map((file) =>
6364
+ planDoctorFixFileChange({ file: file.path, after: file.content }),
6365
+ ),
6366
+ ],
6367
+ };
6103
6368
  }
6104
6369
 
6105
6370
  function legacyGeneratedScriptNames(scripts: Record<string, string>): string[] {
6106
- return Object.entries(legacyGeneratedPackageScripts)
6107
- .filter(([name, command]) => scripts[name] === command)
6108
- .map(([name]) => name);
6371
+ return Object.keys(legacyGeneratedPackageScripts).filter((name) =>
6372
+ isLegacyGeneratedPackageScript(name, scripts[name]),
6373
+ );
6109
6374
  }
6110
6375
 
6111
6376
  async function appUsesDirectBunTestImports(
@@ -6126,7 +6391,8 @@ function hasCurrentGeneratedTsxScript(
6126
6391
  ): boolean {
6127
6392
  return Object.values(currentGeneratedPackageScripts).some(
6128
6393
  (command) =>
6129
- command.startsWith("tsx ") && Object.values(scripts).includes(command),
6394
+ (command.startsWith("tsx ") || command.includes("--import tsx")) &&
6395
+ Object.values(scripts).includes(command),
6130
6396
  );
6131
6397
  }
6132
6398
 
@@ -6136,23 +6402,6 @@ function missingTestSupportFiles(files: string[]): string[] {
6136
6402
  .filter((file) => !files.includes(file));
6137
6403
  }
6138
6404
 
6139
- async function createMissingTestSupportFiles(
6140
- targetDir: string,
6141
- files: string[],
6142
- shouldCreate: boolean,
6143
- ): Promise<string[]> {
6144
- if (!shouldCreate) return [];
6145
- const createdSupportFiles: string[] = [];
6146
- for (const file of testSupportTemplateFiles) {
6147
- if (files.includes(file.path)) continue;
6148
- const destination = path.join(targetDir, file.path);
6149
- await mkdir(path.dirname(destination), { recursive: true });
6150
- await writeFile(destination, file.content);
6151
- createdSupportFiles.push(file.path);
6152
- }
6153
- return createdSupportFiles;
6154
- }
6155
-
6156
6405
  function packageScriptFixMessage(
6157
6406
  updatedScripts: string[],
6158
6407
  createdSupportFiles: string[],
@@ -6659,14 +6908,14 @@ function findRouteGroupContract(
6659
6908
  );
6660
6909
  }
6661
6910
 
6662
- async function fixDirectOpenApiArrayDrift(
6911
+ async function planDirectOpenApiArrayDrift(
6663
6912
  targetDir: string,
6664
6913
  files: string[],
6665
6914
  config: ResolvedBeignetConfig,
6666
6915
  convention: InspectConvention,
6667
6916
  contracts: InspectedContract[],
6668
6917
  matchedRoutes: MatchedRoutesResult,
6669
- ): Promise<InspectFix | undefined> {
6918
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6670
6919
  const routePath = path.join(targetDir, config.paths.openapiRoute);
6671
6920
  let source: string;
6672
6921
  try {
@@ -6709,21 +6958,32 @@ async function fixDirectOpenApiArrayDrift(
6709
6958
  )}`;
6710
6959
  if (next === source) return undefined;
6711
6960
 
6712
- await writeFile(routePath, next);
6713
6961
  return {
6714
- code: "BEIGNET_OPENAPI_MISSING",
6715
- file: config.paths.openapiRoute,
6716
- message: `Added ${missingContracts
6717
- .map((contract) => contract.exportName)
6718
- .join(", ")} to the direct createOpenAPIHandler contract array.`,
6962
+ id: "openapi.register-missing",
6963
+ fixes: [
6964
+ {
6965
+ code: "BEIGNET_OPENAPI_MISSING",
6966
+ file: config.paths.openapiRoute,
6967
+ message: `Added ${missingContracts
6968
+ .map((contract) => contract.exportName)
6969
+ .join(", ")} to the direct createOpenAPIHandler contract array.`,
6970
+ },
6971
+ ],
6972
+ changes: [
6973
+ planDoctorFixFileChange({
6974
+ file: config.paths.openapiRoute,
6975
+ before: source,
6976
+ after: next,
6977
+ }),
6978
+ ],
6719
6979
  };
6720
6980
  }
6721
6981
 
6722
- async function fixUnregisteredRouteGroups(
6982
+ async function planUnregisteredRouteGroups(
6723
6983
  targetDir: string,
6724
6984
  files: string[],
6725
6985
  config: ResolvedBeignetConfig,
6726
- ): Promise<InspectFix | undefined> {
6986
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6727
6987
  if (!files.includes(config.paths.server)) return undefined;
6728
6988
 
6729
6989
  const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
@@ -6794,25 +7054,36 @@ async function fixUnregisteredRouteGroups(
6794
7054
  )}`;
6795
7055
  if (next === original) return undefined;
6796
7056
 
6797
- await writeFile(targetPath, next);
6798
7057
  return {
6799
- code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
6800
- file: targetFile,
6801
- message: `Registered ${groupsToAppend
6802
- .map((routeGroup) => routeGroup.name)
6803
- .join(", ")} in the central route list.`,
7058
+ id: "routes.register-missing",
7059
+ fixes: [
7060
+ {
7061
+ code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
7062
+ file: targetFile,
7063
+ message: `Registered ${groupsToAppend
7064
+ .map((routeGroup) => routeGroup.name)
7065
+ .join(", ")} in the central route list.`,
7066
+ },
7067
+ ],
7068
+ changes: [
7069
+ planDoctorFixFileChange({
7070
+ file: targetFile,
7071
+ before: original,
7072
+ after: next,
7073
+ }),
7074
+ ],
6804
7075
  };
6805
7076
  }
6806
7077
 
6807
- async function fixUnregisteredSchedules(
7078
+ async function planUnregisteredSchedules(
6808
7079
  targetDir: string,
6809
7080
  files: string[],
6810
7081
  drift: WorkflowRegistrationDrift,
6811
7082
  config: ResolvedBeignetConfig,
6812
- ): Promise<InspectFix | undefined> {
7083
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6813
7084
  if (!drift.schedules.centralExists) return undefined;
6814
7085
 
6815
- return applyWorkflowRegistryFix({
7086
+ return planWorkflowRegistryOperation("schedules.register-missing", {
6816
7087
  targetDir,
6817
7088
  files,
6818
7089
  centralFile: config.paths.schedules,
@@ -6825,15 +7096,15 @@ async function fixUnregisteredSchedules(
6825
7096
  });
6826
7097
  }
6827
7098
 
6828
- async function fixUnregisteredTasks(
7099
+ async function planUnregisteredTasks(
6829
7100
  targetDir: string,
6830
7101
  files: string[],
6831
7102
  drift: WorkflowRegistrationDrift,
6832
7103
  config: ResolvedBeignetConfig,
6833
- ): Promise<InspectFix | undefined> {
7104
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6834
7105
  if (!drift.tasks.centralExists) return undefined;
6835
7106
 
6836
- return applyWorkflowRegistryFix({
7107
+ return planWorkflowRegistryOperation("tasks.register-missing", {
6837
7108
  targetDir,
6838
7109
  files,
6839
7110
  centralFile: config.paths.tasks,
@@ -6847,6 +7118,84 @@ async function fixUnregisteredTasks(
6847
7118
  });
6848
7119
  }
6849
7120
 
7121
+ async function planCoLocatedScheduleAndTaskRegistries(
7122
+ targetDir: string,
7123
+ files: string[],
7124
+ drift: WorkflowRegistrationDrift,
7125
+ config: ResolvedBeignetConfig,
7126
+ ): Promise<PlannedDoctorFixOperation | undefined> {
7127
+ if (!drift.schedules.centralExists || !drift.tasks.centralExists) {
7128
+ return undefined;
7129
+ }
7130
+
7131
+ const centralFile = config.paths.schedules;
7132
+ let original: string;
7133
+ try {
7134
+ original = await readFile(path.join(targetDir, centralFile), "utf8");
7135
+ } catch {
7136
+ return undefined;
7137
+ }
7138
+
7139
+ let next = original;
7140
+ const fixes: InspectFix[] = [];
7141
+ const repairedKinds: Array<"schedules" | "tasks"> = [];
7142
+
7143
+ const scheduleEdit = planWorkflowRegistryEdit({
7144
+ files,
7145
+ centralFile,
7146
+ source: next,
7147
+ unregistered: drift.schedules.unregistered,
7148
+ code: "BEIGNET_SCHEDULE_UNREGISTERED",
7149
+ listName: "the schedules array",
7150
+ importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
7151
+ append: (source, entry, importLine) =>
7152
+ appendToNamedArray(source, "schedules", entry, importLine),
7153
+ });
7154
+ if (scheduleEdit) {
7155
+ next = scheduleEdit.after;
7156
+ fixes.push(scheduleEdit.fix);
7157
+ repairedKinds.push("schedules");
7158
+ }
7159
+
7160
+ const taskEdit = planWorkflowRegistryEdit({
7161
+ files,
7162
+ centralFile,
7163
+ source: next,
7164
+ unregistered: drift.tasks.unregistered,
7165
+ code: "BEIGNET_TASK_UNREGISTERED",
7166
+ listName: "defineTasks([...])",
7167
+ importSpecifier: (indexFile) => relativeModule(centralFile, indexFile),
7168
+ append: (source, entry, importLine) =>
7169
+ appendToNamedArray(source, "tasks", entry, importLine),
7170
+ });
7171
+ if (taskEdit) {
7172
+ next = taskEdit.after;
7173
+ fixes.push(taskEdit.fix);
7174
+ repairedKinds.push("tasks");
7175
+ }
7176
+
7177
+ if (fixes.length === 0 || next === original) return undefined;
7178
+
7179
+ const id: DoctorFixOperationId =
7180
+ repairedKinds.length === 2
7181
+ ? "workflows.register-missing"
7182
+ : repairedKinds[0] === "schedules"
7183
+ ? "schedules.register-missing"
7184
+ : "tasks.register-missing";
7185
+
7186
+ return {
7187
+ id,
7188
+ fixes,
7189
+ changes: [
7190
+ planDoctorFixFileChange({
7191
+ file: centralFile,
7192
+ before: original,
7193
+ after: next,
7194
+ }),
7195
+ ],
7196
+ };
7197
+ }
7198
+
6850
7199
  /**
6851
7200
  * Wire fully unregistered feature listener registries into the central
6852
7201
  * listener registry and ensure the providers file registers that central
@@ -6858,12 +7207,12 @@ async function fixUnregisteredTasks(
6858
7207
  * register against, or when the registry name is already imported from another
6859
7208
  * file. The diagnostic stays in every bail case.
6860
7209
  */
6861
- async function fixUnregisteredListeners(
7210
+ async function planUnregisteredListeners(
6862
7211
  targetDir: string,
6863
7212
  files: string[],
6864
7213
  drift: WorkflowRegistrationDrift,
6865
7214
  config: ResolvedBeignetConfig,
6866
- ): Promise<InspectFix | undefined> {
7215
+ ): Promise<PlannedDoctorFixOperation | undefined> {
6867
7216
  const candidates = drift.listeners.unregistered.filter(
6868
7217
  (entry) => entry.fullyUnregistered,
6869
7218
  );
@@ -6960,32 +7309,61 @@ async function fixUnregisteredListeners(
6960
7309
  return undefined;
6961
7310
  }
6962
7311
 
6963
- await mkdir(path.dirname(listenersPath), { recursive: true });
6964
- await writeFile(listenersPath, nextListeners);
6965
- if (nextProviders !== originalProviders) {
6966
- await writeFile(providersPath, nextProviders);
6967
- }
6968
7312
  return {
6969
- code: "BEIGNET_LISTENER_UNREGISTERED",
6970
- file: listenersFile,
6971
- message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
7313
+ id: "listeners.register-missing",
7314
+ fixes: [
7315
+ {
7316
+ code: "BEIGNET_LISTENER_UNREGISTERED",
7317
+ file: listenersFile,
7318
+ message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
7319
+ },
7320
+ ],
7321
+ changes: [
7322
+ ...(nextListeners === originalListeners
7323
+ ? []
7324
+ : [
7325
+ planDoctorFixFileChange({
7326
+ file: listenersFile,
7327
+ before: originalListeners,
7328
+ after: nextListeners,
7329
+ }),
7330
+ ]),
7331
+ ...(nextProviders === originalProviders
7332
+ ? []
7333
+ : [
7334
+ planDoctorFixFileChange({
7335
+ file: providersFile,
7336
+ before: originalProviders,
7337
+ after: nextProviders,
7338
+ }),
7339
+ ]),
7340
+ ],
6972
7341
  };
6973
7342
  }
6974
7343
 
6975
- async function fixUnregisteredOutboxEntries(
7344
+ async function planUnregisteredOutboxEntries(
6976
7345
  targetDir: string,
6977
7346
  files: string[],
6978
7347
  drift: WorkflowRegistrationDrift,
6979
7348
  config: ResolvedBeignetConfig,
6980
- ): Promise<InspectFix[]> {
6981
- if (!drift.outbox.exists) return [];
7349
+ ): Promise<PlannedDoctorFixOperation | undefined> {
7350
+ if (!drift.outbox.exists) return undefined;
7351
+
7352
+ const outboxPath = path.join(targetDir, config.paths.outbox);
7353
+ let original: string;
7354
+ try {
7355
+ original = await readFile(outboxPath, "utf8");
7356
+ } catch {
7357
+ return undefined;
7358
+ }
6982
7359
 
7360
+ let next = original;
6983
7361
  const fixes: InspectFix[] = [];
6984
7362
 
6985
- const eventFix = await applyWorkflowRegistryFix({
6986
- targetDir,
7363
+ const eventFix = planWorkflowRegistryEdit({
6987
7364
  files,
6988
7365
  centralFile: config.paths.outbox,
7366
+ source: next,
6989
7367
  unregistered: drift.outbox.events,
6990
7368
  code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
6991
7369
  listName: "the outbox events list",
@@ -6993,12 +7371,15 @@ async function fixUnregisteredOutboxEntries(
6993
7371
  append: (source, entry, importLine) =>
6994
7372
  appendToOutboxRegistryArray(source, "events", entry, importLine),
6995
7373
  });
6996
- if (eventFix) fixes.push(eventFix);
7374
+ if (eventFix) {
7375
+ next = eventFix.after;
7376
+ fixes.push(eventFix.fix);
7377
+ }
6997
7378
 
6998
- const jobFix = await applyWorkflowRegistryFix({
6999
- targetDir,
7379
+ const jobFix = planWorkflowRegistryEdit({
7000
7380
  files,
7001
7381
  centralFile: config.paths.outbox,
7382
+ source: next,
7002
7383
  unregistered: drift.outbox.jobs,
7003
7384
  code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
7004
7385
  listName: "the outbox jobs list",
@@ -7006,9 +7387,23 @@ async function fixUnregisteredOutboxEntries(
7006
7387
  append: (source, entry, importLine) =>
7007
7388
  appendToOutboxRegistryArray(source, "jobs", entry, importLine),
7008
7389
  });
7009
- if (jobFix) fixes.push(jobFix);
7390
+ if (jobFix) {
7391
+ next = jobFix.after;
7392
+ fixes.push(jobFix.fix);
7393
+ }
7010
7394
 
7011
- return fixes;
7395
+ if (fixes.length === 0 || next === original) return undefined;
7396
+ return {
7397
+ id: "outbox.register-missing",
7398
+ fixes,
7399
+ changes: [
7400
+ planDoctorFixFileChange({
7401
+ file: config.paths.outbox,
7402
+ before: original,
7403
+ after: next,
7404
+ }),
7405
+ ],
7406
+ };
7012
7407
  }
7013
7408
 
7014
7409
  /**
@@ -7020,7 +7415,7 @@ async function fixUnregisteredOutboxEntries(
7020
7415
  * some members are already individually registered (appending the registry
7021
7416
  * spread would run those members twice). The diagnostic stays in that case.
7022
7417
  */
7023
- async function applyWorkflowRegistryFix(options: {
7418
+ type WorkflowRegistryPlanOptions = {
7024
7419
  targetDir: string;
7025
7420
  files: string[];
7026
7421
  centralFile: string;
@@ -7029,12 +7424,12 @@ async function applyWorkflowRegistryFix(options: {
7029
7424
  listName: string;
7030
7425
  importSpecifier: (indexFile: string) => string;
7031
7426
  append: (source: string, entry: string, importLine?: string) => AppendResult;
7032
- }): Promise<InspectFix | undefined> {
7033
- const candidates = options.unregistered.filter(
7034
- (entry) => entry.fullyUnregistered,
7035
- );
7036
- if (candidates.length === 0) return undefined;
7427
+ };
7037
7428
 
7429
+ async function planWorkflowRegistryOperation(
7430
+ id: DoctorFixOperationId,
7431
+ options: WorkflowRegistryPlanOptions,
7432
+ ): Promise<PlannedDoctorFixOperation | undefined> {
7038
7433
  const centralPath = path.join(options.targetDir, options.centralFile);
7039
7434
  let original: string;
7040
7435
  try {
@@ -7043,7 +7438,30 @@ async function applyWorkflowRegistryFix(options: {
7043
7438
  return undefined;
7044
7439
  }
7045
7440
 
7046
- let next = original;
7441
+ const edit = planWorkflowRegistryEdit({ ...options, source: original });
7442
+ if (!edit) return undefined;
7443
+ return {
7444
+ id,
7445
+ fixes: [edit.fix],
7446
+ changes: [
7447
+ planDoctorFixFileChange({
7448
+ file: options.centralFile,
7449
+ before: original,
7450
+ after: edit.after,
7451
+ }),
7452
+ ],
7453
+ };
7454
+ }
7455
+
7456
+ function planWorkflowRegistryEdit(
7457
+ options: Omit<WorkflowRegistryPlanOptions, "targetDir"> & { source: string },
7458
+ ): { after: string; fix: InspectFix } | undefined {
7459
+ const candidates = options.unregistered.filter(
7460
+ (entry) => entry.fullyUnregistered,
7461
+ );
7462
+ if (candidates.length === 0) return undefined;
7463
+
7464
+ let next = options.source;
7047
7465
  const registeredNames: string[] = [];
7048
7466
 
7049
7467
  for (const { registry } of candidates) {
@@ -7075,13 +7493,15 @@ async function applyWorkflowRegistryFix(options: {
7075
7493
  }
7076
7494
  }
7077
7495
 
7078
- if (next === original || registeredNames.length === 0) return undefined;
7496
+ if (next === options.source || registeredNames.length === 0) return undefined;
7079
7497
 
7080
- await writeFile(centralPath, next);
7081
7498
  return {
7082
- code: options.code,
7083
- file: options.centralFile,
7084
- message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
7499
+ after: next,
7500
+ fix: {
7501
+ code: options.code,
7502
+ file: options.centralFile,
7503
+ message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
7504
+ },
7085
7505
  };
7086
7506
  }
7087
7507