@beignet/cli 0.0.52 → 0.0.53

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 (68) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +131 -1
  3. package/dist/analysis/layers.d.ts +2 -0
  4. package/dist/analysis/layers.d.ts.map +1 -1
  5. package/dist/analysis/layers.js +5 -0
  6. package/dist/analysis/layers.js.map +1 -1
  7. package/dist/analysis/port-wiring.d.ts +4 -2
  8. package/dist/analysis/port-wiring.d.ts.map +1 -1
  9. package/dist/analysis/port-wiring.js +39 -10
  10. package/dist/analysis/port-wiring.js.map +1 -1
  11. package/dist/analysis/source-index.d.ts +2 -1
  12. package/dist/analysis/source-index.d.ts.map +1 -1
  13. package/dist/analysis/source-index.js +34 -5
  14. package/dist/analysis/source-index.js.map +1 -1
  15. package/dist/analysis/workspace-routes.d.ts +8 -0
  16. package/dist/analysis/workspace-routes.d.ts.map +1 -0
  17. package/dist/analysis/workspace-routes.js +291 -0
  18. package/dist/analysis/workspace-routes.js.map +1 -0
  19. package/dist/analysis/workspace.d.ts +24 -3
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +166 -21
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map-changes.d.ts.map +1 -1
  24. package/dist/app-map-changes.js +19 -7
  25. package/dist/app-map-changes.js.map +1 -1
  26. package/dist/app-map.d.ts.map +1 -1
  27. package/dist/app-map.js +58 -55
  28. package/dist/app-map.js.map +1 -1
  29. package/dist/config.d.ts +7 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +19 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +20 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/inspect.d.ts.map +1 -1
  37. package/dist/inspect.js +89 -21
  38. package/dist/inspect.js.map +1 -1
  39. package/dist/lint.d.ts.map +1 -1
  40. package/dist/lint.js +106 -38
  41. package/dist/lint.js.map +1 -1
  42. package/dist/make/shared.js +1 -1
  43. package/dist/make.js +18 -1
  44. package/dist/make.js.map +1 -1
  45. package/dist/provider-add.js +2 -2
  46. package/dist/provider-add.js.map +1 -1
  47. package/dist/provider-audit.d.ts.map +1 -1
  48. package/dist/provider-audit.js +22 -6
  49. package/dist/provider-audit.js.map +1 -1
  50. package/dist/templates/shared.js +3 -3
  51. package/package.json +3 -2
  52. package/skills/app-structure/SKILL.md +42 -0
  53. package/src/analysis/layers.ts +9 -0
  54. package/src/analysis/port-wiring.ts +66 -10
  55. package/src/analysis/source-index.ts +56 -4
  56. package/src/analysis/workspace-routes.ts +385 -0
  57. package/src/analysis/workspace.ts +281 -34
  58. package/src/app-map-changes.ts +31 -5
  59. package/src/app-map.ts +75 -75
  60. package/src/config.ts +33 -0
  61. package/src/index.ts +25 -0
  62. package/src/inspect.ts +123 -16
  63. package/src/lint.ts +146 -43
  64. package/src/make/shared.ts +1 -1
  65. package/src/make.ts +31 -1
  66. package/src/provider-add.ts +2 -2
  67. package/src/provider-audit.ts +30 -10
  68. package/src/templates/shared.ts +3 -3
package/src/lint.ts CHANGED
@@ -3,11 +3,12 @@ import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import {
5
5
  canonicalFeatureLayerPath,
6
- classifySourcePath as classifyPath,
6
+ classifyWorkspaceSourcePath as classifyPath,
7
7
  type SourceLayer,
8
8
  } from "./analysis/layers.js";
9
9
  import {
10
10
  isAnalysisSourceFile,
11
+ isAnalysisSourceFileExtension,
11
12
  stripSourceFileExtension,
12
13
  } from "./analysis/source-files.js";
