@superblocksteam/vite-plugin-file-sync 2.0.123 → 2.0.124-next.1

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 (271) hide show
  1. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +16 -1
  2. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  3. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +32 -1
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
  6. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +3 -1
  7. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
  8. package/dist/ai-service/agent/tools/apis/api-comparator.d.ts.map +1 -1
  9. package/dist/ai-service/agent/tools/apis/api-comparator.js +1 -4
  10. package/dist/ai-service/agent/tools/apis/api-comparator.js.map +1 -1
  11. package/dist/ai-service/agent/tools/apis/api-testing-state.js +7 -0
  12. package/dist/ai-service/agent/tools/apis/api-testing-state.js.map +1 -1
  13. package/dist/ai-service/agent/tools/apis/get-api-docs.d.ts +1 -1
  14. package/dist/ai-service/agent/tools/apis/write-api.d.ts +2 -2
  15. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  16. package/dist/ai-service/agent/tools/build-capture-screenshot.js +11 -33
  17. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  18. package/dist/ai-service/agent/tools/build-copy-directory.d.ts +1 -1
  19. package/dist/ai-service/agent/tools/build-finalize.d.ts.map +1 -1
  20. package/dist/ai-service/agent/tools/build-finalize.js +11 -2
  21. package/dist/ai-service/agent/tools/build-finalize.js.map +1 -1
  22. package/dist/ai-service/agent/tools/build-install-packages.d.ts +41 -1
  23. package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
  24. package/dist/ai-service/agent/tools/build-install-packages.js +217 -0
  25. package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
  26. package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts +28 -0
  27. package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts.map +1 -0
  28. package/dist/ai-service/agent/tools/build-lookup-npm-package.js +78 -0
  29. package/dist/ai-service/agent/tools/build-lookup-npm-package.js.map +1 -0
  30. package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +4 -4
  31. package/dist/ai-service/agent/tools/build-navigate-preview.d.ts +16 -0
  32. package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -0
  33. package/dist/ai-service/agent/tools/build-navigate-preview.js +68 -0
  34. package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -0
  35. package/dist/ai-service/agent/tools/build-write-file.d.ts +1 -1
  36. package/dist/ai-service/agent/tools/databases/dev-database.d.ts +9 -9
  37. package/dist/ai-service/agent/tools/index.d.ts +2 -0
  38. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  39. package/dist/ai-service/agent/tools/index.js +2 -0
  40. package/dist/ai-service/agent/tools/index.js.map +1 -1
  41. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +3 -3
  42. package/dist/ai-service/agent/tools/report-security-findings.d.ts +11 -5
  43. package/dist/ai-service/agent/tools/report-security-findings.d.ts.map +1 -1
  44. package/dist/ai-service/agent/tools/report-security-findings.js +1 -0
  45. package/dist/ai-service/agent/tools/report-security-findings.js.map +1 -1
  46. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  47. package/dist/ai-service/agent/tools.js +3 -1
  48. package/dist/ai-service/agent/tools.js.map +1 -1
  49. package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
  50. package/dist/ai-service/agent/tools2/tools/grep.d.ts +2 -2
  51. package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +2 -2
  52. package/dist/ai-service/app-interface/npm-error-parser.d.ts +161 -0
  53. package/dist/ai-service/app-interface/npm-error-parser.d.ts.map +1 -0
  54. package/dist/ai-service/app-interface/npm-error-parser.js +510 -0
  55. package/dist/ai-service/app-interface/npm-error-parser.js.map +1 -0
  56. package/dist/ai-service/app-interface/npm-package-lookup.d.ts +286 -0
  57. package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -0
  58. package/dist/ai-service/app-interface/npm-package-lookup.js +793 -0
  59. package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -0
  60. package/dist/ai-service/app-interface/npm-registry.d.ts +735 -47
  61. package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
  62. package/dist/ai-service/app-interface/npm-registry.js +1882 -153
  63. package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
  64. package/dist/ai-service/app-interface/shell.d.ts +118 -1
  65. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  66. package/dist/ai-service/app-interface/shell.js +590 -157
  67. package/dist/ai-service/app-interface/shell.js.map +1 -1
  68. package/dist/ai-service/app-skills/manager.d.ts +1 -1
  69. package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
  70. package/dist/ai-service/app-skills/manager.js +39 -7
  71. package/dist/ai-service/app-skills/manager.js.map +1 -1
  72. package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts +9 -0
  73. package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts.map +1 -0
  74. package/dist/ai-service/checklist/api-migration-checklist-gate.js +30 -0
  75. package/dist/ai-service/checklist/api-migration-checklist-gate.js.map +1 -0
  76. package/dist/ai-service/checklist/api-migration-origins.d.ts +4 -0
  77. package/dist/ai-service/checklist/api-migration-origins.d.ts.map +1 -0
  78. package/dist/ai-service/checklist/api-migration-origins.js +8 -0
  79. package/dist/ai-service/checklist/api-migration-origins.js.map +1 -0
  80. package/dist/ai-service/checklist/persisted-checklist-store.d.ts +2 -3
  81. package/dist/ai-service/checklist/persisted-checklist-store.d.ts.map +1 -1
  82. package/dist/ai-service/checklist/persisted-checklist-store.js +6 -7
  83. package/dist/ai-service/checklist/persisted-checklist-store.js.map +1 -1
  84. package/dist/ai-service/context-archive-paths.d.ts +4 -0
  85. package/dist/ai-service/context-archive-paths.d.ts.map +1 -0
  86. package/dist/ai-service/context-archive-paths.js +28 -0
  87. package/dist/ai-service/context-archive-paths.js.map +1 -0
  88. package/dist/ai-service/context-download.d.ts +3 -1
  89. package/dist/ai-service/context-download.d.ts.map +1 -1
  90. package/dist/ai-service/context-download.js +48 -9
  91. package/dist/ai-service/context-download.js.map +1 -1
  92. package/dist/ai-service/context-upload.d.ts +3 -2
  93. package/dist/ai-service/context-upload.d.ts.map +1 -1
  94. package/dist/ai-service/context-upload.js +44 -26
  95. package/dist/ai-service/context-upload.js.map +1 -1
  96. package/dist/ai-service/dev-database-client.d.ts +3 -11
  97. package/dist/ai-service/dev-database-client.d.ts.map +1 -1
  98. package/dist/ai-service/dev-database-client.js.map +1 -1
  99. package/dist/ai-service/features.d.ts +4 -0
  100. package/dist/ai-service/features.d.ts.map +1 -1
  101. package/dist/ai-service/features.js +8 -0
  102. package/dist/ai-service/features.js.map +1 -1
  103. package/dist/ai-service/filter-disabled-tools-for-migration.d.ts.map +1 -1
  104. package/dist/ai-service/filter-disabled-tools-for-migration.js +4 -0
  105. package/dist/ai-service/filter-disabled-tools-for-migration.js.map +1 -1
  106. package/dist/ai-service/index.d.ts +103 -0
  107. package/dist/ai-service/index.d.ts.map +1 -1
  108. package/dist/ai-service/index.js +610 -25
  109. package/dist/ai-service/index.js.map +1 -1
  110. package/dist/ai-service/llm/client.d.ts +38 -3
  111. package/dist/ai-service/llm/client.d.ts.map +1 -1
  112. package/dist/ai-service/llm/client.js +73 -22
  113. package/dist/ai-service/llm/client.js.map +1 -1
  114. package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -1
  115. package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
  116. package/dist/ai-service/llm/context-v2/adapter.js +11 -11
  117. package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
  118. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts +44 -0
  119. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -0
  120. package/dist/ai-service/llm/context-v2/compaction/client-side.js +170 -0
  121. package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -0
  122. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts +56 -0
  123. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts.map +1 -0
  124. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js +9 -0
  125. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js.map +1 -0
  126. package/dist/ai-service/llm/context-v2/compaction/index.d.ts +5 -0
  127. package/dist/ai-service/llm/context-v2/compaction/index.d.ts.map +1 -0
  128. package/dist/ai-service/llm/context-v2/compaction/index.js +3 -0
  129. package/dist/ai-service/llm/context-v2/compaction/index.js.map +1 -0
  130. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +30 -0
  131. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -0
  132. package/dist/ai-service/llm/context-v2/compaction/server-side.js +130 -0
  133. package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -0
  134. package/dist/ai-service/llm/context-v2/context-management.d.ts +203 -0
  135. package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -0
  136. package/dist/ai-service/llm/context-v2/context-management.js +183 -0
  137. package/dist/ai-service/llm/context-v2/context-management.js.map +1 -0
  138. package/dist/ai-service/llm/context-v2/context.d.ts +64 -22
  139. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  140. package/dist/ai-service/llm/context-v2/context.js +233 -157
  141. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  142. package/dist/ai-service/llm/context-v2/conversation-context.d.ts +24 -7
  143. package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
  144. package/dist/ai-service/llm/context-v2/conversation-context.js +1 -1
  145. package/dist/ai-service/llm/context-v2/index.d.ts +0 -4
  146. package/dist/ai-service/llm/context-v2/index.d.ts.map +1 -1
  147. package/dist/ai-service/llm/context-v2/index.js +0 -4
  148. package/dist/ai-service/llm/context-v2/index.js.map +1 -1
  149. package/dist/ai-service/llm/context-v2/manager.d.ts +24 -3
  150. package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
  151. package/dist/ai-service/llm/context-v2/manager.js +146 -4
  152. package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
  153. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts +2 -7
  154. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts.map +1 -1
  155. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js +5 -73
  156. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js.map +1 -1
  157. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts +57 -0
  158. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts.map +1 -0
  159. package/dist/ai-service/llm/context-v2/storage/event-store.js +10 -0
  160. package/dist/ai-service/llm/context-v2/storage/event-store.js.map +1 -0
  161. package/dist/ai-service/llm/context-v2/storage/event-types.d.ts +50 -0
  162. package/dist/ai-service/llm/context-v2/storage/event-types.d.ts.map +1 -0
  163. package/dist/ai-service/llm/context-v2/storage/event-types.js +10 -0
  164. package/dist/ai-service/llm/context-v2/storage/event-types.js.map +1 -0
  165. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts +87 -0
  166. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts.map +1 -0
  167. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js +184 -0
  168. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js.map +1 -0
  169. package/dist/ai-service/llm/context-v2/storage/migration.d.ts +49 -0
  170. package/dist/ai-service/llm/context-v2/storage/migration.d.ts.map +1 -0
  171. package/dist/ai-service/llm/context-v2/storage/migration.js +126 -0
  172. package/dist/ai-service/llm/context-v2/storage/migration.js.map +1 -0
  173. package/dist/ai-service/llm/context-v2/types.d.ts +16 -11
  174. package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
  175. package/dist/ai-service/llm/context-v2/types.js +2 -2
  176. package/dist/ai-service/llm/context-v2/types.js.map +1 -1
  177. package/dist/ai-service/llm/impl/clark.d.ts +8 -0
  178. package/dist/ai-service/llm/impl/clark.d.ts.map +1 -1
  179. package/dist/ai-service/llm/impl/clark.js +8 -0
  180. package/dist/ai-service/llm/impl/clark.js.map +1 -1
  181. package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
  182. package/dist/ai-service/llm/interaction/adapters/vercel.js +17 -1
  183. package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
  184. package/dist/ai-service/llm/provider.d.ts.map +1 -1
  185. package/dist/ai-service/llm/provider.js +1 -3
  186. package/dist/ai-service/llm/provider.js.map +1 -1
  187. package/dist/ai-service/llm/stream/observers/context.d.ts +23 -0
  188. package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
  189. package/dist/ai-service/llm/stream/observers/context.js +62 -0
  190. package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
  191. package/dist/ai-service/llm/tool-context-integrity.d.ts +25 -0
  192. package/dist/ai-service/llm/tool-context-integrity.d.ts.map +1 -0
  193. package/dist/ai-service/llm/tool-context-integrity.js +141 -0
  194. package/dist/ai-service/llm/tool-context-integrity.js.map +1 -0
  195. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
  196. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
  197. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +6 -1
  198. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
  199. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
  200. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
  201. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +3 -2
  202. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
  203. package/dist/ai-service/state-machine/clark-fsm.d.ts +23 -2
  204. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  205. package/dist/ai-service/state-machine/clark-fsm.js +37 -2
  206. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  207. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  208. package/dist/ai-service/state-machine/handlers/agent-planning.js +29 -2
  209. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  210. package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
  211. package/dist/ai-service/state-machine/handlers/awaiting-user.js +9 -0
  212. package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
  213. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  214. package/dist/ai-service/state-machine/handlers/llm-generating.js +25 -2
  215. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  216. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts +6 -0
  217. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -0
  218. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +36 -0
  219. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -0
  220. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +30 -1
  221. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
  222. package/dist/ai-service/state-machine/helpers/stable-peer.js +85 -2
  223. package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
  224. package/dist/ai-service/state-machine/helpers/user-preferences.d.ts +8 -0
  225. package/dist/ai-service/state-machine/helpers/user-preferences.d.ts.map +1 -0
  226. package/dist/ai-service/state-machine/helpers/user-preferences.js +11 -0
  227. package/dist/ai-service/state-machine/helpers/user-preferences.js.map +1 -0
  228. package/dist/ai-service/template-renderer.d.ts +18 -1
  229. package/dist/ai-service/template-renderer.d.ts.map +1 -1
  230. package/dist/ai-service/template-renderer.js +85 -20
  231. package/dist/ai-service/template-renderer.js.map +1 -1
  232. package/dist/ai-service/types.d.ts +157 -0
  233. package/dist/ai-service/types.d.ts.map +1 -1
  234. package/dist/ai-service/types.js +137 -0
  235. package/dist/ai-service/types.js.map +1 -1
  236. package/dist/ai-service/util/llm-config-utils.d.ts +21 -22
  237. package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
  238. package/dist/ai-service/util/llm-config-utils.js +40 -67
  239. package/dist/ai-service/util/llm-config-utils.js.map +1 -1
  240. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  241. package/dist/file-sync-vite-plugin.js +5 -0
  242. package/dist/file-sync-vite-plugin.js.map +1 -1
  243. package/dist/file-system-helpers.d.ts +17 -0
  244. package/dist/file-system-helpers.d.ts.map +1 -1
  245. package/dist/file-system-helpers.js +22 -0
  246. package/dist/file-system-helpers.js.map +1 -1
  247. package/dist/git-service/index.d.ts.map +1 -1
  248. package/dist/git-service/index.js +15 -1
  249. package/dist/git-service/index.js.map +1 -1
  250. package/dist/migration/migration-routes.d.ts.map +1 -1
  251. package/dist/migration/migration-routes.js +26 -3
  252. package/dist/migration/migration-routes.js.map +1 -1
  253. package/dist/migration/translation-prompt.d.ts.map +1 -1
  254. package/dist/migration/translation-prompt.js +5 -0
  255. package/dist/migration/translation-prompt.js.map +1 -1
  256. package/dist/migration-templates/app-fullstack/package.json +2 -2
  257. package/dist/policy-gate-callback-mapper.d.ts +24 -0
  258. package/dist/policy-gate-callback-mapper.d.ts.map +1 -0
  259. package/dist/policy-gate-callback-mapper.js +61 -0
  260. package/dist/policy-gate-callback-mapper.js.map +1 -0
  261. package/dist/policy-gate-runner.d.ts +32 -0
  262. package/dist/policy-gate-runner.d.ts.map +1 -0
  263. package/dist/policy-gate-runner.js +191 -0
  264. package/dist/policy-gate-runner.js.map +1 -0
  265. package/dist/router-parser.d.ts.map +1 -1
  266. package/dist/router-parser.js +107 -11
  267. package/dist/router-parser.js.map +1 -1
  268. package/dist/sync-service/list-dir.d.ts.map +1 -1
  269. package/dist/sync-service/list-dir.js +13 -3
  270. package/dist/sync-service/list-dir.js.map +1 -1
  271. package/package.json +28 -10
