@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/dist/inspect.js CHANGED
@@ -1,13 +1,14 @@
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";
5
5
  import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
6
+ import { applyPlannedDoctorFixes, createDoctorFixPlan, planDoctorFixFileChange, publicDoctorFixPlan, } from "./doctor-fixes.js";
6
7
  import { formatGithubAnnotation, } from "./github-annotations.js";
7
8
  import { createListenersRegistrySource, listenersFilePath, providersFilePath, updateListenersRegistrySource, wireListenersProviderSource, } from "./make/shared.js";
8
9
  import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderMetadataIssues, installedPackageNames, providerDoctorRulesForInstalledPackages, providerOperationalConfigFiles, providerRequiredEnvExists, providerRequiredTableExists, providerVariantRegistrationSeverity, readProviderListEntries, readProviderPackageMetadataSource, registrationTokenHint, serverProviderFiles, } from "./provider-audit.js";
9
- import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryValueInfo, } from "./registry-edits.js";
10
- import { currentGeneratedPackageScripts, externalVersions, legacyGeneratedPackageScripts, } from "./templates/shared.js";
10
+ import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, codeCharacterIndexes, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryValueInfo, } from "./registry-edits.js";
11
+ import { currentGeneratedPackageScripts, externalVersions, isLegacyGeneratedPackageScript, legacyGeneratedPackageScripts, } from "./templates/shared.js";
11
12
  import { testSupportTemplateFiles } from "./templates/testing.js";
12
13
  import { getCliVersion } from "./version.js";
13
14
  export async function inspectApp(options = {}) {
@@ -33,7 +34,40 @@ export async function inspectApp(options = {}) {
33
34
  fixes: [],
34
35
  };
35
36
  }
37
+ /** Plan every currently available low-risk doctor repair without writing. */
38
+ export async function planDoctorFixes(options = {}) {
39
+ const plan = publicDoctorFixPlan(await buildDoctorFixPlan(options));
40
+ const inspected = await inspectApp(options);
41
+ return {
42
+ ...plan,
43
+ convention: inspected.convention,
44
+ diagnostics: inspected.diagnostics,
45
+ };
46
+ }
47
+ /** Apply a current doctor fix plan after verifying its plan ID. */
48
+ export async function applyDoctorFixPlan(options) {
49
+ const plan = await buildDoctorFixPlan(options);
50
+ if (plan.planId !== options.planId) {
51
+ throw new Error("Doctor fix plan is stale because the available repairs changed. No files were changed; run beignet doctor --fix --dry-run again.");
52
+ }
53
+ return applyPlannedDoctorFixes(plan, options.fixIds);
54
+ }
36
55
  export async function applyDoctorFixes(options = {}) {
56
+ return (await applyDoctorFixesWithResult(options)).fixes;
57
+ }
58
+ export async function applyDoctorFixesWithResult(options = {}) {
59
+ const plan = await buildDoctorFixPlan(options);
60
+ return applyPlannedDoctorFixes(plan);
61
+ }
62
+ export function createDoctorFixInspectionResult(inspected, applied) {
63
+ return {
64
+ ...inspected,
65
+ planId: applied.planId,
66
+ operationIds: applied.operationIds,
67
+ fixes: applied.fixes,
68
+ };
69
+ }
70
+ async function buildDoctorFixPlan(options = {}) {
37
71
  const targetDir = path.resolve(options.cwd ?? process.cwd());
38
72
  await assertDirectory(targetDir);
39
73
  const files = await listFiles(targetDir);
@@ -43,28 +77,41 @@ export async function applyDoctorFixes(options = {}) {
43
77
  const convention = inspectConvention(files, config);
44
78
  const contracts = await readContracts(targetDir, files, config);
45
79
  const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts);
46
- const fixes = [];
47
- const packageFix = await fixPackageScripts(targetDir, files, convention);
80
+ const operations = [];
81
+ const packageFix = await planPackageScripts(targetDir, files, convention);
48
82
  if (packageFix)
49
- fixes.push(packageFix);
50
- const routeGroupFix = await fixUnregisteredRouteGroups(targetDir, files, config);
83
+ operations.push(packageFix);
84
+ const routeGroupFix = await planUnregisteredRouteGroups(targetDir, files, config);
51
85
  if (routeGroupFix)
52
- fixes.push(routeGroupFix);
86
+ operations.push(routeGroupFix);
53
87
  const drift = await workflowRegistrationDrift(targetDir, files, config);
54
- const scheduleFix = await fixUnregisteredSchedules(targetDir, files, drift, config);
55
- if (scheduleFix)
56
- fixes.push(scheduleFix);
57
- const taskFix = await fixUnregisteredTasks(targetDir, files, drift, config);
58
- if (taskFix)
59
- fixes.push(taskFix);
60
- fixes.push(...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)));
61
- const listenerFix = await fixUnregisteredListeners(targetDir, files, drift, config);
88
+ if (config.paths.schedules === config.paths.tasks) {
89
+ const workflowFix = await planCoLocatedScheduleAndTaskRegistries(targetDir, files, drift, config);
90
+ if (workflowFix)
91
+ operations.push(workflowFix);
92
+ }
93
+ else {
94
+ const scheduleFix = await planUnregisteredSchedules(targetDir, files, drift, config);
95
+ if (scheduleFix)
96
+ operations.push(scheduleFix);
97
+ const taskFix = await planUnregisteredTasks(targetDir, files, drift, config);
98
+ if (taskFix)
99
+ operations.push(taskFix);
100
+ }
101
+ const outboxFix = await planUnregisteredOutboxEntries(targetDir, files, drift, config);
102
+ if (outboxFix)
103
+ operations.push(outboxFix);
104
+ const listenerFix = await planUnregisteredListeners(targetDir, files, drift, config);
62
105
  if (listenerFix)