13
14
  import {
@@ -15,7 +16,12 @@ import {
15
16
  moduleReferences,
16
17
  } from "./analysis/source-index.js";
17
18
  import {
19
+ type AnalysisWorkspace,
18
20
  createAnalysisWorkspace,
21
+ findProjectForFile,
22
+ projectForFile,
23
+ projectPath,
24
+ projectSource,
19
25
  resolveLocalModulePath,
20
26
  } from "./analysis/workspace.js";
21
27
  import { createPainter } from "./ansi.js";
@@ -81,12 +87,19 @@ export async function lintApp(
81
87
  const importGraph: ImportGraph = new Map();
82
88
 
83
89
  for (const file of sourceFiles) {
84
- const sourceLayer = classifyPath(file, config);
90
+ const sourceLayer = classifyPath(file, workspace);
91
+ const source = projectSource(workspace, file);
85
92
  const sourceFile = await workspace.readSourceFile(file);
86
93
  sourceFileByPath.set(file, sourceFile);
87
94
  importGraph.set(file, await parseImports(workspace, file));
88
95
 
89
- const canonicalLayerPath = canonicalFeatureLayerPath(file, config);
96
+ const localCanonicalPath = canonicalFeatureLayerPath(
97
+ source.file,
98
+ source.project.config,
99
+ );
100
+ const canonicalLayerPath = localCanonicalPath
101
+ ? projectPath(source.project, localCanonicalPath)
102
+ : undefined;
90
103
  if (canonicalLayerPath) {
91
104
  diagnostics.push({
92
105
  severity: "error",
@@ -99,10 +112,17 @@ export async function lintApp(
99
112
  });
100
113
  }
101
114
 
115
+ for (const reference of importGraph.get(file) ?? []) {
116
+ const issue = workspaceImportViolation(workspace, file, reference);
117
+ if (issue) diagnostics.push(issue);
118
+ }
102
119
  if (sourceLayer === "test" || sourceLayer === "unknown") continue;
103
120
 
104
- if (isLegacySharedDomainPath(file, config)) {
105
- const canonicalSharedDomain = sharedDomainPath(config);
121
+ if (isLegacySharedDomainPath(source.file, source.project.config)) {
122
+ const canonicalSharedDomain = projectPath(
123
+ source.project,
124
+ sharedDomainPath(source.project.config),
125
+ );
106
126
  diagnostics.push({
107
127
  severity: "error",
108
128
  code: "BEIGNET_CANONICAL_STRUCTURE",
@@ -119,7 +139,7 @@ export async function lintApp(
119
139
  lintUnresolvedModuleReference(file, sourceLayer, reference) ??
120
140
  (sourceLayer === "schema"
121
141
  ? undefined
122
- : lintImport(file, sourceLayer, reference, config));
142
+ : lintImport(file, sourceLayer, reference, workspace));
123
143
  if (diagnostic) {
124
144
  diagnostics.push({
125
145
  ...diagnostic,
@@ -132,14 +152,14 @@ export async function lintApp(
132
152
 
133
153
  diagnostics.push(
134
154
  ...lintTransitiveBoundaries({
135
- config,
155
+ workspace,
136
156
  graph: importGraph,
137
157
  }),
138
158
  );
139
159
 
140
160
  diagnostics.push(
141
161
  ...lintRuntimeBoundary({
142
- config,
162
+ workspace,
143
163
  graph: importGraph,
144
164
  sourceFileByPath,
145
165
  }),
@@ -245,15 +265,20 @@ function lintImport(
245
265
  file: string,
246
266
  sourceLayer: SourceLayer,
247
267
  reference: ImportReference,
248
- config: ResolvedBeignetConfig,
268
+ workspace: AnalysisWorkspace,
249
269
  ): LintDiagnostic | undefined {
250
270
  if (reference.kind === "type") return undefined;
251
271
 
252
272
  const targetLayer = reference.resolvedPath
253
- ? classifyPath(reference.resolvedPath, config)
273
+ ? classifyPath(reference.resolvedPath, workspace)
254
274
  : undefined;
255
275
  const featureImport = reference.resolvedPath
256
- ? featureImportViolation(file, sourceLayer, reference.resolvedPath, config)
276
+ ? featureImportViolation(
277
+ file,
278
+ sourceLayer,
279
+ reference.resolvedPath,
280
+ workspace,
281
+ )
257
282
  : undefined;
258
283
  if (featureImport) {
259
284
  return {
@@ -580,16 +605,16 @@ function matchBeignetProviderPackage(importPath: string): string | undefined {
580
605
  }
581
606
 
582
607
  function lintTransitiveBoundaries({
583
- config,
608
+ workspace,
584
609
  graph,
585
610
  }: {
586
- config: ResolvedBeignetConfig;
611
+ workspace: AnalysisWorkspace;
587
612
  graph: ImportGraph;
588
613
  }): LintDiagnostic[] {
589
614
  const diagnostics: LintDiagnostic[] = [];
590
615
 
591
616
  for (const root of graph.keys()) {
592
- const rootLayer = classifyPath(root, config);
617
+ const rootLayer = classifyPath(root, workspace);
593
618
  if (
594
619
  ![
595
620
  "agent-capability",
@@ -609,7 +634,7 @@ function lintTransitiveBoundaries({
609
634
  }
610
635
 
611
636
  diagnostics.push(
612
- ...lintTransitiveBoundaryRoot({ config, graph, root, rootLayer }),
637
+ ...lintTransitiveBoundaryRoot({ workspace, graph, root, rootLayer }),
613
638
  );
614
639
  }
615
640
 
@@ -617,12 +642,12 @@ function lintTransitiveBoundaries({
617
642
  }
618
643
 
619
644
  function lintTransitiveBoundaryRoot({
620
- config,
645
+ workspace,
621
646
  graph,
622
647
  root,
623
648
  rootLayer,
624
649
  }: {
625
- config: ResolvedBeignetConfig;
650
+ workspace: AnalysisWorkspace;
626
651
  graph: ImportGraph;
627
652
  root: string;
628
653
  rootLayer: SourceLayer;
@@ -649,7 +674,7 @@ function lintTransitiveBoundaryRoot({
649
674
  current.file === root
650
675
  ? undefined
651
676
  : (lintUnresolvedModuleReference(root, rootLayer, reference) ??
652
- lintImport(root, rootLayer, reference, config));
677
+ lintImport(root, rootLayer, reference, workspace));
653
678
 
654
679
  if (violation) {
655
680
  diagnostics.push({
@@ -664,7 +689,7 @@ function lintTransitiveBoundaryRoot({
664
689
  reference.resolvedPath &&
665
690
  graph.has(reference.resolvedPath) &&
666
691
  isTransparentArchitectureLayer(
667
- classifyPath(reference.resolvedPath, config),
692
+ classifyPath(reference.resolvedPath, workspace),
668
693
  )
669
694
  ) {
670
695
  queue.push({ file: reference.resolvedPath, chain });
@@ -680,11 +705,11 @@ function isTransparentArchitectureLayer(layer: SourceLayer): boolean {
680
705
  }
681
706
 
682
707
  function lintRuntimeBoundary({
683
- config,
708
+ workspace,
684
709
  graph,
685
710
  sourceFileByPath,
686
711
  }: {
687
- config: ResolvedBeignetConfig;
712
+ workspace: AnalysisWorkspace;
688
713
  graph: ImportGraph;
689
714
  sourceFileByPath: Map<string, ts.SourceFile>;
690
715
  }): LintDiagnostic[] {
@@ -694,7 +719,7 @@ function lintRuntimeBoundary({
694
719
  const rootKind = runtimeBoundaryRootKind(
695
720
  file,
696
721
  sourceFileByPath.get(file),
697
- config,
722
+ workspace,
698
723
  );
699
724
  if (!rootKind) continue;
700
725
 
@@ -702,7 +727,7 @@ function lintRuntimeBoundary({
702
727
  ...lintRuntimeBoundaryRoot({
703
728
  root: file,
704
729
  rootKind,
705
- config,
730
+ workspace,
706
731
  graph,
707
732
  }),
708
733
  );
@@ -714,12 +739,12 @@ function lintRuntimeBoundary({
714
739
  function lintRuntimeBoundaryRoot({
715
740
  root,
716
741
  rootKind,
717
- config,
742
+ workspace,
718
743
  graph,
719
744
  }: {
720
745
  root: string;
721
746
  rootKind: RuntimeBoundaryRootKind;
722
- config: ResolvedBeignetConfig;
747
+ workspace: AnalysisWorkspace;
723
748
  graph: ImportGraph;
724
749
  }): LintDiagnostic[] {
725
750
  const diagnostics: LintDiagnostic[] = [];
@@ -745,7 +770,11 @@ function lintRuntimeBoundaryRoot({
745
770
  const chain = reference.resolvedPath
746
771
  ? [...current.chain, reference.resolvedPath]
747
772
  : current.chain;
748
- const violation = runtimeBoundaryViolation(reference, config, rootKind);
773
+ const violation = runtimeBoundaryViolation(
774
+ reference,
775
+ workspace,
776
+ rootKind,
777
+ );
749
778
 
750
779
  if (violation) {
751
780
  diagnostics.push({
@@ -773,14 +802,16 @@ function lintRuntimeBoundaryRoot({
773
802
  function runtimeBoundaryRootKind(
774
803
  file: string,
775
804
  sourceFile: ts.SourceFile | undefined,
776
- config: ResolvedBeignetConfig,
805
+ workspace: AnalysisWorkspace,
777
806
  ): RuntimeBoundaryRootKind | undefined {
778
- const layer = classifyPath(file, config);
807
+ const layer = classifyPath(file, workspace);
779
808
  if (layer === "contract") return "contract";
780
809
  if (layer === "schema") return "schema";
781
810
  if (layer === "client") return "client";
782
811
  if (sourceFile && hasUseClientDirective(sourceFile)) return "client";
783
- if (isGeneratedClientAdapter(file, config)) return "client";
812
+ const source = projectSource(workspace, file);
813
+ if (isGeneratedClientAdapter(source.file, source.project.config))
814
+ return "client";
784
815
  return undefined;
785
816
  }
786
817
 
@@ -809,7 +840,7 @@ function isGeneratedClientAdapter(
809
840
 
810
841
  function runtimeBoundaryViolation(
811
842
  reference: ImportReference,
812
- config: ResolvedBeignetConfig,
843
+ workspace: AnalysisWorkspace,
813
844
  rootKind: RuntimeBoundaryRootKind,
814
845
  ): string | undefined {
815
846
  if (!reference.staticallyResolvable) {
@@ -817,7 +848,7 @@ function runtimeBoundaryViolation(
817
848
  }
818
849
 
819
850
  if (reference.resolvedPath) {
820
- const targetLayer = classifyPath(reference.resolvedPath, config);
851
+ const targetLayer = classifyPath(reference.resolvedPath, workspace);
821
852
  const prohibitedLayer =
822
853
  rootKind === "client"
823
854
  ? undefined
@@ -901,25 +932,30 @@ function featureImportViolation(
901
932
  file: string,
902
933
  sourceLayer: SourceLayer,
903
934
  resolvedPath: string,
904
- config: ResolvedBeignetConfig,
935
+ workspace: AnalysisWorkspace,
905
936
  ): string | undefined {
906
937
  if (sourceLayer !== "domain") return undefined;
907
938
 
908
- const sourceFeature = featureLayerInfo(file, config);
909
- const targetFeature = featureLayerInfo(resolvedPath, config);
939
+ const sourceFeature = featureLayerInfo(file, workspace);
940
+ const targetFeature = featureLayerInfo(resolvedPath, workspace);
910
941
  if (!sourceFeature || !targetFeature) return undefined;
911
942
  if (sourceFeature.layer !== "domain" || targetFeature.layer !== "domain") {
912
943
  return undefined;
913
944
  }
914
- if (sourceFeature.feature === targetFeature.feature) return undefined;
945
+ if (
946
+ sourceFeature.feature === targetFeature.feature &&
947
+ sourceFeature.owner === targetFeature.owner
948
+ )
949
+ return undefined;
915
950
  if (targetFeature.feature === "shared") return undefined;
916
951
 
917
952
  return `feature-specific domain files must not import another feature's domain (${stripKnownExtension(
918
953
  resolvedPath,
919
- )}). Move shared-kernel domain concepts to ${sharedDomainPath(config)}.`;
954
+ )}). Move shared-kernel domain concepts to ${sharedDomainPath(projectForFile(workspace, file).config)}.`;
920
955
  }
921
956
 
922
957
  const domainBannedPackages = new Set([
958
+ "@beignet/core/encryption",
923
959
  "@aws-sdk/client-s3",
924
960
  "@aws-sdk/lib-storage",
925
961
  "@aws-sdk/s3-request-presigner",
@@ -928,12 +964,14 @@ const domainBannedPackages = new Set([
928
964
  "@beignet/core/errors",
929
965
  "@beignet/next",
930
966
  "@beignet/core/ports",
967
+ "@beignet/react-form",
931
968
  "@beignet/react-hook-form",
932
969
  "@beignet/react-query",
933
970
  "@hookform/resolvers",
934
971
  "@libsql/client",
935
972
  "@neondatabase/serverless",
936
973
  "@planetscale/database",
974
+ "@tanstack/react-form",
937
975
  "@tanstack/react-query",
938
976
  "@upstash/ratelimit",
939
977
  "@upstash/redis",
@@ -960,12 +998,14 @@ const coreBannedPackages = new Set([
960
998
  "@aws-sdk/lib-storage",
961
999
  "@aws-sdk/s3-request-presigner",
962
1000
  "@beignet/core/client",
1001
+ "@beignet/react-form",
963
1002
  "@beignet/react-hook-form",
964
1003
  "@beignet/react-query",
965
1004
  "@hookform/resolvers",
966
1005
  "@libsql/client",
967
1006
  "@neondatabase/serverless",
968
1007
  "@planetscale/database",
1008
+ "@tanstack/react-form",
969
1009
  "@tanstack/react-query",
970
1010
  "@upstash/ratelimit",
971
1011
  "@upstash/redis",
@@ -988,6 +1028,7 @@ const coreBannedPackages = new Set([
988
1028
  ]);
989
1029
 
990
1030
  const serverRuntimePackages = new Set([
1031
+ "@beignet/core/encryption",
991
1032
  "@beignet/devtools",
992
1033
  "@beignet/next",
993
1034
  "@beignet/core/openapi",
@@ -1033,19 +1074,22 @@ function featureRelativePath(
1033
1074
 
1034
1075
  function featureLayerInfo(
1035
1076
  filePath: string,
1036
- config: ResolvedBeignetConfig,
1037
- ): { feature: string; layer: SourceLayer } | undefined {
1038
- const normalizedPath = stripKnownExtension(normalizePath(filePath));
1077
+ workspace: AnalysisWorkspace,
1078
+ ): { feature: string; owner: string; layer: SourceLayer } | undefined {
1079
+ const source = projectSource(workspace, filePath);
1080
+ const normalizedPath = stripKnownExtension(source.file);
1039
1081
  const relativePath = featureRelativePath(
1040
1082
  normalizedPath,
1041
- directoryPath(config.paths.features),
1083
+ directoryPath(source.project.config.paths.features),
1042
1084
  );
1043
1085
  if (!relativePath) return undefined;
1044
-
1045
1086
  const [feature] = relativePath.split("/");
1046
1087
  if (!feature) return undefined;
1047
-
1048
- return { feature, layer: classifyPath(filePath, config) };
1088
+ return {
1089
+ feature,
1090
+ owner: source.project.prefix,
1091
+ layer: classifyPath(filePath, workspace),
1092
+ };
1049
1093
  }
1050
1094
 
1051
1095
  function legacySharedDomainPath(config: ResolvedBeignetConfig): string {
@@ -1092,3 +1136,62 @@ function dedupeDiagnostics(diagnostics: LintDiagnostic[]): LintDiagnostic[] {
1092
1136
 
1093
1137
  return deduped;
1094
1138
  }
1139
+
1140
+ function workspaceImportViolation(
1141
+ workspace: AnalysisWorkspace,
1142
+ file: string,
1143
+ reference: ImportReference,
1144
+ ): LintDiagnostic | undefined {
1145
+ if (workspace.projects.length === 1 || !reference.staticallyResolvable)
1146
+ return undefined;
1147
+ const importer = projectForFile(workspace, file);
1148
+ const namedPackage = workspace.projects.find(
1149
+ (project) =>
1150
+ project.name &&
1151
+ (reference.importPath === project.name ||
1152
+ reference.importPath.startsWith(`${project.name}/`)),
1153
+ );
1154
+ const target = reference.resolvedPath
1155
+ ? findProjectForFile(workspace, reference.resolvedPath)
1156
+ : namedPackage;
1157
+ let message: string | undefined;
1158
+ if (
1159
+ importer.localDependencies.has(packageName(reference.importPath)) &&
1160
+ !namedPackage
1161
+ ) {
1162
+ message = `Add the source directory for ${packageName(reference.importPath)} to workspace.packages so Beignet can inspect this local dependency.`;
1163
+ } else if (reference.resolvedPath && !target) {
1164
+ message = `Cannot inspect ${reference.importPath}: its source is outside the configured workspace. Add the owning package directory to workspace.packages.`;
1165
+ } else if (
1166
+ target &&
1167
+ target !== importer &&
1168
+ (!target.name || !importer.dependencies.has(target.name))
1169
+ ) {
1170
+ message = `Declare ${target.name ?? target.prefix} in ${projectPath(importer, "package.json")} before importing its source.`;
1171
+ } else if (
1172
+ (namedPackage &&
1173
+ (!path.posix.extname(reference.importPath) ||
1174
+ isAnalysisSourceFileExtension(
1175
+ path.posix.extname(reference.importPath),
1176
+ )) &&
1177
+ (!reference.resolvedPath ||
1178
+ !workspace.fileSet.has(reference.resolvedPath))) ||
1179
+ (target?.prefix &&
1180
+ reference.resolvedPath &&
1181
+ (!workspace.fileSet.has(reference.resolvedPath) ||
1182
+ !isAnalysisSourceFile(reference.resolvedPath)))
1183
+ ) {
1184
+ message = `Cannot inspect ${reference.importPath}. Install workspace dependencies and expose source files through package exports and the importing package's TypeScript configuration. Build output is not inspected.`;
1185
+ }
1186
+ return message
1187
+ ? {
1188
+ severity: "error",
1189
+ code: "BEIGNET_WORKSPACE_IMPORT",
1190
+ file,
1191
+ importPath: reference.importPath,
1192
+ line: reference.line,
1193
+ column: reference.column,
1194
+ message,
1195
+ }
1196
+ : undefined;
1197
+ }
@@ -1130,7 +1130,7 @@ export function beignetDependencyVersion(packageJson: PackageJsonLike): string {
1130
1130
  export function tanstackReactQueryDependencyVersion(
1131
1131
  packageJson: PackageJsonLike,
1132
1132
  ): string {
1133
- return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.100.7";
1133
+ return packageJson.dependencies?.["@tanstack/react-query"] ?? "^5.102.0";
1134
1134
  }
1135
1135
 
1136
1136
  export async function updatePackageJson(
package/src/make.ts CHANGED
@@ -3173,6 +3173,13 @@ async function missingFeatureUiFormsFiles(
3173
3173
  config: ResolvedBeignetConfig,
3174
3174
  ): Promise<GeneratedFile[]> {
3175
3175
  const formsPath = clientFormsPath(config);
3176
+
3177
+ if (await appDependsOn(targetDir, "@beignet/react-form")) {
3178
+ throw new Error(
3179
+ "beignet make feature --with ui generates React Hook Form components, but this app depends on @beignet/react-form. Generate the feature without --with ui and write the TanStack Form component by hand; the @beignet/react-form#forms skill shows the pattern.",
3180
+ );
3181
+ }
3182
+
3176
3183
  const existing = await readOptionalFile(path.join(targetDir, formsPath));
3177
3184
 
3178
3185
  if (existing === undefined) {
@@ -3196,6 +3203,29 @@ export const rhf = createReactHookForm();
3196
3203
  return [];
3197
3204
  }
3198
3205
 
3206
+ async function appDependsOn(
3207
+ targetDir: string,
3208
+ packageName: string,
3209
+ ): Promise<boolean> {
3210
+ const source = await readOptionalFile(path.join(targetDir, "package.json"));
3211
+ if (source === undefined) return false;
3212
+
3213
+ let packageJson: {
3214
+ dependencies?: Record<string, string>;
3215
+ devDependencies?: Record<string, string>;
3216
+ };
3217
+ try {
3218
+ packageJson = JSON.parse(source);
3219
+ } catch {
3220
+ return false;
3221
+ }
3222
+
3223
+ return Boolean(
3224
+ packageJson.dependencies?.[packageName] ??
3225
+ packageJson.devDependencies?.[packageName],
3226
+ );
3227
+ }
3228
+
3199
3229
  async function assertUploadUiApp(
3200
3230
  targetDir: string,
3201
3231
  config: ResolvedBeignetConfig,
@@ -4187,7 +4217,7 @@ export function decode${names.singularPascal}Cursor(cursor: string): ${names.sin
4187
4217
  export const List${names.pluralPascal}InputSchema = z
4188
4218
  .object({
4189
4219
  limit: z.number().int().min(1).max(100).default(20),
4190
- cursor: z.string().optional().transform((value) => value ?? null),
4220
+ cursor: z.string().optional(),
4191
4221
  name: z.string().trim().min(1).max(120).optional(),
4192
4222
  sortBy: ${names.singularPascal}SortBySchema.default("createdAt"),
4193
4223
  sortDirection: ${names.singularPascal}SortDirectionSchema.default("desc"),
@@ -584,7 +584,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
584
584
  key: "storage",
585
585
  typeName: "StoragePort",
586
586
  importName: "StoragePort",
587
- importFrom: "@beignet/core/storage",
587
+ importFrom: "@beignet/core/ports",
588
588
  },
589
589
  envVars: [
590
590
  "STORAGE_S3_BUCKET",
@@ -649,7 +649,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
649
649
  key: "storage",
650
650
  typeName: "StoragePort",
651
651
  importName: "StoragePort",
652
- importFrom: "@beignet/core/storage",
652
+ importFrom: "@beignet/core/ports",
653
653
  },
654
654
  envVars: ["BLOB_READ_WRITE_TOKEN"],
655
655
  envExample: [
@@ -5,6 +5,7 @@ import {
5
5
  type ProviderPackageMetadata,
6
6
  parseProviderPackageMetadata,
7
7
  } from "@beignet/core/providers";
8
+ import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
8
9
  import {
9
10
  type BeignetDatabaseTables,
10
11
  defaultBeignetConfig,
@@ -169,6 +170,10 @@ export async function auditProviders(
169
170
  const portsSource = files.includes(config.paths.ports)
170
171
  ? await readCachedSource(targetDir, config.paths.ports, sourceCache)
171
172
  : "";
173
+ const declaredAppPorts =
174
+ config.workspace.packages.length > 0
175
+ ? (await analyzePortWiringFiles({ targetDir, files, config })).declared
176
+ : undefined;
172
177
  const infrastructurePath = directoryPath(
173
178
  path.dirname(config.paths.portWiring),
174
179
  );
@@ -211,6 +216,7 @@ export async function auditProviders(
211
216
  schemaDir,
212
217
  sourceCache,
213
218
  portsSource,
219
+ declaredAppPorts,
214
220
  providerEntries,
215
221
  providerEntryRecords,
216
222
  sourceFile: source.file,
@@ -334,10 +340,12 @@ export async function readProviderPackageMetadataSource(
334
340
  const packageJson = await readJsonIfExists<{
335
341
  beignet?: { provider?: unknown };
336
342
  }>(candidate);
343
+ if (packageJson === undefined) continue;
337
344
  const metadata = packageJson?.beignet?.provider;
338
345
  if (metadata !== undefined) {
339
346
  return { packageName, file: candidate, metadata };
340
347
  }
348
+ return undefined;
341
349
  }
342
350
 
343
351
  return undefined;
@@ -1169,14 +1177,21 @@ function providerPackageJsonCandidates(
1169
1177
  targetDir: string,
1170
1178
  packageName: string,
1171
1179
  ): string[] {
1172
- const candidates = [
1173
- path.join(
1174
- targetDir,
1175
- "node_modules",
1176
- ...packageName.split("/"),
1177
- "package.json",
1178
- ),
1179
- ];
1180
+ const candidates: string[] = [];
1181
+ let directory = path.resolve(targetDir);
1182
+ while (true) {
1183
+ candidates.push(
1184
+ path.join(
1185
+ directory,
1186
+ "node_modules",
1187
+ ...packageName.split("/"),
1188
+ "package.json",
1189
+ ),
1190
+ );
1191
+ const parent = path.dirname(directory);
1192
+ if (parent === directory) break;
1193
+ directory = parent;
1194
+ }
1180
1195
 
1181
1196
  if (packageName.startsWith("@beignet/")) {
1182
1197
  candidates.push(
@@ -1217,6 +1232,7 @@ async function providerAuditEntryForRule(options: {
1217
1232
  schemaDir: string;
1218
1233
  sourceCache: Map<string, string>;
1219
1234
  portsSource: string;
1235
+ declaredAppPorts?: ReadonlySet<string>;
1220
1236
  providerEntries: string[];
1221
1237
  providerEntryRecords: ProviderListEntryRecord[];
1222
1238
  sourceFile: string;
@@ -1288,6 +1304,7 @@ async function providerAuditEntryForRule(options: {
1288
1304
  const appPorts = appPortsAudit(
1289
1305
  options.rule.appPorts ?? [],
1290
1306
  options.portsSource,
1307
+ options.declaredAppPorts,
1291
1308
  );
1292
1309
  const notes = providerAuditNotes({
1293
1310
  metadataStatus: "valid",
@@ -1629,11 +1646,14 @@ function aggregateVariantRequirement(
1629
1646
  function appPortsAudit(
1630
1647
  required: readonly { name: string; type: string }[],
1631
1648
  portsSource: string,
1649
+ declaredAppPorts?: ReadonlySet<string>,
1632
1650
  ): ProviderAppPortsAudit {
1633
1651
  if (required.length === 0) return emptyAppPorts();
1634
1652
 
1635
- const missing = required.filter(
1636
- (port) => !portsSource.includes(`${port.name}:`),
1653
+ const missing = required.filter((port) =>
1654
+ declaredAppPorts
1655
+ ? !declaredAppPorts.has(port.name)
1656
+ : !portsSource.includes(`${port.name}:`),
1637
1657
  );
1638
1658
  return {
1639
1659
  status: missing.length === 0 ? "present" : "missing",
@@ -32,7 +32,7 @@ export type TemplateContext = {
32
32
 
33
33
  export const externalVersions = {
34
34
  biome: "^2.4.14",
35
- next: "^16.3.2",
35
+ next: "^16.3.3",
36
36
  react: "^19.2.5",
37
37
  reactDom: "^19.2.5",
38
38
  tsx: "^4.22.4",
@@ -41,7 +41,7 @@ export const externalVersions = {
41
41
  typesReact: "^19.0.0",
42
42
  typesReactDom: "^19.0.0",
43
43
  zod: "^4.0.0",
44
- tanstackReactQuery: "^5.100.7",
44
+ tanstackReactQuery: "^5.102.0",
45
45
  hookformResolvers: "^5.0.0",
46
46
  reactHookForm: "^7.74.0",
47
47
  betterAuth: "1.7.1",
@@ -58,7 +58,7 @@ export const externalVersions = {
58
58
  resend: "^6.14.0",
59
59
  sentryNode: "^10.58.0",
60
60
  stripe: "^22.2.1",
61
- nodemailer: "^9.0.1",
61
+ nodemailer: "^9.1.1",
62
62
  openFeatureServerSdk: "^1.22.0",
63
63
  typesPg: "^8.11.0",
64
64
  ioredis: "^5.0.0",