@@ -39,14 +39,53 @@ import { promisify } from "node:util";
39
39
  import { glob } from "glob";
40
40
  import { detect, resolveCommand } from "package-manager-detector";
41
41
  import { normalizePath } from "vite";
42
+ import { isProtectedCorePackage } from "@superblocksteam/library-shared/types";
43
+ import { bucketNpmRegistryHost, recordNpmInstall, } from "@superblocksteam/telemetry";
42
44
  import { getErrorMeta, getLogger } from "../../util/logger.js";
43
45
  import llmobs from "../llmobs/index.js";
44
- import { NpmInstallError } from "../types.js";
46
+ import { NpmInstallBlocked, NpmInstallError, scrubSecrets, } from "../types.js";
45
47
  import { CLARK_APP_BLACKLIST_PATTERNS, APPLICATION_FILE_PATTERNS, STANDARD_IGNORE_PATTERNS, } from "./constants.js";
46
48
  import { DraftManager, validateFileOperation, } from "./filesystem/index.js";
47
49
  import { Linter } from "./linter.js";
48
- import { maybeWriteNpmrcForDir, prepareForPrivateRegistry, PRESERVE_NPMRC_SCOPES, } from "./npm-registry.js";
50
+ import { parseNpmJsonDiagnostic, parseNpmJsonError, parseNpmJsonSuccess, } from "./npm-error-parser.js";
51
+ import { prepareForPrivateRegistry, PRESERVE_NPMRC_SCOPES, shouldIgnoreInstallScripts, } from "./npm-registry.js";
49
52
  const execPromise = promisify(exec);