63
- fixes.push(listenerFix);
64
- const openApiFix = await fixDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes);
106
+ operations.push(listenerFix);
107
+ const openApiFix = await planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes);
65
108
  if (openApiFix)
66
- fixes.push(openApiFix);
67
- return fixes;
109
+ operations.push(openApiFix);
110
+ return createDoctorFixPlan({
111
+ targetDir,
112
+ strict: Boolean(options.strict),
113
+ operations,
114
+ });
68
115
  }
69
116
  /**
70
117
  * Format inspected routes as a CLI table.
@@ -88,10 +135,54 @@ export function formatRoutes(result) {
88
135
  ...rows.map((row) => [row.method, row.path, row.contract, row.handler]),
89
136
  ]);
90
137
  }
138
+ /**
139
+ * Format a read-only doctor fix plan with exact patches and apply commands.
140
+ */
141
+ export function formatDoctorFixPlan(plan, options = {}) {
142
+ const paint = createPainter(options.color);
143
+ const lines = [];
144
+ if (plan.operations.length === 0) {
145
+ lines.push(`No applicable Beignet fixes found in ${plan.targetDir}.`);
146
+ }
147
+ else {
148
+ const strictFlag = plan.strict ? " --strict" : "";
149
+ lines.push(`Planned ${plan.operations.length} Beignet fix operation${plan.operations.length === 1 ? "" : "s"} in ${plan.targetDir}:`, "");
150
+ for (const operation of plan.operations) {
151
+ lines.push(`${paint("PLAN", "cyan")} ${operation.id}`, ...operation.fixes.map((fix) => ` ${fix.code} ${fix.file}\n ${fix.message}`), ...operation.changes.flatMap((change) => [
152
+ ` ${change.kind === "create" ? "Create" : "Update"} ${change.file}`,
153
+ change.patch.trimEnd(),
154
+ ]), "");
155
+ }
156
+ lines.push(`Plan ID: ${plan.planId}`, "", "Apply the complete plan:", ` beignet doctor --fix${strictFlag} --plan ${plan.planId}`, "", "Apply selected operations:", ` beignet doctor --fix${strictFlag} --plan ${plan.planId} --only ${plan.operations.map((operation) => operation.id).join(",")}`);
157
+ }
158
+ lines.push("", formatDoctorResult({
159
+ targetDir: plan.targetDir,
160
+ strict: plan.strict,
161
+ diagnostics: plan.diagnostics,
162
+ fixes: [],
163
+ }, options));
164
+ return lines.join("\n");
165
+ }
166
+ /**
167
+ * Format planned fixes as GitHub Actions notice annotations.
168
+ */
169
+ export function formatDoctorFixPlanGithub(plan) {
170
+ return [
171
+ ...plan.operations.map((operation) => formatGithubAnnotation({
172
+ severity: "notice",
173
+ file: operation.changes[0]?.file,
174
+ message: `${operation.id}: ${operation.fixes.map((fix) => fix.message).join(" ")} Plan ${plan.planId}.`,
175
+ })),
176
+ ...formatDoctorDiagnosticsGithub(plan.diagnostics),
177
+ ].join("\n");
178
+ }
91
179
  /**
92
180
  * Format doctor diagnostics and applied fixes for CLI output.
93
181
  */
