@beignet/cli 0.0.40 → 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 (93) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +76 -10
  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/db.d.ts +32 -7
  24. package/dist/db.d.ts.map +1 -1
  25. package/dist/db.js +182 -14
  26. package/dist/db.js.map +1 -1
  27. package/dist/explain.d.ts +98 -0
  28. package/dist/explain.d.ts.map +1 -0
  29. package/dist/explain.js +512 -0
  30. package/dist/explain.js.map +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +79 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.js +53 -3
  35. package/dist/inspect.js.map +1 -1
  36. package/dist/lib.d.ts +4 -0
  37. package/dist/lib.d.ts.map +1 -1
  38. package/dist/lib.js +2 -0
  39. package/dist/lib.js.map +1 -1
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +63 -2
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts +1 -1
  44. package/dist/mcp.d.ts.map +1 -1
  45. package/dist/mcp.js +69 -1
  46. package/dist/mcp.js.map +1 -1
  47. package/dist/preflight.d.ts.map +1 -1
  48. package/dist/preflight.js +10 -0
  49. package/dist/preflight.js.map +1 -1
  50. package/dist/provider-add.d.ts.map +1 -1
  51. package/dist/provider-add.js +167 -13
  52. package/dist/provider-add.js.map +1 -1
  53. package/dist/provider-audit.d.ts +6 -0
  54. package/dist/provider-audit.d.ts.map +1 -1
  55. package/dist/provider-audit.js +63 -15
  56. package/dist/provider-audit.js.map +1 -1
  57. package/dist/templates/agents.d.ts.map +1 -1
  58. package/dist/templates/agents.js +17 -4
  59. package/dist/templates/agents.js.map +1 -1
  60. package/dist/templates/base.d.ts.map +1 -1
  61. package/dist/templates/base.js +10 -4
  62. package/dist/templates/base.js.map +1 -1
  63. package/dist/templates/index.d.ts +1 -1
  64. package/dist/templates/index.d.ts.map +1 -1
  65. package/dist/templates/index.js +8 -1
  66. package/dist/templates/index.js.map +1 -1
  67. package/dist/templates/server.d.ts +3 -0
  68. package/dist/templates/server.d.ts.map +1 -1
  69. package/dist/templates/server.js +55 -1
  70. package/dist/templates/server.js.map +1 -1
  71. package/dist/templates/shared.js +1 -1
  72. package/package.json +2 -2
  73. package/skills/app-structure/SKILL.md +22 -4
  74. package/src/analysis/source-index.ts +395 -0
  75. package/src/analysis/workspace.ts +180 -0
  76. package/src/app-map-schema.ts +28 -0
  77. package/src/app-map.ts +1705 -0
  78. package/src/check.ts +27 -4
  79. package/src/db.ts +232 -14
  80. package/src/explain.ts +786 -0
  81. package/src/index.ts +113 -2
  82. package/src/inspect.ts +70 -2
  83. package/src/lib.ts +36 -0
  84. package/src/make.ts +90 -2
  85. package/src/mcp.ts +107 -1
  86. package/src/preflight.ts +14 -0
  87. package/src/provider-add.ts +211 -12
  88. package/src/provider-audit.ts +127 -22
  89. package/src/templates/agents.ts +17 -4
  90. package/src/templates/base.ts +22 -4
  91. package/src/templates/index.ts +18 -1
  92. package/src/templates/server.ts +58 -1
  93. package/src/templates/shared.ts +1 -1
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
@@ -618,7 +618,7 @@ function parseContracts(source: string, file: string): InspectedContract[] {
618
618
  for (const exportMatch of source.matchAll(exportRegex)) {
619
619
  const block = exportMatch[2];
620
620
  const methodMatch =
621
- /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["']\s*\)/.exec(
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(
622
622
  block,
623
623
  );
624
624
  if (methodMatch) {
@@ -1521,6 +1521,7 @@ async function inspectDiagnostics(
1521
1521
  config,
1522
1522
  convention,
1523
1523
  )),
1524
+ ...(await inspectInngestProductionPath(targetDir, files, config)),
1524
1525
  ...(await inspectDatabaseLifecycleDrift(
1525
1526
  targetDir,
1526
1527
  files,
@@ -3095,6 +3096,73 @@ async function inspectProviderRegistrationDrift(
3095
3096
  return diagnostics;
3096
3097
  }
3097
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
+
3098
3166
  function isFeatureUploadDefinition(
3099
3167
  file: string,
3100
3168
  config: ResolvedBeignetConfig,
@@ -4358,7 +4426,7 @@ function callReferencesCentralListenerRegistry(args: {
4358
4426
 
4359
4427
  for (const identifier of args.callIdentifiers) {
4360
4428
  const imported = args.namedImports.get(identifier);
4361
- if (!imported || imported.importedName !== "listeners") continue;
4429
+ if (imported?.importedName !== "listeners") continue;
4362
4430
 
4363
4431
  const importedFile = sourceFileFromImport(
4364
4432
  imported.sourcePath,
package/src/lib.ts CHANGED
@@ -6,9 +6,45 @@
6
6
  * loads command modules lazily.
7
7
  */
8
8
 
9
+ export type {
10
+ AppMapEdge,
11
+ AppMapEdgeKind,
12
+ AppMapNode,
13
+ AppMapNodeKind,
14
+ AppMapNodeStatus,
15
+ AppMapOptions,
16
+ AppMapProjectionOptions,
17
+ AppMapResult,
18
+ AppMapSource,
19
+ AppMapSummary,
20
+ AppMapUnresolvedReference,
21
+ } from "./app-map.js";
22
+ export {
23
+ appMapNodeKinds,
24
+ formatAppMap,
25
+ mapApp,
26
+ projectAppMap,
27
+ } from "./app-map.js";
9
28
  export type { CreateOptions } from "./create.js";
10
29
  export { createProject } from "./create.js";
11
30
  export { runDatabaseCommand } from "./db.js";
31
+ export type {
32
+ ExplainAppOptions,
33
+ ExplainAppResult,
34
+ ExplainEvidence,
35
+ ExplainFinding,
36
+ ExplainRelatedNode,
37
+ ExplainRelationship,
38
+ ExplainSuggestedAction,
39
+ ExplainSuggestedFile,
40
+ ExplainTarget,
41
+ ExplainTargetKind,
42
+ } from "./explain.js";
43
+ export {
44
+ explainApp,
45
+ explainTargetKinds,
46
+ formatExplain,
47
+ } from "./explain.js";
12
48
  export { main } from "./index.js";
13
49
  export {
14
50
  applyDoctorFixes,
package/src/make.ts CHANGED
@@ -1090,6 +1090,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1090
1090
  await updateOutboxRegistry(targetDir, names, "jobs", config, {
1091
1091
  dryRun: true,
1092
1092
  });
1093
+ await updateInngestJobRegistry(targetDir, names, config, { dryRun: true });
1093
1094
 
1094
1095
  const result = await makeFeatureArtifact({
1095
1096
  targetDir,
@@ -1116,6 +1117,9 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1116
1117
  await applyOutboxPortWiring(result, targetDir, config, {
1117
1118
  dryRun: Boolean(options.dryRun),
1118
1119
  });
1120
+ await applyInngestJobRegistryUpdate(result, targetDir, names, config, {
1121
+ dryRun: Boolean(options.dryRun),
1122
+ });
1119
1123
 
1120
1124
  return result;
1121
1125
  }
@@ -3814,6 +3818,92 @@ async function applyOutboxRegistryUpdate(
3814
3818
  }
3815
3819
  }
3816
3820
 
3821
+ function inngestRegistryPath(config: ResolvedBeignetConfig): string {
3822
+ return path.posix.join(path.posix.dirname(config.paths.server), "inngest.ts");
3823
+ }
3824
+
3825
+ async function appUsesInngestJobs(targetDir: string): Promise<boolean> {
3826
+ const source = await readOptionalFile(path.join(targetDir, "package.json"));
3827
+ if (source === undefined) return false;
3828
+
3829
+ try {
3830
+ const packageJson = JSON.parse(source) as PackageJsonLike;
3831
+ return Boolean(
3832
+ packageJson.dependencies?.["@beignet/provider-jobs-inngest"] ??
3833
+ packageJson.devDependencies?.["@beignet/provider-jobs-inngest"],
3834
+ );
3835
+ } catch {
3836
+ return false;
3837
+ }
3838
+ }
3839
+
3840
+ async function updateInngestJobRegistry(
3841
+ targetDir: string,
3842
+ names: JobNames,
3843
+ config: ResolvedBeignetConfig,
3844
+ options: { dryRun: boolean },
3845
+ ): Promise<WriteGeneratedFileResult | undefined> {
3846
+ if (!(await appUsesInngestJobs(targetDir))) return undefined;
3847
+
3848
+ const registryPath = inngestRegistryPath(config);
3849
+ const destination = path.join(targetDir, registryPath);
3850
+ const existing = await readOptionalFile(destination);
3851
+ if (existing === undefined) {
3852
+ throw new Error(
3853
+ `@beignet/provider-jobs-inngest is installed, but ${registryPath} is missing. Run \`beignet provider add jobs-inngest\` to create the execution registry before generating jobs.`,
3854
+ );
3855
+ }
3856
+
3857
+ const featureIndexPath = path.join(
3858
+ featureArtifactDir(names, "jobs", config),
3859
+ "index.ts",
3860
+ );
3861
+ const registryName = `${names.featureSingularCamel}Jobs`;
3862
+ const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
3863
+ let next = existing;
3864
+ if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
3865
+
3866
+ const registry = arrayInitializerInfo(next, "inngestJobs");
3867
+ if (!registry) {
3868
+ throw new Error(
3869
+ `Could not find \`export const inngestJobs = [...]\` in ${registryPath}. Restore the generated registry or register ${registryName} manually.`,
3870
+ );
3871
+ }
3872
+ if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
3873
+ const updatedArray = appendToArrayExpression(registry.text, [
3874
+ `...${registryName}`,
3875
+ ]);
3876
+ next = `${next.slice(0, registry.start)}${updatedArray}${next.slice(
3877
+ registry.end,
3878
+ )}`;
3879
+ }
3880
+
3881
+ if (next === existing) return "skipped";
3882
+ if (!options.dryRun) await writeFile(destination, next);
3883
+ return "updated";
3884
+ }
3885
+
3886
+ async function applyInngestJobRegistryUpdate(
3887
+ result: MakeJobResult,
3888
+ targetDir: string,
3889
+ names: JobNames,
3890
+ config: ResolvedBeignetConfig,
3891
+ options: { dryRun: boolean },
3892
+ ): Promise<void> {
3893
+ const registryResult = await updateInngestJobRegistry(
3894
+ targetDir,
3895
+ names,
3896
+ config,
3897
+ options,
3898
+ );
3899
+ if (registryResult === undefined) return;
3900
+
3901
+ const registryPath = inngestRegistryPath(config);
3902
+ if (registryResult === "updated") result.updatedFiles.push(registryPath);
3903
+ if (registryResult === "skipped") result.skippedFiles.push(registryPath);
3904
+ if (!result.files.includes(registryPath)) result.files.push(registryPath);
3905
+ }
3906
+
3817
3907
  function schemasFile(
3818
3908
  names: ResourceNames,
3819
3909
  mode: ResourceGenerationMode,
@@ -5145,8 +5235,6 @@ export const ${names.jobExportName} = defineJob("${names.jobName}", {
5145
5235
  \tpayload: ${names.payloadSchemaName},
5146
5236
  \tretry: retry.exponential({
5147
5237
  \t\tattempts: 3,
5148
- \t\tinitialDelay: "1s",
5149
- \t\tmaxDelay: "1m",
5150
5238
  \t}),
5151
5239
  \tasync handle({ payload, ctx }) {
5152
5240
  \t\tctx.ports.logger.info("Job handled", {
package/src/mcp.ts CHANGED
@@ -2,7 +2,11 @@ import path from "node:path";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
+ import { mapApp, projectAppMap } from "./app-map.js";
6
+ import { appMapNodeKinds } from "./app-map-schema.js";
7
+ import { checkApp } from "./check.js";
5
8
  import { type ProviderPresetName, providerPresetChoices } from "./choices.js";
9
+ import { explainApp, explainTargetKinds } from "./explain.js";
6
10
  import { applyDoctorFixes, inspectApp } from "./inspect.js";
7
11
  import { lintApp } from "./lint.js";
8
12
  import {
@@ -68,6 +72,8 @@ const makeArtifactChoices = [
68
72
  "use-case",
69
73
  ] as const;
70
74
 
75
+ const defaultMcpCheckTimeoutMs = 10 * 60 * 1_000;
76
+
71
77
  type MakeArtifact = (typeof makeArtifactChoices)[number];
72
78
 
73
79
  const makeInputSchema = {
@@ -325,7 +331,7 @@ async function runProviderAddTool(
325
331
  }
326
332
 
327
333
  /**
328
- * Build the Beignet MCP server with routes, doctor, lint, and make tools.
334
+ * Build the Beignet MCP server with app-map, validation, and generator tools.
329
335
  * Every tool resolves against the app directory captured at launch.
330
336
  */
331
337
  export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
@@ -335,6 +341,106 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
335
341
  version: options.version,
336
342
  });
337
343
 
344
+ server.registerTool(
345
+ "app_map",
346
+ {
347
+ description:
348
+ "Return a deterministic graph of this Beignet app: features, contracts, routes, use cases, authorization, workflows, ports, providers, OpenAPI, tests, dependency edges, and validation findings. Project by feature or node kind to keep agent context focused.",
349
+ inputSchema: {
350
+ feature: z
351
+ .string()
352
+ .optional()
353
+ .describe("Project one feature and its direct relationships."),
354
+ kinds: z
355
+ .array(z.enum(appMapNodeKinds))
356
+ .optional()
357
+ .describe("Include only these app-map node kinds."),
358
+ includeDiagnostics: z
359
+ .boolean()
360
+ .optional()
361
+ .describe("Include doctor and lint findings. Defaults to true."),
362
+ },
363
+ annotations: { readOnlyHint: true },
364
+ },
365
+ async (input) =>
366
+ safeToolResult(async () => {
367
+ const result = await mapApp({ cwd, strict: true });
368
+ return jsonResult(
369
+ projectAppMap(result, {
370
+ feature: input.feature,
371
+ kinds: input.kinds,
372
+ includeDiagnostics: input.includeDiagnostics,
373
+ }),
374
+ );
375
+ }),
376
+ );
377
+
378
+ server.registerTool(
379
+ "explain",
380
+ {
381
+ description:
382
+ "Explain one mapped Beignet feature, route, provider, or diagnostic using deterministic source evidence. Returns relationships, relevant conventions, findings, suggested files, and follow-up commands without changing the app.",
383
+ inputSchema: {
384
+ kind: z.enum(explainTargetKinds).describe("Mapped concept to explain."),
385
+ target: z
386
+ .string()
387
+ .min(1)
388
+ .describe(
389
+ "Feature name, METHOD /path route, provider package or port name, or diagnostic code.",
390
+ ),
391
+ },
392
+ annotations: { readOnlyHint: true },
393
+ },
394
+ async (input) =>
395
+ safeToolResult(async () =>
396
+ jsonResult(
397
+ await explainApp({ cwd, kind: input.kind, target: input.target }),
398
+ ),
399
+ ),
400
+ );
401
+
402
+ server.registerTool(
403
+ "check",
404
+ {
405
+ description:
406
+ "Run the full Beignet app validation loop: Beignet lint, strict doctor, and the app's lint, typecheck, and test scripts. Returns the same versioned JSON as beignet check --json, including every step, bounded failure output, durations, and overall ok status. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
407
+ inputSchema: {
408
+ preflight: z
409
+ .boolean()
410
+ .optional()
411
+ .describe(
412
+ "Include the environment preflight check before app package scripts. Defaults to false.",
413
+ ),
414
+ timeoutMs: z
415
+ .number()
416
+ .int()
417
+ .min(1_000)
418
+ .max(60 * 60 * 1_000)
419
+ .optional()
420
+ .describe(
421
+ "Maximum duration for each app package script in milliseconds. Defaults to 600000 (10 minutes).",
422
+ ),
423
+ },
424
+ annotations: {
425
+ readOnlyHint: false,
426
+ destructiveHint: true,
427
+ idempotentHint: false,
428
+ openWorldHint: true,
429
+ },
430
+ },
431
+ async (input, extra) =>
432
+ safeToolResult(async () =>
433
+ jsonResult(
434
+ await checkApp({
435
+ cwd,
436
+ preflight: input.preflight ?? false,
437
+ scriptTimeoutMs: input.timeoutMs ?? defaultMcpCheckTimeoutMs,
438
+ signal: extra.signal,
439
+ }),
440
+ ),
441
+ ),
442
+ );
443
+
338
444
  server.registerTool(
339
445
  "routes",
340
446
  {
package/src/preflight.ts CHANGED
@@ -245,6 +245,20 @@ export async function runPreflight(
245
245
  }
246
246
  }
247
247
 
248
+ const inngestDev = env.INNGEST_DEV?.trim().toLowerCase();
249
+ if (
250
+ env.NODE_ENV === "production" &&
251
+ inngestDev !== undefined &&
252
+ !["", "0", "false"].includes(inngestDev)
253
+ ) {
254
+ findings.push({
255
+ severity: "error",
256
+ code: "BEIGNET_PREFLIGHT_INNGEST_DEV_IN_PRODUCTION",
257
+ message:
258
+ "INNGEST_DEV enables the local Inngest dev server and must be disabled in production.",
259
+ });
260
+ }
261
+
248
262
  const exampleSource = await readFileIfExists(
249
263
  path.join(options.cwd, ".env.example"),
250
264
  );