gsd-pi 2.43.0-next.8 → 2.44.0-dev.0b97ffd

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 (399) hide show
  1. package/README.md +30 -12
  2. package/dist/cli.js +13 -1
  3. package/dist/help-text.js +24 -0
  4. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +21 -8
  5. package/dist/resources/extensions/gsd/auto-prompts.js +130 -51
  6. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  7. package/dist/resources/extensions/gsd/auto-worktree.js +16 -2
  8. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  9. package/dist/resources/extensions/gsd/dispatch-guard.js +34 -10
  10. package/dist/resources/extensions/gsd/markdown-renderer.js +7 -5
  11. package/dist/resources/extensions/gsd/reactive-graph.js +13 -2
  12. package/dist/resources/extensions/gsd/skill-health.js +3 -1
  13. package/dist/resources/extensions/gsd/tools/plan-milestone.js +2 -11
  14. package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -10
  15. package/dist/resources/extensions/gsd/visualizer-data.js +45 -13
  16. package/dist/resources/extensions/gsd/workspace-index.js +46 -15
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  19. package/dist/web/standalone/.next/build-manifest.json +3 -3
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/required-server-files.json +4 -4
  22. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  23. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  33. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  87. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  93. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  109. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  113. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/index.html +1 -1
  123. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  124. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  125. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  126. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  128. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/page.js +2 -2
  130. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  132. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  133. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  134. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/middleware.js +2 -2
  136. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  138. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  139. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  140. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  141. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-f2a7482d42a5614b.js +1 -0
  142. package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +1 -0
  143. package/dist/web/standalone/.next/static/chunks/app/page-b9367c5ae13b99c6.js +1 -0
  144. package/dist/web/standalone/.next/static/chunks/{main-app-2f2ee7b85712c2bd.js → main-app-fdab67f7802d7832.js} +1 -1
  145. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  146. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  147. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  148. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  149. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  150. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  151. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  152. package/dist/web/standalone/server.js +1 -1
  153. package/package.json +4 -4
  154. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +3 -3
  155. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/core/agent-session.js +11 -34
  157. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  159. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts +2 -2
  161. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +2 -2
  164. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  166. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +4 -4
  167. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
  168. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  169. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  171. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  173. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  175. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +37 -0
  177. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts.map +1 -1
  180. package/packages/pi-coding-agent/dist/core/fallback-resolver.js +2 -3
  181. package/packages/pi-coding-agent/dist/core/fallback-resolver.js.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js +12 -2
  183. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  185. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts +38 -0
  187. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts.map +1 -0
  188. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js +192 -0
  189. package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js.map +1 -0
  190. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.d.ts +2 -0
  191. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.d.ts.map +1 -0
  192. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +255 -0
  193. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -0
  194. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +15 -0
  195. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/model-registry.js +40 -3
  197. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/package-commands.d.ts +25 -0
  199. package/packages/pi-coding-agent/dist/core/package-commands.d.ts.map +1 -0
  200. package/packages/pi-coding-agent/dist/core/package-commands.js +253 -0
  201. package/packages/pi-coding-agent/dist/core/package-commands.js.map +1 -0
  202. package/packages/pi-coding-agent/dist/core/package-commands.test.d.ts +2 -0
  203. package/packages/pi-coding-agent/dist/core/package-commands.test.d.ts.map +1 -0
  204. package/packages/pi-coding-agent/dist/core/package-commands.test.js +225 -0
  205. package/packages/pi-coding-agent/dist/core/package-commands.test.js.map +1 -0
  206. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  207. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  208. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  209. package/packages/pi-coding-agent/dist/core/sdk.js +4 -0
  210. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  211. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  212. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  213. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  214. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  215. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  216. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  217. package/packages/pi-coding-agent/dist/index.js +1 -0
  218. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  219. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  220. package/packages/pi-coding-agent/dist/main.js +11 -199
  221. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  222. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  223. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  224. package/packages/pi-coding-agent/package.json +1 -1
  225. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -37
  226. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  227. package/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +2 -2
  228. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +3 -3
  229. package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +4 -4
  230. package/packages/pi-coding-agent/src/core/extensions/index.ts +5 -0
  231. package/packages/pi-coding-agent/src/core/extensions/loader.ts +23 -0
  232. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  233. package/packages/pi-coding-agent/src/core/extensions/types.ts +44 -0
  234. package/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +15 -2
  235. package/packages/pi-coding-agent/src/core/fallback-resolver.ts +2 -3
  236. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  237. package/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +274 -0
  238. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +288 -0
  239. package/packages/pi-coding-agent/src/core/model-registry.ts +39 -3
  240. package/packages/pi-coding-agent/src/core/package-commands.test.ts +240 -0
  241. package/packages/pi-coding-agent/src/core/package-commands.ts +310 -0
  242. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  243. package/packages/pi-coding-agent/src/core/sdk.ts +4 -0
  244. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  245. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  246. package/packages/pi-coding-agent/src/index.ts +7 -0
  247. package/packages/pi-coding-agent/src/main.ts +11 -232
  248. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  249. package/pkg/package.json +1 -1
  250. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +22 -7
  251. package/src/resources/extensions/gsd/auto-prompts.ts +109 -42
  252. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  253. package/src/resources/extensions/gsd/auto-worktree.ts +16 -3
  254. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  255. package/src/resources/extensions/gsd/dispatch-guard.ts +28 -10
  256. package/src/resources/extensions/gsd/markdown-renderer.ts +7 -5
  257. package/src/resources/extensions/gsd/reactive-graph.ts +12 -2
  258. package/src/resources/extensions/gsd/skill-health.ts +2 -1
  259. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  260. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  261. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  262. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  263. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  264. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  265. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  266. package/src/resources/extensions/gsd/tests/auto-stash-merge.test.ts +3 -3
  267. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  268. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  269. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  270. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  271. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  272. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  273. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  274. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  275. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  276. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  277. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  278. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  279. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  280. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  281. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  282. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  283. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  284. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  285. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  286. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +152 -183
  287. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  288. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  289. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  290. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  291. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  292. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  293. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  294. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  295. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  296. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  297. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  298. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  299. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  300. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  301. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  302. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  303. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  304. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  305. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  306. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  307. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  308. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  309. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  310. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  311. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  312. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  313. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  314. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  315. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  316. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  317. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  318. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  319. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  320. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  321. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  322. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  323. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  324. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  325. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  326. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  327. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  328. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  329. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  330. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  331. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  332. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  333. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  334. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  335. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  336. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  337. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  338. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  339. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  340. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  341. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  342. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  343. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  344. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  345. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  346. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  347. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  348. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  349. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  350. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  351. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  352. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  353. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  354. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  355. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  356. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  357. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  358. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  359. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  360. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  361. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  362. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  363. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  364. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  365. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  366. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  367. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  368. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  369. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  370. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  371. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  372. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  373. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  374. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  375. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  376. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  377. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  378. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  379. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  380. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  381. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  382. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  383. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  384. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  385. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  386. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  387. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  388. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  389. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  390. package/src/resources/extensions/gsd/tools/plan-milestone.ts +1 -18
  391. package/src/resources/extensions/gsd/tools/plan-slice.ts +1 -15
  392. package/src/resources/extensions/gsd/visualizer-data.ts +46 -14
  393. package/src/resources/extensions/gsd/workspace-index.ts +49 -18
  394. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-e07acdb7dd069836.js +0 -1
  395. package/dist/web/standalone/.next/static/chunks/app/layout-745c6ed5fea5fb06.js +0 -1
  396. package/dist/web/standalone/.next/static/chunks/app/page-801b53eff6e83579.js +0 -1
  397. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-e6255954dccfcf0a.js +0 -1
  398. /package/dist/web/standalone/.next/static/{drUWS0zys9uepCfCwecJv → alS4hoANx0TK4UVZY27da}/_buildManifest.js +0 -0
  399. /package/dist/web/standalone/.next/static/{drUWS0zys9uepCfCwecJv → alS4hoANx0TK4UVZY27da}/_ssgManifest.js +0 -0
