@pi-harness/pi-harness 0.1.23 → 0.1.24

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 (295) hide show
  1. package/README.md +11 -9
  2. package/apps/web/dist/assets/index-BcLXLKqO.js +24 -0
  3. package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
  4. package/apps/web/dist/index.html +2 -2
  5. package/apps/web/package.json +6 -6
  6. package/apps/web/profile/cordis.yml +1 -0
  7. package/apps/web/server-dist/bin.js +27 -2
  8. package/apps/web/server-dist/bin.js.map +1 -1
  9. package/apps/web/src/server/bin.ts +27 -2
  10. package/apps/web/test/bin.test.ts +228 -5
  11. package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
  12. package/node_modules/@pi-harness/api-gateway/dist/index.js +113 -42
  13. package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
  14. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
  15. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
  16. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
  17. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
  18. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
  19. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
  20. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
  21. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
  22. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
  23. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
  24. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
  25. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
  26. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
  27. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
  28. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
  29. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
  30. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -1
  31. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  32. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
  33. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
  34. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
  35. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  36. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
  37. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  38. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
  39. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
  40. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
  41. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
  42. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  43. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
  44. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  45. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  46. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  47. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  48. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  49. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  50. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  51. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
  52. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  53. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  54. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  55. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  56. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  57. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
  58. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
  59. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  60. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
  61. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
  62. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
  63. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  64. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  65. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  66. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  67. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
  68. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  69. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  70. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
  71. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  72. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  73. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
  74. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
  75. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  76. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  77. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  78. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  79. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
  80. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  81. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
  82. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
  83. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  84. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
  85. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  86. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  87. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
  88. package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
  89. package/node_modules/@pi-harness/api-gateway/package.json +1 -1
  90. package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
  91. package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
  92. package/node_modules/@pi-harness/cli/dist/main.js +11 -0
  93. package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
  94. package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
  95. package/node_modules/@pi-harness/cli/dist/relaunch.js +2 -4
  96. package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
  97. package/node_modules/@pi-harness/cli/package.json +1 -1
  98. package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
  99. package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
  100. package/node_modules/@pi-harness/host-webserver/dist/index.js +39 -5
  101. package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
  102. package/node_modules/@pi-harness/host-webserver/package.json +1 -1
  103. package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
  104. package/node_modules/@pi-harness/web-app/dist/index.js +12 -3
  105. package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
  106. package/node_modules/@pi-harness/web-app/package.json +1 -1
  107. package/package.json +7 -7
  108. package/packages/api-gateway/dist/index.d.ts.map +1 -1
  109. package/packages/api-gateway/dist/index.js +113 -42
  110. package/packages/api-gateway/dist/index.js.map +1 -1
  111. package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
  112. package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
  113. package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
  114. package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
  115. package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -1
  116. package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
  117. package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
  118. package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -1
  119. package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
  120. package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
  121. package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
  122. package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
  123. package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
  124. package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
  125. package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
  126. package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -1
  127. package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -1
  128. package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
  129. package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +1 -1
  130. package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
  131. package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
  132. package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
  133. package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -1
  134. package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
  135. package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
  136. package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +1 -1
  137. package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
  138. package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
  139. package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
  140. package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
  141. package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
  142. package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
  143. package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
  144. package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
  145. package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
  146. package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
  147. package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
  148. package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +1 -1
  149. package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
  150. package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
  151. package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
  152. package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
  153. package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
  154. package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -1
  155. package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
  156. package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
  157. package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -1
  158. package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
  159. package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
  160. package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
  161. package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
  162. package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
  163. package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
  164. package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -1
  165. package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
  166. package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
  167. package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -1
  168. package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
  169. package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
  170. package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +1 -1
  171. package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
  172. package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
  173. package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
  174. package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
  175. package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
  176. package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +1 -1
  177. package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
  178. package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -1
  179. package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
  180. package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
  181. package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -1
  182. package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
  183. package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
  184. package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +1 -1
  185. package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
  186. package/packages/api-gateway/package.json +1 -1
  187. package/packages/api-gateway/src/index.ts +104 -40
  188. package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
  189. package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
  190. package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
  191. package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
  192. package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -1
  193. package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
  194. package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
  195. package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -1
  196. package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
  197. package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
  198. package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
  199. package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -1
  200. package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
  201. package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
  202. package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
  203. package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -1
  204. package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -1
  205. package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
  206. package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +1 -1
  207. package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
  208. package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
  209. package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
  210. package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -1
  211. package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
  212. package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
  213. package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +1 -1
  214. package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
  215. package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
  216. package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
  217. package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
  218. package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
  219. package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
  220. package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
  221. package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
  222. package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
  223. package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
  224. package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
  225. package/packages/api-gateway/src/marketplace-entries/official/openpets.json +1 -1
  226. package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
  227. package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
  228. package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
  229. package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
  230. package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
  231. package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -1
  232. package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
  233. package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
  234. package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -1
  235. package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
  236. package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
  237. package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
  238. package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
  239. package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
  240. package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
  241. package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -1
  242. package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
  243. package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
  244. package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -1
  245. package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
  246. package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
  247. package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +1 -1
  248. package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
  249. package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
  250. package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
  251. package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
  252. package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
  253. package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +1 -1
  254. package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
  255. package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -1
  256. package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
  257. package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
  258. package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -1
  259. package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
  260. package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
  261. package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +1 -1
  262. package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
  263. package/packages/api-gateway/test/index.test.ts +438 -4
  264. package/packages/api-gateway/test/packed-package.test.ts +70 -0
  265. package/packages/api-gateway/test/reference-docs.test.ts +50 -0
  266. package/packages/api-gateway/test/release-package.test.ts +32 -1
  267. package/packages/api-gateway/test/release-version.test.ts +40 -0
  268. package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
  269. package/packages/bundle-web-app/dist/index.js +12 -3
  270. package/packages/bundle-web-app/dist/index.js.map +1 -1
  271. package/packages/bundle-web-app/package.json +1 -1
  272. package/packages/bundle-web-app/src/index.ts +13 -3
  273. package/packages/bundle-web-app/test/index.test.ts +83 -2
  274. package/packages/cli/dist/main.d.ts +1 -0
  275. package/packages/cli/dist/main.d.ts.map +1 -1
  276. package/packages/cli/dist/main.js +11 -0
  277. package/packages/cli/dist/main.js.map +1 -1
  278. package/packages/cli/dist/relaunch.d.ts.map +1 -1
  279. package/packages/cli/dist/relaunch.js +2 -4
  280. package/packages/cli/dist/relaunch.js.map +1 -1
  281. package/packages/cli/package.json +1 -1
  282. package/packages/cli/src/main.ts +11 -0
  283. package/packages/cli/src/relaunch.ts +2 -3
  284. package/packages/cli/test/cli.test.ts +3 -1
  285. package/packages/cli/test/main.test.ts +134 -0
  286. package/packages/cli/test/relaunch.test.ts +86 -11
  287. package/packages/host-webserver/dist/index.d.ts +1 -0
  288. package/packages/host-webserver/dist/index.d.ts.map +1 -1
  289. package/packages/host-webserver/dist/index.js +39 -5
  290. package/packages/host-webserver/dist/index.js.map +1 -1
  291. package/packages/host-webserver/package.json +1 -1
  292. package/packages/host-webserver/src/index.ts +44 -6
  293. package/packages/host-webserver/test/index.test.ts +36 -5
  294. package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
  295. package/apps/web/dist/assets/index-Bo9CNI_U.js +0 -22
