chainlesschain 0.162.95 → 0.162.97

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 (219) hide show
  1. package/README.md +1 -1
  2. package/bin/chainlesschain.js +8 -18
  3. package/package.json +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-C1YvIYh5.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-jqcXEakj.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-C8Xwqd3_.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-Dx5ypTfA.js} +5 -5
  8. package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-1GZ4Tv2f.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CexR2hyk.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-DwAruKdy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-W2FkU5BY.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-BCLnhLfu.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Dkh5JY5B.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-LfYOgsdA.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-DEgketAC.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-LeFQMaZm.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CrlKYvBc.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-ca_4PGYF.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-BcMWqh2a.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-pVVEA3dw.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-CiKSGTqp.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID-B2bGzdOj.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-sQ6jcgCn.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-rFx_8P04.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-smw2hh6f.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-CZhg2_S2.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-DQE-368G.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-2XPb9izr.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-DCGMz5E6.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-BFwrxK4Q.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-ChxBRwG1.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-C7kyknu_.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-BsMd4XAt.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Bs3yrpow.js} +2 -2
  35. package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-Cc29jdBv.js} +1 -1
  36. package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-zYpYsUsl.js} +4 -4
  37. package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-x9r4yKtS.js} +2 -2
  38. package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-CjUo8prx.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-Cmz4wVrF.js} +1 -1
  40. package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-Y-SBnNxm.js} +5 -5
  41. package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-ogiKnrxj.js} +2 -2
  42. package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C1hygkFR.js} +3 -3
  43. package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-fWWYiTsY.js} +1 -1
  44. package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-CkCNF4gC.js} +3 -3
  45. package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-DThB7WQG.js} +1 -1
  46. package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-Dh64aMq6.js} +1 -1
  47. package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-BSY16zvP.js} +4 -4
  48. package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-C_TDh18K.js} +1 -1
  49. package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CvxZk6og.js} +2 -2
  50. package/src/assets/web-panel/assets/PdhVaultBrowser-0JZ5JF4M.js +7 -0
  51. package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-DV8ubpjM.js} +4 -4
  52. package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-g2Dgls8e.js} +2 -2
  53. package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-DGp8zQ0C.js} +1 -1
  54. package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-C0uHrjWR.js} +1 -1
  55. package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-2XGICaFt.js} +2 -2
  56. package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Cfy72e0Q.js} +2 -2
  57. package/src/assets/web-panel/assets/{Projects-CXydUXp8.js → Projects-Xsvk_kKT.js} +1 -1
  58. package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-vuvDVg7U.js} +1 -1
  59. package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-BUy7kPyQ.js} +1 -1
  60. package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-DlzpC_JE.js} +1 -1
  61. package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-BiWFwPMh.js} +1 -1
  62. package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-dmfZBK-k.js} +1 -1
  63. package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-Cf6tlRKq.js} +2 -2
  64. package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DhzXDRAJ.js} +1 -1
  65. package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-C-bTfgeJ.js} +3 -3
  66. package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-D3KNwBqu.js} +2 -2
  67. package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CHH8i1xY.js} +1 -1
  68. package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-DHiONX0z.js} +1 -1
  69. package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DwWMb-Ui.js} +1 -1
  70. package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-DAq64iWB.js} +1 -1
  71. package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-DJU-kHko.js} +2 -2
  72. package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-RAIzA3u-.js} +1 -1
  73. package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-By4cbadT.js} +1 -1
  74. package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-kCAXsxWu.js} +1 -1
  75. package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-D8pup_CO.js} +2 -2
  76. package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-Bk2c9TUI.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-B0wTiZRa.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-Ca_9t0gk.js} +1 -1
  79. package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-BnfNDDDm.js} +1 -1
  80. package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-fnlF2-Yr.js} +1 -1
  81. package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-Cfshhzyw.js} +1 -1
  82. package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-BWMpDOLG.js} +4 -4
  83. package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn-B6xeXZvw.js} +3 -3
  84. package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-Cb3-FPCE.js} +1 -1
  85. package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CP_opuMK.js} +1 -1
  86. package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-RLzfjxRQ.js} +1 -1
  87. package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-BTRloNKV.js} +1 -1
  88. package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-gXgXTLlU.js} +1 -1
  89. package/src/assets/web-panel/assets/devWarning-CYlmApo_.js +1 -0
  90. package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-CrIpxkRC.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-AsI1SqnE.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B3L5vTSs.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-B5sN_pwd.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-BEcRPNHv.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BVrcoJe2.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BewxG36A.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-Bi87H9yk.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-Bl4OQ--4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-yavsyHif.js → index-BlE2lCS-.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-BlS91Ac0.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-Bm0cNeeQ.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-Bo2Fl2j7.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-Bsz3X-aU.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DOYcemym.js → index-BurvDWMS.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-BzBLtthb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-C0wmEN2j.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-C4bBR-Vd.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-COhACL0I.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-CQ-cnhZM.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-CRr99L7q.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-CaTkUkJp.js} +3 -3
  112. package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CfOjPxso.js} +1 -1
  113. package/src/assets/web-panel/assets/index-Cf_SLgtj.js +1 -0
  114. package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-Ch_TYQNW.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CisGGaF3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-DA-_2qTs.js} +1 -1
  117. package/src/assets/web-panel/assets/index-DYBIDVGn.js +1 -0
  118. package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-DZxK7MW9.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DiXnGU0A.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-DsiyfyBE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-G_OgtHiC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-IjV-dnE8.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-Mltfj22U.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-U-kmEJdu.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-cgf-OVry.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-ch8zcf-h.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-cqOT5RiJ.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-C7sBLech.js → index-cywkZJ-I.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-sE_I7gVL.js} +1 -1
  130. package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-DMwnVZzH.js} +1 -1
  131. package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-Dnpjpvo7.js} +1 -1
  132. package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-iuRh5U9Y.js} +1 -1
  133. package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-Bl4WRV59.js} +1 -1
  134. package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-B9KSXaep.js} +1 -1
  135. package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-TEGIow4r.js} +1 -1
  136. package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve--BECM92v.js} +1 -1
  137. package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-Ca8G7XbL.js} +1 -1
  138. package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-8QPG_T3D.js} +1 -1
  139. package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-p3FsA6mg.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-Ct0yIzrp.js} +1 -1
  141. package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-3nlByl_3.js} +1 -1
  142. package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-N3OsoSRM.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-Cfe3ywq3.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-DVBuDS_3.js} +1 -1
  145. package/src/assets/web-panel/index.html +1 -1
  146. package/src/commands/activitypub.js +4 -3
  147. package/src/commands/agent.js +25 -0
  148. package/src/commands/agents.js +18 -4
  149. package/src/commands/ask.js +13 -0
  150. package/src/commands/command.js +12 -2
  151. package/src/commands/config.js +17 -1
  152. package/src/commands/cost.js +6 -1
  153. package/src/commands/dao.js +4 -3
  154. package/src/commands/dlp.js +2 -1
  155. package/src/commands/hub.js +138 -0
  156. package/src/commands/mtc.js +37 -1
  157. package/src/gateways/discord/discord-formatter.js +18 -1
  158. package/src/gateways/telegram/telegram-formatter.js +6 -3
  159. package/src/gateways/ws/ws-server.js +19 -1
  160. package/src/harness/plugin-manager.js +20 -0
  161. package/src/harness/worktree-isolator.js +17 -1
  162. package/src/index.js +2 -0
  163. package/src/lib/__tests__/model-deprecation.test.js +194 -0
  164. package/src/lib/a2a-protocol.js +24 -9
  165. package/src/lib/agent-core.js +4 -5
  166. package/src/lib/audit-mtc.js +40 -2
  167. package/src/lib/chat-core.js +146 -18
  168. package/src/lib/cli-anything-bridge.js +5 -0
  169. package/src/lib/cli-numeric.js +38 -0
  170. package/src/lib/config-manager.js +37 -6
  171. package/src/lib/cowork/ab-comparator-cli.js +6 -2
  172. package/src/lib/cowork-cron.js +45 -34
  173. package/src/lib/cowork-learning.js +31 -1
  174. package/src/lib/cowork-share.js +39 -3
  175. package/src/lib/cowork-template-marketplace.js +54 -2
  176. package/src/lib/cowork-workflow.js +33 -6
  177. package/src/lib/cross-chain-mtc.js +41 -12
  178. package/src/lib/crypto-manager.js +12 -2
  179. package/src/lib/dbevo.js +19 -8
  180. package/src/lib/downloader.js +24 -9
  181. package/src/lib/evomap-governance.js +4 -4
  182. package/src/lib/evomap-manager.js +54 -7
  183. package/src/lib/fatal-handler.js +48 -0
  184. package/src/lib/file-checkpoint.js +26 -0
  185. package/src/lib/git-integration.js +28 -0
  186. package/src/lib/goal-store.js +36 -10
  187. package/src/lib/hierarchical-memory.js +6 -0
  188. package/src/lib/llm-pricing.js +52 -2
  189. package/src/lib/mcp-oauth.js +37 -7
  190. package/src/lib/memory-manager.js +23 -2
  191. package/src/lib/model-deprecation.js +249 -0
  192. package/src/lib/pdh-feedback-ledger.js +131 -0
  193. package/src/lib/permanent-memory.js +30 -2
  194. package/src/lib/permission-rules.cjs +11 -3
  195. package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
  196. package/src/lib/personal-data-hub-wiring.js +39 -16
  197. package/src/lib/project-instructions.js +10 -4
  198. package/src/lib/runnable-provider.js +62 -31
  199. package/src/lib/session-usage.js +34 -1
  200. package/src/lib/skill-loader.js +4 -1
  201. package/src/lib/skill-packs/generator.js +25 -2
  202. package/src/lib/stream-retry.js +56 -0
  203. package/src/lib/sync-credentials.js +53 -17
  204. package/src/lib/token-tracker.js +12 -2
  205. package/src/lib/with-file-lock.js +87 -0
  206. package/src/repl/agent-repl.js +47 -7
  207. package/src/repl/chat-repl.js +19 -0
  208. package/src/repl/session-cost.js +31 -3
  209. package/src/runtime/agent-core.js +478 -53
  210. package/src/runtime/coding-agent-contract-shared.cjs +58 -1
  211. package/src/runtime/coding-agent-policy.cjs +10 -0
  212. package/src/runtime/file-ref-expander.js +29 -1
  213. package/src/runtime/headless-runner.js +12 -1
  214. package/src/runtime/headless-stream.js +200 -9
  215. package/src/skills/video-editing/tools/commit.js +15 -1
  216. package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
  217. package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
  218. package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
  219. package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