53
+ // `npm install --json` emits the full install summary on stdout. The default
54
+ // 1 MiB exec buffer can be exceeded for fat dependency trees, which would
55
+ // fail a successful install with ERR_CHILD_PROCESS_STDIO_MAXBUFFER and reach
56
+ // `classifyNpmExecError` with truncated stdout — defeating the `--json`
57
+ // error-envelope contract these entry points depend on. 4 MiB comfortably
58
+ // covers real-world npm summaries (per-package entries are tiny) while
59
+ // keeping the worst-case buffer + parsed-object peak bounded.
60
+ const NPM_JSON_MAX_BUFFER = 4 * 1024 * 1024;
61
+ /**
62
+ * Map an install failure to a value in the closed `npm.outcome` enum
63
+ * (APPS-4189). `NpmInstallBlocked` carries the structured reason from
64
+ * P1.2; anything else is bucketed as `other` at the metric boundary.
65
+ * `normalizeNpmOutcome` enforces the allowlist a second time downstream
66
+ * so a stray reason value cannot leak past the emit boundary.
67
+ */
68
+ function outcomeFromInstallError(err) {
69
+ if (err instanceof NpmInstallBlocked)
70
+ return err.reason;
71
+ return "other";
72
+ }
73
+ /**
74
+ * Pick the registry URL the install actually targeted from the most-recent
75
+ * `prepareForPrivateRegistry` / `maybeWriteNpmrcForDir` result. Returns
76
+ * `undefined` (→ `registry_host=unknown`) when no configured row was in
77
+ * effect, including the `unreachable` and `not-configured` cases — those
78
+ * are deliberately reported as `unknown` rather than guessed at public
79
+ * npm, so the dashboard signal "did private-registry routing actually
80
+ * fire?" stays honest.
81
+ */
82
+ function registryUrlFromFetchResult(result) {
83
+ if (!result)
84
+ return undefined;
85
+ if (!result.config.configured)
86
+ return undefined;
87
+ return result.config.default?.url;
88
+ }
50
89
  const DEFAULT_COPY_DIRECTORY_EXCLUDES = new Set([
51
90
  ".git",
52
91
  ".next",
@@ -135,12 +174,22 @@ let AppShell = (() => {
135
174
  virtualFs;
136
175
  pathValidator;
137
176
  enableDeletingLockfiles;
177
+ /**
178
+ * Resolves the per-org npm registry config at install time. The AiService
179
+ * always constructs one in production (SaaS, local dev, and ephemeral
180
+ * pods); the LD flag (`superblocks.npm-registry.enabled`) inside the
181
+ * client decides whether to short-circuit to "not-configured" without a
182
+ * server call. `undefined` here is the test-only path (or callers that
183
+ * deliberately opt out): every `.npmrc` helper becomes a no-op and the
184
+ * install runs against npm's default resolution.
185
+ */
186
+ npmRegistryClient;
138
187
  /**
139
188
  * Tracks files that must be read before they can be written.
140
189
  * Used to enforce read-before-write pattern for AI safety.
141
190
  */
142
191
  requiredReads = new Set();
143
- constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, }) {
192
+ constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, npmRegistryClient, }) {
144
193
  // Normalize path to handle Windows drive letter case
145
194
  this.appRootDirPath = normalizePath(appRootDirPath);
146
195
  this.fsOperationQueue = fsOperationQueue;
@@ -153,6 +202,7 @@ let AppShell = (() => {
153
202
  this.virtualFs = virtualFileSystem;
154
203
  this.pathValidator = pathValidator;
155
204
  this.enableDeletingLockfiles = enableDeletingLockfiles ?? false;
205
+ this.npmRegistryClient = npmRegistryClient;
156
206
  }
157
207
  /**
158
208
  * Toggle the API virtual-file projection. When disabled, reads to `apis/*`
@@ -276,182 +326,565 @@ let AppShell = (() => {
276
326
  warmLinterCache() {
277
327
  void this.linter.warmCache(["**/*.{tsx}"]);
278
328
  }
329
+ /**
330
+ * Emit one `superblocks.npm.install.*` observation (APPS-4189 / P6.2) and
331
+ * annotate the active llmobs span with the same dimensions. Shared by
332
+ * every `AppShell.*install*` method so metric and span streams join on
333
+ * (runner, registry_host, outcome) without drift. Every value flows
334
+ * through the P6.1 sanitization helpers inside `recordNpmInstall`, so a
335
+ * raw registry hostname or token cannot reach an exported attribute even
336
+ * if an upstream caller forgot to scrub.
337
+ */
338
+ observeNpmInstall(runner, registryFetchResult, outcome, durationMs) {
339
+ const tags = recordNpmInstall({
340
+ runner,
341
+ registryHost: registryUrlFromFetchResult(registryFetchResult),
342
+ outcome,
343
+ durationMs,
344
+ });
345
+ llmobs.annotate({ tags });
346
+ }
347
+ /**
348
+ * Env overlay for AppShell-spawned npm/pnpm installs in the live-edit
349
+ * pod. Routes npm's per-run debug log into `<app>/.superblocks/logs` via
350
+ * `NPM_CONFIG_LOGS_DIR` so a failed agent install leaves a collectable log
351
+ * in a predictable place — the same convention the CLI dev-server install
352
+ * uses (`buildInstallEnv` in the SDK). These execs otherwise inherit
353
+ * `process.env` implicitly, so we spread it before adding the override.
354
+ * pnpm has no equivalent logs-dir config and ignores the var (npm-only).
355
+ */
356
+ installLogEnv() {
357
+ return {
358
+ ...process.env,
359
+ NPM_CONFIG_LOGS_DIR: path.join(this.appRootDirPath, ".superblocks", "logs"),
360
+ };
361
+ }
279
362
  async runNpmInstall(abortSignal) {
280
- // Materialize `.npmrc` from the SUPERBLOCKS_NPM_REGISTRY env contract AND
281
- // strip `resolved` URLs from any baked-in lockfile so npm re-resolves
282
- // through the active private registry (brownfield cutover). Serialized
283
- // with other queued FS ops so two concurrent installs can't interleave
284
- // the preserve-scope read-modify-write or rewrite the lockfile mid-read.
285
- await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, {
286
- preserveScopeLines: PRESERVE_NPMRC_SCOPES,
287
- }));
288
- return await new Promise((resolve, reject) => {
289
- const command =
290
- // if we're running in csb mock server, we need to avoid conflicts with the workspace
291
- process.env.SUPERBLOCKS_SERVER_ENV === "local"
292
- ? "pnpm install --config.confirmModulesPurge=false"
293
- : "npm install";
294
- exec(command, {
295
- cwd: this.appRootDirPath,
296
- timeout: 30_000,
297
- signal: abortSignal,
298
- }, (error, _stdout, stderr) => {
299
- if (error) {
300
- return reject(new NpmInstallError(stderr));
301
- }
302
- return resolve();
363
+ const installStart = Date.now();
364
+ let outcome = "success";
365
+ let capturedFetchResult;
366
+ try {
367
+ // `withAuthFailedRetry` owns the `.npmrc` materialization (via
368
+ // `prepareForPrivateRegistry`) and the one-shot auth_failed retry —
369
+ // see its docstring. Telemetry is captured around it so a retry-then-
370
+ // succeed flow still records the right outcome.
371
+ await this.withAuthFailedRetry(async (registryFetchResult) => {
372
+ capturedFetchResult = registryFetchResult;
373
+ // APPS-4370: refuse to spawn npm when the registry server is
374
+ // unreachable AND we have prior evidence (disk marker) that this org
375
+ // was at some point configured with a private registry. See
376
+ // `maybeShortCircuitForUnreachable` for the full rationale.
377
+ this.maybeShortCircuitForUnreachable(registryFetchResult, []);
378
+ const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
379
+ // In csb mock server (local dev) we use pnpm to avoid lockfile / store
380
+ // conflicts with the surrounding workspace; everywhere else we force npm
381
+ // so the `--json` error contract and therefore the registry-blocked
382
+ // advice matrix — applies on this post-processing path too. When the
383
+ // org has opted out via `allow_install_scripts=false`, append
384
+ // `--ignore-scripts` (the flag is honored by both npm and pnpm).
385
+ const isNpm = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
386
+ const baseCommand = isNpm
387
+ ? "npm install --fund=false --audit=false --json"
388
+ : "pnpm install --config.confirmModulesPurge=false";
389
+ const command = ignoreScripts
390
+ ? `${baseCommand} --ignore-scripts`
391
+ : baseCommand;
392
+ return await new Promise((resolve, reject) => {
393
+ exec(command, {
394
+ cwd: this.appRootDirPath,
395
+ env: this.installLogEnv(),
396
+ timeout: 30_000,
397
+ signal: abortSignal,
398
+ // See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
399
+ // default 1 MiB exec buffer on fat dep trees.
400
+ maxBuffer: NPM_JSON_MAX_BUFFER,
401
+ }, (error, stdout, stderr) => {
402
+ if (error) {
403
+ // Route the npm path through the same classifier as the agent-tool
404
+ // install entry points so E404 / E401 / ENOTFOUND surface as a
405
+ // `NpmInstallBlocked` (with row-aware advice) instead of a raw
406
+ // `NpmInstallError(stderr)`. The pnpm path doesn't emit the
407
+ // `--json` envelope, so the classifier falls through to the
408
+ // legacy error for it — registry-blocked scenarios only matter
409
+ // in cloud-edit (which is the npm branch).
410
+ if (isNpm) {
411
+ return reject(this.classifyNpmExecError({ stdout, stderr, message: error.message }, undefined, registryFetchResult));
412
+ }
413
+ return reject(new NpmInstallError(stderr));
414
+ }
415
+ return resolve();
416
+ });
417
+ });
303
418
  });
419
+ }
420
+ catch (err) {
421
+ outcome = outcomeFromInstallError(err);
422
+ throw err;
423
+ }
424
+ finally {
425
+ this.observeNpmInstall("install_all", capturedFetchResult, outcome, Date.now() - installStart);
426
+ }
427
+ }
428
+ /**
429
+ * APPS-4370. If the `prepareForPrivateRegistry` result tells us the
430
+ * server is unreachable AND the per-org disk marker says this org has
431
+ * at some prior point successfully resolved a configured registry,
432
+ * throw `NpmInstallBlocked(registry_unreachable)` BEFORE spawning npm.
433
+ *
434
+ * Without this short-circuit the install spawns anyway and hits one of
435
+ * two failure modes:
436
+ * 1. A previous install left a managed `.npmrc` with rotated credentials
437
+ * → npm 401s mid-deploy and `classifyNpmExecError` tags it
438
+ * `registry_auth_failed`, pointing the user at the wrong remediation.
439
+ * 2. No managed `.npmrc` exists → install silently resolves through
440
+ * public npm, defeating the privacy posture during the outage.
441
+ *
442
+ * Bypassed when:
443
+ * - The fetch result is anything other than `unreachable` (the
444
+ * existing code paths handle `configured` / `stale` / `not-configured`).
445
+ * - `everConfigured` is `undefined` (no store wired in — older call
446
+ * sites, test fixtures) or `false` (greenfield org / first install).
447
+ *
448
+ * `packages` flows onto the structured error so the agent loop can
449
+ * surface "we tried to install <these> and the registry was unreachable".
450
+ */
451
+ maybeShortCircuitForUnreachable(registryFetchResult, packages) {
452
+ if (!registryFetchResult || registryFetchResult.source !== "unreachable") {
453
+ return;
454
+ }
455
+ if (registryFetchResult.everConfigured !== true) {
456
+ return;
457
+ }
458
+ throw new NpmInstallBlocked({
459
+ reason: "registry_unreachable",
460
+ packages,
461
+ hasAnyRegistryConfigured: true,
304
462
  });
305
463
  }
306
- async runPackageManagerInstall(abortSignal) {
307
- // See `runNpmInstall` for serialization + brownfield-lockfile rationale.
308
- await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, {
309
- preserveScopeLines: PRESERVE_NPMRC_SCOPES,
310
- }));
311
- // In cloud edit mode, always use npm because pnpm is not available in the container.
312
- // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
313
- // from local editing, but we must use npm in cloud mode.
314
- const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
315
- let packageManagerAgent;
316
- if (isCloudMode) {
317
- // Force npm in cloud mode
318
- packageManagerAgent = "npm";
464
+ /**
465
+ * Derive the `hasAnyRegistryConfigured` tri-state from the
466
+ * `NpmRegistryFetchResult` that the immediately-preceding
467
+ * `prepareForPrivateRegistry` call returned.
468
+ *
469
+ * - `true` — `configured` or `stale`. At least one row is known
470
+ * to exist (fresh fetch or last-known-good cache).
471
+ * - `false` — `not-configured`. The kill switch is off OR the
472
+ * server returned an empty list. A deliberate "no rows"
473
+ * observation.
474
+ * - `undefined` — `prepareForPrivateRegistry` returned `undefined` (no
475
+ * client wired in) OR the client resolved to `unreachable`
476
+ * (server down with no usable cache). Both mean "we don't
477
+ * know"; `buildBlockedMessage` falls back to the default
478
+ * variant rather than asserting a row count.
479
+ *
480
+ * Kept narrow + private so the signal stays a single derivation site rather
481
+ * than something every catch handler reimplements.
482
+ */
483
+ static deriveHasAnyRegistryConfigured(result) {
484
+ if (!result)
485
+ return undefined;
486
+ switch (result.source) {
487
+ case "configured":
488
+ case "stale":
489
+ return true;
490
+ case "not-configured":
491
+ return false;
492
+ case "unreachable":
493
+ return undefined;
319
494
  }
320
- else {
321
- // NOTE: this largely duplicates the logic in the CLI
322
- const packageManager = await detect({
323
- strategies: [
324
- "packageManager-field",
325
- "lockfile",
326
- "install-metadata",
327
- "devEngines-field",
328
- ],
329
- cwd: this.appRootDirPath,
495
+ }
496
+ /**
497
+ * Convert an exec failure from `npm --json` into a structured error.
498
+ *
499
+ * - When the stdout JSON matches a known registry-blocked condition (E404,
500
+ * ENOTFOUND, E401, ...) we throw `NpmInstallBlocked` so the tool layer
501
+ * can surface it to the agent as a structured result.
502
+ * - Otherwise we fall back to the legacy `NpmInstallError(stderr)` so any
503
+ * pre-existing handler (e.g. agent-planning's POST_PROCESSING_ERRORED
504
+ * branch) behaves exactly as it did before.
505
+ *
506
+ * Only used on the npm code path; pnpm continues to throw `NpmInstallError`
507
+ * directly because pnpm's `--json` error contract differs and the
508
+ * registry-blocked scenarios only matter in cloud-edit (which forces npm).
509
+ *
510
+ * `registryFetchResult` carries the most recent
511
+ * `prepareForPrivateRegistry` result so the structured error can encode
512
+ * whether the org had any configured registry rows at the moment of
513
+ * failure (used by `buildBlockedMessage` to choose between default and
514
+ * row-aware advice variants). The active llmobs span is annotated with the
515
+ * same data points so a downstream `npm.install_blocked.*` facet can
516
+ * correlate reason, host, and row presence.
517
+ */
518
+ classifyNpmExecError(error, requestedPackages, registryFetchResult) {
519
+ const stdout = error.stdout ?? "";
520
+ const hasAnyRegistryConfigured = AppShell.deriveHasAnyRegistryConfigured(registryFetchResult);
521
+ const blocked = parseNpmJsonError(stdout, {
522
+ requestedPackages,
523
+ hasAnyRegistryConfigured,
524
+ });
525
+ if (blocked) {
526
+ // Span tags for `superblocks.npm.install_blocked.*` faceting. Stringified
527
+ // because llmobs tag values are strings; `unknown` is used in lieu of
528
+ // `null`/`undefined` so the tag's cardinality stays bounded and a
529
+ // "field absent" datapoint is still queryable.
530
+ //
531
+ // `npm.install_blocked.registry_host` is routed through the P6.1
532
+ // `bucketNpmRegistryHost` sanitizer (same helper used by
533
+ // `recordNpmInstall`) so the tag value is a bounded enum
534
+ // (`public_npm` / `private` / `unknown`). `NpmInstallBlocked` only
535
+ // scrubs credentials and control characters — without the bucketer the
536
+ // raw customer registry hostname would land in llmobs tags, leaking
537
+ // customer-internal hosts and giving the tag unbounded cardinality.
538
+ llmobs.annotate({
539
+ tags: {
540
+ "npm.install_blocked.reason": blocked.reason,
541
+ "npm.install_blocked.registry_host": bucketNpmRegistryHost(blocked.registryHost),
542
+ "npm.install_blocked.npm_error_code": blocked.npmErrorCode ?? "unknown",
543
+ "npm.install_blocked.has_any_registry_configured": hasAnyRegistryConfigured === undefined
544
+ ? "unknown"
545
+ : String(hasAnyRegistryConfigured),
546
+ },
330
547
  });
331
- if (!packageManager) {
332
- throw new Error("Could not detect package manager");
333
- }
334
- packageManagerAgent = packageManager.agent;
548
+ return blocked;
335
549
  }
336
- const installCommand = resolveCommand(packageManagerAgent, "install", packageManagerAgent === "npm" ? ["--fund=false", "--audit=false"] : []);
337
- if (!installCommand) {
338
- throw new Error(`Could not determine install command for ${packageManagerAgent}, skipping package installation`);
550
+ // Non-registry-blocked failure (ERESOLVE, EPEERINVALID, ETARGET, ).
551
+ // Prefer the JSON diagnostic from stdout because under `--json` stderr
552
+ // is sparse preserving the diagnostic keeps agent-planning's
553
+ // POST_PROCESSING_ERRORED debug message useful instead of collapsing
554
+ // it to a generic exec-failure string.
555
+ const diagnostic = parseNpmJsonDiagnostic(stdout);
556
+ if (stdout && !diagnostic) {
557
+ // npm was invoked with `--json` but neither parser could extract
558
+ // anything from stdout. This silently bypasses the row-aware advice
559
+ // matrix, the structured warn log, and the llmobs span tags above —
560
+ // converting the silent fallback into a queryable signal so operators
561
+ // can find these (npm crash mid-output, stdout truncation, schema
562
+ // drift across npm versions, an interactive prompt sneaking past
563
+ // `--json`). Single line, debug-grade payload (truncated preview
564
+ // only) to keep log volume bounded.
565
+ //
566
+ // Scrub BEFORE slicing: raw npm `--json` stdout can contain registry
567
+ // URLs with userinfo or `_authToken=` lines from a leaked `.npmrc`,
568
+ // and slicing first could split a credential across the 200-char
569
+ // boundary so the regex no longer matches.
570
+ getLogger().warn("[npm] failed to extract structured error from `npm --json` stdout; falling back to legacy NpmInstallError", { stdoutPreview: scrubSecrets(stdout).slice(0, 200) });
339
571
  }
340
- const { command, args } = installCommand;
572
+ return new NpmInstallError(diagnostic || error.stderr || error.message || "");
573
+ }
574
+ /**
575
+ * Wraps an install attempt so that a `registry_auth_failed` failure
576
+ * triggers one cache-invalidation + re-fetch + retry cycle before
577
+ * throwing. The helper owns the `.npmrc` materialisation so the retry
578
+ * picks up a freshly-fetched token without duplicating the prep call
579
+ * in every install method.
580
+ *
581
+ * Guard: only one retry per call — a second `registry_auth_failed`
582
+ * propagates unchanged.
583
+ */
584
+ async withAuthFailedRetry(attempt) {
585
+ const registryFetchResult = await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, { preserveScopeLines: PRESERVE_NPMRC_SCOPES }, this.npmRegistryClient));
341
586
  try {
342
- const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
343
- cwd: this.appRootDirPath,
344
- signal: abortSignal,
345
- });
346
- return stdout;
587
+ return await attempt(registryFetchResult);
347
588
  }
348
589
  catch (error) {
349
- throw new NpmInstallError(error.stderr || error.message);
590
+ if (error instanceof NpmInstallBlocked &&
591
+ error.reason === "registry_auth_failed" &&
592
+ this.npmRegistryClient) {
593
+ this.npmRegistryClient.invalidateCache();
594
+ getLogger().info("[npm-registry] invalidated cache after registry_auth_failed; retrying install once");
595
+ llmobs.annotate({
596
+ tags: {
597
+ "npm.registry.cache_invalidated_on_auth_failed": "true",
598
+ },
599
+ });
600
+ const retryFetchResult = await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, { preserveScopeLines: PRESERVE_NPMRC_SCOPES }, this.npmRegistryClient));
601
+ return await attempt(retryFetchResult);
602
+ }
603
+ throw error;
604
+ }
605
+ }
606
+ async runPackageManagerInstall(abortSignal) {
607
+ const installStart = Date.now();
608
+ let outcome = "success";
609
+ let capturedFetchResult;
610
+ try {
611
+ return await this.withAuthFailedRetry(async (registryFetchResult) => {
612
+ capturedFetchResult = registryFetchResult;
613
+ // APPS-4370: short-circuit before any exec when server is unreachable
614
+ // AND this org's marker says it was previously configured.
615
+ this.maybeShortCircuitForUnreachable(registryFetchResult, []);
616
+ const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
617
+ // In cloud edit mode, always use npm because pnpm is not available in the container.
618
+ // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
619
+ // from local editing, but we must use npm in cloud mode.
620
+ const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
621
+ let packageManagerAgent;
622
+ if (isCloudMode) {
623
+ // Force npm in cloud mode
624
+ packageManagerAgent = "npm";
625
+ }
626
+ else {
627
+ // NOTE: this largely duplicates the logic in the CLI
628
+ const packageManager = await detect({
629
+ strategies: [
630
+ "packageManager-field",
631
+ "lockfile",
632
+ "install-metadata",
633
+ "devEngines-field",
634
+ ],
635
+ cwd: this.appRootDirPath,
636
+ });
637
+ if (!packageManager) {
638
+ throw new Error("Could not detect package manager");
639
+ }
640
+ packageManagerAgent = packageManager.agent;
641
+ }
642
+ const isNpm = packageManagerAgent === "npm";
643
+ const baseInstallArgs = isNpm
644
+ ? ["--fund=false", "--audit=false", "--json"]
645
+ : [];
646
+ const installArgs = ignoreScripts
647
+ ? [...baseInstallArgs, "--ignore-scripts"]
648
+ : baseInstallArgs;
649
+ const installCommand = resolveCommand(packageManagerAgent, "install", installArgs);
650
+ if (!installCommand) {
651
+ throw new Error(`Could not determine install command for ${packageManagerAgent}, skipping package installation`);
652
+ }
653
+ const { command, args } = installCommand;
654
+ try {
655
+ const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
656
+ cwd: this.appRootDirPath,
657
+ env: this.installLogEnv(),
658
+ signal: abortSignal,
659
+ // See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
660
+ // default 1 MiB exec buffer on fat dep trees.
661
+ maxBuffer: NPM_JSON_MAX_BUFFER,
662
+ });
663
+ // On the npm path stdout is a JSON blob; translate to a compact
664
+ // human-readable summary for the agent, falling back to raw stdout
665
+ // when the JSON shape drifts.
666
+ if (isNpm) {
667
+ return parseNpmJsonSuccess(stdout) ?? stdout;
668
+ }
669
+ return stdout;
670
+ }
671
+ catch (error) {
672
+ if (isNpm)
673
+ throw this.classifyNpmExecError(error, undefined, registryFetchResult);
674
+ throw new NpmInstallError(error.stderr || error.message);
675
+ }
676
+ });
677
+ }
678
+ catch (err) {
679
+ outcome = outcomeFromInstallError(err);
680
+ throw err;
681
+ }
682
+ finally {
683
+ this.observeNpmInstall("install_all", capturedFetchResult, outcome, Date.now() - installStart);
350
684
  }
351
685
  }
