@autoview/cli 0.1.0

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 (297) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +407 -0
  3. package/lib/AutoViewAgent.d.ts +109 -0
  4. package/lib/AutoViewAgent.js +123 -0
  5. package/lib/AutoViewAgent.js.map +1 -0
  6. package/lib/agent/emitMcpServer.d.ts +15 -0
  7. package/lib/agent/emitMcpServer.js +157 -0
  8. package/lib/agent/emitMcpServer.js.map +1 -0
  9. package/lib/agent/emitReport.d.ts +14 -0
  10. package/lib/agent/emitReport.js +85 -0
  11. package/lib/agent/emitReport.js.map +1 -0
  12. package/lib/agent/toolSurface.d.ts +130 -0
  13. package/lib/agent/toolSurface.js +342 -0
  14. package/lib/agent/toolSurface.js.map +1 -0
  15. package/lib/agent/verifyAgentTasks.d.ts +87 -0
  16. package/lib/agent/verifyAgentTasks.js +126 -0
  17. package/lib/agent/verifyAgentTasks.js.map +1 -0
  18. package/lib/cli/main.d.ts +2 -0
  19. package/lib/cli/main.js +295 -0
  20. package/lib/cli/main.js.map +1 -0
  21. package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
  22. package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
  23. package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
  24. package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
  25. package/lib/constants/AutoViewFrontendTemplate.js +46 -0
  26. package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
  27. package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
  28. package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
  29. package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
  30. package/lib/context/IAutoViewAgentContext.d.ts +60 -0
  31. package/lib/context/IAutoViewAgentContext.js +3 -0
  32. package/lib/context/IAutoViewAgentContext.js.map +1 -0
  33. package/lib/fromSwagger.d.ts +53 -0
  34. package/lib/fromSwagger.js +513 -0
  35. package/lib/fromSwagger.js.map +1 -0
  36. package/lib/generateDeterministic.d.ts +26 -0
  37. package/lib/generateDeterministic.js +75 -0
  38. package/lib/generateDeterministic.js.map +1 -0
  39. package/lib/index.d.ts +15 -0
  40. package/lib/index.js +41 -0
  41. package/lib/index.js.map +1 -0
  42. package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
  43. package/lib/orchestrate/orchestrateAutoView.js +491 -0
  44. package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
  45. package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
  46. package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
  47. package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
  48. package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
  49. package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
  50. package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
  51. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
  52. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
  53. package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
  54. package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
  55. package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
  56. package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
  57. package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
  58. package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
  59. package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
  60. package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
  61. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
  62. package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
  63. package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
  64. package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
  65. package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
  66. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
  67. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
  68. package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
  69. package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
  70. package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
  71. package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
  72. package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
  73. package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
  74. package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
  75. package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
  76. package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
  77. package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
  78. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
  79. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
  80. package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
  81. package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
  82. package/lib/orchestrate/utils/HistoryMessage.js +25 -0
  83. package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
  84. package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
  85. package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
  86. package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
  87. package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
  88. package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
  89. package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
  90. package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
  91. package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
  92. package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
  93. package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
  94. package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
  95. package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
  96. package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
  97. package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
  98. package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
  99. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
  100. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
  101. package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
  102. package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
  103. package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
  104. package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
  105. package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
  106. package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
  107. package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
  108. package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
  109. package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
  110. package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
  111. package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
  112. package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
  113. package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
  114. package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
  115. package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
  116. package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
  117. package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
  118. package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
  119. package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
  120. package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
  121. package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
  122. package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
  123. package/lib/preview/renderPreview.d.ts +22 -0
  124. package/lib/preview/renderPreview.js +198 -0
  125. package/lib/preview/renderPreview.js.map +1 -0
  126. package/lib/typings/compiler.d.ts +39 -0
  127. package/lib/typings/compiler.js +3 -0
  128. package/lib/typings/compiler.js.map +1 -0
  129. package/lib/typings/events.d.ts +106 -0
  130. package/lib/typings/events.js +3 -0
  131. package/lib/typings/events.js.map +1 -0
  132. package/lib/typings/index.d.ts +10 -0
  133. package/lib/typings/index.js +27 -0
  134. package/lib/typings/index.js.map +1 -0
  135. package/lib/typings/misc.d.ts +78 -0
  136. package/lib/typings/misc.js +3 -0
  137. package/lib/typings/misc.js.map +1 -0
  138. package/lib/utils/ArrayUtil.d.ts +8 -0
  139. package/lib/utils/ArrayUtil.js +30 -0
  140. package/lib/utils/ArrayUtil.js.map +1 -0
  141. package/lib/utils/StringUtil.d.ts +11 -0
  142. package/lib/utils/StringUtil.js +28 -0
  143. package/lib/utils/StringUtil.js.map +1 -0
  144. package/lib/utils/classifyEndpoints.d.ts +62 -0
  145. package/lib/utils/classifyEndpoints.js +216 -0
  146. package/lib/utils/classifyEndpoints.js.map +1 -0
  147. package/lib/utils/endpointFilter.d.ts +26 -0
  148. package/lib/utils/endpointFilter.js +0 -0
  149. package/lib/utils/endpointFilter.js.map +1 -0
  150. package/lib/utils/extractFields.d.ts +85 -0
  151. package/lib/utils/extractFields.js +231 -0
  152. package/lib/utils/extractFields.js.map +1 -0
  153. package/lib/utils/index.d.ts +13 -0
  154. package/lib/utils/index.js +30 -0
  155. package/lib/utils/index.js.map +1 -0
  156. package/lib/utils/normalizeForNestia.d.ts +34 -0
  157. package/lib/utils/normalizeForNestia.js +133 -0
  158. package/lib/utils/normalizeForNestia.js.map +1 -0
  159. package/lib/utils/resourcePlan.d.ts +39 -0
  160. package/lib/utils/resourcePlan.js +95 -0
  161. package/lib/utils/resourcePlan.js.map +1 -0
  162. package/lib/utils/sliceDocument.d.ts +17 -0
  163. package/lib/utils/sliceDocument.js +114 -0
  164. package/lib/utils/sliceDocument.js.map +1 -0
  165. package/lib/utils/toEndpoints.d.ts +90 -0
  166. package/lib/utils/toEndpoints.js +227 -0
  167. package/lib/utils/toEndpoints.js.map +1 -0
  168. package/lib/verify/runWorkflows.d.ts +25 -0
  169. package/lib/verify/runWorkflows.js +366 -0
  170. package/lib/verify/runWorkflows.js.map +1 -0
  171. package/lib/verify/workflows.d.ts +53 -0
  172. package/lib/verify/workflows.js +107 -0
  173. package/lib/verify/workflows.js.map +1 -0
  174. package/package.json +82 -0
  175. package/prompts/AUTOVIEW_RENDER.md +398 -0
  176. package/prompts/AUTOVIEW_REVIEW.md +60 -0
  177. package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
  178. package/src/AutoViewAgent.ts +222 -0
  179. package/src/agent/emitMcpServer.integration.test.ts +168 -0
  180. package/src/agent/emitMcpServer.test.ts +51 -0
  181. package/src/agent/emitMcpServer.ts +178 -0
  182. package/src/agent/emitReport.ts +117 -0
  183. package/src/agent/toolSurface.test.ts +243 -0
  184. package/src/agent/toolSurface.ts +501 -0
  185. package/src/agent/verifyAgentTasks.test.ts +106 -0
  186. package/src/agent/verifyAgentTasks.ts +171 -0
  187. package/src/cli/main.ts +363 -0
  188. package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
  189. package/src/constants/AutoViewFrontendTemplate.ts +42 -0
  190. package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
  191. package/src/context/IAutoViewAgentContext.ts +84 -0
  192. package/src/fromSwagger.test.ts +269 -0
  193. package/src/fromSwagger.ts +500 -0
  194. package/src/generateDeterministic.test.ts +39 -0
  195. package/src/generateDeterministic.ts +77 -0
  196. package/src/index.ts +30 -0
  197. package/src/orchestrate/orchestrateAutoView.ts +590 -0
  198. package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
  199. package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
  200. package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
  201. package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
  202. package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
  203. package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
  204. package/src/orchestrate/renderNavTs.test.ts +74 -0
  205. package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
  206. package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
  207. package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
  208. package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
  209. package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
  210. package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
  211. package/src/orchestrate/utils/HistoryMessage.ts +41 -0
  212. package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
  213. package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
  214. package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
  215. package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
  216. package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
  217. package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
  218. package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
  219. package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
  220. package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
  221. package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
  222. package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
  223. package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
  224. package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
  225. package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
  226. package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
  227. package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
  228. package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
  229. package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
  230. package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
  231. package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
  232. package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
  233. package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
  234. package/src/preview/renderPreview.ts +273 -0
  235. package/src/typings/compiler.ts +47 -0
  236. package/src/typings/events.ts +155 -0
  237. package/src/typings/index.ts +10 -0
  238. package/src/typings/misc.ts +93 -0
  239. package/src/utils/ArrayUtil.ts +16 -0
  240. package/src/utils/StringUtil.ts +29 -0
  241. package/src/utils/classifyEndpoints.test.ts +86 -0
  242. package/src/utils/classifyEndpoints.ts +291 -0
  243. package/src/utils/endpointFilter.test.ts +50 -0
  244. package/src/utils/endpointFilter.ts +0 -0
  245. package/src/utils/extractFields.test.ts +82 -0
  246. package/src/utils/extractFields.ts +306 -0
  247. package/src/utils/index.ts +13 -0
  248. package/src/utils/normalizeForNestia.test.ts +93 -0
  249. package/src/utils/normalizeForNestia.ts +139 -0
  250. package/src/utils/resourcePlan.test.ts +104 -0
  251. package/src/utils/resourcePlan.ts +180 -0
  252. package/src/utils/sliceDocument.test.ts +85 -0
  253. package/src/utils/sliceDocument.ts +119 -0
  254. package/src/utils/toEndpoints.test.ts +251 -0
  255. package/src/utils/toEndpoints.ts +343 -0
  256. package/src/verify/runWorkflows.ts +403 -0
  257. package/src/verify/workflows.test.ts +117 -0
  258. package/src/verify/workflows.ts +154 -0
  259. package/template/CLAUDE.md +140 -0
  260. package/template/Dockerfile +31 -0
  261. package/template/PROMPT.md +80 -0
  262. package/template/SANDBOX.md +70 -0
  263. package/template/app/api/health/route.ts +10 -0
  264. package/template/app/globals.css +97 -0
  265. package/template/app/layout.tsx +30 -0
  266. package/template/app/page.tsx +19 -0
  267. package/template/components/AppShell.tsx +114 -0
  268. package/template/components/auto/CatalogGrid.tsx +159 -0
  269. package/template/components/auto/ConfirmButton.tsx +67 -0
  270. package/template/components/auto/EmbeddedCollection.tsx +144 -0
  271. package/template/components/auto/ResourceDashboard.tsx +104 -0
  272. package/template/components/auto/ResourceDetail.tsx +93 -0
  273. package/template/components/auto/ResourceForm.tsx +235 -0
  274. package/template/components/auto/ResourceIcon.tsx +88 -0
  275. package/template/components/auto/ResourceLanding.tsx +155 -0
  276. package/template/components/auto/ResourceTable.tsx +223 -0
  277. package/template/components/auto/formatValue.tsx +186 -0
  278. package/template/components/auto/types.ts +42 -0
  279. package/template/components/ui/badge.tsx +40 -0
  280. package/template/components/ui/button.tsx +57 -0
  281. package/template/components/ui/card.tsx +86 -0
  282. package/template/components/ui/dialog.tsx +119 -0
  283. package/template/components/ui/input.tsx +23 -0
  284. package/template/components/ui/label.tsx +24 -0
  285. package/template/components/ui/pagination.tsx +117 -0
  286. package/template/components/ui/select.tsx +92 -0
  287. package/template/components/ui/sheet.tsx +135 -0
  288. package/template/components/ui/skeleton.tsx +15 -0
  289. package/template/components/ui/table.tsx +120 -0
  290. package/template/components/ui/tabs.tsx +55 -0
  291. package/template/lib/utils.ts +35 -0
  292. package/template/next.config.mjs +52 -0
  293. package/template/package.json +46 -0
  294. package/template/postcss.config.js +6 -0
  295. package/template/scripts/start-shopping-backend.sh +56 -0
  296. package/template/tailwind.config.ts +96 -0
  297. package/template/tsconfig.json +29 -0