@@ -42,6 +42,7 @@ import type {
42
42
  Extension,
43
43
  ExtensionAPI,
44
44
  ExtensionFactory,
45
+ LifecycleHookHandler,
45
46
  ExtensionRuntime,
46
47
  LoadExtensionsResult,
47
48
  MessageRenderer,
@@ -463,6 +464,22 @@ function createExtensionAPI(
463
464
  extension.commands.set(name, { name, ...options });
464
465
  },
465
466
 
467
+ registerBeforeInstall(handler: LifecycleHookHandler): void {
468
+ extension.lifecycleHooks.beforeInstall.push(handler);
469
+ },
470
+
471
+ registerAfterInstall(handler: LifecycleHookHandler): void {
472
+ extension.lifecycleHooks.afterInstall.push(handler);
473
+ },
474
+
475
+ registerBeforeRemove(handler: LifecycleHookHandler): void {
476
+ extension.lifecycleHooks.beforeRemove.push(handler);
477
+ },
478
+
479
+ registerAfterRemove(handler: LifecycleHookHandler): void {
480
+ extension.lifecycleHooks.afterRemove.push(handler);
481
+ },
482
+
466
483
  registerShortcut(
467
484
  shortcut: KeyId,
468
485
  options: {
@@ -683,6 +700,12 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
683
700
  commands: new Map(),
684
701
  flags: new Map(),
685
702
  shortcuts: new Map(),
703
+ lifecycleHooks: {
704
+ beforeInstall: [],
705
+ afterInstall: [],
706
+ beforeRemove: [],
707
+ afterRemove: [],
708
+ },
686
709
  };
687
710
  }
688
711
 
@@ -48,37 +48,37 @@ function makeThrowingExtension(eventType: string, error: Error): Extension {
48
48
  }
49
49
 
50
50
  describe("ExtensionRunner.emitToolCall", () => {
51
- it("catches throwing extension handler and routes to emitError", async () => {
51
+ it("catches throwing extension handler and routes to emitError", async (t) => {
52
52
  const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
53
- try {
54
- const sessionManager = SessionManager.create(dir, dir);
55
- const authStorage = AuthStorage.create();
56
- const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
53
+ t.after(() => {
54
+ rmSync(dir, { recursive: true, force: true });
55
+ });
57
56
 
58
- const throwingExt = makeThrowingExtension("tool_call", new Error("handler crashed"));
59
- const runtime = makeMinimalRuntime();
60
- const runner = new ExtensionRunner([throwingExt], runtime, dir, sessionManager, modelRegistry);
57
+ const sessionManager = SessionManager.create(dir, dir);
58
+ const authStorage = AuthStorage.create();
59
+ const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
61
60
 
62
- const errors: any[] = [];
63
- runner.onError((err) => errors.push(err));
61
+ const throwingExt = makeThrowingExtension("tool_call", new Error("handler crashed"));
62
+ const runtime = makeMinimalRuntime();
63
+ const runner = new ExtensionRunner([throwingExt], runtime, dir, sessionManager, modelRegistry);
64
64
 
65
- const event: ToolCallEvent = {
66
- type: "tool_call",
67
- toolCallId: "test-123",
68
- toolName: "test_tool",
69
- input: {},
70
- } as ToolCallEvent;
65
+ const errors: any[] = [];
66
+ runner.onError((err) => errors.push(err));
71
67
 
72
- const result = await runner.emitToolCall(event);
68
+ const event: ToolCallEvent = {
69
+ type: "tool_call",
70
+ toolCallId: "test-123",
71
+ toolName: "test_tool",
72
+ input: {},
73
+ } as ToolCallEvent;
73
74
 
74
- // Should not throw — error is caught and routed to emitError
75
- assert.equal(result, undefined);
76
- assert.equal(errors.length, 1);
77
- assert.equal(errors[0].error, "handler crashed");
78
- assert.equal(errors[0].event, "tool_call");
79
- assert.equal(errors[0].extensionPath, "/test/throwing-ext");
80
- } finally {
81
- rmSync(dir, { recursive: true, force: true });
82
- }
75
+ const result = await runner.emitToolCall(event);
76
+
77
+ // Should not throw — error is caught and routed to emitError
78
+ assert.equal(result, undefined);
79
+ assert.equal(errors.length, 1);
80
+ assert.equal(errors[0].error, "handler crashed");
81
+ assert.equal(errors[0].event, "tool_call");
82
+ assert.equal(errors[0].extensionPath, "/test/throwing-ext");
83
83
  });
84
84
  });
@@ -949,6 +949,33 @@ export interface RegisteredCommand {
949
949
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
950
950
  }
951
951
 
952
+ export type LifecycleHookScope = "user" | "project";
953
+ export type LifecycleHookPhase = "beforeInstall" | "afterInstall" | "beforeRemove" | "afterRemove";
954
+
955
+ export interface LifecycleHookContext {
956
+ /** Lifecycle phase currently being executed. */
957
+ phase: LifecycleHookPhase;
958
+ /** Package source string passed to install (npm:, git:, https://, local path). */
959
+ source: string;
960
+ /** Resolved installed package path (or resolved local path), when available for this phase. */
961
+ installedPath?: string;
962
+ /** Where the package was installed. */
963
+ scope: LifecycleHookScope;
964
+ /** Current working directory for the install invocation. */
965
+ cwd: string;
966
+ /** Whether install is running in an interactive TTY. */
967
+ interactive: boolean;
968
+ /** Info-level logging sink for install output. */
969
+ log(message: string): void;
970
+ /** Warning-level logging sink for install output. */
971
+ warn(message: string): void;
972
+ /** Error-level logging sink for install output. */
973
+ error(message: string): void;
974
+ }
975
+
976
+ export type LifecycleHookHandler = (ctx: LifecycleHookContext) => Promise<void> | void;
977
+ export type LifecycleHookMap = Record<LifecycleHookPhase, LifecycleHookHandler[]>;
978
+
952
979
  // ============================================================================
953
980
  // Extension API
954
981
  // ============================================================================
@@ -1019,6 +1046,18 @@ export interface ExtensionAPI {
1019
1046
  /** Register a custom command. */
1020
1047
  registerCommand(name: string, options: Omit<RegisteredCommand, "name">): void;
1021
1048
 
1049
+ /** Register a lifecycle hook run before package installation starts. */
1050
+ registerBeforeInstall(handler: LifecycleHookHandler): void;
1051
+
1052
+ /** Register a lifecycle hook run after package installation completes. */
1053
+ registerAfterInstall(handler: LifecycleHookHandler): void;
1054
+
1055
+ /** Register a lifecycle hook run before package removal starts. */
1056
+ registerBeforeRemove(handler: LifecycleHookHandler): void;
1057
+
1058
+ /** Register a lifecycle hook run after package removal completes. */
1059
+ registerAfterRemove(handler: LifecycleHookHandler): void;
1060
+
1022
1061
  /** Register a keyboard shortcut. */
1023
1062
  registerShortcut(
1024
1063
  shortcut: KeyId,
@@ -1201,6 +1240,10 @@ export interface ExtensionAPI {
1201
1240
 
1202
1241
  /** Configuration for registering a provider via pi.registerProvider(). */
1203
1242
  export interface ProviderConfig {
1243
+ /** Auth behavior for provider availability and request key handling. Defaults to "apiKey". */
1244
+ authMode?: "apiKey" | "oauth" | "externalCli" | "none";
1245
+ /** Optional readiness check. Return false if the provider cannot accept requests (e.g., CLI not authenticated, API key invalid). Called before default auth checks. */
1246
+ isReady?: () => boolean;
1204
1247
  /** Base URL for the API endpoint. Required when defining models. */
1205
1248
  baseUrl?: string;
1206
1249
  /** API key or environment variable name. Required when defining models (unless oauth provided). */
@@ -1382,6 +1425,7 @@ export interface Extension {
1382
1425
  commands: Map<string, RegisteredCommand>;
1383
1426
  flags: Map<string, ExtensionFlag>;
1384
1427
  shortcuts: Map<KeyId, ExtensionShortcut>;
1428
+ lifecycleHooks: LifecycleHookMap;
1385
1429
  }
1386
1430
 
1387
1431
  /** Result of loading extensions. */
@@ -38,6 +38,7 @@ function createResolver(overrides?: {
38
38
  enabled?: boolean;
39
39
  isProviderAvailable?: (provider: string) => boolean;
40
40
  hasAuth?: (provider: string) => boolean;
41
+ isProviderRequestReady?: (provider: string) => boolean;
41
42
  find?: (provider: string, modelId: string) => Model<Api> | undefined;
42
43
  }) {
43
44
  const settingsManager = {
@@ -60,6 +61,7 @@ function createResolver(overrides?: {
60
61
  if (provider === "openai" && modelId === "gpt-4.1") return openaiModel;
61
62
  return undefined;
62
63
  }),
64
+ isProviderRequestReady: overrides?.isProviderRequestReady ?? overrides?.hasAuth ?? (() => true),
63
65
  } as unknown as ModelRegistry;
64
66
 
65
67
  return { resolver: new FallbackResolver(settingsManager, authStorage, modelRegistry), authStorage };
@@ -122,9 +124,9 @@ describe("FallbackResolver — findFallback", () => {
122
124
  assert.equal(result, null);
123
125
  });
124
126
 
125
- it("skips providers without auth", async () => {
127
+ it("skips providers that are not request-ready", async () => {
126
128
  const { resolver } = createResolver({
127
- hasAuth: (provider: string) => provider !== "alibaba",
129
+ isProviderRequestReady: (provider: string) => provider !== "alibaba",
128
130
  });
129
131
 
130
132
  const result = await resolver.findFallback(zaiModel, "quota_exhausted");
@@ -133,6 +135,17 @@ describe("FallbackResolver — findFallback", () => {
133
135
  assert.equal(result!.model.provider, "openai");
134
136
  });
135
137
 
138
+ it("allows fallback to external-cli style providers without stored auth", async () => {
139
+ const { resolver } = createResolver({
140
+ hasAuth: () => false,
141
+ isProviderRequestReady: (provider: string) => provider === "alibaba",
142
+ });
143
+
144
+ const result = await resolver.findFallback(zaiModel, "quota_exhausted");
145
+ assert.notEqual(result, null);
146
+ assert.equal(result!.model.provider, "alibaba");
147
+ });
148
+
136
149
  it("skips providers with no model in registry", async () => {
137
150
  const { resolver } = createResolver({
138
151
  find: (provider: string, modelId: string) => {
@@ -149,9 +149,8 @@ export class FallbackResolver {
149
149
  const model = this.modelRegistry.find(entry.provider, entry.model);
150
150
  if (!model) continue;
151
151
 
152
- // Check if API key is available
153
- const hasAuth = this.authStorage.hasAuth(entry.provider);
154
- if (!hasAuth) continue;
152
+ // Check if provider is request-ready for fallback (authMode-aware)
153
+ if (!this.modelRegistry.isProviderRequestReady(entry.provider)) continue;
155
154
 
156
155
  return {
157
156
  model,
@@ -1,66 +1,54 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it, afterEach } from "node:test";
3
3
  import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { atomicWriteFileSync } from "./fs-utils.js";
7
7
 
8
8
  describe("atomicWriteFileSync", () => {
9
- it("writes file content atomically", () => {
10
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
11
- try {
12
- const filePath = join(dir, "test.txt");
13
- atomicWriteFileSync(filePath, "hello world");
14
- assert.equal(readFileSync(filePath, "utf-8"), "hello world");
15
- } finally {
9
+ let dir: string;
10
+
11
+ afterEach(() => {
12
+ if (dir) {
16
13
  rmSync(dir, { recursive: true, force: true });
17
14
  }
18
15
  });
19
16
 
17
+ it("writes file content atomically", () => {
18
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
19
+ const filePath = join(dir, "test.txt");
20
+ atomicWriteFileSync(filePath, "hello world");
21
+ assert.equal(readFileSync(filePath, "utf-8"), "hello world");
22
+ });
23
+
20
24
  it("overwrites existing file atomically", () => {
21
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
22
- try {
23
- const filePath = join(dir, "test.txt");
24
- atomicWriteFileSync(filePath, "first");
25
- atomicWriteFileSync(filePath, "second");
26
- assert.equal(readFileSync(filePath, "utf-8"), "second");
27
- } finally {
28
- rmSync(dir, { recursive: true, force: true });
29
- }
25
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
26
+ const filePath = join(dir, "test.txt");
27
+ atomicWriteFileSync(filePath, "first");
28
+ atomicWriteFileSync(filePath, "second");
29
+ assert.equal(readFileSync(filePath, "utf-8"), "second");
30
30
  });
31
31
 
32
32
  it("does not leave .tmp file after successful write", () => {
33
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
34
- try {
35
- const filePath = join(dir, "test.txt");
36
- atomicWriteFileSync(filePath, "content");
37
- assert.equal(existsSync(filePath + ".tmp"), false);
38
- } finally {
39
- rmSync(dir, { recursive: true, force: true });
40
- }
33
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
34
+ const filePath = join(dir, "test.txt");
35
+ atomicWriteFileSync(filePath, "content");
36
+ assert.equal(existsSync(filePath + ".tmp"), false);
41
37
  });
42
38
 
43
39
  it("supports Buffer content", () => {
44
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
45
- try {
46
- const filePath = join(dir, "test.bin");
47
- const buf = Buffer.from([0x00, 0x01, 0x02, 0xff]);
48
- atomicWriteFileSync(filePath, buf);
49
- const result = readFileSync(filePath);
50
- assert.deepEqual(result, buf);
51
- } finally {
52
- rmSync(dir, { recursive: true, force: true });
53
- }
40
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
41
+ const filePath = join(dir, "test.bin");
42
+ const buf = Buffer.from([0x00, 0x01, 0x02, 0xff]);
43
+ atomicWriteFileSync(filePath, buf);
44
+ const result = readFileSync(filePath);
45
+ assert.deepEqual(result, buf);
54
46
  });
55
47
 
56
48
  it("supports encoding parameter", () => {
57
- const dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
58
- try {
59
- const filePath = join(dir, "test.txt");
60
- atomicWriteFileSync(filePath, "utf8 content", "utf-8");
61
- assert.equal(readFileSync(filePath, "utf-8"), "utf8 content");
62
- } finally {
63
- rmSync(dir, { recursive: true, force: true });
64
- }
49
+ dir = mkdtempSync(join(tmpdir(), "fs-utils-test-"));
50
+ const filePath = join(dir, "test.txt");
51
+ atomicWriteFileSync(filePath, "utf8 content", "utf-8");
52
+ assert.equal(readFileSync(filePath, "utf-8"), "utf8 content");
65
53
  });
66
54
  });
@@ -0,0 +1,274 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { parseGitUrl } from "../utils/git.js";
7
+ import {
8
+ importExtensionModule,
9
+ loadExtensions,
10
+ type LifecycleHookContext,
11
+ type LifecycleHookMap,
12
+ type LifecycleHookHandler,
13
+ type LifecycleHookPhase,
14
+ type LifecycleHookScope,
15
+ } from "./extensions/index.js";
16
+ import type { DefaultPackageManager } from "./package-manager.js";
17
+
18
+ interface ExtensionManifest {
19
+ dependencies?: {
20
+ runtime?: string[];
21
+ };
22
+ }
23
+
24
+ export interface PackageLifecycleHooksOptions {
25
+ source: string;
26
+ local: boolean;
27
+ cwd: string;
28
+ agentDir: string;
29
+ appName: string;
30
+ packageManager: DefaultPackageManager;
31
+ stdout: NodeJS.WriteStream;
32
+ stderr: NodeJS.WriteStream;
33
+ }
34
+
35
+ export type LifecycleHooksTarget = "source" | "installed";
36
+
37
+ export interface PrepareLifecycleHooksOptions {
38
+ verifyRuntimeDependencies?: boolean;
39
+ }
40
+
41
+ export interface LifecycleHooksRunResult {
42
+ phase: LifecycleHookPhase;
43
+ hooksRun: number;
44
+ hookErrors: number;
45
+ legacyHooksRun: number;
46
+ entryPathCount: number;
47
+ skipped: boolean;
48
+ }
49
+
50
+ interface LoadedLifecycleHooks {
51
+ source: string;
52
+ scope: LifecycleHookScope;
53
+ installedPath?: string;
54
+ cwd: string;
55
+ stdout: NodeJS.WriteStream;
56
+ stderr: NodeJS.WriteStream;
57
+ entryPaths: string[];
58
+ hooksByPath: Map<string, LifecycleHookMap>;
59
+ }
60
+
61
+ function toScope(local: boolean): LifecycleHookScope {
62
+ return local ? "project" : "user";
63
+ }
64
+
65
+ function readManifestRuntimeDeps(dir: string): string[] {
66
+ const manifestPath = join(dir, "extension-manifest.json");
67
+ if (!existsSync(manifestPath)) return [];
68
+ try {
69
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as ExtensionManifest;
70
+ return manifest.dependencies?.runtime?.filter((dep): dep is string => typeof dep === "string") ?? [];
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+
76
+ function collectRuntimeDependencies(installedPath: string, entryPaths: string[]): string[] {
77
+ const deps = new Set<string>();
78
+ const candidateDirs = new Set<string>([installedPath, ...entryPaths.map((entryPath) => dirname(entryPath))]);
79
+ for (const dir of candidateDirs) {
80
+ for (const dep of readManifestRuntimeDeps(dir)) {
81
+ deps.add(dep);
82
+ }
83
+ }
84
+ return Array.from(deps);
85
+ }
86
+
87
+ function verifyRuntimeDependencies(runtimeDeps: string[], source: string, appName: string): void {
88
+ const missing: string[] = [];
89
+ for (const dep of runtimeDeps) {
90
+ const result = spawnSync(dep, ["--version"], { encoding: "utf-8", timeout: 5000 });
91
+ if (result.error || result.status !== 0) {
92
+ missing.push(dep);
93
+ }
94
+ }
95
+ if (missing.length === 0) return;
96
+ throw new Error(
97
+ `Missing runtime dependencies: ${missing.join(", ")}.\n` +
98
+ `Install them and retry: ${appName} install ${source}`,
99
+ );
100
+ }
101
+
102
+ function resolveLocalSourcePath(source: string, cwd: string): string | undefined {
103
+ const trimmed = source.trim();
104
+ if (!trimmed) return undefined;
105
+ if (trimmed.startsWith("npm:")) return undefined;
106
+ if (parseGitUrl(trimmed)) return undefined;
107
+
108
+ let normalized = trimmed;
109
+ if (normalized === "~") {
110
+ normalized = homedir();
111
+ } else if (normalized.startsWith("~/")) {
112
+ normalized = join(homedir(), normalized.slice(2));
113
+ }
114
+
115
+ const absolutePath = resolve(cwd, normalized);
116
+ return existsSync(absolutePath) ? absolutePath : undefined;
117
+ }
118
+
119
+ async function resolveEntryPathsFromTarget(
120
+ options: PackageLifecycleHooksOptions,
121
+ target: LifecycleHooksTarget,
122
+ scope: LifecycleHookScope,
123
+ ): Promise<{ entryPaths: string[]; installedPath?: string }> {
124
+ if (target === "source") {
125
+ const localSourcePath = resolveLocalSourcePath(options.source, options.cwd);
126
+ if (!localSourcePath) return { entryPaths: [] };
127
+ const resolved = await options.packageManager.resolveExtensionSources([localSourcePath], { local: true });
128
+ const entryPaths = resolved.extensions.filter((resource) => resource.enabled).map((resource) => resource.path);
129
+ return { entryPaths, installedPath: localSourcePath };
130
+ }
131
+
132
+ const installedPath = options.packageManager.getInstalledPath(options.source, scope);
133
+ if (!installedPath) return { entryPaths: [] };
134
+ const resolved = await options.packageManager.resolveExtensionSources([installedPath], { local: true });
135
+ const entryPaths = resolved.extensions.filter((resource) => resource.enabled).map((resource) => resource.path);
136
+ return { entryPaths, installedPath };
137
+ }
138
+
139
+ export async function prepareLifecycleHooks(
140
+ options: PackageLifecycleHooksOptions,
141
+ target: LifecycleHooksTarget,
142
+ prepareOptions?: PrepareLifecycleHooksOptions,
143
+ ): Promise<LoadedLifecycleHooks | null> {
144
+ const scope = toScope(options.local);
145
+ const { entryPaths, installedPath } = await resolveEntryPathsFromTarget(options, target, scope);
146
+ if (entryPaths.length === 0) {
147
+ return null;
148
+ }
149
+
150
+ if (prepareOptions?.verifyRuntimeDependencies && installedPath) {
151
+ const runtimeDeps = collectRuntimeDependencies(installedPath, entryPaths);
152
+ verifyRuntimeDependencies(runtimeDeps, options.source, options.appName);
153
+ }
154
+
155
+ const loaded = await loadExtensions(entryPaths, options.cwd);
156
+ for (const { path, error } of loaded.errors) {
157
+ options.stderr.write(`[lifecycle-hooks] Failed to load extension "${path}": ${error}\n`);
158
+ }
159
+
160
+ const hooksByPath = new Map<string, LifecycleHookMap>();
161
+ for (const extension of loaded.extensions) {
162
+ hooksByPath.set(extension.path, extension.lifecycleHooks);
163
+ }
164
+
165
+ return {
166
+ source: options.source,
167
+ scope,
168
+ installedPath,
169
+ cwd: options.cwd,
170
+ stdout: options.stdout,
171
+ stderr: options.stderr,
172
+ entryPaths,
173
+ hooksByPath,
174
+ };
175
+ }
176
+
177
+ async function runHookSafe(
178
+ hook: LifecycleHookHandler,
179
+ context: LifecycleHookContext,
180
+ stderr: NodeJS.WriteStream,
181
+ ): Promise<boolean> {
182
+ try {
183
+ await hook(context);
184
+ return true;
185
+ } catch (error) {
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ stderr.write(`[lifecycle-hooks:${context.phase}] Hook failed: ${message}\n`);
188
+ return false;
189
+ }
190
+ }
191
+
192
+ function getLegacyExportCandidates(phase: LifecycleHookPhase): string[] {
193
+ return [phase];
194
+ }
195
+
196
+ async function runLegacyExportHook(
197
+ entryPath: string,
198
+ phase: LifecycleHookPhase,
199
+ context: LifecycleHookContext,
200
+ ): Promise<LifecycleHookHandler | null> {
201
+ try {
202
+ const module = await importExtensionModule<Record<string, unknown>>(import.meta.url, pathToFileURL(entryPath).href);
203
+ for (const exportName of getLegacyExportCandidates(phase)) {
204
+ const candidate = module[exportName];
205
+ if (typeof candidate === "function") {
206
+ return candidate as LifecycleHookHandler;
207
+ }
208
+ }
209
+ return null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ export async function runLifecycleHooks(
216
+ loaded: LoadedLifecycleHooks | null,
217
+ phase: LifecycleHookPhase,
218
+ ): Promise<LifecycleHooksRunResult> {
219
+ if (!loaded) {
220
+ return {
221
+ phase,
222
+ hooksRun: 0,
223
+ hookErrors: 0,
224
+ legacyHooksRun: 0,
225
+ entryPathCount: 0,
226
+ skipped: true,
227
+ };
228
+ }
229
+
230
+ const context: LifecycleHookContext = {
231
+ phase,
232
+ source: loaded.source,
233
+ installedPath: loaded.installedPath,
234
+ scope: loaded.scope,
235
+ cwd: loaded.cwd,
236
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
237
+ log: (message) => loaded.stdout.write(`${message}\n`),
238
+ warn: (message) => loaded.stderr.write(`${message}\n`),
239
+ error: (message) => loaded.stderr.write(`${message}\n`),
240
+ };
241
+
242
+ let hooksRun = 0;
243
+ let hookErrors = 0;
244
+ let legacyHooksRun = 0;
245
+
246
+ for (const entryPath of loaded.entryPaths) {
247
+ const hookMap = loaded.hooksByPath.get(entryPath);
248
+ const registeredHooks = hookMap?.[phase] ?? [];
249
+ if (registeredHooks.length > 0) {
250
+ for (const hook of registeredHooks) {
251
+ hooksRun += 1;
252
+ const ok = await runHookSafe(hook, context, loaded.stderr);
253
+ if (!ok) hookErrors += 1;
254
+ }
255
+ continue;
256
+ }
257
+
258
+ const legacyHook = await runLegacyExportHook(entryPath, phase, context);
259
+ if (!legacyHook) continue;
260
+
261
+ legacyHooksRun += 1;
262
+ const ok = await runHookSafe(legacyHook, context, loaded.stderr);
263
+ if (!ok) hookErrors += 1;
264
+ }
265
+
266
+ return {
267
+ phase,
268
+ hooksRun,
269
+ hookErrors,
270
+ legacyHooksRun,
271
+ entryPathCount: loaded.entryPaths.length,
272
+ skipped: false,
273
+ };
274
+ }