352
686
  async installSpecificPackages(packages, abortSignal) {
353
687
  if (!packages || packages.length === 0) {
354
688
  throw new Error("No packages specified for installation");
355
689
  }
356
- // See `runNpmInstall` for serialization + brownfield-lockfile rationale.
357
- await this.enqueueFsOperation("maybeWriteNpmrc", () => prepareForPrivateRegistry(this.appRootDirPath, {
358
- preserveScopeLines: PRESERVE_NPMRC_SCOPES,
359
- }));
360
- // In cloud edit mode, always use npm because pnpm is not available in the container.
361
- // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
362
- // from local editing, but we must use npm in cloud mode.
363
- const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
364
- let packageManagerAgent;
365
- if (isCloudMode) {
366
- // Force npm in cloud mode
367
- packageManagerAgent = "npm";
368
- }
369
- else {
370
- const packageManager = await detect({
371
- strategies: [
372
- "packageManager-field",
373
- "lockfile",
374
- "install-metadata",
375
- "devEngines-field",
376
- ],
377
- cwd: this.appRootDirPath,
690
+ const installStart = Date.now();
691
+ let outcome = "success";
692
+ let capturedFetchResult;
693
+ try {
694
+ return await this.withAuthFailedRetry(async (registryFetchResult) => {
695
+ capturedFetchResult = registryFetchResult;
696
+ // APPS-4370: short-circuit before exec when unreachable + everConfigured.
697
+ // Pass through the requested package specs so the structured error
698
+ // surfaces them to the agent ("we tried to install lodash and the
699
+ // private registry was unreachable").
700
+ this.maybeShortCircuitForUnreachable(registryFetchResult, packages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name));
701
+ const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
702
+ // In cloud edit mode, always use npm because pnpm is not available in the container.
703
+ // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
704
+ // from local editing, but we must use npm in cloud mode.
705
+ const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
706
+ let packageManagerAgent;
707
+ if (isCloudMode) {
708
+ // Force npm in cloud mode
709
+ packageManagerAgent = "npm";
710
+ }
711
+ else {
712
+ const packageManager = await detect({
713
+ strategies: [
714
+ "packageManager-field",
715
+ "lockfile",
716
+ "install-metadata",
717
+ "devEngines-field",
718
+ ],
719
+ cwd: this.appRootDirPath,
720
+ });
721
+ if (!packageManager) {
722
+ throw new Error("Could not detect package manager");
723
+ }
724
+ packageManagerAgent = packageManager.agent;
725
+ }
726
+ const isNpm = packageManagerAgent === "npm";
727
+ // Separate regular and dev dependencies
728
+ const regularPackages = packages.filter((pkg) => !pkg.dev);
729
+ const devPackages = packages.filter((pkg) => pkg.dev);
730
+ const outputs = [];
731
+ // Install regular dependencies
732
+ if (regularPackages.length > 0) {
733
+ const packageSpecs = regularPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
734
+ const baseArgs = isNpm
735
+ ? ["--fund=false", "--audit=false", "--json", ...packageSpecs]
736
+ : packageSpecs;
737
+ const addCommand = resolveCommand(packageManagerAgent, "add", ignoreScripts ? ["--ignore-scripts", ...baseArgs] : baseArgs);
738
+ if (!addCommand) {
739
+ throw new Error(`Could not determine add command for ${packageManagerAgent}`);
740
+ }
741
+ const { command, args } = addCommand;
742
+ try {
743
+ const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
744
+ cwd: this.appRootDirPath,
745
+ env: this.installLogEnv(),
746
+ signal: abortSignal,
747
+ // See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
748
+ // default 1 MiB exec buffer on fat dep trees.
749
+ maxBuffer: NPM_JSON_MAX_BUFFER,
750
+ });
751
+ outputs.push(isNpm ? (parseNpmJsonSuccess(stdout) ?? stdout) : stdout);
752
+ }
753
+ catch (error) {
754
+ if (isNpm)
755
+ throw this.classifyNpmExecError(error, regularPackages, registryFetchResult);
756
+ throw new NpmInstallError(error.stderr || error.message);
757
+ }
758
+ }
759
+ // Install dev dependencies
760
+ if (devPackages.length > 0) {
761
+ const packageSpecs = devPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
762
+ const baseArgs = isNpm
763
+ ? [
764
+ "--save-dev",
765
+ "--fund=false",
766
+ "--audit=false",
767
+ "--json",
768
+ ...packageSpecs,
769
+ ]
770
+ : ["-D", ...packageSpecs];
771
+ const addCommand = resolveCommand(packageManagerAgent, "add", ignoreScripts ? ["--ignore-scripts", ...baseArgs] : baseArgs);
772
+ if (!addCommand) {
773
+ throw new Error(`Could not determine add command for ${packageManagerAgent}`);
774
+ }
775
+ const { command, args } = addCommand;
776
+ try {
777
+ const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
778
+ cwd: this.appRootDirPath,
779
+ env: this.installLogEnv(),
780
+ signal: abortSignal,
781
+ // See `NPM_JSON_MAX_BUFFER` — `--json` summary can outgrow the
782
+ // default 1 MiB exec buffer on fat dep trees.
783
+ maxBuffer: NPM_JSON_MAX_BUFFER,
784
+ });
785
+ outputs.push(isNpm ? (parseNpmJsonSuccess(stdout) ?? stdout) : stdout);
786
+ }
787
+ catch (error) {
788
+ if (isNpm)
789
+ throw this.classifyNpmExecError(error, devPackages, registryFetchResult);
790
+ throw new NpmInstallError(error.stderr || error.message);
791
+ }
792
+ }
793
+ return outputs.join("\n");
378
794
  });
