@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.
- package/CHANGELOG.md +23 -0
- package/README.md +96 -25
- package/dist/analysis/source-index.d.ts +1 -1
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +4 -2
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +3 -2
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +51 -39
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +65 -4
- package/dist/app-map.js.map +1 -1
- package/dist/choices.d.ts +18 -0
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +35 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +18 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +20 -7
- package/dist/db.js.map +1 -1
- package/dist/doctor-fixes.d.ts +64 -0
- package/dist/doctor-fixes.d.ts.map +1 -0
- package/dist/doctor-fixes.js +142 -0
- package/dist/doctor-fixes.js.map +1 -0
- package/dist/explain.d.ts +3 -1
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +136 -42
- package/dist/explain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -25
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +38 -9
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +385 -251
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +6 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +3 -2
- package/dist/lib.js.map +1 -1
- package/dist/make/inbox.js +4 -4
- package/dist/make/payments.js +6 -6
- package/dist/make/shared.js +3 -3
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +12 -14
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +33 -23
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +121 -13
- package/dist/mcp.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -1
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +61 -43
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +34 -11
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +5 -5
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/mysql.js +3 -3
- package/dist/templates/db/postgres.js +3 -3
- package/dist/templates/db/sqlite.js +3 -3
- package/dist/templates/server.js +4 -4
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +2 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +7 -4
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/todos.js +2 -2
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +43 -10
- package/src/analysis/source-index.ts +12 -3
- package/src/analysis/workspace.ts +94 -52
- package/src/app-map.ts +82 -4
- package/src/choices.ts +57 -0
- package/src/db.ts +45 -15
- package/src/doctor-fixes.ts +252 -0
- package/src/explain.ts +151 -43
- package/src/index.ts +130 -35
- package/src/inspect.ts +537 -295
- package/src/lib.ts +28 -1
- package/src/make/inbox.ts +4 -4
- package/src/make/payments.ts +6 -6
- package/src/make/shared.ts +3 -3
- package/src/make/tenancy.ts +12 -14
- package/src/make.ts +36 -24
- package/src/mcp.ts +187 -13
- package/src/registry-edits.ts +81 -39
- package/src/templates/agents.ts +34 -11
- package/src/templates/base.ts +5 -4
- package/src/templates/db/mysql.ts +3 -3
- package/src/templates/db/postgres.ts +3 -3
- package/src/templates/db/sqlite.ts +3 -3
- package/src/templates/server.ts +4 -4
- package/src/templates/shared.ts +14 -6
- package/src/templates/todos.ts +2 -2
package/src/inspect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
@@ -47,6 +59,7 @@ import {
|
|
|
47
59
|
appendToNamedArray,
|
|
48
60
|
appendToOutboxRegistryArray,
|
|
49
61
|
arrayInitializerInfo,
|
|
62
|
+
codeCharacterIndexes,
|
|
50
63
|
identifiersFromArrayExpression,
|
|
51
64
|
insertAfterImports,
|
|
52
65
|
matchingDelimiterIndex,
|
|
@@ -55,6 +68,7 @@ import {
|
|
|
55
68
|
import {
|
|
56
69
|
currentGeneratedPackageScripts,
|
|
57
70
|
externalVersions,
|
|
71
|
+
isLegacyGeneratedPackageScript,
|
|
58
72
|
legacyGeneratedPackageScripts,
|
|
59
73
|
} from "./templates/shared.js";
|
|
60
74
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
@@ -117,16 +131,14 @@ export type InspectDiagnostic = {
|
|
|
117
131
|
message: string;
|
|
118
132
|
file?: string;
|
|
119
133
|
contract?: string;
|
|
134
|
+
/** Stable declaration identity for diagnostics about a named app concept. */
|
|
135
|
+
subject?: {
|
|
136
|
+
file: string;
|
|
137
|
+
exportName: string;
|
|
138
|
+
};
|
|
120
139
|
};
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
* Automatic doctor fix that was applied.
|
|
124
|
-
*/
|
|
125
|
-
export type InspectFix = {
|
|
126
|
-
code: string;
|
|
127
|
-
message: string;
|
|
128
|
-
file: string;
|
|
129
|
-
};
|
|
141
|
+
export type { InspectFix } from "./doctor-fixes.js";
|
|
130
142
|
|
|
131
143
|
/**
|
|
132
144
|
* Detected app convention and missing convention files.
|
|
@@ -157,6 +169,18 @@ export type InspectAppResult = {
|
|
|
157
169
|
fixes: InspectFix[];
|
|
158
170
|
};
|
|
159
171
|
|
|
172
|
+
/** Read-only doctor fix plan paired with the diagnostics it was derived from. */
|
|
173
|
+
export type DoctorFixPlanResult = DoctorFixPlan & {
|
|
174
|
+
convention: InspectConvention;
|
|
175
|
+
diagnostics: InspectDiagnostic[];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Post-apply inspection paired with the guarded plan metadata that ran. */
|
|
179
|
+
export type DoctorFixInspectionResult = InspectAppResult & {
|
|
180
|
+
planId: string;
|
|
181
|
+
operationIds: DoctorFixOperationId[];
|
|
182
|
+
};
|
|
183
|
+
|
|
160
184
|
type RouteExport = {
|
|
161
185
|
method: HttpMethod;
|
|
162
186
|
handlerFile: string;
|
|
@@ -235,9 +259,66 @@ export async function inspectApp(
|
|
|
235
259
|
};
|
|
236
260
|
}
|
|
237
261
|
|
|
262
|
+
/** Options for guarded all-or-selected doctor fix application. */
|
|
263
|
+
export type ApplyDoctorFixPlanOptions = InspectAppOptions & {
|
|
264
|
+
planId: string;
|
|
265
|
+
fixIds?: readonly DoctorFixOperationId[];
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Plan every currently available low-risk doctor repair without writing. */
|
|
269
|
+
export async function planDoctorFixes(
|
|
270
|
+
options: InspectAppOptions = {},
|
|
271
|
+
): Promise<DoctorFixPlanResult> {
|
|
272
|
+
const plan = publicDoctorFixPlan(await buildDoctorFixPlan(options));
|
|
273
|
+
const inspected = await inspectApp(options);
|
|
274
|
+
return {
|
|
275
|
+
...plan,
|
|
276
|
+
convention: inspected.convention,
|
|
277
|
+
diagnostics: inspected.diagnostics,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Apply a current doctor fix plan after verifying its plan ID. */
|
|
282
|
+
export async function applyDoctorFixPlan(
|
|
283
|
+
options: ApplyDoctorFixPlanOptions,
|
|
284
|
+
): Promise<DoctorFixApplyResult> {
|
|
285
|
+
const plan = await buildDoctorFixPlan(options);
|
|
286
|
+
if (plan.planId !== options.planId) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"Doctor fix plan is stale because the available repairs changed. No files were changed; run beignet doctor --fix --dry-run again.",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return applyPlannedDoctorFixes(plan, options.fixIds);
|
|
292
|
+
}
|
|
293
|
+
|
|
238
294
|
export async function applyDoctorFixes(
|
|
239
295
|
options: InspectAppOptions = {},
|
|
240
296
|
): Promise<InspectFix[]> {
|
|
297
|
+
return (await applyDoctorFixesWithResult(options)).fixes;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function applyDoctorFixesWithResult(
|
|
301
|
+
options: InspectAppOptions = {},
|
|
302
|
+
): Promise<DoctorFixApplyResult> {
|
|
303
|
+
const plan = await buildDoctorFixPlan(options);
|
|
304
|
+
return applyPlannedDoctorFixes(plan);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createDoctorFixInspectionResult(
|
|
308
|
+
inspected: InspectAppResult,
|
|
309
|
+
applied: DoctorFixApplyResult,
|
|
310
|
+
): DoctorFixInspectionResult {
|
|
311
|
+
return {
|
|
312
|
+
...inspected,
|
|
313
|
+
planId: applied.planId,
|
|
314
|
+
operationIds: applied.operationIds,
|
|
315
|
+
fixes: applied.fixes,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function buildDoctorFixPlan(
|
|
320
|
+
options: InspectAppOptions = {},
|
|
321
|
+
): Promise<PlannedDoctorFixPlan> {
|
|
241
322
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
242
323
|
await assertDirectory(targetDir);
|
|
243
324
|
|
|
@@ -253,44 +334,63 @@ export async function applyDoctorFixes(
|
|
|
253
334
|
config,
|
|
254
335
|
contracts,
|
|
255
336
|
);
|
|
256
|
-
const
|
|
337
|
+
const operations: PlannedDoctorFixOperation[] = [];
|
|
257
338
|
|
|
258
|
-
const packageFix = await
|
|
259
|
-
if (packageFix)
|
|
339
|
+
const packageFix = await planPackageScripts(targetDir, files, convention);
|
|
340
|
+
if (packageFix) operations.push(packageFix);
|
|
260
341
|
|
|
261
|
-
const routeGroupFix = await
|
|
342
|
+
const routeGroupFix = await planUnregisteredRouteGroups(
|
|
262
343
|
targetDir,
|
|
263
344
|
files,
|
|
264
345
|
config,
|
|
265
346
|
);
|
|
266
|
-
if (routeGroupFix)
|
|
347
|
+
if (routeGroupFix) operations.push(routeGroupFix);
|
|
267
348
|
|
|
268
349
|
const drift = await workflowRegistrationDrift(targetDir, files, config);
|
|
269
350
|
|
|
270
|
-
|
|
351
|
+
if (config.paths.schedules === config.paths.tasks) {
|
|
352
|
+
const workflowFix = await planCoLocatedScheduleAndTaskRegistries(
|
|
353
|
+
targetDir,
|
|
354
|
+
files,
|
|
355
|
+
drift,
|
|
356
|
+
config,
|
|
357
|
+
);
|
|
358
|
+
if (workflowFix) operations.push(workflowFix);
|
|
359
|
+
} else {
|
|
360
|
+
const scheduleFix = await planUnregisteredSchedules(
|
|
361
|
+
targetDir,
|
|
362
|
+
files,
|
|
363
|
+
drift,
|
|
364
|
+
config,
|
|
365
|
+
);
|
|
366
|
+
if (scheduleFix) operations.push(scheduleFix);
|
|
367
|
+
|
|
368
|
+
const taskFix = await planUnregisteredTasks(
|
|
369
|
+
targetDir,
|
|
370
|
+
files,
|
|
371
|
+
drift,
|
|
372
|
+
config,
|
|
373
|
+
);
|
|
374
|
+
if (taskFix) operations.push(taskFix);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const outboxFix = await planUnregisteredOutboxEntries(
|
|
271
378
|
targetDir,
|
|
272
379
|
files,
|
|
273
380
|
drift,
|
|
274
381
|
config,
|
|
275
382
|
);
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
const taskFix = await fixUnregisteredTasks(targetDir, files, drift, config);
|
|
279
|
-
if (taskFix) fixes.push(taskFix);
|
|
383
|
+
if (outboxFix) operations.push(outboxFix);
|
|
280
384
|
|
|
281
|
-
|
|
282
|
-
...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const listenerFix = await fixUnregisteredListeners(
|
|
385
|
+
const listenerFix = await planUnregisteredListeners(
|
|
286
386
|
targetDir,
|
|
287
387
|
files,
|
|
288
388
|
drift,
|
|
289
389
|
config,
|
|
290
390
|
);
|
|
291
|
-
if (listenerFix)
|
|
391
|
+
if (listenerFix) operations.push(listenerFix);
|
|
292
392
|
|
|
293
|
-
const openApiFix = await
|
|
393
|
+
const openApiFix = await planDirectOpenApiArrayDrift(
|
|
294
394
|
targetDir,
|
|
295
395
|
files,
|
|
296
396
|
config,
|
|
@@ -298,9 +398,13 @@ export async function applyDoctorFixes(
|
|
|
298
398
|
contracts,
|
|
299
399
|
matchedRoutes,
|
|
300
400
|
);
|
|
301
|
-
if (openApiFix)
|
|
401
|
+
if (openApiFix) operations.push(openApiFix);
|
|
302
402
|
|
|
303
|
-
return
|
|
403
|
+
return createDoctorFixPlan({
|
|
404
|
+
targetDir,
|
|
405
|
+
strict: Boolean(options.strict),
|
|
406
|
+
operations,
|
|
407
|
+
});
|
|
304
408
|
}
|
|
305
409
|
|
|
306
410
|
/**
|
|
@@ -329,12 +433,97 @@ export function formatRoutes(result: InspectAppResult): string {
|
|
|
329
433
|
]);
|
|
330
434
|
}
|
|
331
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Format a read-only doctor fix plan with exact patches and apply commands.
|
|
438
|
+
*/
|
|
439
|
+
export function formatDoctorFixPlan(
|
|
440
|
+
plan: DoctorFixPlanResult,
|
|
441
|
+
options: { color?: boolean } = {},
|
|
442
|
+
): string {
|
|
443
|
+
const paint = createPainter(options.color);
|
|
444
|
+
const lines: string[] = [];
|
|
445
|
+
if (plan.operations.length === 0) {
|
|
446
|
+
lines.push(`No applicable Beignet fixes found in ${plan.targetDir}.`);
|
|
447
|
+
} else {
|
|
448
|
+
const strictFlag = plan.strict ? " --strict" : "";
|
|
449
|
+
lines.push(
|
|
450
|
+
`Planned ${plan.operations.length} Beignet fix operation${plan.operations.length === 1 ? "" : "s"} in ${plan.targetDir}:`,
|
|
451
|
+
"",
|
|
452
|
+
);
|
|
453
|
+
for (const operation of plan.operations) {
|
|
454
|
+
lines.push(
|
|
455
|
+
`${paint("PLAN", "cyan")} ${operation.id}`,
|
|
456
|
+
...operation.fixes.map(
|
|
457
|
+
(fix) => ` ${fix.code} ${fix.file}\n ${fix.message}`,
|
|
458
|
+
),
|
|
459
|
+
...operation.changes.flatMap((change) => [
|
|
460
|
+
` ${change.kind === "create" ? "Create" : "Update"} ${change.file}`,
|
|
461
|
+
change.patch.trimEnd(),
|
|
462
|
+
]),
|
|
463
|
+
"",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
lines.push(
|
|
468
|
+
`Plan ID: ${plan.planId}`,
|
|
469
|
+
"",
|
|
470
|
+
"Apply the complete plan:",
|
|
471
|
+
` beignet doctor --fix${strictFlag} --plan ${plan.planId}`,
|
|
472
|
+
"",
|
|
473
|
+
"Apply selected operations:",
|
|
474
|
+
` beignet doctor --fix${strictFlag} --plan ${plan.planId} --only ${plan.operations.map((operation) => operation.id).join(",")}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
lines.push(
|
|
479
|
+
"",
|
|
480
|
+
formatDoctorResult(
|
|
481
|
+
{
|
|
482
|
+
targetDir: plan.targetDir,
|
|
483
|
+
strict: plan.strict,
|
|
484
|
+
diagnostics: plan.diagnostics,
|
|
485
|
+
fixes: [],
|
|
486
|
+
},
|
|
487
|
+
options,
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
return lines.join("\n");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Format planned fixes as GitHub Actions notice annotations.
|
|
495
|
+
*/
|
|
496
|
+
export function formatDoctorFixPlanGithub(plan: DoctorFixPlanResult): string {
|
|
497
|
+
return [
|
|
498
|
+
...plan.operations.map((operation) =>
|
|
499
|
+
formatGithubAnnotation({
|
|
500
|
+
severity: "notice",
|
|
501
|
+
file: operation.changes[0]?.file,
|
|
502
|
+
message: `${operation.id}: ${operation.fixes.map((fix) => fix.message).join(" ")} Plan ${plan.planId}.`,
|
|
503
|
+
}),
|
|
504
|
+
),
|
|
505
|
+
...formatDoctorDiagnosticsGithub(plan.diagnostics),
|
|
506
|
+
].join("\n");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
type DoctorFormatResult = Pick<
|
|
510
|
+
InspectAppResult,
|
|
511
|
+
"targetDir" | "strict" | "diagnostics" | "fixes"
|
|
512
|
+
>;
|
|
513
|
+
|
|
332
514
|
/**
|
|
333
515
|
* Format doctor diagnostics and applied fixes for CLI output.
|
|
334
516
|
*/
|
|
335
517
|
export function formatDoctor(
|
|
336
518
|
result: InspectAppResult,
|
|
337
519
|
options: { color?: boolean } = {},
|
|
520
|
+
): string {
|
|
521
|
+
return formatDoctorResult(result, options);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function formatDoctorResult(
|
|
525
|
+
result: DoctorFormatResult,
|
|
526
|
+
options: { color?: boolean } = {},
|
|
338
527
|
): string {
|
|
339
528
|
const paint = createPainter(options.color);
|
|
340
529
|
const fixLines =
|
|
@@ -456,7 +645,7 @@ function productionHardeningChecklist(
|
|
|
456
645
|
];
|
|
457
646
|
}
|
|
458
647
|
|
|
459
|
-
function formatDoctorFooter(result:
|
|
648
|
+
function formatDoctorFooter(result: DoctorFormatResult): string {
|
|
460
649
|
const count = (severity: InspectDiagnostic["severity"]) =>
|
|
461
650
|
result.diagnostics.filter((diagnostic) => diagnostic.severity === severity)
|
|
462
651
|
.length;
|
|
@@ -469,15 +658,19 @@ function formatDoctorFooter(result: InspectAppResult): string {
|
|
|
469
658
|
* Format doctor diagnostics as GitHub Actions annotations.
|
|
470
659
|
*/
|
|
471
660
|
export function formatDoctorGithub(result: InspectAppResult): string {
|
|
472
|
-
return result.diagnostics
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
661
|
+
return formatDoctorDiagnosticsGithub(result.diagnostics).join("\n");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatDoctorDiagnosticsGithub(
|
|
665
|
+
diagnostics: InspectDiagnostic[],
|
|
666
|
+
): string[] {
|
|
667
|
+
return diagnostics.map((diagnostic) =>
|
|
668
|
+
formatGithubAnnotation({
|
|
669
|
+
severity: githubAnnotationSeverity(diagnostic.severity),
|
|
670
|
+
file: diagnostic.file,
|
|
671
|
+
message: `${diagnostic.code}: ${diagnostic.message}`,
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
481
674
|
}
|
|
482
675
|
|
|
483
676
|
function githubAnnotationSeverity(
|
|
@@ -878,73 +1071,19 @@ function balancedObjectSource(
|
|
|
878
1071
|
startIndex: number,
|
|
879
1072
|
): string | undefined {
|
|
880
1073
|
if (source[startIndex] !== "{") return undefined;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
let quote: '"' | "'" | "`" | undefined;
|
|
884
|
-
let escaped = false;
|
|
885
|
-
|
|
886
|
-
for (let index = startIndex; index < source.length; index += 1) {
|
|
887
|
-
const char = source[index];
|
|
888
|
-
|
|
889
|
-
if (quote) {
|
|
890
|
-
if (escaped) {
|
|
891
|
-
escaped = false;
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
if (char === "\\") {
|
|
895
|
-
escaped = true;
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
if (char === quote) quote = undefined;
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
903
|
-
quote = char;
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (char === "{") {
|
|
908
|
-
depth += 1;
|
|
909
|
-
continue;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (char === "}") {
|
|
913
|
-
depth -= 1;
|
|
914
|
-
if (depth === 0) return source.slice(startIndex, index + 1);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
return undefined;
|
|
1074
|
+
const endIndex = matchingDelimiterIndex(source, startIndex, "{", "}");
|
|
1075
|
+
return endIndex === -1 ? undefined : source.slice(startIndex, endIndex + 1);
|
|
919
1076
|
}
|
|
920
1077
|
|
|
921
1078
|
function topLevelObjectKeys(body: string): string[] {
|
|
922
1079
|
const keys: string[] = [];
|
|
923
1080
|
let depth = 0;
|
|
924
|
-
let
|
|
925
|
-
let escaped = false;
|
|
1081
|
+
let skipUntil = 0;
|
|
926
1082
|
|
|
927
|
-
for (
|
|
1083
|
+
for (const index of codeCharacterIndexes(body)) {
|
|
1084
|
+
if (index < skipUntil) continue;
|
|
928
1085
|
const char = body[index];
|
|
929
1086
|
|
|
930
|
-
if (quote) {
|
|
931
|
-
if (escaped) {
|
|
932
|
-
escaped = false;
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
if (char === "\\") {
|
|
936
|
-
escaped = true;
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
if (char === quote) quote = undefined;
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
944
|
-
quote = char;
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
1087
|
if (char === "{" || char === "[" || char === "(") {
|
|
949
1088
|
depth += 1;
|
|
950
1089
|
continue;
|
|
@@ -963,8 +1102,7 @@ function topLevelObjectKeys(body: string): string[] {
|
|
|
963
1102
|
const name = identifier[0];
|
|
964
1103
|
const next = body.slice(index + name.length).match(/^\s*:/);
|
|
965
1104
|
if (next) keys.push(name);
|
|
966
|
-
|
|
967
|
-
index += name.length - 1;
|
|
1105
|
+
skipUntil = index + name.length;
|
|
968
1106
|
}
|
|
969
1107
|
|
|
970
1108
|
return keys;
|
|
@@ -3831,6 +3969,7 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
3831
3969
|
severity: "error",
|
|
3832
3970
|
code,
|
|
3833
3971
|
file: centralExists ? centralFile : registry.indexFile,
|
|
3972
|
+
subject: workflowArtifactSubject(registry, member),
|
|
3834
3973
|
message: centralExists
|
|
3835
3974
|
? `${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.`
|
|
3836
3975
|
: `${workflowArtifactOrigin(registry, member)}, but ${centralFile} does not exist, so the ${noun} never runs. Register ${registry.registryName} in ${centralFile} (or run \`${makeCommand}\` to create it).`,
|
|
@@ -3846,6 +3985,7 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
3846
3985
|
severity: "warning",
|
|
3847
3986
|
code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
|
|
3848
3987
|
file: config.paths.outbox,
|
|
3988
|
+
subject: workflowArtifactSubject(registry, member),
|
|
3849
3989
|
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.`,
|
|
3850
3990
|
});
|
|
3851
3991
|
}
|
|
@@ -3857,6 +3997,7 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
3857
3997
|
severity: "warning",
|
|
3858
3998
|
code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
|
|
3859
3999
|
file: config.paths.outbox,
|
|
4000
|
+
subject: workflowArtifactSubject(registry, member),
|
|
3860
4001
|
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.`,
|
|
3861
4002
|
});
|
|
3862
4003
|
}
|
|
@@ -3878,6 +4019,7 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
3878
4019
|
drift.listeners.wiringFile ??
|
|
3879
4020
|
drift.listeners.eventBusFile ??
|
|
3880
4021
|
registry.indexFile,
|
|
4022
|
+
subject: workflowArtifactSubject(registry, member),
|
|
3881
4023
|
message: `${workflowArtifactOrigin(registry, member)}, but no registerListeners(...) call references it, so the listener never receives events. Register ${registry.registryName} with registerListeners(...) in ${listenerTarget}.`,
|
|
3882
4024
|
});
|
|
3883
4025
|
}
|
|
@@ -3983,6 +4125,16 @@ function workflowArtifactOrigin(
|
|
|
3983
4125
|
return `${memberFile} declares ${member} (exported through ${registry.registryName} in ${registry.indexFile})`;
|
|
3984
4126
|
}
|
|
3985
4127
|
|
|
4128
|
+
function workflowArtifactSubject(
|
|
4129
|
+
registry: FeatureWorkflowRegistry,
|
|
4130
|
+
member: string,
|
|
4131
|
+
): NonNullable<InspectDiagnostic["subject"]> {
|
|
4132
|
+
return {
|
|
4133
|
+
file: registry.memberFiles.get(member) ?? registry.indexFile,
|
|
4134
|
+
exportName: member,
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
|
|
3986
4138
|
async function workflowRegistrationDrift(
|
|
3987
4139
|
targetDir: string,
|
|
3988
4140
|
files: string[],
|
|
@@ -5464,26 +5616,9 @@ function splitTopLevelArguments(source: string): string[] {
|
|
|
5464
5616
|
const args: string[] = [];
|
|
5465
5617
|
let start = 0;
|
|
5466
5618
|
let depth = 0;
|
|
5467
|
-
let inString: string | undefined;
|
|
5468
|
-
let escaped = false;
|
|
5469
5619
|
|
|
5470
|
-
for (
|
|
5620
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
5471
5621
|
const char = source[index];
|
|
5472
|
-
if (inString) {
|
|
5473
|
-
if (escaped) {
|
|
5474
|
-
escaped = false;
|
|
5475
|
-
} else if (char === "\\") {
|
|
5476
|
-
escaped = true;
|
|
5477
|
-
} else if (char === inString) {
|
|
5478
|
-
inString = undefined;
|
|
5479
|
-
}
|
|
5480
|
-
continue;
|
|
5481
|
-
}
|
|
5482
|
-
|
|
5483
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
5484
|
-
inString = char;
|
|
5485
|
-
continue;
|
|
5486
|
-
}
|
|
5487
5622
|
if (char === "(" || char === "{" || char === "[") {
|
|
5488
5623
|
depth += 1;
|
|
5489
5624
|
continue;
|
|
@@ -6020,8 +6155,7 @@ async function inspectPackageScripts(
|
|
|
6020
6155
|
severity: "warning",
|
|
6021
6156
|
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
6022
6157
|
file: "package.json",
|
|
6023
|
-
message:
|
|
6024
|
-
'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
|
|
6158
|
+
message: `package.json does not define a test script. Add "test": "${currentGeneratedPackageScripts.test}" or run beignet doctor --fix.`,
|
|
6025
6159
|
});
|
|
6026
6160
|
}
|
|
6027
6161
|
|
|
@@ -6033,7 +6167,7 @@ async function inspectPackageScripts(
|
|
|
6033
6167
|
severity: "warning",
|
|
6034
6168
|
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
6035
6169
|
file: "package.json",
|
|
6036
|
-
message: `package.json still uses legacy generated
|
|
6170
|
+
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.`,
|
|
6037
6171
|
});
|
|
6038
6172
|
}
|
|
6039
6173
|
|
|
@@ -6067,11 +6201,11 @@ async function inspectPackageScripts(
|
|
|
6067
6201
|
return diagnostics;
|
|
6068
6202
|
}
|
|
6069
6203
|
|
|
6070
|
-
async function
|
|
6204
|
+
async function planPackageScripts(
|
|
6071
6205
|
targetDir: string,
|
|
6072
6206
|
files: string[],
|
|
6073
6207
|
convention: InspectConvention,
|
|
6074
|
-
): Promise<
|
|
6208
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6075
6209
|
if (!convention.resourceGenerator || !files.includes("package.json")) {
|
|
6076
6210
|
return undefined;
|
|
6077
6211
|
}
|
|
@@ -6097,10 +6231,8 @@ async function fixPackageScripts(
|
|
|
6097
6231
|
}
|
|
6098
6232
|
|
|
6099
6233
|
if (!hasDirectBunTestImports) {
|
|
6100
|
-
for (const
|
|
6101
|
-
|
|
6102
|
-
)) {
|
|
6103
|
-
if (packageJson.scripts[name] === legacyCommand) {
|
|
6234
|
+
for (const name of Object.keys(legacyGeneratedPackageScripts)) {
|
|
6235
|
+
if (isLegacyGeneratedPackageScript(name, packageJson.scripts[name])) {
|
|
6104
6236
|
packageJson.scripts[name] = currentGeneratedPackageScripts[name];
|
|
6105
6237
|
if (!updatedScripts.includes(name)) updatedScripts.push(name);
|
|
6106
6238
|
}
|
|
@@ -6118,29 +6250,25 @@ async function fixPackageScripts(
|
|
|
6118
6250
|
}
|
|
6119
6251
|
|
|
6120
6252
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
6121
|
-
const createdSupportFiles =
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
);
|
|
6253
|
+
const createdSupportFiles =
|
|
6254
|
+
packageJson.scripts.test === currentGeneratedPackageScripts.test
|
|
6255
|
+
? missingTestSupportFiles(files)
|
|
6256
|
+
: [];
|
|
6126
6257
|
|
|
6127
6258
|
if (next === original && createdSupportFiles.length === 0) return undefined;
|
|
6128
6259
|
|
|
6129
|
-
|
|
6130
|
-
|
|
6260
|
+
let fix: InspectFix | undefined;
|
|
6131
6261
|
if (addedTestScript) {
|
|
6132
|
-
|
|
6262
|
+
fix = {
|
|
6133
6263
|
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
6134
6264
|
file: "package.json",
|
|
6135
6265
|
message:
|
|
6136
6266
|
createdSupportFiles.length > 0
|
|
6137
|
-
? `Added "test": "
|
|
6138
|
-
:
|
|
6267
|
+
? `Added "test": "${currentGeneratedPackageScripts.test}" and ${createdSupportFiles.join(", ")}.`
|
|
6268
|
+
: `Added "test": "${currentGeneratedPackageScripts.test}".`,
|
|
6139
6269
|
};
|
|
6140
|
-
}
|
|
6141
|
-
|
|
6142
|
-
if (updatedScripts.length > 0) {
|
|
6143
|
-
return {
|
|
6270
|
+
} else if (updatedScripts.length > 0) {
|
|
6271
|
+
fix = {
|
|
6144
6272
|
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
6145
6273
|
file: "package.json",
|
|
6146
6274
|
message: packageScriptFixMessage(
|
|
@@ -6149,31 +6277,48 @@ async function fixPackageScripts(
|
|
|
6149
6277
|
addedTsx,
|
|
6150
6278
|
),
|
|
6151
6279
|
};
|
|
6152
|
-
}
|
|
6153
|
-
|
|
6154
|
-
if (createdSupportFiles.length > 0) {
|
|
6155
|
-
return {
|
|
6280
|
+
} else if (createdSupportFiles.length > 0) {
|
|
6281
|
+
fix = {
|
|
6156
6282
|
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
6157
6283
|
file: "package.json",
|
|
6158
6284
|
message: `Restored ${createdSupportFiles.join(", ")}.`,
|
|
6159
6285
|
};
|
|
6160
|
-
}
|
|
6161
|
-
|
|
6162
|
-
if (addedTsx) {
|
|
6163
|
-
return {
|
|
6286
|
+
} else if (addedTsx) {
|
|
6287
|
+
fix = {
|
|
6164
6288
|
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
6165
6289
|
file: "package.json",
|
|
6166
6290
|
message: 'Added "tsx" to devDependencies.',
|
|
6167
6291
|
};
|
|
6168
6292
|
}
|
|
6169
6293
|
|
|
6170
|
-
return undefined;
|
|
6294
|
+
if (!fix) return undefined;
|
|
6295
|
+
|
|
6296
|
+
return {
|
|
6297
|
+
id: "package.repair-generated-support",
|
|
6298
|
+
fixes: [fix],
|
|
6299
|
+
changes: [
|
|
6300
|
+
...(next === original
|
|
6301
|
+
? []
|
|
6302
|
+
: [
|
|
6303
|
+
planDoctorFixFileChange({
|
|
6304
|
+
file: "package.json",
|
|
6305
|
+
before: original,
|
|
6306
|
+
after: next,
|
|
6307
|
+
}),
|
|
6308
|
+
]),
|
|
6309
|
+
...testSupportTemplateFiles
|
|
6310
|
+
.filter((file) => createdSupportFiles.includes(file.path))
|
|
6311
|
+
.map((file) =>
|
|
6312
|
+
planDoctorFixFileChange({ file: file.path, after: file.content }),
|
|
6313
|
+
),
|
|
6314
|
+
],
|
|
6315
|
+
};
|
|
6171
6316
|
}
|
|
6172
6317
|
|
|
6173
6318
|
function legacyGeneratedScriptNames(scripts: Record<string, string>): string[] {
|
|
6174
|
-
return Object.
|
|
6175
|
-
|
|
6176
|
-
|
|
6319
|
+
return Object.keys(legacyGeneratedPackageScripts).filter((name) =>
|
|
6320
|
+
isLegacyGeneratedPackageScript(name, scripts[name]),
|
|
6321
|
+
);
|
|
6177
6322
|
}
|
|
6178
6323
|
|
|
6179
6324
|
async function appUsesDirectBunTestImports(
|
|
@@ -6194,7 +6339,8 @@ function hasCurrentGeneratedTsxScript(
|
|
|
6194
6339
|
): boolean {
|
|
6195
6340
|
return Object.values(currentGeneratedPackageScripts).some(
|
|
6196
6341
|
(command) =>
|
|
6197
|
-
command.startsWith("tsx ")
|
|
6342
|
+
(command.startsWith("tsx ") || command.includes("--import tsx")) &&
|
|
6343
|
+
Object.values(scripts).includes(command),
|
|
6198
6344
|
);
|
|
6199
6345
|
}
|
|
6200
6346
|
|
|
@@ -6204,23 +6350,6 @@ function missingTestSupportFiles(files: string[]): string[] {
|
|
|
6204
6350
|
.filter((file) => !files.includes(file));
|
|
6205
6351
|
}
|
|
6206
6352
|
|
|
6207
|
-
async function createMissingTestSupportFiles(
|
|
6208
|
-
targetDir: string,
|
|
6209
|
-
files: string[],
|
|
6210
|
-
shouldCreate: boolean,
|
|
6211
|
-
): Promise<string[]> {
|
|
6212
|
-
if (!shouldCreate) return [];
|
|
6213
|
-
const createdSupportFiles: string[] = [];
|
|
6214
|
-
for (const file of testSupportTemplateFiles) {
|
|
6215
|
-
if (files.includes(file.path)) continue;
|
|
6216
|
-
const destination = path.join(targetDir, file.path);
|
|
6217
|
-
await mkdir(path.dirname(destination), { recursive: true });
|
|
6218
|
-
await writeFile(destination, file.content);
|
|
6219
|
-
createdSupportFiles.push(file.path);
|
|
6220
|
-
}
|
|
6221
|
-
return createdSupportFiles;
|
|
6222
|
-
}
|
|
6223
|
-
|
|
6224
6353
|
function packageScriptFixMessage(
|
|
6225
6354
|
updatedScripts: string[],
|
|
6226
6355
|
createdSupportFiles: string[],
|
|
@@ -6274,6 +6403,8 @@ function dedupeDiagnostics(
|
|
|
6274
6403
|
diagnostic.code,
|
|
6275
6404
|
diagnostic.file ?? "",
|
|
6276
6405
|
diagnostic.contract ?? "",
|
|
6406
|
+
diagnostic.subject?.file ?? "",
|
|
6407
|
+
diagnostic.subject?.exportName ?? "",
|
|
6277
6408
|
diagnostic.message,
|
|
6278
6409
|
].join(":");
|
|
6279
6410
|
if (seen.has(key)) continue;
|
|
@@ -6463,6 +6594,10 @@ async function inspectFeatureRouteRegistration(
|
|
|
6463
6594
|
code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
|
|
6464
6595
|
file: config.paths.server,
|
|
6465
6596
|
contract: routeGroupContract.exportName,
|
|
6597
|
+
subject: {
|
|
6598
|
+
file: routeGroup.file,
|
|
6599
|
+
exportName: routeGroup.name,
|
|
6600
|
+
},
|
|
6466
6601
|
message: contract
|
|
6467
6602
|
? `${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.`
|
|
6468
6603
|
: `${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.`,
|
|
@@ -6584,44 +6719,8 @@ function balancedArraySource(
|
|
|
6584
6719
|
startIndex: number,
|
|
6585
6720
|
): string | undefined {
|
|
6586
6721
|
if (source[startIndex] !== "[") return undefined;
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
let quote: '"' | "'" | "`" | undefined;
|
|
6590
|
-
let escaped = false;
|
|
6591
|
-
|
|
6592
|
-
for (let index = startIndex; index < source.length; index += 1) {
|
|
6593
|
-
const char = source[index];
|
|
6594
|
-
|
|
6595
|
-
if (quote) {
|
|
6596
|
-
if (escaped) {
|
|
6597
|
-
escaped = false;
|
|
6598
|
-
continue;
|
|
6599
|
-
}
|
|
6600
|
-
if (char === "\\") {
|
|
6601
|
-
escaped = true;
|
|
6602
|
-
continue;
|
|
6603
|
-
}
|
|
6604
|
-
if (char === quote) quote = undefined;
|
|
6605
|
-
continue;
|
|
6606
|
-
}
|
|
6607
|
-
|
|
6608
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
6609
|
-
quote = char;
|
|
6610
|
-
continue;
|
|
6611
|
-
}
|
|
6612
|
-
|
|
6613
|
-
if (char === "[") {
|
|
6614
|
-
depth += 1;
|
|
6615
|
-
continue;
|
|
6616
|
-
}
|
|
6617
|
-
|
|
6618
|
-
if (char === "]") {
|
|
6619
|
-
depth -= 1;
|
|
6620
|
-
if (depth === 0) return source.slice(startIndex, index + 1);
|
|
6621
|
-
}
|
|
6622
|
-
}
|
|
6623
|
-
|
|
6624
|
-
return undefined;
|
|
6722
|
+
const endIndex = matchingDelimiterIndex(source, startIndex, "[", "]");
|
|
6723
|
+
return endIndex === -1 ? undefined : source.slice(startIndex, endIndex + 1);
|
|
6625
6724
|
}
|
|
6626
6725
|
|
|
6627
6726
|
function routeGroupContractRef(
|
|
@@ -6651,21 +6750,10 @@ function routeGroupContractRef(
|
|
|
6651
6750
|
}
|
|
6652
6751
|
|
|
6653
6752
|
function registeredRouteGroups(source: string): Set<string> {
|
|
6654
|
-
const
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
const defineRoutesEnd = defineRoutesSource.indexOf("]),");
|
|
6659
|
-
const searchSource =
|
|
6660
|
-
defineRoutesEnd === -1
|
|
6661
|
-
? defineRoutesSource
|
|
6662
|
-
: defineRoutesSource.slice(0, defineRoutesEnd);
|
|
6663
|
-
|
|
6664
|
-
return new Set(
|
|
6665
|
-
[...searchSource.matchAll(/\b([A-Za-z_$][\w$]*)\b/g)].map(
|
|
6666
|
-
(match) => match[1],
|
|
6667
|
-
),
|
|
6668
|
-
);
|
|
6753
|
+
const routes = firstDefineRoutesArgInfo(source);
|
|
6754
|
+
return routes?.text.startsWith("[")
|
|
6755
|
+
? identifiersFromArrayExpression(routes.text)
|
|
6756
|
+
: new Set();
|
|
6669
6757
|
}
|
|
6670
6758
|
|
|
6671
6759
|
async function registeredRouteGroupsForServer(
|
|
@@ -6727,14 +6815,14 @@ function findRouteGroupContract(
|
|
|
6727
6815
|
);
|
|
6728
6816
|
}
|
|
6729
6817
|
|
|
6730
|
-
async function
|
|
6818
|
+
async function planDirectOpenApiArrayDrift(
|
|
6731
6819
|
targetDir: string,
|
|
6732
6820
|
files: string[],
|
|
6733
6821
|
config: ResolvedBeignetConfig,
|
|
6734
6822
|
convention: InspectConvention,
|
|
6735
6823
|
contracts: InspectedContract[],
|
|
6736
6824
|
matchedRoutes: MatchedRoutesResult,
|
|
6737
|
-
): Promise<
|
|
6825
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6738
6826
|
const routePath = path.join(targetDir, config.paths.openapiRoute);
|
|
6739
6827
|
let source: string;
|
|
6740
6828
|
try {
|
|
@@ -6777,21 +6865,32 @@ async function fixDirectOpenApiArrayDrift(
|
|
|
6777
6865
|
)}`;
|
|
6778
6866
|
if (next === source) return undefined;
|
|
6779
6867
|
|
|
6780
|
-
await writeFile(routePath, next);
|
|
6781
6868
|
return {
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6869
|
+
id: "openapi.register-missing",
|
|
6870
|
+
fixes: [
|
|
6871
|
+
{
|
|
6872
|
+
code: "BEIGNET_OPENAPI_MISSING",
|
|
6873
|
+
file: config.paths.openapiRoute,
|
|
6874
|
+
message: `Added ${missingContracts
|
|
6875
|
+
.map((contract) => contract.exportName)
|
|
6876
|
+
.join(", ")} to the direct createOpenAPIHandler contract array.`,
|
|
6877
|
+
},
|
|
6878
|
+
],
|
|
6879
|
+
changes: [
|
|
6880
|
+
planDoctorFixFileChange({
|
|
6881
|
+
file: config.paths.openapiRoute,
|
|
6882
|
+
before: source,
|
|
6883
|
+
after: next,
|
|
6884
|
+
}),
|
|
6885
|
+
],
|
|
6787
6886
|
};
|
|
6788
6887
|
}
|
|
6789
6888
|
|
|
6790
|
-
async function
|
|
6889
|
+
async function planUnregisteredRouteGroups(
|
|
6791
6890
|
targetDir: string,
|
|
6792
6891
|
files: string[],
|
|
6793
6892
|
config: ResolvedBeignetConfig,
|
|
6794
|
-
): Promise<
|
|
6893
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6795
6894
|
if (!files.includes(config.paths.server)) return undefined;
|
|
6796
6895
|
|
|
6797
6896
|
const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
|
|
@@ -6862,25 +6961,36 @@ async function fixUnregisteredRouteGroups(
|
|
|
6862
6961
|
)}`;
|
|
6863
6962
|
if (next === original) return undefined;
|
|
6864
6963
|
|
|
6865
|
-
await writeFile(targetPath, next);
|
|
6866
6964
|
return {
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6965
|
+
id: "routes.register-missing",
|
|
6966
|
+
fixes: [
|
|
6967
|
+
{
|
|
6968
|
+
code: "BEIGNET_ROUTE_GROUP_UNREGISTERED",
|
|
6969
|
+
file: targetFile,
|
|
6970
|
+
message: `Registered ${groupsToAppend
|
|
6971
|
+
.map((routeGroup) => routeGroup.name)
|
|
6972
|
+
.join(", ")} in the central route list.`,
|
|
6973
|
+
},
|
|
6974
|
+
],
|
|
6975
|
+
changes: [
|
|
6976
|
+
planDoctorFixFileChange({
|
|
6977
|
+
file: targetFile,
|
|
6978
|
+
before: original,
|
|
6979
|
+
after: next,
|
|
6980
|
+
}),
|
|
6981
|
+
],
|
|
6872
6982
|
};
|
|
6873
6983
|
}
|
|
6874
6984
|
|
|
6875
|
-
async function
|
|
6985
|
+
async function planUnregisteredSchedules(
|
|
6876
6986
|
targetDir: string,
|
|
6877
6987
|
files: string[],
|
|
6878
6988
|
drift: WorkflowRegistrationDrift,
|
|
6879
6989
|
config: ResolvedBeignetConfig,
|
|
6880
|
-
): Promise<
|
|
6990
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6881
6991
|
if (!drift.schedules.centralExists) return undefined;
|
|
6882
6992
|
|
|
6883
|
-
return
|
|
6993
|
+
return planWorkflowRegistryOperation("schedules.register-missing", {
|
|
6884
6994
|
targetDir,
|
|
6885
6995
|
files,
|
|
6886
6996
|
centralFile: config.paths.schedules,
|
|
@@ -6893,15 +7003,15 @@ async function fixUnregisteredSchedules(
|
|
|
6893
7003
|
});
|
|
6894
7004
|
}
|
|
6895
7005
|
|
|
6896
|
-
async function
|
|
7006
|
+
async function planUnregisteredTasks(
|
|
6897
7007
|
targetDir: string,
|
|
6898
7008
|
files: string[],
|
|
6899
7009
|
drift: WorkflowRegistrationDrift,
|
|
6900
7010
|
config: ResolvedBeignetConfig,
|
|
6901
|
-
): Promise<
|
|
7011
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6902
7012
|
if (!drift.tasks.centralExists) return undefined;
|
|
6903
7013
|
|
|
6904
|
-
return
|
|
7014
|
+
return planWorkflowRegistryOperation("tasks.register-missing", {
|
|
6905
7015
|
targetDir,
|
|
6906
7016
|
files,
|
|
6907
7017
|
centralFile: config.paths.tasks,
|
|
@@ -6915,6 +7025,84 @@ async function fixUnregisteredTasks(
|
|
|
6915
7025
|
});
|
|
6916
7026
|
}
|
|
6917
7027
|
|
|
7028
|
+
async function planCoLocatedScheduleAndTaskRegistries(
|
|
7029
|
+
targetDir: string,
|
|
7030
|
+
files: string[],
|
|
7031
|
+
drift: WorkflowRegistrationDrift,
|
|
7032
|
+
config: ResolvedBeignetConfig,
|
|
7033
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7034
|
+
if (!drift.schedules.centralExists || !drift.tasks.centralExists) {
|
|
7035
|
+
return undefined;
|
|
7036
|
+
}
|
|
7037
|
+
|
|
7038
|
+
const centralFile = config.paths.schedules;
|
|
7039
|
+
let original: string;
|
|
7040
|
+
try {
|
|
7041
|
+
original = await readFile(path.join(targetDir, centralFile), "utf8");
|
|
7042
|
+
} catch {
|
|
7043
|
+
return undefined;
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
let next = original;
|
|
7047
|
+
const fixes: InspectFix[] = [];
|
|
7048
|
+
const repairedKinds: Array<"schedules" | "tasks"> = [];
|
|
7049
|
+
|
|
7050
|
+
const scheduleEdit = planWorkflowRegistryEdit({
|
|
7051
|
+
files,
|
|
7052
|
+
centralFile,
|
|
7053
|
+
source: next,
|
|
7054
|
+
unregistered: drift.schedules.unregistered,
|
|
7055
|
+
code: "BEIGNET_SCHEDULE_UNREGISTERED",
|
|
7056
|
+
listName: "the schedules array",
|
|
7057
|
+
importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
|
|
7058
|
+
append: (source, entry, importLine) =>
|
|
7059
|
+
appendToNamedArray(source, "schedules", entry, importLine),
|
|
7060
|
+
});
|
|
7061
|
+
if (scheduleEdit) {
|
|
7062
|
+
next = scheduleEdit.after;
|
|
7063
|
+
fixes.push(scheduleEdit.fix);
|
|
7064
|
+
repairedKinds.push("schedules");
|
|
7065
|
+
}
|
|
7066
|
+
|
|
7067
|
+
const taskEdit = planWorkflowRegistryEdit({
|
|
7068
|
+
files,
|
|
7069
|
+
centralFile,
|
|
7070
|
+
source: next,
|
|
7071
|
+
unregistered: drift.tasks.unregistered,
|
|
7072
|
+
code: "BEIGNET_TASK_UNREGISTERED",
|
|
7073
|
+
listName: "defineTasks([...])",
|
|
7074
|
+
importSpecifier: (indexFile) => relativeModule(centralFile, indexFile),
|
|
7075
|
+
append: (source, entry, importLine) =>
|
|
7076
|
+
appendToNamedArray(source, "tasks", entry, importLine),
|
|
7077
|
+
});
|
|
7078
|
+
if (taskEdit) {
|
|
7079
|
+
next = taskEdit.after;
|
|
7080
|
+
fixes.push(taskEdit.fix);
|
|
7081
|
+
repairedKinds.push("tasks");
|
|
7082
|
+
}
|
|
7083
|
+
|
|
7084
|
+
if (fixes.length === 0 || next === original) return undefined;
|
|
7085
|
+
|
|
7086
|
+
const id: DoctorFixOperationId =
|
|
7087
|
+
repairedKinds.length === 2
|
|
7088
|
+
? "workflows.register-missing"
|
|
7089
|
+
: repairedKinds[0] === "schedules"
|
|
7090
|
+
? "schedules.register-missing"
|
|
7091
|
+
: "tasks.register-missing";
|
|
7092
|
+
|
|
7093
|
+
return {
|
|
7094
|
+
id,
|
|
7095
|
+
fixes,
|
|
7096
|
+
changes: [
|
|
7097
|
+
planDoctorFixFileChange({
|
|
7098
|
+
file: centralFile,
|
|
7099
|
+
before: original,
|
|
7100
|
+
after: next,
|
|
7101
|
+
}),
|
|
7102
|
+
],
|
|
7103
|
+
};
|
|
7104
|
+
}
|
|
7105
|
+
|
|
6918
7106
|
/**
|
|
6919
7107
|
* Wire fully unregistered feature listener registries into the central
|
|
6920
7108
|
* listener registry and ensure the providers file registers that central
|
|
@@ -6926,12 +7114,12 @@ async function fixUnregisteredTasks(
|
|
|
6926
7114
|
* register against, or when the registry name is already imported from another
|
|
6927
7115
|
* file. The diagnostic stays in every bail case.
|
|
6928
7116
|
*/
|
|
6929
|
-
async function
|
|
7117
|
+
async function planUnregisteredListeners(
|
|
6930
7118
|
targetDir: string,
|
|
6931
7119
|
files: string[],
|
|
6932
7120
|
drift: WorkflowRegistrationDrift,
|
|
6933
7121
|
config: ResolvedBeignetConfig,
|
|
6934
|
-
): Promise<
|
|
7122
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
6935
7123
|
const candidates = drift.listeners.unregistered.filter(
|
|
6936
7124
|
(entry) => entry.fullyUnregistered,
|
|
6937
7125
|
);
|
|
@@ -7028,32 +7216,61 @@ async function fixUnregisteredListeners(
|
|
|
7028
7216
|
return undefined;
|
|
7029
7217
|
}
|
|
7030
7218
|
|
|
7031
|
-
await mkdir(path.dirname(listenersPath), { recursive: true });
|
|
7032
|
-
await writeFile(listenersPath, nextListeners);
|
|
7033
|
-
if (nextProviders !== originalProviders) {
|
|
7034
|
-
await writeFile(providersPath, nextProviders);
|
|
7035
|
-
}
|
|
7036
7219
|
return {
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7220
|
+
id: "listeners.register-missing",
|
|
7221
|
+
fixes: [
|
|
7222
|
+
{
|
|
7223
|
+
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
7224
|
+
file: listenersFile,
|
|
7225
|
+
message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
|
|
7226
|
+
},
|
|
7227
|
+
],
|
|
7228
|
+
changes: [
|
|
7229
|
+
...(nextListeners === originalListeners
|
|
7230
|
+
? []
|
|
7231
|
+
: [
|
|
7232
|
+
planDoctorFixFileChange({
|
|
7233
|
+
file: listenersFile,
|
|
7234
|
+
before: originalListeners,
|
|
7235
|
+
after: nextListeners,
|
|
7236
|
+
}),
|
|
7237
|
+
]),
|
|
7238
|
+
...(nextProviders === originalProviders
|
|
7239
|
+
? []
|
|
7240
|
+
: [
|
|
7241
|
+
planDoctorFixFileChange({
|
|
7242
|
+
file: providersFile,
|
|
7243
|
+
before: originalProviders,
|
|
7244
|
+
after: nextProviders,
|
|
7245
|
+
}),
|
|
7246
|
+
]),
|
|
7247
|
+
],
|
|
7040
7248
|
};
|
|
7041
7249
|
}
|
|
7042
7250
|
|
|
7043
|
-
async function
|
|
7251
|
+
async function planUnregisteredOutboxEntries(
|
|
7044
7252
|
targetDir: string,
|
|
7045
7253
|
files: string[],
|
|
7046
7254
|
drift: WorkflowRegistrationDrift,
|
|
7047
7255
|
config: ResolvedBeignetConfig,
|
|
7048
|
-
): Promise<
|
|
7049
|
-
if (!drift.outbox.exists) return
|
|
7256
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7257
|
+
if (!drift.outbox.exists) return undefined;
|
|
7258
|
+
|
|
7259
|
+
const outboxPath = path.join(targetDir, config.paths.outbox);
|
|
7260
|
+
let original: string;
|
|
7261
|
+
try {
|
|
7262
|
+
original = await readFile(outboxPath, "utf8");
|
|
7263
|
+
} catch {
|
|
7264
|
+
return undefined;
|
|
7265
|
+
}
|
|
7050
7266
|
|
|
7267
|
+
let next = original;
|
|
7051
7268
|
const fixes: InspectFix[] = [];
|
|
7052
7269
|
|
|
7053
|
-
const eventFix =
|
|
7054
|
-
targetDir,
|
|
7270
|
+
const eventFix = planWorkflowRegistryEdit({
|
|
7055
7271
|
files,
|
|
7056
7272
|
centralFile: config.paths.outbox,
|
|
7273
|
+
source: next,
|
|
7057
7274
|
unregistered: drift.outbox.events,
|
|
7058
7275
|
code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
|
|
7059
7276
|
listName: "the outbox events list",
|
|
@@ -7061,12 +7278,15 @@ async function fixUnregisteredOutboxEntries(
|
|
|
7061
7278
|
append: (source, entry, importLine) =>
|
|
7062
7279
|
appendToOutboxRegistryArray(source, "events", entry, importLine),
|
|
7063
7280
|
});
|
|
7064
|
-
if (eventFix)
|
|
7281
|
+
if (eventFix) {
|
|
7282
|
+
next = eventFix.after;
|
|
7283
|
+
fixes.push(eventFix.fix);
|
|
7284
|
+
}
|
|
7065
7285
|
|
|
7066
|
-
const jobFix =
|
|
7067
|
-
targetDir,
|
|
7286
|
+
const jobFix = planWorkflowRegistryEdit({
|
|
7068
7287
|
files,
|
|
7069
7288
|
centralFile: config.paths.outbox,
|
|
7289
|
+
source: next,
|
|
7070
7290
|
unregistered: drift.outbox.jobs,
|
|
7071
7291
|
code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
|
|
7072
7292
|
listName: "the outbox jobs list",
|
|
@@ -7074,9 +7294,23 @@ async function fixUnregisteredOutboxEntries(
|
|
|
7074
7294
|
append: (source, entry, importLine) =>
|
|
7075
7295
|
appendToOutboxRegistryArray(source, "jobs", entry, importLine),
|
|
7076
7296
|
});
|
|
7077
|
-
if (jobFix)
|
|
7297
|
+
if (jobFix) {
|
|
7298
|
+
next = jobFix.after;
|
|
7299
|
+
fixes.push(jobFix.fix);
|
|
7300
|
+
}
|
|
7078
7301
|
|
|
7079
|
-
return
|
|
7302
|
+
if (fixes.length === 0 || next === original) return undefined;
|
|
7303
|
+
return {
|
|
7304
|
+
id: "outbox.register-missing",
|
|
7305
|
+
fixes,
|
|
7306
|
+
changes: [
|
|
7307
|
+
planDoctorFixFileChange({
|
|
7308
|
+
file: config.paths.outbox,
|
|
7309
|
+
before: original,
|
|
7310
|
+
after: next,
|
|
7311
|
+
}),
|
|
7312
|
+
],
|
|
7313
|
+
};
|
|
7080
7314
|
}
|
|
7081
7315
|
|
|
7082
7316
|
/**
|
|
@@ -7088,7 +7322,7 @@ async function fixUnregisteredOutboxEntries(
|
|
|
7088
7322
|
* some members are already individually registered (appending the registry
|
|
7089
7323
|
* spread would run those members twice). The diagnostic stays in that case.
|
|
7090
7324
|
*/
|
|
7091
|
-
|
|
7325
|
+
type WorkflowRegistryPlanOptions = {
|
|
7092
7326
|
targetDir: string;
|
|
7093
7327
|
files: string[];
|
|
7094
7328
|
centralFile: string;
|
|
@@ -7097,12 +7331,12 @@ async function applyWorkflowRegistryFix(options: {
|
|
|
7097
7331
|
listName: string;
|
|
7098
7332
|
importSpecifier: (indexFile: string) => string;
|
|
7099
7333
|
append: (source: string, entry: string, importLine?: string) => AppendResult;
|
|
7100
|
-
}
|
|
7101
|
-
const candidates = options.unregistered.filter(
|
|
7102
|
-
(entry) => entry.fullyUnregistered,
|
|
7103
|
-
);
|
|
7104
|
-
if (candidates.length === 0) return undefined;
|
|
7334
|
+
};
|
|
7105
7335
|
|
|
7336
|
+
async function planWorkflowRegistryOperation(
|
|
7337
|
+
id: DoctorFixOperationId,
|
|
7338
|
+
options: WorkflowRegistryPlanOptions,
|
|
7339
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7106
7340
|
const centralPath = path.join(options.targetDir, options.centralFile);
|
|
7107
7341
|
let original: string;
|
|
7108
7342
|
try {
|
|
@@ -7111,7 +7345,30 @@ async function applyWorkflowRegistryFix(options: {
|
|
|
7111
7345
|
return undefined;
|
|
7112
7346
|
}
|
|
7113
7347
|
|
|
7114
|
-
|
|
7348
|
+
const edit = planWorkflowRegistryEdit({ ...options, source: original });
|
|
7349
|
+
if (!edit) return undefined;
|
|
7350
|
+
return {
|
|
7351
|
+
id,
|
|
7352
|
+
fixes: [edit.fix],
|
|
7353
|
+
changes: [
|
|
7354
|
+
planDoctorFixFileChange({
|
|
7355
|
+
file: options.centralFile,
|
|
7356
|
+
before: original,
|
|
7357
|
+
after: edit.after,
|
|
7358
|
+
}),
|
|
7359
|
+
],
|
|
7360
|
+
};
|
|
7361
|
+
}
|
|
7362
|
+
|
|
7363
|
+
function planWorkflowRegistryEdit(
|
|
7364
|
+
options: Omit<WorkflowRegistryPlanOptions, "targetDir"> & { source: string },
|
|
7365
|
+
): { after: string; fix: InspectFix } | undefined {
|
|
7366
|
+
const candidates = options.unregistered.filter(
|
|
7367
|
+
(entry) => entry.fullyUnregistered,
|
|
7368
|
+
);
|
|
7369
|
+
if (candidates.length === 0) return undefined;
|
|
7370
|
+
|
|
7371
|
+
let next = options.source;
|
|
7115
7372
|
const registeredNames: string[] = [];
|
|
7116
7373
|
|
|
7117
7374
|
for (const { registry } of candidates) {
|
|
@@ -7143,13 +7400,15 @@ async function applyWorkflowRegistryFix(options: {
|
|
|
7143
7400
|
}
|
|
7144
7401
|
}
|
|
7145
7402
|
|
|
7146
|
-
if (next ===
|
|
7403
|
+
if (next === options.source || registeredNames.length === 0) return undefined;
|
|
7147
7404
|
|
|
7148
|
-
await writeFile(centralPath, next);
|
|
7149
7405
|
return {
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7406
|
+
after: next,
|
|
7407
|
+
fix: {
|
|
7408
|
+
code: options.code,
|
|
7409
|
+
file: options.centralFile,
|
|
7410
|
+
message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
|
|
7411
|
+
},
|
|
7153
7412
|
};
|
|
7154
7413
|
}
|
|
7155
7414
|
|
|
@@ -7347,26 +7606,9 @@ function firstCallArgInfo(
|
|
|
7347
7606
|
argStart: number,
|
|
7348
7607
|
): { text: string; start: number; end: number } | undefined {
|
|
7349
7608
|
let depth = 0;
|
|
7350
|
-
let inString: string | undefined;
|
|
7351
|
-
let escaped = false;
|
|
7352
7609
|
|
|
7353
|
-
for (
|
|
7610
|
+
for (const index of codeCharacterIndexes(source, argStart)) {
|
|
7354
7611
|
const char = source[index];
|
|
7355
|
-
if (inString) {
|
|
7356
|
-
if (escaped) {
|
|
7357
|
-
escaped = false;
|
|
7358
|
-
} else if (char === "\\") {
|
|
7359
|
-
escaped = true;
|
|
7360
|
-
} else if (char === inString) {
|
|
7361
|
-
inString = undefined;
|
|
7362
|
-
}
|
|
7363
|
-
continue;
|
|
7364
|
-
}
|
|
7365
|
-
|
|
7366
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
7367
|
-
inString = char;
|
|
7368
|
-
continue;
|
|
7369
|
-
}
|
|
7370
7612
|
|
|
7371
7613
|
if (char === "[" || char === "(" || char === "{") {
|
|
7372
7614
|
depth++;
|