@beignet/cli 0.0.39 → 0.0.41

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 (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +110 -16
  3. package/dist/analysis/source-index.d.ts +38 -0
  4. package/dist/analysis/source-index.d.ts.map +1 -0
  5. package/dist/analysis/source-index.js +271 -0
  6. package/dist/analysis/source-index.js.map +1 -0
  7. package/dist/analysis/workspace.d.ts +16 -0
  8. package/dist/analysis/workspace.d.ts.map +1 -0
  9. package/dist/analysis/workspace.js +134 -0
  10. package/dist/analysis/workspace.js.map +1 -0
  11. package/dist/app-map-schema.d.ts +5 -0
  12. package/dist/app-map-schema.d.ts.map +1 -0
  13. package/dist/app-map-schema.js +26 -0
  14. package/dist/app-map-schema.js.map +1 -0
  15. package/dist/app-map.d.ts +96 -0
  16. package/dist/app-map.d.ts.map +1 -0
  17. package/dist/app-map.js +1141 -0
  18. package/dist/app-map.js.map +1 -0
  19. package/dist/check.d.ts +7 -0
  20. package/dist/check.d.ts.map +1 -1
  21. package/dist/check.js +20 -5
  22. package/dist/check.js.map +1 -1
  23. package/dist/config.d.ts +1 -1
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +4 -2
  26. package/dist/config.js.map +1 -1
  27. package/dist/db.d.ts +32 -7
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +182 -14
  30. package/dist/db.js.map +1 -1
  31. package/dist/explain.d.ts +98 -0
  32. package/dist/explain.d.ts.map +1 -0
  33. package/dist/explain.js +512 -0
  34. package/dist/explain.js.map +1 -0
  35. package/dist/framework.d.ts +3 -0
  36. package/dist/framework.d.ts.map +1 -0
  37. package/dist/framework.js +6 -0
  38. package/dist/framework.js.map +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +79 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/inspect.d.ts +4 -2
  43. package/dist/inspect.d.ts.map +1 -1
  44. package/dist/inspect.js +173 -36
  45. package/dist/inspect.js.map +1 -1
  46. package/dist/lib.d.ts +4 -0
  47. package/dist/lib.d.ts.map +1 -1
  48. package/dist/lib.js +2 -0
  49. package/dist/lib.js.map +1 -1
  50. package/dist/make/payments.d.ts.map +1 -1
  51. package/dist/make/payments.js +2 -0
  52. package/dist/make/payments.js.map +1 -1
  53. package/dist/make.d.ts.map +1 -1
  54. package/dist/make.js +157 -3
  55. package/dist/make.js.map +1 -1
  56. package/dist/mcp.d.ts +1 -1
  57. package/dist/mcp.d.ts.map +1 -1
  58. package/dist/mcp.js +69 -1
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/preflight.d.ts.map +1 -1
  61. package/dist/preflight.js +10 -0
  62. package/dist/preflight.js.map +1 -1
  63. package/dist/provider-add.d.ts.map +1 -1
  64. package/dist/provider-add.js +167 -13
  65. package/dist/provider-add.js.map +1 -1
  66. package/dist/provider-audit.d.ts +6 -0
  67. package/dist/provider-audit.d.ts.map +1 -1
  68. package/dist/provider-audit.js +63 -15
  69. package/dist/provider-audit.js.map +1 -1
  70. package/dist/templates/agents.d.ts.map +1 -1
  71. package/dist/templates/agents.js +17 -4
  72. package/dist/templates/agents.js.map +1 -1
  73. package/dist/templates/base.d.ts.map +1 -1
  74. package/dist/templates/base.js +10 -4
  75. package/dist/templates/base.js.map +1 -1
  76. package/dist/templates/index.d.ts +1 -1
  77. package/dist/templates/index.d.ts.map +1 -1
  78. package/dist/templates/index.js +8 -1
  79. package/dist/templates/index.js.map +1 -1
  80. package/dist/templates/server.d.ts +3 -0
  81. package/dist/templates/server.d.ts.map +1 -1
  82. package/dist/templates/server.js +55 -1
  83. package/dist/templates/server.js.map +1 -1
  84. package/dist/templates/shadcn.js +1 -1
  85. package/dist/templates/shared.js +1 -1
  86. package/package.json +2 -2
  87. package/skills/app-structure/SKILL.md +38 -5
  88. package/src/analysis/source-index.ts +395 -0
  89. package/src/analysis/workspace.ts +180 -0
  90. package/src/app-map-schema.ts +28 -0
  91. package/src/app-map.ts +1705 -0
  92. package/src/check.ts +27 -4
  93. package/src/config.ts +7 -3
  94. package/src/db.ts +232 -14
  95. package/src/explain.ts +786 -0
  96. package/src/framework.ts +13 -0
  97. package/src/index.ts +113 -2
  98. package/src/inspect.ts +259 -40
  99. package/src/lib.ts +36 -0
  100. package/src/make/payments.ts +7 -0
  101. package/src/make.ts +232 -2
  102. package/src/mcp.ts +107 -1
  103. package/src/preflight.ts +14 -0
  104. package/src/provider-add.ts +211 -12
  105. package/src/provider-audit.ts +127 -22
  106. package/src/templates/agents.ts +17 -4
  107. package/src/templates/base.ts +22 -4
  108. package/src/templates/index.ts +18 -1
  109. package/src/templates/server.ts +58 -1
  110. package/src/templates/shadcn.ts +1 -1
  111. package/src/templates/shared.ts +1 -1
  112. package/src/test-helpers/generated-app.ts +1 -1
@@ -0,0 +1,13 @@
1
+ import type { ResolvedBeignetConfig } from "./config.js";
2
+
3
+ export function assertNextFramework(
4
+ config: ResolvedBeignetConfig,
5
+ command: string,
6
+ generatedSurface: string,
7
+ ): void {
8
+ if (config.framework === "next") return;
9
+
10
+ throw new Error(
11
+ `${command} currently supports framework: "next" because it generates ${generatedSurface}. For framework: "web", define the framework-neutral artifact directly and mount its HTTP entrypoint through @beignet/web.`,
12
+ );
13
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  type StricliProcess,
14
14
  text_en,
15
15
  } from "@stricli/core";
16
+ import { type AppMapNodeKind, appMapNodeKinds } from "./app-map-schema.js";
16
17
  import {
17
18
  type CompletionShell,
18
19
  type CreateProviderName,
@@ -36,6 +37,7 @@ import {
36
37
  } from "./choices.js";
37
38
  // Type-only: the runtime module stays behind the create command's lazy loader.
38
39
  import type { CreateSelections } from "./create-prompts.js";
40
+ import type { ExplainTargetKind } from "./explain.js";
39
41
  import type { AddProviderPresetResult } from "./provider-add.js";
40
42
  import { getCliVersion } from "./version.js";
41
43
 
@@ -106,6 +108,13 @@ type RoutesFlags = {
106
108
  cwd?: string;
107
109
  };
108
110
 
111
+ type MapFlags = RoutesFlags & {
112
+ feature?: string;
113
+ kind?: readonly AppMapNodeKind[];
114
+ };
115
+
116
+ type ExplainFlags = RoutesFlags;
117
+
109
118
  type LintFlags = {
110
119
  json?: boolean;
111
120
  cwd?: string;
@@ -366,6 +375,21 @@ const routesFlagParameters = {
366
375
  cwd: cwdFlag,
367
376
  } satisfies FlagParametersForType<RoutesFlags, CliContext>;
368
377
 
378
+ const mapFlagParameters = {
379
+ json: jsonFlag,
380
+ feature: parsedStringFlag(
381
+ "Project one feature and its direct relationships.",
382
+ ),
383
+ kind: {
384
+ kind: "enum",
385
+ values: appMapNodeKinds,
386
+ optional: true,
387
+ variadic: ",",
388
+ brief: "Include only these node kinds. Accepts comma-separated values.",
389
+ },
390
+ cwd: cwdFlag,
391
+ } satisfies FlagParametersForType<MapFlags, CliContext>;
392
+
369
393
  const lintFlagParameters = {
370
394
  json: jsonFlag,
371
395
  cwd: cwdFlag,
@@ -585,6 +609,19 @@ const providerPresetPositional = {
585
609
  ],
586
610
  } as const;
587
611
 
612
+ function explainTargetPositional(kind: ExplainTargetKind) {
613
+ return {
614
+ kind: "tuple",
615
+ parameters: [
616
+ {
617
+ parse: parseString,
618
+ placeholder: kind,
619
+ brief: `${kind[0]?.toUpperCase()}${kind.slice(1)} to explain.`,
620
+ },
621
+ ],
622
+ } as const;
623
+ }
624
+
588
625
  function resolveOutputFormat(flags: {
589
626
  json?: boolean;
590
627
  format?: OutputFormat;
@@ -829,6 +866,77 @@ const routesCommand = buildCommand<RoutesFlags, [], CliContext>({
829
866
  },
830
867
  });
831
868
 
869
+ const mapCommand = buildCommand<MapFlags, [], CliContext>({
870
+ docs: {
871
+ brief: "Map the app's architecture and registered workflows.",
872
+ fullDescription:
873
+ "Build a deterministic graph of features, HTTP contracts, routes, use cases, policies, workflows, ports, providers, OpenAPI, tests, dependencies, and doctor findings.",
874
+ },
875
+ parameters: {
876
+ flags: mapFlagParameters,
877
+ },
878
+ loader: async () => {
879
+ const { formatAppMap, mapApp, projectAppMap } = await import(
880
+ "./app-map.js"
881
+ );
882
+
883
+ return async function runMap(this: CliContext, flags: MapFlags) {
884
+ const result = projectAppMap(
885
+ await mapApp({ cwd: flags.cwd, strict: true }),
886
+ {
887
+ feature: flags.feature,
888
+ kinds: flags.kind,
889
+ },
890
+ );
891
+ writeOutput(
892
+ this,
893
+ flags.json ? JSON.stringify(result, null, 2) : formatAppMap(result),
894
+ );
895
+ };
896
+ },
897
+ });
898
+
899
+ function explainCommand(kind: ExplainTargetKind) {
900
+ return buildCommand<ExplainFlags, [string], CliContext>({
901
+ docs: {
902
+ brief: `Explain one mapped Beignet ${kind}.`,
903
+ fullDescription:
904
+ "Returns source-backed relationships, conventions, findings, suggested files, and follow-up commands without generating advice or changing the app.",
905
+ },
906
+ parameters: {
907
+ flags: routesFlagParameters,
908
+ positional: explainTargetPositional(kind),
909
+ },
910
+ loader: async () => {
911
+ const { explainApp, formatExplain } = await import("./explain.js");
912
+
913
+ return async function runExplain(
914
+ this: CliContext,
915
+ flags: ExplainFlags,
916
+ target: string,
917
+ ) {
918
+ const result = await explainApp({ kind, target, cwd: flags.cwd });
919
+ writeOutput(
920
+ this,
921
+ flags.json ? JSON.stringify(result, null, 2) : formatExplain(result),
922
+ );
923
+ };
924
+ },
925
+ });
926
+ }
927
+
928
+ const explainRoutes = buildRouteMap({
929
+ docs: {
930
+ brief: "Explain mapped Beignet app concepts with source evidence.",
931
+ },
932
+ routes: {
933
+ diagnostic: explainCommand("diagnostic"),
934
+ feature: explainCommand("feature"),
935
+ provider: explainCommand("provider"),
936
+ route: explainCommand("route"),
937
+ },
938
+ });
939
+
832
940
  const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
833
941
  docs: {
834
942
  brief: "Inspect app wiring and framework conventions.",
@@ -1035,7 +1143,7 @@ const lintCommand = buildCommand<LintFlags, [], CliContext>({
1035
1143
  const mcpCommand = buildCommand<McpFlags, [], CliContext>({
1036
1144
  docs: {
1037
1145
  brief:
1038
- "Run a Model Context Protocol server exposing this app's routes, doctor, lint, and generators over stdio.",
1146
+ "Run a Model Context Protocol server exposing the app map, validation, and generators over stdio.",
1039
1147
  },
1040
1148
  parameters: {
1041
1149
  flags: {
@@ -2109,7 +2217,7 @@ const makeRoutes = buildRouteMap({
2109
2217
  const rootRoutes = buildRouteMap({
2110
2218
  docs: {
2111
2219
  brief: "Beignet CLI",
2112
- fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
2220
+ fullDescription: `Create apps, generate framework files, map and explain architecture, inspect routes, and check Beignet conventions.
2113
2221
 
2114
2222
  Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
2115
2223
  },
@@ -2119,7 +2227,9 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
2119
2227
  create: createCommand,
2120
2228
  db: dbRoutes,
2121
2229
  doctor: doctorCommand,
2230
+ explain: explainRoutes,
2122
2231
  lint: lintCommand,
2232
+ map: mapCommand,
2123
2233
  make: makeRoutes,
2124
2234
  mcp: mcpCommand,
2125
2235
  outbox: outboxRoutes,
@@ -2431,6 +2541,7 @@ ${openStep}
2431
2541
 
2432
2542
  Inspect the app:
2433
2543
  ${cli} routes
2544
+ ${cli} map
2434
2545
  ${cli} check
2435
2546
 
2436
2547
  Generate a feature:
package/src/inspect.ts CHANGED
@@ -105,7 +105,7 @@ type InspectedCatalogErrorRef = {
105
105
  export type InspectedRoute = InspectedContract & {
106
106
  handlerFile?: string;
107
107
  handlerExport?: HttpMethod;
108
- handlerSource: "next-route" | "route-local" | "missing";
108
+ handlerSource: "next-route" | "route-local" | "web-server" | "missing";
109
109
  };
110
110
 
111
111
  /**
@@ -132,11 +132,13 @@ export type InspectFix = {
132
132
  * Detected app convention and missing convention files.
133
133
  */
134
134
  export type InspectConvention = {
135
- kind: "standard" | "next" | "custom";
135
+ kind: "standard" | "next" | "web" | "custom";
136
136
  nextLayout: boolean;
137
+ webLayout: boolean;
137
138
  resourceGenerator: boolean;
138
139
  configFile?: string;
139
140
  missingNextLayout: string[];
141
+ missingWebLayout: string[];
140
142
  missingResourceGenerator: string[];
141
143
  };
142
144
 
@@ -204,13 +206,12 @@ export async function inspectApp(
204
206
  : await loadBeignetConfig(targetDir, files);
205
207
  const convention = inspectConvention(files, config);
206
208
  const contracts = await readContracts(targetDir, files, config);
207
- const routeFiles = files.filter(
208
- (file) =>
209
- file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
210
- file.endsWith("/route.ts"),
209
+ const matchedRoutes = await inspectRouteSurface(
210
+ targetDir,
211
+ files,
212
+ config,
213
+ contracts,
211
214
  );
212
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
213
- const matchedRoutes = matchRoutes(contracts, routeExports);
214
215
  const diagnostics = await inspectDiagnostics(
215
216
  targetDir,
216
217
  files,
@@ -246,13 +247,12 @@ export async function applyDoctorFixes(
246
247
  : await loadBeignetConfig(targetDir, files);
247
248
  const convention = inspectConvention(files, config);
248
249
  const contracts = await readContracts(targetDir, files, config);
249
- const routeFiles = files.filter(
250
- (file) =>
251
- file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
252
- file.endsWith("/route.ts"),
250
+ const matchedRoutes = await inspectRouteSurface(
251
+ targetDir,
252
+ files,
253
+ config,
254
+ contracts,
253
255
  );
254
- const routeExports = await readRouteExports(targetDir, routeFiles, config);
255
- const matchedRoutes = matchRoutes(contracts, routeExports);
256
256
  const fixes: InspectFix[] = [];
257
257
 
258
258
  const packageFix = await fixPackageScripts(targetDir, files, convention);
@@ -318,7 +318,9 @@ export function formatRoutes(result: InspectAppResult): string {
318
318
  handler:
319
319
  route.handlerSource === "missing"
320
320
  ? "missing"
321
- : `${route.handlerFile ?? "unknown"}:${route.handlerExport ?? route.method}`,
321
+ : route.handlerSource === "web-server"
322
+ ? `${route.handlerFile ?? "unknown"}:registry`
323
+ : `${route.handlerFile ?? "unknown"}:${route.handlerExport ?? route.method}`,
322
324
  }));
323
325
 
324
326
  return table([
@@ -556,6 +558,10 @@ function inspectConvention(
556
558
  `${directoryPath(config.paths.routes)}/`,
557
559
  config.paths.server,
558
560
  ]);
561
+ const missingWebLayout = missingRequirements(files, [
562
+ `${directoryPath(config.paths.contracts)}/`,
563
+ config.paths.server,
564
+ ]);
559
565
  const missingResourceGenerator = missingRequirements(files, [
560
566
  config.paths.appContext,
561
567
  config.paths.portWiring,
@@ -564,14 +570,23 @@ function inspectConvention(
564
570
  config.paths.useCaseBuilder,
565
571
  ]);
566
572
  const nextLayout = missingNextLayout.length === 0;
573
+ const webLayout = missingWebLayout.length === 0;
567
574
  const resourceGenerator = missingResourceGenerator.length === 0;
568
575
 
569
576
  return {
570
- kind: resourceGenerator ? "standard" : nextLayout ? "next" : "custom",
577
+ kind: resourceGenerator
578
+ ? "standard"
579
+ : config.framework === "web" && webLayout
580
+ ? "web"
581
+ : nextLayout
582
+ ? "next"
583
+ : "custom",
571
584
  nextLayout,
585
+ webLayout,
572
586
  resourceGenerator,
573
587
  configFile: config.configFile,
574
588
  missingNextLayout,
589
+ missingWebLayout,
575
590
  missingResourceGenerator,
576
591
  };
577
592
  }
@@ -603,7 +618,7 @@ function parseContracts(source: string, file: string): InspectedContract[] {
603
618
  for (const exportMatch of source.matchAll(exportRegex)) {
604
619
  const block = exportMatch[2];
605
620
  const methodMatch =
606
- /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["']\s*\)/.exec(
621
+ /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["'](?:\s*,\s*["'][^"']+["'])?\s*\)/.exec(
607
622
  block,
608
623
  );
609
624
  if (methodMatch) {
@@ -1289,6 +1304,73 @@ function matchRoutes(
1289
1304
  };
1290
1305
  }
1291
1306
 
1307
+ async function inspectRouteSurface(
1308
+ targetDir: string,
1309
+ files: string[],
1310
+ config: ResolvedBeignetConfig,
1311
+ contracts: InspectedContract[],
1312
+ ): Promise<MatchedRoutesResult> {
1313
+ if (config.framework === "next") {
1314
+ const routeFiles = files.filter(
1315
+ (file) =>
1316
+ file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
1317
+ file.endsWith("/route.ts"),
1318
+ );
1319
+ const routeExports = await readRouteExports(targetDir, routeFiles, config);
1320
+ return matchRoutes(contracts, routeExports);
1321
+ }
1322
+
1323
+ if (!files.includes(config.paths.server)) {
1324
+ return {
1325
+ routes: contracts.map((contract) => ({
1326
+ ...contract,
1327
+ handlerSource: "missing",
1328
+ })),
1329
+ unmatchedRouteHandlers: [],
1330
+ };
1331
+ }
1332
+
1333
+ const serverSource = await readFile(
1334
+ path.join(targetDir, config.paths.server),
1335
+ "utf8",
1336
+ );
1337
+ const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
1338
+ const registeredGroups = await registeredRouteGroupsForServer(
1339
+ targetDir,
1340
+ files,
1341
+ config,
1342
+ serverSource,
1343
+ );
1344
+ const registeredContracts = contractsForRegisteredRouteGroups(
1345
+ contracts,
1346
+ routeGroups,
1347
+ registeredGroups,
1348
+ );
1349
+ const registeredKeys = new Set(registeredContracts.map(contractKey));
1350
+ const handlerFile =
1351
+ routeRegistryFileFromServerSource(
1352
+ serverSource,
1353
+ config.paths.server,
1354
+ files,
1355
+ ) ?? config.paths.server;
1356
+
1357
+ return {
1358
+ routes: contracts.map((contract) =>
1359
+ registeredKeys.has(contractKey(contract))
1360
+ ? {
1361
+ ...contract,
1362
+ handlerFile,
1363
+ handlerSource: "web-server" as const,
1364
+ }
1365
+ : {
1366
+ ...contract,
1367
+ handlerSource: "missing" as const,
1368
+ },
1369
+ ),
1370
+ unmatchedRouteHandlers: [],
1371
+ };
1372
+ }
1373
+
1292
1374
  function findContract(
1293
1375
  contracts: InspectedContract[],
1294
1376
  routeExport: RouteExport,
@@ -1349,28 +1431,53 @@ async function inspectDiagnostics(
1349
1431
  strict: boolean,
1350
1432
  ): Promise<InspectDiagnostic[]> {
1351
1433
  const diagnostics: InspectDiagnostic[] = [];
1352
-
1353
- if (!convention.nextLayout) {
1434
+ const frameworkLayout =
1435
+ config.framework === "next" ? convention.nextLayout : convention.webLayout;
1436
+ const missingFrameworkLayout =
1437
+ config.framework === "next"
1438
+ ? convention.missingNextLayout
1439
+ : convention.missingWebLayout;
1440
+
1441
+ if (!frameworkLayout) {
1442
+ const expectedLayout =
1443
+ config.framework === "next"
1444
+ ? `${directoryPath(config.paths.contracts)}/, ${directoryPath(config.paths.routes)}/, and ${config.paths.server}`
1445
+ : `${directoryPath(config.paths.contracts)}/ and ${config.paths.server}`;
1446
+ const layoutName =
1447
+ config.framework === "next"
1448
+ ? "standard Beignet app layout"
1449
+ : "standard Beignet web app layout";
1354
1450
  diagnostics.push({
1355
1451
  severity: "warning",
1356
1452
  code: "BEIGNET_APP_LAYOUT_NOT_FOUND",
1357
- message: `This directory does not match the standard Beignet app layout. CLI inspection expects ${directoryPath(config.paths.contracts)}/, ${directoryPath(config.paths.routes)}/, and ${config.paths.server}. Missing: ${convention.missingNextLayout.join(", ")}. Add the missing app files or configure their paths in beignet.config.ts.`,
1453
+ message: `This directory does not match the ${layoutName}. CLI inspection expects ${expectedLayout}. Missing: ${missingFrameworkLayout.join(", ")}. Add the missing app files or configure their paths in beignet.config.ts.`,
1358
1454
  });
1359
1455
  }
1360
1456
 
1361
- for (const route of matchedRoutes.routes) {
1362
- if (route.handlerSource !== "missing") continue;
1363
- diagnostics.push({
1364
- severity: "error",
1365
- code: "BEIGNET_ROUTE_MISSING",
1366
- file: route.file,
1367
- contract: route.exportName,
1368
- message: `${route.exportName} (${route.method} ${route.path}) has no matching Next route handler. Add ${methodArticle(route.method)} ${route.method} export to ${directoryPath(config.paths.routes)}/[[...path]]/route.ts or a matching route file, or remove the unused contract.`,
1369
- });
1457
+ diagnostics.push(
1458
+ ...(await inspectFrameworkAdapter(targetDir, files, config)),
1459
+ );
1460
+
1461
+ if (config.framework === "next" || !convention.resourceGenerator) {
1462
+ for (const route of matchedRoutes.routes) {
1463
+ if (route.handlerSource !== "missing") continue;
1464
+ diagnostics.push({
1465
+ severity: "error",
1466
+ code: "BEIGNET_ROUTE_MISSING",
1467
+ file: route.file,
1468
+ contract: route.exportName,
1469
+ message:
1470
+ config.framework === "next"
1471
+ ? `${route.exportName} (${route.method} ${route.path}) has no matching Next route handler. Add ${methodArticle(route.method)} ${route.method} export to ${directoryPath(config.paths.routes)}/[[...path]]/route.ts or a matching route file, or remove the unused contract.`
1472
+ : `${route.exportName} (${route.method} ${route.path}) is not registered in the route surface passed to createFetchServer(...). Register the contract through defineRoutes(...) or remove the unused contract.`,
1473
+ });
1474
+ }
1370
1475
  }
1371
1476
 
1372
- for (const routeHandler of matchedRoutes.unmatchedRouteHandlers) {
1373
- diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
1477
+ if (config.framework === "next") {
1478
+ for (const routeHandler of matchedRoutes.unmatchedRouteHandlers) {
1479
+ diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
1480
+ }
1374
1481
  }
1375
1482
 
1376
1483
  diagnostics.push(
@@ -1414,6 +1521,7 @@ async function inspectDiagnostics(
1414
1521
  config,
1415
1522
  convention,
1416
1523
  )),
1524
+ ...(await inspectInngestProductionPath(targetDir, files, config)),
1417
1525
  ...(await inspectDatabaseLifecycleDrift(
1418
1526
  targetDir,
1419
1527
  files,
@@ -1460,6 +1568,39 @@ async function inspectDiagnostics(
1460
1568
  return dedupeDiagnostics(diagnostics);
1461
1569
  }
1462
1570
 
1571
+ async function inspectFrameworkAdapter(
1572
+ targetDir: string,
1573
+ files: string[],
1574
+ config: ResolvedBeignetConfig,
1575
+ ): Promise<InspectDiagnostic[]> {
1576
+ if (!files.includes(config.paths.server)) return [];
1577
+
1578
+ const source = await readFile(
1579
+ path.join(targetDir, config.paths.server),
1580
+ "utf8",
1581
+ );
1582
+ const expectedFactory =
1583
+ config.framework === "next" ? "createNextServer" : "createFetchServer";
1584
+ const otherFactory =
1585
+ config.framework === "next" ? "createFetchServer" : "createNextServer";
1586
+
1587
+ if (
1588
+ containsCallExpression(source, expectedFactory) ||
1589
+ !containsCallExpression(source, otherFactory)
1590
+ ) {
1591
+ return [];
1592
+ }
1593
+
1594
+ return [
1595
+ {
1596
+ severity: "error",
1597
+ code: "BEIGNET_FRAMEWORK_MISMATCH",
1598
+ file: config.paths.server,
1599
+ message: `${config.configFile ?? "beignet.config.*"} selects framework: "${config.framework}", but ${config.paths.server} calls ${otherFactory}(...). Use ${expectedFactory}(...) or update the configured framework profile.`,
1600
+ },
1601
+ ];
1602
+ }
1603
+
1463
1604
  async function inspectCanonicalConformance(
1464
1605
  targetDir: string,
1465
1606
  files: string[],
@@ -1626,11 +1767,13 @@ async function inspectCanonicalConformance(
1626
1767
  }
1627
1768
 
1628
1769
  if (!importsAppContext(serverSource, config.paths.server, config, files)) {
1770
+ const serverFactory =
1771
+ config.framework === "next" ? "createNextServer" : "createFetchServer";
1629
1772
  diagnostics.push({
1630
1773
  severity: "warning",
1631
1774
  code: "BEIGNET_SERVER_APP_CONTEXT",
1632
1775
  file: config.paths.server,
1633
- message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through createNextServer and hooks.`,
1776
+ message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through ${serverFactory} and hooks.`,
1634
1777
  });
1635
1778
  }
1636
1779
 
@@ -2333,7 +2476,7 @@ async function inspectProductionReadiness(
2333
2476
  }
2334
2477
 
2335
2478
  if (installedPackages.has("@beignet/provider-auth-better-auth")) {
2336
- if (!hasBetterAuthRoute(files, config)) {
2479
+ if (config.framework === "next" && !hasBetterAuthRoute(files, config)) {
2337
2480
  diagnostics.push({
2338
2481
  severity: "warning",
2339
2482
  code: "BEIGNET_AUTH_ROUTE_MISSING",
@@ -2589,7 +2732,10 @@ async function inspectPaymentsProductionReadiness(
2589
2732
  });
2590
2733
  }
2591
2734
 
2592
- if (!(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))) {
2735
+ if (
2736
+ config.framework === "next" &&
2737
+ !(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))
2738
+ ) {
2593
2739
  diagnostics.push({
2594
2740
  severity: "warning",
2595
2741
  code: "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
@@ -2950,6 +3096,73 @@ async function inspectProviderRegistrationDrift(
2950
3096
  return diagnostics;
2951
3097
  }
2952
3098
 
3099
+ async function inspectInngestProductionPath(
3100
+ targetDir: string,
3101
+ files: string[],
3102
+ config: ResolvedBeignetConfig,
3103
+ ): Promise<InspectDiagnostic[]> {
3104
+ const packageJson = await readPackageJson(targetDir, files);
3105
+ if (
3106
+ !installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")
3107
+ ) {
3108
+ return [];
3109
+ }
3110
+
3111
+ const diagnostics: InspectDiagnostic[] = [];
3112
+ const serverDir = directoryPath(path.dirname(config.paths.server));
3113
+ const registryFile = `${serverDir}/inngest.ts`;
3114
+ const routeFile = `${directoryPath(config.paths.routes)}/inngest/route.ts`;
3115
+
3116
+ if (!files.includes(registryFile)) {
3117
+ diagnostics.push({
3118
+ severity: "warning",
3119
+ code: "BEIGNET_INNGEST_REGISTRY_MISSING",
3120
+ file: registryFile,
3121
+ message: `@beignet/provider-jobs-inngest is installed, but ${registryFile} is missing. Run beignet provider add jobs-inngest or create a central createInngestJobFunctions(...) registry so dispatched jobs can execute.`,
3122
+ });
3123
+ } else {
3124
+ const registrySource = await readFile(
3125
+ path.join(targetDir, registryFile),
3126
+ "utf8",
3127
+ );
3128
+ const registry = arrayInitializerInfo(registrySource, "inngestJobs");
3129
+ const registeredJobs = registry
3130
+ ? identifiersFromArrayExpression(registry.text)
3131
+ : new Set<string>();
3132
+ const featureRoot = directoryPath(config.paths.features);
3133
+ const jobIndexes = files.filter((file) =>
3134
+ new RegExp(`^${escapeRegExp(featureRoot)}/[^/]+/jobs/index\\.ts$`).test(
3135
+ file,
3136
+ ),
3137
+ );
3138
+ for (const jobIndex of jobIndexes) {
3139
+ const source = await readFile(path.join(targetDir, jobIndex), "utf8");
3140
+ const match = source.match(/export const\s+([A-Za-z_$][\w$]*Jobs)\s*=/);
3141
+ const registryName = match?.[1];
3142
+ if (!registryName || registeredJobs.has(registryName)) {
3143
+ continue;
3144
+ }
3145
+ diagnostics.push({
3146
+ severity: "warning",
3147
+ code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
3148
+ file: registryFile,
3149
+ message: `${jobIndex} exports ${registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${registryName} so Inngest can execute the job.`,
3150
+ });
3151
+ }
3152
+ }
3153
+
3154
+ if (config.framework === "next" && !files.includes(routeFile)) {
3155
+ diagnostics.push({
3156
+ severity: "warning",
3157
+ code: "BEIGNET_INNGEST_ROUTE_MISSING",
3158
+ file: routeFile,
3159
+ message: `@beignet/provider-jobs-inngest is installed in a Next.js app, but ${routeFile} is missing. Expose the shared Inngest client and function registry with serve(...) so Inngest can discover and invoke jobs.`,
3160
+ });
3161
+ }
3162
+
3163
+ return diagnostics;
3164
+ }
3165
+
2953
3166
  function isFeatureUploadDefinition(
2954
3167
  file: string,
2955
3168
  config: ResolvedBeignetConfig,
@@ -4213,7 +4426,7 @@ function callReferencesCentralListenerRegistry(args: {
4213
4426
 
4214
4427
  for (const identifier of args.callIdentifiers) {
4215
4428
  const imported = args.namedImports.get(identifier);
4216
- if (!imported || imported.importedName !== "listeners") continue;
4429
+ if (imported?.importedName !== "listeners") continue;
4217
4430
 
4218
4431
  const importedFile = sourceFileFromImport(
4219
4432
  imported.sourcePath,
@@ -4260,6 +4473,7 @@ async function inspectServerlessFootguns(
4260
4473
  const source = await readFile(path.join(targetDir, file), "utf8");
4261
4474
 
4262
4475
  if (
4476
+ config.framework === "next" &&
4263
4477
  convention.resourceGenerator &&
4264
4478
  isNextRouteFile(file, config) &&
4265
4479
  importsEagerServer(source, file, files, config)
@@ -6461,15 +6675,19 @@ async function registeredRouteGroupsForServer(
6461
6675
  serverSource: string,
6462
6676
  ): Promise<Set<string>> {
6463
6677
  const registeredGroups = registeredRouteGroups(serverSource);
6464
- const imports = parseNamedImports(serverSource, config, config.paths.server);
6678
+ const imports = parseNamedImportSources(serverSource);
6465
6679
 
6466
6680
  for (const identifier of routeOptionIdentifiers(serverSource)) {
6467
6681
  const imported = imports.get(identifier);
6468
- if (!imported?.contractFile || !files.includes(imported.contractFile)) {
6469
- continue;
6470
- }
6682
+ if (!imported) continue;
6683
+ const importedFile = sourceFileFromImport(
6684
+ imported.sourcePath,
6685
+ config.paths.server,
6686
+ files,
6687
+ );
6688
+ if (!importedFile || !files.includes(importedFile)) continue;
6471
6689
 
6472
- const source = await readFile(path.join(targetDir, imported.contractFile), {
6690
+ const source = await readFile(path.join(targetDir, importedFile), {
6473
6691
  encoding: "utf8",
6474
6692
  });
6475
6693
  for (const routeGroup of registeredRouteGroups(source)) {
@@ -7458,6 +7676,7 @@ async function inspectResourceSlices(
7458
7676
  );
7459
7677
  }
7460
7678
  if (
7679
+ config.framework === "next" &&
7461
7680
  !hasRequirement(files, routeFile) &&
7462
7681
  !hasRequirement(files, catchAllRouteFile)
7463
7682
  ) {