@beignet/cli 0.0.45 → 0.0.47
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 +20 -0
- package/README.md +19 -9
- package/dist/choices.d.ts +1 -1
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +3 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +4 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -2
- package/dist/db.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +520 -41
- package/dist/inspect.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +2 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +14 -2
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +4 -1
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.js +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +7 -1
- package/src/choices.ts +3 -0
- package/src/db.ts +4 -2
- package/src/inspect.ts +730 -41
- package/src/make/tenancy.ts +2 -1
- package/src/mcp.ts +2 -2
- package/src/templates/agents.ts +14 -2
- package/src/templates/server.ts +4 -1
- package/src/templates/shared.ts +1 -1
package/dist/inspect.js
CHANGED
|
@@ -3,6 +3,7 @@ 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 { renderDatabaseSchema, resolveDatabaseSchemaDialect } from "./db.js";
|
|
6
7
|
import { applyPlannedDoctorFixes, createDoctorFixPlan, planDoctorFixFileChange, publicDoctorFixPlan, } from "./doctor-fixes.js";
|
|
7
8
|
import { formatGithubAnnotation, } from "./github-annotations.js";
|
|
8
9
|
import { createListenersRegistrySource, listenersFilePath, providersFilePath, updateListenersRegistrySource, wireListenersProviderSource, } from "./make/shared.js";
|
|
@@ -104,15 +105,83 @@ async function buildDoctorFixPlan(options = {}) {
|
|
|
104
105
|
const listenerFix = await planUnregisteredListeners(targetDir, files, drift, config);
|
|
105
106
|
if (listenerFix)
|
|
106
107
|
operations.push(listenerFix);
|
|
108
|
+
const databaseFix = await planMissingProviderTables(targetDir, files, config);
|
|
109
|
+
if (databaseFix)
|
|
110
|
+
operations.push(databaseFix);
|
|
111
|
+
const inngestFix = await planUnregisteredInngestJobs(targetDir, files, config);
|
|
112
|
+
if (inngestFix)
|
|
113
|
+
operations.push(inngestFix);
|
|
114
|
+
const nonOverlappingOperations = nonOverlappingDoctorFixOperations(operations);
|
|
115
|
+
operations.splice(0, operations.length, ...nonOverlappingOperations);
|
|
116
|
+
const sourceOverrides = new Map();
|
|
117
|
+
for (const operation of operations) {
|
|
118
|
+
for (const change of operation.changes) {
|
|
119
|
+
sourceOverrides.set(change.file, change.after);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const runtimeManifestFix = await planRuntimeManifestRegistries(targetDir, files, config, convention, sourceOverrides);
|
|
123
|
+
if (runtimeManifestFix) {
|
|
124
|
+
addRuntimeManifestFix(operations, runtimeManifestFix);
|
|
125
|
+
}
|
|
107
126
|
const openApiFix = await planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes);
|
|
108
127
|
if (openApiFix)
|
|
109
128
|
operations.push(openApiFix);
|
|
110
129
|
return createDoctorFixPlan({
|
|
111
130
|
targetDir,
|
|
112
131
|
strict: Boolean(options.strict),
|
|
113
|
-
operations,
|
|
132
|
+
operations: nonOverlappingDoctorFixOperations(operations),
|
|
114
133
|
});
|
|
115
134
|
}
|
|
135
|
+
function nonOverlappingDoctorFixOperations(operations) {
|
|
136
|
+
const claimedFiles = new Set();
|
|
137
|
+
const safeOperations = [];
|
|
138
|
+
for (const operation of operations) {
|
|
139
|
+
if (operation.changes.some((change) => claimedFiles.has(change.file))) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
safeOperations.push(operation);
|
|
143
|
+
for (const change of operation.changes)
|
|
144
|
+
claimedFiles.add(change.file);
|
|
145
|
+
}
|
|
146
|
+
return safeOperations;
|
|
147
|
+
}
|
|
148
|
+
const workflowDoctorFixOperationIds = new Set([
|
|
149
|
+
"schedules.register-missing",
|
|
150
|
+
"tasks.register-missing",
|
|
151
|
+
"workflows.register-missing",
|
|
152
|
+
"outbox.register-missing",
|
|
153
|
+
"listeners.register-missing",
|
|
154
|
+
"inngest.register-missing",
|
|
155
|
+
"runtime-manifest.register-missing",
|
|
156
|
+
]);
|
|
157
|
+
function addRuntimeManifestFix(operations, runtimeManifestFix) {
|
|
158
|
+
const runtimeChange = runtimeManifestFix.changes[0];
|
|
159
|
+
if (!runtimeChange)
|
|
160
|
+
return;
|
|
161
|
+
const overlappingOperation = operations.find((operation) => operation.changes.some((change) => change.file === runtimeChange.file));
|
|
162
|
+
if (!overlappingOperation) {
|
|
163
|
+
operations.push(runtimeManifestFix);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!workflowDoctorFixOperationIds.has(overlappingOperation.id))
|
|
167
|
+
return;
|
|
168
|
+
overlappingOperation.id = "workflows.register-missing";
|
|
169
|
+
overlappingOperation.fixes.push(...runtimeManifestFix.fixes);
|
|
170
|
+
overlappingOperation.changes = overlappingOperation.changes.map((change) => change.file === runtimeChange.file
|
|
171
|
+
? planDoctorFixFileChange({
|
|
172
|
+
file: change.file,
|
|
173
|
+
before: change.before,
|
|
174
|
+
after: runtimeChange.after,
|
|
175
|
+
})
|
|
176
|
+
: change);
|
|
177
|
+
const existingWorkflowOperation = operations.find((operation) => operation !== overlappingOperation &&
|
|
178
|
+
operation.id === "workflows.register-missing");
|
|
179
|
+
if (!existingWorkflowOperation)
|
|
180
|
+
return;
|
|
181
|
+
existingWorkflowOperation.fixes.push(...overlappingOperation.fixes);
|
|
182
|
+
existingWorkflowOperation.changes.push(...overlappingOperation.changes);
|
|
183
|
+
operations.splice(operations.indexOf(overlappingOperation), 1);
|
|
184
|
+
}
|
|
116
185
|
/**
|
|
117
186
|
* Format inspected routes as a CLI table.
|
|
118
187
|
*/
|
|
@@ -1967,23 +2036,23 @@ async function inspectInngestProductionPath(targetDir, files, config) {
|
|
|
1967
2036
|
else {
|
|
1968
2037
|
const registrySource = await readFile(path.join(targetDir, registryFile), "utf8");
|
|
1969
2038
|
const registry = arrayInitializerInfo(registrySource, "inngestJobs");
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
:
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2039
|
+
const featureJobs = (await readFeatureWorkflowRegistries(targetDir, files, config)).filter((entry) => entry.kind === "jobs");
|
|
2040
|
+
const registeredJobs = canonicalRegisteredWorkflowIdentifiers({
|
|
2041
|
+
source: registrySource,
|
|
2042
|
+
registered: registry
|
|
2043
|
+
? identifiersFromArrayExpression(registry.text)
|
|
2044
|
+
: new Set(),
|
|
2045
|
+
centralFile: registryFile,
|
|
2046
|
+
files,
|
|
2047
|
+
registries: featureJobs,
|
|
2048
|
+
});
|
|
2049
|
+
const unregisteredJobs = unregisteredWorkflowRegistries(featureJobs, registeredJobs);
|
|
2050
|
+
for (const { registry: featureRegistry } of unregisteredJobs) {
|
|
1982
2051
|
diagnostics.push({
|
|
1983
2052
|
severity: "warning",
|
|
1984
2053
|
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
1985
2054
|
file: registryFile,
|
|
1986
|
-
message: `${
|
|
2055
|
+
message: `${featureRegistry.indexFile} exports ${featureRegistry.registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${featureRegistry.registryName} so Inngest can execute the job.`,
|
|
1987
2056
|
});
|
|
1988
2057
|
}
|
|
1989
2058
|
}
|
|
@@ -2416,6 +2485,7 @@ async function inspectRuntimeManifestDrift(targetDir, files, config, convention)
|
|
|
2416
2485
|
if (!manifest)
|
|
2417
2486
|
return [];
|
|
2418
2487
|
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
2488
|
+
const manifestSource = await readFile(path.join(targetDir, manifest.file), "utf8");
|
|
2419
2489
|
const diagnostics = [];
|
|
2420
2490
|
const checks = [
|
|
2421
2491
|
{
|
|
@@ -2450,7 +2520,14 @@ async function inspectRuntimeManifestDrift(targetDir, files, config, convention)
|
|
|
2450
2520
|
},
|
|
2451
2521
|
];
|
|
2452
2522
|
for (const check of checks) {
|
|
2453
|
-
const
|
|
2523
|
+
const kindRegistries = registries.filter((registry) => registry.kind === check.kind);
|
|
2524
|
+
const missing = unregisteredWorkflowRegistries(kindRegistries, canonicalRegisteredWorkflowIdentifiers({
|
|
2525
|
+
source: manifestSource,
|
|
2526
|
+
registered: check.registered,
|
|
2527
|
+
centralFile: manifest.file,
|
|
2528
|
+
files,
|
|
2529
|
+
registries: kindRegistries,
|
|
2530
|
+
}));
|
|
2454
2531
|
for (const { registry, missingMembers } of missing) {
|
|
2455
2532
|
for (const member of missingMembers) {
|
|
2456
2533
|
diagnostics.push({
|
|
@@ -2561,6 +2638,30 @@ function unregisteredWorkflowRegistries(registries, registered, memberApplies =
|
|
|
2561
2638
|
}
|
|
2562
2639
|
return unregistered;
|
|
2563
2640
|
}
|
|
2641
|
+
function canonicalRegisteredWorkflowIdentifiers(options) {
|
|
2642
|
+
const canonical = new Set(options.registered);
|
|
2643
|
+
const imports = parseNamedImportSources(options.source);
|
|
2644
|
+
for (const identifier of options.registered) {
|
|
2645
|
+
const imported = imports.get(identifier);
|
|
2646
|
+
if (!imported)
|
|
2647
|
+
continue;
|
|
2648
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, options.centralFile, options.files);
|
|
2649
|
+
if (!importedFile)
|
|
2650
|
+
continue;
|
|
2651
|
+
for (const registry of options.registries) {
|
|
2652
|
+
if (imported.importedName === registry.registryName &&
|
|
2653
|
+
importedFile === registry.indexFile) {
|
|
2654
|
+
canonical.add(registry.registryName);
|
|
2655
|
+
}
|
|
2656
|
+
if (registry.members.includes(imported.importedName) &&
|
|
2657
|
+
(importedFile === registry.indexFile ||
|
|
2658
|
+
registry.memberFiles.get(imported.importedName) === importedFile)) {
|
|
2659
|
+
canonical.add(imported.importedName);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return canonical;
|
|
2664
|
+
}
|
|
2564
2665
|
async function readFeatureWorkflowRegistries(targetDir, files, config) {
|
|
2565
2666
|
const registries = [];
|
|
2566
2667
|
const featuresPath = directoryPath(config.paths.features);
|
|
@@ -2616,6 +2717,7 @@ function exportedMemberFiles(source, indexFile, files) {
|
|
|
2616
2717
|
return memberFiles;
|
|
2617
2718
|
}
|
|
2618
2719
|
async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
2720
|
+
const manifests = [];
|
|
2619
2721
|
for (const file of files) {
|
|
2620
2722
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2621
2723
|
continue;
|
|
@@ -2624,21 +2726,27 @@ async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
|
2624
2726
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2625
2727
|
if (!source.includes("defineRuntimeManifest"))
|
|
2626
2728
|
continue;
|
|
2729
|
+
const calls = runtimeManifestCallMatches(source);
|
|
2730
|
+
if (calls.length === 0)
|
|
2731
|
+
continue;
|
|
2732
|
+
if (calls.length !== 1)
|
|
2733
|
+
return undefined;
|
|
2627
2734
|
const identifiers = runtimeManifestIdentifiers(source);
|
|
2628
2735
|
if (identifiers) {
|
|
2629
|
-
|
|
2736
|
+
manifests.push({
|
|
2630
2737
|
file,
|
|
2631
2738
|
...identifiers,
|
|
2632
|
-
};
|
|
2739
|
+
});
|
|
2633
2740
|
}
|
|
2634
2741
|
}
|
|
2635
|
-
return undefined;
|
|
2742
|
+
return manifests.length === 1 ? manifests[0] : undefined;
|
|
2636
2743
|
}
|
|
2637
2744
|
function runtimeManifestIdentifiers(source) {
|
|
2638
|
-
const
|
|
2639
|
-
if (
|
|
2745
|
+
const calls = runtimeManifestCallMatches(source);
|
|
2746
|
+
if (calls.length !== 1)
|
|
2640
2747
|
return undefined;
|
|
2641
|
-
const
|
|
2748
|
+
const call = calls[0];
|
|
2749
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
2642
2750
|
if (!arg)
|
|
2643
2751
|
return undefined;
|
|
2644
2752
|
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
@@ -2654,6 +2762,12 @@ function runtimeManifestIdentifiers(source) {
|
|
|
2654
2762
|
: new Set(),
|
|
2655
2763
|
};
|
|
2656
2764
|
}
|
|
2765
|
+
function runtimeManifestCallMatches(source) {
|
|
2766
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
2767
|
+
return [
|
|
2768
|
+
...source.matchAll(/\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/g),
|
|
2769
|
+
].filter((match) => match.index !== undefined && codeIndexes.has(match.index));
|
|
2770
|
+
}
|
|
2657
2771
|
function identifiersFromPropertyArray(source, property) {
|
|
2658
2772
|
const value = objectPropertyValueInfo(source, property);
|
|
2659
2773
|
if (!value?.text.startsWith("["))
|
|
@@ -2661,24 +2775,46 @@ function identifiersFromPropertyArray(source, property) {
|
|
|
2661
2775
|
return identifiersFromArrayExpression(value.text);
|
|
2662
2776
|
}
|
|
2663
2777
|
function objectPropertyValueInfo(source, property) {
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2778
|
+
let depth = 0;
|
|
2779
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
2780
|
+
const char = source[index];
|
|
2781
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
2782
|
+
depth++;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
2786
|
+
depth--;
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (depth !== 1 ||
|
|
2790
|
+
source.slice(index, index + property.length) !== property ||
|
|
2791
|
+
/[A-Za-z0-9_$]/.test(source[index - 1] ?? "")) {
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
let colon = index + property.length;
|
|
2795
|
+
if (/[A-Za-z0-9_$]/.test(source[colon] ?? ""))
|
|
2796
|
+
continue;
|
|
2797
|
+
while (colon < source.length && /\s/.test(source[colon]))
|
|
2798
|
+
colon++;
|
|
2799
|
+
if (source[colon] !== ":")
|
|
2800
|
+
continue;
|
|
2801
|
+
let start = colon + 1;
|
|
2802
|
+
while (start < source.length && /\s/.test(source[start]))
|
|
2803
|
+
start++;
|
|
2804
|
+
const open = source[start];
|
|
2805
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
2806
|
+
if (!close)
|
|
2807
|
+
return undefined;
|
|
2808
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
2809
|
+
if (end === -1)
|
|
2810
|
+
return undefined;
|
|
2811
|
+
return {
|
|
2812
|
+
text: source.slice(start, end + 1),
|
|
2813
|
+
start,
|
|
2814
|
+
end: end + 1,
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
return undefined;
|
|
2682
2818
|
}
|
|
2683
2819
|
function registeredWorkflowIdentifiers(source, kind) {
|
|
2684
2820
|
if (kind === "tasks") {
|
|
@@ -3084,8 +3220,9 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
3084
3220
|
const schemaIndexSource = await readFile(path.join(targetDir, schemaIndexFile), "utf8");
|
|
3085
3221
|
for (const schemaFile of schemaFiles) {
|
|
3086
3222
|
const moduleName = path.basename(schemaFile, ".ts");
|
|
3087
|
-
if (schemaIndexSource
|
|
3223
|
+
if (schemaIndexExportKind(schemaIndexSource, `./${moduleName}`)) {
|
|
3088
3224
|
continue;
|
|
3225
|
+
}
|
|
3089
3226
|
diagnostics.push({
|
|
3090
3227
|
severity: "warning",
|
|
3091
3228
|
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
@@ -4276,6 +4413,333 @@ function findRouteGroupContract(contracts, routeGroupContract) {
|
|
|
4276
4413
|
contract.file ===
|
|
4277
4414
|
routeGroupContract.file.replace(/\.ts$/, "/index.ts")));
|
|
4278
4415
|
}
|
|
4416
|
+
const managedDatabaseSchemaMarker = "// Managed by beignet db schema sync.";
|
|
4417
|
+
const databaseSchemaExports = {
|
|
4418
|
+
audit: "auditLog",
|
|
4419
|
+
idempotency: "idempotencyRecords",
|
|
4420
|
+
outbox: "outboxMessages",
|
|
4421
|
+
};
|
|
4422
|
+
async function planMissingProviderTables(targetDir, files, config) {
|
|
4423
|
+
const infrastructurePath = directoryPath(path.dirname(config.paths.portWiring));
|
|
4424
|
+
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
4425
|
+
const output = `${schemaDir}/beignet.ts`;
|
|
4426
|
+
const indexFile = `${schemaDir}/index.ts`;
|
|
4427
|
+
if (!files.includes(indexFile))
|
|
4428
|
+
return undefined;
|
|
4429
|
+
const sourceCache = new Map();
|
|
4430
|
+
const usageFiles = durableDrizzleUsageFiles(files, config);
|
|
4431
|
+
const requiredDefaultTables = [];
|
|
4432
|
+
const missingTables = [];
|
|
4433
|
+
const fixes = [];
|
|
4434
|
+
for (const requirement of durableDrizzleTableRequirements) {
|
|
4435
|
+
const defaultTableName = defaultBeignetConfig.database.tables[requirement.table];
|
|
4436
|
+
if (config.database.tables[requirement.table] !== defaultTableName) {
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
const configuredTables = await configuredDurableDrizzleTableNames(targetDir, usageFiles, config, requirement, sourceCache);
|
|
4440
|
+
if (!configuredTables.has(defaultTableName))
|
|
4441
|
+
continue;
|
|
4442
|
+
requiredDefaultTables.push(requirement.table);
|
|
4443
|
+
if (await durableDrizzleTableExists(targetDir, files, config, infrastructurePath, schemaDir, requirement, defaultTableName, sourceCache)) {
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
missingTables.push(requirement.table);
|
|
4447
|
+
fixes.push({
|
|
4448
|
+
code: requirement.code,
|
|
4449
|
+
file: output,
|
|
4450
|
+
message: `Added the default ${defaultTableName} provider table export to ${output}.`,
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
const outputPath = path.join(targetDir, output);
|
|
4454
|
+
let originalOutput;
|
|
4455
|
+
try {
|
|
4456
|
+
originalOutput = await readFile(outputPath, "utf8");
|
|
4457
|
+
}
|
|
4458
|
+
catch (error) {
|
|
4459
|
+
if (error.code !== "ENOENT")
|
|
4460
|
+
throw error;
|
|
4461
|
+
}
|
|
4462
|
+
const existingTables = originalOutput === undefined
|
|
4463
|
+
? []
|
|
4464
|
+
: managedDatabaseSchemaTables(originalOutput);
|
|
4465
|
+
if (existingTables === undefined)
|
|
4466
|
+
return undefined;
|
|
4467
|
+
const originalIndex = await readFile(path.join(targetDir, indexFile), "utf8");
|
|
4468
|
+
const managedExportKind = schemaIndexExportKind(originalIndex, "./beignet");
|
|
4469
|
+
if (managedExportKind === "named")
|
|
4470
|
+
return undefined;
|
|
4471
|
+
const needsManagedExport = originalOutput !== undefined &&
|
|
4472
|
+
existingTables.some((table) => requiredDefaultTables.includes(table)) &&
|
|
4473
|
+
managedExportKind === undefined;
|
|
4474
|
+
if (missingTables.length === 0 && !needsManagedExport)
|
|
4475
|
+
return undefined;
|
|
4476
|
+
if (needsManagedExport) {
|
|
4477
|
+
fixes.push({
|
|
4478
|
+
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
4479
|
+
file: indexFile,
|
|
4480
|
+
message: `Exported ${output} from ${indexFile}.`,
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
const selectedTables = Object.keys(databaseSchemaExports).filter((table) => existingTables.includes(table) || missingTables.includes(table));
|
|
4484
|
+
let nextOutput = originalOutput;
|
|
4485
|
+
if (missingTables.length > 0) {
|
|
4486
|
+
let dialect;
|
|
4487
|
+
try {
|
|
4488
|
+
dialect = await resolveDatabaseSchemaDialect(targetDir, undefined);
|
|
4489
|
+
}
|
|
4490
|
+
catch {
|
|
4491
|
+
return undefined;
|
|
4492
|
+
}
|
|
4493
|
+
nextOutput = renderDatabaseSchema(dialect, selectedTables);
|
|
4494
|
+
}
|
|
4495
|
+
if (nextOutput === undefined)
|
|
4496
|
+
return undefined;
|
|
4497
|
+
const addedExportNames = missingTables.map((table) => databaseSchemaExports[table]);
|
|
4498
|
+
const otherSchemaFiles = files.filter((file) => file.startsWith(`${schemaDir}/`) &&
|
|
4499
|
+
file.endsWith(".ts") &&
|
|
4500
|
+
file !== indexFile &&
|
|
4501
|
+
file !== output);
|
|
4502
|
+
for (const schemaFile of [indexFile, ...otherSchemaFiles]) {
|
|
4503
|
+
const source = schemaFile === indexFile
|
|
4504
|
+
? originalIndex
|
|
4505
|
+
: await readFile(path.join(targetDir, schemaFile), "utf8");
|
|
4506
|
+
if (addedExportNames.some((name) => exportsIdentifier(source, name))) {
|
|
4507
|
+
return undefined;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
const nextIndex = managedExportKind === "all"
|
|
4511
|
+
? originalIndex
|
|
4512
|
+
: `${originalIndex.trimEnd()}\nexport * from "./beignet";\n`;
|
|
4513
|
+
const changes = [
|
|
4514
|
+
...(originalOutput === nextOutput
|
|
4515
|
+
? []
|
|
4516
|
+
: [
|
|
4517
|
+
planDoctorFixFileChange({
|
|
4518
|
+
file: output,
|
|
4519
|
+
before: originalOutput,
|
|
4520
|
+
after: nextOutput,
|
|
4521
|
+
}),
|
|
4522
|
+
]),
|
|
4523
|
+
...(originalIndex === nextIndex
|
|
4524
|
+
? []
|
|
4525
|
+
: [
|
|
4526
|
+
planDoctorFixFileChange({
|
|
4527
|
+
file: indexFile,
|
|
4528
|
+
before: originalIndex,
|
|
4529
|
+
after: nextIndex,
|
|
4530
|
+
}),
|
|
4531
|
+
]),
|
|
4532
|
+
];
|
|
4533
|
+
if (changes.length === 0)
|
|
4534
|
+
return undefined;
|
|
4535
|
+
return {
|
|
4536
|
+
id: "database.sync-provider-tables",
|
|
4537
|
+
fixes,
|
|
4538
|
+
changes,
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
function schemaIndexExportKind(source, moduleSpecifier) {
|
|
4542
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
4543
|
+
const escaped = escapeRegExp(moduleSpecifier);
|
|
4544
|
+
const allPattern = new RegExp(`\\bexport\\s*\\*\\s*from\\s*["']${escaped}["']`, "g");
|
|
4545
|
+
if ([...source.matchAll(allPattern)].some((match) => match.index !== undefined && codeIndexes.has(match.index))) {
|
|
4546
|
+
return "all";
|
|
4547
|
+
}
|
|
4548
|
+
const customPatterns = [
|
|
4549
|
+
new RegExp(`\\bexport\\s*\\{[\\s\\S]*?\\}\\s*from\\s*["']${escaped}["']`, "g"),
|
|
4550
|
+
new RegExp(`\\bexport\\s*\\*\\s+as\\s+[A-Za-z_$][\\w$]*\\s+from\\s*["']${escaped}["']`, "g"),
|
|
4551
|
+
];
|
|
4552
|
+
return customPatterns.some((pattern) => [...source.matchAll(pattern)].some((match) => match.index !== undefined && codeIndexes.has(match.index)))
|
|
4553
|
+
? "named"
|
|
4554
|
+
: undefined;
|
|
4555
|
+
}
|
|
4556
|
+
function exportsIdentifier(source, identifier) {
|
|
4557
|
+
const executableSource = Array.from({ length: source.length }, () => " ");
|
|
4558
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
4559
|
+
executableSource[index] = source.charAt(index);
|
|
4560
|
+
}
|
|
4561
|
+
const escaped = escapeRegExp(identifier);
|
|
4562
|
+
const patterns = [
|
|
4563
|
+
new RegExp(`\\bexport\\s+(?:const|let|var|class|function)\\s+${escaped}\\b`),
|
|
4564
|
+
new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`),
|
|
4565
|
+
];
|
|
4566
|
+
const code = executableSource.join("");
|
|
4567
|
+
return patterns.some((pattern) => pattern.test(code));
|
|
4568
|
+
}
|
|
4569
|
+
function managedDatabaseSchemaTables(source) {
|
|
4570
|
+
if (!source.startsWith(managedDatabaseSchemaMarker))
|
|
4571
|
+
return undefined;
|
|
4572
|
+
const dialect = /@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)\/schema/.exec(source)?.[1];
|
|
4573
|
+
if (!dialect)
|
|
4574
|
+
return undefined;
|
|
4575
|
+
const tables = Object.entries(databaseSchemaExports)
|
|
4576
|
+
.filter(([, exportName]) => new RegExp(`\\b${escapeRegExp(exportName)}\\b`).test(source))
|
|
4577
|
+
.map(([table]) => table);
|
|
4578
|
+
if (tables.length === 0)
|
|
4579
|
+
return undefined;
|
|
4580
|
+
return renderDatabaseSchema(dialect, tables) === source ? tables : undefined;
|
|
4581
|
+
}
|
|
4582
|
+
async function planUnregisteredInngestJobs(targetDir, files, config) {
|
|
4583
|
+
const packageJson = await readPackageJson(targetDir, files);
|
|
4584
|
+
if (!installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")) {
|
|
4585
|
+
return undefined;
|
|
4586
|
+
}
|
|
4587
|
+
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
4588
|
+
const registryFile = `${serverDir}/inngest.ts`;
|
|
4589
|
+
if (!files.includes(registryFile))
|
|
4590
|
+
return undefined;
|
|
4591
|
+
const source = await readFile(path.join(targetDir, registryFile), "utf8");
|
|
4592
|
+
const registry = arrayInitializerInfo(source, "inngestJobs");
|
|
4593
|
+
if (!registry)
|
|
4594
|
+
return undefined;
|
|
4595
|
+
const featureJobs = (await readFeatureWorkflowRegistries(targetDir, files, config)).filter((entry) => entry.kind === "jobs");
|
|
4596
|
+
const unregistered = unregisteredWorkflowRegistries(featureJobs, canonicalRegisteredWorkflowIdentifiers({
|
|
4597
|
+
source,
|
|
4598
|
+
registered: identifiersFromArrayExpression(registry.text),
|
|
4599
|
+
centralFile: registryFile,
|
|
4600
|
+
files,
|
|
4601
|
+
registries: featureJobs,
|
|
4602
|
+
}));
|
|
4603
|
+
return planWorkflowRegistryOperation("inngest.register-missing", {
|
|
4604
|
+
targetDir,
|
|
4605
|
+
files,
|
|
4606
|
+
centralFile: registryFile,
|
|
4607
|
+
unregistered,
|
|
4608
|
+
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
4609
|
+
listName: "the Inngest jobs array",
|
|
4610
|
+
importSpecifier: (indexFile) => relativeModule(registryFile, indexFile),
|
|
4611
|
+
append: (nextSource, entry, importLine) => appendToNamedArray(nextSource, "inngestJobs", entry, importLine),
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
const runtimeManifestRepairChecks = [
|
|
4615
|
+
{
|
|
4616
|
+
kind: "listeners",
|
|
4617
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
4618
|
+
listName: "the runtime manifest listeners array",
|
|
4619
|
+
},
|
|
4620
|
+
{
|
|
4621
|
+
kind: "schedules",
|
|
4622
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
4623
|
+
listName: "the runtime manifest schedules array",
|
|
4624
|
+
},
|
|
4625
|
+
{
|
|
4626
|
+
kind: "tasks",
|
|
4627
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
4628
|
+
listName: "the runtime manifest tasks array",
|
|
4629
|
+
},
|
|
4630
|
+
{
|
|
4631
|
+
kind: "events",
|
|
4632
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
4633
|
+
listName: "the runtime manifest outbox events array",
|
|
4634
|
+
},
|
|
4635
|
+
{
|
|
4636
|
+
kind: "jobs",
|
|
4637
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
4638
|
+
listName: "the runtime manifest outbox jobs array",
|
|
4639
|
+
},
|
|
4640
|
+
];
|
|
4641
|
+
async function planRuntimeManifestRegistries(targetDir, files, config, convention, sourceOverrides = new Map()) {
|
|
4642
|
+
if (!convention.resourceGenerator)
|
|
4643
|
+
return undefined;
|
|
4644
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
4645
|
+
if (!manifest)
|
|
4646
|
+
return undefined;
|
|
4647
|
+
const original = sourceOverrides.get(manifest.file) ??
|
|
4648
|
+
(await readFile(path.join(targetDir, manifest.file), "utf8"));
|
|
4649
|
+
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
4650
|
+
let next = original;
|
|
4651
|
+
const fixes = [];
|
|
4652
|
+
for (const check of runtimeManifestRepairChecks) {
|
|
4653
|
+
const parsedRegistered = runtimeManifestIdentifiers(next)?.[runtimeManifestIdentifierKey(check.kind)];
|
|
4654
|
+
if (!parsedRegistered)
|
|
4655
|
+
continue;
|
|
4656
|
+
const kindRegistries = registries.filter((registry) => registry.kind === check.kind);
|
|
4657
|
+
const registered = canonicalRegisteredWorkflowIdentifiers({
|
|
4658
|
+
source: next,
|
|
4659
|
+
registered: parsedRegistered,
|
|
4660
|
+
centralFile: manifest.file,
|
|
4661
|
+
files,
|
|
4662
|
+
registries: kindRegistries,
|
|
4663
|
+
});
|
|
4664
|
+
const edit = planWorkflowRegistryEdit({
|
|
4665
|
+
files,
|
|
4666
|
+
centralFile: manifest.file,
|
|
4667
|
+
source: next,
|
|
4668
|
+
unregistered: unregisteredWorkflowRegistries(kindRegistries, registered),
|
|
4669
|
+
code: check.code,
|
|
4670
|
+
listName: check.listName,
|
|
4671
|
+
importSpecifier: (indexFile) => relativeModule(manifest.file, indexFile),
|
|
4672
|
+
append: (nextSource, entry, importLine) => appendToRuntimeManifestArray(nextSource, check.kind, entry, importLine),
|
|
4673
|
+
});
|
|
4674
|
+
if (!edit)
|
|
4675
|
+
continue;
|
|
4676
|
+
next = edit.after;
|
|
4677
|
+
fixes.push(edit.fix);
|
|
4678
|
+
}
|
|
4679
|
+
if (fixes.length === 0 || next === original)
|
|
4680
|
+
return undefined;
|
|
4681
|
+
return {
|
|
4682
|
+
id: "runtime-manifest.register-missing",
|
|
4683
|
+
fixes,
|
|
4684
|
+
changes: [
|
|
4685
|
+
planDoctorFixFileChange({
|
|
4686
|
+
file: manifest.file,
|
|
4687
|
+
before: original,
|
|
4688
|
+
after: next,
|
|
4689
|
+
}),
|
|
4690
|
+
],
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
function runtimeManifestIdentifierKey(kind) {
|
|
4694
|
+
return kind;
|
|
4695
|
+
}
|
|
4696
|
+
function runtimeManifestArrayInfo(source, kind) {
|
|
4697
|
+
const calls = runtimeManifestCallMatches(source);
|
|
4698
|
+
if (calls.length !== 1)
|
|
4699
|
+
return undefined;
|
|
4700
|
+
const call = calls[0];
|
|
4701
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
4702
|
+
if (!arg)
|
|
4703
|
+
return undefined;
|
|
4704
|
+
if (kind === "listeners" || kind === "schedules" || kind === "tasks") {
|
|
4705
|
+
const value = objectPropertyValueInfo(arg.text, kind);
|
|
4706
|
+
return value
|
|
4707
|
+
? {
|
|
4708
|
+
text: value.text,
|
|
4709
|
+
start: arg.start + value.start,
|
|
4710
|
+
end: arg.start + value.end,
|
|
4711
|
+
}
|
|
4712
|
+
: undefined;
|
|
4713
|
+
}
|
|
4714
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
4715
|
+
if (!outbox)
|
|
4716
|
+
return undefined;
|
|
4717
|
+
const value = objectPropertyValueInfo(outbox.text, kind);
|
|
4718
|
+
return value
|
|
4719
|
+
? {
|
|
4720
|
+
text: value.text,
|
|
4721
|
+
start: arg.start + outbox.start + value.start,
|
|
4722
|
+
end: arg.start + outbox.start + value.end,
|
|
4723
|
+
}
|
|
4724
|
+
: undefined;
|
|
4725
|
+
}
|
|
4726
|
+
function appendToRuntimeManifestArray(source, kind, entry, importLine) {
|
|
4727
|
+
const value = runtimeManifestArrayInfo(source, kind);
|
|
4728
|
+
if (!value?.text.startsWith("["))
|
|
4729
|
+
return { kind: "missing" };
|
|
4730
|
+
const entryName = entry.replace(/^\.{3}/, "").trim();
|
|
4731
|
+
if (identifiersFromArrayExpression(value.text).has(entryName)) {
|
|
4732
|
+
return { kind: "unchanged" };
|
|
4733
|
+
}
|
|
4734
|
+
const nextArray = appendToArrayExpression(value.text, [entry]);
|
|
4735
|
+
let next = `${source.slice(0, value.start)}${nextArray}${source.slice(value.end)}`;
|
|
4736
|
+
if (importLine && !next.includes(importLine)) {
|
|
4737
|
+
next = insertAfterImports(next, importLine);
|
|
4738
|
+
}
|
|
4739
|
+
return next === source
|
|
4740
|
+
? { kind: "unchanged" }
|
|
4741
|
+
: { kind: "updated", source: next };
|
|
4742
|
+
}
|
|
4279
4743
|
async function planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes) {
|
|
4280
4744
|
const routePath = path.join(targetDir, config.paths.openapiRoute);
|
|
4281
4745
|
let source;
|
|
@@ -4679,7 +5143,8 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4679
5143
|
let next = options.source;
|
|
4680
5144
|
const registeredNames = [];
|
|
4681
5145
|
for (const { registry } of candidates) {
|
|
4682
|
-
const
|
|
5146
|
+
const imports = parseNamedImportSources(next);
|
|
5147
|
+
const imported = imports.get(registry.registryName);
|
|
4683
5148
|
let importLine;
|
|
4684
5149
|
if (imported) {
|
|
4685
5150
|
const importedFile = sourceFileFromImport(imported.sourcePath, options.centralFile, options.files);
|
|
@@ -4687,6 +5152,14 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4687
5152
|
return undefined;
|
|
4688
5153
|
}
|
|
4689
5154
|
else {
|
|
5155
|
+
const aliasesRegistry = [...imports.entries()].some(([localName, candidate]) => localName !== registry.registryName &&
|
|
5156
|
+
candidate.importedName === registry.registryName &&
|
|
5157
|
+
sourceFileFromImport(candidate.sourcePath, options.centralFile, options.files) === registry.indexFile);
|
|
5158
|
+
if (aliasesRegistry)
|
|
5159
|
+
return undefined;
|
|
5160
|
+
if (sourceContainsCodeIdentifier(next, registry.registryName)) {
|
|
5161
|
+
return undefined;
|
|
5162
|
+
}
|
|
4690
5163
|
importLine = `import { ${registry.registryName} } from "${options.importSpecifier(registry.indexFile)}";`;
|
|
4691
5164
|
}
|
|
4692
5165
|
const result = options.append(next, `...${registry.registryName}`, importLine);
|
|
@@ -4708,6 +5181,12 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4708
5181
|
},
|
|
4709
5182
|
};
|
|
4710
5183
|
}
|
|
5184
|
+
function sourceContainsCodeIdentifier(source, identifier) {
|
|
5185
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
5186
|
+
const pattern = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(identifier)}(?![A-Za-z0-9_$])`, "g");
|
|
5187
|
+
return [...source.matchAll(pattern)].some((match) => match.index !== undefined &&
|
|
5188
|
+
codeIndexes.has(match.index + (match[1]?.length ?? 0)));
|
|
5189
|
+
}
|
|
4711
5190
|
function routeRegistryFileFromServerSource(source, serverFile, files) {
|
|
4712
5191
|
const imports = parseNamedImportSources(source);
|
|
4713
5192
|
for (const identifier of routeOptionIdentifiers(source)) {
|