@@ -1,7 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
4
+ import { request as httpRequest } from "node:http";
5
+ import { createServer } from "node:net";
3
6
  import { tmpdir } from "node:os";
4
7
  import { join } from "node:path";
8
+ import { setTimeout as delay } from "node:timers/promises";
5
9
  import { afterEach, describe, expect, test } from "vitest";
6
10
 
7
11
  const BIN = join(import.meta.dirname, "..", "server-dist", "bin.js");
@@ -14,18 +18,35 @@ interface LauncherRun {
14
18
  readonly stderr: string;
15
19
  }
16
20
 
21
+ interface LauncherExit {
22
+ readonly code: number | null;
23
+ readonly stdout: string;
24
+ readonly stderr: string;
25
+ }
26
+
17
27
  const directories: string[] = [];
18
28
 
19
29
  afterEach(async () => {
20
30
  await Promise.all(directories.splice(0).map(async (directory) => rm(directory, { recursive: true, force: true })));
21
31
  });
22
32
 
23
- // Boots the built launcher on an ephemeral port with an isolated agent directory, waits for the console URL, sends the signal, and reports how the process exited.
24
- async function runLauncher(signal: NodeJS.Signals, extraEnv: Record<string, string> = {}): Promise<LauncherRun> {
25
- const agentDir = await mkdtemp(join(tmpdir(), "pi-harness-web-bin-"));
26
- directories.push(agentDir);
33
+ async function makeTempDir(label: string): Promise<string> {
34
+ const directory = await mkdtemp(join(tmpdir(), `pi-harness-web-bin-${label}-`));
35
+ directories.push(directory);
36
+ return directory;
37
+ }
38
+
39
+ // Boots the built launcher on an ephemeral port with an isolated agent directory, waits for the console URL, runs the optional probe against the live server, sends the signal, and reports how the process exited.
40
+ async function runLauncher(
41
+ signal: NodeJS.Signals,
42
+ extraEnv: Record<string, string> = {},
43
+ spawnCwd?: string,
44
+ probe?: (consoleUrl: string) => Promise<void>,
45
+ ): Promise<LauncherRun> {
46
+ const agentDir = await makeTempDir("agent");
27
47
  const child = spawn(process.execPath, [BIN], {
28
48
  stdio: ["ignore", "pipe", "pipe"],
49
+ ...(spawnCwd === undefined ? {} : { cwd: spawnCwd }),
29
50
  env: {
30
51
  ...process.env,
31
52
  PI_HARNESS_PORT: "0",
@@ -57,6 +78,12 @@ async function runLauncher(signal: NodeJS.Signals, extraEnv: Record<string, stri
57
78
  child.stdout.on("data", check);
58
79
  void exit.then(({ code }) => reject(new Error("launcher exited before printing the console URL (code " + String(code) + "): " + stderr)));
59
80
  });
81
+ try {
82
+ await probe?.(consoleUrl);
83
+ } catch (error) {
84
+ child.kill("SIGKILL");
85
+ throw error;
86
+ }
60
87
  const started = Date.now();
61
88
  child.kill(signal);
62
89
  const timeout = setTimeout(() => child.kill("SIGKILL"), 15_000);
@@ -65,6 +92,59 @@ async function runLauncher(signal: NodeJS.Signals, extraEnv: Record<string, stri
65
92
  return { consoleUrl, ...result, elapsedMs: Date.now() - started, stderr };
66
93
  }
67
94
 
95
+ // Runs the built launcher until it exits on its own, for the startup guards that must abort before a console URL is ever printed.
96
+ async function runLauncherToExit(extraEnv: Record<string, string>): Promise<LauncherExit> {
97
+ const agentDir = await makeTempDir("guard");
98
+ const child = spawn(process.execPath, [BIN], {
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ env: {
101
+ ...process.env,
102
+ PI_HARNESS_PORT: "0",
103
+ PI_AGENT_DIR: agentDir,
104
+ PI_HARNESS_PROVIDER: "anthropic",
105
+ PI_HARNESS_MODEL: "claude-sonnet-4-5",
106
+ PI_HARNESS_DISABLE_UPDATE_CHECK: "1",
107
+ ...extraEnv,
108
+ },
109
+ });
110
+ let stdout = "";
111
+ let stderr = "";
112
+ child.stdout.setEncoding("utf8");
113
+ child.stderr.setEncoding("utf8");
114
+ child.stdout.on("data", (chunk: string) => {
115
+ stdout += chunk;
116
+ });
117
+ child.stderr.on("data", (chunk: string) => {
118
+ stderr += chunk;
119
+ });
120
+ const timeout = setTimeout(() => child.kill("SIGKILL"), 30_000);
121
+ const code = await new Promise<number | null>((resolve) => child.once("exit", (exitCode) => resolve(exitCode)));
122
+ clearTimeout(timeout);
123
+ return { code, stdout, stderr };
124
+ }
125
+
126
+ // fetch() drops a caller-supplied Host header because "host" is a forbidden header name, so a request that has to carry a proxy hostname goes out over node:http instead.
127
+ function statusWithHostHeader(port: number, hostHeader: string): Promise<number> {
128
+ return new Promise((resolve, reject) => {
129
+ const outgoing = httpRequest({ host: "127.0.0.1", port, path: "/api/status", headers: { host: hostHeader } }, (response) => {
130
+ response.resume();
131
+ resolve(response.statusCode ?? 0);
132
+ });
133
+ outgoing.once("error", reject);
134
+ outgoing.end();
135
+ });
136
+ }
137
+
138
+ // Takes a loopback port from the kernel and releases it again, so a launcher run can be probed over HTTP without reading the console URL from stdout.
139
+ async function reserveLoopbackPort(): Promise<number> {
140
+ const server = createServer();
141
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
142
+ const address = server.address();
143
+ const port = typeof address === "object" && address !== null ? address.port : 0;
144
+ await new Promise<void>((resolve) => server.close(() => resolve()));
145
+ return port;
146
+ }
147
+
68
148
  describe("web launcher", () => {
69
149
  test("completes graceful shutdown on SIGINT and exits with code 130 instead of dying by signal", async () => {
70
150
  const run = await runLauncher("SIGINT");
@@ -87,4 +167,147 @@ describe("web launcher", () => {
87
167
  expect(run.consoleUrl).toMatch(/^http:\/\/\[::1\]:\d+$/u);
88
168
  expect(run.code).toBe(130);
89
169
  }, 30_000);
170
+
171
+ test("resolves a relative PI_AGENT_DIR against the working directory and ignores surrounding whitespace", async () => {
172
+ const workingDir = await makeTempDir("cwd");
173
+ const home = await makeTempDir("home");
174
+ await mkdir(join(workingDir, "agent-state"));
175
+
176
+ const run = await runLauncher("SIGINT", { PI_AGENT_DIR: " ./agent-state ", HOME: home }, workingDir);
177
+
178
+ expect(run.consoleUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/u);
179
+ expect(run.stderr).not.toContain("must be an absolute path");
180
+ // Booting writes the model store into the agent directory, so these two paths pin which base the relative value resolved against: the working directory, not the home directory.
181
+ expect(existsSync(join(workingDir, "agent-state", "models-store.json"))).toBe(true);
182
+ expect(existsSync(join(home, "agent-state"))).toBe(false);
183
+ expect(run.code).toBe(130);
184
+ }, 60_000);
185
+
186
+ test("treats a blank PI_AGENT_DIR as unset and falls back to the home agent directory", async () => {
187
+ const workingDir = await makeTempDir("cwd");
188
+ const home = await makeTempDir("home");
189
+ await mkdir(join(home, ".pi", "agent"), { recursive: true });
190
+
191
+ const run = await runLauncher("SIGINT", { PI_AGENT_DIR: " ", HOME: home }, workingDir);
192
+
193
+ expect(run.stderr).not.toContain("must be an absolute path");
194
+ expect(existsSync(join(home, ".pi", "agent", "models-store.json"))).toBe(true);
195
+ expect(existsSync(join(workingDir, ".pi"))).toBe(false);
196
+ expect(run.code).toBe(130);
197
+ }, 60_000);
198
+
199
+ test("keeps serving and shuts down gracefully when a closed stdout pipe breaks the console URL write", async () => {
200
+ const agentDir = await makeTempDir("epipe");
201
+ const port = await reserveLoopbackPort();
202
+ const child = spawn(process.execPath, [BIN], {
203
+ stdio: ["ignore", "pipe", "pipe"],
204
+ env: {
205
+ ...process.env,
206
+ PI_HARNESS_PORT: String(port),
207
+ PI_AGENT_DIR: agentDir,
208
+ PI_HARNESS_PROVIDER: "anthropic",
209
+ PI_HARNESS_MODEL: "claude-sonnet-4-5",
210
+ PI_HARNESS_DISABLE_UPDATE_CHECK: "1",
211
+ },
212
+ });
213
+ let stderr = "";
214
+ let exited = false;
215
+ child.stderr.setEncoding("utf8");
216
+ child.stderr.on("data", (chunk: string) => {
217
+ stderr += chunk;
218
+ });
219
+ const exit = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) =>
220
+ child.once("exit", (code, signal) => {
221
+ exited = true;
222
+ resolve({ code, signal });
223
+ }),
224
+ );
225
+ // Closing the read end of the pipe is what `pi-harness | head -1` does: the launcher's next write raises EPIPE on process.stdout.
226
+ child.stdout.destroy();
227
+
228
+ await expect
229
+ .poll(
230
+ async () => {
231
+ if (exited) return "exited";
232
+ try {
233
+ await fetch(`http://127.0.0.1:${String(port)}/`);
234
+ return "listening";
235
+ } catch {
236
+ return "starting";
237
+ }
238
+ },
239
+ { interval: 200, timeout: 40_000 },
240
+ )
241
+ .not.toBe("starting");
242
+ // The console URL is written shortly after the server starts listening, so wait past that point before deciding the process survived the broken pipe.
243
+ await delay(3_000);
244
+
245
+ expect(stderr).not.toContain("EPIPE");
246
+ expect(exited).toBe(false);
247
+ child.kill("SIGINT");
248
+ const result = await exit;
249
+ expect(result.code).toBe(130);
250
+ }, 60_000);
251
+ });
252
+
253
+ describe("web launcher startup guards", () => {
254
+ test("refuses a non-loopback PI_HARNESS_HOST while remote access is not opted in", async () => {
255
+ for (const remoteHost of ["0.0.0.0", "192.0.2.10"]) {
256
+ const run = await runLauncherToExit({ PI_HARNESS_HOST: remoteHost });
257
+
258
+ expect(run.stderr).toContain("Refusing non-loopback PI_HARNESS_HOST");
259
+ expect(run.stdout).not.toContain("Pi Harness web console:");
260
+ expect(run.code).not.toBe(0);
261
+ }
262
+ }, 60_000);
263
+
264
+ test("binds a host outside the loopback allowlist once PI_HARNESS_ALLOW_REMOTE opts in", async () => {
265
+ // "LOCALHOST" is not one of the literals the launcher treats as loopback, so it takes the opt-in path, yet getaddrinfo still resolves it to the loopback address: the bind succeeds without putting the console on the network.
266
+ const run = await runLauncher("SIGINT", { PI_HARNESS_HOST: "LOCALHOST", PI_HARNESS_ALLOW_REMOTE: "1" });
267
+
268
+ expect(run.stderr).not.toContain("Refusing non-loopback PI_HARNESS_HOST");
269
+ expect(run.consoleUrl).toMatch(/^http:\/\/LOCALHOST:\d+$/u);
270
+ expect(run.code).toBe(130);
271
+ }, 60_000);
272
+
273
+ test("hands an opted-in host to listen and reports the bind failure when no interface owns it", async () => {
274
+ // 192.0.2.10 is TEST-NET-1 and is assigned to no interface, so this pins that the guard let the host through: the run dies in listen rather than in the guard.
275
+ const run = await runLauncherToExit({ PI_HARNESS_HOST: "192.0.2.10", PI_HARNESS_ALLOW_REMOTE: "1" });
276
+
277
+ expect(run.stderr).not.toContain("Refusing non-loopback PI_HARNESS_HOST");
278
+ expect(run.stderr).toContain("EADDRNOTAVAIL");
279
+ expect(run.code).toBe(1);
280
+ }, 60_000);
281
+
282
+ test("accepts a proxy hostname in the Host header only when PI_HARNESS_ALLOWED_HOSTS names it", async () => {
283
+ const statuses: number[] = [];
284
+ const run = await runLauncher("SIGINT", { PI_HARNESS_ALLOWED_HOSTS: " proxy.internal.example , " }, undefined, async (consoleUrl) => {
285
+ const port = Number(new URL(consoleUrl).port);
286
+ statuses.push(await statusWithHostHeader(port, `proxy.internal.example:${String(port)}`));
287
+ statuses.push(await statusWithHostHeader(port, `other.internal.example:${String(port)}`));
288
+ });
289
+
290
+ expect(statuses).toEqual([200, 400]);
291
+ expect(run.code).toBe(130);
292
+ }, 60_000);
293
+
294
+ test("rejects a PI_HARNESS_ALLOWED_HOSTS entry that is not a bare hostname", async () => {
295
+ for (const entry of ["https://proxy.example", "proxy.example:8443", "proxy.example/app"]) {
296
+ const run = await runLauncherToExit({ PI_HARNESS_ALLOWED_HOSTS: entry });
297
+
298
+ expect(run.stderr).toContain("PI_HARNESS_ALLOWED_HOSTS entries must be bare hostnames");
299
+ expect(run.stdout).not.toContain("Pi Harness web console:");
300
+ expect(run.code).not.toBe(0);
301
+ }
302
+ }, 60_000);
303
+
304
+ test("rejects a PI_HARNESS_PORT that is not an integer inside the valid range", async () => {
305
+ for (const badPort of ["70000", "-1", "abc"]) {
306
+ const run = await runLauncherToExit({ PI_HARNESS_PORT: badPort });
307
+
308
+ expect(run.stderr).toContain("PI_HARNESS_PORT must be an integer between 0 and 65535");
309
+ expect(run.stdout).not.toContain("Pi Harness web console:");
310
+ expect(run.code).not.toBe(0);
311
+ }
312
+ }, 60_000);
90
313
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;;;;mBAmoBlC,OAAO;;AAHxB,wBA+2CE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;;;;mBAkqBlC,OAAO;;AAHxB,wBAg5CE"}
@@ -1,10 +1,10 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { mkdir, mkdtemp, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { SessionManager } from "@earendil-works/pi-coding-agent";
6
6
  import { atomicWriteFile } from "@pi-harness/core";
7
- import { MARKETPLACE_CAPABILITIES, MARKETPLACE_CATEGORIES, MARKETPLACE_PLUGINS, attachMarketplaceStatistics, createMarketplaceStatisticsLoader, paginateMarketplace, searchMarketplace, needsMarketplacePackageInstall, sortMarketplaceByRecommendation, } from "./marketplace.js";
7
+ import { MARKETPLACE_CAPABILITIES, MARKETPLACE_CATEGORIES, MARKETPLACE_PLUGINS, attachMarketplaceStatistics, createMarketplaceStatisticsLoader, paginateMarketplace, searchMarketplace, needsMarketplacePackageInstall, marketplaceNpmPackageName, sortMarketplaceByRecommendation, } from "./marketplace.js";
8
8
  import { parseGitWorktrees } from "./workspaces.js";
9
9
  const loadMarketplaceStatistics = createMarketplaceStatisticsLoader();
10
10
  const DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
@@ -25,18 +25,19 @@ function sendJson(response, status, payload) {
25
25
  response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "content-length": Buffer.byteLength(body) });
26
26
  response.end(body);
27
27
  }
28
+ // A multi-byte UTF-8 sequence can straddle a chunk boundary, so the raw bytes are collected and decoded once; decoding each chunk on its own would replace the split sequence with U+FFFD.
28
29
  async function bodyText(request, maxBytes = DEFAULT_BODY_LIMIT_BYTES) {
29
30
  const chunks = [];
30
31
  let length = 0;
31
32
  for await (const chunk of request) {
32
33
  const input = chunk;
33
- const value = typeof input === "string" ? input : Buffer.from(input).toString("utf8");
34
- length += Buffer.byteLength(value);
34
+ const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.isBuffer(input) ? input : Buffer.from(input);
35
+ length += buffer.length;
35
36
  if (length > maxBytes)
36
37
  throw new PayloadTooLargeError();
37
- chunks.push(value);
38
+ chunks.push(buffer);
38
39
  }
39
- return chunks.join("");
40
+ return Buffer.concat(chunks).toString("utf8");
40
41
  }
41
42
  function errorText(error) {
42
43
  return error instanceof Error ? error.message : String(error);
@@ -56,10 +57,16 @@ function jsonSafe(value, seen = new WeakSet()) {
56
57
  return Object.prototype.toString.call(value);
57
58
  if (seen.has(value))
58
59
  return "[Circular]";
60
+ // Only the ancestors of the current node stay marked: a sibling that repeats the same object (Pi pushes one AgentMessage instance into both the message list and its events) is legitimate data, not a cycle.
59
61
  seen.add(value);
60
- if (Array.isArray(value))
61
- return value.map((item) => jsonSafe(item, seen));
62
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item, seen)]));
62
+ try {
63
+ if (Array.isArray(value))
64
+ return value.map((item) => jsonSafe(item, seen));
65
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item, seen)]));
66
+ }
67
+ finally {
68
+ seen.delete(value);
69
+ }
63
70
  }