379
- if (!packageManager) {
380
- throw new Error("Could not detect package manager");
381
- }
382
- packageManagerAgent = packageManager.agent;
383
- }
384
- // Separate regular and dev dependencies
385
- const regularPackages = packages.filter((pkg) => !pkg.dev);
386
- const devPackages = packages.filter((pkg) => pkg.dev);
387
- const outputs = [];
388
- // Install regular dependencies
389
- if (regularPackages.length > 0) {
390
- const packageSpecs = regularPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
391
- const addCommand = resolveCommand(packageManagerAgent, "add", packageManagerAgent === "npm"
392
- ? ["--fund=false", "--audit=false", ...packageSpecs]
393
- : packageSpecs);
394
- if (!addCommand) {
395
- throw new Error(`Could not determine add command for ${packageManagerAgent}`);
396
- }
397
- const { command, args } = addCommand;
398
- try {
399
- const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
400
- cwd: this.appRootDirPath,
401
- signal: abortSignal,
402
- });
403
- outputs.push(stdout);
404
- }
405
- catch (error) {
406
- throw new NpmInstallError(error.stderr || error.message);
407
- }
408
795
  }
409
- // Install dev dependencies
410
- if (devPackages.length > 0) {
411
- const packageSpecs = devPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
412
- const addCommand = resolveCommand(packageManagerAgent, "add", packageManagerAgent === "npm"
413
- ? ["--save-dev", "--fund=false", "--audit=false", ...packageSpecs]
414
- : ["-D", ...packageSpecs]);
415
- if (!addCommand) {
416
- throw new Error(`Could not determine add command for ${packageManagerAgent}`);
417
- }
418
- const { command, args } = addCommand;
419
- try {
420
- const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
421
- cwd: this.appRootDirPath,
422
- signal: abortSignal,
423
- });
424
- outputs.push(stdout);
425
- }
426
- catch (error) {
427
- throw new NpmInstallError(error.stderr || error.message);
428
- }
796
+ catch (err) {
797
+ outcome = outcomeFromInstallError(err);
798
+ throw err;
799
+ }
800
+ finally {
801
+ this.observeNpmInstall("install_specific", capturedFetchResult, outcome, Date.now() - installStart);
429
802
  }