94
182
  export function formatDoctor(result, options = {}) {
183
+ return formatDoctorResult(result, options);
184
+ }
185
+ function formatDoctorResult(result, options = {}) {
95
186
  const paint = createPainter(options.color);
96
187
  const fixLines = result.fixes.length === 0
97
188
  ? []
@@ -193,13 +284,14 @@ function formatDoctorFooter(result) {
193
284
  * Format doctor diagnostics as GitHub Actions annotations.
194
285
  */
195
286
  export function formatDoctorGithub(result) {
196
- return result.diagnostics
197
- .map((diagnostic) => formatGithubAnnotation({
287
+ return formatDoctorDiagnosticsGithub(result.diagnostics).join("\n");
288
+ }
289
+ function formatDoctorDiagnosticsGithub(diagnostics) {
290
+ return diagnostics.map((diagnostic) => formatGithubAnnotation({
198
291
  severity: githubAnnotationSeverity(diagnostic.severity),
199
292
  file: diagnostic.file,
200
293
  message: `${diagnostic.code}: ${diagnostic.message}`,
201
- }))
202
- .join("\n");
294
+ }));
203
295
  }
204
296
  function githubAnnotationSeverity(severity) {
205
297
  if (severity === "hint")
@@ -498,64 +590,17 @@ function objectArgumentBodies(source, callPattern) {
498
590
  function balancedObjectSource(source, startIndex) {
499
591
  if (source[startIndex] !== "{")
500
592
  return undefined;
501
- let depth = 0;
502
- let quote;
503
- let escaped = false;
504
- for (let index = startIndex; index < source.length; index += 1) {
505
- const char = source[index];
506
- if (quote) {
507
- if (escaped) {
508
- escaped = false;
509
- continue;
510
- }
511
- if (char === "\\") {
512
- escaped = true;
513
- continue;
514
- }
515
- if (char === quote)
516
- quote = undefined;
517
- continue;
518
- }
519
- if (char === '"' || char === "'" || char === "`") {
520
- quote = char;
521
- continue;
522
- }
523
- if (char === "{") {
524
- depth += 1;
525
- continue;
526
- }
527
- if (char === "}") {
528
- depth -= 1;
529
- if (depth === 0)
530
- return source.slice(startIndex, index + 1);
531
- }
532
- }
533
- return undefined;
593
+ const endIndex = matchingDelimiterIndex(source, startIndex, "{", "}");
594
+ return endIndex === -1 ? undefined : source.slice(startIndex, endIndex + 1);
534
595
  }
535
596
  function topLevelObjectKeys(body) {
536
597
  const keys = [];
537
598
  let depth = 0;
538
- let quote;
539
- let escaped = false;
540
- for (let index = 0; index < body.length; index += 1) {
541
- const char = body[index];
542
- if (quote) {
543
- if (escaped) {
544
- escaped = false;
545
- continue;
546
- }
547
- if (char === "\\") {
548
- escaped = true;
549
- continue;
550
- }
551
- if (char === quote)
552
- quote = undefined;
599
+ let skipUntil = 0;
600
+ for (const index of codeCharacterIndexes(body)) {
601
+ if (index < skipUntil)
553
602
  continue;
554
- }
555
- if (char === '"' || char === "'" || char === "`") {
556
- quote = char;
557
- continue;
558
- }
603
+ const char = body[index];
559
604
  if (char === "{" || char === "[" || char === "(") {
560
605
  depth += 1;
561
606
  continue;
@@ -573,7 +618,7 @@ function topLevelObjectKeys(body) {
573
618
  const next = body.slice(index + name.length).match(/^\s*:/);
574
619
  if (next)
575
620
  keys.push(name);
576
- index += name.length - 1;
621
+ skipUntil = index + name.length;
577
622
  }
578
623
  return keys;
579
624
  }
@@ -2307,6 +2352,7 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
2307
2352
  severity: "error",
2308
2353
  code,
2309
2354
  file: centralExists ? centralFile : registry.indexFile,
2355
+ subject: workflowArtifactSubject(registry, member),
2310
2356
  message: centralExists
2311
2357
  ? `${workflowArtifactOrigin(registry, member)}, but it is not registered in ${registrationTarget} in ${centralFile}, so the ${noun} never runs. Import ${registry.registryName} and spread it into the ${kind} list.`
2312
2358
  : `${workflowArtifactOrigin(registry, member)}, but ${centralFile} does not exist, so the ${noun} never runs. Register ${registry.registryName} in ${centralFile} (or run \`${makeCommand}\` to create it).`,
@@ -2321,6 +2367,7 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
2321
2367
  severity: "warning",
2322
2368
  code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
2323
2369
  file: config.paths.outbox,
2370
+ subject: workflowArtifactSubject(registry, member),
2324
2371
  message: `${workflowArtifactOrigin(registry, member)} and listeners react to it, but it is not registered in the events list of defineOutboxRegistry({...}) in ${config.paths.outbox}, so outbox drain cannot deliver it. Import ${registry.registryName} and spread it into the events list.`,
2325
2372
  });
2326
2373
  }
@@ -2331,6 +2378,7 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
2331
2378
  severity: "warning",
2332
2379
  code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
2333
2380
  file: config.paths.outbox,
2381
+ subject: workflowArtifactSubject(registry, member),
2334
2382
  message: `${workflowArtifactOrigin(registry, member)}, but it is not registered in the jobs list of defineOutboxRegistry({...}) in ${config.paths.outbox}, so outbox drain cannot deliver it. Import ${registry.registryName} and spread it into the jobs list.`,
2335
2383
  });
2336
2384
  }
@@ -2350,6 +2398,7 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
2350
2398
  file: drift.listeners.wiringFile ??
2351
2399
  drift.listeners.eventBusFile ??
2352
2400
  registry.indexFile,
2401
+ subject: workflowArtifactSubject(registry, member),
2353
2402
  message: `${workflowArtifactOrigin(registry, member)}, but no registerListeners(...) call references it, so the listener never receives events. Register ${registry.registryName} with registerListeners(...) in ${listenerTarget}.`,
2354
2403
  });
2355
2404
  }
@@ -2418,6 +2467,12 @@ function workflowArtifactOrigin(registry, member) {
2418
2467
  }
2419
2468
  return `${memberFile} declares ${member} (exported through ${registry.registryName} in ${registry.indexFile})`;
2420
2469
  }
2470
+ function workflowArtifactSubject(registry, member) {
2471
+ return {
2472
+ file: registry.memberFiles.get(member) ?? registry.indexFile,
2473
+ exportName: member,
2474
+ };
2475
+ }
2421
2476
  async function workflowRegistrationDrift(targetDir, files, config) {
2422
2477
  const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
2423
2478
  const byKind = (kind) => registries.filter((registry) => registry.kind === kind);
@@ -3416,26 +3471,8 @@ function splitTopLevelArguments(source) {
3416
3471
  const args = [];
3417
3472
  let start = 0;
3418
3473
  let depth = 0;
3419
- let inString;
3420
- let escaped = false;
3421
- for (let index = 0; index < source.length; index++) {
3474
+ for (const index of codeCharacterIndexes(source)) {
3422
3475
  const char = source[index];
3423
- if (inString) {
3424
- if (escaped) {
3425
- escaped = false;
3426
- }
3427
- else if (char === "\\") {
3428
- escaped = true;
3429
- }
3430
- else if (char === inString) {
3431
- inString = undefined;
3432
- }
3433
- continue;
3434
- }
3435
- if (char === '"' || char === "'" || char === "`") {
3436
- inString = char;
3437
- continue;
3438
- }
3439
3476
  if (char === "(" || char === "{" || char === "[") {
3440
3477
  depth += 1;
3441
3478
  continue;
@@ -3724,7 +3761,7 @@ async function inspectPackageScripts(targetDir, files, convention) {
3724
3761
  severity: "warning",
3725
3762
  code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
3726
3763
  file: "package.json",
3727
- message: 'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
3764
+ message: `package.json does not define a test script. Add "test": "${currentGeneratedPackageScripts.test}" or run beignet doctor --fix.`,
3728
3765
  });
3729
3766
  }
3730
3767
  const legacyScripts = hasDirectBunTestImports
@@ -3735,7 +3772,7 @@ async function inspectPackageScripts(targetDir, files, convention) {
3735
3772
  severity: "warning",
3736
3773
  code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
3737
3774
  file: "package.json",
3738
- 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.`,
3775
+ 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.`,
3739
3776
  });
3740
3777
  }
3741
3778
  const needsTsx = hasCurrentGeneratedTsxScript(scripts) &&
@@ -3761,7 +3798,7 @@ async function inspectPackageScripts(targetDir, files, convention) {
3761
3798
  }
3762
3799
  return diagnostics;
3763
3800
  }
