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