430
- return outputs.join("\n");
431
803
  }
432
804
  async uninstallPackages(packages, options) {
433
- // Uninstalls hit the same registry resolution rules as installs (e.g.
434
- // dependency tree solver may re-fetch metadata), so refresh `.npmrc`
435
- // first. Without this, an uninstall against a deployment whose
436
- // private-registry token has been rotated would 401 against the stale
437
- // `_authToken=` line. Same serialization rationale as `runNpmInstall`.
438
- await this.enqueueFsOperation("maybeWriteNpmrc", () => maybeWriteNpmrcForDir(this.appRootDirPath, {
439
- preserveScopeLines: PRESERVE_NPMRC_SCOPES,
440
- }));
441
- return await new Promise((resolve, reject) => {
442
- // Use pnpm for local env, npm otherwise (same pattern as runNpmInstall)
443
- const packageManager = process.env.SUPERBLOCKS_SERVER_ENV === "local" ? "pnpm" : "npm";
444
- const uninstallCmd = packageManager === "pnpm" ? "remove" : "uninstall";
445
- const command = `${packageManager} ${uninstallCmd} ${packages.join(" ")}`;
446
- exec(command, {
447
- cwd: this.appRootDirPath,
448
- timeout: 30_000,
449
- signal: options?.abortSignal,
450
- }, (error, stdout, stderr) => {
451
- if (error) {
452
- return reject(new NpmInstallError(stderr || error.message));
453
- }
454
- return resolve({ stdout, stderr });
805
+ // Core-package guard (APPS-4527): refuse to uninstall packages the
806
+ // platform itself depends on (`@superblocksteam/*`, react, react-router,
807
+ // vite, …). Removing one breaks the dev server, the bundle builder, or
808
+ // the app runtime no caller has a legitimate reason. Checked before
809
+ // any registry work so the refusal is deterministic and offline.
810
+ const protectedRequested = packages.filter((p) => isProtectedCorePackage(p));
811
+ if (protectedRequested.length > 0) {
812
+ throw new Error(`Refusing to uninstall protected core package(s): ${protectedRequested.join(", ")}. ` +
813
+ "These are required by the Superblocks platform (dev server, build pipeline, or app runtime) and must remain in the app.");
814
+ }
815
+ // Uninstalls hit the same registry resolution rules as installs (the
816
+ // dependency-tree solver re-reifies the remaining tree and can re-fetch
817
+ // metadata), so they run through the SAME safety wrapper as every
818
+ // install path rather than a bespoke one-shot exec:
819
+ //
820
+ // - `withAuthFailedRetry` owns the `.npmrc` materialization (via
821
+ // `prepareForPrivateRegistry`, same serialization as `runNpmInstall`)
822
+ // AND retries once on `registry_auth_failed` after invalidating the
823
+ // registry cache, so an uninstall against a deployment whose
824
+ // private-registry token was rotated mid-session recovers instead of
825
+ // hard-failing on the stale `_authToken=` line.
826
+ // - `maybeShortCircuitForUnreachable` (APPS-4370) refuses to spawn when
827
+ // the config server is unreachable AND this org was previously
828
+ // configured. Without it the uninstall escaped the fail-closed gate,
829
+ // 401'd against the stale token, and surfaced `registry_auth_failed`
830
+ // (rotate-credentials advice) instead of `registry_unreachable`
831
+ // (registry-down advice) — the wrong remediation.
832
+ // - The npm path runs with `--json` and routes failures through
833
+ // `classifyNpmExecError`, identical to `installSpecificPackages`, so
834
+ // E404 / E401 / ENOTFOUND surface as a structured `NpmInstallBlocked`
835
+ // (and the auth-failed case feeds the retry above).
836
+ //
837
+ // `shouldIgnoreInstallScripts` honours the org's `allowInstallScripts`
838
+ // policy on uninstall too — `preuninstall` / `postuninstall` lifecycle
839
+ // scripts are just as dangerous as install-time ones.
840
+ return await this.withAuthFailedRetry(async (registryFetchResult) => {
841
+ this.maybeShortCircuitForUnreachable(registryFetchResult, packages);
842
+ const ignoreScripts = shouldIgnoreInstallScripts(registryFetchResult);
843
+ // pnpm in local dev (avoids store/lockfile conflicts with the
844
+ // surrounding workspace); npm everywhere else so the `--json` error
845
+ // contract — and therefore the registry-blocked advice matrix —
846
+ // applies. Mirrors `runNpmInstall` / `installSpecificPackages`.
847
+ const isNpm = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
848
+ const packageManager = isNpm ? "npm" : "pnpm";
849
+ const uninstallCmd = isNpm ? "uninstall" : "remove";
850
+ const jsonFlag = isNpm ? " --json" : "";
851
+ const baseCommand = `${packageManager} ${uninstallCmd} ${packages.join(" ")}${jsonFlag}`;
852
+ const command = ignoreScripts
853
+ ? `${baseCommand} --ignore-scripts`
854
+ : baseCommand;
855
+ return await new Promise((resolve, reject) => {
856
+ exec(command, {
857
+ cwd: this.appRootDirPath,
858
+ env: this.installLogEnv(),
859
+ timeout: 30_000,
860
+ signal: options?.abortSignal,
861
+ // `--json` summary can outgrow the default 1 MiB exec buffer.
862
+ maxBuffer: NPM_JSON_MAX_BUFFER,
863
+ }, (error, stdout, stderr) => {
864
+ if (error) {
865
+ // Same classifier as the install paths: a registry block
866
+ // (E404 / E401 / ENOTFOUND, ...) becomes a structured
867
+ // `NpmInstallBlocked` (with row-aware advice + the
868
+ // auth-failed retry signal consumed by `withAuthFailedRetry`);
869
+ // anything else falls back to the legacy `NpmInstallError`.
870
+ // pnpm doesn't emit the `--json` envelope, so it keeps the
871
+ // raw-stderr error.
872
+ if (isNpm) {
873
+ return reject(this.classifyNpmExecError({ stdout, stderr, message: error.message }, undefined, registryFetchResult));
874
+ }
875
+ return reject(new NpmInstallError(stderr || error.message));
876
+ }
877
+ // On the npm path stdout is a `--json` blob; collapse it to a
878
+ // compact human-readable summary (same as
879
+ // `installSpecificPackages`), falling back to raw stdout on
880
+ // shape drift.
881
+ return resolve({
882
+ stdout: isNpm
883
+ ? (parseNpmJsonSuccess(stdout) ?? stdout)
884
+ : stdout,
885
+ stderr,
886
+ });
887
+ });
455
888
  });
456
889
  });
457
890
  }