@beignet/cli 0.0.51 → 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 (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +182 -19
  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 +9 -2
  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 +41 -11
  10. package/dist/analysis/port-wiring.js.map +1 -1
  11. package/dist/analysis/source-files.d.ts +15 -0
  12. package/dist/analysis/source-files.d.ts.map +1 -0
  13. package/dist/analysis/source-files.js +52 -0
  14. package/dist/analysis/source-files.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +2 -1
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +34 -5
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace-routes.d.ts +8 -0
  20. package/dist/analysis/workspace-routes.d.ts.map +1 -0
  21. package/dist/analysis/workspace-routes.js +291 -0
  22. package/dist/analysis/workspace-routes.js.map +1 -0
  23. package/dist/analysis/workspace.d.ts +24 -3
  24. package/dist/analysis/workspace.d.ts.map +1 -1
  25. package/dist/analysis/workspace.js +184 -43
  26. package/dist/analysis/workspace.js.map +1 -1
  27. package/dist/app-map-changes.d.ts.map +1 -1
  28. package/dist/app-map-changes.js +24 -14
  29. package/dist/app-map-changes.js.map +1 -1
  30. package/dist/app-map.d.ts.map +1 -1
  31. package/dist/app-map.js +87 -81
  32. package/dist/app-map.js.map +1 -1
  33. package/dist/check.d.ts +12 -0
  34. package/dist/check.d.ts.map +1 -1
  35. package/dist/check.js +92 -25
  36. package/dist/check.js.map +1 -1
  37. package/dist/config.d.ts +7 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +19 -0
  40. package/dist/config.js.map +1 -1
  41. package/dist/git-changes.d.ts +17 -1
  42. package/dist/git-changes.d.ts.map +1 -1
  43. package/dist/git-changes.js +118 -84
  44. package/dist/git-changes.js.map +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +23 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/inspect.d.ts.map +1 -1
  49. package/dist/inspect.js +424 -129
  50. package/dist/inspect.js.map +1 -1
  51. package/dist/lint.d.ts.map +1 -1
  52. package/dist/lint.js +108 -43
  53. package/dist/lint.js.map +1 -1
  54. package/dist/make/shared.js +1 -1
  55. package/dist/make.js +18 -1
  56. package/dist/make.js.map +1 -1
  57. package/dist/mcp.js +1 -1
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/provider-add.js +2 -2
  60. package/dist/provider-add.js.map +1 -1
  61. package/dist/provider-audit.d.ts.map +1 -1
  62. package/dist/provider-audit.js +22 -6
  63. package/dist/provider-audit.js.map +1 -1
  64. package/dist/templates/agents.d.ts.map +1 -1
  65. package/dist/templates/agents.js +36 -13
  66. package/dist/templates/agents.js.map +1 -1
  67. package/dist/templates/base.js +5 -5
  68. package/dist/templates/db/mysql.d.ts.map +1 -1
  69. package/dist/templates/db/mysql.js +51 -22
  70. package/dist/templates/db/mysql.js.map +1 -1
  71. package/dist/templates/db/postgres.d.ts.map +1 -1
  72. package/dist/templates/db/postgres.js +60 -20
  73. package/dist/templates/db/postgres.js.map +1 -1
  74. package/dist/templates/db/sqlite.d.ts.map +1 -1
  75. package/dist/templates/db/sqlite.js +51 -23
  76. package/dist/templates/db/sqlite.js.map +1 -1
  77. package/dist/templates/shadcn.js +1 -1
  78. package/dist/templates/shadcn.js.map +1 -1
  79. package/dist/templates/shared.js +6 -6
  80. package/dist/templates/shared.js.map +1 -1
  81. package/dist/templates/testing.d.ts.map +1 -1
  82. package/dist/templates/testing.js +7 -19
  83. package/dist/templates/testing.js.map +1 -1
  84. package/dist/test-discovery.d.ts +9 -0
  85. package/dist/test-discovery.d.ts.map +1 -0
  86. package/dist/test-discovery.js +40 -0
  87. package/dist/test-discovery.js.map +1 -0
  88. package/package.json +8 -5
  89. package/skills/app-structure/SKILL.md +74 -7
  90. package/src/analysis/layers.ts +13 -2
  91. package/src/analysis/port-wiring.ts +68 -11
  92. package/src/analysis/source-files.ts +61 -0
  93. package/src/analysis/source-index.ts +56 -4
  94. package/src/analysis/workspace-routes.ts +385 -0
  95. package/src/analysis/workspace.ts +302 -59
  96. package/src/app-map-changes.ts +41 -13
  97. package/src/app-map.ts +114 -103
  98. package/src/check.ts +115 -25
  99. package/src/config.ts +33 -0
  100. package/src/git-changes.ts +218 -86
  101. package/src/index.ts +33 -5
  102. package/src/inspect.ts +608 -156
  103. package/src/lint.ts +152 -48
  104. package/src/make/shared.ts +1 -1
  105. package/src/make.ts +31 -1
  106. package/src/mcp.ts +1 -1
  107. package/src/provider-add.ts +2 -2
  108. package/src/provider-audit.ts +30 -10
  109. package/src/templates/agents.ts +36 -13
  110. package/src/templates/base.ts +5 -5
  111. package/src/templates/db/mysql.ts +51 -22
  112. package/src/templates/db/postgres.ts +60 -20
  113. package/src/templates/db/sqlite.ts +51 -23
  114. package/src/templates/shadcn.ts +1 -1
  115. package/src/templates/shared.ts +6 -6
  116. package/src/templates/testing.ts +12 -19
  117. package/src/test-discovery.ts +53 -0
package/src/lint.ts CHANGED
@@ -3,15 +3,25 @@ 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
+ import {
10
+ isAnalysisSourceFile,
11
+ isAnalysisSourceFileExtension,
12
+ stripSourceFileExtension,
13
+ } from "./analysis/source-files.js";
9
14
  import {
10
15
  type ModuleReference,
11
16
  moduleReferences,
12
17
  } from "./analysis/source-index.js";
13
18
  import {
19
+ type AnalysisWorkspace,
14
20
  createAnalysisWorkspace,
21
+ findProjectForFile,
22
+ projectForFile,
23
+ projectPath,
24
+ projectSource,
15
25
  resolveLocalModulePath,
16
26
  } from "./analysis/workspace.js";
17
27
  import { createPainter } from "./ansi.js";
@@ -77,12 +87,19 @@ export async function lintApp(
77
87
  const importGraph: ImportGraph = new Map();
78
88
 
79
89
  for (const file of sourceFiles) {
80
- const sourceLayer = classifyPath(file, config);
90
+ const sourceLayer = classifyPath(file, workspace);
91
+ const source = projectSource(workspace, file);
81
92
  const sourceFile = await workspace.readSourceFile(file);
82
93
  sourceFileByPath.set(file, sourceFile);
83
94
  importGraph.set(file, await parseImports(workspace, file));
84
95
 
85
- 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;
86
103
  if (canonicalLayerPath) {
87
104
  diagnostics.push({
88
105
  severity: "error",
@@ -95,10 +112,17 @@ export async function lintApp(
95
112
  });
96
113
  }
97
114
 
115
+ for (const reference of importGraph.get(file) ?? []) {
116
+ const issue = workspaceImportViolation(workspace, file, reference);
117
+ if (issue) diagnostics.push(issue);
118
+ }
98
119
  if (sourceLayer === "test" || sourceLayer === "unknown") continue;
99
120
 
100
- if (isLegacySharedDomainPath(file, config)) {
101
- 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
+ );
102
126
  diagnostics.push({
103
127
  severity: "error",
104
128
  code: "BEIGNET_CANONICAL_STRUCTURE",
@@ -115,7 +139,7 @@ export async function lintApp(
115
139
  lintUnresolvedModuleReference(file, sourceLayer, reference) ??
116
140
  (sourceLayer === "schema"
117
141
  ? undefined
118
- : lintImport(file, sourceLayer, reference, config));
142
+ : lintImport(file, sourceLayer, reference, workspace));
119
143
  if (diagnostic) {
120
144
  diagnostics.push({
121
145
  ...diagnostic,
@@ -128,14 +152,14 @@ export async function lintApp(
128
152
 
129
153
  diagnostics.push(
130
154
  ...lintTransitiveBoundaries({
131
- config,
155
+ workspace,
132
156
  graph: importGraph,
133
157
  }),
134
158
  );
135
159
 
136
160
  diagnostics.push(
137
161
  ...lintRuntimeBoundary({
138
- config,
162
+ workspace,
139
163
  graph: importGraph,
140
164
  sourceFileByPath,
141
165
  }),
@@ -202,10 +226,7 @@ export function formatLintGithub(result: LintAppResult): string {
202
226
  }
203
227
 
204
228
  function lintSourceFiles(files: string[]): string[] {
205
- return files.filter((file) => {
206
- if (/\.d\.(c|m)?ts$/.test(file)) return false;
207
- return /\.(c|m)?tsx?$/.test(file);
208
- });
229
+ return files.filter(isAnalysisSourceFile);
209
230
  }
210
231
 
211
232
  async function parseImports(
@@ -237,22 +258,27 @@ function packageName(importPath: string): string {
237
258
  }
238
259
 
239
260
  function stripKnownExtension(filePath: string): string {
240
- return filePath.replace(/\.(c|m)?tsx?$/, "");
261
+ return stripSourceFileExtension(filePath);
241
262
  }
242
263
 
243
264
  function lintImport(
244
265
  file: string,
245
266
  sourceLayer: SourceLayer,
246
267
  reference: ImportReference,
247
- config: ResolvedBeignetConfig,
268
+ workspace: AnalysisWorkspace,
248
269
  ): LintDiagnostic | undefined {
249
270
  if (reference.kind === "type") return undefined;
250
271
 
251
272
  const targetLayer = reference.resolvedPath
252
- ? classifyPath(reference.resolvedPath, config)
273
+ ? classifyPath(reference.resolvedPath, workspace)
253
274
  : undefined;
254
275
  const featureImport = reference.resolvedPath
255
- ? featureImportViolation(file, sourceLayer, reference.resolvedPath, config)
276
+ ? featureImportViolation(
277
+ file,
278
+ sourceLayer,
279
+ reference.resolvedPath,
280
+ workspace,
281
+ )
256
282
  : undefined;
257
283
  if (featureImport) {
258
284
  return {
@@ -579,16 +605,16 @@ function matchBeignetProviderPackage(importPath: string): string | undefined {
579
605
  }
580
606
 
581
607
  function lintTransitiveBoundaries({
582
- config,
608
+ workspace,
583
609
  graph,
584
610
  }: {
585
- config: ResolvedBeignetConfig;
611
+ workspace: AnalysisWorkspace;
586
612
  graph: ImportGraph;
587
613
  }): LintDiagnostic[] {
588
614
  const diagnostics: LintDiagnostic[] = [];
589
615
 
590
616
  for (const root of graph.keys()) {
591
- const rootLayer = classifyPath(root, config);
617
+ const rootLayer = classifyPath(root, workspace);
592
618
  if (
593
619
  ![
594
620
  "agent-capability",
@@ -608,7 +634,7 @@ function lintTransitiveBoundaries({
608
634
  }
609
635
 
610
636
  diagnostics.push(
611
- ...lintTransitiveBoundaryRoot({ config, graph, root, rootLayer }),
637
+ ...lintTransitiveBoundaryRoot({ workspace, graph, root, rootLayer }),
612
638
  );
613
639
  }
614
640
 
@@ -616,12 +642,12 @@ function lintTransitiveBoundaries({
616
642
  }
617
643
 
618
644
  function lintTransitiveBoundaryRoot({
619
- config,
645
+ workspace,
620
646
  graph,
621
647
  root,
622
648
  rootLayer,
623
649
  }: {
624
- config: ResolvedBeignetConfig;
650
+ workspace: AnalysisWorkspace;
625
651
  graph: ImportGraph;
626
652
  root: string;
627
653
  rootLayer: SourceLayer;
@@ -648,7 +674,7 @@ function lintTransitiveBoundaryRoot({
648
674
  current.file === root
649
675
  ? undefined
650
676
  : (lintUnresolvedModuleReference(root, rootLayer, reference) ??
651
- lintImport(root, rootLayer, reference, config));
677
+ lintImport(root, rootLayer, reference, workspace));
652
678
 
653
679
  if (violation) {
654
680
  diagnostics.push({
@@ -663,7 +689,7 @@ function lintTransitiveBoundaryRoot({
663
689
  reference.resolvedPath &&
664
690
  graph.has(reference.resolvedPath) &&
665
691
  isTransparentArchitectureLayer(
666
- classifyPath(reference.resolvedPath, config),
692
+ classifyPath(reference.resolvedPath, workspace),
667
693
  )
668
694
  ) {
669
695
  queue.push({ file: reference.resolvedPath, chain });
@@ -679,11 +705,11 @@ function isTransparentArchitectureLayer(layer: SourceLayer): boolean {
679
705
  }
680
706
 
681
707
  function lintRuntimeBoundary({
682
- config,
708
+ workspace,
683
709
  graph,
684
710
  sourceFileByPath,
685
711
  }: {
686
- config: ResolvedBeignetConfig;
712
+ workspace: AnalysisWorkspace;
687
713
  graph: ImportGraph;
688
714
  sourceFileByPath: Map<string, ts.SourceFile>;
689
715
  }): LintDiagnostic[] {
@@ -693,7 +719,7 @@ function lintRuntimeBoundary({
693
719
  const rootKind = runtimeBoundaryRootKind(
694
720
  file,
695
721
  sourceFileByPath.get(file),
696
- config,
722
+ workspace,
697
723
  );
698
724
  if (!rootKind) continue;
699
725
 
@@ -701,7 +727,7 @@ function lintRuntimeBoundary({
701
727
  ...lintRuntimeBoundaryRoot({
702
728
  root: file,
703
729
  rootKind,
704
- config,
730
+ workspace,
705
731
  graph,
706
732
  }),
707
733
  );
@@ -713,12 +739,12 @@ function lintRuntimeBoundary({
713
739
  function lintRuntimeBoundaryRoot({
714
740
  root,
715
741
  rootKind,
716
- config,
742
+ workspace,
717
743
  graph,
718
744
  }: {
719
745
  root: string;
720
746
  rootKind: RuntimeBoundaryRootKind;
721
- config: ResolvedBeignetConfig;
747
+ workspace: AnalysisWorkspace;
722
748
  graph: ImportGraph;
723
749
  }): LintDiagnostic[] {
724
750
  const diagnostics: LintDiagnostic[] = [];
@@ -744,7 +770,11 @@ function lintRuntimeBoundaryRoot({
744
770
  const chain = reference.resolvedPath
745
771
  ? [...current.chain, reference.resolvedPath]
746
772
  : current.chain;
747
- const violation = runtimeBoundaryViolation(reference, config, rootKind);
773
+ const violation = runtimeBoundaryViolation(
774
+ reference,
775
+ workspace,
776
+ rootKind,
777
+ );
748
778
 
749
779
  if (violation) {
750
780
  diagnostics.push({
@@ -772,14 +802,16 @@ function lintRuntimeBoundaryRoot({
772
802
  function runtimeBoundaryRootKind(
773
803
  file: string,
774
804
  sourceFile: ts.SourceFile | undefined,
775
- config: ResolvedBeignetConfig,
805
+ workspace: AnalysisWorkspace,
776
806
  ): RuntimeBoundaryRootKind | undefined {
777
- const layer = classifyPath(file, config);
807
+ const layer = classifyPath(file, workspace);
778
808
  if (layer === "contract") return "contract";
779
809
  if (layer === "schema") return "schema";
780
810
  if (layer === "client") return "client";
781
811
  if (sourceFile && hasUseClientDirective(sourceFile)) return "client";
782
- if (isGeneratedClientAdapter(file, config)) return "client";
812
+ const source = projectSource(workspace, file);
813
+ if (isGeneratedClientAdapter(source.file, source.project.config))
814
+ return "client";
783
815
  return undefined;
784
816
  }
785
817
 
@@ -808,7 +840,7 @@ function isGeneratedClientAdapter(
808
840
 
809
841
  function runtimeBoundaryViolation(
810
842
  reference: ImportReference,
811
- config: ResolvedBeignetConfig,
843
+ workspace: AnalysisWorkspace,
812
844
  rootKind: RuntimeBoundaryRootKind,
813
845
  ): string | undefined {
814
846
  if (!reference.staticallyResolvable) {
@@ -816,7 +848,7 @@ function runtimeBoundaryViolation(
816
848
  }
817
849
 
818
850
  if (reference.resolvedPath) {
819
- const targetLayer = classifyPath(reference.resolvedPath, config);
851
+ const targetLayer = classifyPath(reference.resolvedPath, workspace);
820
852
  const prohibitedLayer =
821
853
  rootKind === "client"
822
854
  ? undefined
@@ -900,25 +932,30 @@ function featureImportViolation(
900
932
  file: string,
901
933
  sourceLayer: SourceLayer,
902
934
  resolvedPath: string,
903
- config: ResolvedBeignetConfig,
935
+ workspace: AnalysisWorkspace,
904
936
  ): string | undefined {
905
937
  if (sourceLayer !== "domain") return undefined;
906
938
 
907
- const sourceFeature = featureLayerInfo(file, config);
908
- const targetFeature = featureLayerInfo(resolvedPath, config);
939
+ const sourceFeature = featureLayerInfo(file, workspace);
940
+ const targetFeature = featureLayerInfo(resolvedPath, workspace);
909
941
  if (!sourceFeature || !targetFeature) return undefined;
910
942
  if (sourceFeature.layer !== "domain" || targetFeature.layer !== "domain") {
911
943
  return undefined;
912
944
  }
913
- if (sourceFeature.feature === targetFeature.feature) return undefined;
945
+ if (
946
+ sourceFeature.feature === targetFeature.feature &&
947
+ sourceFeature.owner === targetFeature.owner
948
+ )
949
+ return undefined;
914
950
  if (targetFeature.feature === "shared") return undefined;
915
951
 
916
952
  return `feature-specific domain files must not import another feature's domain (${stripKnownExtension(
917
953
  resolvedPath,
918
- )}). Move shared-kernel domain concepts to ${sharedDomainPath(config)}.`;
954
+ )}). Move shared-kernel domain concepts to ${sharedDomainPath(projectForFile(workspace, file).config)}.`;
919
955
  }
920
956
 
921
957
  const domainBannedPackages = new Set([
958
+ "@beignet/core/encryption",
922
959
  "@aws-sdk/client-s3",
923
960
  "@aws-sdk/lib-storage",
924
961
  "@aws-sdk/s3-request-presigner",
@@ -927,12 +964,14 @@ const domainBannedPackages = new Set([
927
964
  "@beignet/core/errors",
928
965
  "@beignet/next",
929
966
  "@beignet/core/ports",
967
+ "@beignet/react-form",
930
968
  "@beignet/react-hook-form",
931
969
  "@beignet/react-query",
932
970
  "@hookform/resolvers",
933
971
  "@libsql/client",
934
972
  "@neondatabase/serverless",
935
973
  "@planetscale/database",
974
+ "@tanstack/react-form",
936
975
  "@tanstack/react-query",
937
976
  "@upstash/ratelimit",
938
977
  "@upstash/redis",
@@ -959,12 +998,14 @@ const coreBannedPackages = new Set([
959
998
  "@aws-sdk/lib-storage",
960
999
  "@aws-sdk/s3-request-presigner",
961
1000
  "@beignet/core/client",
1001
+ "@beignet/react-form",
962
1002
  "@beignet/react-hook-form",
963
1003
  "@beignet/react-query",
964
1004
  "@hookform/resolvers",
965
1005
  "@libsql/client",
966
1006
  "@neondatabase/serverless",
967
1007
  "@planetscale/database",
1008
+ "@tanstack/react-form",
968
1009
  "@tanstack/react-query",
969
1010
  "@upstash/ratelimit",
970
1011
  "@upstash/redis",
@@ -987,6 +1028,7 @@ const coreBannedPackages = new Set([
987
1028
  ]);
988
1029
 
989
1030
  const serverRuntimePackages = new Set([
1031
+ "@beignet/core/encryption",
990
1032
  "@beignet/devtools",
991
1033
  "@beignet/next",
992
1034
  "@beignet/core/openapi",
@@ -1032,19 +1074,22 @@ function featureRelativePath(
1032
1074
 
1033
1075
  function featureLayerInfo(
1034
1076
  filePath: string,
1035
- config: ResolvedBeignetConfig,
1036
- ): { feature: string; layer: SourceLayer } | undefined {
1037
- 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);
1038
1081
  const relativePath = featureRelativePath(
1039
1082
  normalizedPath,
1040
- directoryPath(config.paths.features),
1083
+ directoryPath(source.project.config.paths.features),
1041
1084
  );
1042
1085
  if (!relativePath) return undefined;
1043
-
1044
1086
  const [feature] = relativePath.split("/");
1045
1087
  if (!feature) return undefined;
1046
-
1047
- return { feature, layer: classifyPath(filePath, config) };
1088
+ return {
1089
+ feature,
1090
+ owner: source.project.prefix,
1091
+ layer: classifyPath(filePath, workspace),
1092
+ };
1048
1093
  }
1049
1094
 
1050
1095
  function legacySharedDomainPath(config: ResolvedBeignetConfig): string {
@@ -1091,3 +1136,62 @@ function dedupeDiagnostics(diagnostics: LintDiagnostic[]): LintDiagnostic[] {
1091
1136
 
1092
1137
  return deduped;
1093
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"),
package/src/mcp.ts CHANGED
@@ -687,7 +687,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
687
687
  "check",
688
688
  {
689
689
  description:
690
- "Run the full Beignet app validation loop: Beignet lint, strict doctor, optional disconnected or connected preflight, 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.",
690
+ "Run the full Beignet app validation loop: Beignet lint, strict doctor, optional disconnected or connected preflight, 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, source-state provenance, and overall ok status. An ending fingerprint that differs from the baseline makes the result stale and unsuccessful. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
691
691
  inputSchema: {
692
692
  preflight: z
693
693
  .boolean()
@@ -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",