@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,289 @@
1
+ import { OpenApi } from "@typia/interface";
2
+
3
+ import {
4
+ filterEndpoints,
5
+ IAutoViewEndpoint,
6
+ IEndpointFilter,
7
+ IPlannedScreen,
8
+ isCatalogCollection,
9
+ resourcePlan,
10
+ ScreenKind,
11
+ toEndpoints,
12
+ unsupportedOperations,
13
+ } from "../../utils";
14
+ import { IAutoViewProductPlan } from "../structures/IAutoViewProductPlan";
15
+ import { responseCollection } from "./renderResourcePage";
16
+
17
+ /**
18
+ * Deterministic Product Plan — the LLM ProductPlan's replacement.
19
+ *
20
+ * The old Phase 2 asked an LLM to invent an information architecture from a
21
+ * commerce-baked prompt: non-reproducible, domain-locked, no CRUD completeness.
22
+ * This builds the exact same {@link IAutoViewProductPlan} shape the Scaffold /
23
+ * Render phases consume, but structurally — from {@link resourcePlan}. Same
24
+ * swagger always yields the same screens; every resource that exposes a
25
+ * capability gets its screen; nothing is invented.
26
+ *
27
+ * Actors are not modeled yet (the READ layer dropped `authorizationActor` when
28
+ * it went swagger-native), so every screen is assigned the single `actor`
29
+ * passed in. Multi-actor navigation is a follow-up that restores per-endpoint
30
+ * actor tagging.
31
+ */
32
+ const UI_PATTERN: Record<ScreenKind, IAutoViewProductPlan.UiPattern> = {
33
+ list: "table",
34
+ detail: "detail",
35
+ create: "form",
36
+ edit: "form",
37
+ };
38
+
39
+ /**
40
+ * A "top-level" screen earns a spot in the landing hub + primary nav: a list
41
+ * table, a catalog, or a singleton read (a bracket-less detail page like
42
+ * `/system`). Entity-detail / create / edit pages — which carry a `[param]` and
43
+ * are reached from a list — do not.
44
+ */
45
+ function isTopLevel(screen: IAutoViewProductPlan.IScreen): boolean {
46
+ // A landing card is a TOP-LEVEL resource (chain depth 1). Depth ≥ 2 — a nested
47
+ // resource (`/sales/[saleId]/questions`) OR a param-less sub-view
48
+ // (`/sales/details`, `/channels/hierarchical`) — is reached from its parent,
49
+ // never the home hub. Using the structural depth, not a `[` heuristic, keeps
50
+ // bracket-less nested paths off the hub.
51
+ if (screen.depth !== undefined && screen.depth !== 1) return false;
52
+ if (screen.path.includes("[")) return false;
53
+ return (
54
+ screen.uiPattern === "table" ||
55
+ screen.uiPattern === "catalog" ||
56
+ screen.uiPattern === "detail"
57
+ );
58
+ }
59
+
60
+ /** Humanize a snake_case resource name for prose: `file_version_legal_holds`
61
+ * → `file version legal holds`. Keeps it lower-case for mid-sentence use. */
62
+ function humanizeResource(resource: string): string {
63
+ return resource.replace(/_/g, " ").trim();
64
+ }
65
+
66
+ function purposeOf(screen: IPlannedScreen): string {
67
+ const name = humanizeResource(screen.resource);
68
+ switch (screen.kind) {
69
+ case "list":
70
+ return `Browse and search ${name}.`;
71
+ case "detail":
72
+ return `View one ${name} and act on it.`;
73
+ case "create":
74
+ return `Create a new ${name}.`;
75
+ case "edit":
76
+ return `Edit an existing ${name}.`;
77
+ }
78
+ }
79
+
80
+ function notesOf(screen: IPlannedScreen): string {
81
+ switch (screen.kind) {
82
+ case "list":
83
+ return screen.secondary.some((c) => c.role === "search")
84
+ ? "Includes search/filter; rows link to the detail screen."
85
+ : "Rows link to the detail screen.";
86
+ case "detail":
87
+ return "Surfaces update / delete / action endpoints as buttons.";
88
+ case "create":
89
+ return "Render a form from the request-body schema; submit creates.";
90
+ case "edit":
91
+ return "Render a form from the request-body schema, prefilled from detail.";
92
+ }
93
+ }
94
+
95
+ function accessorsOf(screen: IPlannedScreen): string[] {
96
+ return [
97
+ screen.primary.accessor.join("."),
98
+ ...screen.secondary.map((c) => c.endpoint.accessor.join(".")),
99
+ ].filter((a, i, arr) => a.length > 0 && arr.indexOf(a) === i);
100
+ }
101
+
102
+ /**
103
+ * How a would-be list screen's primary response reads structurally:
104
+ *
105
+ * - `browsable` — an array or a page wrapper (`{ pagination, data: T[] }`). The
106
+ * only shape that genuinely belongs in a table.
107
+ * - `void` — no response body (health checks, fire-and-forget). Nothing to show.
108
+ * - `singleton-read` — a single object returned by a GET (system status, the
109
+ * current actor, a singleton config). Real data, but it is one record, not a
110
+ * list — render it as a detail view, not a fake one-row table.
111
+ * - `action` — a single object returned by a mutating verb (auth login/refresh,
112
+ * activate). Not a view at all — omit it.
113
+ *
114
+ * Purely structural: response shape + HTTP method, no domain keywords. This is
115
+ * what turns "every PATCH/GET without an item param becomes a table" (which
116
+ * dumped auth/monitoring verbs into the nav) into an honest screen set.
117
+ */
118
+ type ListResponseClass = "browsable" | "void" | "singleton-read" | "action";
119
+
120
+ function classifyListResponse(
121
+ op: IAutoViewEndpoint | undefined,
122
+ document: OpenApi.IDocument,
123
+ ): ListResponseClass {
124
+ if (op === undefined || (op.responseBody === null && op.responseSchema === null))
125
+ return "void";
126
+ // Use the SAME collection detection the table renderer uses, so a response is
127
+ // classed browsable iff a real row collection (incl. resource-named / inline
128
+ // wrappers, DigitalOcean) can actually be rendered as a table.
129
+ if (responseCollection(op, document) !== null) return "browsable";
130
+ return op.method.toLowerCase() === "get" ? "singleton-read" : "action";
131
+ }
132
+
133
+ export function buildDeterministicPlan(
134
+ document: OpenApi.IDocument,
135
+ actor: string,
136
+ filter: IEndpointFilter = {},
137
+ ): IAutoViewProductPlan {
138
+ const endpoints = filterEndpoints(toEndpoints(document), filter);
139
+ const planned = resourcePlan(endpoints, document);
140
+ const byAccessor = new Map(endpoints.map((e) => [e.accessor.join("."), e]));
141
+
142
+ const noiseOmissions: IAutoViewProductPlan.IOmission[] = [];
143
+ const resourceScreens: IAutoViewProductPlan.IScreen[] = [];
144
+ for (const s of planned) {
145
+ const screen: IAutoViewProductPlan.IScreen = {
146
+ path: s.path,
147
+ title: s.title,
148
+ purpose: purposeOf(s),
149
+ actor,
150
+ endpoints: accessorsOf(s),
151
+ uiPattern: UI_PATTERN[s.kind],
152
+ notes: notesOf(s),
153
+ depth: s.depth,
154
+ };
155
+ // A table screen must have a browsable response, or it is noise. Downgrade
156
+ // singleton reads to detail views; omit voids and action verbs.
157
+ if (screen.uiPattern === "table") {
158
+ const op = byAccessor.get(s.primary.accessor.join("."));
159
+ const cls = classifyListResponse(op, document);
160
+ if (cls === "void" || cls === "action") {
161
+ noiseOmissions.push({
162
+ target: `${op?.method.toUpperCase() ?? "?"} ${op?.path ?? s.path}`,
163
+ reason:
164
+ cls === "void"
165
+ ? "No response body to display."
166
+ : "Action endpoint (single object from a mutating verb) — not a browsable list.",
167
+ });
168
+ continue;
169
+ }
170
+ if (cls === "singleton-read") {
171
+ resourceScreens.push({
172
+ ...screen,
173
+ uiPattern: "detail",
174
+ purpose: `View ${humanizeResource(s.resource)}.`,
175
+ notes: "Singleton read — one record, not a list.",
176
+ });
177
+ continue;
178
+ }
179
+ // Browsable: a collection whose rows carry an image + a title reads far
180
+ // better as a catalog of cards than a dense table. Purely structural —
181
+ // the element schema decides, no domain keywords.
182
+ const collection = op ? responseCollection(op, document) : null;
183
+ if (collection !== null && isCatalogCollection(collection.columns)) {
184
+ resourceScreens.push({
185
+ ...screen,
186
+ uiPattern: "catalog",
187
+ notes: "Image + title rows — rendered as a card catalog.",
188
+ });
189
+ continue;
190
+ }
191
+ }
192
+ resourceScreens.push(screen);
193
+ }
194
+
195
+ // Drop write-only resources. A browse app shows data, so a resource that has
196
+ // no list / detail / search screen at all — only a create/update form or an
197
+ // RPC verb (OAuth2 `token`/`revoke`, a `metadata_query/execute_read`) — is
198
+ // pure nav noise: a lone form seated next to real resources in the primary
199
+ // nav. Omit it (auditably) so a large swagger's sidebar shows browsable
200
+ // resources, not every endpoint.
201
+ const resourceKeyOf = (p: string): string =>
202
+ p.split("/").filter((s) => s.length > 0 && !s.startsWith("[")).at(0) ?? p;
203
+ const resourceHasRead = new Map<string, boolean>();
204
+ for (const s of resourceScreens) {
205
+ const key = resourceKeyOf(s.path);
206
+ const isRead = s.uiPattern === "table" || s.uiPattern === "detail";
207
+ resourceHasRead.set(key, (resourceHasRead.get(key) ?? false) || isRead);
208
+ }
209
+ const browsableScreens = resourceScreens
210
+ .filter((s) => resourceHasRead.get(resourceKeyOf(s.path)) === true)
211
+ // Alphabetical by path so a large swagger's sidebar is scannable (same
212
+ // resource's pages stay grouped: /x, /x/[id], /x/[id]/edit, /x/new).
213
+ .sort((a, b) => a.path.localeCompare(b.path));
214
+ for (const s of resourceScreens) {
215
+ if (resourceHasRead.get(resourceKeyOf(s.path)) === true) continue;
216
+ for (const accessor of s.endpoints) {
217
+ const op = byAccessor.get(accessor);
218
+ noiseOmissions.push({
219
+ target: op ? `${op.method.toUpperCase()} ${op.path}` : s.path,
220
+ reason:
221
+ "Write-only resource (no list/detail/search to browse) — omitted from the UI as nav noise.",
222
+ });
223
+ }
224
+ }
225
+
226
+ // Root landing hub. Without an explicit `/` screen the dev server shows the
227
+ // template's "not customized yet" placeholder; this links to each resource's
228
+ // list and fetches nothing of its own.
229
+ const home: IAutoViewProductPlan.IScreen = {
230
+ path: "/",
231
+ title: "Home",
232
+ purpose: "Landing hub linking to each resource.",
233
+ actor,
234
+ endpoints: [],
235
+ uiPattern: "landing",
236
+ notes: "Links to each resource's list screen; no data fetch of its own.",
237
+ };
238
+ const screens = [home, ...browsableScreens];
239
+
240
+ // Single-actor navigation: home + every top-level screen leads the primary
241
+ // nav. "Top-level" = a list table or a singleton read (a bracket-less detail
242
+ // page like `/system`); entity-detail / create / edit pages (which carry a
243
+ // `[param]` or are reached from a list) go to secondary.
244
+ const topLevelPaths = browsableScreens
245
+ .filter((s) => isTopLevel(s))
246
+ .map((s) => s.path);
247
+ const secondary = browsableScreens
248
+ .filter((s) => !isTopLevel(s))
249
+ .map((s) => s.path);
250
+ const navigation: IAutoViewProductPlan.INavigation[] = [
251
+ { actor, primary: ["/", ...topLevelPaths], secondary },
252
+ ];
253
+
254
+ // Operations the READ layer could not turn into a screen — surfaced as
255
+ // honest omissions instead of vanishing. Noise screens (auth/monitoring verbs)
256
+ // removed above are reported here too, so "omitted, not broken" stays auditable.
257
+ const intentionalOmissions: IAutoViewProductPlan.IOmission[] = [
258
+ ...noiseOmissions,
259
+ ...unsupportedOperations(document).map((u) => ({
260
+ target: `${u.method} ${u.path}`,
261
+ reason: `Unsupported content type — ${u.reason}`,
262
+ })),
263
+ ];
264
+
265
+ // Integrity backstop — "정확하게 읽는다" rule #1: nothing the READ layer parsed
266
+ // may silently vanish. Every operation must be referenced by some screen
267
+ // (primary or secondary) or appear in an omission. Anything left over (a
268
+ // second create on a resource that already had one, a deeply nested
269
+ // delete with no detail to host it, an auth verb that did not classify) is
270
+ // recorded here so coverage is total and auditable rather than lossy.
271
+ const referenced = new Set<string>();
272
+ for (const s of browsableScreens) for (const a of s.endpoints) referenced.add(a);
273
+ const omittedPaths = new Set(
274
+ intentionalOmissions.map((o) => o.target),
275
+ );
276
+ for (const e of endpoints) {
277
+ const accessor = e.accessor.join(".");
278
+ if (referenced.has(accessor)) continue;
279
+ const target = `${e.method.toUpperCase()} ${e.path}`;
280
+ if (omittedPaths.has(target)) continue;
281
+ intentionalOmissions.push({
282
+ target,
283
+ reason: `Not yet mapped to a screen (accessor ${accessor}) — a duplicate-role or deeply nested endpoint the deterministic plan does not surface yet.`,
284
+ });
285
+ omittedPaths.add(target);
286
+ }
287
+
288
+ return { screens, navigation, intentionalOmissions };
289
+ }
@@ -0,0 +1,90 @@
1
+ import { OpenApiConverter } from "@typia/utils";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { buildDeterministicSdkMap } from "./buildDeterministicSdkMap";
5
+
6
+ function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
7
+ return OpenApiConverter.upgradeDocument({
8
+ openapi: "3.0.0",
9
+ info: { title: "t", version: "1.0.0" },
10
+ paths,
11
+ components: { schemas },
12
+ // biome-ignore lint: test fixture
13
+ } as never);
14
+ }
15
+
16
+ const listResp = (item: string) => ({
17
+ 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: `#/components/schemas/${item}` } } } } },
18
+ });
19
+ const objResp = (name: string) => ({
20
+ 200: { description: "ok", content: { "application/json": { schema: { $ref: `#/components/schemas/${name}` } } } },
21
+ });
22
+ const param = (n: string) => ({ name: n, in: "path", required: true, schema: { type: "string" } });
23
+
24
+ describe("buildDeterministicSdkMap", () => {
25
+ it("derives resources from the swagger's CRUD shape (no LLM)", () => {
26
+ const d = doc(
27
+ {
28
+ "/sales": { get: { operationId: "index", responses: listResp("Sale") } },
29
+ "/sales/{saleId}": { get: { operationId: "at", parameters: [param("saleId")], responses: objResp("Sale") } },
30
+ "/orders": { get: { operationId: "index", responses: listResp("Order") } },
31
+ },
32
+ {
33
+ Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
34
+ Order: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
35
+ },
36
+ );
37
+ const map = buildDeterministicSdkMap(d);
38
+ expect(map.resources.map((r) => r.name).sort()).toEqual(["orders", "sales"]);
39
+ // every resource carries a non-empty namespace + purpose
40
+ for (const r of map.resources) {
41
+ expect(r.namespace.length).toBeGreaterThan(0);
42
+ expect(r.purpose.length).toBeGreaterThan(0);
43
+ }
44
+ });
45
+
46
+ it("falls back to a single `user` actor when no role segment is present", () => {
47
+ const d = doc(
48
+ { "/pet": { get: { operationId: "index", responses: listResp("Pet") } } },
49
+ { Pet: { type: "object", properties: { id: { type: "string" } } } },
50
+ );
51
+ const map = buildDeterministicSdkMap(d);
52
+ expect(map.actors.map((a) => a.name)).toEqual(["user"]);
53
+ expect(map.actors[0]!.journeys.length).toBeGreaterThanOrEqual(1);
54
+ });
55
+
56
+ it("derives multiple actors from role segments in the accessor", () => {
57
+ const d = doc(
58
+ {
59
+ "/shoppings/customers/sales": { get: { operationId: "index", responses: listResp("Sale") } },
60
+ "/shoppings/sellers/sales": { post: { operationId: "create", requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } }, responses: objResp("Sale") } },
61
+ },
62
+ { Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
63
+ );
64
+ const map = buildDeterministicSdkMap(d);
65
+ expect(map.actors.map((a) => a.name)).toContain("customer");
66
+ expect(map.actors.map((a) => a.name)).toContain("seller");
67
+ // every actor has at least one journey (the SDK-map invariant)
68
+ for (const a of map.actors) expect(a.journeys.length).toBeGreaterThanOrEqual(1);
69
+ });
70
+
71
+ it("satisfies the downstream SDK-map invariants (≥1 resource, ≥1 actor, namespaces match accessors)", () => {
72
+ const d = doc(
73
+ {
74
+ "/sales/{saleId}": { get: { operationId: "at", parameters: [param("saleId")], responses: objResp("Sale") } },
75
+ },
76
+ { Sale: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
77
+ );
78
+ const map = buildDeterministicSdkMap(d);
79
+ expect(map.resources.length).toBeGreaterThanOrEqual(1);
80
+ expect(map.actors.length).toBeGreaterThanOrEqual(1);
81
+ });
82
+
83
+ it("is deterministic — same document, same map", () => {
84
+ const d = doc(
85
+ { "/sales": { get: { operationId: "index", responses: listResp("Sale") } } },
86
+ { Sale: { type: "object", properties: { id: { type: "string" } } } },
87
+ );
88
+ expect(buildDeterministicSdkMap(d)).toEqual(buildDeterministicSdkMap(d));
89
+ });
90
+ });
@@ -0,0 +1,169 @@
1
+ import { OpenApi } from "@typia/interface";
2
+
3
+ import { EndpointRole, classifyEndpoints } from "../../utils/classifyEndpoints";
4
+ import { toEndpoints } from "../../utils/toEndpoints";
5
+ import { IAutoViewSdkMap } from "../structures/IAutoViewSdkMap";
6
+
7
+ /**
8
+ * Deterministic SDK domain map — the LLM-free replacement for the Phase-1 SDK
9
+ * Study, and the fix for "a large swagger overflows the model's context".
10
+ *
11
+ * The original SDK Study compiled the WHOLE SDK source and asked the model to
12
+ * infer resources / actors / journeys. On a 200-type swagger that prompt is
13
+ * 300k+ tokens — a hard context overflow. But the downstream phases barely use
14
+ * the map's prose: the Product Plan is already deterministic and reads only
15
+ * `actors[0].name`; Scaffold/Review embed the map as a wiki document; Render
16
+ * reads an actor's journeys only on the LLM rerender fallback. So the map can be
17
+ * derived structurally — same swagger, same map, zero tokens — without changing
18
+ * what reaches the user.
19
+ *
20
+ * - resources: the top-level CRUD groups from {@link classifyEndpoints}.
21
+ * - actors: role segments found in the accessors (`customers`, `sellers`,
22
+ * `admins`), normalized; falls back to a single `user`.
23
+ * - journeys: a short structural summary per actor (never empty — the SDK-map
24
+ * invariant requires ≥1 per actor).
25
+ */
26
+ export function buildDeterministicSdkMap(
27
+ document: OpenApi.IDocument,
28
+ ): IAutoViewSdkMap {
29
+ const endpoints = toEndpoints(document);
30
+ const groups = classifyEndpoints(endpoints, document);
31
+
32
+ const actors = deriveActors(endpoints);
33
+ const actorNames = actors.map((a) => a.name);
34
+
35
+ // Top-level resources only (depth 1). Nested groups (`sales/questions`) fold
36
+ // into their parent for the domain map; the Product Plan still renders their
37
+ // screens from the full classification.
38
+ const resources: IAutoViewSdkMap.IResource[] = groups
39
+ .filter((g) => g.depth === 1 && g.resource !== "root")
40
+ .map((g) => {
41
+ const sample = g.endpoints[0]?.endpoint;
42
+ const namespace =
43
+ sample !== undefined && sample.accessor.length > 1
44
+ ? sample.accessor.slice(0, -1).join(".")
45
+ : g.resource;
46
+ return {
47
+ name: g.resource,
48
+ namespace,
49
+ purpose: purposeOf(g.resource, g.roles),
50
+ actorsInvolved: actorNames,
51
+ notes: notesOf(g.roles),
52
+ };
53
+ });
54
+
55
+ return {
56
+ resources,
57
+ actors,
58
+ notableConstraints: deriveConstraints(endpoints),
59
+ };
60
+ }
61
+
62
+ /* -------------------------------------------------------------------------- */
63
+ /* actors */
64
+ /* -------------------------------------------------------------------------- */
65
+
66
+ /**
67
+ * Accessor segments that name an actor role, mapped to their canonical
68
+ * singular display name. Real swaggers expose an actor as a namespace segment
69
+ * (`shoppings.customers.sales`, `shoppings.sellers.sales`); we read the role
70
+ * straight off the accessor instead of guessing from the domain.
71
+ */
72
+ const ROLE_SEGMENTS: Record<string, string> = {
73
+ customer: "customer",
74
+ customers: "customer",
75
+ seller: "seller",
76
+ sellers: "seller",
77
+ admin: "administrator",
78
+ admins: "administrator",
79
+ administrator: "administrator",
80
+ administrators: "administrator",
81
+ member: "member",
82
+ members: "member",
83
+ buyer: "buyer",
84
+ buyers: "buyer",
85
+ manager: "manager",
86
+ managers: "manager",
87
+ guest: "guest",
88
+ guests: "guest",
89
+ };
90
+
91
+ function deriveActors(
92
+ endpoints: ReturnType<typeof toEndpoints>,
93
+ ): IAutoViewSdkMap.IActor[] {
94
+ const names: string[] = [];
95
+ const seen = new Set<string>();
96
+ for (const e of endpoints) {
97
+ for (const seg of e.accessor) {
98
+ const canon = ROLE_SEGMENTS[seg.toLowerCase()];
99
+ if (canon !== undefined && !seen.has(canon)) {
100
+ seen.add(canon);
101
+ names.push(canon);
102
+ }
103
+ }
104
+ }
105
+ const finalNames = names.length > 0 ? names : ["user"];
106
+ // Lead with the end-user actor: the Product Plan assigns every screen to
107
+ // `actors[0]`, so a customer-facing label reads more naturally on the nav than
108
+ // an admin one. Purely cosmetic — the screen set is identical either way.
109
+ const ordered = [...finalNames].sort(
110
+ (a, b) => actorRank(a) - actorRank(b),
111
+ );
112
+ return ordered.map((name) => ({
113
+ name,
114
+ journeys: [`Sign in as ${name} → navigate the resources they can access.`],
115
+ }));
116
+ }
117
+
118
+ /** End-users first, privileged roles last; unknowns keep their found order. */
119
+ const ACTOR_PRIORITY = [
120
+ "customer",
121
+ "buyer",
122
+ "member",
123
+ "user",
124
+ "guest",
125
+ "seller",
126
+ "manager",
127
+ "administrator",
128
+ ];
129
+ function actorRank(name: string): number {
130
+ const i = ACTOR_PRIORITY.indexOf(name);
131
+ return i === -1 ? ACTOR_PRIORITY.length : i;
132
+ }
133
+
134
+ /* -------------------------------------------------------------------------- */
135
+ /* resource prose (deterministic) */
136
+ /* -------------------------------------------------------------------------- */
137
+
138
+ function purposeOf(resource: string, roles: Set<EndpointRole>): string {
139
+ const verbs: string[] = [];
140
+ if (roles.has("list") || roles.has("search")) verbs.push("browse");
141
+ if (roles.has("detail")) verbs.push("view");
142
+ if (roles.has("create")) verbs.push("create");
143
+ if (roles.has("update")) verbs.push("update");
144
+ if (roles.has("delete")) verbs.push("remove");
145
+ if (roles.has("action")) verbs.push("act on");
146
+ const verb = verbs.length > 0 ? verbs.join(", ") : "work with";
147
+ return `Lets a user ${verb} ${resource}.`;
148
+ }
149
+
150
+ function notesOf(roles: Set<EndpointRole>): string {
151
+ return roles.has("search")
152
+ ? "Supports search/filter/pagination via query parameters."
153
+ : "";
154
+ }
155
+
156
+ function deriveConstraints(
157
+ endpoints: ReturnType<typeof toEndpoints>,
158
+ ): string[] {
159
+ const constraints: string[] = [];
160
+ if (endpoints.some((e) => e.query !== null)) {
161
+ constraints.push(
162
+ "Some list endpoints accept query parameters for search/filter/pagination.",
163
+ );
164
+ }
165
+ if (endpoints.some((e) => e.method.toLowerCase() === "delete")) {
166
+ constraints.push("Delete endpoints are present — confirm before destructive actions.");
167
+ }
168
+ return constraints;
169
+ }
@@ -0,0 +1,136 @@
1
+ import { spawn } from "child_process";
2
+ import fs from "fs/promises";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ /**
7
+ * Per-package.json cache directory holding an installed `node_modules`. Sits
8
+ * under `$XDG_CACHE_HOME` when set, else `~/.cache`, partitioned by
9
+ * `(cacheNamespace, hash)` so callers with different dependency sets — and
10
+ * different downstream consumers (typecheck vs runtime audit) — never collide
11
+ * on the same cache key.
12
+ *
13
+ * Returns the absolute path to the populated `node_modules` directory on
14
+ * success, or `null` when the cache could not be set up (no writable cache
15
+ * root, the install crashed, etc.). Callers fall back to a plain in-place
16
+ * install in that case so the behavior degrades gracefully.
17
+ *
18
+ * `extraDeps` lets a caller add packages on top of the bare frontend dependency
19
+ * set without renaming the cache namespace: the runtime-audit cache uses
20
+ * `["playwright@^1.50.0"]` so the cached `node_modules` already contains
21
+ * Playwright the next time the agent runs an audit, on top of the same
22
+ * React/Next/shadcn tree as the typecheck cache.
23
+ */
24
+ export async function ensureCachedNodeModules(args: {
25
+ cacheNamespace: string;
26
+ hash: string;
27
+ packageJson: string;
28
+ extraDeps?: readonly string[];
29
+ }): Promise<string | null> {
30
+ if (args.hash === "no-package-json") return null;
31
+ const cacheRoot = path.join(
32
+ process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"),
33
+ `autoview-${args.cacheNamespace}`,
34
+ args.hash,
35
+ );
36
+ const nodeModules = path.join(cacheRoot, "node_modules");
37
+ try {
38
+ // `node_modules/.package-lock.json` is the canonical "install completed"
39
+ // sentinel npm writes at the end of `npm install`. Its presence means
40
+ // the previous run finished cleanly and the cache is reusable.
41
+ await fs.access(path.join(nodeModules, ".package-lock.json"));
42
+ return nodeModules;
43
+ } catch {
44
+ // Cache miss: populate it. Use a sibling temp dir + rename so a
45
+ // concurrent agent run cannot observe a half-written node_modules.
46
+ const installer = await fs.mkdtemp(
47
+ path.join(
48
+ os.tmpdir(),
49
+ `autoview-${args.cacheNamespace}-cache-${args.hash}-`,
50
+ ),
51
+ );
52
+ try {
53
+ await fs.writeFile(
54
+ path.join(installer, "package.json"),
55
+ args.packageJson,
56
+ "utf-8",
57
+ );
58
+ await runNpm(installer, [
59
+ "install",
60
+ "--silent",
61
+ "--no-audit",
62
+ "--no-fund",
63
+ ]);
64
+ if (args.extraDeps !== undefined && args.extraDeps.length > 0) {
65
+ await runNpm(installer, [
66
+ "install",
67
+ "--silent",
68
+ "--no-audit",
69
+ "--no-fund",
70
+ "--no-save",
71
+ ...args.extraDeps,
72
+ ]);
73
+ }
74
+ await fs.mkdir(cacheRoot, { recursive: true });
75
+ // Atomically move node_modules into place. If something is already
76
+ // there (concurrent run won the race), rm-then-rename overwrites it
77
+ // — both are valid installs of the same package.json.
78
+ await fs.rm(nodeModules, { recursive: true, force: true });
79
+ await fs.rename(path.join(installer, "node_modules"), nodeModules);
80
+ } catch {
81
+ await fs.rm(installer, { recursive: true, force: true }).catch(() => {});
82
+ return null;
83
+ }
84
+ await fs.rm(installer, { recursive: true, force: true }).catch(() => {});
85
+ try {
86
+ await fs.access(path.join(nodeModules, ".package-lock.json"));
87
+ return nodeModules;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Create a symlink from `<workingDir>/node_modules` → `<cachedNodeModules>`.
96
+ * Returns `false` when the symlink could not be created (Windows without admin,
97
+ * filesystem refuses links, etc.) so the caller falls back to a plain in-place
98
+ * install.
99
+ */
100
+ export async function tryLinkNodeModules(
101
+ workingDir: string,
102
+ cachedNodeModules: string,
103
+ ): Promise<boolean> {
104
+ const target = path.join(workingDir, "node_modules");
105
+ try {
106
+ await fs.symlink(cachedNodeModules, target, "dir");
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ function runNpm(cwd: string, args: string[]): Promise<void> {
114
+ return new Promise((resolve, reject) => {
115
+ const child = spawn("npm", args, { cwd, stdio: "pipe" });
116
+ let stderr = "";
117
+ child.stderr.on("data", (chunk) => {
118
+ stderr += chunk.toString("utf-8");
119
+ });
120
+ child.on("error", (err) => {
121
+ reject(
122
+ new Error(`Failed to spawn \`npm ${args.join(" ")}\`: ${err.message}`),
123
+ );
124
+ });
125
+ child.on("close", (code) => {
126
+ if (code === 0) resolve();
127
+ else
128
+ reject(
129
+ new Error(
130
+ `\`npm ${args.slice(0, 3).join(" ")}...\` exited with code ${code}` +
131
+ (stderr.length > 0 ? `:\n${stderr.slice(-1500)}` : ""),
132
+ ),
133
+ );
134
+ });
135
+ });
136
+ }