@@ -32,6 +32,7 @@
32
32
  import crypto from "node:crypto";
33
33
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
34
34
  import { join } from "node:path";
35
+ import { withFileLock } from "./with-file-lock.js";
35
36
 
36
37
  export const _deps = {
37
38
  existsSync,
@@ -260,49 +261,59 @@ export function addSchedule(cwd, input) {
260
261
  const err = validateCron(cron);
261
262
  if (err) throw new Error(`invalid cron: ${err}`);
262
263
 
263
- const schedules = loadSchedules(cwd);
264
- const entry = {
265
- id: `sch-${crypto.randomUUID().slice(0, 12)}`,
266
- cron: cron.trim(),
267
- templateId,
268
- userMessage,
269
- files: Array.isArray(files) ? files : [],
270
- enabled: true,
271
- createdAt: _deps.now().toISOString(),
272
- lastRunAt: null,
273
- lastStatus: null,
274
- };
275
- schedules.push(entry);
276
- saveSchedules(cwd, schedules);
277
- return entry;
264
+ // Serialize the read-modify-write across processes (cc+cc / cc+desktop share
265
+ // the cowork dir) so concurrently-added schedules don't clobber each other.
266
+ return withFileLock(_scheduleFile(cwd), () => {
267
+ const schedules = loadSchedules(cwd);
268
+ const entry = {
269
+ id: `sch-${crypto.randomUUID().slice(0, 12)}`,
270
+ cron: cron.trim(),
271
+ templateId,
272
+ userMessage,
273
+ files: Array.isArray(files) ? files : [],
274
+ enabled: true,
275
+ createdAt: _deps.now().toISOString(),
276
+ lastRunAt: null,
277
+ lastStatus: null,
278
+ };
279
+ schedules.push(entry);
280
+ saveSchedules(cwd, schedules);
281
+ return entry;
282
+ });
278
283
  }
279
284
 
280
285
  export function removeSchedule(cwd, id) {
281
- const schedules = loadSchedules(cwd);
282
- const idx = schedules.findIndex((s) => s.id === id);
283
- if (idx === -1) return false;
284
- schedules.splice(idx, 1);
285
- saveSchedules(cwd, schedules);
286
- return true;
286
+ return withFileLock(_scheduleFile(cwd), () => {
287
+ const schedules = loadSchedules(cwd);
288
+ const idx = schedules.findIndex((s) => s.id === id);
289
+ if (idx === -1) return false;
290
+ schedules.splice(idx, 1);
291
+ saveSchedules(cwd, schedules);
292
+ return true;
293
+ });
287
294
  }
288
295
 
289
296
  export function setScheduleEnabled(cwd, id, enabled) {
290
- const schedules = loadSchedules(cwd);
291
- const s = schedules.find((x) => x.id === id);
292
- if (!s) return false;
293
- s.enabled = !!enabled;
294
- saveSchedules(cwd, schedules);
295
- return true;
297
+ return withFileLock(_scheduleFile(cwd), () => {
298
+ const schedules = loadSchedules(cwd);
299
+ const s = schedules.find((x) => x.id === id);
300
+ if (!s) return false;
301
+ s.enabled = !!enabled;
302
+ saveSchedules(cwd, schedules);
303
+ return true;
304
+ });
296
305
  }
297
306
 
298
307
  export function updateScheduleRunState(cwd, id, { lastRunAt, lastStatus }) {
299
- const schedules = loadSchedules(cwd);
300
- const s = schedules.find((x) => x.id === id);
301
- if (!s) return false;
302
- if (lastRunAt) s.lastRunAt = lastRunAt;
303
- if (lastStatus) s.lastStatus = lastStatus;
304
- saveSchedules(cwd, schedules);
305
- return true;
308
+ return withFileLock(_scheduleFile(cwd), () => {
309
+ const schedules = loadSchedules(cwd);
310
+ const s = schedules.find((x) => x.id === id);
311
+ if (!s) return false;
312
+ if (lastRunAt) s.lastRunAt = lastRunAt;
313
+ if (lastStatus) s.lastStatus = lastStatus;
314
+ saveSchedules(cwd, schedules);
315
+ return true;
316
+ });
306
317
  }
307
318
 
308
319
  // ─── Scheduler ───────────────────────────────────────────────────────────────
@@ -20,6 +20,8 @@ import {
20
20
  writeFileSync,
21
21
  mkdirSync,
22
22
  appendFileSync,
23
+ renameSync,
24
+ unlinkSync,
23
25
  } from "node:fs";
24
26
  import { join } from "node:path";
25
27
 
@@ -29,9 +31,37 @@ export const _deps = {
29
31
  writeFileSync,
30
32
  mkdirSync,
31
33
  appendFileSync,
34
+ renameSync,
35
+ unlinkSync,
32
36
  now: () => new Date(),
33
37
  };
34
38
 
39
+ /**
40
+ * Atomically write a learned-template JSON via _deps. A crash mid-write would
41
+ * truncate `<templateId>.json`; loadUserTemplate's catch then drops it, losing
42
+ * the learned prompt extension. Temp sibling + rename (atomic within a
43
+ * filesystem). Graceful-degrade to a direct write when the injected fs lacks
44
+ * renameSync — the real fs always has it, so production is always atomic.
45
+ */
46
+ function _atomicWriteJson(file, data) {
47
+ if (typeof _deps.renameSync !== "function") {
48
+ _deps.writeFileSync(file, data, "utf-8");
49
+ return;
50
+ }
51
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
52
+ try {
53
+ _deps.writeFileSync(tmp, data, "utf-8");
54
+ _deps.renameSync(tmp, file);
55
+ } catch (err) {
56
+ try {
57
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
58
+ } catch {
59
+ /* best-effort temp cleanup */
60
+ }
61
+ throw err;
62
+ }
63
+ }
64
+
35
65
  /** Minimum historical runs before a template qualifies for a patch suggestion. */
36
66
  export const MIN_RUNS_FOR_PATCH = 10;
37
67
  /** Minimum distinct failures needed to trigger a suggestion. */
@@ -414,7 +444,7 @@ export function applyPromptPatch(cwd, patch) {
414
444
  updatedAt: _deps.now().toISOString(),
415
445
  };
416
446
  const file = join(dir, `${patch.templateId}.json`);
417
- _deps.writeFileSync(file, JSON.stringify(doc, null, 2), "utf-8");
447
+ _atomicWriteJson(file, JSON.stringify(doc, null, 2));
418
448
 
419
449
  // Audit trail — never pruned automatically
420
450
  _deps.appendFileSync(
@@ -15,7 +15,14 @@
15
15
  * @module cowork-share
16
16
  */
17
17
 
18
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import {
19
+ existsSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ renameSync,
24
+ unlinkSync,
25
+ } from "node:fs";
19
26
  import { join } from "node:path";
20
27
  import crypto, { createHash } from "node:crypto";
21
28
  import {
@@ -29,9 +36,38 @@ export const _deps = {
29
36
  mkdirSync,
30
37
  readFileSync,
31
38
  writeFileSync,
39
+ renameSync,
40
+ unlinkSync,
32
41
  now: () => new Date().toISOString(),
33
42
  };
34
43
 
44
+ /**
45
+ * Atomically write a JSON file via _deps. Imported result packets are persistent
46
+ * local state (shared-results/<taskId>.json) and exported packets are read by
47
+ * other tools — a crash mid-write must not leave a truncated/invalid packet
48
+ * (verifyPacket/readPacket would reject it). Temp sibling + rename (atomic
49
+ * within a filesystem). Graceful-degrade to a direct write when the injected fs
50
+ * lacks renameSync — the real fs always has it, so production is always atomic.
51
+ */
52
+ function _atomicWriteJson(file, data) {
53
+ if (typeof _deps.renameSync !== "function") {
54
+ _deps.writeFileSync(file, data, "utf-8");
55
+ return;
56
+ }
57
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
58
+ try {
59
+ _deps.writeFileSync(tmp, data, "utf-8");
60
+ _deps.renameSync(tmp, file);
61
+ } catch (err) {
62
+ try {
63
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
64
+ } catch {
65
+ /* best-effort temp cleanup */
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+
35
71
  const SUPPORTED_SIG_ALG = "Ed25519";
36
72
 
37
73
  const PACKET_VERSION = 1;
@@ -256,7 +292,7 @@ export function findHistoryRecord(cwd, taskId) {
256
292
  * Write a packet to disk as pretty-printed JSON.
257
293
  */
258
294
  export function writePacket(filePath, packet) {
259
- _deps.writeFileSync(filePath, JSON.stringify(packet, null, 2), "utf-8");
295
+ _atomicWriteJson(filePath, JSON.stringify(packet, null, 2));
260
296
  return filePath;
261
297
  }
262
298
 
@@ -317,7 +353,7 @@ export function importResultPacket(cwd, packet) {
317
353
  const dir = join(cwd, ".chainlesschain", "cowork", "shared-results");
318
354
  _deps.mkdirSync(dir, { recursive: true });
319
355
  const file = join(dir, `${packet.payload.taskId}.json`);
320
- _deps.writeFileSync(file, JSON.stringify(packet, null, 2), "utf-8");
356
+ _atomicWriteJson(file, JSON.stringify(packet, null, 2));
321
357
  return { file, taskId: packet.payload.taskId };
322
358
  }
323
359
 
@@ -22,6 +22,7 @@ import {
22
22
  writeFileSync,
23
23
  readdirSync,
24
24
  unlinkSync,
25
+ renameSync,
25
26
  } from "node:fs";
26
27
  import { join } from "node:path";
27
28
 
@@ -32,10 +33,38 @@ export const _deps = {
32
33
  writeFileSync,
33
34
  readdirSync,
34
35
  unlinkSync,
36
+ renameSync,
35
37
  // Injected at runtime by CLI to avoid eager evomap-client load
36
38
  evomapClient: null,
37
39
  };
38
40
 
41
+ /**
42
+ * Atomically write a JSON file via _deps. A user template (<id>.json) is
43
+ * persistent state; a crash mid-write truncates it and listUserTemplates /
44
+ * loadUserTemplate's JSON.parse then drops or throws on it. Temp sibling +
45
+ * rename (atomic within a filesystem). Graceful-degrade to a direct write when
46
+ * the injected fs lacks renameSync — the real fs always has it, so production
47
+ * is always atomic.
48
+ */
49
+ function _atomicWriteJson(file, data) {
50
+ if (typeof _deps.renameSync !== "function") {
51
+ _deps.writeFileSync(file, data, "utf-8");
52
+ return;
53
+ }
54
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
55
+ try {
56
+ _deps.writeFileSync(tmp, data, "utf-8");
57
+ _deps.renameSync(tmp, file);
58
+ } catch (err) {
59
+ try {
60
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
61
+ } catch {
62
+ /* best-effort temp cleanup */
63
+ }
64
+ throw err;
65
+ }
66
+ }
67
+
39
68
  const EVOMAP_CATEGORY = "cowork-template";
40
69
 
41
70
  // ─── Paths ───────────────────────────────────────────────────────────────────
@@ -48,6 +77,26 @@ function _userTemplateFile(cwd, id) {
48
77
  return join(_userTemplatesDir(cwd), `${id}.json`);
49
78
  }
50
79
 
80
+ /**
81
+ * A template id is interpolated into a filesystem path (user-templates/<id>.json)
82
+ * and arrives from installed templates whose payload comes from an external hub /
83
+ * MCP download. A malicious id like "../../.ssh/authorized_keys" or
84
+ * "C:\\Windows\\..." would escape the user-templates dir and overwrite/delete
85
+ * arbitrary files. Restrict it to a single safe path segment.
86
+ */
87
+ function _isSafeTemplateId(id) {
88
+ return (
89
+ typeof id === "string" &&
90
+ id.length > 0 &&
91
+ id !== "." &&
92
+ id !== ".." &&
93
+ !id.includes("/") &&
94
+ !id.includes("\\") &&
95
+ !id.includes(":") &&
96
+ !id.includes("\0")
97
+ );
98
+ }
99
+
51
100
  // ─── Serialization ───────────────────────────────────────────────────────────
52
101
 
53
102
  const SHARED_FIELDS = [
@@ -148,18 +197,21 @@ export function listUserTemplates(cwd) {
148
197
  /** Save a template to the local user-templates directory. */
149
198
  export function saveUserTemplate(cwd, template) {
150
199
  if (!template?.id) throw new Error("template.id is required");
200
+ if (!_isSafeTemplateId(template.id)) {
201
+ throw new Error(`Unsafe template id (path traversal): ${template.id}`);
202
+ }
151
203
  const dir = _userTemplatesDir(cwd);
152
204
  _deps.mkdirSync(dir, { recursive: true });
153
- _deps.writeFileSync(
205
+ _atomicWriteJson(
154
206
  _userTemplateFile(cwd, template.id),
155
207
  JSON.stringify(template, null, 2),
156
- "utf-8",
157
208
  );
158
209
  return template;
159
210
  }
160
211
 
161
212
  /** Remove an installed user template. Returns true if removed. */
162
213
  export function removeUserTemplate(cwd, id) {
214
+ if (!_isSafeTemplateId(id)) return false;
163
215
  const file = _userTemplateFile(cwd, id);
164
216
  if (!_deps.existsSync(file)) return false;
165
217
  _deps.unlinkSync(file);
@@ -29,6 +29,7 @@ import {
29
29
  readdirSync,
30
30
  unlinkSync,
31
31
  appendFileSync,
32
+ renameSync,
32
33
  } from "node:fs";
33
34
  import { join } from "node:path";
34
35
  import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
@@ -47,6 +48,7 @@ export const _deps = {
47
48
  readdirSync,
48
49
  unlinkSync,
49
50
  appendFileSync,
51
+ renameSync,
50
52
  now: () => Date.now(),
51
53
  runTask: null, // injected by CLI
52
54
  // Timer seams (injectable for deterministic retry/timeout tests).
@@ -55,6 +57,32 @@ export const _deps = {
55
57
  clearTimeout: (t) => clearTimeout(t),
56
58
  };
57
59
 
60
+ /**
61
+ * Atomically write a JSON file via _deps. A saved workflow definition is
62
+ * persistent user state; a crash mid-write truncates <id>.json and getWorkflow
63
+ * then fails to parse it. Temp sibling + rename (atomic within a filesystem).
64
+ * Graceful-degrade to a direct write when the injected fs lacks renameSync — the
65
+ * real fs always has it, so production is always atomic.
66
+ */
67
+ function _atomicWriteJson(file, data) {
68
+ if (typeof _deps.renameSync !== "function") {
69
+ _deps.writeFileSync(file, data, "utf-8");
70
+ return;
71
+ }
72
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
73
+ try {
74
+ _deps.writeFileSync(tmp, data, "utf-8");
75
+ _deps.renameSync(tmp, file);
76
+ } catch (err) {
77
+ try {
78
+ if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
79
+ } catch {
80
+ /* best-effort temp cleanup */
81
+ }
82
+ throw err;
83
+ }
84
+ }
85
+
58
86
  // ─── Paths ───────────────────────────────────────────────────────────────────
59
87
 
60
88
  function workflowsDir(cwd) {
@@ -302,7 +330,10 @@ export function resolveForEachItems(forEach, resultsById) {
302
330
  export function substituteItem(template, item) {
303
331
  if (typeof template !== "string") return template;
304
332
  const repl = typeof item === "string" ? item : JSON.stringify(item);
305
- return template.replace(/\$\{item\}/g, repl);
333
+ // Insert via a function so $-sequences in the item ($&, $`, $', $$ — common in
334
+ // JSON values / shell snippets) are placed literally rather than interpreted
335
+ // as String.replace patterns, which would corrupt the substituted value.
336
+ return template.replace(/\$\{item\}/g, () => repl);
306
337
  }
307
338
 
308
339
  /** Evaluate a step's `when` expression. Missing expression → always true. */
@@ -376,11 +407,7 @@ export function saveWorkflow(cwd, wf) {
376
407
  if (!valid) throw new Error(`Invalid workflow: ${errors.join("; ")}`);
377
408
  const dir = workflowsDir(cwd);
378
409
  _deps.mkdirSync(dir, { recursive: true });
379
- _deps.writeFileSync(
380
- workflowFile(cwd, wf.id),
381
- JSON.stringify(wf, null, 2),
382
- "utf-8",
383
- );
410
+ _atomicWriteJson(workflowFile(cwd, wf.id), JSON.stringify(wf, null, 2));
384
411
  return wf;
385
412
  }
386
413
 
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import mtcLib from "@chainlesschain/core-mtc";
22
+ import { withFileLock } from "./with-file-lock.js";
22
23
 
23
24
  const {
24
25
  assembleBatch,
@@ -95,6 +96,28 @@ function ensureDirs(dir) {
95
96
  }
96
97
  }
97
98
 
99
+ /**
100
+ * Atomically write a UTF-8 file. The cross-chain MTC store holds config, trust
101
+ * anchors (federation security material), staged bridge ops, the batch-seq
102
+ * counter, and closed batches — a crash mid-write corrupts ledger/security
103
+ * state. Temp sibling + rename (atomic within a filesystem); temp names end in
104
+ * `.tmp` (never `.json`), so a hard-crash leftover is ignored by listStagedOps.
105
+ */
106
+ function atomicWriteFileSync(filePath, data) {
107
+ const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
108
+ try {
109
+ fs.writeFileSync(tmp, data, "utf-8");
110
+ fs.renameSync(tmp, filePath);
111
+ } catch (err) {
112
+ try {
113
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
114
+ } catch {
115
+ /* best-effort temp cleanup */
116
+ }
117
+ throw err;
118
+ }
119
+ }
120
+
98
121
  // ─────────────────────────────────────────────────────────────────────
99
122
  // Namespace
100
123
  // ─────────────────────────────────────────────────────────────────────
@@ -173,7 +196,7 @@ export function saveCrossChainMtcConfig(dir, patch) {
173
196
  "config.mode must be independent, federated, or light-client",
174
197
  );
175
198
  }
176
- fs.writeFileSync(configPath(dir), JSON.stringify(merged, null, 2), "utf-8");
199
+ atomicWriteFileSync(configPath(dir), JSON.stringify(merged, null, 2));
177
200
  return merged;
178
201
  }
179
202
 
@@ -201,11 +224,7 @@ export function loadTrustAnchors(dir) {
201
224
  }
202
225
 
203
226
  function saveTrustAnchorStore(dir, store) {
204
- fs.writeFileSync(
205
- trustAnchorsPath(dir),
206
- JSON.stringify(store, null, 2),
207
- "utf-8",
208
- );
227
+ atomicWriteFileSync(trustAnchorsPath(dir), JSON.stringify(store, null, 2));
209
228
  }
210
229
 
211
230
  /**
@@ -456,7 +475,7 @@ export function stageBridgeOp(dir, op, opts = {}) {
456
475
  if (fs.existsSync(file)) {
457
476
  return { staged: false, path: file, reason: "ALREADY_STAGED" };
458
477
  }
459
- fs.writeFileSync(file, JSON.stringify(op, null, 2), "utf-8");
478
+ atomicWriteFileSync(file, JSON.stringify(op, null, 2));
460
479
  return { staged: true, path: file };
461
480
  }
462
481
 
@@ -498,7 +517,7 @@ function nextBatchSeq(dir, pair) {
498
517
  }
499
518
  const next = (map[pair] || 0) + 1;
500
519
  map[pair] = next;
501
- fs.writeFileSync(p, JSON.stringify(map, null, 2), "utf-8");
520
+ atomicWriteFileSync(p, JSON.stringify(map, null, 2));
502
521
  return next;
503
522
  }
504
523
 
@@ -514,6 +533,18 @@ function nextBatchSeq(dir, pair) {
514
533
  * skipped: { reason: string } | null }}
515
534
  */
516
535
  export function closeBatch(dir, opts = {}) {
536
+ // Serialize the whole staging-scan → batch-write → staging-cleanup section
537
+ // across processes. Without it, two concurrent closes both read the same
538
+ // staged ops, allocate the same per-pair sequence (namespace collision), and
539
+ // batch the same ops twice before clearing staging. Best-effort lock (never
540
+ // hangs); generous timeout since the section includes batch assembly+signing.
541
+ ensureDirs(dir);
542
+ return withFileLock(batchesDir(dir), () => _closeBatchImpl(dir, opts), {
543
+ timeoutMs: 15000,
544
+ });
545
+ }
546
+
547
+ function _closeBatchImpl(dir, opts = {}) {
517
548
  ensureDirs(dir);
518
549
  const cfg = loadCrossChainMtcConfig(dir);
519
550
  const grouped = listStagedOps(dir);
@@ -546,16 +577,14 @@ export function closeBatch(dir, opts = {}) {
546
577
  `${pair}-${String(seq).padStart(6, "0")}`,
547
578
  );
548
579
  fs.mkdirSync(outDir, { recursive: true });
549
- fs.writeFileSync(
580
+ atomicWriteFileSync(
550
581
  path.join(outDir, "landmark.json"),
551
582
  JSON.stringify(result.landmark, null, 2),
552
- "utf-8",
553
583
  );
554
584
  result.envelopes.forEach((env, i) => {
555
- fs.writeFileSync(
585
+ atomicWriteFileSync(
556
586
  path.join(outDir, `envelope-${String(i).padStart(4, "0")}.json`),
557
587
  JSON.stringify(env, null, 2),
558
- "utf-8",
559
588
  );
560
589
  });
561
590
 
@@ -136,14 +136,24 @@ export function decryptFile(inputPath, password, outputPath) {
136
136
  * Check if a file is encrypted with our format.
137
137
  */
138
138
  export function isEncryptedFile(filePath) {
139
+ let fd;
139
140
  try {
140
- const fd = fs.openSync(filePath, "r");
141
+ fd = fs.openSync(filePath, "r");
141
142
  const buf = Buffer.alloc(MAGIC_HEADER.length);
142
143
  fs.readSync(fd, buf, 0, MAGIC_HEADER.length, 0);
143
- fs.closeSync(fd);
144
144
  return buf.equals(MAGIC_HEADER);
145
145
  } catch (_err) {
146
146
  return false;
147
+ } finally {
148
+ // Close in finally so a readSync throw (permission change mid-op, OS error)
149
+ // can't leak the descriptor — repeated failures would otherwise exhaust FDs.
150
+ if (fd !== undefined) {
151
+ try {
152
+ fs.closeSync(fd);
153
+ } catch {
154
+ /* already closed / invalid fd — nothing to release */
155
+ }
156
+ }
147
157
  }
148
158
  }
149
159
 
package/src/lib/dbevo.js CHANGED
@@ -41,6 +41,17 @@ export const SUGGESTION_TYPE = Object.freeze({
41
41
 
42
42
  let _migrations = []; // registered migration definitions
43
43
  let _migrationHistory = []; // executed migration records from DB
44
+
45
+ /**
46
+ * Compare two version strings with NUMERIC collation so "10" sorts AFTER "2".
47
+ * A migration framework must order numeric versions correctly; plain
48
+ * localeCompare()/Array.sort() is lexicographic ("10" < "2"), which would run
49
+ * migration v10 before v2 and corrupt the schema. Numeric collation also leaves
50
+ * zero-padded / date / semver-ish versions ordered as expected.
51
+ */
52
+ function cmpVersion(a, b) {
53
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
54
+ }
44
55
  let _queryLogs = [];
45
56
  let _suggestions = new Map();
46
57
  let _slowQueryThresholdMs = 100;
@@ -147,7 +158,7 @@ export function registerMigration(
147
158
  });
148
159
 
149
160
  // Keep sorted by version
150
- _migrations.sort((a, b) => a.version.localeCompare(b.version));
161
+ _migrations.sort((a, b) => cmpVersion(a.version, b.version));
151
162
 
152
163
  return { registered: true, version, checksum };
153
164
  }
@@ -178,13 +189,13 @@ export function getCurrentVersion(db) {
178
189
  );
179
190
 
180
191
  if (active.length === 0) return null;
181
- return active.sort((a, b) => b.localeCompare(a))[0];
192
+ return active.sort((a, b) => cmpVersion(b, a))[0];
182
193
  }
183
194
 
184
195
  export function getPendingMigrations(db) {
185
196
  const current = getCurrentVersion(db);
186
197
  return _migrations.filter(
187
- (m) => !current || m.version.localeCompare(current) > 0,
198
+ (m) => !current || cmpVersion(m.version, current) > 0,
188
199
  );
189
200
  }
190
201
 
@@ -193,7 +204,7 @@ export function migrateUp(db, targetVersion) {
193
204
  if (pending.length === 0) return { migrated: false, reason: "no_pending" };
194
205
 
195
206
  const toRun = targetVersion
196
- ? pending.filter((m) => m.version.localeCompare(targetVersion) <= 0)
207
+ ? pending.filter((m) => cmpVersion(m.version, targetVersion) <= 0)
197
208
  : pending;
198
209
 
199
210
  if (toRun.length === 0) return { migrated: false, reason: "no_pending" };
@@ -248,13 +259,13 @@ export function migrateDown(db, targetVersion) {
248
259
  .filter((m) => {
249
260
  if (targetVersion) {
250
261
  return (
251
- m.version.localeCompare(current) <= 0 &&
252
- m.version.localeCompare(targetVersion) > 0
262
+ cmpVersion(m.version, current) <= 0 &&
263
+ cmpVersion(m.version, targetVersion) > 0
253
264
  );
254
265
  }
255
266
  return m.version === current;
256
267
  })
257
- .sort((a, b) => b.version.localeCompare(a.version));
268
+ .sort((a, b) => cmpVersion(b.version, a.version));
258
269
 
259
270
  if (toRollBack.length === 0)
260
271
  return { rolledBack: false, reason: "nothing_to_rollback" };
@@ -331,7 +342,7 @@ export function validateMigrations() {
331
342
  if (_migrations.length === 0) return { valid: true, issues: [] };
332
343
 
333
344
  const issues = [];
334
- const versions = _migrations.map((m) => m.version).sort();
345
+ const versions = _migrations.map((m) => m.version).sort(cmpVersion);
335
346
 
336
347
  // Check for gaps in version sequence
337
348
  for (let i = 1; i < versions.length; i++) {
@@ -7,7 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { pipeline } from "node:stream/promises";
10
- import { execSync } from "node:child_process";
10
+ import { execFileSync } from "node:child_process";
11
11
  import ora from "ora";
12
12
  import { GITHUB_RELEASES_URL } from "../constants.js";
13
13
  import { getBinDir, ensureDir } from "./paths.js";
@@ -208,16 +208,31 @@ async function fetchRelease(url) {
208
208
  return response.json();
209
209
  }
210
210
 
211
- function extractZip(zipPath, destDir) {
212
- const p = getPlatform();
213
- if (p === "win32") {
214
- // Use PowerShell on Windows
215
- execSync(
216
- `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`,
217
- { stdio: "ignore" },
211
+ export function extractZip(
212
+ zipPath,
213
+ destDir,
214
+ { execFileImpl = execFileSync, platform = getPlatform() } = {},
215
+ ) {
216
+ // Never interpolate the paths into a shell string — `zipPath`/`destDir` derive
217
+ // from the (remote) release asset name, so a `"`/`$()`/`'` in a path would be a
218
+ // command-injection vector. execFileSync runs the tool directly (no shell), and
219
+ // on Windows the paths go through the environment so the -Command script stays
220
+ // a constant the shell can't be tricked by.
221
+ if (platform === "win32") {
222
+ execFileImpl(
223
+ "powershell",
224
+ [
225
+ "-NoProfile",
226
+ "-Command",
227
+ "Expand-Archive -Path $env:CC_ZIP_SRC -DestinationPath $env:CC_ZIP_DEST -Force",
228
+ ],
229
+ {
230
+ stdio: "ignore",
231
+ env: { ...process.env, CC_ZIP_SRC: zipPath, CC_ZIP_DEST: destDir },
232
+ },
218
233
  );
219
234
  } else {
220
- execSync(`unzip -o "${zipPath}" -d "${destDir}"`, { stdio: "ignore" });
235
+ execFileImpl("unzip", ["-o", zipPath, "-d", destDir], { stdio: "ignore" });
221
236
  }
222
237
  }
223
238