3764
- async function fixPackageScripts(targetDir, files, convention) {
3801
+ async function planPackageScripts(targetDir, files, convention) {
3765
3802
  if (!convention.resourceGenerator || !files.includes("package.json")) {
3766
3803
  return undefined;
3767
3804
  }
@@ -3777,8 +3814,8 @@ async function fixPackageScripts(targetDir, files, convention) {
3777
3814
  updatedScripts.push("test");
3778
3815
  }
3779
3816
  if (!hasDirectBunTestImports) {
3780
- for (const [name, legacyCommand] of Object.entries(legacyGeneratedPackageScripts)) {
3781
- if (packageJson.scripts[name] === legacyCommand) {
3817
+ for (const name of Object.keys(legacyGeneratedPackageScripts)) {
3818
+ if (isLegacyGeneratedPackageScript(name, packageJson.scripts[name])) {
3782
3819
  packageJson.scripts[name] = currentGeneratedPackageScripts[name];
3783
3820
  if (!updatedScripts.includes(name))
3784
3821
  updatedScripts.push(name);
@@ -3794,47 +3831,65 @@ async function fixPackageScripts(targetDir, files, convention) {
3794
3831
  packageJson.devDependencies.tsx = externalVersions.tsx;
3795
3832
  }
3796
3833
  const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
3797
- const createdSupportFiles = await createMissingTestSupportFiles(targetDir, files, packageJson.scripts.test === currentGeneratedPackageScripts.test);
3834
+ const createdSupportFiles = packageJson.scripts.test === currentGeneratedPackageScripts.test
3835
+ ? missingTestSupportFiles(files)
3836
+ : [];
3798
3837
  if (next === original && createdSupportFiles.length === 0)
3799
3838
  return undefined;
3800
- if (next !== original)
3801
- await writeFile(filePath, next);
3839
+ let fix;
3802
3840
  if (addedTestScript) {
3803
- return {
3841
+ fix = {
3804
3842
  code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
3805
3843
  file: "package.json",
3806
3844
  message: createdSupportFiles.length > 0
3807
- ? `Added "test": "tsx lib/beignet-test-runner.ts" and ${createdSupportFiles.join(", ")}.`
3808
- : 'Added "test": "tsx lib/beignet-test-runner.ts".',
3845
+ ? `Added "test": "${currentGeneratedPackageScripts.test}" and ${createdSupportFiles.join(", ")}.`
3846
+ : `Added "test": "${currentGeneratedPackageScripts.test}".`,
3809
3847
  };
3810
3848
  }
3811
- if (updatedScripts.length > 0) {
3812
- return {
3849
+ else if (updatedScripts.length > 0) {
3850
+ fix = {
3813
3851
  code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
3814
3852
  file: "package.json",
3815
3853
  message: packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx),
3816
3854
  };
3817
3855
  }
3818
- if (createdSupportFiles.length > 0) {
3819
- return {
3856
+ else if (createdSupportFiles.length > 0) {
3857
+ fix = {
3820
3858
  code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
3821
3859
  file: "package.json",
3822
3860
  message: `Restored ${createdSupportFiles.join(", ")}.`,
3823
3861
  };
3824
3862
  }
3825
- if (addedTsx) {
3826
- return {
3863
+ else if (addedTsx) {
3864
+ fix = {
3827
3865
  code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
3828
3866
  file: "package.json",
3829
3867
  message: 'Added "tsx" to devDependencies.',
3830
3868
  };
3831
3869
  }
3832
- return undefined;
3870
+ if (!fix)
3871
+ return undefined;
3872
+ return {
3873
+ id: "package.repair-generated-support",
3874
+ fixes: [fix],
3875
+ changes: [
3876
+ ...(next === original
3877
+ ? []
3878
+ : [
3879
+ planDoctorFixFileChange({
3880
+ file: "package.json",
3881
+ before: original,
3882
+ after: next,
3883
+ }),
3884
+ ]),
3885
+ ...testSupportTemplateFiles
3886
+ .filter((file) => createdSupportFiles.includes(file.path))
3887
+ .map((file) => planDoctorFixFileChange({ file: file.path, after: file.content })),
3888
+ ],
3889
+ };
3833
3890
  }
3834
3891
  function legacyGeneratedScriptNames(scripts) {
3835
- return Object.entries(legacyGeneratedPackageScripts)
3836
- .filter(([name, command]) => scripts[name] === command)
3837
- .map(([name]) => name);
3892
+ return Object.keys(legacyGeneratedPackageScripts).filter((name) => isLegacyGeneratedPackageScript(name, scripts[name]));
3838
3893
  }
3839
3894
  async function appUsesDirectBunTestImports(targetDir, files) {
3840
3895
  for (const file of files) {
@@ -3849,27 +3904,14 @@ async function appUsesDirectBunTestImports(targetDir, files) {
3849
3904
  return false;
3850
3905
  }
3851
3906
  function hasCurrentGeneratedTsxScript(scripts) {
3852
- return Object.values(currentGeneratedPackageScripts).some((command) => command.startsWith("tsx ") && Object.values(scripts).includes(command));
3907
+ return Object.values(currentGeneratedPackageScripts).some((command) => (command.startsWith("tsx ") || command.includes("--import tsx")) &&
3908
+ Object.values(scripts).includes(command));
3853
3909
  }
3854
3910
  function missingTestSupportFiles(files) {
3855
3911
  return testSupportTemplateFiles
3856
3912
  .map((file) => file.path)
3857
3913
  .filter((file) => !files.includes(file));
3858
3914
  }
3859
- async function createMissingTestSupportFiles(targetDir, files, shouldCreate) {
3860
- if (!shouldCreate)
3861
- return [];
3862
- const createdSupportFiles = [];
3863
- for (const file of testSupportTemplateFiles) {
3864
- if (files.includes(file.path))
3865
- continue;
3866
- const destination = path.join(targetDir, file.path);
3867
- await mkdir(path.dirname(destination), { recursive: true });
3868
- await writeFile(destination, file.content);
3869
- createdSupportFiles.push(file.path);
3870
- }
3871
- return createdSupportFiles;
3872
- }
3873
3915
  function packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx) {
3874
3916
  const parts = [
3875
3917
  `Updated generated package scripts: ${updatedScripts.join(", ")}.`,
@@ -3910,6 +3952,8 @@ function dedupeDiagnostics(diagnostics) {
3910
3952
  diagnostic.code,
3911
3953
  diagnostic.file ?? "",
3912
3954
  diagnostic.contract ?? "",
3955
+ diagnostic.subject?.file ?? "",
3956
+ diagnostic.subject?.exportName ?? "",
3913
3957
  diagnostic.message,
3914
3958
  ].join(":");
3915
3959
  if (seen.has(key))
@@ -4024,6 +4068,10 @@ async function inspectFeatureRouteRegistration(targetDir, files, config, convent
4024
4068
  code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
4025
4069
  file: config.paths.server,
4026
4070
  contract: routeGroupContract.exportName,
4071
+ subject: {
4072
+ file: routeGroup.file,
4073
+ exportName: routeGroup.name,
4074
+ },
4027
4075
  message: contract
4028
4076
  ? `${routeGroup.file} declares ${routeGroup.name} for ${contract.exportName} (${contract.method} ${contract.path}), but ${routeGroup.name} is not registered in defineRoutes([...]) in ${config.paths.server}. Import ${routeGroup.name} and add it to the central route list.`
4029
4077
  : `${routeGroup.file} declares ${routeGroup.name} for ${routeGroupContract.exportName}, but ${routeGroup.name} is not registered in defineRoutes([...]) in ${config.paths.server}. Import ${routeGroup.name} and add it to the central route list.`,
@@ -4108,39 +4156,8 @@ function routeGroupRoutesArrayBody(source, callStart) {
4108
4156
  function balancedArraySource(source, startIndex) {
4109
4157
  if (source[startIndex] !== "[")
4110
4158
  return undefined;
4111
- let depth = 0;
4112
- let quote;
4113
- let escaped = false;
4114
- for (let index = startIndex; index < source.length; index += 1) {
4115
- const char = source[index];
4116
- if (quote) {
4117
- if (escaped) {
4118
- escaped = false;
4119
- continue;
4120
- }
4121
- if (char === "\\") {
4122
- escaped = true;
4123
- continue;
4124
- }
4125
- if (char === quote)
4126
- quote = undefined;
4127
- continue;
4128
- }
4129
- if (char === '"' || char === "'" || char === "`") {
4130
- quote = char;
4131
- continue;
4132
- }
4133
- if (char === "[") {
4134
- depth += 1;
4135
- continue;
4136
- }
4137
- if (char === "]") {
4138
- depth -= 1;
4139
- if (depth === 0)
4140
- return source.slice(startIndex, index + 1);
4141
- }
4142
- }
4143
- return undefined;
4159
+ const endIndex = matchingDelimiterIndex(source, startIndex, "[", "]");
4160
+ return endIndex === -1 ? undefined : source.slice(startIndex, endIndex + 1);
4144
4161
  }
4145
4162
  function routeGroupContractRef(expression, imports) {
4146
4163
  const namedMatch = /^([A-Za-z_$][\w$]*)$/.exec(expression);
@@ -4161,15 +4178,10 @@ function routeGroupContractRef(expression, imports) {
4161
4178
  return undefined;
4162
4179
  }
4163
4180
  function registeredRouteGroups(source) {
4164
- const defineRoutesMatch = /defineRoutes(?:<[^>]+>)?\(\[/.exec(source);
4165
- if (!defineRoutesMatch)
4166
- return new Set();
4167
- const defineRoutesSource = source.slice(defineRoutesMatch.index);
4168
- const defineRoutesEnd = defineRoutesSource.indexOf("]),");
4169
- const searchSource = defineRoutesEnd === -1
4170
- ? defineRoutesSource
4171
- : defineRoutesSource.slice(0, defineRoutesEnd);
4172
- return new Set([...searchSource.matchAll(/\b([A-Za-z_$][\w$]*)\b/g)].map((match) => match[1]));
4181
+ const routes = firstDefineRoutesArgInfo(source);
4182
+ return routes?.text.startsWith("[")
4183
+ ? identifiersFromArrayExpression(routes.text)
4184
+ : new Set();
4173
4185
  }
4174
4186
  async function registeredRouteGroupsForServer(targetDir, files, config, serverSource) {
4175
4187
  const registeredGroups = registeredRouteGroups(serverSource);
@@ -4207,7 +4219,7 @@ function findRouteGroupContract(contracts, routeGroupContract) {
4207
4219
  contract.file ===
4208
4220
  routeGroupContract.file.replace(/\.ts$/, "/index.ts")));
4209
4221
  }
4210
- async function fixDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes) {
4222
+ async function planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes) {
4211
4223
  const routePath = path.join(targetDir, config.paths.openapiRoute);
4212
4224
  let source;
4213
4225
  try {
@@ -4231,16 +4243,27 @@ async function fixDirectOpenApiArrayDrift(targetDir, files, config, convention,
4231
4243
  const next = `${source.slice(0, firstArg.start)}${nextArray}${source.slice(firstArg.end)}`;
4232
4244
  if (next === source)
4233
4245
  return undefined;
4234
- await writeFile(routePath, next);
4235
4246
  return {
4236
- code: "BEIGNET_OPENAPI_MISSING",
4237
- file: config.paths.openapiRoute,
4238
- message: `Added ${missingContracts
4239
- .map((contract) => contract.exportName)
4240
- .join(", ")} to the direct createOpenAPIHandler contract array.`,
4247
+ id: "openapi.register-missing",
4248
+ fixes: [
4249
+ {
4250
+ code: "BEIGNET_OPENAPI_MISSING",
4251
+ file: config.paths.openapiRoute,
4252
+ message: `Added ${missingContracts
4253
+ .map((contract) => contract.exportName)
4254
+ .join(", ")} to the direct createOpenAPIHandler contract array.`,
4255
+ },
4256
+ ],
4257
+ changes: [
4258
+ planDoctorFixFileChange({
4259
+ file: config.paths.openapiRoute,
4260
+ before: source,
4261
+ after: next,
4262
+ }),
4263
+ ],
4241
4264
  };
4242
4265
  }
4243
- async function fixUnregisteredRouteGroups(targetDir, files, config) {
4266
+ async function planUnregisteredRouteGroups(targetDir, files, config) {
4244
4267
  if (!files.includes(config.paths.server))
4245
4268
  return undefined;
4246
4269
  const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
@@ -4283,19 +4306,30 @@ async function fixUnregisteredRouteGroups(targetDir, files, config) {
4283
4306
  next = `${next.slice(0, nextFirstArg.start)}${nextArray}${next.slice(nextFirstArg.end)}`;
4284
4307
  if (next === original)
4285
4308
  return undefined;
4286
- await writeFile(targetPath, next);
4287
4309
  return {
4288
- code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
4289
- file: targetFile,
4290
- message: `Registered ${groupsToAppend
4291
- .map((routeGroup) => routeGroup.name)
4292
- .join(", ")} in the central route list.`,
4310
+ id: "routes.register-missing",
4311
+ fixes: [
4312
+ {
4313
+ code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
4314
+ file: targetFile,
4315
+ message: `Registered ${groupsToAppend
4316
+ .map((routeGroup) => routeGroup.name)
4317
+ .join(", ")} in the central route list.`,
4318
+ },
4319
+ ],
4320
+ changes: [
4321
+ planDoctorFixFileChange({
4322
+ file: targetFile,
4323
+ before: original,
4324
+ after: next,
4325
+ }),
4326
+ ],
4293
4327
  };
4294
4328
  }
4295
- async function fixUnregisteredSchedules(targetDir, files, drift, config) {
4329
+ async function planUnregisteredSchedules(targetDir, files, drift, config) {
4296
4330
  if (!drift.schedules.centralExists)
4297
4331
  return undefined;
4298
- return applyWorkflowRegistryFix({
4332
+ return planWorkflowRegistryOperation("schedules.register-missing", {
4299
4333
  targetDir,
4300
4334
  files,
4301
4335
  centralFile: config.paths.schedules,
@@ -4306,10 +4340,10 @@ async function fixUnregisteredSchedules(targetDir, files, drift, config) {
4306
4340
  append: (source, entry, importLine) => appendToNamedArray(source, "schedules", entry, importLine),
4307
4341
  });
4308
4342
  }
4309
- async function fixUnregisteredTasks(targetDir, files, drift, config) {
4343
+ async function planUnregisteredTasks(targetDir, files, drift, config) {
4310
4344
  if (!drift.tasks.centralExists)
4311
4345
  return undefined;
4312
- return applyWorkflowRegistryFix({
4346
+ return planWorkflowRegistryOperation("tasks.register-missing", {
4313
4347
  targetDir,
4314
4348
  files,
4315
4349
  centralFile: config.paths.tasks,
@@ -4320,6 +4354,70 @@ async function fixUnregisteredTasks(targetDir, files, drift, config) {
4320
4354
  append: (source, entry, importLine) => appendToNamedArray(source, "tasks", entry, importLine),
4321
4355
  });
4322
4356
  }
4357
+ async function planCoLocatedScheduleAndTaskRegistries(targetDir, files, drift, config) {
4358
+ if (!drift.schedules.centralExists || !drift.tasks.centralExists) {
4359
+ return undefined;
4360
+ }
4361
+ const centralFile = config.paths.schedules;
4362
+ let original;
4363
+ try {
4364
+ original = await readFile(path.join(targetDir, centralFile), "utf8");
4365
+ }
4366
+ catch {
4367
+ return undefined;
4368
+ }
4369
+ let next = original;
4370
+ const fixes = [];
4371
+ const repairedKinds = [];
4372
+ const scheduleEdit = planWorkflowRegistryEdit({
4373
+ files,
4374
+ centralFile,
4375
+ source: next,
4376
+ unregistered: drift.schedules.unregistered,
4377
+ code: "BEIGNET_SCHEDULE_UNREGISTERED",
4378
+ listName: "the schedules array",
4379
+ importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
4380
+ append: (source, entry, importLine) => appendToNamedArray(source, "schedules", entry, importLine),
4381
+ });
4382
+ if (scheduleEdit) {
4383
+ next = scheduleEdit.after;
4384
+ fixes.push(scheduleEdit.fix);
4385
+ repairedKinds.push("schedules");
4386
+ }
4387
+ const taskEdit = planWorkflowRegistryEdit({
4388
+ files,
4389
+ centralFile,
4390
+ source: next,
4391
+ unregistered: drift.tasks.unregistered,
4392
+ code: "BEIGNET_TASK_UNREGISTERED",
4393
+ listName: "defineTasks([...])",
4394
+ importSpecifier: (indexFile) => relativeModule(centralFile, indexFile),
4395
+ append: (source, entry, importLine) => appendToNamedArray(source, "tasks", entry, importLine),
4396
+ });
4397
+ if (taskEdit) {
4398
+ next = taskEdit.after;
4399
+ fixes.push(taskEdit.fix);
4400
+ repairedKinds.push("tasks");
4401
+ }
4402
+ if (fixes.length === 0 || next === original)
4403
+ return undefined;
4404
+ const id = repairedKinds.length === 2
4405
+ ? "workflows.register-missing"
4406
+ : repairedKinds[0] === "schedules"
4407
+ ? "schedules.register-missing"
4408
+ : "tasks.register-missing";
4409
+ return {
4410
+ id,
4411
+ fixes,
4412
+ changes: [
4413
+ planDoctorFixFileChange({
4414
+ file: centralFile,
4415
+ before: original,
4416
+ after: next,
4417
+ }),
4418
+ ],
4419
+ };
4420
+ }
4323
4421
  /**
4324
4422
  * Wire fully unregistered feature listener registries into the central
4325
4423
  * listener registry and ensure the providers file registers that central
@@ -4331,7 +4429,7 @@ async function fixUnregisteredTasks(targetDir, files, drift, config) {
4331
4429
  * register against, or when the registry name is already imported from another
4332
4430
  * file. The diagnostic stays in every bail case.
4333
4431
  */
4334
- async function fixUnregisteredListeners(targetDir, files, drift, config) {
4432
+ async function planUnregisteredListeners(targetDir, files, drift, config) {
4335
4433
  const candidates = drift.listeners.unregistered.filter((entry) => entry.fullyUnregistered);
4336
4434
  if (candidates.length === 0)
4337
4435
  return undefined;
@@ -4407,60 +4505,93 @@ async function fixUnregisteredListeners(targetDir, files, drift, config) {
4407
4505
  nextProviders === originalProviders) {
4408
4506
  return undefined;
4409
4507
  }
4410
- await mkdir(path.dirname(listenersPath), { recursive: true });
4411
- await writeFile(listenersPath, nextListeners);
4412
- if (nextProviders !== originalProviders) {
4413
- await writeFile(providersPath, nextProviders);
4414
- }
4415
4508
  return {
4416
- code: "BEIGNET_LISTENER_UNREGISTERED",
4417
- file: listenersFile,
4418
- message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
4509
+ id: "listeners.register-missing",
4510
+ fixes: [
4511
+ {
4512
+ code: "BEIGNET_LISTENER_UNREGISTERED",
4513
+ file: listenersFile,
4514
+ message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
4515
+ },
4516
+ ],
4517
+ changes: [
4518
+ ...(nextListeners === originalListeners
4519
+ ? []
4520
+ : [
4521
+ planDoctorFixFileChange({
4522
+ file: listenersFile,
4523
+ before: originalListeners,
4524
+ after: nextListeners,
4525
+ }),
4526
+ ]),
4527
+ ...(nextProviders === originalProviders
4528
+ ? []
4529
+ : [
4530
+ planDoctorFixFileChange({
4531
+ file: providersFile,
4532
+ before: originalProviders,
4533
+ after: nextProviders,
4534
+ }),
4535
+ ]),
4536
+ ],
4419
4537
  };
4420
4538
  }
4421
- async function fixUnregisteredOutboxEntries(targetDir, files, drift, config) {
4539
+ async function planUnregisteredOutboxEntries(targetDir, files, drift, config) {
4422
4540
  if (!drift.outbox.exists)
4423
- return [];
4541
+ return undefined;
4542
+ const outboxPath = path.join(targetDir, config.paths.outbox);
4543
+ let original;
4544
+ try {
4545
+ original = await readFile(outboxPath, "utf8");
4546
+ }
4547
+ catch {
4548
+ return undefined;
4549
+ }
4550
+ let next = original;
4424
4551
  const fixes = [];
4425
- const eventFix = await applyWorkflowRegistryFix({
4426
- targetDir,
4552
+ const eventFix = planWorkflowRegistryEdit({
4427
4553
  files,
4428
4554
  centralFile: config.paths.outbox,
4555
+ source: next,
4429
4556
  unregistered: drift.outbox.events,
4430
4557
  code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
4431
4558
  listName: "the outbox events list",
4432
4559
  importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
4433
4560
  append: (source, entry, importLine) => appendToOutboxRegistryArray(source, "events", entry, importLine),
4434
4561
  });
4435
- if (eventFix)
4436
- fixes.push(eventFix);
4437
- const jobFix = await applyWorkflowRegistryFix({
4438
- targetDir,
4562
+ if (eventFix) {
4563
+ next = eventFix.after;
4564
+ fixes.push(eventFix.fix);
4565
+ }
4566
+ const jobFix = planWorkflowRegistryEdit({
4439
4567
  files,
4440
4568
  centralFile: config.paths.outbox,
4569
+ source: next,
4441
4570
  unregistered: drift.outbox.jobs,
4442
4571
  code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
4443
4572
  listName: "the outbox jobs list",
4444
4573
  importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
4445
4574
  append: (source, entry, importLine) => appendToOutboxRegistryArray(source, "jobs", entry, importLine),
4446
4575
  });
4447
- if (jobFix)
4448
- fixes.push(jobFix);
4449
- return fixes;
4450
- }
4451
- /**
4452
- * Append `...<feature><Kind>` registry spreads to an existing central
4453
- * registry file.
4454
- *
4455
- * Bails without writing when an import for the registry name resolves to a
4456
- * different file, when the registry anchor is missing or unparseable, or when
4457
- * some members are already individually registered (appending the registry
4458
- * spread would run those members twice). The diagnostic stays in that case.
4459
- */
4460
- async function applyWorkflowRegistryFix(options) {
4461
- const candidates = options.unregistered.filter((entry) => entry.fullyUnregistered);
4462
- if (candidates.length === 0)
4576
+ if (jobFix) {
4577
+ next = jobFix.after;
4578
+ fixes.push(jobFix.fix);
4579
+ }
4580
+ if (fixes.length === 0 || next === original)
4463
4581
  return undefined;
4582
+ return {
4583
+ id: "outbox.register-missing",
4584
+ fixes,
4585
+ changes: [
4586
+ planDoctorFixFileChange({
4587
+ file: config.paths.outbox,
4588
+ before: original,
4589
+ after: next,
4590
+ }),
4591
+ ],
4592
+ };
4593
+ }
4594
+ async function planWorkflowRegistryOperation(id, options) {
4464
4595
  const centralPath = path.join(options.targetDir, options.centralFile);
4465
4596
  let original;
4466
4597
  try {
@@ -4469,7 +4600,26 @@ async function applyWorkflowRegistryFix(options) {
4469
4600
  catch {
4470
4601
  return undefined;
4471
4602
  }
4472
- let next = original;
4603
+ const edit = planWorkflowRegistryEdit({ ...options, source: original });
4604
+ if (!edit)
4605
+ return undefined;
4606
+ return {
4607
+ id,
4608
+ fixes: [edit.fix],
4609
+ changes: [
4610
+ planDoctorFixFileChange({
4611
+ file: options.centralFile,
4612
+ before: original,
4613
+ after: edit.after,
4614
+ }),
4615
+ ],
4616
+ };
4617
+ }
4618
+ function planWorkflowRegistryEdit(options) {
4619
+ const candidates = options.unregistered.filter((entry) => entry.fullyUnregistered);
4620
+ if (candidates.length === 0)
4621
+ return undefined;
4622
+ let next = options.source;
4473
4623
  const registeredNames = [];
4474
4624
  for (const { registry } of candidates) {
4475
4625
  const imported = parseNamedImportSources(next).get(registry.registryName);
@@ -4490,13 +4640,15 @@ async function applyWorkflowRegistryFix(options) {
4490
4640
  registeredNames.push(registry.registryName);
4491
4641
  }
4492
4642
  }
4493
- if (next === original || registeredNames.length === 0)
4643
+ if (next === options.source || registeredNames.length === 0)
4494
4644
  return undefined;
4495
- await writeFile(centralPath, next);
4496
4645
  return {
4497
- code: options.code,
4498
- file: options.centralFile,
4499
- message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
4646
+ after: next,
4647
+ fix: {
4648
+ code: options.code,
4649
+ file: options.centralFile,
4650
+ message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
4651
+ },
4500
4652
  };
4501
4653
  }
4502
4654
  function routeRegistryFileFromServerSource(source, serverFile, files) {
@@ -4588,26 +4740,8 @@ function firstCreateOpenAPIHandlerArgInfo(source) {
4588
4740
  }
4589
4741
  function firstCallArgInfo(source, argStart) {
4590
4742
  let depth = 0;
4591
- let inString;
4592
- let escaped = false;
4593
- for (let index = argStart; index < source.length; index++) {
4743
+ for (const index of codeCharacterIndexes(source, argStart)) {
4594
4744
  const char = source[index];
4595
- if (inString) {
4596
- if (escaped) {
4597
- escaped = false;
4598
- }
4599
- else if (char === "\\") {
4600
- escaped = true;
4601
- }
4602
- else if (char === inString) {
4603
- inString = undefined;
4604
- }
4605
- continue;
4606
- }
4607
- if (char === '"' || char === "'" || char === "`") {
4608
- inString = char;
4609
- continue;
4610
- }
4611
4745
  if (char === "[" || char === "(" || char === "{") {
4612
4746
  depth++;
4613
4747
  continue;