@@ -0,0 +1,1415 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderResourcePage = renderResourcePage;
4
+ exports.responseCollection = responseCollection;
5
+ const extractFields_1 = require("../../utils/extractFields");
6
+ /**
7
+ * Deterministic page generator — the replacement for the LLM Render phase.
8
+ *
9
+ * Given one planned screen and the operations it composes, emit a complete,
10
+ * runnable `page.tsx` that wires the typed SDK call to one of the universal
11
+ * `Resource*` components (table / detail / form / landing). The component set is
12
+ * static (shipped in the template); this only produces the thin per-route page:
13
+ *
14
+ * - column / field metadata baked from {@link extractFields} (every property of
15
+ * the row / response / request type → one column / row / input — dense and
16
+ * complete by construction, never an LLM-chosen subset),
17
+ * - a typed SDK call whose `props` argument is cast to
18
+ * `Parameters<typeof api.functional.X.method>[1]` so the generated project
19
+ * typechecks against Nestia's exact generated types without this generator
20
+ * needing to import them.
21
+ *
22
+ * Same swagger → same pages, every run. No LLM, no retries, no typecheck
23
+ * roulette.
24
+ */
25
+ function renderResourcePage(screen, endpoints, document, allScreens) {
26
+ switch (screen.uiPattern) {
27
+ case "landing":
28
+ return renderLanding(screen, allScreens, endpoints, document);
29
+ case "table":
30
+ return renderTable(screen, endpoints, document, allScreens);
31
+ case "catalog":
32
+ return renderCatalog(screen, endpoints, document, allScreens);
33
+ case "detail":
34
+ return renderDetail(screen, endpoints, document, allScreens);
35
+ case "form":
36
+ return renderForm(screen, endpoints, document);
37
+ default:
38
+ // dashboard / wizard not modeled deterministically yet — fall back to a
39
+ // table off the primary endpoint so the screen still shows real data.
40
+ return renderTable(screen, endpoints, document, allScreens);
41
+ }
42
+ }
43
+ /* -------------------------------------------------------------------------- */
44
+ /* shared helpers */
45
+ /* -------------------------------------------------------------------------- */
46
+ const lit = (value) => JSON.stringify(value);
47
+ /** A JS property name reachable with dot notation. */
48
+ const JS_IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
49
+ /**
50
+ * Emit a member access that is valid even when the key is not a JS identifier.
51
+ * Inline response collections key the array off whatever the API named it, and
52
+ * real swaggers use names like `1-clicks` (DigitalOcean) that nestia sanitizes
53
+ * in the SDK accessor (`_1_clicks`) but that survive verbatim as the response
54
+ * object key (`res["1_clicks"]`). Dot access there is a hard syntax error, so
55
+ * fall back to bracket access for any non-identifier key.
56
+ */
57
+ function memberOf(base, key) {
58
+ return JS_IDENT.test(key) ? `${base}.${key}` : `${base}[${lit(key)}]`;
59
+ }
60
+ /** Turn an accessor segment (`cancel`, `bulkErase`) into a button label. */
61
+ function titleizeSegment(segment) {
62
+ const spaced = segment
63
+ .replace(/[_-]+/g, " ")
64
+ .replace(/([a-z])([A-Z])/g, "$1 $2");
65
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
66
+ }
67
+ /** Primary operation for a screen = the first accessor in `endpoints`. */
68
+ function primaryOp(screen, endpoints) {
69
+ var _a;
70
+ const primary = screen.endpoints[0];
71
+ if (primary === undefined)
72
+ return null;
73
+ return ((_a = endpoints.find((op) => op.accessor.join(".") === primary)) !== null && _a !== void 0 ? _a : null);
74
+ }
75
+ /** Field names that identify a record — shown first in a dense table. */
76
+ const LABEL_NAMES = new Set([
77
+ "name",
78
+ "title",
79
+ "label",
80
+ "code",
81
+ "nickname",
82
+ "email",
83
+ "status",
84
+ "state",
85
+ "type",
86
+ ]);
87
+ /** Column display rank: readable scalars first, heavy nested shapes last. */
88
+ function columnRank(f) {
89
+ const n = f.name.toLowerCase();
90
+ if (LABEL_NAMES.has(n))
91
+ return 0;
92
+ if (n === "id" || n.endsWith("_id"))
93
+ return 1;
94
+ if (f.kind === "enum" || f.kind === "boolean")
95
+ return 2;
96
+ if (f.kind === "number")
97
+ return 3;
98
+ if (f.kind === "string")
99
+ return 4; // includes dates
100
+ // ref / object / array / union / unknown — the noisy `{…}` / count cells last.
101
+ return 6;
102
+ }
103
+ /**
104
+ * Order table columns so the human-readable fields (name, status, code, id,
105
+ * enums, dates) come first and the heavy nested shapes (refs, objects, arrays)
106
+ * trail at the end. Stable within a rank, so the swagger's field order is kept
107
+ * as the tiebreaker. Keeps EVERY column (density) but makes the left edge — what
108
+ * the user sees without scrolling — the useful part.
109
+ */
110
+ function orderColumns(fields) {
111
+ return fields
112
+ .map((f, i) => ({ f, i }))
113
+ .sort((a, b) => columnRank(a.f) - columnRank(b.f) || a.i - b.i)
114
+ .map((x) => x.f);
115
+ }
116
+ /** Subset of a field spec carried into the page as a `ColumnSpec` literal. */
117
+ /** A type name's base, dropping the variant suffix: `IShoppingSale.ISummary`
118
+ * → `IShoppingSale`, so a ref field's variant still resolves to its resource. */
119
+ function baseTypeName(name) {
120
+ var _a;
121
+ return (_a = name.split(".")[0]) !== null && _a !== void 0 ? _a : name;
122
+ }
123
+ /**
124
+ * Map a record TYPE to the list route whose detail screen renders it, so a ref
125
+ * field holding that type can link to its detail page. Built from the detail
126
+ * screens' primary response types — purely structural, no domain knowledge.
127
+ */
128
+ function resourceLinkIndex(allScreens, endpoints) {
129
+ var _a, _b;
130
+ const index = new Map();
131
+ for (const s of allScreens) {
132
+ if (s.uiPattern !== "detail")
133
+ continue;
134
+ const typeName = (_b = (_a = primaryOp(s, endpoints)) === null || _a === void 0 ? void 0 : _a.responseBody) === null || _b === void 0 ? void 0 : _b.typeName;
135
+ if (typeName === undefined)
136
+ continue;
137
+ const base = s.path.replace(/\/\[[^\]]+\]$/, ""); // /sales/[id] → /sales
138
+ // Only top-level resources: a nested detail's base still carries unfilled
139
+ // parent brackets (`/channels/[channelsId]/categories`), which would make
140
+ // `${base}/${id}` a broken bracket href. Link only to clean routes.
141
+ if (base.includes("["))
142
+ continue;
143
+ const key = baseTypeName(typeName);
144
+ if (!index.has(key))
145
+ index.set(key, base);
146
+ }
147
+ return index;
148
+ }
149
+ function toColumnSpec(f, linkIndex) {
150
+ const spec = { name: f.name, kind: f.kind };
151
+ if (f.format !== undefined)
152
+ spec.format = f.format;
153
+ if (f.ref !== undefined)
154
+ spec.ref = f.ref;
155
+ if (f.itemKind !== undefined)
156
+ spec.itemKind = f.itemKind;
157
+ if (f.enumValues !== undefined)
158
+ spec.enumValues = f.enumValues;
159
+ // A ref whose type resolves to a browsable resource becomes a link to that
160
+ // resource's detail (`/sellers/${value.id}`). Skipped for catalog cards
161
+ // (which are already wrapped in a link) by not passing an index there.
162
+ if (linkIndex !== undefined && f.kind === "ref" && f.ref !== undefined) {
163
+ const base = linkIndex.get(baseTypeName(f.ref));
164
+ if (base !== undefined)
165
+ spec.hrefBase = base;
166
+ }
167
+ return spec;
168
+ }
169
+ function toFieldInput(f) {
170
+ return Object.assign(Object.assign({}, toColumnSpec(f)), { required: f.required });
171
+ }
172
+ /** A runtime default literal for a required field, so live calls don't 400. */
173
+ function defaultLiteralForField(f) {
174
+ switch (f.kind) {
175
+ case "number":
176
+ return "0";
177
+ case "boolean":
178
+ return "false";
179
+ case "enum":
180
+ return f.enumValues && f.enumValues.length > 0
181
+ ? lit(f.enumValues[0])
182
+ : '""';
183
+ case "string":
184
+ return '""';
185
+ case "array":
186
+ return "[]";
187
+ default:
188
+ return "{}";
189
+ }
190
+ }
191
+ /** Required-field defaults for a named component schema, as an object literal. */
192
+ function bodyDefaultsLiteral(typeName, doc) {
193
+ const required = (0, extractFields_1.extractFields)(typeName, doc).filter((f) => f.required);
194
+ if (required.length === 0)
195
+ return "{}";
196
+ const parts = required.map((f) => `${lit(f.name)}: ${defaultLiteralForField(f)}`);
197
+ return `{ ${parts.join(", ")} }`;
198
+ }
199
+ /** A default literal for a query schema property. */
200
+ function defaultFromSchema(schema, doc) {
201
+ var _a, _b, _c;
202
+ // A schema-declared `default` wins — it is the value the API author intended
203
+ // (e.g. petstore's `status` defaults to `available`), and a missing query
204
+ // param the server treats as required would otherwise 400.
205
+ const declared = schema.default;
206
+ if (declared !== undefined)
207
+ return lit(declared);
208
+ // A bare `const` (a single-value enum / discriminant, `{ const: "pending" }`)
209
+ // is itself the concrete value.
210
+ if ("const" in schema)
211
+ return lit(schema.const);
212
+ if ("$ref" in schema && typeof schema.$ref === "string") {
213
+ // Resolve a named enum/type so its value can be read, when we have the doc.
214
+ const target = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc.components) === null || _a === void 0 ? void 0 : _a.schemas) === null || _b === void 0 ? void 0 : _b[(_c = schema.$ref.split("/").pop()) !== null && _c !== void 0 ? _c : ""];
215
+ return target !== undefined ? defaultFromSchema(target, doc) : "{}";
216
+ }
217
+ if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
218
+ // upgradeDocument normalizes a string enum into `oneOf: [{ const }, …]` —
219
+ // the first non-null const is a safe concrete value.
220
+ for (const branch of schema.oneOf) {
221
+ if (branch && "const" in branch)
222
+ return lit(branch.const);
223
+ }
224
+ return "{}";
225
+ }
226
+ if (!("type" in schema))
227
+ return "{}";
228
+ switch (schema.type) {
229
+ case "boolean":
230
+ return "false";
231
+ case "integer":
232
+ case "number":
233
+ return "0";
234
+ case "string": {
235
+ const enumValues = schema.enum;
236
+ if (Array.isArray(enumValues) && enumValues.length > 0)
237
+ return lit(enumValues[0]);
238
+ return '""';
239
+ }
240
+ case "array":
241
+ return "[]";
242
+ default:
243
+ return "{}";
244
+ }
245
+ }
246
+ /** True when a property carries a usable default-ish value (default or enum). */
247
+ function hasUsableDefault(schema) {
248
+ if (schema.default !== undefined)
249
+ return true;
250
+ if ("const" in schema)
251
+ return true;
252
+ const enumValues = schema.enum;
253
+ if (Array.isArray(enumValues) && enumValues.length > 0)
254
+ return true;
255
+ if ("oneOf" in schema && Array.isArray(schema.oneOf))
256
+ return schema.oneOf.some((b) => b && "const" in b);
257
+ return false;
258
+ }
259
+ /**
260
+ * Default literal for an SDK call's `query` object. Resolves `$ref` / `allOf`
261
+ * (nestia promotes a query into a named component like `IApiX.GetQuery`, so the
262
+ * schema is a `$ref` with no inline `properties`), then fills every property
263
+ * that is REQUIRED or carries a schema default. The latter matters because a
264
+ * spec-optional param with a `default` is often server-required (petstore's
265
+ * `status`) — sending it keeps the list call from 400-ing on first load.
266
+ */
267
+ function queryDefaultsLiteral(schema, doc) {
268
+ const { properties, required } = (0, extractFields_1.resolveProperties)(schema, doc);
269
+ const requiredSet = new Set(required);
270
+ const parts = [];
271
+ for (const [name, prop] of Object.entries(properties)) {
272
+ if (!requiredSet.has(name) && !hasUsableDefault(prop))
273
+ continue;
274
+ parts.push(`${lit(name)}: ${defaultFromSchema(prop, doc)}`);
275
+ }
276
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : "{}";
277
+ }
278
+ /**
279
+ * Build the `props` object literal for an SDK call.
280
+ *
281
+ * - path parameters reference the `const <name>` variables the page declares
282
+ * from `useParams()`,
283
+ * - `query` / `body` are filled with required-field defaults (or, for a form,
284
+ * the live `values` object),
285
+ *
286
+ * Returns `null` when the operation takes neither params, query, nor body — the
287
+ * caller then emits `fn(connection)` with no second argument (Nestia drops it).
288
+ */
289
+ function buildProps(op, doc, opts = {}) {
290
+ var _a;
291
+ const parts = [];
292
+ for (const p of op.parameters)
293
+ parts.push(`${p.name}: ${p.name}`);
294
+ if (op.query !== null)
295
+ parts.push(`query: ${queryDefaultsLiteral(op.query, doc)}`);
296
+ if (op.requestBody !== null) {
297
+ parts.push(`body: ${(_a = opts.bodyExpr) !== null && _a !== void 0 ? _a : bodyDefaultsLiteral(op.requestBody.typeName, doc)}`);
298
+ }
299
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : null;
300
+ }
301
+ /**
302
+ * `api.functional.<accessor>(connection[, props])`. The props argument is cast
303
+ * through `unknown` to `Parameters<typeof fn>[1]` — a path param sourced from
304
+ * `useParams()` is a string, while the SDK may type an id as `number`, and a
305
+ * form body is a `Record`, while the SDK types it as a specific interface.
306
+ * Casting through `unknown` guarantees the generated project compiles against
307
+ * ANY swagger's exact Nestia types; the values themselves are schema-correct
308
+ * (numeric params are coerced with `Number(...)`, bodies use schema defaults).
309
+ */
310
+ function buildCall(op, props) {
311
+ const fn = `api.functional.${op.accessor.join(".")}`;
312
+ if (props === null)
313
+ return `${fn}(connection)`;
314
+ return `${fn}(connection, (${props}) as unknown as Parameters<typeof ${fn}>[1])`;
315
+ }
316
+ /** Dynamic route segment names in path order: `/sales/[id]/questions` → ["id"]. */
317
+ function routeParamNames(routePath) {
318
+ return [...routePath.matchAll(/\[([^\]]+)\]/g)].map((m) => m[1]);
319
+ }
320
+ /**
321
+ * Map each of an operation's path params to a route segment, aligned to the
322
+ * TAIL of the route. The route segment name and the SDK param name can differ —
323
+ * the route is `/sales/[id]/questions` while the SDK function expects `saleId` —
324
+ * so we map by position rather than by name. Tail-alignment matters when the
325
+ * screen route carries more `[segment]`s than the op has params: a resource may
326
+ * be nested under an ancestor it does not parametrize (GitHub's flat
327
+ * `/projects/columns/{column_id}` rendered under
328
+ * `/projects/[projectsId]/columns/[columnsId]`). The op's own params are always
329
+ * the DEEPEST segments, so the leading ancestor segments are the unmapped ones —
330
+ * never the op's id, which would otherwise be sourced from the wrong segment.
331
+ */
332
+ function paramSourceMap(op, routePath) {
333
+ const routeNames = routeParamNames(routePath);
334
+ const offset = Math.max(0, routeNames.length - op.parameters.length);
335
+ const map = new Map();
336
+ op.parameters.forEach((p, i) => { var _a; return map.set(p.name, (_a = routeNames[offset + i]) !== null && _a !== void 0 ? _a : p.name); });
337
+ return map;
338
+ }
339
+ /** Path param names typed as `integer`/`number` by the schema (so the page
340
+ * coerces them with `Number(...)`, not `String(...)` — DigitalOcean and many
341
+ * real APIs use numeric ids, which `typia.assert` rejects as strings). */
342
+ function numericParams(ops) {
343
+ const set = new Set();
344
+ for (const op of ops) {
345
+ for (const p of op.parameters) {
346
+ const type = p.schema.type;
347
+ if (type === "integer" || type === "number")
348
+ set.add(p.name);
349
+ }
350
+ }
351
+ return set;
352
+ }
353
+ /** `const <opParam> = <Number|String>(params.<routeSegmentAtSamePosition>)`. */
354
+ function declareParams(names, source, numeric) {
355
+ return names
356
+ .map((n) => {
357
+ var _a;
358
+ const src = `(params as Record<string, unknown>).${(_a = source.get(n)) !== null && _a !== void 0 ? _a : n}`;
359
+ return numeric.has(n)
360
+ ? ` const ${n} = Number(${src});`
361
+ : ` const ${n} = String(${src} ?? "");`;
362
+ })
363
+ .join("\n");
364
+ }
365
+ /** Declarations sourcing every path param of `op` from the route, by position. */
366
+ function paramDeclarations(op, routePath) {
367
+ if (op.parameters.length === 0)
368
+ return "";
369
+ return declareParams(op.parameters.map((p) => p.name), paramSourceMap(op, routePath), numericParams([op]));
370
+ }
371
+ /**
372
+ * Rewrite a route's `[segment]` brackets into `${var}` interpolations, where
373
+ * `var` is the page's declared variable that sources that segment. Used to build
374
+ * hrefs (edit link, row → detail, parent → child) — the route segment is named
375
+ * by the canonical scheme (`[salesId]`) while the page variable is the SDK param
376
+ * (`saleId`), so we map back through the same positional source map.
377
+ */
378
+ function substituteRouteParams(routePath, op) {
379
+ const inverse = new Map();
380
+ for (const [opParam, src] of paramSourceMap(op, routePath)) {
381
+ inverse.set(src, opParam);
382
+ }
383
+ return routePath.replace(/\[([^\]]+)\]/g, (_, seg) => {
384
+ const mapped = inverse.get(seg);
385
+ // A mapped segment interpolates the page's declared op-param variable. An
386
+ // UNMAPPED segment (a hierarchy ancestor the op does not parametrize) has no
387
+ // such variable — emitting the bare segment name would reference an
388
+ // undeclared identifier (a tsc error). Read it straight from `useParams()`
389
+ // instead, so the href always carries a real value. `params` is in scope
390
+ // wherever a route has `[segment]`s (see `needsParams`).
391
+ return mapped !== undefined
392
+ ? "${" + mapped + "}"
393
+ : "${String((params as Record<string, unknown>)[" + lit(seg) + '] ?? "")}';
394
+ });
395
+ }
396
+ /**
397
+ * A `router.push` / `Link` target built ENTIRELY from `useParams()` — every
398
+ * `[segment]` reads its value off the route params. Used for the
399
+ * navigate-to-ancestor-list redirect after a delete, where the remaining
400
+ * segments are all ancestors (the entity's own id was stripped) whose values
401
+ * live in `params`, not in any declared op variable. Returns a string literal
402
+ * when the path has no dynamic segment, a template literal otherwise.
403
+ */
404
+ function routeHrefFromParams(routePath) {
405
+ if (!routePath.includes("["))
406
+ return lit(routePath);
407
+ const body = routePath.replace(/\[([^\]]+)\]/g, (_, seg) => "${String((params as Record<string, unknown>)[" + lit(seg) + '] ?? "")}');
408
+ return "`" + body + "`";
409
+ }
410
+ /* -------------------------------------------------------------------------- */
411
+ /* table */
412
+ /* -------------------------------------------------------------------------- */
413
+ /**
414
+ * Top-level response fields from the best available source: the named type, or
415
+ * the raw inline `responseSchema` when the named type is empty/dead (nestia's
416
+ * dead `*.GetResponse` ref) or absent entirely (a purely inline response).
417
+ */
418
+ function responseTopFields(op, doc) {
419
+ const named = op.responseBody !== null ? (0, extractFields_1.extractFields)(op.responseBody.typeName, doc) : [];
420
+ if (named.length > 0)
421
+ return named;
422
+ return op.responseSchema !== null
423
+ ? (0, extractFields_1.extractFieldsFromSchema)(op.responseSchema, doc)
424
+ : [];
425
+ }
426
+ /**
427
+ * The browsable ROW collection a response carries, or `null` for a single object
428
+ * / void. Shared by the screen classifier (list vs detail) and the table
429
+ * renderer so they never disagree. Recognizes a bare array, a generically named
430
+ * wrapper (`data`/`entries`), a wrapper named after the resource
431
+ * (`{ droplets: [...] }`, DigitalOcean), and an element that is a `$ref` type OR
432
+ * an INLINE object (DO's droplet shape is inline).
433
+ */
434
+ function responseCollection(op, doc) {
435
+ var _a, _b;
436
+ if (op.responseBody !== null && op.responseBody.isArray) {
437
+ const columns = (0, extractFields_1.extractFields)(op.responseBody.typeName, doc);
438
+ return columns.length > 0 ? { field: null, columns } : null;
439
+ }
440
+ const top = responseTopFields(op, doc);
441
+ if (top.length === 0)
442
+ return null;
443
+ const dataField = (_a = (0, extractFields_1.findCollectionField)(top)) !== null && _a !== void 0 ? _a : top.find((f) => f.kind === "array");
444
+ if (dataField !== undefined) {
445
+ const columns = dataField.ref !== undefined
446
+ ? (0, extractFields_1.extractFields)(dataField.ref, doc) // named element type
447
+ : ((_b = dataField.itemFields) !== null && _b !== void 0 ? _b : []); // inline element
448
+ if (columns.length > 0)
449
+ return { field: dataField.name, columns };
450
+ }
451
+ return null; // a single object, not a collection
452
+ }
453
+ /**
454
+ * Resolve a list endpoint's table columns + the JS expression that reads the row
455
+ * array off the response. accessExpr always guards null/non-array so the
456
+ * simulator omitting the field cannot crash the table on `rows.length`.
457
+ */
458
+ function tableResponse(op, doc) {
459
+ const collection = responseCollection(op, doc);
460
+ if (collection !== null) {
461
+ return {
462
+ columns: collection.columns,
463
+ accessExpr: collection.field === null
464
+ ? "(Array.isArray(res) ? res : [])"
465
+ : `(${memberOf("res", collection.field)} ?? [])`,
466
+ };
467
+ }
468
+ // Single object — show it as a one-row table (or empty when there is nothing).
469
+ const top = responseTopFields(op, doc);
470
+ return top.length > 0
471
+ ? { columns: top, accessExpr: "(res ? [res] : [])" }
472
+ : { columns: [], accessExpr: "[]" };
473
+ }
474
+ /** Page size for the generated pager — also the "is there a next page?" guess. */
475
+ const PAGE_SIZE = 20;
476
+ const PAGE_NAMES = new Set(["page", "offset", "skip", "page_number"]);
477
+ const LIMIT_NAMES = new Set([
478
+ "limit",
479
+ "per_page",
480
+ "page_size",
481
+ "pagesize",
482
+ "size",
483
+ "take",
484
+ ]);
485
+ const SEARCH_NAMES = new Set([
486
+ "search",
487
+ "q",
488
+ "keyword",
489
+ "query",
490
+ "search_text",
491
+ "term",
492
+ ]);
493
+ /**
494
+ * A schema's effective primitive type, unwrapping the nullable form
495
+ * `upgradeDocument` produces (`oneOf: [{ type: "null" }, { type: "integer" }]`)
496
+ * — so a `page?: number | null` param is still seen as a number.
497
+ */
498
+ function effectiveType(schema) {
499
+ if ("oneOf" in schema && Array.isArray(schema.oneOf)) {
500
+ const real = schema.oneOf.filter((b) => b.type !== "null");
501
+ if (real.length === 1)
502
+ return real[0].type;
503
+ }
504
+ return schema.type;
505
+ }
506
+ /** Numeric? a param whose effective type is integer/number. */
507
+ function isNumericSchema(schema) {
508
+ const t = effectiveType(schema);
509
+ return t === "integer" || t === "number";
510
+ }
511
+ /** True when a schema is (possibly nullable) plain string — not an object. */
512
+ function isStringSchema(schema) {
513
+ return effectiveType(schema) === "string";
514
+ }
515
+ /**
516
+ * The list-control params an endpoint exposes — a page/offset, a page size, and
517
+ * a text search — found across its query AND request body. Lets the table wire
518
+ * real pagination + search to whatever the swagger calls them (`page`/`offset`,
519
+ * `limit`/`per_page`, `search`/`q`), in either location. Search is only taken
520
+ * when it is a plain string param (shopping's object-shaped `search` is skipped).
521
+ */
522
+ function requestControls(op, doc) {
523
+ var _a, _b;
524
+ const fields = [];
525
+ if (op.query !== null) {
526
+ for (const [name, schema] of Object.entries((0, extractFields_1.resolveProperties)(op.query, doc).properties))
527
+ fields.push({ name, location: "query", schema });
528
+ }
529
+ if (op.requestBody !== null) {
530
+ const comp = ((_b = (_a = doc.components) === null || _a === void 0 ? void 0 : _a.schemas) !== null && _b !== void 0 ? _b : {})[op.requestBody.typeName];
531
+ if (comp !== undefined) {
532
+ for (const [name, schema] of Object.entries((0, extractFields_1.resolveProperties)(comp, doc).properties))
533
+ fields.push({ name, location: "body", schema });
534
+ }
535
+ }
536
+ const controls = {};
537
+ for (const f of fields) {
538
+ const lower = f.name.toLowerCase();
539
+ if (controls.page === undefined &&
540
+ PAGE_NAMES.has(lower) &&
541
+ isNumericSchema(f.schema)) {
542
+ controls.page = {
543
+ name: f.name,
544
+ location: f.location,
545
+ offsetBased: lower === "offset" || lower === "skip",
546
+ };
547
+ }
548
+ else if (controls.limit === undefined &&
549
+ LIMIT_NAMES.has(lower) &&
550
+ isNumericSchema(f.schema)) {
551
+ controls.limit = { name: f.name, location: f.location };
552
+ }
553
+ else if (controls.search === undefined &&
554
+ SEARCH_NAMES.has(lower) &&
555
+ isStringSchema(f.schema)) {
556
+ controls.search = { name: f.name, location: f.location };
557
+ }
558
+ }
559
+ return controls;
560
+ }
561
+ function renderTable(screen, endpoints, doc, allScreens) {
562
+ const op = primaryOp(screen, endpoints);
563
+ if (op === null)
564
+ return renderLanding(screen, allScreens, endpoints, doc);
565
+ const { columns: rawColumns, accessExpr } = tableResponse(op, doc);
566
+ const columns = orderColumns(rawColumns);
567
+ const linkIndex = resourceLinkIndex(allScreens, endpoints);
568
+ const columnsLit = `[\n${columns
569
+ .map((c) => ` ${lit(toColumnSpec(c, linkIndex))}`)
570
+ .join(",\n")}\n]`;
571
+ // `|| path has [segment]`: even when this list op takes no path param, an
572
+ // ancestor segment in the route is read from `useParams()` to build row/child
573
+ // hrefs (see substituteRouteParams), so `params` must be declared.
574
+ const needsParams = op.parameters.length > 0 || screen.path.includes("[");
575
+ const controls = requestControls(op, doc);
576
+ const hasPage = controls.page !== undefined;
577
+ const hasSearch = controls.search !== undefined;
578
+ const hasControls = hasPage || hasSearch;
579
+ // Build the SDK call. With pagination/search, merge the page/search STATE into
580
+ // the request (query or body — wherever the param lives); otherwise the plain
581
+ // schema defaults.
582
+ let call;
583
+ if (hasControls) {
584
+ const pageValue = controls.page
585
+ ? controls.page.offsetBased
586
+ ? "(page - 1) * PAGE_SIZE"
587
+ : "page"
588
+ : null;
589
+ const overridesFor = (loc) => {
590
+ var _a, _b, _c;
591
+ const out = [];
592
+ if (((_a = controls.page) === null || _a === void 0 ? void 0 : _a.location) === loc && pageValue !== null)
593
+ out.push(`${lit(controls.page.name)}: ${pageValue}`);
594
+ if (((_b = controls.limit) === null || _b === void 0 ? void 0 : _b.location) === loc)
595
+ out.push(`${lit(controls.limit.name)}: PAGE_SIZE`);
596
+ if (((_c = controls.search) === null || _c === void 0 ? void 0 : _c.location) === loc)
597
+ out.push(`${lit(controls.search.name)}: search`);
598
+ return out;
599
+ };
600
+ const parts = op.parameters.map((p) => `${p.name}: ${p.name}`);
601
+ if (op.query !== null) {
602
+ const defaults = queryDefaultsLiteral(op.query, doc);
603
+ const ov = overridesFor("query");
604
+ parts.push(`query: ${ov.length > 0 ? `{ ...${defaults}, ${ov.join(", ")} }` : defaults}`);
605
+ }
606
+ if (op.requestBody !== null) {
607
+ const defaults = bodyDefaultsLiteral(op.requestBody.typeName, doc);
608
+ const ov = overridesFor("body");
609
+ parts.push(`body: ${ov.length > 0 ? `{ ...${defaults}, ${ov.join(", ")} }` : defaults}`);
610
+ }
611
+ call = buildCall(op, parts.length > 0 ? `{ ${parts.join(", ")} }` : null);
612
+ }
613
+ else {
614
+ call = buildCall(op, buildProps(op, doc));
615
+ }
616
+ // A nested sub-collection (e.g. `/sales/{saleId}/reviews`) whose route does
617
+ // not carry those path params cannot be fetched standalone — firing the call
618
+ // with empty ids would 400/404 and show a scary error card. Render the schema
619
+ // (dense column headers — still on-target for "complete tables") with an
620
+ // honest empty state, and skip the doomed fetch entirely.
621
+ const unsatisfiable = needsParams && !screen.path.includes("[");
622
+ if (unsatisfiable) {
623
+ const parents = op.parameters.map((p) => p.name).join(", ");
624
+ return renderStaticTable(screen, columnsLit, `This is a nested collection — it needs a parent (${parents}). Open the parent record to view its ${screen.title.toLowerCase()}.`);
625
+ }
626
+ // Row → detail link, when a detail screen exists for this resource. The
627
+ // parent segments of the list route (`/sales/[salesId]/questions`) are
628
+ // substituted with the page's param vars; the row's own id is appended.
629
+ const detailScreen = allScreens.find((s) => s.uiPattern === "detail" && s.path.startsWith(`${screen.path}/[`));
630
+ const rowBase = substituteRouteParams(screen.path, op);
631
+ const rowHref = detailScreen
632
+ ? `\n rowHref={(row) => {\n const id = row.id;\n return id === undefined || id === null ? null : ${"`"}${rowBase}/${"${String(id)}"}${"`"};\n }}`
633
+ : "";
634
+ const paramHook = needsParams
635
+ ? ` const params = useParams();\n${paramDeclarations(op, screen.path)}\n`
636
+ : "";
637
+ const depExtra = [hasPage ? "page" : null, hasSearch ? "search" : null]
638
+ .filter((s) => s !== null)
639
+ .join(", ");
640
+ const baseDeps = needsParams
641
+ ? `tick, ${op.parameters.map((p) => p.name).join(", ")}`
642
+ : "tick";
643
+ const effectDeps = `[${baseDeps}${depExtra ? `, ${depExtra}` : ""}]`;
644
+ // Typing a search resets to page 1 so results start from the top.
645
+ const searchHandler = hasPage
646
+ ? "(v) => { setPage(1); setSearch(v); }"
647
+ : "(v) => setSearch(v)";
648
+ const searchProp = hasSearch
649
+ ? `\n search={{ value: search, onChange: ${searchHandler} }}`
650
+ : "";
651
+ const paginationProp = hasPage
652
+ ? `\n pagination={{ page, onPrev: () => setPage((p) => Math.max(1, p - 1)), onNext: () => setPage((p) => p + 1), hasNext }}`
653
+ : "";
654
+ return [
655
+ `"use client";`,
656
+ ``,
657
+ `import * as React from "react";`,
658
+ needsParams ? `import { useParams } from "next/navigation";` : null,
659
+ ``,
660
+ `import api from "@/src/lib/api";`,
661
+ `import { connection } from "@/src/lib/connection";`,
662
+ `import { ResourceTable } from "@/components/auto/ResourceTable";`,
663
+ `import type { ColumnSpec } from "@/components/auto/types";`,
664
+ ``,
665
+ `const COLUMNS: ColumnSpec[] = ${columnsLit};`,
666
+ hasControls ? `const PAGE_SIZE = ${PAGE_SIZE};` : null,
667
+ ``,
668
+ `export default function Page() {`,
669
+ paramHook ? paramHook : null,
670
+ ` const [rows, setRows] = React.useState<readonly unknown[]>([]);`,
671
+ ` const [loading, setLoading] = React.useState(true);`,
672
+ ` const [error, setError] = React.useState<string | null>(null);`,
673
+ ` const [tick, setTick] = React.useState(0);`,
674
+ hasPage ? ` const [page, setPage] = React.useState(1);` : null,
675
+ hasPage ? ` const [hasNext, setHasNext] = React.useState(false);` : null,
676
+ hasSearch ? ` const [search, setSearch] = React.useState("");` : null,
677
+ ``,
678
+ ` React.useEffect(() => {`,
679
+ ` let alive = true;`,
680
+ ` setLoading(true);`,
681
+ ` setError(null);`,
682
+ ` ${call}`,
683
+ ` .then((res) => {`,
684
+ ` if (!alive) return;`,
685
+ ` const data = ${accessExpr} as readonly unknown[];`,
686
+ ` setRows(data);`,
687
+ hasPage ? ` setHasNext(data.length >= PAGE_SIZE);` : null,
688
+ ` })`,
689
+ ` .catch((e) => {`,
690
+ ` if (alive) setError(e instanceof Error ? e.message : String(e));`,
691
+ ` })`,
692
+ ` .finally(() => {`,
693
+ ` if (alive) setLoading(false);`,
694
+ ` });`,
695
+ ` return () => {`,
696
+ ` alive = false;`,
697
+ ` };`,
698
+ ` }, ${effectDeps});`,
699
+ ``,
700
+ ` return (`,
701
+ ` <ResourceTable`,
702
+ ` title=${lit(screen.title)}`,
703
+ ` description=${lit(screen.purpose)}`,
704
+ ` columns={COLUMNS}`,
705
+ ` rows={rows}`,
706
+ ` loading={loading}`,
707
+ ` error={error}`,
708
+ ` onRetry={() => setTick((t) => t + 1)}${rowHref}${searchProp}${paginationProp}`,
709
+ ` />`,
710
+ ` );`,
711
+ `}`,
712
+ ``,
713
+ ]
714
+ .filter((line) => line !== null)
715
+ .join("\n");
716
+ }
717
+ /**
718
+ * A table that shows its schema (column headers) but fetches nothing — used for
719
+ * nested sub-collections that cannot be loaded without a parent selection. Keeps
720
+ * the endpoint visible as a real screen with dense headers and an honest empty
721
+ * state, instead of a broken error card from a doomed fetch.
722
+ */
723
+ function renderStaticTable(screen, columnsLit, hint) {
724
+ return [
725
+ `"use client";`,
726
+ ``,
727
+ `import * as React from "react";`,
728
+ ``,
729
+ `import { ResourceTable } from "@/components/auto/ResourceTable";`,
730
+ `import type { ColumnSpec } from "@/components/auto/types";`,
731
+ ``,
732
+ `const COLUMNS: ColumnSpec[] = ${columnsLit};`,
733
+ ``,
734
+ `export default function Page() {`,
735
+ ` return (`,
736
+ ` <ResourceTable`,
737
+ ` title=${lit(screen.title)}`,
738
+ ` description=${lit(screen.purpose)}`,
739
+ ` columns={COLUMNS}`,
740
+ ` rows={[]}`,
741
+ ` loading={false}`,
742
+ ` error={null}`,
743
+ ` emptyHint=${lit(hint)}`,
744
+ ` />`,
745
+ ` );`,
746
+ `}`,
747
+ ``,
748
+ ].join("\n");
749
+ }
750
+ /* -------------------------------------------------------------------------- */
751
+ /* catalog */
752
+ /* -------------------------------------------------------------------------- */
753
+ /** Secondary columns shown under a catalog card's title. */
754
+ const CATALOG_META_LIMIT = 4;
755
+ /**
756
+ * Catalog (card grid) page — same fetch scaffold as the table, but renders
757
+ * `CatalogGrid` with an image + title + a few meta fields baked from the schema.
758
+ * Reached only for collections the plan flagged as image+title bearing; falls
759
+ * back to a table if the signals are somehow absent or the list needs a parent.
760
+ */
761
+ function renderCatalog(screen, endpoints, doc, allScreens) {
762
+ const op = primaryOp(screen, endpoints);
763
+ if (op === null)
764
+ return renderTable(screen, endpoints, doc, allScreens);
765
+ const { columns: rawColumns, accessExpr } = tableResponse(op, doc);
766
+ const columns = orderColumns(rawColumns);
767
+ const image = (0, extractFields_1.imageFieldOf)(columns);
768
+ const title = (0, extractFields_1.titleFieldOf)(columns);
769
+ // `|| path has [segment]`: even when this list op takes no path param, an
770
+ // ancestor segment in the route is read from `useParams()` to build row/child
771
+ // hrefs (see substituteRouteParams), so `params` must be declared.
772
+ const needsParams = op.parameters.length > 0 || screen.path.includes("[");
773
+ // Missing signal or a nested list that needs a parent → the table renderer
774
+ // handles both (dense columns + honest empty state) better than a broken grid.
775
+ if (image === undefined ||
776
+ title === undefined ||
777
+ (needsParams && !screen.path.includes("["))) {
778
+ return renderTable(screen, endpoints, doc, allScreens);
779
+ }
780
+ const meta = columns
781
+ .filter((c) => c.name !== image.name && c.name !== title.name)
782
+ .slice(0, CATALOG_META_LIMIT);
783
+ const specLit = lit({
784
+ imageField: image.name,
785
+ imageIsArray: image.kind === "array",
786
+ titleField: title.name,
787
+ meta: meta.map((c) => toColumnSpec(c)),
788
+ });
789
+ const call = buildCall(op, buildProps(op, doc));
790
+ const detailScreen = allScreens.find((s) => s.uiPattern === "detail" && s.path.startsWith(`${screen.path}/[`));
791
+ const rowBase = substituteRouteParams(screen.path, op);
792
+ const rowHref = detailScreen
793
+ ? `\n rowHref={(row) => {\n const id = row.id;\n return id === undefined || id === null ? null : ${"`"}${rowBase}/${"${String(id)}"}${"`"};\n }}`
794
+ : "";
795
+ const paramHook = needsParams
796
+ ? ` const params = useParams();\n${paramDeclarations(op, screen.path)}\n`
797
+ : "";
798
+ const effectDeps = needsParams
799
+ ? `[tick, ${op.parameters.map((p) => p.name).join(", ")}]`
800
+ : "[tick]";
801
+ return [
802
+ `"use client";`,
803
+ ``,
804
+ `import * as React from "react";`,
805
+ needsParams ? `import { useParams } from "next/navigation";` : null,
806
+ ``,
807
+ `import api from "@/src/lib/api";`,
808
+ `import { connection } from "@/src/lib/connection";`,
809
+ `import { CatalogGrid } from "@/components/auto/CatalogGrid";`,
810
+ `import type { CatalogSpec } from "@/components/auto/CatalogGrid";`,
811
+ ``,
812
+ `const SPEC: CatalogSpec = ${specLit};`,
813
+ ``,
814
+ `export default function Page() {`,
815
+ paramHook ? paramHook : null,
816
+ ` const [rows, setRows] = React.useState<readonly unknown[]>([]);`,
817
+ ` const [loading, setLoading] = React.useState(true);`,
818
+ ` const [error, setError] = React.useState<string | null>(null);`,
819
+ ` const [tick, setTick] = React.useState(0);`,
820
+ ``,
821
+ ` React.useEffect(() => {`,
822
+ ` let alive = true;`,
823
+ ` setLoading(true);`,
824
+ ` setError(null);`,
825
+ ` ${call}`,
826
+ ` .then((res) => {`,
827
+ ` if (!alive) return;`,
828
+ ` setRows(${accessExpr} as readonly unknown[]);`,
829
+ ` })`,
830
+ ` .catch((e) => {`,
831
+ ` if (alive) setError(e instanceof Error ? e.message : String(e));`,
832
+ ` })`,
833
+ ` .finally(() => {`,
834
+ ` if (alive) setLoading(false);`,
835
+ ` });`,
836
+ ` return () => {`,
837
+ ` alive = false;`,
838
+ ` };`,
839
+ ` }, ${effectDeps});`,
840
+ ``,
841
+ ` return (`,
842
+ ` <CatalogGrid`,
843
+ ` title=${lit(screen.title)}`,
844
+ ` description=${lit(screen.purpose)}`,
845
+ ` spec={SPEC}`,
846
+ ` rows={rows}`,
847
+ ` loading={loading}`,
848
+ ` error={error}`,
849
+ ` onRetry={() => setTick((t) => t + 1)}${rowHref}`,
850
+ ` />`,
851
+ ` );`,
852
+ `}`,
853
+ ``,
854
+ ]
855
+ .filter((line) => line !== null)
856
+ .join("\n");
857
+ }
858
+ /* -------------------------------------------------------------------------- */
859
+ /* detail */
860
+ /* -------------------------------------------------------------------------- */
861
+ function renderDetail(screen, endpoints, doc, allScreens) {
862
+ var _a, _b, _c;
863
+ const op = primaryOp(screen, endpoints);
864
+ if (op === null)
865
+ return renderLanding(screen, allScreens, endpoints, doc);
866
+ const typeName = (_b = (_a = op.responseBody) === null || _a === void 0 ? void 0 : _a.typeName) !== null && _b !== void 0 ? _b : null;
867
+ const fields = typeName ? (0, extractFields_1.extractFields)(typeName, doc) : [];
868
+ const linkIndex = resourceLinkIndex(allScreens, endpoints);
869
+ const fieldsLit = `[\n${fields
870
+ .map((f) => ` ${lit(toColumnSpec(f, linkIndex))}`)
871
+ .join(",\n")}\n]`;
872
+ const props = buildProps(op, doc);
873
+ const call = buildCall(op, props);
874
+ // Edit-link action, when an edit screen exists for this entity.
875
+ const editScreen = allScreens.find((s) => s.uiPattern === "form" && s.path === `${screen.path}/edit`);
876
+ // The detail route is dynamic (`/orders/[ordersId]`). The edit link must
877
+ // substitute the id VALUE into the bracket segment, not leave a literal
878
+ // `[ordersId]` — Next.js rejects a `<Link href>` that still contains a bracket
879
+ // at runtime ("Dynamic href found in <Link>"), a defect tsc + next build
880
+ // cannot see.
881
+ const editHref = `${substituteRouteParams(screen.path, op)}/edit`;
882
+ // Secondary endpoints the detail composes (its update / delete / action
883
+ // operations) become real buttons: DELETE → a destructive button, POST → an
884
+ // action button labeled by the verb. PUT/PATCH updates are covered by the
885
+ // Edit link, so they are not duplicated here. This is what makes the detail
886
+ // page functional — you can act on the record, not just look at it.
887
+ const secondaryOps = screen.endpoints
888
+ .slice(1)
889
+ .map((a) => endpoints.find((e) => e.accessor.join(".") === a))
890
+ .filter((o) => o !== undefined);
891
+ const actionOps = secondaryOps.filter((o) => {
892
+ const m = o.method.toUpperCase();
893
+ return m === "DELETE" || m === "POST";
894
+ });
895
+ const hasActions = actionOps.length > 0;
896
+ // Direct child collections (`/sales/[id]/questions`) — embedded inline below
897
+ // the record as preview tables, turning a flat detail into a master-detail
898
+ // view. Their list endpoints reuse this page's id params (a child collection
899
+ // adds no new bracket of its own).
900
+ const childCollectionScreens = allScreens.filter((s) => {
901
+ if (s.uiPattern !== "table" && s.uiPattern !== "catalog")
902
+ return false;
903
+ if (!s.path.startsWith(`${screen.path}/`))
904
+ return false;
905
+ const rest = s.path.slice(screen.path.length + 1);
906
+ return rest.length > 0 && !rest.includes("/"); // direct child collection only
907
+ });
908
+ const childOps = childCollectionScreens
909
+ .map((c) => primaryOp(c, endpoints))
910
+ .filter((o) => o !== null);
911
+ // Declare every path param any operation on this page needs (primary detail,
912
+ // each action, each embedded child list), sourced from useParams — the route
913
+ // may name a segment `[id]` while the SDK calls it `saleId`. The primary op's
914
+ // order matches the route; actions and child lists share its leading params.
915
+ const paramNames = Array.from(new Set([op, ...actionOps, ...childOps].flatMap((o) => o.parameters.map((p) => p.name))));
916
+ const needsParams = paramNames.length > 0;
917
+ // Source every param from the route segment at ITS OWN op's position, unioned
918
+ // across all ops. Different ops name the same id differently — the detail's
919
+ // `sales.at` calls it `id`, the child `sales.snapshots.index` calls it
920
+ // `saleId`, an action `categories.create` calls it `channelCode` — but they
921
+ // all address the same leading route segment (`[salesId]` / `[channelsId]`).
922
+ // Mapping each op's params positionally and merging declares every alias from
923
+ // the right segment, instead of leaving a child/action id sourced from a
924
+ // route param name that does not exist (→ empty string → failed fetch).
925
+ const allParamOps = [op, ...actionOps, ...childOps];
926
+ const mergedParamSource = new Map();
927
+ for (const o of allParamOps) {
928
+ for (const [param, src] of paramSourceMap(o, screen.path)) {
929
+ if (!mergedParamSource.has(param))
930
+ mergedParamSource.set(param, src);
931
+ }
932
+ }
933
+ const paramDecls = declareParams(paramNames, mergedParamSource, numericParams(allParamOps));
934
+ // Dedupe by visible label — actor-scoped duplicates (admin + seller `erase`)
935
+ // collapse to one route here, so both would render an identical "Delete"
936
+ // button. Keep the first; track labels so child links don't repeat them.
937
+ // List route this record belongs to (`/sales/[salesId]` → `/sales`) — where a
938
+ // successful delete navigates, since the record no longer exists.
939
+ const listPath = screen.path.replace(/\/\[[^\]]+\]$/, "") || "/";
940
+ const usedLabels = new Set();
941
+ const actionButtons = [];
942
+ let hasDelete = false;
943
+ for (const o of actionOps) {
944
+ const isDelete = o.method.toUpperCase() === "DELETE";
945
+ // Label from the last STATIC path segment (`/orders/{id}/cancel` → "Cancel"),
946
+ // not the accessor's auto-generated tail (`postById`) which reads as noise.
947
+ const verb = (_c = o.path
948
+ .split("/")
949
+ .filter((s) => s.length > 0 && !s.startsWith("{"))
950
+ .pop()) !== null && _c !== void 0 ? _c : "Action";
951
+ const label = isDelete ? "Delete" : titleizeSegment(verb);
952
+ if (usedLabels.has(label))
953
+ continue;
954
+ usedLabels.add(label);
955
+ const acall = buildCall(o, buildProps(o, doc));
956
+ if (isDelete) {
957
+ // Confirmed delete → on success navigate to the list (record is gone).
958
+ hasDelete = true;
959
+ actionButtons.push(` <ConfirmButton label=${lit(label)} disabled={busy} onConfirm={() => del(() => ${acall})} />`);
960
+ }
961
+ else {
962
+ // A mutating action re-fetches the record so the change is visible.
963
+ actionButtons.push(` <Button variant="outline" size="sm" disabled={busy} onClick={() => act(() => ${acall})}>${label}</Button>`);
964
+ }
965
+ }
966
+ // Inline child-collection embeds. Each direct child list is rendered as a
967
+ // compact preview table below the record (master-detail), not a button that
968
+ // jumps away. Skip a child whose label already appears as an action button (a
969
+ // POST `/sales/{id}/questions` action + the `/sales/[id]/questions` list both
970
+ // read "Questions"). The child list reuses this page's id params, substituted
971
+ // by name through `buildProps`.
972
+ const childEmbeds = childCollectionScreens
973
+ .map((c) => {
974
+ const cop = primaryOp(c, endpoints);
975
+ if (cop === null)
976
+ return null;
977
+ const label = titleizeSegment(c.path.slice(screen.path.length + 1));
978
+ if (usedLabels.has(label))
979
+ return null;
980
+ usedLabels.add(label);
981
+ const { columns, accessExpr } = tableResponse(cop, doc);
982
+ const columnsLit = `[\n${orderColumns(columns)
983
+ .map((f) => ` ${lit(toColumnSpec(f, linkIndex))}`)
984
+ .join(",\n")}\n ]`;
985
+ return {
986
+ title: label,
987
+ href: substituteRouteParams(c.path, op),
988
+ columnsLit,
989
+ call: buildCall(cop, buildProps(cop, doc)),
990
+ accessExpr,
991
+ };
992
+ })
993
+ .filter((x) => x !== null);
994
+ const hasEmbeds = childEmbeds.length > 0;
995
+ const embedsJsx = childEmbeds
996
+ .map((e) => [
997
+ ` <EmbeddedCollection`,
998
+ ` title=${lit(e.title)}`,
999
+ ` href={${"`"}${e.href}${"`"}}`,
1000
+ ` columns={${e.columnsLit}}`,
1001
+ ` load={() =>`,
1002
+ ` ${e.call}`,
1003
+ ` .then((res) => (${e.accessExpr}) as readonly unknown[])`,
1004
+ ` }`,
1005
+ ` />`,
1006
+ ].join("\n"))
1007
+ .join("\n");
1008
+ const actionInner = [
1009
+ editScreen
1010
+ ? ` <Button asChild variant="outline" size="sm">\n <Link href={${"`"}${editHref}${"`"}}>Edit</Link>\n </Button>`
1011
+ : null,
1012
+ ...actionButtons,
1013
+ hasActions
1014
+ ? ` {actionMsg ? <span className="self-center text-xs text-muted-foreground">{actionMsg}</span> : null}`
1015
+ : null,
1016
+ ]
1017
+ .filter((l) => l !== null)
1018
+ .join("\n");
1019
+ const needButton = editScreen !== undefined || hasActions;
1020
+ const needLink = editScreen !== undefined;
1021
+ const actions = needButton
1022
+ ? `\n actions={\n <>\n${actionInner}\n </>\n }`
1023
+ : "";
1024
+ const effectDeps = needsParams ? `[tick, ${paramNames.join(", ")}]` : "[tick]";
1025
+ const navNames = [
1026
+ needsParams ? "useParams" : null,
1027
+ hasDelete ? "useRouter" : null,
1028
+ ].filter((s) => s !== null);
1029
+ const navImport = navNames.length > 0
1030
+ ? `import { ${navNames.join(", ")} } from "next/navigation";`
1031
+ : null;
1032
+ return [
1033
+ `"use client";`,
1034
+ ``,
1035
+ `import * as React from "react";`,
1036
+ navImport,
1037
+ needLink ? `import Link from "next/link";` : null,
1038
+ ``,
1039
+ `import api from "@/src/lib/api";`,
1040
+ `import { connection } from "@/src/lib/connection";`,
1041
+ `import { ResourceDetail } from "@/components/auto/ResourceDetail";`,
1042
+ hasEmbeds
1043
+ ? `import { EmbeddedCollection } from "@/components/auto/EmbeddedCollection";`
1044
+ : null,
1045
+ needButton ? `import { Button } from "@/components/ui/button";` : null,
1046
+ hasDelete
1047
+ ? `import { ConfirmButton } from "@/components/auto/ConfirmButton";`
1048
+ : null,
1049
+ `import type { ColumnSpec } from "@/components/auto/types";`,
1050
+ ``,
1051
+ `const FIELDS: ColumnSpec[] = ${fieldsLit};`,
1052
+ ``,
1053
+ `export default function Page() {`,
1054
+ needsParams ? ` const params = useParams();` : null,
1055
+ hasDelete ? ` const router = useRouter();` : null,
1056
+ needsParams ? paramDecls : null,
1057
+ ` const [data, setData] = React.useState<unknown>(null);`,
1058
+ ` const [loading, setLoading] = React.useState(true);`,
1059
+ ` const [error, setError] = React.useState<string | null>(null);`,
1060
+ ` const [tick, setTick] = React.useState(0);`,
1061
+ hasActions ? ` const [busy, setBusy] = React.useState(false);` : null,
1062
+ hasActions
1063
+ ? ` const [actionMsg, setActionMsg] = React.useState<string | null>(null);`
1064
+ : null,
1065
+ hasActions ? ` const act = (fn: () => Promise<unknown>) => {` : null,
1066
+ hasActions ? ` setBusy(true);` : null,
1067
+ hasActions ? ` setActionMsg(null);` : null,
1068
+ hasActions ? ` fn()` : null,
1069
+ hasActions
1070
+ ? ` .then(() => { setActionMsg("Done."); setTick((t) => t + 1); })`
1071
+ : null,
1072
+ hasActions
1073
+ ? ` .catch((e) => setActionMsg(e instanceof Error ? e.message : String(e)))`
1074
+ : null,
1075
+ hasActions ? ` .finally(() => setBusy(false));` : null,
1076
+ hasActions ? ` };` : null,
1077
+ hasDelete ? ` const del = (fn: () => Promise<unknown>) => {` : null,
1078
+ hasDelete ? ` setBusy(true);` : null,
1079
+ hasDelete ? ` setActionMsg(null);` : null,
1080
+ hasDelete ? ` fn()` : null,
1081
+ hasDelete ? ` .then(() => router.push(${routeHrefFromParams(listPath)}))` : null,
1082
+ hasDelete
1083
+ ? ` .catch((e) => { setActionMsg(e instanceof Error ? e.message : String(e)); setBusy(false); });`
1084
+ : null,
1085
+ hasDelete ? ` };` : null,
1086
+ ``,
1087
+ ` React.useEffect(() => {`,
1088
+ ` let alive = true;`,
1089
+ ` setLoading(true);`,
1090
+ ` setError(null);`,
1091
+ ` ${call}`,
1092
+ ` .then((res) => {`,
1093
+ ` if (alive) setData(res);`,
1094
+ ` })`,
1095
+ ` .catch((e) => {`,
1096
+ ` if (alive) setError(e instanceof Error ? e.message : String(e));`,
1097
+ ` })`,
1098
+ ` .finally(() => {`,
1099
+ ` if (alive) setLoading(false);`,
1100
+ ` });`,
1101
+ ` return () => {`,
1102
+ ` alive = false;`,
1103
+ ` };`,
1104
+ ` }, ${effectDeps});`,
1105
+ ``,
1106
+ ` return (`,
1107
+ hasEmbeds ? ` <>` : null,
1108
+ ` <ResourceDetail`,
1109
+ ` title=${lit(screen.title)}`,
1110
+ ` fields={FIELDS}`,
1111
+ ` data={data}`,
1112
+ ` loading={loading}`,
1113
+ ` error={error}`,
1114
+ ` onRetry={() => setTick((t) => t + 1)}${actions}`,
1115
+ ` />`,
1116
+ hasEmbeds ? ` <div className="px-5 pb-8 md:px-8">` : null,
1117
+ hasEmbeds ? ` <div className="max-w-3xl space-y-5">` : null,
1118
+ hasEmbeds ? embedsJsx : null,
1119
+ hasEmbeds ? ` </div>` : null,
1120
+ hasEmbeds ? ` </div>` : null,
1121
+ hasEmbeds ? ` </>` : null,
1122
+ ` );`,
1123
+ `}`,
1124
+ ``,
1125
+ ]
1126
+ .filter((line) => line !== null)
1127
+ .join("\n");
1128
+ }
1129
+ /* -------------------------------------------------------------------------- */
1130
+ /* form (create / edit) */
1131
+ /* -------------------------------------------------------------------------- */
1132
+ function renderForm(screen, endpoints, doc) {
1133
+ var _a;
1134
+ const op = primaryOp(screen, endpoints);
1135
+ if (op === null || op.requestBody === null) {
1136
+ // No request body to drive a form — degrade to a detail-less notice.
1137
+ return renderEmptyForm(screen);
1138
+ }
1139
+ const inputs = (0, extractFields_1.extractFields)(op.requestBody.typeName, doc);
1140
+ const fieldsLit = `[\n${inputs
1141
+ .map((f) => ` ${lit(toFieldInput(f))}`)
1142
+ .join(",\n")}\n]`;
1143
+ const props = buildProps(op, doc, { bodyExpr: "values" });
1144
+ const call = buildCall(op, props);
1145
+ // Edit forms prefill from the current record: find the detail GET among the
1146
+ // screen's secondary endpoints and fetch it on mount.
1147
+ const isEdit = screen.path.endsWith("/edit");
1148
+ const detailOp = isEdit
1149
+ ? ((_a = screen.endpoints
1150
+ .slice(1)
1151
+ .map((a) => endpoints.find((e) => e.accessor.join(".") === a))
1152
+ .find((e) => e !== undefined &&
1153
+ e.method.toLowerCase() === "get" &&
1154
+ e.parameters.length > 0)) !== null && _a !== void 0 ? _a : null)
1155
+ : null;
1156
+ // Params for both the update op and the prefill detail op, sourced by name
1157
+ // from the shared route segments (same merge as the detail page).
1158
+ const paramOps = detailOp !== null ? [op, detailOp] : [op];
1159
+ const paramNames = Array.from(new Set(paramOps.flatMap((o) => o.parameters.map((p) => p.name))));
1160
+ const needsParams = paramNames.length > 0;
1161
+ const mergedSource = new Map();
1162
+ for (const o of paramOps)
1163
+ for (const [pn, src] of paramSourceMap(o, screen.path))
1164
+ if (!mergedSource.has(pn))
1165
+ mergedSource.set(pn, src);
1166
+ const paramDecls = declareParams(paramNames, mergedSource, numericParams(paramOps));
1167
+ const paramHook = needsParams
1168
+ ? ` const params = useParams();\n${paramDecls}\n`
1169
+ : "";
1170
+ const detailCall = detailOp !== null ? buildCall(detailOp, buildProps(detailOp, doc)) : null;
1171
+ return [
1172
+ `"use client";`,
1173
+ ``,
1174
+ `import * as React from "react";`,
1175
+ needsParams ? `import { useParams } from "next/navigation";` : null,
1176
+ ``,
1177
+ `import api from "@/src/lib/api";`,
1178
+ `import { connection } from "@/src/lib/connection";`,
1179
+ `import { ResourceForm } from "@/components/auto/ResourceForm";`,
1180
+ `import type { FieldInput } from "@/components/auto/types";`,
1181
+ ``,
1182
+ `const FIELDS: FieldInput[] = ${fieldsLit};`,
1183
+ ``,
1184
+ `export default function Page() {`,
1185
+ paramHook ? paramHook : null,
1186
+ ` const [submitting, setSubmitting] = React.useState(false);`,
1187
+ ` const [error, setError] = React.useState<string | null>(null);`,
1188
+ ` const [result, setResult] = React.useState<string | null>(null);`,
1189
+ detailCall !== null
1190
+ ? ` const [initial, setInitial] = React.useState<Record<string, unknown> | undefined>(undefined);`
1191
+ : null,
1192
+ detailCall !== null ? `` : null,
1193
+ detailCall !== null ? ` React.useEffect(() => {` : null,
1194
+ detailCall !== null ? ` let alive = true;` : null,
1195
+ detailCall !== null ? ` ${detailCall}` : null,
1196
+ detailCall !== null
1197
+ ? // cast through `unknown`: the prefill source may type as `void` (a 204
1198
+ // detail, e.g. GitHub's collaborator check) which does not overlap Record.
1199
+ ` .then((res) => { if (alive) setInitial(res as unknown as Record<string, unknown>); })`
1200
+ : null,
1201
+ detailCall !== null ? ` .catch(() => undefined);` : null,
1202
+ detailCall !== null ? ` return () => { alive = false; };` : null,
1203
+ detailCall !== null
1204
+ ? ` }, [${paramNames.join(", ")}]);`
1205
+ : null,
1206
+ ``,
1207
+ ` const onSubmit = (values: Record<string, unknown>) => {`,
1208
+ ` setSubmitting(true);`,
1209
+ ` setError(null);`,
1210
+ ` setResult(null);`,
1211
+ ` ${call}`,
1212
+ ` .then(() => {`,
1213
+ ` setResult("Saved successfully.");`,
1214
+ ` })`,
1215
+ ` .catch((e) => {`,
1216
+ ` setError(e instanceof Error ? e.message : String(e));`,
1217
+ ` })`,
1218
+ ` .finally(() => {`,
1219
+ ` setSubmitting(false);`,
1220
+ ` });`,
1221
+ ` };`,
1222
+ ``,
1223
+ ` return (`,
1224
+ ` <ResourceForm`,
1225
+ ` title=${lit(screen.title)}`,
1226
+ ` description=${lit(screen.purpose)}`,
1227
+ ` fields={FIELDS}`,
1228
+ ` submitting={submitting}`,
1229
+ ` error={error}`,
1230
+ ` result={result}`,
1231
+ detailCall !== null ? ` initialValues={initial}` : null,
1232
+ ` onSubmit={onSubmit}`,
1233
+ ` />`,
1234
+ ` );`,
1235
+ `}`,
1236
+ ``,
1237
+ ]
1238
+ .filter((line) => line !== null)
1239
+ .join("\n");
1240
+ }
1241
+ function renderEmptyForm(screen) {
1242
+ return [
1243
+ `"use client";`,
1244
+ ``,
1245
+ `import { ResourceForm } from "@/components/auto/ResourceForm";`,
1246
+ ``,
1247
+ `export default function Page() {`,
1248
+ ` return (`,
1249
+ ` <ResourceForm`,
1250
+ ` title=${lit(screen.title)}`,
1251
+ ` fields={[]}`,
1252
+ ` submitting={false}`,
1253
+ ` onSubmit={() => undefined}`,
1254
+ ` />`,
1255
+ ` );`,
1256
+ `}`,
1257
+ ``,
1258
+ ].join("\n");
1259
+ }
1260
+ /* -------------------------------------------------------------------------- */
1261
+ /* landing */
1262
+ /* -------------------------------------------------------------------------- */
1263
+ /**
1264
+ * Resource hub-ness — how many DISTINCT child resources nest under a resource,
1265
+ * i.e. how many other resources hang off its id (`/files/{file_id}/metadata`,
1266
+ * `/files/{file_id}/versions`, … → `files` parents `metadata`, `versions`, …).
1267
+ * A resource that parents several others is a genuine domain hub; one only ever
1268
+ * read on its own (`/x/{id}`) parents nothing and scores 0. Deterministic from
1269
+ * the path shape — the producer→consumer graph the IR already carries — used to
1270
+ * float the few real hubs (`files`/`folders`/`users`) above the long tail on the
1271
+ * landing page, instead of every path-param hit (which almost every resource has
1272
+ * from its own detail route and so does not discriminate).
1273
+ */
1274
+ function resourceHubness(endpoints) {
1275
+ var _a;
1276
+ const children = new Map();
1277
+ for (const e of endpoints) {
1278
+ const segs = e.path.split("/").filter((s) => s.length > 0);
1279
+ for (let i = 1; i < segs.length; i++) {
1280
+ if (!segs[i].startsWith("{"))
1281
+ continue;
1282
+ const parent = segs[i - 1];
1283
+ const child = segs[i + 1];
1284
+ if (child === undefined || child.startsWith("{"))
1285
+ continue;
1286
+ const set = (_a = children.get(parent)) !== null && _a !== void 0 ? _a : new Set();
1287
+ set.add(child);
1288
+ children.set(parent, set);
1289
+ }
1290
+ }
1291
+ const score = new Map();
1292
+ for (const [res, set] of children)
1293
+ score.set(res, set.size);
1294
+ return score;
1295
+ }
1296
+ /** How many hub resources get a live count card on the home dashboard. Past
1297
+ * this the cards become noise; the long tail stays in the link grid below. */
1298
+ const DASHBOARD_STAT_LIMIT = 8;
1299
+ function renderLanding(screen, allScreens, endpoints, doc) {
1300
+ const hubness = resourceHubness(endpoints);
1301
+ const resourceOf = (path) => { var _a; return (_a = path.replace(/^\//, "").split("/")[0]) !== null && _a !== void 0 ? _a : ""; };
1302
+ const weightOf = (path) => { var _a; return (_a = hubness.get(resourceOf(path))) !== null && _a !== void 0 ? _a : 0; };
1303
+ // Links: every top-level browsable screen (chain depth 1, bracket-less). A
1304
+ // nested resource or param-less sub-view is reached from its parent, never
1305
+ // the home hub.
1306
+ const isTopLevel = (s) => (s.depth === undefined || s.depth === 1) && !s.path.includes("[");
1307
+ const links = allScreens
1308
+ .filter((s) => isTopLevel(s) &&
1309
+ (s.uiPattern === "table" ||
1310
+ s.uiPattern === "catalog" ||
1311
+ s.uiPattern === "detail"))
1312
+ .map((s) => ({
1313
+ href: s.path,
1314
+ title: s.title,
1315
+ weight: weightOf(s.path),
1316
+ }));
1317
+ const linksLit = `[\n${links.map((l) => ` ${lit(l)}`).join(",\n")}\n]`;
1318
+ // Stats: the top hub resources whose list is fetchable standalone (no path
1319
+ // params), surfaced as live count cards. The count prefers a known total
1320
+ // field (pagination) and falls back to the fetched array length — both
1321
+ // deterministic from the response shape already resolved by `tableResponse`.
1322
+ const statScreens = allScreens
1323
+ .filter((s) => isTopLevel(s) && s.uiPattern === "table")
1324
+ .map((s) => ({ screen: s, op: primaryOp(s, endpoints) }))
1325
+ .filter((x) => x.op !== null && x.op.parameters.length === 0)
1326
+ .sort((a, b) => weightOf(b.screen.path) - weightOf(a.screen.path))
1327
+ .slice(0, DASHBOARD_STAT_LIMIT);
1328
+ const statTitlesLit = `[\n${statScreens
1329
+ .map((x) => ` ${lit({ href: x.screen.path, title: x.screen.title })}`)
1330
+ .join(",\n")}\n]`;
1331
+ const fetchers = statScreens.map((x) => {
1332
+ var _a;
1333
+ const call = buildCall(x.op, buildProps(x.op, doc));
1334
+ const { accessExpr } = tableResponse(x.op, doc);
1335
+ const collection = responseCollection(x.op, doc);
1336
+ const titleField = collection !== null ? (_a = (0, extractFields_1.titleFieldOf)(collection.columns)) === null || _a === void 0 ? void 0 : _a.name : undefined;
1337
+ const href = lit(x.screen.path);
1338
+ // First couple of record titles as a "recent" preview, when the row type
1339
+ // has a title-ish field; otherwise an empty preview.
1340
+ const recentExpr = titleField !== undefined
1341
+ ? `rows.slice(0, 2).map((row) => { const v = (row as Record<string, unknown>)?.[${lit(titleField)}]; return typeof v === "string" || typeof v === "number" ? String(v) : null; }).filter((s): s is string => s !== null)`
1342
+ : "[] as string[]";
1343
+ return [
1344
+ ` ${call}`,
1345
+ ` .then((res) => {`,
1346
+ ` const r = res as unknown as { pagination?: { records?: number; total?: number }; total?: number; count?: number };`,
1347
+ ` const t = r?.pagination?.records ?? r?.pagination?.total ?? r?.total ?? r?.count;`,
1348
+ ` const rows = (${accessExpr} as readonly unknown[]);`,
1349
+ ` const n = typeof t === "number" ? t : rows.length;`,
1350
+ ` const recent = ${recentExpr};`,
1351
+ ` return [${href}, { count: n, recent }] as const;`,
1352
+ ` })`,
1353
+ ` .catch(() => [${href}, { count: null, recent: [] as string[] }] as const)`,
1354
+ ].join("\n");
1355
+ });
1356
+ const hasStats = fetchers.length > 0;
1357
+ const fetchBlock = hasStats
1358
+ ? [
1359
+ ` const [counts, setCounts] = React.useState<Record<string, { count: number | null; recent: string[] }>>({});`,
1360
+ ``,
1361
+ ` React.useEffect(() => {`,
1362
+ ` let alive = true;`,
1363
+ ` Promise.all([`,
1364
+ fetchers.join(",\n"),
1365
+ ` ]).then((entries) => {`,
1366
+ ` if (!alive) return;`,
1367
+ ` setCounts(Object.fromEntries(entries));`,
1368
+ ` });`,
1369
+ ` return () => {`,
1370
+ ` alive = false;`,
1371
+ ` };`,
1372
+ ` }, []);`,
1373
+ ``,
1374
+ ` const stats: DashboardStat[] = STAT_TITLES.map((s) => ({`,
1375
+ ` href: s.href,`,
1376
+ ` title: s.title,`,
1377
+ ` count: counts[s.href]?.count ?? null,`,
1378
+ ` recent: counts[s.href]?.recent ?? [],`,
1379
+ ` }));`,
1380
+ ].join("\n")
1381
+ : ` const stats: DashboardStat[] = [];`;
1382
+ return [
1383
+ `"use client";`,
1384
+ ``,
1385
+ `import * as React from "react";`,
1386
+ ``,
1387
+ hasStats ? `import api from "@/src/lib/api";` : null,
1388
+ hasStats ? `import { connection } from "@/src/lib/connection";` : null,
1389
+ `import { ResourceDashboard } from "@/components/auto/ResourceDashboard";`,
1390
+ `import type { DashboardStat } from "@/components/auto/ResourceDashboard";`,
1391
+ `import type { LandingLink } from "@/components/auto/ResourceLanding";`,
1392
+ ``,
1393
+ `const LINKS: LandingLink[] = ${linksLit};`,
1394
+ hasStats
1395
+ ? `\nconst STAT_TITLES: { href: string; title: string }[] = ${statTitlesLit};`
1396
+ : null,
1397
+ ``,
1398
+ `export default function Page() {`,
1399
+ fetchBlock,
1400
+ ``,
1401
+ ` return (`,
1402
+ ` <ResourceDashboard`,
1403
+ ` title=${lit(screen.title)}`,
1404
+ ` subtitle=${lit(screen.purpose)}`,
1405
+ ` stats={stats}`,
1406
+ ` links={LINKS}`,
1407
+ ` />`,
1408
+ ` );`,
1409
+ `}`,
1410
+ ``,
1411
+ ]
1412
+ .filter((l) => l !== null)
1413
+ .join("\n");
1414
+ }
1415
+ //# sourceMappingURL=renderResourcePage.js.map