gsd-pi 2.70.0 → 2.70.1-dev.3e19108

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 (254) hide show
  1. package/dist/loader.js +4 -0
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +152 -2
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +33 -19
  4. package/dist/resources/extensions/gsd/auto-prompts.js +7 -3
  5. package/dist/resources/extensions/gsd/auto-start.js +28 -12
  6. package/dist/resources/extensions/gsd/auto.js +12 -8
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  8. package/dist/resources/extensions/gsd/commands-handlers.js +22 -8
  9. package/dist/resources/extensions/gsd/doctor-engine-checks.js +12 -0
  10. package/dist/resources/extensions/gsd/doctor-format.js +2 -0
  11. package/dist/resources/extensions/gsd/guided-flow.js +33 -20
  12. package/dist/resources/extensions/gsd/init-wizard.js +3 -11
  13. package/dist/resources/extensions/gsd/pre-execution-checks.js +5 -3
  14. package/dist/resources/extensions/gsd/prompts/discuss.md +31 -13
  15. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
  16. package/dist/resources/extensions/gsd/validate-directory.js +30 -12
  17. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
  18. package/dist/resources/extensions/gsd/workflow-mcp.js +12 -1
  19. package/dist/resources/extensions/slash-commands/audit.js +2 -1
  20. package/dist/resources/extensions/subagent/isolation.js +4 -2
  21. package/dist/update-check.d.ts +1 -0
  22. package/dist/update-check.js +30 -27
  23. package/dist/update-cmd.js +3 -11
  24. package/dist/web/standalone/.next/BUILD_ID +1 -1
  25. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  26. package/dist/web/standalone/.next/build-manifest.json +4 -4
  27. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  28. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  29. package/dist/web/standalone/.next/required-server-files.json +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  41. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  57. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  69. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  89. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  99. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  105. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  121. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +2 -2
  125. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  134. package/dist/web/standalone/.next/server/app/index.html +1 -1
  135. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  136. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  137. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  138. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  140. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  141. package/dist/web/standalone/.next/server/app/page.js +2 -2
  142. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  144. package/dist/web/standalone/.next/server/chunks/63.js +3 -3
  145. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  146. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  148. package/dist/web/standalone/.next/server/middleware.js +2 -2
  149. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  150. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  151. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  152. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  153. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  154. package/dist/web/standalone/.next/static/chunks/2826.dd3dc8bbd3025fa5.js +9 -0
  155. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  156. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  157. package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
  158. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  159. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  160. package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
  161. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  162. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  163. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  164. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  165. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  166. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  167. package/dist/web/standalone/server.js +1 -1
  168. package/dist/web-mode.js +4 -0
  169. package/package.json +11 -11
  170. package/packages/mcp-server/dist/env-writer.d.ts +39 -0
  171. package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
  172. package/packages/mcp-server/dist/env-writer.js +158 -0
  173. package/packages/mcp-server/dist/env-writer.js.map +1 -0
  174. package/packages/mcp-server/dist/server.d.ts +11 -2
  175. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  176. package/packages/mcp-server/dist/server.js +102 -2
  177. package/packages/mcp-server/dist/server.js.map +1 -1
  178. package/packages/mcp-server/dist/workflow-tools.d.ts +2 -0
  179. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  180. package/packages/mcp-server/dist/workflow-tools.js +35 -3
  181. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  182. package/packages/mcp-server/src/env-writer.test.ts +280 -0
  183. package/packages/mcp-server/src/env-writer.ts +183 -0
  184. package/packages/mcp-server/src/import-candidates.test.ts +48 -0
  185. package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
  186. package/packages/mcp-server/src/server.ts +137 -3
  187. package/packages/mcp-server/src/workflow-tools.ts +34 -1
  188. package/packages/pi-agent-core/dist/agent.d.ts +8 -0
  189. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  190. package/packages/pi-agent-core/dist/agent.js +3 -0
  191. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  192. package/packages/pi-agent-core/src/agent.test.ts +82 -0
  193. package/packages/pi-agent-core/src/agent.ts +12 -0
  194. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
  195. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
  196. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +133 -0
  197. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
  198. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +1 -0
  199. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  200. package/packages/pi-coding-agent/dist/core/lsp/config.js +38 -15
  201. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  202. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  203. package/packages/pi-coding-agent/dist/core/sdk.js +10 -0
  204. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  205. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  206. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +58 -21
  207. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  208. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  209. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +3 -1
  210. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  211. package/packages/pi-coding-agent/package.json +1 -1
  212. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +152 -0
  213. package/packages/pi-coding-agent/src/core/lsp/config.ts +43 -17
  214. package/packages/pi-coding-agent/src/core/sdk.ts +8 -0
  215. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +83 -27
  216. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +7 -5
  217. package/pkg/package.json +1 -1
  218. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +229 -2
  219. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +205 -0
  220. package/src/resources/extensions/gsd/auto-model-selection.ts +39 -25
  221. package/src/resources/extensions/gsd/auto-prompts.ts +7 -3
  222. package/src/resources/extensions/gsd/auto-start.ts +37 -14
  223. package/src/resources/extensions/gsd/auto.ts +12 -8
  224. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  225. package/src/resources/extensions/gsd/commands-handlers.ts +22 -7
  226. package/src/resources/extensions/gsd/doctor-engine-checks.ts +14 -0
  227. package/src/resources/extensions/gsd/doctor-format.ts +1 -0
  228. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  229. package/src/resources/extensions/gsd/guided-flow.ts +36 -17
  230. package/src/resources/extensions/gsd/init-wizard.ts +3 -13
  231. package/src/resources/extensions/gsd/pre-execution-checks.ts +6 -3
  232. package/src/resources/extensions/gsd/prompts/discuss.md +31 -13
  233. package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
  234. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +43 -0
  235. package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +207 -0
  236. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +48 -1
  237. package/src/resources/extensions/gsd/tests/resource-loader-import-path.test.ts +8 -7
  238. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +33 -1
  239. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +87 -1
  240. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
  241. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +180 -1
  242. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
  243. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
  244. package/src/resources/extensions/gsd/validate-directory.ts +33 -11
  245. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
  246. package/src/resources/extensions/gsd/workflow-mcp.ts +16 -1
  247. package/src/resources/extensions/slash-commands/audit.ts +2 -1
  248. package/src/resources/extensions/subagent/isolation.ts +4 -3
  249. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
  250. package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
  251. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  252. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  253. /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → cHCEWiRJM5bXJa9HkP1QU}/_buildManifest.js +0 -0
  254. /package/dist/web/standalone/.next/static/{Nl6lg7zP5dNgNBV1107v1 → cHCEWiRJM5bXJa9HkP1QU}/_ssgManifest.js +0 -0