64
71
  function createStatus(services, events) {
65
72
  const activeModel = services.runtime.session.model ?? services.models.model;
@@ -306,6 +313,7 @@ function sanitizeLogValue(value, state, depth = 0) {
306
313
  return "[Max Depth]";
307
314
  if (state.seen.has(value))
308
315
  return "[Circular]";
316
+ // Unmarked once its subtree is done, so an object logged twice as a sibling is rendered twice instead of being mistaken for a cycle.
309
317
  state.seen.add(value);
310
318
  try {
311
319
  if (value instanceof Date)
@@ -339,6 +347,9 @@ function sanitizeLogValue(value, state, depth = 0) {
339
347
  catch {
340
348
  return "[Unavailable]";
341
349
  }
350
+ finally {
351
+ state.seen.delete(value);
352
+ }
342
353
  }
343
354
  function sanitizeLogArguments(args) {
344
355
  const sanitized = sanitizeLogValue(args, { remaining: loggerPanelNodeLimit, seen: new WeakSet() });
@@ -478,17 +489,18 @@ function gitTerminationMessage(command, termination) {
478
489
  ? `git ${command} timed out after ${GIT_TIMEOUT_MS / 1000} seconds`
479
490
  : `git ${command} produced more output than the buffer limit allows`;
480
491
  }
492
+ // `-z` is what makes the paths usable: without it git wraps any path holding a space, a quote or a non-ASCII byte in C-style quoting, and the quoted form is not a pathspec git will accept back on diff, add or ls-files.
481
493
  function gitStatus(cwd) {
482
494
  return new Promise((resolveStatus, rejectStatus) => {
483
- execFile("git", ["status", "--short", "--untracked-files=all"], { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 512 * 1024 }, (error, stdout, stderr) => {
495
+ execFile("git", ["status", "--short", "-z", "--untracked-files=all"], { cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: 512 * 1024 }, (error, stdout, stderr) => {
484
496
  if (error === null) {
485
497
  resolveStatus({ output: stdout, truncated: false });
486
498
  return;
487
499
  }
488
500
  const termination = gitTermination(error);
489
501
  if (termination === "output-limit") {
490
- // Keep the complete lines that arrived before the kill; the final line may have been cut mid-path.
491
- resolveStatus({ output: stdout.slice(0, stdout.lastIndexOf("\n") + 1), truncated: true });
502
+ // Keep the complete records that arrived before the kill; the final one may have been cut mid-path.
503
+ resolveStatus({ output: stdout.slice(0, stdout.lastIndexOf("\0") + 1), truncated: true });
492
504
  return;
493
505
  }
494
506
  if (termination === undefined && error.code === 128 && /not a git repository/i.test(stderr)) {
@@ -539,6 +551,26 @@ async function isDirectory(path) {
539
551
  return false;
540
552
  }
541
553
  }
554
+ async function isFile(path) {
555
+ try {
556
+ return (await stat(path)).isFile();
557
+ }
558
+ catch {
559
+ return false;
560
+ }
561
+ }
562
+ // The loader resolves a bare plugin specifier from the profile file upwards (`createRequire(profilePath).resolve(name)`), never from the shell the harness was started in, so a marketplace package has to land in the package that owns the profile or it installs successfully and then fails to load.
563
+ async function marketplaceInstallDirectory(configPath, fallback) {
564
+ let directory = dirname(resolve(configPath));
565
+ let parent = dirname(directory);
566
+ while (parent !== directory) {
567
+ if (await isFile(join(directory, "package.json")))
568
+ return directory;
569
+ directory = parent;
570
+ parent = dirname(directory);
571
+ }
572
+ return fallback;
573
+ }
542
574
  function pickDirectory() {
543
575
  return new Promise((resolvePath, reject) => {
544
576
  execFile("osascript", ["-e", 'POSIX path of (choose folder with prompt "选择 Pi 工作区")'], { timeout: 120_000, maxBuffer: 64 * 1024 }, (error, stdout, stderr) => {
@@ -989,15 +1021,17 @@ export default {
989
1021
  sendJson(response, 409, { error: "Plugin is already installed", plugin });
990
1022
  return;
991
1023
  }
992
- const packageJsonPath = join(services.launch.cwd, "package.json");
993
- const packageLockPath = join(services.launch.cwd, "package-lock.json");
1024
+ const installDirectory = await marketplaceInstallDirectory(configPath, services.launch.cwd);
1025
+ const packageJsonPath = join(installDirectory, "package.json");
1026
+ const packageLockPath = join(installDirectory, "package-lock.json");
994
1027
  const packageJsonBefore = await readFile(packageJsonPath, "utf8").catch(() => undefined);
995
1028
  const packageLockBefore = await readFile(packageLockPath, "utf8").catch(() => undefined);
996
1029
  let profileBefore;
997
1030
  let entryId;
998
1031
  try {
999
1032
  if (needsMarketplacePackageInstall(plugin)) {
1000
- await runProcess("npm", ["install", "--save-exact", "--package-lock=false", `${plugin.packageName}@${plugin.version}`], services.launch.cwd);
1033
+ const specifier = `${marketplaceNpmPackageName(plugin.packageName)}@${plugin.version}`;
1034
+ await runProcess("npm", ["install", "--save-exact", "--package-lock=false", specifier], installDirectory);
1001
1035
  }
1002
1036
  profileBefore = await appendMarketplaceProfile(configPath, plugin);
1003
1037
  if (!needsMarketplacePackageInstall(plugin)) {
@@ -1021,9 +1055,14 @@ export default {
1021
1055
  await loader.remove(entryId).catch(() => { });
1022
1056
  if (profileBefore !== undefined)
1023
1057
  await writeFile(configPath, profileBefore, "utf8").catch(() => { });
1024
- if (packageJsonBefore !== undefined)
1058
+ // A manifest npm created for this install is removed rather than left behind; restoring is only possible when one existed before.
1059
+ if (packageJsonBefore === undefined)
1060
+ await unlink(packageJsonPath).catch(() => { });
1061
+ else
1025
1062
  await writeFile(packageJsonPath, packageJsonBefore, "utf8").catch(() => { });
1026
- if (packageLockBefore !== undefined)
1063
+ if (packageLockBefore === undefined)
1064
+ await unlink(packageLockPath).catch(() => { });
1065
+ else
1027
1066
  await writeFile(packageLockPath, packageLockBefore, "utf8").catch(() => { });
1028
1067
  sendJson(response, 502, { error: errorText(error) });
1029
1068
  }
@@ -1125,8 +1164,9 @@ export default {
1125
1164
  }
1126
1165
  const profileEntryId = entry.options.id;
1127
1166
  const profileBefore = await readFile(configPath, "utf8");
1128
- const packageJsonPath = join(services.launch.cwd, "package.json");
1129
- const packageLockPath = join(services.launch.cwd, "package-lock.json");
1167
+ const installDirectory = await marketplaceInstallDirectory(configPath, services.launch.cwd);
1168
+ const packageJsonPath = join(installDirectory, "package.json");
1169
+ const packageLockPath = join(installDirectory, "package-lock.json");
1130
1170
  const packageJsonBefore = await readFile(packageJsonPath, "utf8").catch(() => undefined);
1131
1171
  const packageLockBefore = await readFile(packageLockPath, "utf8").catch(() => undefined);
1132
1172
  await entry.parent.remove(entry.options.id);
@@ -1134,7 +1174,7 @@ export default {
1134
1174
  try {
1135
1175
  await updateMarketplaceProfile(configPath, profileEntryId, { remove: true });
1136
1176
  if (needsMarketplacePackageInstall(plugin))
1137
- await runProcess("npm", ["uninstall", "--package-lock=false", plugin.packageName], services.launch.cwd);
1177
+ await runProcess("npm", ["uninstall", "--package-lock=false", marketplaceNpmPackageName(plugin.packageName)], installDirectory);
1138
1178
  sendJson(response, 200, { uninstalled: true, id: payload.id });
1139
1179
  }
1140
1180
  catch (error) {
@@ -1246,18 +1286,22 @@ export default {
1246
1286
  sendJson(response, 500, { error: errorText(error) });
1247
1287
  return;
1248
1288
  }
1249
- const items = status.output
1250
- .split("\n")
1251
- .map((line) => line.trimEnd())
1252
- .filter((line) => line.length > 0)
1253
- .map((line) => {
1254
- const status = line.slice(0, 2).trim() || "??";
1255
- return {
1256
- path: line.slice(3),
1257
- status,
1258
- label: status === "??" ? "untracked" : status.includes("D") ? "deleted" : status.includes("A") ? "added" : "modified",
1259
- };
1260
- });
1289
+ const records = status.output.split("\0");
1290
+ const items = [];
1291
+ for (let index = 0; index < records.length; index += 1) {
1292
+ const record = records[index] ?? "";
1293
+ if (record.length < 4)
1294
+ continue;
1295
+ const code = record.slice(0, 2).trim() || "??";
1296
+ // Rename and copy records carry the original path in a second NUL-terminated field, which is not a workspace change of its own.
1297
+ if (code.startsWith("R") || code.startsWith("C"))
1298
+ index += 1;
1299
+ items.push({
1300
+ path: record.slice(3),
1301
+ status: code,
1302
+ label: code === "??" ? "untracked" : code.includes("D") ? "deleted" : code.includes("A") ? "added" : "modified",
1303
+ });
1304
+ }
1261
1305
  sendJson(response, 200, { items, ...(status.truncated ? { truncated: true } : {}) });
1262
1306
  },
1263
1307
  });
@@ -1744,21 +1788,48 @@ export default {
1744
1788
  sendJson(response, 409, { error: "Cannot batch-delete the active session" });
1745
1789
  return;
1746
1790
  }
1747
- await mutateSessionMetadata(manager, context.logger, async (metadata) => {
1791
+ if (action === "delete") {
1792
+ // The unlinks run outside the metadata mutation: a failure halfway through must not discard the key removals for the files that are already gone from disk.
1793
+ const removed = [];
1794
+ const failed = [];
1748
1795
  for (const path of paths) {
1749
- if (action === "delete") {
1796
+ try {
1750
1797
  await unlink(path);
1751
- delete metadata[path];
1798
+ removed.push(path);
1752
1799
  }
1753
- else {
1754
- const current = metadata[path] ?? {};
1755
- metadata[path] = {
1756
- ...current,
1757
- ...(action === "archive" || action === "unarchive" ? { archived: action === "archive" } : {}),
1758
- ...(action === "pin" || action === "unpin" ? { pinned: action === "pin" } : {}),
1759
- };
1800
+ catch (error) {
1801
+ // A session another tab already deleted leaves nothing to unlink, but its metadata entry still has to go.
1802
+ if (error.code === "ENOENT")
1803
+ removed.push(path);
1804
+ else
1805
+ failed.push({ path, error: errorText(error) });
1760
1806
  }
1761
1807
  }
1808
+ await mutateSessionMetadata(manager, context.logger, (metadata) => {
1809
+ for (const path of removed)
1810
+ delete metadata[path];
1811
+ });
1812
+ if (failed.length > 0) {
1813
+ sendJson(response, 409, {
1814
+ error: `Some sessions could not be deleted: ${failed.map((item) => item.path).join(", ")}`,
1815
+ action,
1816
+ count: removed.length,
1817
+ failed,
1818
+ });
1819
+ return;
1820
+ }
1821
+ sendJson(response, 200, { action, count: removed.length });
1822
+ return;
1823
+ }
1824
+ await mutateSessionMetadata(manager, context.logger, (metadata) => {
1825
+ for (const path of paths) {
1826
+ const current = metadata[path] ?? {};
1827
+ metadata[path] = {
1828
+ ...current,
1829
+ ...(action === "archive" || action === "unarchive" ? { archived: action === "archive" } : {}),
1830
+ ...(action === "pin" || action === "unpin" ? { pinned: action === "pin" } : {}),
1831
+ };
1832
+ }
1762
1833
  });
1763
1834
  sendJson(response, 200, { action, count: paths.length });
1764
1835
  }