@youweichen/pi-harness 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/pi-harness.mjs +1819 -0
  4. package/deploy/com.youweichen.pi-harness.plist +48 -0
  5. package/deploy/nginx-subpath.conf +88 -0
  6. package/deploy/pi-harness-task.xml +71 -0
  7. package/deploy/pi-harness.service +31 -0
  8. package/dist/server/agent-service.js +3051 -0
  9. package/dist/server/app-version.js +19 -0
  10. package/dist/server/attachments.js +526 -0
  11. package/dist/server/auth-file.js +61 -0
  12. package/dist/server/bg-servers.js +154 -0
  13. package/dist/server/client-eviction.js +19 -0
  14. package/dist/server/client-id.js +9 -0
  15. package/dist/server/client-state.js +202 -0
  16. package/dist/server/codemode-image-routes.js +36 -0
  17. package/dist/server/codemode-presentation.js +19 -0
  18. package/dist/server/component-updates.js +135 -0
  19. package/dist/server/context-breakdown.js +56 -0
  20. package/dist/server/control-socket.js +177 -0
  21. package/dist/server/conversation.js +65 -0
  22. package/dist/server/dir-browser.js +77 -0
  23. package/dist/server/editor-snapshot.js +38 -0
  24. package/dist/server/ensure-bash.js +80 -0
  25. package/dist/server/extension-display.js +23 -0
  26. package/dist/server/extensions-catalog.js +64 -0
  27. package/dist/server/extensions-model.js +46 -0
  28. package/dist/server/extensions-routes.js +255 -0
  29. package/dist/server/extensions-worker.js +187 -0
  30. package/dist/server/files-service.js +795 -0
  31. package/dist/server/index.js +1029 -0
  32. package/dist/server/markdown-images.js +42 -0
  33. package/dist/server/model-admin.js +801 -0
  34. package/dist/server/model-config-merge.js +32 -0
  35. package/dist/server/native-mcp-config.js +103 -0
  36. package/dist/server/native-mcp-presentation.js +12 -0
  37. package/dist/server/native-prompt-template.js +12 -0
  38. package/dist/server/native-tools.js +32 -0
  39. package/dist/server/node-sources.js +142 -0
  40. package/dist/server/node-workbench.js +668 -0
  41. package/dist/server/origin-policy.js +50 -0
  42. package/dist/server/patch-node-pty.js +96 -0
  43. package/dist/server/pi-environment.js +203 -0
  44. package/dist/server/plan/extension.js +74 -0
  45. package/dist/server/plan/progress.js +90 -0
  46. package/dist/server/plan/settings.js +41 -0
  47. package/dist/server/plan/state.js +113 -0
  48. package/dist/server/plugin-facilities.js +304 -0
  49. package/dist/server/plugin-updater.js +245 -0
  50. package/dist/server/plugins.js +712 -0
  51. package/dist/server/private-file.js +28 -0
  52. package/dist/server/process-utils.js +112 -0
  53. package/dist/server/prompt-delivery.js +80 -0
  54. package/dist/server/protocol-version.js +11 -0
  55. package/dist/server/protocol.js +6 -0
  56. package/dist/server/provider-auth.js +99 -0
  57. package/dist/server/query-cache.js +45 -0
  58. package/dist/server/recovery-state.js +37 -0
  59. package/dist/server/scm.js +459 -0
  60. package/dist/server/security-headers.js +24 -0
  61. package/dist/server/serialize.js +254 -0
  62. package/dist/server/session-file-read.js +225 -0
  63. package/dist/server/session-record-worker.js +21 -0
  64. package/dist/server/session-reload.js +13 -0
  65. package/dist/server/session-tree-controller.js +250 -0
  66. package/dist/server/session-tree.js +191 -0
  67. package/dist/server/settings-service.js +53 -0
  68. package/dist/server/skills-service.js +87 -0
  69. package/dist/server/slash-commands.js +280 -0
  70. package/dist/server/sqlite-preview.js +58 -0
  71. package/dist/server/sqlite-query.js +67 -0
  72. package/dist/server/sqlite-worker.js +12 -0
  73. package/dist/server/system-prompt-files.js +158 -0
  74. package/dist/server/system-prompt-routes.js +40 -0
  75. package/dist/server/system-prompt-view.js +49 -0
  76. package/dist/server/task-progress.js +233 -0
  77. package/dist/server/terminals.js +833 -0
  78. package/dist/server/text-sniff.js +269 -0
  79. package/dist/server/thinking-timing.js +91 -0
  80. package/dist/server/token-auth.js +37 -0
  81. package/dist/server/tool-output.js +97 -0
  82. package/dist/server/tool-text-incidents.js +82 -0
  83. package/dist/server/tool-text.js +67 -0
  84. package/dist/server/uploads.js +112 -0
  85. package/dist/server/user-attachments.js +96 -0
  86. package/dist/server/webui-context.js +257 -0
  87. package/dist/server/wiki-links.js +136 -0
  88. package/dist/server/wiki-pdf-worker.js +23 -0
  89. package/dist/server/wiki-pdf.js +36 -0
  90. package/dist/server/wiki-routes.js +177 -0
  91. package/dist/server/wiki-service.js +511 -0
  92. package/electron/main.mjs +607 -0
  93. package/extensions/webui.ts +192 -0
  94. package/package.json +122 -4
  95. package/web/dist/apple-touch-icon.png +0 -0
  96. package/web/dist/assets/NodeWorkbench-C4U1PldE.js +3 -0
  97. package/web/dist/assets/TerminalPanel-CWh6vw_N.js +2 -0
  98. package/web/dist/assets/abnfDiagram-VCTEODGH-D5Ti0Kpa.js +1 -0
  99. package/web/dist/assets/arc-DKJilLd6.js +1 -0
  100. package/web/dist/assets/architectureDiagram-5GKGNRK7-Cf0w38g-.js +36 -0
  101. package/web/dist/assets/blockDiagram-I7D4REHJ-BOTLWLIL.js +129 -0
  102. package/web/dist/assets/c4Diagram-7LVT6UL2-CLCCoJs3.js +38 -0
  103. package/web/dist/assets/channel-D8Hd7T4P.js +1 -0
  104. package/web/dist/assets/chunk-2Q5K7J3B-BHjga2Ge.js +1 -0
  105. package/web/dist/assets/chunk-5VM5RSS4-CQJ5VfDZ.js +15 -0
  106. package/web/dist/assets/chunk-F27PBJKO-C67qoPJH.js +1 -0
  107. package/web/dist/assets/chunk-IMKFNOWR-CgXXGwI8.js +231 -0
  108. package/web/dist/assets/chunk-JWPE2WC7-D0RPyByP.js +1 -0
  109. package/web/dist/assets/chunk-POPQ4Y6H-BeYC9s9x.js +1 -0
  110. package/web/dist/assets/chunk-SVP7TREG-eQPl4UBQ.js +88 -0
  111. package/web/dist/assets/chunk-TICWLB2K-DT73arPU.js +206 -0
  112. package/web/dist/assets/chunk-XXDRQBXY-B4FwQcP-.js +1 -0
  113. package/web/dist/assets/classDiagram-ZZMXUADV-B_btgYRB.js +1 -0
  114. package/web/dist/assets/classDiagram-v2-VYDZK3BY-B_btgYRB.js +1 -0
  115. package/web/dist/assets/cose-bilkent-JH36ORCC-Bnn5nlQN.js +1 -0
  116. package/web/dist/assets/cynefin-OW5HDTMX-CStWX9kE.js +178 -0
  117. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-BMVHC03f.js +62 -0
  118. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
  119. package/web/dist/assets/dagre-GXQ25YYZ-ClSaO4ml.js +4 -0
  120. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  121. package/web/dist/assets/diagram-S7CK7UJ4-C43LL08C.js +30 -0
  122. package/web/dist/assets/diagram-UQ7AKVKN-BPVn_ySW.js +41 -0
  123. package/web/dist/assets/diagram-VSXAHHWV-C34FFhRi.js +3 -0
  124. package/web/dist/assets/diagram-VX7I27RA-BRk4bncf.js +24 -0
  125. package/web/dist/assets/diagram-Z3DM3KII-O5hYPLEd.js +24 -0
  126. package/web/dist/assets/ebnfDiagram-PWID7BFC-DVGEBm99.js +1 -0
  127. package/web/dist/assets/erDiagram-RLTQ6QDP-M-BLpTEV.js +99 -0
  128. package/web/dist/assets/flowDiagram-HODETNUW-BwdLopEx.js +168 -0
  129. package/web/dist/assets/ganttDiagram-EL5Y4UJY-DYxx2ppA.js +292 -0
  130. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-DUU0wMYB.js +106 -0
  131. package/web/dist/assets/index-CI2KfVSr.css +10 -0
  132. package/web/dist/assets/index-DQcpggFU.js +275 -0
  133. package/web/dist/assets/infoDiagram-27XIBGKW-CFaW5C0C.js +2 -0
  134. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  135. package/web/dist/assets/ishikawaDiagram-5VMMS53U--ENP1MTB.js +70 -0
  136. package/web/dist/assets/journeyDiagram-3NMN7TZE-DgnAzSTj.js +139 -0
  137. package/web/dist/assets/kanban-definition-UXKFOSKX-_STZN1vU.js +89 -0
  138. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  139. package/web/dist/assets/layout-CkUClkXt.js +1 -0
  140. package/web/dist/assets/linear-sYHiUOxa.js +1 -0
  141. package/web/dist/assets/markdown-vNTCZJ6d.js +51 -0
  142. package/web/dist/assets/mermaid.core-B8fKGWsc.js +312 -0
  143. package/web/dist/assets/mindmap-definition-YA3MSWOX-DP0BYOEx.js +96 -0
  144. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  145. package/web/dist/assets/pegDiagram-XKGWAZYB-DkWTmZre.js +1 -0
  146. package/web/dist/assets/pieDiagram-E7YTZNPT-DfZgMV4u.js +39 -0
  147. package/web/dist/assets/quadrantDiagram-AXDQQJYC-Djl4TT7q.js +7 -0
  148. package/web/dist/assets/railroadDiagram-O6MQD6OU-DbKF6zXn.js +1 -0
  149. package/web/dist/assets/react-BNj3n7jX.js +24 -0
  150. package/web/dist/assets/requirementDiagram-BXWQKSXE-idola5qb.js +84 -0
  151. package/web/dist/assets/sankeyDiagram-P5KCCOFB-KA0hQOle.js +40 -0
  152. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BXSKvxzP.js +162 -0
  153. package/web/dist/assets/sizeCapture-INFHLROL-C_Ewv3S5.js +1 -0
  154. package/web/dist/assets/stateDiagram-D77RDMKH-DIf5X8oo.js +1 -0
  155. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DYwe_2mB.js +1 -0
  156. package/web/dist/assets/swimlanes-42K2YHIH-Cx_Rc8M2.js +1 -0
  157. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-52Y8tgRV.js +8 -0
  158. package/web/dist/assets/timeline-definition-24CTP7MA-BRrDhWVz.js +120 -0
  159. package/web/dist/assets/vennDiagram-4TSXK5OY-Ad6G0YwD.js +34 -0
  160. package/web/dist/assets/wardleyDiagram-VM6X3IG4-X6-ENpSi.js +78 -0
  161. package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
  162. package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
  163. package/web/dist/assets/xychartDiagram-S5SC5T6Z-DhegNS6d.js +7 -0
  164. package/web/dist/brand-mark.svg +5 -0
  165. package/web/dist/favicon-32.png +0 -0
  166. package/web/dist/favicon.svg +5 -0
  167. package/web/dist/fonts/WIKI-FONTS.md +5 -0
  168. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  169. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  170. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  171. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  172. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  173. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  174. package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
  175. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  176. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  177. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  178. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  179. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  180. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  181. package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  182. package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  183. package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  184. package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  185. package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  186. package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  187. package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  188. package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  189. package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  190. package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  191. package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
  192. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  193. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  194. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  195. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  196. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  197. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  198. package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
  199. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  200. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  201. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  202. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  203. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  204. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  205. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  206. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  207. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  208. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  209. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  210. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  211. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  212. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  213. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  214. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  215. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  216. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  217. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  218. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  219. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  220. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  221. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  222. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  223. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  224. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  225. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  226. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  227. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  228. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  229. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  230. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  231. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  232. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  233. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  234. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  235. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  236. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  237. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  238. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  239. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  240. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  241. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  242. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  243. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  244. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  245. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  246. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  247. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  248. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  249. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  250. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  251. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  252. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  253. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  254. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  255. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  256. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  257. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  258. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  259. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  260. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  261. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  262. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  263. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  264. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  265. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  266. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  267. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  268. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  269. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  270. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  271. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  272. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  273. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  274. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  275. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  276. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  277. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  278. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  279. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  280. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  281. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  282. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  283. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  284. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  285. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  286. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  287. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  288. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  289. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  290. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  291. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  292. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  293. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  294. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  295. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  296. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  297. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  298. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  299. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  300. package/web/dist/fonts/source-serif-4/LICENSE +93 -0
  301. package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  302. package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  303. package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  304. package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  305. package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  306. package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  307. package/web/dist/icon/1a-mark.svg +1 -0
  308. package/web/dist/icon.ico +0 -0
  309. package/web/dist/index.html +18 -0
  310. package/web/public/apple-touch-icon.png +0 -0
  311. package/web/public/brand-mark.svg +5 -0
  312. package/web/public/favicon-32.png +0 -0
  313. package/web/public/favicon.svg +5 -0
  314. package/web/public/fonts/WIKI-FONTS.md +5 -0
  315. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  316. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  317. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  318. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  319. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  320. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  321. package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
  322. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  323. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  324. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  325. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  326. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  327. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  328. package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  329. package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  330. package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  331. package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  332. package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  333. package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  334. package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  335. package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  336. package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  337. package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  338. package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
  339. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  340. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  341. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  342. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  343. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  344. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  345. package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
  346. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  347. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  348. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  349. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  350. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  351. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  352. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  353. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  354. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  355. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  356. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  357. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  358. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  359. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  360. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  361. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  362. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  363. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  364. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  365. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  366. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  367. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  368. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  369. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  370. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  371. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  372. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  373. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  374. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  375. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  376. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  377. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  378. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  379. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  380. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  381. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  382. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  383. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  384. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  385. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  386. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  387. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  388. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  389. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  390. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  391. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  392. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  393. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  394. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  395. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  396. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  397. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  398. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  399. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  400. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  401. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  402. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  403. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  404. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  405. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  406. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  407. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  408. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  409. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  410. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  411. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  412. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  413. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  414. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  415. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  416. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  417. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  418. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  419. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  420. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  421. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  422. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  423. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  424. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  425. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  426. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  427. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  428. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  429. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  430. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  431. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  432. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  433. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  434. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  435. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  436. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  437. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  438. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  439. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  440. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  441. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  442. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  443. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  444. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  445. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  446. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  447. package/web/public/fonts/source-serif-4/LICENSE +93 -0
  448. package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  449. package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  450. package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  451. package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  452. package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  453. package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  454. package/web/public/icon/1a-mark.svg +1 -0
  455. package/web/public/icon.ico +0 -0
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3
+ function text(v, name, max) {
4
+ if (typeof v !== "string" || !v.trim() || v.length > max)
5
+ throw new Error(`${name} must be non-empty and at most ${max} characters`);
6
+ return v;
7
+ }
8
+ export function editableState(value) {
9
+ if (!object(value))
10
+ throw new Error("Submit a complete plan object");
11
+ const title = text(value.title, "title", 80), completionCriteria = text(value.completionCriteria, "completionCriteria", 240);
12
+ if (!["active", "completed", "cancelled", "failed"].includes(String(value.status)))
13
+ throw new Error("Invalid plan status");
14
+ if (!Array.isArray(value.steps) || value.steps.length < 1 || value.steps.length > 16)
15
+ throw new Error("steps must contain 1–16 items");
16
+ const steps = value.steps.map(step => {
17
+ if (!object(step))
18
+ throw new Error("Invalid step");
19
+ return { id: text(step.id, "step.id", 64), title: text(step.title, "step.title", 80), ...(step.detail !== undefined ? { detail: text(step.detail, "step.detail", 500) } : {}) };
20
+ });
21
+ const ids = new Set(steps.map(step => step.id));
22
+ if (ids.size !== steps.length)
23
+ throw new Error("Step IDs must be unique");
24
+ if (!Array.isArray(value.completedStepIds) || value.completedStepIds.some(id => typeof id !== "string" || !ids.has(id)) || new Set(value.completedStepIds).size !== value.completedStepIds.length)
25
+ throw new Error("completedStepIds must be unique existing step IDs");
26
+ const completedStepIds = value.completedStepIds;
27
+ if (value.currentStepId !== null && (typeof value.currentStepId !== "string" || !ids.has(value.currentStepId) || completedStepIds.includes(value.currentStepId)))
28
+ throw new Error("currentStepId must be null or an incomplete existing step ID");
29
+ if (value.status !== "active" && value.currentStepId !== null)
30
+ throw new Error("Terminal plans must have currentStepId: null");
31
+ if (value.status === "completed" && completedStepIds.length !== steps.length)
32
+ throw new Error("Completed plans must complete every step");
33
+ return { title, completionCriteria, status: value.status, steps, completedStepIds: [...completedStepIds], currentStepId: value.currentStepId,
34
+ ...(value.changeSummary !== undefined ? { changeSummary: text(value.changeSummary, "changeSummary", 240) } : {}) };
35
+ }
36
+ export function changesBetween(before, after) {
37
+ const changes = [];
38
+ for (const [index, step] of after.steps.entries()) {
39
+ const oldIndex = before?.steps.findIndex(old => old.id === step.id) ?? -1;
40
+ const old = before?.steps[oldIndex];
41
+ if (!old)
42
+ changes.push({ kind: "added", stepId: step.id, title: step.title, position: index + 1 });
43
+ else if (old.title !== step.title || old.detail !== step.detail || oldIndex !== index)
44
+ changes.push({ kind: "updated", stepId: step.id, title: step.title, position: index + 1 });
45
+ if (after.completedStepIds.includes(step.id) && !before?.completedStepIds.includes(step.id))
46
+ changes.push({ kind: "completed", stepId: step.id, title: step.title, position: index + 1 });
47
+ else if (after.currentStepId === step.id && before?.currentStepId !== step.id)
48
+ changes.push({ kind: "started", stepId: step.id, title: step.title, position: index + 1 });
49
+ else if (old && !after.completedStepIds.includes(step.id) && after.currentStepId !== step.id && (before?.completedStepIds.includes(step.id) || before?.currentStepId === step.id))
50
+ changes.push({ kind: "pending", stepId: step.id, title: step.title, position: index + 1 });
51
+ }
52
+ for (const step of before?.steps ?? [])
53
+ if (!after.steps.some(next => next.id === step.id))
54
+ changes.push({ kind: "removed", stepId: step.id, title: step.title });
55
+ if (before && before.status !== after.status)
56
+ changes.push({ kind: "status", status: after.status });
57
+ return changes;
58
+ }
59
+ export function transition(value, current, newId = randomUUID) {
60
+ if (!object(value) || !["create", "update"].includes(String(value.action)))
61
+ throw new Error("action must be create or update");
62
+ if (value.action === "update") {
63
+ const reason = !current ? "No plan exists on this branch" : current.status !== "active" ? "The latest plan has ended" : value.planId !== current.planId ? "planId does not match this branch's latest plan" : value.expectedRevision !== current.revision ? "expectedRevision is stale" : undefined;
64
+ if (reason)
65
+ throw new Error(`${reason}. ${!current || current.status !== "active" ? "Use create for a new plan." : "Use update with the current planId and expectedRevision and submit the full corrected plan."}\nCurrent editable state: ${current ? JSON.stringify({ action: current.status === "active" ? "update" : "create", planId: current.planId, expectedRevision: current.revision, ...editableState(current) }) : "none; use create"}`);
66
+ }
67
+ const state = editableState(value);
68
+ const changes = changesBetween(value.action === "update" ? current : undefined, state);
69
+ if (value.action === "create" && current?.status === "active")
70
+ changes.unshift({ kind: "replaced", planId: current.planId, revision: current.revision, reason: "Replaced by a newly created plan" });
71
+ return { schemaVersion: 3, planId: value.action === "create" ? newId() : current.planId, revision: value.action === "create" ? 1 : current.revision + 1, ...state, changes };
72
+ }
73
+ /** Validate and copy only known fields. Never forward arbitrary extension details. */
74
+ export function planSnapshot(value) {
75
+ try {
76
+ if (!object(value) || value.schemaVersion !== 3 || typeof value.planId !== "string" || !value.planId || value.planId.length > 128 || !Number.isSafeInteger(value.revision) || Number(value.revision) < 1 || !Array.isArray(value.changes) || value.changes.length > 64)
77
+ return;
78
+ const state = editableState(value);
79
+ const changes = value.changes.map(change => {
80
+ if (!object(change))
81
+ throw new Error("Invalid change");
82
+ if (change.kind === "replaced") {
83
+ if (!Number.isSafeInteger(change.revision) || Number(change.revision) < 1)
84
+ throw new Error("Invalid replacement revision");
85
+ return { kind: "replaced", planId: text(change.planId, "planId", 128), revision: Number(change.revision), reason: text(change.reason, "reason", 240) };
86
+ }
87
+ if (change.kind === "status" && ["active", "completed", "cancelled", "failed"].includes(String(change.status)))
88
+ return { kind: "status", status: change.status };
89
+ if (!["added", "updated", "removed", "completed", "started", "pending"].includes(String(change.kind)))
90
+ throw new Error("Invalid change kind");
91
+ if (change.position !== undefined && (!Number.isInteger(change.position) || Number(change.position) < 1 || Number(change.position) > 16))
92
+ throw new Error("Invalid position");
93
+ return { kind: change.kind, stepId: text(change.stepId, "stepId", 64), title: text(change.title, "title", 80), ...(change.position !== undefined ? { position: Number(change.position) } : {}) };
94
+ });
95
+ return { schemaVersion: 3, planId: value.planId, revision: Number(value.revision), ...state, changes };
96
+ }
97
+ catch {
98
+ return;
99
+ }
100
+ }
101
+ export function snapshotFromDetails(details) {
102
+ return object(details) ? planSnapshot(details.planSnapshot) : undefined;
103
+ }
104
+ export function latestPlan(branch) {
105
+ for (let i = branch.length - 1; i >= 0; i--) {
106
+ const entry = branch[i];
107
+ if (entry.type !== "message" || entry.message.role !== "toolResult" || entry.message.toolName !== "plan" || entry.message.isError)
108
+ continue;
109
+ const snapshot = snapshotFromDetails(entry.message.details);
110
+ if (snapshot)
111
+ return { snapshot, entryId: entry.id, toolCallId: entry.message.toolCallId };
112
+ }
113
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * 插件宿主设施:插件私有 KV 存储 + 加密 secrets,从 plugins.ts 抽出的纯设施。
3
+ *
4
+ * storage —— <pluginDir>/storage.json 单文件 JSON KV:
5
+ * - 全内存缓存、写入 tmp+rename 原子落盘(同 client-state.ts 的做法);
6
+ * - 供插件存非敏感配置(窗口布局、上次选中项…),替代各家手搓的
7
+ * read/write config.json 样板;
8
+ * - 生命周期跟插件目录绑定(uninstall 即删除),跨升级保留。
9
+ *
10
+ * secrets —— AES-256-GCM 加密的机密存储(密码/API key/token):
11
+ * - 密钥文件 <dataDir>/secrets.key(随机 32 字节,首次生成;chmod 0600 仅对
12
+ * POSIX 有意义,Windows 上 NTFS 权限继承用户目录默认 ACL);
13
+ * - 密文文件随插件目录 <pluginDir>/secrets.bin——拷到别的机器因无密钥解不开
14
+ * (fail closed);卸载插件即连密文一起删除;
15
+ * - 威胁模型:防「 casually 复制/查看文件」(混淆级保护)与「密文外泄」,
16
+ * 不能防同一用户账号下的完整进程妥协——本地个人工具的合理折衷。
17
+ */
18
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
19
+ import { writeFileAtomic } from "./private-file.js";
20
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
21
+ import { readFile as fspReadFile, readdir as fspReaddir, rm as fspRm, mkdir as fspMkdir, writeFile as fspWriteFile } from "node:fs/promises";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import { createRequire } from "node:module";
24
+ import { spawnSync } from "node:child_process";
25
+ /** tmp+rename 原子写(错误由调用方隔离——插件设施的 IO 一律尽力而为)。 */
26
+ function atomicWrite(file, data, mode) {
27
+ if (mode === undefined) {
28
+ mkdirSync(dirname(file), { recursive: true });
29
+ const tmp = `${file}.tmp-${process.pid}`;
30
+ writeFileSync(tmp, data);
31
+ renameSync(tmp, file);
32
+ return;
33
+ }
34
+ writeFileAtomic(file, data, { mode });
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // storage
38
+ // ---------------------------------------------------------------------------
39
+ /** 每插件的 JSON 文件 KV。所有方法同步(数据量小,避免并发写乱序)。 */
40
+ export class PluginStorage {
41
+ file;
42
+ cache;
43
+ constructor(file) {
44
+ this.file = file;
45
+ }
46
+ load() {
47
+ if (this.cache)
48
+ return this.cache;
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(this.file, "utf8"));
51
+ this.cache = parsed && typeof parsed === "object" ? parsed : {};
52
+ }
53
+ catch {
54
+ this.cache = {}; // 不存在/损坏 = 空表(损坏不致命,重新积累)
55
+ }
56
+ return this.cache;
57
+ }
58
+ get(key, fallback) {
59
+ const v = this.load()[key];
60
+ return v === undefined ? fallback : v;
61
+ }
62
+ all() {
63
+ return { ...this.load() };
64
+ }
65
+ set(key, value) {
66
+ if (!key)
67
+ throw new Error("storage.set: key 不能为空");
68
+ const store = this.load();
69
+ store[key] = value;
70
+ try {
71
+ atomicWrite(this.file, JSON.stringify(store), 0o600);
72
+ }
73
+ catch (err) {
74
+ console.error(`[plugin-storage] 写入失败 (${this.file}):`, err);
75
+ }
76
+ }
77
+ delete(key) {
78
+ const store = this.load();
79
+ if (!(key in store))
80
+ return;
81
+ delete store[key];
82
+ try {
83
+ atomicWrite(this.file, JSON.stringify(store), 0o600);
84
+ }
85
+ catch (err) {
86
+ console.error(`[plugin-storage] 写入失败 (${this.file}):`, err);
87
+ }
88
+ }
89
+ }
90
+ function seal(key, plaintext) {
91
+ const iv = randomBytes(12);
92
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
93
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
94
+ return { iv: iv.toString("hex"), tag: cipher.getAuthTag().toString("hex"), ct: ct.toString("hex") };
95
+ }
96
+ function unseal(key, blob) {
97
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(blob.iv, "hex"));
98
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
99
+ return Buffer.concat([decipher.update(Buffer.from(blob.ct, "hex")), decipher.final()]).toString("utf8");
100
+ }
101
+ /** 读或创建全局密钥文件(懒加载一次)。 */
102
+ function loadOrCreateKey(dataDir) {
103
+ const keyFile = join(dataDir, "secrets.key");
104
+ try {
105
+ if (existsSync(keyFile)) {
106
+ const encoded = readFileSync(keyFile, "utf8").trim();
107
+ if (/^[a-f\d]{64}$/i.test(encoded))
108
+ return Buffer.from(encoded, "hex");
109
+ throw new Error("invalid secrets key");
110
+ }
111
+ }
112
+ catch {
113
+ /* fallthrough → regenerate */
114
+ }
115
+ const key = randomBytes(32);
116
+ atomicWrite(keyFile, `${key.toString("hex")}\n`, 0o600); // 创建即 0600,没有先 0644 再 chmod 的窗口
117
+ return key;
118
+ }
119
+ /** 每插件的加密 KV。所有方法同步;任何读写失败都静默回退(机密丢失优于崩进程)。 */
120
+ export class PluginSecrets {
121
+ store;
122
+ file;
123
+ constructor(dataDir, pluginDir) {
124
+ this.file = join(pluginDir, "secrets.bin");
125
+ this.key = PluginSecrets.keyFor(dataDir);
126
+ }
127
+ key;
128
+ static keys = new Map();
129
+ /** 按 dataDir 惰性生成/复用密钥(同进程内共享,避免重复 IO)。 */
130
+ static keyFor(dataDir) {
131
+ let k = PluginSecrets.keys.get(dataDir);
132
+ if (!k) {
133
+ k = loadOrCreateKey(dataDir);
134
+ PluginSecrets.keys.set(dataDir, k);
135
+ }
136
+ return k;
137
+ }
138
+ load() {
139
+ if (this.store)
140
+ return this.store;
141
+ try {
142
+ const parsed = JSON.parse(readFileSync(this.file, "utf8"));
143
+ this.store =
144
+ parsed && parsed.v === 1 && parsed.items && typeof parsed.items === "object"
145
+ ? parsed
146
+ : { v: 1, items: {} };
147
+ }
148
+ catch {
149
+ this.store = { v: 1, items: {} };
150
+ }
151
+ return this.store;
152
+ }
153
+ set(name, value) {
154
+ if (!name)
155
+ throw new Error("secrets.set: name 不能为空");
156
+ const s = this.load();
157
+ s.items[name] = seal(this.key, value);
158
+ try {
159
+ atomicWrite(this.file, JSON.stringify(s), 0o600);
160
+ }
161
+ catch (err) {
162
+ console.error("[plugin-secrets] 写入失败:", err);
163
+ }
164
+ }
165
+ get(name) {
166
+ const blob = this.load().items[name];
167
+ if (!blob)
168
+ return undefined;
169
+ try {
170
+ return unseal(this.key, blob);
171
+ }
172
+ catch {
173
+ return undefined; // 换机器 / 密钥轮换 → 解不开返回空(fail closed)
174
+ }
175
+ }
176
+ has(name) {
177
+ return name in this.load().items;
178
+ }
179
+ delete(name) {
180
+ const s = this.load();
181
+ if (!(name in s.items))
182
+ return;
183
+ delete s.items[name];
184
+ try {
185
+ atomicWrite(this.file, JSON.stringify(s), 0o600);
186
+ }
187
+ catch (err) {
188
+ console.error("[plugin-secrets] 写入失败:", err);
189
+ }
190
+ }
191
+ list() {
192
+ return Object.keys(this.load().items);
193
+ }
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // deps(宿主代插件自动补装运行时依赖)
197
+ // ---------------------------------------------------------------------------
198
+ const DEP_TIMEOUT_MS = 180_000; // 慢网安装兜底(含第一次拉取包元数据)
199
+ /** 从插件目录出发能否解析到这个模块(模拟插件自身 import() 的查找链)。 */
200
+ export function isDepAvailable(pluginDir, spec) {
201
+ try {
202
+ createRequire(join(pluginDir, "index.mjs")).resolve(spec);
203
+ return true;
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
209
+ const depInstallLocks = new Map();
210
+ /** 确保依赖就绪:先逐个解析,缺了才一次性 `npm install` 补装,装完复查。
211
+ * 返回 true = 全部可用;false = 安装失败或超时。同目录并发调用单飞合并。
212
+ *
213
+ * 这是 webmail / db-client / vscode-editor 三家手搓 ensureXxxMod 的上收——
214
+ * 之前每家都自己拼 spawn 参数、自己处理 win32 的 npm.cmd、自己等 install 完成。 */
215
+ export function ensurePluginDeps(pluginDir, specs, onProgress) {
216
+ if (specs.length === 0)
217
+ return Promise.resolve(true);
218
+ const missing = specs.filter((s) => !isDepAvailable(pluginDir, s));
219
+ if (missing.length === 0)
220
+ return Promise.resolve(true);
221
+ const lockKey = join(pluginDir, missing.sort().join("|"));
222
+ const inflight = depInstallLocks.get(lockKey);
223
+ if (inflight)
224
+ return inflight;
225
+ const run = async () => {
226
+ // 无 package.json 时 npm 会沿目录树向上找最近一个,可能把依赖装进父目录——
227
+ // 先落一个最小 package.json 钉住安装位置。
228
+ if (!existsSync(join(pluginDir, "package.json"))) {
229
+ try {
230
+ atomicWrite(join(pluginDir, "package.json"), JSON.stringify({ name: "plugin-runtime-deps", private: true }, null, 2));
231
+ }
232
+ catch { }
233
+ }
234
+ onProgress?.(`正在安装依赖:${missing.join(", ")}…(首次约需几分钟)`);
235
+ // win32 的 npm 是 .cmd——spawnSync 直接跑会被 EINVAL 拒绝,必须走 shell;
236
+ // posix 不用 shell(路径不含空格假设成立,与宿主其它 spawn 一致)。
237
+ const res = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "--no-audit", "--no-fund", ...missing], { cwd: pluginDir, timeout: DEP_TIMEOUT_MS, shell: process.platform === "win32", encoding: "utf8" });
238
+ if (res.error || res.status !== 0) {
239
+ console.error(`[plugin-deps] ${join(pluginDir)} npm install 失败:`, res.error ?? res.stderr?.slice(0, 500));
240
+ return false;
241
+ }
242
+ const stillMissing = specs.filter((s) => !isDepAvailable(pluginDir, s));
243
+ if (stillMissing.length) {
244
+ console.error(`[plugin-deps] 安装完成但仍缺:${stillMissing.join(", ")}`);
245
+ return false;
246
+ }
247
+ onProgress?.("依赖安装完成");
248
+ return true;
249
+ };
250
+ const p = run().finally(() => depInstallLocks.delete(lockKey));
251
+ depInstallLocks.set(lockKey, p);
252
+ return p;
253
+ }
254
+ export class WorkspaceFS {
255
+ root;
256
+ /** root 是活值 getter(返回当前工作区绝对路径),跟随 set_cwd。 */
257
+ constructor(root) {
258
+ this.root = root;
259
+ }
260
+ /** 相对路径 → 活根下的绝对路径;越界抛错。空串 = 根本身。 */
261
+ abs(rel) {
262
+ const rootDir = resolve(this.root());
263
+ const target = resolve(rootDir, typeof rel === "string" ? rel : "");
264
+ if (target !== rootDir && !target.startsWith(rootDir + sepOf())) {
265
+ throw new Error(`路径越界:${String(rel)}`);
266
+ }
267
+ return target;
268
+ }
269
+ /** 单层目录列表(浅层;深度遍历请插件自行递归)。 */
270
+ async list(relDir = "") {
271
+ try {
272
+ const dirents = await fspReaddir(this.abs(relDir), { withFileTypes: true });
273
+ return dirents
274
+ .slice(0, 2000)
275
+ .map((d) => ({ name: d.name, type: d.isDirectory() ? "dir" : "file" }));
276
+ }
277
+ catch (err) {
278
+ throw new Error(`读取目录失败:${err.message}`);
279
+ }
280
+ }
281
+ /** 读文件(二进制)。声明为 async:路径校验失败以 rejected promise 表达
282
+ * (非 async 版本会同步 throw,破坏调用方 .catch/.rejects 契约)。 */
283
+ async read(relPath) {
284
+ return fspReadFile(this.abs(relPath));
285
+ }
286
+ /** 读文本(默认上限 512KB,超出截断——预览同款约定)。 */
287
+ async readText(relPath, maxBytes = 512 * 1024) {
288
+ const buf = await this.read(relPath);
289
+ return buf.subarray(0, maxBytes).toString("utf8");
290
+ }
291
+ /** 写文件(自动补父目录;注意相对路径锚定当前项目——切换 cwd 后写进新项目)。 */
292
+ async write(relPath, data) {
293
+ const target = this.abs(relPath);
294
+ await fspMkdir(dirname(target), { recursive: true });
295
+ await fspWriteFile(target, data);
296
+ }
297
+ /** 删除文件/目录(递归;只允许删工作区内的路径)。 */
298
+ async remove(relPath) {
299
+ await fspRm(this.abs(relPath), { recursive: true, force: false });
300
+ }
301
+ }
302
+ function sepOf() {
303
+ return process.platform === "win32" ? "\\" : "/";
304
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * 插件更新辅助(备份/回滚 + 远端 sha 对比)——纯逻辑,供 CLI(bin/pi-harness.mjs)
3
+ * 与单测共用。零网络依赖:远端 sha 经注入的 exec 获取(生产 = git ls-remote;
4
+ * 测试 = fake exec 或本地 git 仓库路径,git ls-remote 支持本地仓库,完全离线)。
5
+ *
6
+ * 布局:
7
+ * <dataDir>/plugins/<id>/ 安装本体(含 .pi-source.json + .pi-git-sha)
8
+ * <dataDir>/plugin-backups/<id>-<ts>/ 覆盖安装前的旧版本快照(保留最近 N 份)
9
+ */
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, cpSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { execFile } from "node:child_process";
13
+ const PLUGIN_ID_RE = /^[A-Za-z0-9_-]+$/;
14
+ /** 保留的备份份数(超出删除最旧的)。 */
15
+ export const BACKUP_KEEP = 3;
16
+ /** 默认执行器:execFile 直跑 git(不经 shell),15s 超时。 */
17
+ export const execGit = (cmd, args) => new Promise((resolve) => {
18
+ execFile(cmd, args, { timeout: 15_000, encoding: "utf8" }, (err, stdout, stderr) => {
19
+ if (err)
20
+ resolve({ ok: false, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
21
+ else
22
+ resolve({ ok: true, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
23
+ });
24
+ });
25
+ /**
26
+ * 覆盖安装前备份旧插件目录 → <dataDir>/plugin-backups/<id>-<ts>/。
27
+ * 目标不存在/备份失败返回 null(调用方可继续——备份是尽力而为的保护)。
28
+ */
29
+ export function ensureBackup(dataDir, id, opts) {
30
+ if (!PLUGIN_ID_RE.test(id))
31
+ return null;
32
+ const target = join(dataDir, "plugins", id);
33
+ if (!existsSync(target))
34
+ return null;
35
+ const ts = stamp();
36
+ const dest = join(dataDir, "plugin-backups", `${id}-${ts}`);
37
+ try {
38
+ mkdirSync(dirnameOf(dest), { recursive: true });
39
+ cpSync(target, dest, {
40
+ recursive: true,
41
+ // 与安装一致:不备份 .git/node_modules(纯运行目录),config.json 等保留。
42
+ filter: (s) => !/(^|[\\/])(\.git|node_modules)([\\/]|$)/.test(s),
43
+ });
44
+ writeFileSync(join(dest, ".pi-backup.json"), JSON.stringify({ id, ts, source: opts?.source }, null, 2) + "\n");
45
+ pruneBackups(dataDir, id);
46
+ return ts;
47
+ }
48
+ catch (err) {
49
+ try {
50
+ rmSync(dest, { recursive: true, force: true });
51
+ }
52
+ catch {
53
+ /* 清理失败忽略 */
54
+ }
55
+ console.warn(`[plugin-updater] 备份 ${id} 失败:`, err instanceof Error ? err.message : err);
56
+ return null;
57
+ }
58
+ }
59
+ /** 该插件的备份目录列表(按时间从新到旧)。 */
60
+ export function listBackups(dataDir, id) {
61
+ if (!PLUGIN_ID_RE.test(id))
62
+ return [];
63
+ const dir = join(dataDir, "plugin-backups");
64
+ let names = [];
65
+ try {
66
+ names = readdirSync(dir);
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const re = new RegExp(`^${id.replace(/[^A-Za-z0-9_-]/g, "")}-(\\d{8}-\\d{9})$`);
72
+ return names
73
+ .filter((n) => re.test(n) && existsSync(join(dir, n, ".pi-backup.json")))
74
+ .sort()
75
+ .reverse();
76
+ }
77
+ /**
78
+ * 回滚到最近一份备份:删当前 plugins/<id> → 拷贝备份回 → 删备份。
79
+ * 返回最近备份 ts;无备份返回 null。
80
+ */
81
+ export function restoreBackup(dataDir, id) {
82
+ const backups = listBackups(dataDir, id);
83
+ if (backups.length === 0)
84
+ return null;
85
+ const src = join(dataDir, "plugin-backups", backups[0]);
86
+ const target = join(dataDir, "plugins", id);
87
+ try {
88
+ if (existsSync(target))
89
+ rmSync(target, { recursive: true, force: true });
90
+ mkdirSync(join(dataDir, "plugins"), { recursive: true });
91
+ cpSync(src, target, { recursive: true });
92
+ rmSync(src, { recursive: true, force: true });
93
+ return backups[0];
94
+ }
95
+ catch (err) {
96
+ console.warn(`[plugin-updater] 回滚 ${id} 失败:`, err instanceof Error ? err.message : err);
97
+ return null;
98
+ }
99
+ }
100
+ /** 保留最近 BACKUP_KEEP 份,删除更旧的。 */
101
+ export function pruneBackups(dataDir, id, keep = BACKUP_KEEP) {
102
+ const backups = listBackups(dataDir, id);
103
+ for (const b of backups.slice(keep)) {
104
+ try {
105
+ rmSync(join(dataDir, "plugin-backups", b), { recursive: true, force: true });
106
+ }
107
+ catch {
108
+ /* 忽略 */
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * 解析一个安装源并取远端 HEAD sha(前 12 位)。
114
+ * - 本地 git 仓库路径 / file:// → git ls-remote <path> HEAD(离线)
115
+ * - GitHub owner/repo、URL → git ls-remote https://github.com/o/r.git HEAD
116
+ * - 无法识别 / git 不在 / 网络失败 → null(调用方标记「无法检查」)
117
+ */
118
+ export async function resolveRemoteSha(spec, exec = execGit) {
119
+ const clean = String(spec ?? "").trim();
120
+ if (!clean)
121
+ return null;
122
+ let remote = null;
123
+ if (existsSync(clean)) {
124
+ remote = clean; // 本地 git 仓库路径
125
+ }
126
+ else if (/^file:\/\//i.test(clean)) {
127
+ remote = clean.slice("file://".length);
128
+ }
129
+ else {
130
+ // GitHub 形态(owner/repo、URL、git@)
131
+ let s = clean.replace(/^git@([^:]+):/, "");
132
+ const m = s.match(/^https?:\/\/(?:www\.)?github\.com\/(.+?)(?:\.git)?\/?$/i);
133
+ if (m)
134
+ [, s] = m;
135
+ s = s.split("#")[0]; // 去掉 #分支
136
+ const segs = s.split("/").filter(Boolean);
137
+ if (segs.length < 2)
138
+ return null;
139
+ // 只取 owner/repo(/tree/<ref>/<subpath> 等后缀不影响远端 sha)
140
+ const repo = segs[0] + "/" + segs[1].replace(/\.git$/, "");
141
+ remote = `https://github.com/${repo}.git`;
142
+ }
143
+ if (!remote)
144
+ return null;
145
+ const res = await exec("git", ["ls-remote", remote, "HEAD"]);
146
+ if (!res.ok)
147
+ return null;
148
+ // 行格式: <sha>\tHEAD(可能多行——取第一行)
149
+ const sha = (res.stdout.match(/^([0-9a-f]{40,64})\s+HEAD/m)?.[1]) ?? null;
150
+ return sha ? sha.slice(0, 12) : null;
151
+ }
152
+ /** 扫描全部已装插件,对比本地 sha 与远端 sha,报告更新状态。 */
153
+ export async function checkPluginUpdates(dataDir, exec = execGit) {
154
+ const pluginsDir = join(dataDir, "plugins");
155
+ let names = [];
156
+ try {
157
+ names = readdirSync(pluginsDir).sort();
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ const out = [];
163
+ for (const n of names) {
164
+ if (!PLUGIN_ID_RE.test(n))
165
+ continue;
166
+ const dir = join(pluginsDir, n);
167
+ try {
168
+ const sourceJson = readFileSync(join(dir, ".pi-source.json"), "utf8");
169
+ const { source } = JSON.parse(sourceJson);
170
+ if (!source)
171
+ continue; // 无来源记录(手工拷入)→ skip
172
+ let localSha = null;
173
+ try {
174
+ localSha = readFileSync(join(dir, ".pi-git-sha"), "utf8").trim() || null;
175
+ }
176
+ catch {
177
+ localSha = null; // 无 sha 记录 → 保守认为可更新(不知道装了哪个版本)
178
+ }
179
+ let remoteSha = null;
180
+ let error;
181
+ try {
182
+ remoteSha = await resolveRemoteSha(source, exec);
183
+ }
184
+ catch (err) {
185
+ error = err instanceof Error ? err.message : String(err);
186
+ remoteSha = null;
187
+ }
188
+ if (!remoteSha && !error)
189
+ error = "无法检查(非 git 源或 git 不可用)";
190
+ let name;
191
+ let version;
192
+ try {
193
+ const m = JSON.parse(readFileSync(join(dir, "manifest.json"), "utf8"));
194
+ name = m.name;
195
+ version = m.version;
196
+ }
197
+ catch {
198
+ /* 坏 manifest:仍报告 */
199
+ }
200
+ const updatable = !!remoteSha && (!localSha || localSha !== remoteSha);
201
+ out.push({
202
+ id: n,
203
+ name,
204
+ version,
205
+ source,
206
+ localSha,
207
+ remoteSha,
208
+ updatable,
209
+ error,
210
+ });
211
+ }
212
+ catch {
213
+ continue; // 坏目录跳过
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+ /** Last stamp handed out, so the next one is always strictly greater. */
219
+ let lastStampMs = 0;
220
+ /**
221
+ * Backup directory name: `YYYYMMDD-HHMMSSmmm`.
222
+ *
223
+ * Monotonic on purpose. The name is the backup's only identity — two calls
224
+ * landing in the same millisecond produced the same directory, so the second
225
+ * cpSync() merged into the first instead of creating a new backup, silently
226
+ * losing one and leaving pruneBackups() keeping fewer than `keep`. Taking
227
+ * several backups in a row is fast enough that this is not theoretical: eight
228
+ * consecutive calls routinely share a millisecond.
229
+ *
230
+ * Colliding calls are pushed to the next millisecond rather than given a
231
+ * disambiguating suffix, because listBackups() matches names against a strict
232
+ * `\d{8}-\d{9}$` pattern — a suffixed backup would be invisible to it, and
233
+ * therefore never pruned either.
234
+ */
235
+ function stamp() {
236
+ const now = Math.max(Date.now(), lastStampMs + 1);
237
+ lastStampMs = now;
238
+ const d = new Date(now);
239
+ const p = (x, n = 2) => String(x).padStart(n, "0");
240
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`;
241
+ }
242
+ function dirnameOf(p) {
243
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
244
+ return i >= 0 ? p.slice(0, i) : null;
245
+ }