@@ -0,0 +1,280 @@
1
+ // @gsd-build/mcp-server — Tests for env-writer utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { describe, it, afterEach } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ import {
11
+ checkExistingEnvKeys,
12
+ detectDestination,
13
+ writeEnvKey,
14
+ applySecrets,
15
+ isSafeEnvVarKey,
16
+ isSupportedDeploymentEnvironment,
17
+ shellEscapeSingle,
18
+ } from './env-writer.js';
19
+
20
+ function makeTempDir(prefix: string): string {
21
+ return mkdtempSync(join(tmpdir(), `${prefix}-`));
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // checkExistingEnvKeys
26
+ // ---------------------------------------------------------------------------
27
+
28
+ describe('checkExistingEnvKeys', () => {
29
+ it('finds key in .env file', async () => {
30
+ const tmp = makeTempDir('env-check');
31
+ try {
32
+ const envPath = join(tmp, '.env');
33
+ writeFileSync(envPath, 'API_KEY=secret123\nOTHER=val\n');
34
+ const result = await checkExistingEnvKeys(['API_KEY'], envPath);
35
+ assert.deepStrictEqual(result, ['API_KEY']);
36
+ } finally {
37
+ rmSync(tmp, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ it('finds key in process.env', async () => {
42
+ const tmp = makeTempDir('env-check');
43
+ const saved = process.env.GSD_MCP_TEST_KEY_1;
44
+ try {
45
+ process.env.GSD_MCP_TEST_KEY_1 = 'some-value';
46
+ const envPath = join(tmp, '.env');
47
+ const result = await checkExistingEnvKeys(['GSD_MCP_TEST_KEY_1'], envPath);
48
+ assert.deepStrictEqual(result, ['GSD_MCP_TEST_KEY_1']);
49
+ } finally {
50
+ delete process.env.GSD_MCP_TEST_KEY_1;
51
+ if (saved !== undefined) process.env.GSD_MCP_TEST_KEY_1 = saved;
52
+ rmSync(tmp, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ it('returns empty for missing keys', async () => {
57
+ const tmp = makeTempDir('env-check');
58
+ try {
59
+ const envPath = join(tmp, '.env');
60
+ writeFileSync(envPath, 'OTHER=val\n');
61
+ delete process.env.DEFINITELY_NOT_SET_MCP_XYZ;
62
+ const result = await checkExistingEnvKeys(['DEFINITELY_NOT_SET_MCP_XYZ'], envPath);
63
+ assert.deepStrictEqual(result, []);
64
+ } finally {
65
+ rmSync(tmp, { recursive: true, force: true });
66
+ }
67
+ });
68
+
69
+ it('handles missing .env file gracefully', async () => {
70
+ const tmp = makeTempDir('env-check');
71
+ try {
72
+ const envPath = join(tmp, 'nonexistent.env');
73
+ delete process.env.DEFINITELY_NOT_SET_MCP_XYZ;
74
+ const result = await checkExistingEnvKeys(['DEFINITELY_NOT_SET_MCP_XYZ'], envPath);
75
+ assert.deepStrictEqual(result, []);
76
+ } finally {
77
+ rmSync(tmp, { recursive: true, force: true });
78
+ }
79
+ });
80
+ });
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // detectDestination
84
+ // ---------------------------------------------------------------------------
85
+
86
+ describe('detectDestination', () => {
87
+ it('returns vercel when vercel.json exists', () => {
88
+ const tmp = makeTempDir('dest');
89
+ try {
90
+ writeFileSync(join(tmp, 'vercel.json'), '{}');
91
+ assert.equal(detectDestination(tmp), 'vercel');
92
+ } finally {
93
+ rmSync(tmp, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ it('returns convex when convex/ dir exists', () => {
98
+ const tmp = makeTempDir('dest');
99
+ try {
100
+ mkdirSync(join(tmp, 'convex'));
101
+ assert.equal(detectDestination(tmp), 'convex');
102
+ } finally {
103
+ rmSync(tmp, { recursive: true, force: true });
104
+ }
105
+ });
106
+
107
+ it('returns dotenv when neither exists', () => {
108
+ const tmp = makeTempDir('dest');
109
+ try {
110
+ assert.equal(detectDestination(tmp), 'dotenv');
111
+ } finally {
112
+ rmSync(tmp, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ it('vercel takes priority over convex', () => {
117
+ const tmp = makeTempDir('dest');
118
+ try {
119
+ writeFileSync(join(tmp, 'vercel.json'), '{}');
120
+ mkdirSync(join(tmp, 'convex'));
121
+ assert.equal(detectDestination(tmp), 'vercel');
122
+ } finally {
123
+ rmSync(tmp, { recursive: true, force: true });
124
+ }
125
+ });
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // writeEnvKey
130
+ // ---------------------------------------------------------------------------
131
+
132
+ describe('writeEnvKey', () => {
133
+ it('creates .env file with new key', async () => {
134
+ const tmp = makeTempDir('write');
135
+ try {
136
+ const envPath = join(tmp, '.env');
137
+ await writeEnvKey(envPath, 'NEW_KEY', 'new-value');
138
+ const content = readFileSync(envPath, 'utf8');
139
+ assert.ok(content.includes('NEW_KEY=new-value'));
140
+ } finally {
141
+ rmSync(tmp, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ it('updates existing key in-place', async () => {
146
+ const tmp = makeTempDir('write');
147
+ try {
148
+ const envPath = join(tmp, '.env');
149
+ writeFileSync(envPath, 'EXISTING=old\nOTHER=keep\n');
150
+ await writeEnvKey(envPath, 'EXISTING', 'new');
151
+ const content = readFileSync(envPath, 'utf8');
152
+ assert.ok(content.includes('EXISTING=new'));
153
+ assert.ok(content.includes('OTHER=keep'));
154
+ assert.ok(!content.includes('old'));
155
+ } finally {
156
+ rmSync(tmp, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ it('escapes newlines in values', async () => {
161
+ const tmp = makeTempDir('write');
162
+ try {
163
+ const envPath = join(tmp, '.env');
164
+ await writeEnvKey(envPath, 'MULTI', 'line1\nline2');
165
+ const content = readFileSync(envPath, 'utf8');
166
+ assert.ok(content.includes('MULTI=line1\\nline2'));
167
+ } finally {
168
+ rmSync(tmp, { recursive: true, force: true });
169
+ }
170
+ });
171
+
172
+ it('rejects non-string values', async () => {
173
+ const tmp = makeTempDir('write');
174
+ try {
175
+ const envPath = join(tmp, '.env');
176
+ await assert.rejects(
177
+ () => writeEnvKey(envPath, 'KEY', undefined as unknown as string),
178
+ /expects a string value/,
179
+ );
180
+ } finally {
181
+ rmSync(tmp, { recursive: true, force: true });
182
+ }
183
+ });
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // applySecrets (dotenv)
188
+ // ---------------------------------------------------------------------------
189
+
190
+ describe('applySecrets', () => {
191
+ const savedKeys: Record<string, string | undefined> = {};
192
+
193
+ afterEach(() => {
194
+ for (const [k, v] of Object.entries(savedKeys)) {
195
+ if (v === undefined) delete process.env[k];
196
+ else process.env[k] = v;
197
+ }
198
+ });
199
+
200
+ it('writes keys to .env and hydrates process.env', async () => {
201
+ const tmp = makeTempDir('apply');
202
+ const envPath = join(tmp, '.env');
203
+ savedKeys.GSD_APPLY_TEST_A = process.env.GSD_APPLY_TEST_A;
204
+ try {
205
+ const { applied, errors } = await applySecrets(
206
+ [{ key: 'GSD_APPLY_TEST_A', value: 'val-a' }],
207
+ 'dotenv',
208
+ { envFilePath: envPath },
209
+ );
210
+ assert.deepStrictEqual(applied, ['GSD_APPLY_TEST_A']);
211
+ assert.deepStrictEqual(errors, []);
212
+ assert.equal(process.env.GSD_APPLY_TEST_A, 'val-a');
213
+ const content = readFileSync(envPath, 'utf8');
214
+ assert.ok(content.includes('GSD_APPLY_TEST_A=val-a'));
215
+ } finally {
216
+ rmSync(tmp, { recursive: true, force: true });
217
+ }
218
+ });
219
+
220
+ it('returns errors for invalid vercel environment', async () => {
221
+ const tmp = makeTempDir('apply');
222
+ try {
223
+ const { applied, errors } = await applySecrets(
224
+ [{ key: 'KEY', value: 'val' }],
225
+ 'vercel',
226
+ {
227
+ envFilePath: join(tmp, '.env'),
228
+ environment: 'staging' as 'development',
229
+ execFn: async () => ({ code: 0, stderr: '' }),
230
+ },
231
+ );
232
+ assert.deepStrictEqual(applied, []);
233
+ assert.ok(errors[0]?.includes('unsupported'));
234
+ } finally {
235
+ rmSync(tmp, { recursive: true, force: true });
236
+ }
237
+ });
238
+ });
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Validation helpers
242
+ // ---------------------------------------------------------------------------
243
+
244
+ describe('isSafeEnvVarKey', () => {
245
+ it('accepts valid keys', () => {
246
+ assert.ok(isSafeEnvVarKey('API_KEY'));
247
+ assert.ok(isSafeEnvVarKey('_PRIVATE'));
248
+ assert.ok(isSafeEnvVarKey('key123'));
249
+ });
250
+
251
+ it('rejects invalid keys', () => {
252
+ assert.ok(!isSafeEnvVarKey('123BAD'));
253
+ assert.ok(!isSafeEnvVarKey('has-dash'));
254
+ assert.ok(!isSafeEnvVarKey('has space'));
255
+ assert.ok(!isSafeEnvVarKey(''));
256
+ });
257
+ });
258
+
259
+ describe('isSupportedDeploymentEnvironment', () => {
260
+ it('accepts valid environments', () => {
261
+ assert.ok(isSupportedDeploymentEnvironment('development'));
262
+ assert.ok(isSupportedDeploymentEnvironment('preview'));
263
+ assert.ok(isSupportedDeploymentEnvironment('production'));
264
+ });
265
+
266
+ it('rejects invalid environments', () => {
267
+ assert.ok(!isSupportedDeploymentEnvironment('staging'));
268
+ assert.ok(!isSupportedDeploymentEnvironment('test'));
269
+ });
270
+ });
271
+
272
+ describe('shellEscapeSingle', () => {
273
+ it('wraps in single quotes', () => {
274
+ assert.equal(shellEscapeSingle('hello'), "'hello'");
275
+ });
276
+
277
+ it('escapes embedded single quotes', () => {
278
+ assert.equal(shellEscapeSingle("it's"), "'it'\\''s'");
279
+ });
280
+ });
@@ -0,0 +1,183 @@
1
+ // @gsd-build/mcp-server — Environment variable write utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Shared helpers for writing env vars to .env files, detecting project
5
+ // destinations, and checking existing keys. Used by secure_env_collect
6
+ // MCP tool. No TUI dependencies — pure filesystem + process.env operations.
7
+
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+ import { existsSync, statSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // checkExistingEnvKeys
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Check which keys already exist in a .env file or process.env.
18
+ * Returns the subset of `keys` that are already set.
19
+ */
20
+ export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
21
+ let fileContent = "";
22
+ try {
23
+ fileContent = await readFile(envFilePath, "utf8");
24
+ } catch {
25
+ // ENOENT or other read error — proceed with empty content
26
+ }
27
+
28
+ const existing: string[] = [];
29
+ for (const key of keys) {
30
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
32
+ if (regex.test(fileContent) || key in process.env) {
33
+ existing.push(key);
34
+ }
35
+ }
36
+ return existing;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // detectDestination
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Detect the write destination based on project files in basePath.
45
+ * Priority: vercel.json → convex/ dir → fallback "dotenv".
46
+ */
47
+ export function detectDestination(basePath: string): "dotenv" | "vercel" | "convex" {
48
+ if (existsSync(resolve(basePath, "vercel.json"))) {
49
+ return "vercel";
50
+ }
51
+ const convexPath = resolve(basePath, "convex");
52
+ try {
53
+ if (existsSync(convexPath) && statSync(convexPath).isDirectory()) {
54
+ return "convex";
55
+ }
56
+ } catch {
57
+ // stat error — treat as not found
58
+ }
59
+ return "dotenv";
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // writeEnvKey
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Write a single key=value pair to a .env file.
68
+ * Updates existing keys in-place, appends new ones at the end.
69
+ */
70
+ export async function writeEnvKey(filePath: string, key: string, value: string): Promise<void> {
71
+ if (typeof value !== "string") {
72
+ throw new TypeError(`writeEnvKey expects a string value for key "${key}", got ${typeof value}`);
73
+ }
74
+ let content = "";
75
+ try {
76
+ content = await readFile(filePath, "utf8");
77
+ } catch {
78
+ content = "";
79
+ }
80
+ const escaped = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "");
81
+ const line = `${key}=${escaped}`;
82
+ const regex = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=.*$`, "m");
83
+ if (regex.test(content)) {
84
+ content = content.replace(regex, line);
85
+ } else {
86
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
87
+ content += `${line}\n`;
88
+ }
89
+ await writeFile(filePath, content, "utf8");
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Validation helpers
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export function isSafeEnvVarKey(key: string): boolean {
97
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
98
+ }
99
+
100
+ export function isSupportedDeploymentEnvironment(env: string): boolean {
101
+ return env === "development" || env === "preview" || env === "production";
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Shell helpers (for vercel/convex CLI)
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export function shellEscapeSingle(value: string): string {
109
+ return `'${value.replace(/'/g, `'\\''`)}'`;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // applySecrets
114
+ // ---------------------------------------------------------------------------
115
+
116
+ interface ApplyResult {
117
+ applied: string[];
118
+ errors: string[];
119
+ }
120
+
121
+ /**
122
+ * Apply collected secrets to the target destination.
123
+ * Dotenv writes are handled directly; vercel/convex shell out via execFn.
124
+ */
125
+ export async function applySecrets(
126
+ provided: Array<{ key: string; value: string }>,
127
+ destination: "dotenv" | "vercel" | "convex",
128
+ opts: {
129
+ envFilePath: string;
130
+ environment?: string;
131
+ execFn?: (cmd: string, args: string[]) => Promise<{ code: number; stderr: string }>;
132
+ },
133
+ ): Promise<ApplyResult> {
134
+ const applied: string[] = [];
135
+ const errors: string[] = [];
136
+
137
+ if (destination === "dotenv") {
138
+ for (const { key, value } of provided) {
139
+ try {
140
+ await writeEnvKey(opts.envFilePath, key, value);
141
+ applied.push(key);
142
+ // Hydrate process.env so the current session sees the new value
143
+ process.env[key] = value;
144
+ } catch (err: unknown) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ errors.push(`${key}: ${msg}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ if ((destination === "vercel" || destination === "convex") && opts.execFn) {
152
+ const env = opts.environment ?? "development";
153
+ if (!isSupportedDeploymentEnvironment(env)) {
154
+ errors.push(`environment: unsupported target environment "${env}"`);
155
+ return { applied, errors };
156
+ }
157
+ for (const { key, value } of provided) {
158
+ if (!isSafeEnvVarKey(key)) {
159
+ errors.push(`${key}: invalid environment variable name`);
160
+ continue;
161
+ }
162
+ const cmd = destination === "vercel"
163
+ ? `printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`
164
+ : "";
165
+ try {
166
+ const result = destination === "vercel"
167
+ ? await opts.execFn("sh", ["-c", cmd])
168
+ : await opts.execFn("npx", ["convex", "env", "set", key, value]);
169
+ if (result.code !== 0) {
170
+ errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
171
+ } else {
172
+ applied.push(key);
173
+ process.env[key] = value;
174
+ }
175
+ } catch (err: unknown) {
176
+ const msg = err instanceof Error ? err.message : String(err);
177
+ errors.push(`${key}: ${msg}`);
178
+ }
179
+ }
180
+ }
181
+
182
+ return { applied, errors };
183
+ }
@@ -0,0 +1,48 @@
1
+ // GSD-2 — Regression tests for importLocalModule candidate resolution (#3954)
2
+ import { describe, it } from "node:test";
3
+ import assert from "node:assert/strict";
4
+
5
+ import { _buildImportCandidates } from "./workflow-tools.js";
6
+
7
+ describe("_buildImportCandidates", () => {
8
+ it("includes dist/ fallback for src/ paths", () => {
9
+ const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
10
+ assert.ok(
11
+ candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")),
12
+ "should include dist/ swapped candidate",
13
+ );
14
+ });
15
+
16
+ it("includes src/ fallback for dist/ paths", () => {
17
+ const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js");
18
+ assert.ok(
19
+ candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")),
20
+ "should include src/ swapped candidate",
21
+ );
22
+ });
23
+
24
+ it("includes .ts variants for .js paths", () => {
25
+ const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
26
+ assert.ok(
27
+ candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")),
28
+ "should include .ts variant for original src/ path",
29
+ );
30
+ assert.ok(
31
+ candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/dist/")),
32
+ "should include .ts variant for swapped dist/ path",
33
+ );
34
+ });
35
+
36
+ it("returns original path first", () => {
37
+ const input = "../../../src/resources/extensions/gsd/db-writer.js";
38
+ const candidates = _buildImportCandidates(input);
39
+ assert.equal(candidates[0], input, "first candidate should be the original path");
40
+ });
41
+
42
+ it("handles paths without src/ or dist/ gracefully", () => {
43
+ const candidates = _buildImportCandidates("./local-module.js");
44
+ assert.equal(candidates.length, 2, "should have original + .ts variant only");
45
+ assert.equal(candidates[0], "./local-module.js");
46
+ assert.equal(candidates[1], "./local-module.ts");
47
+ });
48
+ });