@runeya/runeya 2.0.39 → 2.0.41

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 (206) hide show
  1. package/agent-manager-2I47P7CA.js +7 -0
  2. package/{chunk-PW6A3BLD.js → chunk-NJJMESIY.js} +400 -93
  3. package/chunk-NJJMESIY.js.map +1 -0
  4. package/index.js +1 -1
  5. package/package.json +1 -1
  6. package/{src-A3WSWXEY.js → src-BCBD4MPB.js} +1041 -336
  7. package/src-BCBD4MPB.js.map +1 -0
  8. package/web/assets/{AgentScalarChatInterface.vue-HiYdPZh0.js → AgentScalarChatInterface.vue-bk9hxmaQ.js} +1 -1
  9. package/web/assets/{AgentTerminalPage-BV1OTr-r.js → AgentTerminalPage-B04ZDAX6.js} +1 -1
  10. package/web/assets/AiChatPage-CRVc_0Nu.js +1 -0
  11. package/web/assets/{AppLayout-DW_-KBWL.css → AppLayout-BBifPBJu.css} +1 -1
  12. package/web/assets/AppLayout-DvrAr1Tw.js +1 -0
  13. package/web/assets/{AuditPage-Bc4NZud3.js → AuditPage-B64guLBx.js} +1 -1
  14. package/web/assets/CloudAccountPage-4nEDwbbP.js +1 -0
  15. package/web/assets/CloudAccountPage-C_OlcjXc.js +1 -0
  16. package/web/assets/CloudAccountPage-ChVg2wnJ.css +1 -0
  17. package/web/assets/{CloudMessagingPage-CWcahAog.js → CloudMessagingPage-ClNaEqU1.js} +1 -1
  18. package/web/assets/{DashboardPage-nhKxGmnu.js → DashboardPage-8z5zI4At.js} +1 -1
  19. package/web/assets/{EditorIcon-vSDu502Z.js → EditorIcon-QuM8Jw0F.js} +1 -1
  20. package/web/assets/{EnvironmentForm-8WEcxUYY.js → EnvironmentForm-CctbC-gb.js} +1 -1
  21. package/web/assets/{EnvironmentFormPage-CpwzzSeB.js → EnvironmentFormPage-BOLckpdf.js} +1 -1
  22. package/web/assets/{EnvironmentListPage-Dyq7x-l_.js → EnvironmentListPage-CWbCtHUP.js} +1 -1
  23. package/web/assets/{EnvironmentSelector-BXe42bF9.js → EnvironmentSelector-CSSr9Uoa.js} +1 -1
  24. package/web/assets/{KanbanBoardPage-DHAWtkkJ.js → KanbanBoardPage-wjk3mufd.js} +3 -3
  25. package/web/assets/{KanbanListPage-CjQfqoXf.js → KanbanListPage-CBa8MOZq.js} +1 -1
  26. package/web/assets/LaunchProjectPage-CnhTqUn6.js +1 -0
  27. package/web/assets/LaunchProjectPage-CxXC2U3O.css +1 -0
  28. package/web/assets/{LaunchServicesPage-DYLGFHSm.js → LaunchServicesPage-oHDlEgqh.js} +1 -1
  29. package/web/assets/{LogViewer-CEQDlzBl.js → LogViewer-CbXki1mv.js} +1 -1
  30. package/web/assets/{ProcessControls-RuqRu4r_.js → ProcessControls-E3Ka8erw.js} +1 -1
  31. package/web/assets/{ProcessDetailPage-CnK8kLHx.js → ProcessDetailPage-lofKmzDr.js} +1 -1
  32. package/web/assets/{ProcessMetrics-tNC5ggO0.js → ProcessMetrics-DNIFOkvO.js} +1 -1
  33. package/web/assets/ProjectDetailPage-B42imx75.css +1 -0
  34. package/web/assets/ProjectDetailPage-Dz2WCS0H.js +1 -0
  35. package/web/assets/{ProjectListPage-BGihYeDx.js → ProjectListPage-MH2MfkaC.js} +1 -1
  36. package/web/assets/{RandomJobTestPage-BgjN2Lqt.js → RandomJobTestPage-C1yrM6Zy.js} +1 -1
  37. package/web/assets/{ScenarioEditorPage-nU_bK6Wm.js → ScenarioEditorPage-Dy6iy9r8.js} +1 -1
  38. package/web/assets/{SendToChatDialog-DG6GidKi.js → SendToChatDialog-DCpOcZ4B.js} +1 -1
  39. package/web/assets/{ServiceControlsBar-lvV0380I.js → ServiceControlsBar-B3dt-TpS.js} +1 -1
  40. package/web/assets/{ServiceDetailPage-gG_F4CpK.js → ServiceDetailPage-Dt4DRm6q.js} +1 -1
  41. package/web/assets/{ServiceFormPage-5pLarqaj.js → ServiceFormPage-ks_mGrTT.js} +1 -1
  42. package/web/assets/{SettingsPage-DGtNwgNF.js → SettingsPage-Ca--x3I2.js} +4 -4
  43. package/web/assets/{SetupPage-zLbZ7HFD.js → SetupPage-DnbrJAZf.js} +1 -1
  44. package/web/assets/{UnlockPage-BNUgOO2j.js → UnlockPage-Brv5TJMY.js} +1 -1
  45. package/web/assets/{VariablesTable-Dh7ySbKc.js → VariablesTable-DWg01MKw.js} +1 -1
  46. package/web/assets/{VersionBadge-Bcr43sQb.js → VersionBadge-DzEXeyD9.js} +1 -1
  47. package/web/assets/WikiPage-GjwXa8Oq.js +1 -0
  48. package/web/assets/{WikiView-bePXeeW1.js → WikiView-CulrPak1.js} +1 -1
  49. package/web/assets/{add-scalar-classes-BZ33Hgh4.js → add-scalar-classes-CqTKiXd8.js} +3 -3
  50. package/web/assets/auth-errors-CUm_h-nn.js +1 -0
  51. package/web/assets/{autocomplete-Mxh2ZegH.js → autocomplete-DEB2Qu-k.js} +1 -1
  52. package/web/assets/{basic-shape-diamond.svg-C_plmghk.js → basic-shape-diamond.svg-KT7ENzRk.js} +1 -1
  53. package/web/assets/{basic-shape-hexagon.svg-A1qvevt_.js → basic-shape-hexagon.svg-BG-V5Q5m.js} +1 -1
  54. package/web/assets/{basic-shape-primary-circle-ellipse-round.svg-Df8UWBuG.js → basic-shape-primary-circle-ellipse-round.svg-DkS-BSnd.js} +1 -1
  55. package/web/assets/{basic-shape-primary-square-rectangle.svg-hAiISQ_N.js → basic-shape-primary-square-rectangle.svg-JAOlu5yc.js} +1 -1
  56. package/web/assets/{basic-shape-shield.svg-j04XHSeq.js → basic-shape-shield.svg-BhPI20FC.js} +1 -1
  57. package/web/assets/{browser-_K3D-Q-7.js → browser-D77gc3Rr.js} +1 -1
  58. package/web/assets/{browser-B4MIWpa7.js → browser-Dh2HVE1I.js} +1 -1
  59. package/web/assets/{card-DGaeJPst.js → card-DIJopohC.js} +1 -1
  60. package/web/assets/{checkbox-C5uNb3Rm.js → checkbox-gEptCzM4.js} +1 -1
  61. package/web/assets/{chip-DDjgi9qj.js → chip-Rl8eKYOz.js} +1 -1
  62. package/web/assets/{client-D9DmTFwP.js → client-CPmn2crt.js} +1 -1
  63. package/web/assets/{cloud-chat.store-BPUP_Hxk.js → cloud-chat.store-DoQXE_CW.js} +1 -1
  64. package/web/assets/{cloud-session-B1oqwDJc.js → cloud-session-GnprlXNn.js} +1 -1
  65. package/web/assets/{column-sUzyGFk3.js → column-CHn9iHRy.js} +1 -1
  66. package/web/assets/{computer-device-desktop-monitor.svg-BBuhFhn-.js → computer-device-desktop-monitor.svg-DRH73_n8.js} +1 -1
  67. package/web/assets/{computer-device-desktop.svg-LL5HezrB.js → computer-device-desktop.svg-DXLn48TE.js} +1 -1
  68. package/web/assets/{computer-device-laptop.svg-C4xmfwdM.js → computer-device-laptop.svg-Cx3qvopX.js} +1 -1
  69. package/web/assets/{computer-device-mobile-phone-android-samsung-back.svg-B-uD42mr.js → computer-device-mobile-phone-android-samsung-back.svg-91VumZ5n.js} +1 -1
  70. package/web/assets/{computer-device-mobile-phone-android-samsung.svg-qPPbtnL9.js → computer-device-mobile-phone-android-samsung.svg-B1rGCVDW.js} +1 -1
  71. package/web/assets/{computer-device-mobile-phone-iphone-x-back.svg-B1Xa9B8p.js → computer-device-mobile-phone-iphone-x-back.svg-BpI2fov9.js} +1 -1
  72. package/web/assets/{computer-device-mobile-phone-iphone-x.svg-B9zWUZbf.js → computer-device-mobile-phone-iphone-x.svg-CgXARUwD.js} +1 -1
  73. package/web/assets/{computer-device-mobile-tablet-touch.svg-CriHRQuM.js → computer-device-mobile-tablet-touch.svg-CCbrFsKS.js} +1 -1
  74. package/web/assets/{computer-device-mobile-tablet.svg-BOKVzupN.js → computer-device-mobile-tablet.svg-DjMvb6nR.js} +1 -1
  75. package/web/assets/{computer-device-network-ethernet-cat6.svg-D0L6yx6b.js → computer-device-network-ethernet-cat6.svg-Bqaea3Cl.js} +1 -1
  76. package/web/assets/{computer-device-network-lan-www.svg-n9m6VZVC.js → computer-device-network-lan-www.svg-CJxUMWWI.js} +1 -1
  77. package/web/assets/{computer-device-network-wifi-connection.svg-BqJzn-5W.js → computer-device-network-wifi-connection.svg-BUWGehfn.js} +1 -1
  78. package/web/assets/{computer-device-network-wifi-router.svg-DKcOeajJ.js → computer-device-network-wifi-router.svg-BC12dD1O.js} +1 -1
  79. package/web/assets/{cssMode-CLuhsa-I.js → cssMode-Df4SqVsZ.js} +1 -1
  80. package/web/assets/{divider-UPIM4dQl.js → divider-xI7dNZxb.js} +1 -1
  81. package/web/assets/{ecology-science-erlenmeyer-flask.svg-DyL2i1-k.js → ecology-science-erlenmeyer-flask.svg-DsEq5QkC.js} +1 -1
  82. package/web/assets/{fieldset-CIZfn4mb.js → fieldset-DHi1E3Rv.js} +1 -1
  83. package/web/assets/{freemarker2-BQV9kA1Y.js → freemarker2-DcSf8AuF.js} +1 -1
  84. package/web/assets/{handlebars-ib1US5Pw.js → handlebars-B866hvQq.js} +1 -1
  85. package/web/assets/{html-D-hYOiBO.js → html-Bqs2Jvnl.js} +1 -1
  86. package/web/assets/{htmlMode-WY_1KZ8Q.js → htmlMode-ZcQnxlbi.js} +1 -1
  87. package/web/assets/{image-flash-lightning.svg-wPJhlaEM.js → image-flash-lightning.svg-BnJa70PB.js} +1 -1
  88. package/web/assets/{image-picture-flower.svg-C15wjNsW.js → image-picture-flower.svg-CnHedFRr.js} +1 -1
  89. package/web/assets/{index-CcDpHMIW.js → index-DKBnMXVN.js} +259 -259
  90. package/web/assets/{interface-alert-exclamation-diamond.svg-C2MhzWE9.js → interface-alert-exclamation-diamond.svg-COmucSk_.js} +1 -1
  91. package/web/assets/{interface-alert-exclamation-triangle-warning.svg-D8heEy48.js → interface-alert-exclamation-triangle-warning.svg-M36BIAo6.js} +1 -1
  92. package/web/assets/{interface-alert-information-circle.svg-zrf3-9VP.js → interface-alert-information-circle.svg-1kMmqhO2.js} +1 -1
  93. package/web/assets/{interface-award-crown.svg-CT401HIW.js → interface-award-crown.svg-DptGWkkU.js} +1 -1
  94. package/web/assets/{interface-bookmark.svg-CqHV5l4k.js → interface-bookmark-tag.svg-D7TAAj3n.js} +1 -1
  95. package/web/assets/{interface-bookmark-tag.svg-h5CZEHt8.js → interface-bookmark.svg-ViiyYT_L.js} +1 -1
  96. package/web/assets/{interface-calendar-date-one.svg-DUdLsM_8.js → interface-calendar-date-one.svg-CWorrxsZ.js} +1 -1
  97. package/web/assets/{interface-content-book-open-pages.svg-DNR8SCd0.js → interface-content-book-open-pages.svg-Br_e_4Im.js} +1 -1
  98. package/web/assets/{interface-content-book-page.svg-Da9e-SX6.js → interface-content-book-page.svg-Du0uHoZ7.js} +1 -1
  99. package/web/assets/{interface-content-file.svg-BZM7jMWF.js → interface-content-file.svg-C972356w.js} +1 -1
  100. package/web/assets/{interface-content-folder.svg-BisjJGhi.js → interface-content-folder.svg-Erjx4HGU.js} +1 -1
  101. package/web/assets/interface-content-folder.svg-nZraYlQ1.js +1 -0
  102. package/web/assets/{interface-copy-clipboard.svg-AIcyJXPF.js → interface-copy-clipboard.svg-B9XvjK6U.js} +1 -1
  103. package/web/assets/{interface-edit-attachment.svg-XSvdzubS.js → interface-edit-attachment.svg-BWe41qK5.js} +1 -1
  104. package/web/assets/{interface-edit-binocular.svg-CCB9rIw_.js → interface-edit-binocular.svg-ChJsM_P7.js} +1 -1
  105. package/web/assets/{interface-edit-magic-wand.svg-Dd4afdpz.js → interface-edit-magic-wand.svg-BUs2NS7s.js} +1 -1
  106. package/web/assets/{interface-edit-tool-paint-roller.svg-DpO7rr7P.js → interface-edit-tool-paint-roller.svg-CdrYKzH6.js} +1 -1
  107. package/web/assets/{interface-edit-tool-pencil.svg-DbKruCrP.js → interface-edit-tool-pencil.svg-C8on9j4c.js} +1 -1
  108. package/web/assets/{interface-favorite-award.svg-DNIpxtuy.js → interface-favorite-award.svg-Bh_lIFBs.js} +1 -1
  109. package/web/assets/{interface-favorite-flag.svg-CgGWpm8Y.js → interface-favorite-flag.svg-BfecybVt.js} +1 -1
  110. package/web/assets/{interface-favorite-heart.svg-Bke1OXMv.js → interface-favorite-heart.svg-DDreuf_y.js} +1 -1
  111. package/web/assets/{interface-favorite-star.svg-DgwsiTjP.js → interface-favorite-star.svg-BJNTsPaT.js} +1 -1
  112. package/web/assets/{interface-favorite-stars-sparkles.svg-dKGlP2sr.js → interface-favorite-stars-sparkles.svg-CKOBWT4J.js} +1 -1
  113. package/web/assets/{interface-hierarchy-flowchart.svg-Bved245R.js → interface-hierarchy-flowchart.svg-7rufYb_n.js} +1 -1
  114. package/web/assets/{interface-home-house.svg-DJIEYhIa.js → interface-home-house.svg-CrKToh2q.js} +1 -1
  115. package/web/assets/{interface-hyperlink.svg-BBh6NFX7.js → interface-hyperlink.svg-BOjFniK_.js} +1 -1
  116. package/web/assets/{interface-lighting-brightness.svg-NqCt5nK8.js → interface-lighting-brightness.svg-B3-sdI-a.js} +1 -1
  117. package/web/assets/{interface-lock-closed.svg-CclDxo01.js → interface-lock-closed.svg-D3pw1rIY.js} +1 -1
  118. package/web/assets/{interface-lock-open-unlock.svg-CYPJVug8.js → interface-lock-open-unlock.svg-CJ_XAmbt.js} +1 -1
  119. package/web/assets/{interface-login-key.svg-DaW7pEY5.js → interface-login-key.svg-D--OEw7I.js} +1 -1
  120. package/web/assets/{interface-search.svg-j89Vz_N5.js → interface-search.svg-_5NliDrL.js} +1 -1
  121. package/web/assets/{interface-setting-cog.svg-CY2WkccU.js → interface-setting-cog.svg-uK8uMEZ2.js} +1 -1
  122. package/web/assets/{interface-share-megaphone-bullhorn.svg-C927tb6q.js → interface-share-megaphone-bullhorn.svg-CIdM6tKR.js} +1 -1
  123. package/web/assets/{interface-share-rocket.svg-B3sZvIoQ.js → interface-share-rocket.svg-CN-zhWTK.js} +1 -1
  124. package/web/assets/{interface-share-satellite.svg-CQ8A4dXG.js → interface-share-satellite.svg-CVTJ9m_O.js} +1 -1
  125. package/web/assets/{interface-share-space-ship.svg-BTGyj986.js → interface-share-space-ship.svg-jIDLHLi6.js} +1 -1
  126. package/web/assets/{interface-share.svg-BSQ8cEXV.js → interface-share.svg-33frw3Wv.js} +1 -1
  127. package/web/assets/{interface-signal-square.svg-1h2-Znr2.js → interface-signal-square.svg-Wg7BKl9x.js} +1 -1
  128. package/web/assets/{interface-time-clock-circle.svg-CgUcH4er.js → interface-time-clock-circle.svg-CZ6md3Ju.js} +1 -1
  129. package/web/assets/{interface-time-hour-glass.svg-Biz9ylcj.js → interface-time-hour-glass.svg-dRVhEG12.js} +1 -1
  130. package/web/assets/{interface-users-multiple.svg-B2SNNyd6.js → interface-users-multiple.svg-D7jrCQNN.js} +1 -1
  131. package/web/assets/{interface-weather-moon.svg-DFIwJsKU.js → interface-weather-moon.svg-BYWPWUQx.js} +1 -1
  132. package/web/assets/{javascript-w0UZtcmg.js → javascript-BATb-HYh.js} +1 -1
  133. package/web/assets/{jsonMode-CJUdXIyV.js → jsonMode-CP6RXg9k.js} +1 -1
  134. package/web/assets/{liquid-CjedKIbd.js → liquid-BCpH2Ett.js} +1 -1
  135. package/web/assets/{lspLanguageFeatures-DGvsogF_.js → lspLanguageFeatures-BWSdWSw7.js} +1 -1
  136. package/web/assets/{mail-chat-bubble-square.svg-D16l2agS.js → mail-chat-bubble-square.svg-BwFA9sJS.js} +1 -1
  137. package/web/assets/{mail-send-email-paper-airplane.svg-Bz-my4zw.js → mail-send-email-paper-airplane.svg-DWRvHccC.js} +1 -1
  138. package/web/assets/{mail-send-envelope.svg-Cgh2a1hO.js → mail-send-envelope.svg-6QBx4SLE.js} +1 -1
  139. package/web/assets/{mdx-Y5OqFrRV.js → mdx-JagJo-dt.js} +1 -1
  140. package/web/assets/{message-Bh-eAVAb.js → message-TwI_r5mZ.js} +1 -1
  141. package/web/assets/{minus-CxgjeEyu.js → minus-oYqzuR4a.js} +1 -1
  142. package/web/assets/modal-BEYg-JLN.js +1 -0
  143. package/web/assets/{modal-ciXJUOol.js → modal-CFM8h6A5.js} +1 -1
  144. package/web/assets/{money-cashier-receipt.svg-BP5bjawX.js → money-cashier-receipt.svg-Cl2LyS7q.js} +1 -1
  145. package/web/assets/{money-currency-dollar-pay.svg-vKtIhvs8.js → money-currency-dollar-pay.svg-DiytkCh0.js} +1 -1
  146. package/web/assets/{money-graph-arrow-increase.svg-CaeoOB93.js → money-graph-arrow-increase.svg-D4qMSRlp.js} +1 -1
  147. package/web/assets/{money-graph-bar-chart-increase.svg-CjRjHwbF.js → money-graph-bar-chart-increase.svg-CdCIWvvQ.js} +1 -1
  148. package/web/assets/{multiselect-CsE4Xly1.js → multiselect-CSyl93Ho.js} +1 -1
  149. package/web/assets/{nature-ecology-leaf.svg-umy4cUUw.js → nature-ecology-leaf.svg-GjBsj54D.js} +1 -1
  150. package/web/assets/{password-Brra1Twl.js → password-xomJK8iH.js} +1 -1
  151. package/web/assets/{phone-telephone.svg-DQ18gSw7.js → phone-telephone.svg-CBtGAjHv.js} +1 -1
  152. package/web/assets/{plus-e_D9UcW9.js → plus-C8MR9x_j.js} +1 -1
  153. package/web/assets/process.store-vy8KqH_z.js +1 -0
  154. package/web/assets/{programming-bug.svg-BL4Hus_Q.js → programming-bug.svg-BHCuUnau.js} +1 -1
  155. package/web/assets/{programming-cloud.svg-DrTlrDBe.js → programming-cloud.svg-DPdcvBlm.js} +1 -1
  156. package/web/assets/{programming-computer-database-server.svg-CnwbhD5W.js → programming-computer-database-server.svg-CaruocqU.js} +1 -1
  157. package/web/assets/{programming-computer-database.svg-GymDSTl9.js → programming-computer-database.svg-DFSRGAGE.js} +1 -1
  158. package/web/assets/{programming-module-four-layout.svg-DspyCTiu.js → programming-module-four-layout.svg-GUN-7Tqn.js} +1 -1
  159. package/web/assets/{programming-module-three.svg-BuYXTrWu.js → programming-module-three.svg-DYC-KU3a.js} +1 -1
  160. package/web/assets/{programming-module.svg-DIlRMVbW.js → programming-module.svg-NW_oL81i.js} +1 -1
  161. package/web/assets/{programming-script-code.svg-DPXJcL07.js → programming-script-code.svg-BPISkQtk.js} +1 -1
  162. package/web/assets/{python-3fAbpxqq.js → python-CXVm4gDt.js} +1 -1
  163. package/web/assets/{radiobutton-w7t3rGDJ.js → radiobutton-BLlyiAho.js} +1 -1
  164. package/web/assets/{razor-GDM0jSNH.js → razor-BcnFJswZ.js} +1 -1
  165. package/web/assets/router-Z6Tmw2JR.js +1 -0
  166. package/web/assets/{settings.store-D0eLKI4A.js → settings.store-CPAOxEyN.js} +1 -1
  167. package/web/assets/{shopping-cart.svg-B1y-4w0M.js → shopping-cart.svg-CA8D1Bj1.js} +1 -1
  168. package/web/assets/{shopping-gift-present.svg-DJ3mGWql.js → shopping-gift-present.svg-DA0s4ges.js} +1 -1
  169. package/web/assets/{shopping-shipping-box-parcel-package.svg-Bj3iHRT5.js → shopping-shipping-box-parcel-package.svg-BkLM7KC9.js} +1 -1
  170. package/web/assets/{src-wVYcxep_.js → src-DQG3PBVG.js} +1 -1
  171. package/web/assets/{src-BotO3dQ7.js → src-Dyv5MRfg.js} +1 -1
  172. package/web/assets/{tabpanel-DOZdp_HS.js → tabpanel-CNQhqmGV.js} +1 -1
  173. package/web/assets/{tag-new-circle.svg-Bb_ByVey.js → tag-new-circle.svg-BVZMrQWv.js} +1 -1
  174. package/web/assets/{travel-map-earth-globe.svg-Mso-K6XY.js → travel-map-earth-globe.svg-BZXWAlAZ.js} +1 -1
  175. package/web/assets/trpc-DVqzpQq9.js +1 -0
  176. package/web/assets/{tsMode-BUHQk9ZI.js → tsMode-BjuYVSR3.js} +1 -1
  177. package/web/assets/{typescript-Quc5d9qP.js → typescript-Bw5AHC7f.js} +1 -1
  178. package/web/assets/useAiChatDrawer-BCH3idQo.js +1 -0
  179. package/web/assets/useCloudOrg-A1wW_KrI.css +1 -0
  180. package/web/assets/useCloudOrg-BthYAZho.js +1 -0
  181. package/web/assets/{xml-CkYBw41O.js → xml-ESsLInqI.js} +1 -1
  182. package/web/assets/{yaml-CmSvkxzN.js → yaml-CEuOVTz-.js} +1 -1
  183. package/web/index.html +1 -1
  184. package/agent-manager-U3HLZ6QZ.js +0 -7
  185. package/chunk-PW6A3BLD.js.map +0 -1
  186. package/src-A3WSWXEY.js.map +0 -1
  187. package/web/assets/AiChatPage-Ct_f2ned.js +0 -1
  188. package/web/assets/AppLayout-BX_Buqtz.js +0 -1
  189. package/web/assets/CloudAccountPage-BqnCKZxn.js +0 -1
  190. package/web/assets/CloudAccountPage-CRN-tnI1.css +0 -1
  191. package/web/assets/CloudAccountPage-kTdYEt2S.js +0 -1
  192. package/web/assets/LaunchProjectPage-57kJRakH.css +0 -1
  193. package/web/assets/LaunchProjectPage-Bfs_AIgu.js +0 -1
  194. package/web/assets/ProjectDetailPage-BxLE6-54.css +0 -1
  195. package/web/assets/ProjectDetailPage-Cd3-8PAI.js +0 -1
  196. package/web/assets/WikiPage-DDx_jeaY.js +0 -1
  197. package/web/assets/auth-errors-BJV46AcQ.js +0 -1
  198. package/web/assets/interface-content-folder.svg-p32kmmD9.js +0 -1
  199. package/web/assets/modal-CIFHhhV7.js +0 -1
  200. package/web/assets/process.store-BhNwQ2dn.js +0 -1
  201. package/web/assets/router-C6yOFC1g.js +0 -1
  202. package/web/assets/trpc-BWtKBok1.js +0 -1
  203. package/web/assets/useAiChatDrawer-CRJCTTTO.js +0 -1
  204. package/web/assets/useCloudOrg-CBr3kqWH.js +0 -1
  205. package/web/assets/useCloudOrg-Gz6ek71z.css +0 -1
  206. /package/{agent-manager-U3HLZ6QZ.js.map → agent-manager-2I47P7CA.js.map} +0 -0
@@ -31,6 +31,7 @@ import {
31
31
  agentManager,
32
32
  agentSpawner,
33
33
  agentStore,
34
+ allVaultRoots,
34
35
  clearEncryptionKey,
35
36
  clearPendingLegacyKey,
36
37
  decryptValue,
@@ -38,9 +39,11 @@ import {
38
39
  encryptValue,
39
40
  encryptVariableSlots,
40
41
  ensureEncryptionKey,
42
+ ensureLocalVault,
41
43
  env,
42
44
  environmentStore,
43
45
  extractEncryptedVariableSlots,
46
+ forgetProject,
44
47
  getCollection,
45
48
  getEncryptionKey,
46
49
  getPendingLegacyKey,
@@ -48,17 +51,31 @@ import {
48
51
  hasPendingLegacyKey,
49
52
  isCiMode,
50
53
  isEncryptedValue,
54
+ keyRoot,
55
+ keyedFilePath,
56
+ legacyMachineFilePath,
57
+ listCloudVaults,
51
58
  loadFromEnv,
59
+ localVaultRoot,
60
+ machineFilePath,
61
+ machineRoot,
52
62
  nativeParsers,
63
+ orgVaultRoot,
64
+ projectVaultRoot,
53
65
  readEntity,
66
+ registerProjectLoader,
67
+ rootOfProject,
54
68
  serviceStore,
55
69
  setEncryptionKey,
56
70
  setEncryptionKeyInMemory,
57
71
  setPendingLegacyKey,
72
+ setProjectServices,
73
+ setProjectVault,
58
74
  stableEncryptValue,
59
75
  traefikManager,
76
+ vaultFor,
60
77
  writeEntity
61
- } from "./chunk-PW6A3BLD.js";
78
+ } from "./chunk-NJJMESIY.js";
62
79
 
63
80
  // ../server/src/create-server.ts
64
81
  import express from "express";
@@ -101,7 +118,7 @@ async function verifyJwt(token) {
101
118
  // ../server/src/services/api-key-store.ts
102
119
  import { randomBytes as randomBytes2, randomUUID, timingSafeEqual } from "crypto";
103
120
  import { readFile, writeFile, rename, mkdir, chmod } from "fs/promises";
104
- import { join } from "path";
121
+ import { dirname } from "path";
105
122
  function maskApiKey(apiKey) {
106
123
  const { key, ...rest } = apiKey;
107
124
  return { ...rest, keyPreview: key.slice(-4) };
@@ -111,15 +128,19 @@ var FILE_PRIVATE = "api-keys.private.json";
111
128
  var ApiKeyStore = class {
112
129
  keys = /* @__PURE__ */ new Map();
113
130
  loaded = false;
131
+ /**
132
+ * Encrypted at rest, so the file sits beside the key that opens it — read
133
+ * under a different key it would decrypt to blanks, then be saved back blank.
134
+ */
114
135
  filePath(isPublic) {
115
- return join(env.DATA_DIR, isPublic ? FILE_PUBLIC : FILE_PRIVATE);
136
+ return keyedFilePath(isPublic ? FILE_PUBLIC : FILE_PRIVATE);
116
137
  }
117
138
  async ensureDir() {
118
- await mkdir(env.DATA_DIR, { recursive: true });
139
+ await mkdir(dirname(await this.filePath(true)), { recursive: true });
119
140
  }
120
141
  async persistFile(isPublic) {
121
142
  await this.ensureDir();
122
- const path = this.filePath(isPublic);
143
+ const path = await this.filePath(isPublic);
123
144
  const keys = Array.from(this.keys.values()).filter((k) => k.isPublic === isPublic).map((k) => ({ ...k, key: encryptValue(k.key) }));
124
145
  const tmp = path + ".tmp";
125
146
  await writeFile(tmp, JSON.stringify({ keys }, null, 2), "utf-8");
@@ -145,8 +166,8 @@ var ApiKeyStore = class {
145
166
  async load() {
146
167
  if (this.loaded) return;
147
168
  this.loaded = true;
148
- await this.loadFile(this.filePath(true));
149
- await this.loadFile(this.filePath(false));
169
+ await this.loadFile(await this.filePath(true));
170
+ await this.loadFile(await this.filePath(false));
150
171
  }
151
172
  /**
152
173
  * Keys as exposed to callers — secrets stripped. Use `reveal` to obtain one
@@ -419,53 +440,201 @@ import { TRPCError as TRPCError2 } from "@trpc/server";
419
440
  // ../server/src/services/project-store.ts
420
441
  import { randomUUID as randomUUID2 } from "crypto";
421
442
  import { EventEmitter } from "events";
422
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
443
+ import { readFile as readFile2, writeFile as writeFile3, rename as rename2, mkdir as mkdir2, chmod as chmod2, rm } from "fs/promises";
423
444
  import { join as join2 } from "path";
424
- var FILENAME = "projects.json";
445
+
446
+ // ../server/src/utils/ensure-data-gitignore.ts
447
+ import { writeFile as writeFile2 } from "fs/promises";
448
+ import { join } from "path";
449
+ var GITIGNORE_CONTENT = `
450
+ #################
451
+ # Do not edit this file #
452
+ #################
453
+
454
+
455
+ # Encryption key (contains secrets \u2014 never commit)
456
+ .encryption.key
457
+ .encryption.key.enc
458
+
459
+
460
+ # Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
461
+ !.runeya-verify
462
+
463
+ # This machine's cloud session token (encrypted, and specific to this install)
464
+ cloud-session.json
465
+
466
+ # Backup in local only
467
+ backup-*
468
+
469
+ # Per-user environment overrides (never commit \u2014 each dev has their own)
470
+ environment-overrides.json
471
+
472
+ # Private user data (conversations, personal state \u2014 never commit)
473
+ *.private
474
+ *.private.json*
475
+ conversations/
476
+ conversations.private.json
477
+
478
+
479
+
480
+ # Runtime logs
481
+ *.log
482
+ *.log.*
483
+ `;
484
+ async function ensureDataGitignore(dataDir) {
485
+ const gitignorePath = join(dataDir, ".gitignore");
486
+ await writeFile2(gitignorePath, GITIGNORE_CONTENT, "utf-8");
487
+ }
488
+
489
+ // ../server/src/services/project-store.ts
490
+ var FILENAME = "project.json";
491
+ var LEGACY_FILENAME = "projects.json";
425
492
  function toDiskForm(project) {
426
493
  const { activeEnvironmentId: _runtimeOnly, ...rest } = project;
427
494
  return rest;
428
495
  }
496
+ async function readLocalProject() {
497
+ const root = localVaultRoot();
498
+ let current = null;
499
+ try {
500
+ const raw = await readFile2(join2(root, FILENAME), "utf-8");
501
+ const parsed = ProjectSchema.safeParse(JSON.parse(raw));
502
+ if (parsed.success) current = parsed.data;
503
+ } catch {
504
+ }
505
+ let legacy;
506
+ try {
507
+ legacy = JSON.parse(await readFile2(join2(root, LEGACY_FILENAME), "utf-8"));
508
+ } catch {
509
+ return { local: current, strayCloud: [] };
510
+ }
511
+ const strayCloud = legacy.filter((p) => p.orgId);
512
+ if (current) return { local: current, strayCloud };
513
+ const locals = legacy.filter((p) => !p.orgId);
514
+ if (locals.length > 1) {
515
+ const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
516
+ throw new Error(
517
+ `${join2(root, LEGACY_FILENAME)} holds ${locals.length} local projects: ${names}. A directory now holds a single local project \u2014 move the others to their own directory, or into an organization, then start again.`
518
+ );
519
+ }
520
+ return { local: locals[0] ?? null, strayCloud };
521
+ }
522
+ async function readCloudProjects() {
523
+ const found = [];
524
+ for (const { orgId, projectId, root } of await listCloudVaults()) {
525
+ try {
526
+ const raw = await readFile2(join2(root, FILENAME), "utf-8");
527
+ const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });
528
+ if (parsed.success) found.push({ project: parsed.data, root });
529
+ } catch {
530
+ }
531
+ }
532
+ return found;
533
+ }
534
+ var LocalProjectExistsError = class extends Error {
535
+ constructor(projectName) {
536
+ super(
537
+ `This directory already holds the local project "${projectName}". A directory holds one local project: use another directory, or create the project in an organization.`
538
+ );
539
+ this.projectName = projectName;
540
+ this.name = "LocalProjectExistsError";
541
+ }
542
+ code = "LOCAL_PROJECT_EXISTS";
543
+ };
429
544
  var ProjectStore = class {
430
545
  projects = /* @__PURE__ */ new Map();
431
546
  loaded = false;
432
547
  /** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
433
548
  events = new EventEmitter();
434
- getFilePath() {
435
- return join2(env.DATA_DIR, FILENAME);
549
+ getFilePath(root = localVaultRoot()) {
550
+ return join2(root, FILENAME);
436
551
  }
437
- async ensureDir() {
438
- await mkdir2(env.DATA_DIR, { recursive: true });
552
+ async writeAtomic(filePath, data) {
553
+ const tmpPath = filePath + ".tmp";
554
+ await writeFile3(tmpPath, data, "utf-8");
555
+ await rename2(tmpPath, filePath);
556
+ await chmod2(filePath, 384);
557
+ }
558
+ /** Adopt a project read from disk, remembering which vault it came from. */
559
+ async absorb(p, root) {
560
+ const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
561
+ const defaultEnv = projectEnvs.find((e) => e.default);
562
+ this.projects.set(p.id, {
563
+ ...p,
564
+ environmentIds: p.environmentIds ?? [],
565
+ groupOrder: p.groupOrder ?? [],
566
+ // Prefer the env marked default, fall back to first env
567
+ activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
568
+ });
569
+ setProjectVault(p.id, root);
570
+ setProjectServices(p.id, p.serviceIds ?? []);
439
571
  }
572
+ /**
573
+ * Read every vault: the launch directory, then each organization's projects.
574
+ *
575
+ * Cloud vaults are discovered by walking `~/.runeya/orgs/*/projects/*` rather
576
+ * than from an index. An index is one more thing that can disagree with the
577
+ * disk — and a project present on disk but missing from it is exactly the
578
+ * "already here, still shown as downloadable" bug.
579
+ */
440
580
  async load() {
441
581
  if (this.loaded) return;
442
- try {
443
- const raw = await readFile2(this.getFilePath(), "utf-8");
444
- const data = JSON.parse(raw);
445
- for (const p of data) {
446
- const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
447
- const defaultEnv = projectEnvs.find((e) => e.default);
448
- this.projects.set(p.id, {
449
- ...p,
450
- environmentIds: p.environmentIds ?? [],
451
- groupOrder: p.groupOrder ?? [],
452
- // Prefer the env marked default, fall back to first env
453
- activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
454
- });
582
+ this.loaded = true;
583
+ const { local, strayCloud } = await readLocalProject();
584
+ if (local) await this.absorb(local, localVaultRoot());
585
+ let migrated = false;
586
+ for (const project of strayCloud) {
587
+ try {
588
+ await this.absorb(project, vaultFor(project).root);
589
+ migrated = true;
590
+ } catch {
455
591
  }
456
- } catch {
457
592
  }
458
- this.loaded = true;
593
+ for (const { project, root } of await readCloudProjects()) {
594
+ await this.absorb(project, root);
595
+ }
596
+ if (migrated) {
597
+ await this.save();
598
+ await serviceStore.relocateAll();
599
+ await environmentStore.relocateAll();
600
+ }
459
601
  }
602
+ /**
603
+ * Persist every vault this store knows about.
604
+ *
605
+ * Writing all of them rather than only the one that changed keeps a project
606
+ * that moved between vaults — a local project adopted by an org, a cloud
607
+ * project pulled for the first time — from lingering in the vault it left.
608
+ */
460
609
  async save() {
461
- await this.ensureDir();
462
- const filePath = this.getFilePath();
463
- const tmpPath = filePath + ".tmp";
464
- const projectsToSave = Array.from(this.projects.values()).map(toDiskForm);
465
- const data = JSON.stringify(projectsToSave, null, 2);
466
- await writeFile2(tmpPath, data, "utf-8");
467
- await rename2(tmpPath, filePath);
468
- await chmod2(filePath, 384);
610
+ const locals = [];
611
+ const cloud = [];
612
+ for (const project of this.projects.values()) {
613
+ if (!project.orgId) locals.push(project);
614
+ else cloud.push({ project, root: vaultFor(project).root });
615
+ }
616
+ if (locals.length > 1) {
617
+ const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
618
+ throw new Error(
619
+ `Refusing to save ${locals.length} local projects in one directory: ${names}. A directory holds a single local project.`
620
+ );
621
+ }
622
+ const [local] = locals;
623
+ if (local) {
624
+ await ensureLocalVault();
625
+ await ensureDataGitignore(localVaultRoot());
626
+ await this.writeAtomic(this.getFilePath(), JSON.stringify(toDiskForm(local), null, 2));
627
+ } else {
628
+ await rm(this.getFilePath(), { force: true }).catch(() => {
629
+ });
630
+ }
631
+ await rename2(join2(localVaultRoot(), LEGACY_FILENAME), join2(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
632
+ });
633
+ for (const { project, root } of cloud) {
634
+ await mkdir2(root, { recursive: true });
635
+ await this.writeAtomic(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
636
+ setProjectVault(project.id, root);
637
+ }
469
638
  this.events.emit("saved");
470
639
  }
471
640
  async list() {
@@ -478,6 +647,12 @@ var ProjectStore = class {
478
647
  }
479
648
  async create(input) {
480
649
  await this.load();
650
+ if (!input.orgId) {
651
+ const existing = Array.from(this.projects.values()).find((p) => !p.orgId);
652
+ if (existing) {
653
+ throw new LocalProjectExistsError(existing.name);
654
+ }
655
+ }
481
656
  const project = {
482
657
  id: randomUUID2(),
483
658
  orgId: input.orgId ?? null,
@@ -489,6 +664,8 @@ var ProjectStore = class {
489
664
  groupOrder: input.groupOrder ?? []
490
665
  };
491
666
  this.projects.set(project.id, project);
667
+ setProjectVault(project.id, vaultFor(project).root);
668
+ setProjectServices(project.id, project.serviceIds);
492
669
  await this.save();
493
670
  return project;
494
671
  }
@@ -507,16 +684,51 @@ var ProjectStore = class {
507
684
  ...input.groupOrder !== void 0 && { groupOrder: input.groupOrder }
508
685
  };
509
686
  this.projects.set(input.id, updated);
687
+ setProjectVault(updated.id, vaultFor(updated).root);
688
+ setProjectServices(updated.id, updated.serviceIds);
510
689
  await this.save();
511
690
  return updated;
512
691
  }
513
692
  async delete(id) {
514
693
  await this.load();
515
- if (!this.projects.has(id)) return false;
694
+ const project = this.projects.get(id);
695
+ if (!project) return false;
516
696
  this.projects.delete(id);
517
697
  await this.save();
698
+ await this.discardVault(project);
518
699
  return true;
519
700
  }
701
+ /**
702
+ * Remove what a deleted cloud project left behind.
703
+ *
704
+ * Its vault is a directory of its own, so dropping the project means dropping
705
+ * the directory — otherwise `readCloudVaults` would resurrect it on the next
706
+ * load. Local projects share their vault with the directory's own files and
707
+ * are only removed from the list.
708
+ */
709
+ async discardVault(project) {
710
+ forgetProject(project.id);
711
+ if (!project.orgId) return;
712
+ await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {
713
+ });
714
+ }
715
+ /**
716
+ * Stop holding a project on this machine, leaving the doc alone.
717
+ *
718
+ * Memory only: the caller removes the vault itself. Its contents are dropped
719
+ * too, because a service or environment surviving in memory would be written
720
+ * back on the next save — and, its project being unknown by then, into the
721
+ * local vault.
722
+ */
723
+ async forgetLocally(projectId) {
724
+ await this.load();
725
+ const project = this.projects.get(projectId);
726
+ if (!project) return;
727
+ this.projects.delete(projectId);
728
+ forgetProject(projectId);
729
+ await serviceStore.forgetLocally(project.serviceIds ?? []);
730
+ await environmentStore.forgetByProject(projectId);
731
+ }
520
732
  /** All projects linked to a given organization (for the sync bridge). */
521
733
  async listByOrg(orgId) {
522
734
  await this.load();
@@ -551,20 +763,26 @@ var ProjectStore = class {
551
763
  }
552
764
  const updated = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };
553
765
  this.projects.set(id, updated);
766
+ setProjectVault(id, vaultFor(updated).root);
767
+ setProjectServices(id, updated.serviceIds);
554
768
  await this.save();
555
769
  return { project: updated, created: false };
556
770
  }
557
771
  const project = { ...incoming, activeEnvironmentId: void 0 };
558
772
  this.projects.set(id, project);
773
+ setProjectVault(id, vaultFor(project).root);
774
+ setProjectServices(id, project.serviceIds);
559
775
  await this.save();
560
776
  return { project, created: true };
561
777
  }
562
778
  /** Delete a project that was removed on another peer. */
563
779
  async applyRemoteDelete(id) {
564
780
  await this.load();
565
- if (!this.projects.has(id)) return false;
781
+ const project = this.projects.get(id);
782
+ if (!project) return false;
566
783
  this.projects.delete(id);
567
784
  await this.save();
785
+ await this.discardVault(project);
568
786
  return true;
569
787
  }
570
788
  async addService(projectId, serviceId) {
@@ -574,6 +792,7 @@ var ProjectStore = class {
574
792
  if (project.serviceIds.includes(serviceId)) return project;
575
793
  const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };
576
794
  this.projects.set(projectId, updated);
795
+ setProjectServices(projectId, updated.serviceIds);
577
796
  await this.save();
578
797
  return updated;
579
798
  }
@@ -583,6 +802,7 @@ var ProjectStore = class {
583
802
  if (!project) return null;
584
803
  const updated = { ...project, serviceIds: project.serviceIds.filter((id) => id !== serviceId) };
585
804
  this.projects.set(projectId, updated);
805
+ setProjectServices(projectId, updated.serviceIds);
586
806
  await this.save();
587
807
  return updated;
588
808
  }
@@ -626,64 +846,115 @@ var ProjectStore = class {
626
846
  }
627
847
  };
628
848
  var projectStore = new ProjectStore();
849
+ registerProjectLoader(() => projectStore.list().then(() => void 0));
629
850
 
630
851
  // ../server/src/services/environment-overrides-store.ts
631
- import { readFile as readFile3, writeFile as writeFile3, rename as rename3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
632
- import { join as join3 } from "path";
852
+ import { readFile as readFile3, writeFile as writeFile4, rename as rename3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
853
+ import { dirname as dirname2, join as join3 } from "path";
633
854
  var FILENAME2 = "environment-overrides.json";
634
855
  var EnvironmentOverridesStore = class {
635
856
  data = {};
636
857
  loaded = false;
637
858
  diskEncryptedSlots = {};
638
859
  saveQueue = Promise.resolve();
639
- getFilePath() {
640
- return join3(env.DATA_DIR, FILENAME2);
860
+ /** Which vault each override was read from, until its project is known. */
861
+ roots = /* @__PURE__ */ new Map();
862
+ /**
863
+ * An override belongs to the project whose variable it answers.
864
+ *
865
+ * Nothing in an override says so — they are keyed by variable id alone — so
866
+ * the owning project is resolved through the environment that defines the
867
+ * variable. Overrides are never synced: they stay in the project's vault on
868
+ * this machine, and no other machine sees them.
869
+ *
870
+ * An override whose variable no longer exists (a deleted environment, a row
871
+ * left behind) has no project to belong to. It stays wherever it was read,
872
+ * falling back to the key's own directory — never dropped, since the variable
873
+ * may simply not be loaded yet.
874
+ */
875
+ async rootFor(variableId) {
876
+ const projectId = await environmentStore.projectIdOfVariable(variableId);
877
+ if (projectId) return rootOfProject(projectId);
878
+ return this.roots.get(variableId) ?? dirname2(await keyedFilePath(FILENAME2));
641
879
  }
642
- async ensureDir() {
643
- await mkdir3(env.DATA_DIR, { recursive: true });
880
+ /** Where earlier versions kept them: read once, rewritten to their vaults. */
881
+ getLegacyFilePath() {
882
+ return legacyMachineFilePath(FILENAME2);
644
883
  }
884
+ /**
885
+ * Read every vault, then the places older versions used.
886
+ *
887
+ * Variable ids are unique, so merging the files cannot collide; a vault read
888
+ * later simply wins over a legacy copy of the same override.
889
+ */
645
890
  async load() {
646
891
  if (this.loaded) return;
647
- try {
648
- const raw = await readFile3(this.getFilePath(), "utf-8");
649
- const parsed = JSON.parse(raw);
650
- if (!getEncryptionKey()) {
651
- return;
892
+ if (!getEncryptionKey()) return;
893
+ this.loaded = true;
894
+ const roots = await allVaultRoots();
895
+ const sources = [
896
+ this.getLegacyFilePath(),
897
+ await keyedFilePath(FILENAME2),
898
+ ...roots.map((root) => join3(root, FILENAME2))
899
+ ];
900
+ for (const path of sources) {
901
+ let parsed;
902
+ try {
903
+ parsed = JSON.parse(await readFile3(path, "utf-8"));
904
+ } catch {
905
+ continue;
652
906
  }
653
- this.diskEncryptedSlots = extractEncryptedVariableSlots(
654
- parsed
907
+ Object.assign(
908
+ this.diskEncryptedSlots,
909
+ extractEncryptedVariableSlots(parsed)
655
910
  );
656
911
  const decrypted = decryptVariableSlots(
657
912
  parsed,
658
913
  "environment-overrides"
659
914
  );
660
- this.data = Object.fromEntries(
661
- Object.entries(decrypted).map(([id, override]) => [id, { value: override.value }])
662
- );
663
- } catch {
664
- this.data = {};
915
+ for (const [id, override] of Object.entries(decrypted)) {
916
+ this.data[id] = { value: override.value };
917
+ this.roots.set(id, dirname2(path));
918
+ }
665
919
  }
666
- this.loaded = true;
667
920
  }
668
921
  save() {
669
922
  this.saveQueue = this.saveQueue.then(() => this.doSave()).catch(() => this.doSave());
670
923
  return this.saveQueue;
671
924
  }
925
+ /**
926
+ * Write each vault that holds — or held — an override.
927
+ *
928
+ * A vault emptied by this save is still written, as an empty object: an
929
+ * override that moved to its project's vault must not survive in the one it
930
+ * came from.
931
+ */
672
932
  async doSave() {
673
- await this.ensureDir();
674
- const filePath = this.getFilePath();
675
- const tmpPath = filePath + ".tmp";
676
- const encrypted = encryptVariableSlots(
677
- this.data,
678
- this.diskEncryptedSlots
679
- );
680
- this.diskEncryptedSlots = extractEncryptedVariableSlots(
681
- encrypted
682
- );
683
- const json = JSON.stringify(encrypted, null, 2);
684
- await writeFile3(tmpPath, json, "utf-8");
685
- await rename3(tmpPath, filePath);
686
- await chmod3(filePath, 384);
933
+ const byRoot = /* @__PURE__ */ new Map();
934
+ for (const root of this.roots.values()) byRoot.set(root, {});
935
+ for (const [variableId, override] of Object.entries(this.data)) {
936
+ const root = await this.rootFor(variableId);
937
+ this.roots.set(variableId, root);
938
+ const bucket = byRoot.get(root) ?? {};
939
+ bucket[variableId] = override;
940
+ byRoot.set(root, bucket);
941
+ }
942
+ for (const [root, overrides] of byRoot) {
943
+ const encrypted = encryptVariableSlots(
944
+ overrides,
945
+ this.diskEncryptedSlots
946
+ );
947
+ Object.assign(
948
+ this.diskEncryptedSlots,
949
+ extractEncryptedVariableSlots(encrypted)
950
+ );
951
+ await mkdir3(root, { recursive: true });
952
+ const path = join3(root, FILENAME2);
953
+ const tmpPath = path + ".tmp";
954
+ await writeFile4(tmpPath, JSON.stringify(encrypted, null, 2), "utf-8");
955
+ await rename3(tmpPath, path);
956
+ await chmod3(path, 384);
957
+ }
687
958
  }
688
959
  async getOverride(variableId) {
689
960
  await this.load();
@@ -1095,16 +1366,15 @@ var environmentResolver = new EnvironmentResolver();
1095
1366
 
1096
1367
  // ../server/src/services/audit-service.ts
1097
1368
  import { appendFile, readFile as readFile4, stat, rename as rename4, unlink, readdir, mkdir as mkdir4 } from "fs/promises";
1098
- import { join as join4 } from "path";
1099
1369
  var AUDIT_FILENAME = "audit.log";
1100
1370
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
1101
1371
  var MAX_AGE_DAYS = 30;
1102
1372
  var AuditService = class {
1103
1373
  getFilePath() {
1104
- return join4(env.DATA_DIR, AUDIT_FILENAME);
1374
+ return machineFilePath(AUDIT_FILENAME);
1105
1375
  }
1106
1376
  getRotatedPath() {
1107
- return join4(env.DATA_DIR, `audit.log.${Date.now()}.bak`);
1377
+ return machineFilePath(`audit.log.${Date.now()}.bak`);
1108
1378
  }
1109
1379
  /** Append an audit entry (newline-delimited JSON) */
1110
1380
  async log(entry) {
@@ -1115,7 +1385,7 @@ var AuditService = class {
1115
1385
  const line = JSON.stringify(record) + "\n";
1116
1386
  const filepath = this.getFilePath();
1117
1387
  try {
1118
- await mkdir4(env.DATA_DIR, { recursive: true });
1388
+ await mkdir4(machineRoot(), { recursive: true });
1119
1389
  await appendFile(filepath, line, "utf-8");
1120
1390
  await this.rotateIfNeeded();
1121
1391
  } catch (err) {
@@ -1168,7 +1438,7 @@ var AuditService = class {
1168
1438
  }
1169
1439
  /** Clean up old rotated logs (older than MAX_AGE_DAYS) */
1170
1440
  async cleanup() {
1171
- const files = await readdir(env.DATA_DIR);
1441
+ const files = await readdir(machineRoot());
1172
1442
  const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1e3;
1173
1443
  for (const file of files) {
1174
1444
  if (file.startsWith("audit.log.") && file.endsWith(".bak")) {
@@ -1176,7 +1446,7 @@ var AuditService = class {
1176
1446
  const ts = Number.parseInt(tsStr, 10);
1177
1447
  if (!Number.isNaN(ts) && ts < cutoff) {
1178
1448
  try {
1179
- await unlink(join4(env.DATA_DIR, file));
1449
+ await unlink(machineFilePath(file));
1180
1450
  } catch {
1181
1451
  }
1182
1452
  }
@@ -1220,12 +1490,20 @@ var projectRouter = router({
1220
1490
  serviceIds: z2.array(z2.string().max(128)).max(1e4).optional()
1221
1491
  })
1222
1492
  ).mutation(async ({ input }) => {
1223
- const project = await projectStore.create({
1224
- ...input,
1225
- description: input.description ?? "",
1226
- serviceIds: input.serviceIds ?? [],
1227
- environmentIds: []
1228
- });
1493
+ let project;
1494
+ try {
1495
+ project = await projectStore.create({
1496
+ ...input,
1497
+ description: input.description ?? "",
1498
+ serviceIds: input.serviceIds ?? [],
1499
+ environmentIds: []
1500
+ });
1501
+ } catch (err) {
1502
+ if (err instanceof LocalProjectExistsError) {
1503
+ throw new TRPCError2({ code: "CONFLICT", message: err.code, cause: err });
1504
+ }
1505
+ throw new TRPCError2({ code: "CONFLICT", message: err.message });
1506
+ }
1229
1507
  const localEnv = await environmentStore.create({
1230
1508
  projectId: project.id,
1231
1509
  name: "local",
@@ -1383,8 +1661,7 @@ import { z as z3 } from "zod";
1383
1661
  import { TRPCError as TRPCError3 } from "@trpc/server";
1384
1662
 
1385
1663
  // ../server/src/services/settings-manager.ts
1386
- import { readFile as readFile5, writeFile as writeFile4, rename as rename5, mkdir as mkdir5, chmod as chmod4 } from "fs/promises";
1387
- import { join as join5 } from "path";
1664
+ import { readFile as readFile5, writeFile as writeFile5, rename as rename5, mkdir as mkdir5, chmod as chmod4 } from "fs/promises";
1388
1665
  var SETTINGS_FILE = "settings.json";
1389
1666
  var SETTINGS_PRIVATE_FILE = "settings.private.json";
1390
1667
  var SCHEMA_VERSION_FILE = "schema-version.json";
@@ -1394,23 +1671,25 @@ var SettingsManager = class {
1394
1671
  settingsPrivate = null;
1395
1672
  schemaVersion = null;
1396
1673
  async ensureDir() {
1397
- await mkdir5(env.DATA_DIR, { recursive: true });
1674
+ await mkdir5(machineRoot(), { recursive: true });
1398
1675
  }
1399
1676
  async atomicWrite(filename, data) {
1400
1677
  await this.ensureDir();
1401
- const filePath = join5(env.DATA_DIR, filename);
1678
+ const filePath = machineFilePath(filename);
1402
1679
  const tmpPath = filePath + ".tmp";
1403
- await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf-8");
1680
+ await writeFile5(tmpPath, JSON.stringify(data, null, 2), "utf-8");
1404
1681
  await rename5(tmpPath, filePath);
1405
1682
  await chmod4(filePath, 384);
1406
1683
  }
1684
+ /** Read the machine file, falling back to the launch directory of old. */
1407
1685
  async readJson(filename) {
1408
- try {
1409
- const raw = await readFile5(join5(env.DATA_DIR, filename), "utf-8");
1410
- return JSON.parse(raw);
1411
- } catch {
1412
- return null;
1686
+ for (const path of [machineFilePath(filename), legacyMachineFilePath(filename)]) {
1687
+ try {
1688
+ return JSON.parse(await readFile5(path, "utf-8"));
1689
+ } catch {
1690
+ }
1413
1691
  }
1692
+ return null;
1414
1693
  }
1415
1694
  async getSettings() {
1416
1695
  if (!this.settings) {
@@ -2544,7 +2823,10 @@ var settingsRouter = router({
2544
2823
  */
2545
2824
  getPaths: protectedProcedure.query(() => ({
2546
2825
  cwd: process.cwd(),
2547
- dataDir: resolve(env.DATA_DIR)
2826
+ dataDir: resolve(env.DATA_DIR),
2827
+ // Where cloud projects live: their vaults belong to the machine, not to
2828
+ // whichever directory the server happened to be started in.
2829
+ machineDir: machineRoot()
2548
2830
  })),
2549
2831
  /**
2550
2832
  * Update settings (mode, cloudUrl)
@@ -3092,15 +3374,15 @@ import { z as z11 } from "zod";
3092
3374
  import { TRPCError as TRPCError8 } from "@trpc/server";
3093
3375
  import { execFile as execFile2, spawn } from "child_process";
3094
3376
  import { stat as stat2 } from "fs/promises";
3095
- import { resolve as resolve2, join as join7 } from "path";
3377
+ import { resolve as resolve2, join as join5 } from "path";
3096
3378
  import { homedir as homedir2 } from "os";
3097
3379
 
3098
3380
  // ../server/src/services/user-config-manager.ts
3099
- import { readFile as readFile6, writeFile as writeFile5, rename as rename6, mkdir as mkdir6, chmod as chmod5 } from "fs/promises";
3100
- import { join as join6 } from "path";
3381
+ import { readFile as readFile6, writeFile as writeFile6, rename as rename6, mkdir as mkdir6, chmod as chmod5 } from "fs/promises";
3382
+ import { join as join4 } from "path";
3101
3383
  import { homedir } from "os";
3102
3384
  import { z as z10 } from "zod";
3103
- var CONFIG_DIR = join6(homedir(), ".runeya");
3385
+ var CONFIG_DIR = join4(homedir(), ".runeya");
3104
3386
  var CONFIG_FILE = "runeya.conf";
3105
3387
  var EditorPreferencesSchema = z10.object({
3106
3388
  default: z10.string().max(64).optional()
@@ -3121,16 +3403,16 @@ var UserConfigManager = class {
3121
3403
  }
3122
3404
  async atomicWrite(data) {
3123
3405
  await this.ensureDir();
3124
- const filePath = join6(CONFIG_DIR, CONFIG_FILE);
3406
+ const filePath = join4(CONFIG_DIR, CONFIG_FILE);
3125
3407
  const tmpPath = filePath + ".tmp";
3126
- await writeFile5(tmpPath, JSON.stringify(data, null, 2), "utf-8");
3408
+ await writeFile6(tmpPath, JSON.stringify(data, null, 2), "utf-8");
3127
3409
  await rename6(tmpPath, filePath);
3128
3410
  await chmod5(filePath, 384);
3129
3411
  }
3130
3412
  async getConfig() {
3131
3413
  if (!this.config) {
3132
3414
  try {
3133
- const raw = await readFile6(join6(CONFIG_DIR, CONFIG_FILE), "utf-8");
3415
+ const raw = await readFile6(join4(CONFIG_DIR, CONFIG_FILE), "utf-8");
3134
3416
  const parsed = UserConfigSchema.safeParse(JSON.parse(raw));
3135
3417
  this.config = parsed.success ? parsed.data : {};
3136
3418
  } catch {
@@ -3266,7 +3548,7 @@ var editorRouter = router({
3266
3548
  throw new TRPCError8({ code: "BAD_REQUEST", message: `Unknown editor: ${input.editorKey}` });
3267
3549
  }
3268
3550
  const cwd = await getServiceCwd(input.serviceId);
3269
- const filePath = join7(cwd, input.relativePath);
3551
+ const filePath = join5(cwd, input.relativePath);
3270
3552
  if (!filePath.startsWith(cwd)) {
3271
3553
  throw new TRPCError8({ code: "BAD_REQUEST", message: "Path traversal not allowed" });
3272
3554
  }
@@ -3289,8 +3571,8 @@ var editorRouter = router({
3289
3571
  // ../server/src/trpc/routers/api-keys.ts
3290
3572
  import { z as z12 } from "zod";
3291
3573
  import { TRPCError as TRPCError9 } from "@trpc/server";
3292
- import { mkdir as mkdir7, writeFile as writeFile6 } from "fs/promises";
3293
- import { join as join8, dirname } from "path";
3574
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
3575
+ import { join as join6, dirname as dirname3 } from "path";
3294
3576
  var ALLOWED_SKILL_PATHS = /* @__PURE__ */ new Set([
3295
3577
  ".claude/skills/runeya/SKILL.md",
3296
3578
  ".cursor/rules/runeya.mdc",
@@ -3337,9 +3619,9 @@ var apiKeysRouter = router({
3337
3619
  if (!ALLOWED_SKILL_PATHS.has(input.relativePath)) {
3338
3620
  throw new TRPCError9({ code: "BAD_REQUEST", message: "Chemin non autoris\xE9" });
3339
3621
  }
3340
- const abs = join8(process.cwd(), input.relativePath);
3341
- await mkdir7(dirname(abs), { recursive: true });
3342
- await writeFile6(abs, input.content, { encoding: "utf-8", mode: 420 });
3622
+ const abs = join6(process.cwd(), input.relativePath);
3623
+ await mkdir7(dirname3(abs), { recursive: true });
3624
+ await writeFile7(abs, input.content, { encoding: "utf-8", mode: 420 });
3343
3625
  return { path: input.relativePath };
3344
3626
  })
3345
3627
  });
@@ -3668,27 +3950,54 @@ function buildApiDocs(apiBaseUrl, apiToken) {
3668
3950
  import { readdir as readdir2, access, readFile as readFile8 } from "fs/promises";
3669
3951
  import { watch } from "fs";
3670
3952
  import { homedir as homedir3 } from "os";
3671
- import { join as join10 } from "path";
3953
+ import { join as join8 } from "path";
3672
3954
 
3673
3955
  // ../server/src/services/conversation-store.ts
3674
3956
  import { randomUUID as randomUUID3 } from "crypto";
3675
- import { readFile as readFile7, writeFile as writeFile7, rename as rename7, mkdir as mkdir8, chmod as chmod6 } from "fs/promises";
3676
- import { join as join9 } from "path";
3957
+ import { readFile as readFile7, writeFile as writeFile8, rename as rename7, mkdir as mkdir8, chmod as chmod6 } from "fs/promises";
3958
+ import { dirname as dirname4, join as join7 } from "path";
3677
3959
  import { EventEmitter as EventEmitter2 } from "events";
3678
3960
  var FILENAME3 = "conversations.private.json";
3961
+ async function localProjectId() {
3962
+ const projects = await projectStore.list().catch(() => []);
3963
+ return projects.find((p) => !p.orgId)?.id ?? null;
3964
+ }
3679
3965
  var ConversationStore = class {
3680
3966
  conversations = /* @__PURE__ */ new Map();
3967
+ /** Which vault each conversation was read from, until its project is known. */
3968
+ roots = /* @__PURE__ */ new Map();
3681
3969
  loaded = false;
3682
3970
  events = new EventEmitter2().setMaxListeners(0);
3683
3971
  saveQueue = Promise.resolve();
3684
3972
  snapshotConversation(conv) {
3685
3973
  return structuredClone(conv);
3686
3974
  }
3975
+ /**
3976
+ * Encrypted, so it lives beside the key that opens it — usually the machine
3977
+ * root, but a launch directory owning its own key keeps both together.
3978
+ */
3687
3979
  getFilePath() {
3688
- return join9(env.DATA_DIR, FILENAME3);
3980
+ return keyedFilePath(FILENAME3);
3981
+ }
3982
+ /** Where older versions kept it — read only, so an older Runeya keeps working. */
3983
+ getLegacyFilePath() {
3984
+ return legacyMachineFilePath(FILENAME3);
3985
+ }
3986
+ /**
3987
+ * Read the machine file, falling back to the launch directory of old.
3988
+ *
3989
+ * The legacy file is left in place: the next save writes the new location,
3990
+ * and a directory still opened by an older Runeya keeps working meanwhile.
3991
+ */
3992
+ async readFromDisk() {
3993
+ try {
3994
+ return await readFile7(await this.getFilePath(), "utf-8");
3995
+ } catch {
3996
+ return readFile7(this.getLegacyFilePath(), "utf-8");
3997
+ }
3689
3998
  }
3690
3999
  async ensureDir() {
3691
- await mkdir8(env.DATA_DIR, { recursive: true });
4000
+ await mkdir8(dirname4(await this.getFilePath()), { recursive: true });
3692
4001
  }
3693
4002
  encryptMessages(messages) {
3694
4003
  return encryptValue(JSON.stringify(messages));
@@ -3700,6 +4009,7 @@ var ConversationStore = class {
3700
4009
  toDisk(conv) {
3701
4010
  return {
3702
4011
  id: conv.id,
4012
+ projectId: conv.projectId,
3703
4013
  title: conv.title,
3704
4014
  encryptedMessages: this.encryptMessages(conv.messages),
3705
4015
  isCli: conv.isCli,
@@ -3720,6 +4030,7 @@ var ConversationStore = class {
3720
4030
  const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
3721
4031
  return {
3722
4032
  id: disk.id,
4033
+ projectId: disk.projectId,
3723
4034
  title: disk.title,
3724
4035
  messages: this.decryptMessages(disk.encryptedMessages),
3725
4036
  isCli: disk.isCli ?? disk.isClaudeCode ?? false,
@@ -3736,33 +4047,81 @@ var ConversationStore = class {
3736
4047
  updatedAt: disk.updatedAt
3737
4048
  };
3738
4049
  }
4050
+ /**
4051
+ * Read every project vault, then the homes older versions used.
4052
+ *
4053
+ * A conversation found in a legacy file is attributed to the local project of
4054
+ * that directory — which is what it was about, back when one directory meant
4055
+ * one project. With no local project to point at, it is left unattributed
4056
+ * rather than filed under a project picked at random.
4057
+ */
3739
4058
  async load() {
3740
4059
  if (this.loaded) return;
3741
4060
  if (!getEncryptionKey()) {
3742
4061
  return;
3743
4062
  }
3744
- try {
3745
- const raw = await readFile7(this.getFilePath(), "utf-8");
3746
- const data = JSON.parse(raw);
4063
+ this.loaded = true;
4064
+ const vaults = await allVaultRoots();
4065
+ const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME3)];
4066
+ const sources = [
4067
+ ...legacyPaths.map((path) => ({ path, legacy: true })),
4068
+ ...vaults.map((root) => ({ path: join7(root, FILENAME3), legacy: false }))
4069
+ ];
4070
+ for (const { path, legacy } of sources) {
4071
+ let data;
4072
+ try {
4073
+ data = JSON.parse(await readFile7(path, "utf-8"));
4074
+ } catch {
4075
+ continue;
4076
+ }
4077
+ const inherited = legacy ? await localProjectId() : null;
3747
4078
  for (const item of data) {
3748
4079
  try {
3749
- this.conversations.set(item.id, this.fromDisk(item));
4080
+ const conv = this.fromDisk(item);
4081
+ if (!conv.projectId && inherited) conv.projectId = inherited;
4082
+ this.conversations.set(item.id, conv);
4083
+ this.roots.set(item.id, dirname4(path));
3750
4084
  } catch {
3751
4085
  }
3752
4086
  }
3753
- } catch {
3754
4087
  }
3755
- this.loaded = true;
3756
4088
  }
4089
+ /**
4090
+ * Write each vault that holds — or held — a conversation.
4091
+ *
4092
+ * A vault emptied by this save is still written, as an empty list: a
4093
+ * conversation that found its project must not survive in the file it was
4094
+ * read from.
4095
+ */
3757
4096
  async saveInternal() {
3758
- await this.ensureDir();
3759
4097
  if (!getEncryptionKey()) return;
3760
- const data = Array.from(this.conversations.values()).map((c) => this.toDisk(c));
3761
- const filePath = this.getFilePath();
3762
- const tmpPath = `${filePath}.tmp`;
3763
- await writeFile7(tmpPath, JSON.stringify(data, null, 2), "utf-8");
3764
- await rename7(tmpPath, filePath);
3765
- await chmod6(filePath, 384);
4098
+ const byRoot = /* @__PURE__ */ new Map();
4099
+ for (const root of this.roots.values()) byRoot.set(root, []);
4100
+ for (const conv of this.conversations.values()) {
4101
+ const root = await this.rootFor(conv);
4102
+ this.roots.set(conv.id, root);
4103
+ const bucket = byRoot.get(root) ?? [];
4104
+ bucket.push(this.toDisk(conv));
4105
+ byRoot.set(root, bucket);
4106
+ }
4107
+ for (const [root, data] of byRoot) {
4108
+ await mkdir8(root, { recursive: true });
4109
+ const filePath = join7(root, FILENAME3);
4110
+ const tmpPath = `${filePath}.tmp`;
4111
+ await writeFile8(tmpPath, JSON.stringify(data, null, 2), "utf-8");
4112
+ await rename7(tmpPath, filePath);
4113
+ await chmod6(filePath, 384);
4114
+ }
4115
+ }
4116
+ /**
4117
+ * The vault a conversation is written to: its project's.
4118
+ *
4119
+ * One that names no project stays where it was read, falling back to the
4120
+ * key's own directory — never lost for want of an owner.
4121
+ */
4122
+ async rootFor(conv) {
4123
+ if (conv.projectId) return rootOfProject(conv.projectId);
4124
+ return this.roots.get(conv.id) ?? dirname4(await keyedFilePath(FILENAME3));
3766
4125
  }
3767
4126
  async save() {
3768
4127
  const write = this.saveQueue.then(() => this.saveInternal());
@@ -3778,11 +4137,12 @@ var ConversationStore = class {
3778
4137
  await this.load();
3779
4138
  return this.conversations.get(id) ?? null;
3780
4139
  }
3781
- async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId) {
4140
+ async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId) {
3782
4141
  await this.load();
3783
4142
  const now = Date.now();
3784
4143
  const conv = {
3785
4144
  id: randomUUID3(),
4145
+ projectId,
3786
4146
  title: title ?? "New conversation",
3787
4147
  messages: [],
3788
4148
  isCli,
@@ -3858,7 +4218,7 @@ var ConversationStore = class {
3858
4218
  var conversationStore = new ConversationStore();
3859
4219
 
3860
4220
  // ../server/src/services/claude-transcript-sync.ts
3861
- var PROJECTS_DIR = join10(homedir3(), ".claude", "projects");
4221
+ var PROJECTS_DIR = join8(homedir3(), ".claude", "projects");
3862
4222
  var DEFAULT_TITLES = /* @__PURE__ */ new Set(["", "New conversation", "Nouvelle conversation"]);
3863
4223
  async function findClaudeSessionFile(sessionId) {
3864
4224
  let entries;
@@ -3869,7 +4229,7 @@ async function findClaudeSessionFile(sessionId) {
3869
4229
  }
3870
4230
  const filename = `${sessionId}.jsonl`;
3871
4231
  for (const dir of entries) {
3872
- const candidate = join10(PROJECTS_DIR, dir, filename);
4232
+ const candidate = join8(PROJECTS_DIR, dir, filename);
3873
4233
  try {
3874
4234
  await access(candidate);
3875
4235
  return candidate;
@@ -4032,7 +4392,7 @@ async function startClaudeTranscriptWatcher() {
4032
4392
  }
4033
4393
  for (const dir of topLevel) {
4034
4394
  try {
4035
- const w = watch(join10(PROJECTS_DIR, dir), (_event, filename) => {
4395
+ const w = watch(join8(PROJECTS_DIR, dir), (_event, filename) => {
4036
4396
  handleFsEvent(filename ? filename.toString() : null);
4037
4397
  });
4038
4398
  w.on("error", () => {
@@ -4058,8 +4418,8 @@ function stopClaudeTranscriptWatcher() {
4058
4418
  // ../server/src/services/codex-sessions.ts
4059
4419
  import { readdir as readdir3 } from "fs/promises";
4060
4420
  import { homedir as homedir4 } from "os";
4061
- import { join as join11 } from "path";
4062
- var SESSIONS_DIR = join11(homedir4(), ".codex", "sessions");
4421
+ import { join as join9 } from "path";
4422
+ var SESSIONS_DIR = join9(homedir4(), ".codex", "sessions");
4063
4423
  async function findCodexSessionFile(sessionId) {
4064
4424
  const suffix = `-${sessionId}.jsonl`;
4065
4425
  async function walk(dir) {
@@ -4070,7 +4430,7 @@ async function findCodexSessionFile(sessionId) {
4070
4430
  return null;
4071
4431
  }
4072
4432
  for (const entry of entries) {
4073
- const full = join11(dir, entry.name);
4433
+ const full = join9(dir, entry.name);
4074
4434
  if (entry.isDirectory()) {
4075
4435
  const found = await walk(full);
4076
4436
  if (found) return found;
@@ -5030,19 +5390,18 @@ var ScenarioStore = class {
5030
5390
  var scenarioStore = new ScenarioStore();
5031
5391
 
5032
5392
  // ../server/src/services/scenario-run-store.ts
5033
- import { mkdir as mkdir10, rm, readFile as readFile10 } from "fs/promises";
5034
- import { join as join12 } from "path";
5393
+ import { mkdir as mkdir10, rm as rm2, readFile as readFile10 } from "fs/promises";
5394
+ import { join as join10 } from "path";
5035
5395
 
5036
5396
  // ../server/src/services/run-store-utils.ts
5037
- import { chmod as chmod7, mkdir as mkdir9, rename as rename8, writeFile as writeFile8 } from "fs/promises";
5038
- import { dirname as dirname2, resolve as resolve3, sep } from "path";
5039
- import { readFile as readFile9 } from "fs/promises";
5397
+ import { chmod as chmod7, mkdir as mkdir9, rename as rename8, writeFile as writeFile9, readFile as readFile9 } from "fs/promises";
5398
+ import { dirname as dirname5, resolve as resolve3, sep } from "path";
5040
5399
  var SAFE_PATH_SEGMENT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
5041
5400
  function resolveConversationRunDir(rootDirName, conversationId) {
5042
5401
  if (!SAFE_PATH_SEGMENT_RE.test(conversationId)) {
5043
5402
  throw new Error("Invalid conversationId");
5044
5403
  }
5045
- const base = resolve3(env.DATA_DIR, rootDirName);
5404
+ const base = resolve3(machineRoot(), rootDirName);
5046
5405
  const dir = resolve3(base, conversationId);
5047
5406
  if (!dir.startsWith(base + sep)) {
5048
5407
  throw new Error("Invalid conversationId");
@@ -5050,9 +5409,9 @@ function resolveConversationRunDir(rootDirName, conversationId) {
5050
5409
  return dir;
5051
5410
  }
5052
5411
  async function atomicWriteSecure(filePath, content) {
5053
- await mkdir9(dirname2(filePath), { recursive: true });
5412
+ await mkdir9(dirname5(filePath), { recursive: true });
5054
5413
  const tmpPath = `${filePath}.tmp`;
5055
- await writeFile8(tmpPath, content, "utf-8");
5414
+ await writeFile9(tmpPath, content, "utf-8");
5056
5415
  await rename8(tmpPath, filePath);
5057
5416
  await chmod7(filePath, 384);
5058
5417
  }
@@ -5109,8 +5468,8 @@ var ScenarioRunStore = class _ScenarioRunStore {
5109
5468
  "- Use read-only references from linked upstream files as context.",
5110
5469
  ""
5111
5470
  ].join("\n");
5112
- await atomicWriteSecure(join12(dir, "README.md"), overview);
5113
- await atomicWriteSecure(join12(dir, "USER_REQUEST.md"), `# User request
5471
+ await atomicWriteSecure(join10(dir, "README.md"), overview);
5472
+ await atomicWriteSecure(join10(dir, "USER_REQUEST.md"), `# User request
5114
5473
 
5115
5474
  ${userInput.trim() || "(empty)"}
5116
5475
  `);
@@ -5136,11 +5495,11 @@ ${userInput.trim() || "(empty)"}
5136
5495
  "- Waiting for assignment.",
5137
5496
  ""
5138
5497
  ].join("\n");
5139
- await atomicWriteSecure(join12(dir, fileName), content);
5498
+ await atomicWriteSecure(join10(dir, fileName), content);
5140
5499
  }
5141
5500
  }
5142
5501
  async deleteRunDir(conversationId) {
5143
- await rm(this.getRunDir(conversationId), { recursive: true, force: true });
5502
+ await rm2(this.getRunDir(conversationId), { recursive: true, force: true });
5144
5503
  this.activeRuns.delete(conversationId);
5145
5504
  }
5146
5505
  ensureRunState(conversationId) {
@@ -5204,18 +5563,18 @@ ${userInput.trim() || "(empty)"}
5204
5563
  ...Object.keys(runState.nodeAgentIds).length > 0 ? { nodeAgentIds: { ...runState.nodeAgentIds } } : {},
5205
5564
  ...Object.keys(runState.waitingInputs).length > 0 ? { waitingInputs: { ...runState.waitingInputs } } : {}
5206
5565
  };
5207
- await atomicWriteJsonSecure(join12(dir, "run-state.json"), data);
5566
+ await atomicWriteJsonSecure(join10(dir, "run-state.json"), data);
5208
5567
  } catch {
5209
5568
  }
5210
5569
  }
5211
5570
  async loadPersistedRunState(conversationId) {
5212
5571
  const dir = this.getRunDir(conversationId);
5213
- return readJsonFileSafe(join12(dir, "run-state.json"));
5572
+ return readJsonFileSafe(join10(dir, "run-state.json"));
5214
5573
  }
5215
5574
  async readUserRequest(conversationId) {
5216
5575
  try {
5217
5576
  const dir = this.getRunDir(conversationId);
5218
- const content = await readFile10(join12(dir, "USER_REQUEST.md"), "utf-8");
5577
+ const content = await readFile10(join10(dir, "USER_REQUEST.md"), "utf-8");
5219
5578
  return content.replace(/^#\s*User request\s*\n+/, "").trim() || null;
5220
5579
  } catch {
5221
5580
  return null;
@@ -5224,7 +5583,7 @@ ${userInput.trim() || "(empty)"}
5224
5583
  async readReadme(conversationId) {
5225
5584
  try {
5226
5585
  const dir = this.getRunDir(conversationId);
5227
- return await readFile10(join12(dir, "README.md"), "utf-8");
5586
+ return await readFile10(join10(dir, "README.md"), "utf-8");
5228
5587
  } catch {
5229
5588
  return null;
5230
5589
  }
@@ -5233,14 +5592,14 @@ ${userInput.trim() || "(empty)"}
5233
5592
  const dir = this.getRunDir(conversationId);
5234
5593
  await mkdir10(dir, { recursive: true });
5235
5594
  await atomicWriteJsonSecure(
5236
- join12(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE),
5595
+ join10(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE),
5237
5596
  scenario,
5238
5597
  true
5239
5598
  );
5240
5599
  }
5241
5600
  async loadScenarioSnapshot(conversationId) {
5242
5601
  const dir = this.getRunDir(conversationId);
5243
- return readJsonFileSafe(join12(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE));
5602
+ return readJsonFileSafe(join10(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE));
5244
5603
  }
5245
5604
  };
5246
5605
  var scenarioRunStore = new ScenarioRunStore();
@@ -5286,9 +5645,9 @@ async function fetchProviderModels(provider, plaintextKey) {
5286
5645
 
5287
5646
  // ../server/src/services/image-storage.service.ts
5288
5647
  import { randomUUID as randomUUID4 } from "crypto";
5289
- import { mkdir as mkdir11, writeFile as writeFile9, readFile as readFile11, unlink as unlink2, readdir as readdir4 } from "fs/promises";
5648
+ import { mkdir as mkdir11, writeFile as writeFile10, readFile as readFile11, unlink as unlink2, readdir as readdir4 } from "fs/promises";
5290
5649
  import { existsSync } from "fs";
5291
- import { join as join13, extname, resolve as resolve4, sep as sep2 } from "path";
5650
+ import { join as join11, extname, resolve as resolve4, sep as sep2 } from "path";
5292
5651
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
5293
5652
  "image/png",
5294
5653
  "image/jpeg",
@@ -5325,15 +5684,15 @@ var ImageStorageService = class {
5325
5684
  const imageId = randomUUID4();
5326
5685
  const safeExt = ext.replace(/[^a-z.]/g, "");
5327
5686
  const fileName = `${imageId}${safeExt}`;
5328
- const filePath = join13(this.dir, fileName);
5687
+ const filePath = join11(this.dir, fileName);
5329
5688
  const resolvedPath = resolve4(filePath);
5330
5689
  const resolvedDir = resolve4(this.dir);
5331
5690
  if (resolvedPath !== resolvedDir && !resolvedPath.startsWith(resolvedDir + sep2)) {
5332
5691
  throw new Error("Invalid file path");
5333
5692
  }
5334
- await writeFile9(filePath, buffer, { mode: 384 });
5693
+ await writeFile10(filePath, buffer, { mode: 384 });
5335
5694
  const meta = { imageId, filePath, mimeType, originalName, createdAt: Date.now(), contextId };
5336
- await writeFile9(`${filePath}.meta.json`, JSON.stringify(meta), { encoding: "utf-8", mode: 384 });
5695
+ await writeFile10(`${filePath}.meta.json`, JSON.stringify(meta), { encoding: "utf-8", mode: 384 });
5337
5696
  return meta;
5338
5697
  }
5339
5698
  /**
@@ -5351,7 +5710,7 @@ var ImageStorageService = class {
5351
5710
  const files = await readdir4(this.dir);
5352
5711
  const metaFile = files.find((f) => f.startsWith(imageId) && f.endsWith(".meta.json"));
5353
5712
  if (!metaFile) return null;
5354
- const raw = JSON.parse(await readFile11(join13(this.dir, metaFile), "utf-8"));
5713
+ const raw = JSON.parse(await readFile11(join11(this.dir, metaFile), "utf-8"));
5355
5714
  if (expectedContextId !== void 0 && raw.contextId !== expectedContextId) return null;
5356
5715
  if (!existsSync(raw.filePath)) return null;
5357
5716
  return { filePath: raw.filePath, mimeType: raw.mimeType, contextId: raw.contextId };
@@ -5377,7 +5736,7 @@ var ImageStorageService = class {
5377
5736
  await Promise.all(
5378
5737
  files.filter((f) => f.endsWith(".meta.json")).map(async (file) => {
5379
5738
  try {
5380
- const raw = JSON.parse(await readFile11(join13(this.dir, file), "utf-8"));
5739
+ const raw = JSON.parse(await readFile11(join11(this.dir, file), "utf-8"));
5381
5740
  if (raw.contextId === contextId) {
5382
5741
  await unlink2(raw.filePath).catch(() => {
5383
5742
  });
@@ -5407,7 +5766,7 @@ var ImageStorageService = class {
5407
5766
  for (const file of files) {
5408
5767
  if (!file.endsWith(".meta.json")) continue;
5409
5768
  try {
5410
- const raw = JSON.parse(await readFile11(join13(this.dir, file), "utf-8"));
5769
+ const raw = JSON.parse(await readFile11(join11(this.dir, file), "utf-8"));
5411
5770
  if (now - raw.createdAt > ttlMs) expired.push(raw);
5412
5771
  } catch {
5413
5772
  }
@@ -5418,8 +5777,7 @@ var ImageStorageService = class {
5418
5777
  var _singleton;
5419
5778
  function getSingleton() {
5420
5779
  if (!_singleton) {
5421
- const dataDir = process.env["RUNEYA_DATA_DIR"] ?? ".runeya";
5422
- _singleton = new ImageStorageService(resolve4(dataDir, "images"));
5780
+ _singleton = new ImageStorageService(resolve4(machineRoot(), "images"));
5423
5781
  }
5424
5782
  return _singleton;
5425
5783
  }
@@ -5480,9 +5838,9 @@ import { basename } from "path";
5480
5838
  import { TRPCError as TRPCError10 } from "@trpc/server";
5481
5839
 
5482
5840
  // ../server/src/services/cloud-session-store.ts
5483
- import { readFile as readFile12, writeFile as writeFile10, rename as rename9, mkdir as mkdir12, chmod as chmod8, unlink as unlink3 } from "fs/promises";
5841
+ import { readFile as readFile12, writeFile as writeFile11, rename as rename9, mkdir as mkdir12, chmod as chmod8, unlink as unlink3 } from "fs/promises";
5842
+ import { dirname as dirname6 } from "path";
5484
5843
  import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
5485
- import { join as join14 } from "path";
5486
5844
  var FILENAME4 = "cloud-session.json";
5487
5845
  var STATE_TTL_MS = 5 * 60 * 1e3;
5488
5846
  var CloudSessionStore = class {
@@ -5490,13 +5848,34 @@ var CloudSessionStore = class {
5490
5848
  loaded = false;
5491
5849
  /** Pending login states (CSRF binding between `start` and the callback). */
5492
5850
  pending = /* @__PURE__ */ new Map();
5851
+ /**
5852
+ * Encrypted, so it lives beside the key that opens it — usually the machine
5853
+ * root, but a launch directory owning its own key keeps both together.
5854
+ */
5493
5855
  getFilePath() {
5494
- return join14(env.DATA_DIR, FILENAME4);
5856
+ return keyedFilePath(FILENAME4);
5857
+ }
5858
+ /** Where older versions kept it — read only, so an older Runeya keeps working. */
5859
+ getLegacyFilePath() {
5860
+ return legacyMachineFilePath(FILENAME4);
5861
+ }
5862
+ /**
5863
+ * Read the machine file, falling back to the launch directory of old.
5864
+ *
5865
+ * The legacy file is left in place: the next save writes the new location,
5866
+ * and a directory still opened by an older Runeya keeps working meanwhile.
5867
+ */
5868
+ async readFromDisk() {
5869
+ try {
5870
+ return await readFile12(await this.getFilePath(), "utf-8");
5871
+ } catch {
5872
+ return readFile12(this.getLegacyFilePath(), "utf-8");
5873
+ }
5495
5874
  }
5496
5875
  async load() {
5497
5876
  if (this.loaded) return;
5498
5877
  try {
5499
- const raw = await readFile12(this.getFilePath(), "utf-8");
5878
+ const raw = await this.readFromDisk();
5500
5879
  this.session = JSON.parse(raw);
5501
5880
  } catch {
5502
5881
  }
@@ -5537,10 +5916,10 @@ var CloudSessionStore = class {
5537
5916
  }
5538
5917
  }
5539
5918
  async save() {
5540
- await mkdir12(env.DATA_DIR, { recursive: true });
5541
- const filePath = this.getFilePath();
5919
+ await mkdir12(dirname6(await this.getFilePath()), { recursive: true });
5920
+ const filePath = await this.getFilePath();
5542
5921
  const tmpPath = filePath + ".tmp";
5543
- await writeFile10(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
5922
+ await writeFile11(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
5544
5923
  await rename9(tmpPath, filePath);
5545
5924
  await chmod8(filePath, 384);
5546
5925
  }
@@ -5569,7 +5948,7 @@ var CloudSessionStore = class {
5569
5948
  this.session = null;
5570
5949
  this.pending.clear();
5571
5950
  try {
5572
- await unlink3(this.getFilePath());
5951
+ await unlink3(await this.getFilePath());
5573
5952
  } catch {
5574
5953
  }
5575
5954
  }
@@ -6211,7 +6590,7 @@ function tailTranscript(opts) {
6211
6590
 
6212
6591
  // ../server/src/services/ai-runners/claude-pty-runner.ts
6213
6592
  async function getAgentManager() {
6214
- const { agentManager: agentManager2 } = await import("./agent-manager-U3HLZ6QZ.js");
6593
+ const { agentManager: agentManager2 } = await import("./agent-manager-2I47P7CA.js");
6215
6594
  return agentManager2;
6216
6595
  }
6217
6596
  function buildPtyArgs(params, sessionId, opts) {
@@ -6864,7 +7243,7 @@ async function shutdownCodexAppServer() {
6864
7243
  }
6865
7244
  }
6866
7245
  async function getAgentManager2() {
6867
- const { agentManager: agentManager2 } = await import("./agent-manager-U3HLZ6QZ.js");
7246
+ const { agentManager: agentManager2 } = await import("./agent-manager-2I47P7CA.js");
6868
7247
  return agentManager2;
6869
7248
  }
6870
7249
  function parseRuneyaCurlCommand(command) {
@@ -7745,7 +8124,20 @@ var chatRouter = router({
7745
8124
  conversationStore.offChanged(handler);
7746
8125
  }
7747
8126
  }),
7748
- list: protectedProcedure.query(() => conversationStore.list()),
8127
+ /**
8128
+ * Conversations of one project.
8129
+ *
8130
+ * A conversation belongs to the project it was opened from, so the caller
8131
+ * says which one it is looking at. Ones that name no project — written
8132
+ * before the field existed, or that a migration could not attribute — are
8133
+ * returned whatever the project: hiding them would make them unreachable.
8134
+ */
8135
+ list: protectedProcedure.input(z15.object({ projectId: Id128Schema.optional() }).optional()).query(async ({ input }) => {
8136
+ const all = await conversationStore.list();
8137
+ const projectId = input?.projectId;
8138
+ if (!projectId) return all;
8139
+ return all.filter((c) => !c.projectId || c.projectId === projectId);
8140
+ }),
7749
8141
  get: protectedProcedure.input(z15.object({ id: Id128Schema })).query(async ({ input }) => {
7750
8142
  const conv = await conversationStore.get(input.id);
7751
8143
  if (!conv) throw new TRPCError11({ code: "NOT_FOUND", message: "Conversation not found" });
@@ -7759,7 +8151,10 @@ var chatRouter = router({
7759
8151
  scenarioId: Id128Schema.optional(),
7760
8152
  scenarioInput: z15.string().max(1e5).optional(),
7761
8153
  kanbanBoardId: Id128Schema.optional(),
7762
- kanbanCardId: Id128Schema.optional()
8154
+ kanbanCardId: Id128Schema.optional(),
8155
+ // The project the conversation is about: only the caller knows which
8156
+ // one it is looking at.
8157
+ projectId: Id128Schema.optional()
7763
8158
  })).mutation(async ({ input }) => {
7764
8159
  let scenario = null;
7765
8160
  if (input.scenarioId) {
@@ -7774,7 +8169,8 @@ var chatRouter = router({
7774
8169
  input.runner,
7775
8170
  input.kanbanBoardId,
7776
8171
  input.kanbanCardId,
7777
- input.providerId
8172
+ input.providerId,
8173
+ input.projectId
7778
8174
  );
7779
8175
  if (scenario) {
7780
8176
  await scenarioRunStore.initializeConversationWorkspace(
@@ -8996,7 +9392,7 @@ async function* streamWithActiveProvider(opts) {
8996
9392
  // ../server/src/services/scenario-runner.ts
8997
9393
  import { mkdir as mkdir13 } from "fs/promises";
8998
9394
  import { randomUUID as randomUUID7 } from "crypto";
8999
- import { join as join15 } from "path";
9395
+ import { join as join12 } from "path";
9000
9396
 
9001
9397
  // ../server/src/services/scenario-presets.ts
9002
9398
  var BUILT_IN_PRESETS = [
@@ -9770,7 +10166,7 @@ async function appendNodeOutput(conversationId, node, output) {
9770
10166
  if (!dir) return;
9771
10167
  const slug = node.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 60) || "member";
9772
10168
  const safeId = node.id.replace(/[^a-z0-9_-]/gi, "-").slice(0, 80);
9773
- const filePath = join15(dir, `${slug}_${safeId}.md`);
10169
+ const filePath = join12(dir, `${slug}_${safeId}.md`);
9774
10170
  const content = `## Output
9775
10171
 
9776
10172
  ${output}
@@ -9785,7 +10181,7 @@ async function appendToReadme(conversationId, bus, text) {
9785
10181
  try {
9786
10182
  await mkdir13(dir, { recursive: true });
9787
10183
  const { appendFile: appendFile2 } = await import("fs/promises");
9788
- await appendFile2(join15(dir, "README.md"), line, "utf-8");
10184
+ await appendFile2(join12(dir, "README.md"), line, "utf-8");
9789
10185
  } catch {
9790
10186
  }
9791
10187
  publishToBus(bus, { type: "readme_append", text: line });
@@ -9805,7 +10201,7 @@ async function writeFinalMd(conversationId, bus, userRequest, content) {
9805
10201
 
9806
10202
  ${content}
9807
10203
  `;
9808
- const filePath = join15(dir, "FINAL.md");
10204
+ const filePath = join12(dir, "FINAL.md");
9809
10205
  await atomicWriteSecure(filePath, fullContent);
9810
10206
  await appendToReadme(
9811
10207
  conversationId,
@@ -10984,10 +11380,10 @@ var kanbanQueueRouter = router({
10984
11380
  });
10985
11381
 
10986
11382
  // ../server/src/trpc/routers/kanban.ts
10987
- import { mkdir as mkdir14, readdir as readdir5, rm as rm2, stat as stat3, writeFile as writeFile11 } from "fs/promises";
11383
+ import { mkdir as mkdir14, readdir as readdir5, rm as rm3, stat as stat3, writeFile as writeFile12 } from "fs/promises";
10988
11384
  import { createHash } from "crypto";
10989
11385
  import { tmpdir } from "os";
10990
- import { join as join16 } from "path";
11386
+ import { join as join13 } from "path";
10991
11387
  import { z as z20 } from "zod";
10992
11388
  import { TRPCError as TRPCError17 } from "@trpc/server";
10993
11389
  function summarizeCard(card, boardId, columnTitle) {
@@ -11038,30 +11434,30 @@ function safeDecode(value) {
11038
11434
  }
11039
11435
  var MAX_ATTACHMENT_BYTES = 55 * 1024 * 1024;
11040
11436
  function attachmentCacheDir() {
11041
- return join16(tmpdir(), "runeya-kanban-attachments");
11437
+ return join13(tmpdir(), "runeya-kanban-attachments");
11042
11438
  }
11043
11439
  var ATTACHMENT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11044
11440
  async function purgeStaleAttachments(dir, ttlMs = ATTACHMENT_CACHE_TTL_MS) {
11045
11441
  try {
11046
11442
  const cutoff = Date.now() - ttlMs;
11047
11443
  for (const name of await readdir5(dir)) {
11048
- const path = join16(dir, name);
11444
+ const path = join13(dir, name);
11049
11445
  const info = await stat3(path).catch(() => null);
11050
- if (info && info.mtimeMs < cutoff) await rm2(path, { force: true });
11446
+ if (info && info.mtimeMs < cutoff) await rm3(path, { force: true });
11051
11447
  }
11052
11448
  } catch {
11053
11449
  }
11054
11450
  }
11055
11451
  function attachmentCacheDirs() {
11056
- return [attachmentCacheDir(), join16(env.DATA_DIR, "kanban-attachments")];
11452
+ return [attachmentCacheDir(), join13(machineRoot(), "kanban-attachments")];
11057
11453
  }
11058
11454
  async function storeAttachment(id, name, body, dirs = attachmentCacheDirs()) {
11059
11455
  const refusals = [];
11060
11456
  for (const dir of dirs) {
11061
11457
  try {
11062
11458
  await mkdir14(dir, { recursive: true });
11063
- const path = join16(dir, safeLocalName(id, name));
11064
- await writeFile11(path, body);
11459
+ const path = join13(dir, safeLocalName(id, name));
11460
+ await writeFile12(path, body);
11065
11461
  void purgeStaleAttachments(dir);
11066
11462
  return path;
11067
11463
  } catch (err) {
@@ -11363,8 +11759,8 @@ var utilityRouter = router({
11363
11759
  // ../server/src/trpc/routers/wiki.ts
11364
11760
  import { z as z23 } from "zod";
11365
11761
  import { TRPCError as TRPCError19 } from "@trpc/server";
11366
- import { readFile as readFile13, readdir as readdir6, stat as stat4, realpath, writeFile as writeFile12, mkdir as mkdir15, rename as rename10, rm as rm3 } from "fs/promises";
11367
- import { resolve as resolvePath, join as join17, relative, sep as sep3, dirname as dirname3 } from "path";
11762
+ import { readFile as readFile13, readdir as readdir6, stat as stat4, realpath, writeFile as writeFile13, mkdir as mkdir15, rename as rename10, rm as rm4 } from "fs/promises";
11763
+ import { resolve as resolvePath, join as join14, relative, sep as sep3, dirname as dirname7 } from "path";
11368
11764
  var SCOPE_SCHEMA = z23.union([
11369
11765
  z23.literal("global"),
11370
11766
  z23.string().regex(/^service:[A-Za-z0-9_-]{1,128}$/, "Invalid service scope")
@@ -11402,7 +11798,7 @@ async function resolveScope(scope) {
11402
11798
  }
11403
11799
  const dataDirAbs = resolvePath(env.DATA_DIR);
11404
11800
  await mkdir15(dataDirAbs, { recursive: true });
11405
- const base = dirname3(await realpath(dataDirAbs));
11801
+ const base = dirname7(await realpath(dataDirAbs));
11406
11802
  const target = resolvePath(base, sub);
11407
11803
  if (target !== base && !(target + sep3).startsWith(base + sep3)) {
11408
11804
  throw new TRPCError19({ code: "FORBIDDEN", message: "Hors du dossier de donn\xE9es du serveur" });
@@ -11434,7 +11830,7 @@ async function walkLocal(root, dir, depth, counter) {
11434
11830
  for (const e of entries) {
11435
11831
  if (counter.n >= MAX_FILES) break;
11436
11832
  if (e.name.startsWith(".") && e.name !== ".") continue;
11437
- const abs = join17(dir, e.name);
11833
+ const abs = join14(dir, e.name);
11438
11834
  const rel = relative(root, abs).split(sep3).join("/");
11439
11835
  if (e.isDirectory()) {
11440
11836
  if (IGNORED_DIRS.has(e.name)) continue;
@@ -11468,7 +11864,7 @@ async function resolveSafeLocal(root, relPath, requireExist) {
11468
11864
  }
11469
11865
  return real;
11470
11866
  }
11471
- const parent = dirname3(target);
11867
+ const parent = dirname7(target);
11472
11868
  let realParent;
11473
11869
  try {
11474
11870
  realParent = await realpath(parent);
@@ -11514,8 +11910,8 @@ var wikiRouter = router({
11514
11910
  return agentManager.agentMutation(target.agentId, "wiki.writeMarkdown", { cwd: target.cwd, path: input.path, content: input.content });
11515
11911
  }
11516
11912
  const file = await resolveSafeLocal(target.root, input.path, false);
11517
- await mkdir15(dirname3(file), { recursive: true });
11518
- await writeFile12(file, input.content, "utf-8");
11913
+ await mkdir15(dirname7(file), { recursive: true });
11914
+ await writeFile13(file, input.content, "utf-8");
11519
11915
  return { path: input.path, ok: true };
11520
11916
  }),
11521
11917
  createFile: protectedProcedure.input(z23.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
@@ -11533,8 +11929,8 @@ var wikiRouter = router({
11533
11929
  } catch (err) {
11534
11930
  if (err instanceof TRPCError19) throw err;
11535
11931
  }
11536
- await mkdir15(dirname3(file), { recursive: true });
11537
- await writeFile12(file, "", "utf-8");
11932
+ await mkdir15(dirname7(file), { recursive: true });
11933
+ await writeFile13(file, "", "utf-8");
11538
11934
  return { path: input.path, ok: true };
11539
11935
  }),
11540
11936
  createDir: protectedProcedure.input(z23.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
@@ -11559,7 +11955,7 @@ var wikiRouter = router({
11559
11955
  } catch (err) {
11560
11956
  if (err instanceof TRPCError19) throw err;
11561
11957
  }
11562
- await mkdir15(dirname3(to), { recursive: true });
11958
+ await mkdir15(dirname7(to), { recursive: true });
11563
11959
  await rename10(from, to);
11564
11960
  return { fromPath: input.fromPath, toPath: input.toPath, ok: true };
11565
11961
  }),
@@ -11583,7 +11979,7 @@ var wikiRouter = router({
11583
11979
  }
11584
11980
  const fp = await resolveSafeLocal(target.root, input.path, true);
11585
11981
  if (fp === target.root) throw new TRPCError19({ code: "FORBIDDEN", message: "Impossible de supprimer la racine" });
11586
- await rm3(fp, { recursive: true, force: true });
11982
+ await rm4(fp, { recursive: true, force: true });
11587
11983
  return { path: input.path, ok: true };
11588
11984
  })
11589
11985
  });
@@ -11601,31 +11997,50 @@ import { z as z24 } from "zod";
11601
11997
  import { TRPCError as TRPCError20 } from "@trpc/server";
11602
11998
 
11603
11999
  // ../server/src/services/cloud-sync-store.ts
11604
- import { readFile as readFile14, writeFile as writeFile13, rename as rename11, mkdir as mkdir16, chmod as chmod9 } from "fs/promises";
11605
- import { join as join18 } from "path";
12000
+ import { readFile as readFile14, writeFile as writeFile14, rename as rename11, mkdir as mkdir16, chmod as chmod9 } from "fs/promises";
11606
12001
  var FILENAME5 = "cloud-sync.json";
11607
12002
  var CloudSyncStore = class {
11608
12003
  links = /* @__PURE__ */ new Map();
11609
12004
  loaded = false;
12005
+ /** Machine-wide: this describes the machine, not any one project. */
11610
12006
  getFilePath() {
11611
- return join18(env.DATA_DIR, FILENAME5);
12007
+ return machineFilePath(FILENAME5);
12008
+ }
12009
+ /** Where older versions kept it — read only, so an older Runeya keeps working. */
12010
+ getLegacyFilePath() {
12011
+ return legacyMachineFilePath(FILENAME5);
12012
+ }
12013
+ /**
12014
+ * Read the machine file, falling back to the launch directory of old.
12015
+ *
12016
+ * The legacy file is left in place: the next save writes the new location,
12017
+ * and a directory still opened by an older Runeya keeps working meanwhile.
12018
+ */
12019
+ async readFromDisk() {
12020
+ try {
12021
+ return await readFile14(this.getFilePath(), "utf-8");
12022
+ } catch {
12023
+ return readFile14(this.getLegacyFilePath(), "utf-8");
12024
+ }
11612
12025
  }
11613
12026
  async load() {
11614
12027
  if (this.loaded) return;
11615
12028
  try {
11616
- const raw = await readFile14(this.getFilePath(), "utf-8");
12029
+ const raw = await this.readFromDisk();
11617
12030
  const data = JSON.parse(raw);
11618
- for (const l of data) this.links.set(l.orgId, { orgId: l.orgId, cloudUrl: l.cloudUrl });
12031
+ for (const l of data) {
12032
+ this.links.set(l.orgId, { orgId: l.orgId, cloudUrl: l.cloudUrl });
12033
+ }
11619
12034
  } catch {
11620
12035
  }
11621
12036
  this.loaded = true;
11622
12037
  }
11623
12038
  async save() {
11624
- await mkdir16(env.DATA_DIR, { recursive: true });
12039
+ await mkdir16(machineRoot(), { recursive: true });
11625
12040
  const filePath = this.getFilePath();
11626
12041
  const tmpPath = filePath + ".tmp";
11627
12042
  const data = JSON.stringify(Array.from(this.links.values()), null, 2);
11628
- await writeFile13(tmpPath, data, "utf-8");
12043
+ await writeFile14(tmpPath, data, "utf-8");
11629
12044
  await rename11(tmpPath, filePath);
11630
12045
  await chmod9(filePath, 384);
11631
12046
  }
@@ -11653,7 +12068,82 @@ var cloudSyncStore = new CloudSyncStore();
11653
12068
 
11654
12069
  // ../server/src/services/cloud-sync-bridge.ts
11655
12070
  import { randomUUID as randomUUID11 } from "crypto";
12071
+ import { mkdir as mkdir18, rm as rm5 } from "fs/promises";
11656
12072
  import * as Y from "yjs";
12073
+
12074
+ // ../server/src/services/org-lock.ts
12075
+ import { open as open2, readFile as readFile15, unlink as unlink4, mkdir as mkdir17 } from "fs/promises";
12076
+ import { readFileSync, unlinkSync } from "fs";
12077
+ import { join as join15 } from "path";
12078
+ var held = /* @__PURE__ */ new Set();
12079
+ function lockPath(orgId) {
12080
+ return join15(orgVaultRoot(orgId), "org.lock");
12081
+ }
12082
+ function isRunning(pid) {
12083
+ if (!Number.isInteger(pid) || pid <= 0) return false;
12084
+ try {
12085
+ process.kill(pid, 0);
12086
+ return true;
12087
+ } catch (err) {
12088
+ return err.code === "EPERM";
12089
+ }
12090
+ }
12091
+ async function readLock(path) {
12092
+ try {
12093
+ const parsed = JSON.parse(await readFile15(path, "utf-8"));
12094
+ return typeof parsed?.pid === "number" ? parsed : null;
12095
+ } catch {
12096
+ return null;
12097
+ }
12098
+ }
12099
+ async function acquireOrgLock(orgId) {
12100
+ const path = lockPath(orgId);
12101
+ await mkdir17(orgVaultRoot(orgId), { recursive: true });
12102
+ for (let attempt = 0; attempt < 2; attempt++) {
12103
+ try {
12104
+ const handle = await open2(path, "wx");
12105
+ try {
12106
+ const contents = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
12107
+ await handle.writeFile(JSON.stringify(contents), "utf-8");
12108
+ } finally {
12109
+ await handle.close();
12110
+ }
12111
+ held.add(orgId);
12112
+ return { acquired: true };
12113
+ } catch (err) {
12114
+ if (err.code !== "EEXIST") throw err;
12115
+ }
12116
+ const holder2 = await readLock(path);
12117
+ if (holder2 && holder2.pid !== process.pid && isRunning(holder2.pid)) {
12118
+ return { acquired: false, heldBy: holder2.pid };
12119
+ }
12120
+ await unlink4(path).catch(() => {
12121
+ });
12122
+ }
12123
+ const holder = await readLock(path);
12124
+ return holder ? { acquired: false, heldBy: holder.pid } : { acquired: true };
12125
+ }
12126
+ async function releaseOrgLock(orgId) {
12127
+ held.delete(orgId);
12128
+ const path = lockPath(orgId);
12129
+ const holder = await readLock(path);
12130
+ if (holder && holder.pid !== process.pid) return;
12131
+ await unlink4(path).catch(() => {
12132
+ });
12133
+ }
12134
+ function releaseAllOrgLocksSync() {
12135
+ for (const orgId of held) {
12136
+ const path = lockPath(orgId);
12137
+ try {
12138
+ const holder = JSON.parse(readFileSync(path, "utf-8"));
12139
+ if (holder.pid === process.pid) unlinkSync(path);
12140
+ } catch {
12141
+ }
12142
+ }
12143
+ held.clear();
12144
+ }
12145
+
12146
+ // ../server/src/services/cloud-sync-bridge.ts
11657
12147
  var LOCAL_ORIGIN = "local-bridge";
11658
12148
  var REMOTE_ORIGIN = "remote";
11659
12149
  var RECONNECT_DELAY_MS = 3e3;
@@ -11690,6 +12180,32 @@ var OrgSyncConnection = class {
11690
12180
  lastIncompatibleVersion = null;
11691
12181
  /** Local state still owed to the cloud (initial sync, or a rejected episode). */
11692
12182
  needsPush = false;
12183
+ /**
12184
+ * Set while a newly followed project is being materialized.
12185
+ *
12186
+ * Reconciliation reads "absent locally" as "removed from the project", which
12187
+ * is true for an edit and catastrophically false mid-pull: a project whose
12188
+ * files have not landed yet would have its services and environments wiped
12189
+ * from the doc — for every member of the org, not just here.
12190
+ */
12191
+ pulling = false;
12192
+ /**
12193
+ * Projects of this org materialized on this machine — the ones whose vault
12194
+ * exists. Refreshed from disk rather than held in a saved list: a list is one
12195
+ * more thing that can disagree with what is actually there, and a project on
12196
+ * disk but missing from it is the "already here, still offered as a download"
12197
+ * bug this replaces.
12198
+ */
12199
+ tracked = /* @__PURE__ */ new Set();
12200
+ /**
12201
+ * Whether the doc's opening state has been applied.
12202
+ *
12203
+ * It separates "the org already had these projects" from "someone just
12204
+ * created one". The first must not land here — joining a fifty-project org
12205
+ * would drop all fifty on the disk — while the second should, so a colleague
12206
+ * creating a project sees it appear on their teammates' machines.
12207
+ */
12208
+ initialDocApplied = false;
11693
12209
  unsubscribes = [];
11694
12210
  onSaved = () => void this.reconcileLocalToDoc();
11695
12211
  constructor(link) {
@@ -11709,6 +12225,7 @@ var OrgSyncConnection = class {
11709
12225
  return this.link;
11710
12226
  }
11711
12227
  async start() {
12228
+ await this.refreshTracked();
11712
12229
  this.doc.on("update", (update, origin) => {
11713
12230
  if (this.stopped || this.initializing) return;
11714
12231
  if (origin === LOCAL_ORIGIN) void this.pushUpdate(update);
@@ -11716,7 +12233,9 @@ var OrgSyncConnection = class {
11716
12233
  for (const name of COLLECTIONS) {
11717
12234
  this.collection(name).observeDeep((events, txn) => {
11718
12235
  if (txn.origin !== REMOTE_ORIGIN) return;
11719
- void this.applyDocEventsToStore(name, events);
12236
+ const isInitial = !this.initialDocApplied;
12237
+ if (name === "projects") this.initialDocApplied = true;
12238
+ void this.applyDocEventsToStore(name, events, isInitial);
11720
12239
  });
11721
12240
  }
11722
12241
  projectStore.events.on("saved", this.onSaved);
@@ -11903,25 +12422,152 @@ var OrgSyncConnection = class {
11903
12422
  }
11904
12423
  }
11905
12424
  }
12425
+ // ---- Project selection ------------------------------------------------
12426
+ /**
12427
+ * Whether a doc entity is one this machine holds.
12428
+ *
12429
+ * The org is the unit of transport — the doc carries every project a member
12430
+ * can see — but not the unit of storage: a machine materializes only the
12431
+ * projects its user opened. Without this filter, opening one project of a
12432
+ * fifty-project org would drop the other forty-nine onto this disk.
12433
+ *
12434
+ * Having a vault is what "followed" means. Opening a project creates its
12435
+ * directory, dropping it removes the directory, and nothing else has to be
12436
+ * kept in agreement with the disk.
12437
+ */
12438
+ isTracked(name, id) {
12439
+ if (name === "projects") return this.tracked.has(id);
12440
+ const fields = readEntity(this.collection(name), id);
12441
+ if (!fields) return false;
12442
+ if (name === "environments") {
12443
+ const projectId = fields["projectId"];
12444
+ return typeof projectId === "string" && this.tracked.has(projectId);
12445
+ }
12446
+ const projects = this.collection("projects");
12447
+ for (const projectId of this.tracked) {
12448
+ const project = readEntity(projects, projectId);
12449
+ const serviceIds = project?.["serviceIds"];
12450
+ if (Array.isArray(serviceIds) && serviceIds.includes(id)) return true;
12451
+ }
12452
+ return false;
12453
+ }
12454
+ /** Re-read which projects of this org have a vault on this machine. */
12455
+ async refreshTracked() {
12456
+ const vaults = await listCloudVaults();
12457
+ this.tracked = new Set(
12458
+ vaults.filter((v) => v.orgId === this.link.orgId).map((v) => v.projectId)
12459
+ );
12460
+ }
12461
+ /**
12462
+ * Whether a *removal* announced by the doc concerns this machine.
12463
+ *
12464
+ * Deletions cannot go through `isTracked`: the entity is already gone from
12465
+ * the doc, so there is nothing left to read its `projectId` from. Projects
12466
+ * still answer from the selection; for their contents we lean on
12467
+ * `deleteLocally` being a no-op for anything this machine never pulled, which
12468
+ * is precisely the untracked case.
12469
+ */
12470
+ isTrackedForDeletion(name, id) {
12471
+ if (name === "projects") return this.tracked.has(id);
12472
+ return true;
12473
+ }
12474
+ /**
12475
+ * Follow a project: give it a vault, then fill it from the doc.
12476
+ *
12477
+ * Idempotent on purpose. Widening a selection is not the only way a project
12478
+ * comes to be missing — a first pull can fail on a rejected session, or land
12479
+ * before the doc arrived — and re-opening the project has to be able to retry
12480
+ * it. Returns whether the doc actually held the project.
12481
+ */
12482
+ async followProject(projectId) {
12483
+ const inDoc = readEntity(this.collection("projects"), projectId) !== null;
12484
+ const existing = await projectStore.get(projectId);
12485
+ if (existing && !existing.orgId) {
12486
+ console.error(
12487
+ "[cloud-sync] project %s is already a local project here \u2014 not following it for org %s",
12488
+ projectId,
12489
+ this.link.orgId
12490
+ );
12491
+ return false;
12492
+ }
12493
+ const isLocal = existing?.orgId === this.link.orgId;
12494
+ if (!inDoc && !isLocal) return false;
12495
+ await mkdir18(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
12496
+ this.tracked.add(projectId);
12497
+ if (inDoc) await this.pullFromDoc([projectId]);
12498
+ else await this.reconcileLocalToDoc();
12499
+ return true;
12500
+ }
12501
+ /**
12502
+ * Stop following a project on this machine.
12503
+ *
12504
+ * Its vault goes with it — the directory *is* the selection — but the doc is
12505
+ * left untouched: the user stops following the project here, they do not ask
12506
+ * for it to be deleted for the whole organization.
12507
+ */
12508
+ async unfollowProject(projectId) {
12509
+ this.tracked.delete(projectId);
12510
+ await projectStore.forgetLocally(projectId);
12511
+ await rm5(projectVaultRoot(this.link.orgId, projectId), { recursive: true, force: true }).catch(() => {
12512
+ });
12513
+ }
12514
+ /**
12515
+ * Materialize locally everything the doc holds for these projects.
12516
+ *
12517
+ * Contents land before their project, so that the moment the project becomes
12518
+ * local — and therefore "owned" by reconciliation — its services and
12519
+ * environments are already on disk. The guard covers the rest of the window,
12520
+ * including the saves this very method triggers.
12521
+ */
12522
+ async pullFromDoc(projectIds) {
12523
+ this.pulling = true;
12524
+ try {
12525
+ for (const name of ["services", "environments", "projects"]) {
12526
+ const collection = this.collection(name);
12527
+ for (const id of Array.from(collection.keys())) {
12528
+ if (name === "projects" ? !projectIds.includes(id) : !this.isTracked(name, id)) continue;
12529
+ const fields = readEntity(collection, id);
12530
+ if (fields) await this.upsertLocally(name, id, fields);
12531
+ }
12532
+ }
12533
+ } finally {
12534
+ this.pulling = false;
12535
+ }
12536
+ await this.reconcileLocalToDoc();
12537
+ }
11906
12538
  // ---- Local ↔ Doc reconciliation --------------------------------------
11907
- async applyDocEventsToStore(name, events) {
12539
+ async applyDocEventsToStore(name, events, isInitial = false) {
11908
12540
  const collection = this.collection(name);
11909
12541
  const deleted = /* @__PURE__ */ new Set();
11910
12542
  const affected = /* @__PURE__ */ new Set();
12543
+ const added = /* @__PURE__ */ new Set();
11911
12544
  for (const event of events) {
11912
12545
  if (event.target === collection) {
11913
12546
  for (const [key, change] of event.changes.keys) {
11914
12547
  if (change.action === "delete") deleted.add(key);
11915
- else affected.add(key);
12548
+ else {
12549
+ affected.add(key);
12550
+ if (change.action === "add") added.add(key);
12551
+ }
11916
12552
  }
11917
12553
  } else {
11918
12554
  const topKey = event.path[0];
11919
12555
  if (typeof topKey === "string") affected.add(topKey);
11920
12556
  }
11921
12557
  }
11922
- for (const id of deleted) await this.deleteLocally(name, id);
12558
+ for (const id of deleted) {
12559
+ if (!this.isTrackedForDeletion(name, id)) continue;
12560
+ await this.deleteLocally(name, id);
12561
+ }
12562
+ if (name === "projects" && !isInitial) {
12563
+ for (const id of added) {
12564
+ if (deleted.has(id) || this.tracked.has(id)) continue;
12565
+ await this.followProject(id);
12566
+ }
12567
+ }
11923
12568
  for (const id of affected) {
11924
12569
  if (deleted.has(id)) continue;
12570
+ if (!this.isTracked(name, id)) continue;
11925
12571
  const fields = readEntity(collection, id);
11926
12572
  if (!fields) continue;
11927
12573
  await this.upsertLocally(name, id, fields);
@@ -11936,7 +12582,10 @@ var OrgSyncConnection = class {
11936
12582
  await environmentStore.applyRemoteUpsert(id, fields);
11937
12583
  return;
11938
12584
  }
11939
- const result = await projectStore.applyRemoteUpsert(id, fields);
12585
+ const result = await projectStore.applyRemoteUpsert(id, {
12586
+ ...fields,
12587
+ orgId: this.link.orgId
12588
+ });
11940
12589
  if (result?.created) await this.bootstrapEnvironment(id);
11941
12590
  }
11942
12591
  async deleteLocally(name, id) {
@@ -11977,7 +12626,7 @@ var OrgSyncConnection = class {
11977
12626
  * propagate — otherwise a service could never be removed.
11978
12627
  */
11979
12628
  async reconcileLocalToDoc() {
11980
- if (this.stopped) return;
12629
+ if (this.stopped || this.pulling) return;
11981
12630
  const projects = await projectStore.listDiskFormByOrg(this.link.orgId);
11982
12631
  const projectIds = projects.map((p) => p.id);
11983
12632
  const serviceIds = [...new Set(projects.flatMap((p) => p.serviceIds ?? []))];
@@ -12025,6 +12674,8 @@ var OrgSyncConnection = class {
12025
12674
  };
12026
12675
  var CloudSyncBridge = class {
12027
12676
  connections = /* @__PURE__ */ new Map();
12677
+ /** Orgs another instance holds the lock on — reported, not retried blindly. */
12678
+ locked = /* @__PURE__ */ new Set();
12028
12679
  started = false;
12029
12680
  /** Start syncing every organization already linked on disk. */
12030
12681
  async start() {
@@ -12050,15 +12701,55 @@ var CloudSyncBridge = class {
12050
12701
  }
12051
12702
  /** Per-org sync state, for the UI. */
12052
12703
  states() {
12053
- return Array.from(this.connections.entries()).map(([orgId, conn]) => ({
12704
+ const live = Array.from(this.connections.entries()).map(([orgId, conn]) => ({
12054
12705
  orgId,
12055
12706
  state: conn.getState(),
12056
12707
  lastIncompatibleVersion: conn.getLastIncompatibleVersion()
12057
12708
  }));
12709
+ const locked = Array.from(this.locked).filter((orgId) => !this.connections.has(orgId)).map((orgId) => ({ orgId, state: "locked", lastIncompatibleVersion: null }));
12710
+ return [...live, ...locked];
12058
12711
  }
12059
- /** Begin (or restart) syncing a single organization. */
12712
+ /**
12713
+ * Follow one project of an org on this machine, pulling it now.
12714
+ *
12715
+ * Returns false when the org is not connected or the doc does not hold the
12716
+ * project — both mean "not on this machine yet", and the caller reports the
12717
+ * sync state rather than a bare timeout.
12718
+ */
12719
+ async followProject(orgId, projectId) {
12720
+ const conn = this.connections.get(orgId);
12721
+ if (!conn) return false;
12722
+ return conn.followProject(projectId);
12723
+ }
12724
+ /** Stop following one project of an org on this machine. */
12725
+ async unfollowProject(orgId, projectId) {
12726
+ await this.connections.get(orgId)?.unfollowProject(projectId);
12727
+ }
12728
+ /**
12729
+ * Begin (or restart) syncing a single organization.
12730
+ *
12731
+ * Takes the org's lock first. An org's vaults are shared by every Runeya on
12732
+ * this machine, so a second instance syncing the same org would write the
12733
+ * same files from its own copy of the doc — silently overwriting the first.
12734
+ * Refusing to sync is the safe answer: the instance keeps running, that org
12735
+ * simply does not sync here.
12736
+ */
12060
12737
  async connect(link) {
12061
12738
  this.disconnect(link.orgId);
12739
+ const lock = await acquireOrgLock(link.orgId).catch((err) => {
12740
+ console.error("[cloud-sync] could not take the lock for org %s:", link.orgId, err.message);
12741
+ return { acquired: false, heldBy: 0 };
12742
+ });
12743
+ if (!lock.acquired) {
12744
+ console.error(
12745
+ "[cloud-sync] org %s is already being synced by another Runeya (pid %d) \u2014 not syncing it here",
12746
+ link.orgId,
12747
+ lock.heldBy
12748
+ );
12749
+ this.locked.add(link.orgId);
12750
+ return;
12751
+ }
12752
+ this.locked.delete(link.orgId);
12062
12753
  const conn = new OrgSyncConnection(link);
12063
12754
  this.connections.set(link.orgId, conn);
12064
12755
  try {
@@ -12072,19 +12763,22 @@ var CloudSyncBridge = class {
12072
12763
  if (existing) {
12073
12764
  existing.stop();
12074
12765
  this.connections.delete(orgId);
12766
+ void releaseOrgLock(orgId);
12075
12767
  }
12076
12768
  }
12077
12769
  stopAll() {
12078
12770
  for (const conn of this.connections.values()) conn.stop();
12079
12771
  this.connections.clear();
12772
+ this.locked.clear();
12773
+ releaseAllOrgLocksSync();
12080
12774
  }
12081
12775
  };
12082
12776
  var cloudSyncBridge = new CloudSyncBridge();
12083
12777
 
12084
12778
  // ../server/src/services/master-password.ts
12085
12779
  import { randomBytes as randomBytes4, createCipheriv, createDecipheriv } from "crypto";
12086
- import { readFile as readFile15, writeFile as writeFile14, chmod as chmod10, stat as stat5, rename as rename12 } from "fs/promises";
12087
- import { join as join19 } from "path";
12780
+ import { readFile as readFile16, writeFile as writeFile15, chmod as chmod10, stat as stat5, rename as rename12 } from "fs/promises";
12781
+ import { join as join16 } from "path";
12088
12782
  import { Entry } from "@napi-rs/keyring";
12089
12783
  import { hashRaw as argon2HashRaw } from "@node-rs/argon2";
12090
12784
  var KEYRING_SERVICE = "runeya";
@@ -12098,7 +12792,7 @@ var ARGON2_OPTIONS = { memoryCost: 65536, timeCost: 3, parallelism: 4, outputLen
12098
12792
  async function readKeyFile(filePath) {
12099
12793
  const s = await stat5(filePath);
12100
12794
  if (s.size > MAX_KEY_FILE_SIZE) throw new Error(`Fichier suspect : ${filePath} (${s.size} octets)`);
12101
- return readFile15(filePath);
12795
+ return readFile16(filePath);
12102
12796
  }
12103
12797
  function aesEncrypt(key, plaintext) {
12104
12798
  const iv = randomBytes4(12);
@@ -12117,7 +12811,7 @@ function aesDecrypt(key, data) {
12117
12811
  }
12118
12812
  async function getPepperFromFile(dataDir) {
12119
12813
  try {
12120
- const raw = await readKeyFile(join19(dataDir, PEPPER_FILENAME));
12814
+ const raw = await readKeyFile(join16(dataDir, PEPPER_FILENAME));
12121
12815
  const hex = raw.toString("utf-8").trim();
12122
12816
  if (/^[0-9a-f]{64}$/i.test(hex)) return Buffer.from(hex, "hex");
12123
12817
  } catch {
@@ -12125,9 +12819,9 @@ async function getPepperFromFile(dataDir) {
12125
12819
  return null;
12126
12820
  }
12127
12821
  async function writePepperToFile(dataDir, pepper) {
12128
- const filePath = join19(dataDir, PEPPER_FILENAME);
12822
+ const filePath = join16(dataDir, PEPPER_FILENAME);
12129
12823
  const tmpPath = filePath + ".tmp";
12130
- await writeFile14(tmpPath, pepper.toString("hex"), "utf-8");
12824
+ await writeFile15(tmpPath, pepper.toString("hex"), "utf-8");
12131
12825
  await chmod10(tmpPath, 384);
12132
12826
  await rename12(tmpPath, filePath);
12133
12827
  }
@@ -12176,14 +12870,14 @@ async function writeEncryptedKey(dataDir, masterPassword, encryptionKey) {
12176
12870
  const payload = aesEncrypt(derivedKey, encryptionKey);
12177
12871
  const fileContent = Buffer.concat([salt, payload]);
12178
12872
  const encoded = fileContent.toString("base64");
12179
- const filePath = join19(dataDir, ENCRYPTED_KEY_FILENAME);
12873
+ const filePath = join16(dataDir, ENCRYPTED_KEY_FILENAME);
12180
12874
  const tmpPath = filePath + ".tmp";
12181
- await writeFile14(tmpPath, encoded, "utf-8");
12875
+ await writeFile15(tmpPath, encoded, "utf-8");
12182
12876
  await chmod10(tmpPath, 384);
12183
12877
  await rename12(tmpPath, filePath);
12184
12878
  }
12185
12879
  async function readEncryptedKey(dataDir, masterPassword) {
12186
- const filePath = join19(dataDir, ENCRYPTED_KEY_FILENAME);
12880
+ const filePath = join16(dataDir, ENCRYPTED_KEY_FILENAME);
12187
12881
  const raw = await readKeyFile(filePath);
12188
12882
  const fileContent = Buffer.from(raw.toString("utf-8").trim(), "base64");
12189
12883
  const salt = fileContent.subarray(0, 16);
@@ -12203,13 +12897,13 @@ async function getPepperFromKeychain(dataDir) {
12203
12897
  }
12204
12898
  async function writeVerifyFile(dataDir, encryptionKey) {
12205
12899
  const payload = aesEncrypt(encryptionKey, Buffer.from(CANARY_PLAINTEXT, "utf-8"));
12206
- const filePath = join19(dataDir, VERIFY_FILENAME);
12900
+ const filePath = join16(dataDir, VERIFY_FILENAME);
12207
12901
  const tmpPath = filePath + ".tmp";
12208
- await writeFile14(tmpPath, payload.toString("base64"), "utf-8");
12902
+ await writeFile15(tmpPath, payload.toString("base64"), "utf-8");
12209
12903
  await rename12(tmpPath, filePath);
12210
12904
  }
12211
12905
  async function verifyEncryptionKey(dataDir, encryptionKey) {
12212
- const filePath = join19(dataDir, VERIFY_FILENAME);
12906
+ const filePath = join16(dataDir, VERIFY_FILENAME);
12213
12907
  const raw = await readKeyFile(filePath);
12214
12908
  if (!matchesCanary(raw.toString("utf-8"), encryptionKey)) {
12215
12909
  throw new Error("Cl\xE9 d'encryption incorrecte");
@@ -12225,14 +12919,14 @@ function matchesCanary(canaryBase64, encryptionKey) {
12225
12919
  }
12226
12920
  async function readVerifyFile(dataDir) {
12227
12921
  try {
12228
- return (await readKeyFile(join19(dataDir, VERIFY_FILENAME))).toString("utf-8").trim();
12922
+ return (await readKeyFile(join16(dataDir, VERIFY_FILENAME))).toString("utf-8").trim();
12229
12923
  } catch {
12230
12924
  return null;
12231
12925
  }
12232
12926
  }
12233
12927
  async function hasVerifyFile(dataDir) {
12234
12928
  try {
12235
- await stat5(join19(dataDir, VERIFY_FILENAME));
12929
+ await stat5(join16(dataDir, VERIFY_FILENAME));
12236
12930
  return true;
12237
12931
  } catch {
12238
12932
  return false;
@@ -12240,7 +12934,7 @@ async function hasVerifyFile(dataDir) {
12240
12934
  }
12241
12935
  async function hasEncryptedKeyFile(dataDir) {
12242
12936
  try {
12243
- await stat5(join19(dataDir, ENCRYPTED_KEY_FILENAME));
12937
+ await stat5(join16(dataDir, ENCRYPTED_KEY_FILENAME));
12244
12938
  return true;
12245
12939
  } catch {
12246
12940
  return false;
@@ -12253,7 +12947,7 @@ async function reconcileOrgCanary(orgId) {
12253
12947
  if (!key) {
12254
12948
  throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "Encryption key not loaded." });
12255
12949
  }
12256
- const local = await readVerifyFile(env.DATA_DIR);
12950
+ const local = await readVerifyFile(await keyRoot());
12257
12951
  if (!local) {
12258
12952
  throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "This machine has no key canary yet." });
12259
12953
  }
@@ -12275,6 +12969,9 @@ async function reconcileOrgCanary(orgId) {
12275
12969
  });
12276
12970
  }
12277
12971
  }
12972
+ function syncState(orgId) {
12973
+ return cloudSyncBridge.states().find((s) => s.orgId === orgId)?.state ?? "stopped";
12974
+ }
12278
12975
  var cloudSyncRouter = router({
12279
12976
  /**
12280
12977
  * Register/replace a sync link for an org and start syncing immediately.
@@ -12285,12 +12982,56 @@ var cloudSyncRouter = router({
12285
12982
  link: protectedProcedure.input(
12286
12983
  z24.object({
12287
12984
  orgId: z24.string().min(1).max(128),
12288
- cloudUrl: z24.string().url().max(2048)
12985
+ cloudUrl: z24.string().url().max(2048),
12986
+ // No project selection: which projects this machine materializes is
12987
+ // read from the vaults on disk, so linking an org never re-downloads
12988
+ // projects the user had dropped.
12989
+ projectIds: z24.array(z24.string().min(1).max(128)).max(500).optional()
12289
12990
  })
12290
12991
  ).mutation(async ({ input }) => {
12291
12992
  await reconcileOrgCanary(input.orgId);
12292
12993
  await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
12293
- await cloudSyncBridge.connect({ orgId: input.orgId, cloudUrl: input.cloudUrl });
12994
+ const link = await cloudSyncStore.get(input.orgId);
12995
+ await cloudSyncBridge.connect(link ?? { orgId: input.orgId, cloudUrl: input.cloudUrl });
12996
+ return { success: true };
12997
+ }),
12998
+ /**
12999
+ * Follow one more project of an org, and pull it now.
13000
+ *
13001
+ * This is what opening a project in the switcher calls. It links the org on
13002
+ * first use, so the UI never has to know whether a link already exists —
13003
+ * "open this project" is the whole gesture.
13004
+ */
13005
+ trackProject: protectedProcedure.input(
13006
+ z24.object({
13007
+ orgId: z24.string().min(1).max(128),
13008
+ cloudUrl: z24.string().url().max(2048),
13009
+ projectId: z24.string().min(1).max(128)
13010
+ })
13011
+ ).mutation(async ({ input }) => {
13012
+ const existing = await cloudSyncStore.get(input.orgId);
13013
+ if (!existing) {
13014
+ await reconcileOrgCanary(input.orgId);
13015
+ await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
13016
+ const link = await cloudSyncStore.get(input.orgId);
13017
+ if (link) await cloudSyncBridge.connect(link);
13018
+ }
13019
+ const pulled = await cloudSyncBridge.followProject(input.orgId, input.projectId);
13020
+ return { pulled, state: syncState(input.orgId) };
13021
+ }),
13022
+ /**
13023
+ * Stop materializing one project of an org on this machine.
13024
+ *
13025
+ * Removes its vault, and nothing else: the project stays in the org, for
13026
+ * every other member and for this one on another machine.
13027
+ */
13028
+ untrackProject: protectedProcedure.input(
13029
+ z24.object({
13030
+ orgId: z24.string().min(1).max(128),
13031
+ projectId: z24.string().min(1).max(128)
13032
+ })
13033
+ ).mutation(async ({ input }) => {
13034
+ await cloudSyncBridge.unfollowProject(input.orgId, input.projectId);
12294
13035
  return { success: true };
12295
13036
  }),
12296
13037
  /** Stop syncing an org and forget its token. */
@@ -12378,7 +13119,6 @@ var migrationEngine = new MigrationEngine();
12378
13119
 
12379
13120
  // ../server/src/services/file-security.ts
12380
13121
  import { chmod as chmod11, stat as stat6 } from "fs/promises";
12381
- import { join as join20 } from "path";
12382
13122
  var SENSITIVE_FILES = [
12383
13123
  "agents.json",
12384
13124
  "settings.json",
@@ -12390,78 +13130,36 @@ var SENSITIVE_FILES = [
12390
13130
  var SECURE_MODE = 384;
12391
13131
  async function ensureFilePermissions() {
12392
13132
  for (const filename of SENSITIVE_FILES) {
12393
- const filepath = join20(env.DATA_DIR, filename);
12394
- try {
12395
- const stats = await stat6(filepath);
12396
- const mode = stats.mode & 511;
12397
- if (mode !== SECURE_MODE) {
12398
- console.warn("[security] File", filename, "has mode", mode.toString(8) + ", fixing to 600");
12399
- await chmod11(filepath, SECURE_MODE);
12400
- }
12401
- } catch (err) {
12402
- if (err.code !== "ENOENT") {
12403
- console.warn("[security] Cannot check permissions for", filename + ":", err instanceof Error ? err.message : String(err));
13133
+ for (const filepath of [machineFilePath(filename), legacyMachineFilePath(filename)]) {
13134
+ try {
13135
+ const stats = await stat6(filepath);
13136
+ const mode = stats.mode & 511;
13137
+ if (mode !== SECURE_MODE) {
13138
+ console.warn("[security] File", filepath, "has mode", mode.toString(8) + ", fixing to 600");
13139
+ await chmod11(filepath, SECURE_MODE);
13140
+ }
13141
+ } catch (err) {
13142
+ if (err.code !== "ENOENT") {
13143
+ console.warn("[security] Cannot check permissions for", filepath + ":", err instanceof Error ? err.message : String(err));
13144
+ }
12404
13145
  }
12405
13146
  }
12406
13147
  }
12407
13148
  }
12408
13149
 
12409
- // ../server/src/utils/ensure-data-gitignore.ts
12410
- import { writeFile as writeFile15 } from "fs/promises";
12411
- import { join as join21 } from "path";
12412
- var GITIGNORE_CONTENT = `
12413
- #################
12414
- # Do not edit this file #
12415
- #################
12416
-
12417
-
12418
- # Encryption key (contains secrets \u2014 never commit)
12419
- .encryption.key
12420
- .encryption.key.enc
12421
-
12422
-
12423
- # Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
12424
- !.runeya-verify
12425
-
12426
- # This machine's cloud session token (encrypted, and specific to this install)
12427
- cloud-session.json
12428
-
12429
- # Backup in local only
12430
- backup-*
12431
-
12432
- # Per-user environment overrides (never commit \u2014 each dev has their own)
12433
- environment-overrides.json
12434
-
12435
- # Private user data (conversations, personal state \u2014 never commit)
12436
- *.private
12437
- *.private.json*
12438
- conversations/
12439
- conversations.private.json
12440
-
12441
-
12442
-
12443
- # Runtime logs
12444
- *.log
12445
- *.log.*
12446
- `;
12447
- async function ensureDataGitignore(dataDir) {
12448
- const gitignorePath = join21(dataDir, ".gitignore");
12449
- await writeFile15(gitignorePath, GITIGNORE_CONTENT, "utf-8");
12450
- }
12451
-
12452
13150
  // ../server/src/migrations/migrate-from-legacy.ts
12453
13151
  import { createRequire } from "module";
12454
13152
  import { randomBytes as randomBytes5, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "crypto";
12455
13153
  import { randomUUID as randomUUID12 } from "crypto";
12456
13154
  import {
12457
- readFile as readFile16,
13155
+ readFile as readFile17,
12458
13156
  writeFile as writeFile16,
12459
13157
  rename as rename13,
12460
- mkdir as mkdir17,
13158
+ mkdir as mkdir19,
12461
13159
  access as access2,
12462
13160
  readdir as readdir7
12463
13161
  } from "fs/promises";
12464
- import { join as join22 } from "path";
13162
+ import { join as join17 } from "path";
12465
13163
  var _require = createRequire(import.meta.url);
12466
13164
  var PARSER_ID_MAP = {
12467
13165
  "stack-monitor-parser-jsons": "native:json",
@@ -12570,7 +13268,7 @@ async function getSodium() {
12570
13268
  return _sodium;
12571
13269
  }
12572
13270
  async function decryptLegacyFile(filePath, key, sodium) {
12573
- const raw = (await readFile16(filePath, "utf-8")).trim();
13271
+ const raw = (await readFile17(filePath, "utf-8")).trim();
12574
13272
  if (!raw || raw === "[]" || raw === "{}") {
12575
13273
  try {
12576
13274
  return JSON.parse(raw);
@@ -12596,8 +13294,8 @@ async function atomicWrite(path, data) {
12596
13294
  async function runLegacyMigration(dataDir) {
12597
13295
  const { resolve: resolve6 } = await import("path");
12598
13296
  const abCwd = resolve6(process.cwd());
12599
- const legacyRoot = join22(abCwd, env.DATA_DIR);
12600
- const legacyKeyPath = join22(legacyRoot, "dbs", "encryption-key.json");
13297
+ const legacyRoot = join17(abCwd, env.DATA_DIR);
13298
+ const legacyKeyPath = join17(legacyRoot, "dbs", "encryption-key.json");
12601
13299
  try {
12602
13300
  await access2(legacyKeyPath);
12603
13301
  } catch {
@@ -12605,20 +13303,20 @@ async function runLegacyMigration(dataDir) {
12605
13303
  }
12606
13304
  console.log("[legacy-migration] Legacy Runeya data detected, starting migration\u2026");
12607
13305
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
12608
- const legacyTmp = join22(abCwd, ".runeya-tmp");
13306
+ const legacyTmp = join17(abCwd, ".runeya-tmp");
12609
13307
  await rename13(legacyRoot, legacyTmp);
12610
- await mkdir17(dataDir, { recursive: true });
12611
- const legacyOld = join22(dataDir, `backup-${timestamp}`);
13308
+ await mkdir19(dataDir, { recursive: true });
13309
+ const legacyOld = join17(dataDir, `backup-${timestamp}`);
12612
13310
  await rename13(legacyTmp, legacyOld);
12613
13311
  console.log(`[legacy-migration] Legacy data backed up to ${legacyOld}`);
12614
- const keyJson = JSON.parse(await readFile16(join22(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
13312
+ const keyJson = JSON.parse(await readFile17(join17(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
12615
13313
  const sodium = await getSodium();
12616
13314
  const sodiumKey = sodium.from_base64(keyJson.encryptionKey);
12617
13315
  const keyBuffer = Buffer.from(sodiumKey);
12618
13316
  const runyeaKeyHex = keyBuffer.toString("hex");
12619
13317
  await setEncryptionKey(runyeaKeyHex);
12620
13318
  console.log("[legacy-migration] Encryption key loaded in memory (temporary)");
12621
- const dbsDir = join22(legacyOld, "dbs");
13319
+ const dbsDir = join17(legacyOld, "dbs");
12622
13320
  async function readEncrypted(filePath) {
12623
13321
  try {
12624
13322
  return await decryptLegacyFile(filePath, sodiumKey, sodium);
@@ -12627,29 +13325,29 @@ async function runLegacyMigration(dataDir) {
12627
13325
  return null;
12628
13326
  }
12629
13327
  }
12630
- const envsDir = join22(dbsDir, "envs");
13328
+ const envsDir = join17(dbsDir, "envs");
12631
13329
  const envFiles = (await readdir7(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
12632
13330
  const legacyEnvs = {};
12633
13331
  for (const file of envFiles) {
12634
13332
  const name = file.replace(".encrypted.json", "");
12635
- legacyEnvs[name] = await readEncrypted(join22(envsDir, file));
13333
+ legacyEnvs[name] = await readEncrypted(join17(envsDir, file));
12636
13334
  }
12637
- const servicesDir = join22(dbsDir, "services");
13335
+ const servicesDir = join17(dbsDir, "services");
12638
13336
  const serviceFiles = (await readdir7(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
12639
13337
  const legacyServices = {};
12640
13338
  for (const file of serviceFiles) {
12641
13339
  const name = file.replace(".encrypted.json", "");
12642
13340
  if (!name) continue;
12643
- const data = await readEncrypted(join22(servicesDir, file));
13341
+ const data = await readEncrypted(join17(servicesDir, file));
12644
13342
  if (data?.label) legacyServices[name] = data;
12645
13343
  }
12646
- const overridesDir = join22(dbsDir, "overrides");
13344
+ const overridesDir = join17(dbsDir, "overrides");
12647
13345
  const overrideFiles = (await readdir7(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
12648
13346
  const legacyServiceOverrides = {};
12649
13347
  const legacyEnvOverrides = {};
12650
13348
  for (const file of overrideFiles) {
12651
13349
  const baseName = file.replace(".encrypted.json", "");
12652
- const data = await readEncrypted(join22(overridesDir, file));
13350
+ const data = await readEncrypted(join17(overridesDir, file));
12653
13351
  if (!data) continue;
12654
13352
  if (baseName.endsWith("-envs")) {
12655
13353
  legacyServiceOverrides[baseName.slice(0, -5)] = data;
@@ -12657,11 +13355,11 @@ async function runLegacyMigration(dataDir) {
12657
13355
  legacyEnvOverrides[baseName.slice(0, -12)] = data;
12658
13356
  }
12659
13357
  }
12660
- const parsersDir = join22(dbsDir, "parsers");
13358
+ const parsersDir = join17(dbsDir, "parsers");
12661
13359
  const parserFiles = (await readdir7(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
12662
13360
  const legacyParsers = [];
12663
13361
  for (const file of parserFiles) {
12664
- const data = await readEncrypted(join22(parsersDir, file));
13362
+ const data = await readEncrypted(join17(parsersDir, file));
12665
13363
  if (data?.id) legacyParsers.push(data);
12666
13364
  }
12667
13365
  const projectMonoId = randomUUID12();
@@ -13002,7 +13700,7 @@ async function runLegacyMigration(dataDir) {
13002
13700
  }
13003
13701
  let existingSettings = {};
13004
13702
  try {
13005
- existingSettings = JSON.parse(await readFile16(join22(dataDir, "settings.json"), "utf-8"));
13703
+ existingSettings = JSON.parse(await readFile17(join17(dataDir, "settings.json"), "utf-8"));
13006
13704
  } catch {
13007
13705
  }
13008
13706
  const migratedParsers = legacyParsers.map((p) => ({
@@ -13023,11 +13721,11 @@ ${p.transform ?? ""}`,
13023
13721
  ...migratedParsers
13024
13722
  ]
13025
13723
  };
13026
- await atomicWrite(join22(dataDir, "projects.json"), runeProjects);
13027
- await atomicWrite(join22(dataDir, "services.json"), runeServices);
13028
- await atomicWrite(join22(dataDir, "environments.json"), runeEnvs);
13029
- await atomicWrite(join22(dataDir, "environment-overrides.json"), flatOverrides);
13030
- await atomicWrite(join22(dataDir, "settings.json"), newSettings);
13724
+ await atomicWrite(join17(dataDir, "projects.json"), runeProjects);
13725
+ await atomicWrite(join17(dataDir, "services.json"), runeServices);
13726
+ await atomicWrite(join17(dataDir, "environments.json"), runeEnvs);
13727
+ await atomicWrite(join17(dataDir, "environment-overrides.json"), flatOverrides);
13728
+ await atomicWrite(join17(dataDir, "settings.json"), newSettings);
13031
13729
  const svcEnvCount = runeEnvs.filter((e) => e.scope === "service").length;
13032
13730
  console.log(`[legacy-migration] Written: ${runeProjects.length} projects, ${runeServices.length} services, ${runeEnvs.length} environments (${svcEnvCount} service-scoped)`);
13033
13731
  console.log(`[legacy-migration] Overrides: ${Object.keys(flatOverrides).length}, custom parsers: ${migratedParsers.length} (all disabled \u2014 rewrite required)`);
@@ -13041,9 +13739,9 @@ async function findLegacyKeyInBackup(dataDir) {
13041
13739
  const entries = await readdir7(dataDir);
13042
13740
  const backupDirs = entries.filter((e) => e.startsWith("backup-")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).reverse();
13043
13741
  for (const dir of backupDirs) {
13044
- const keyJsonPath = join22(dataDir, dir, "dbs", "encryption-key.json");
13742
+ const keyJsonPath = join17(dataDir, dir, "dbs", "encryption-key.json");
13045
13743
  try {
13046
- const raw = await readFile16(keyJsonPath, "utf-8");
13744
+ const raw = await readFile17(keyJsonPath, "utf-8");
13047
13745
  const parsed = JSON.parse(raw);
13048
13746
  if (!parsed.encryptionKey) continue;
13049
13747
  const sodium = await getSodium();
@@ -13065,8 +13763,8 @@ async function runOverrideOnlyMigration(dataDir) {
13065
13763
  const abDataDir = resolve6(dataDir);
13066
13764
  const abCwd = resolve6(process.cwd());
13067
13765
  const candidateDirs = [
13068
- join22(abCwd, ".runeya", "dbs", "overrides"),
13069
- join22(abDataDir, "dbs", "overrides")
13766
+ join17(abCwd, ".runeya", "dbs", "overrides"),
13767
+ join17(abDataDir, "dbs", "overrides")
13070
13768
  ];
13071
13769
  let overridesDir = null;
13072
13770
  let overrideFiles = [];
@@ -13082,7 +13780,7 @@ async function runOverrideOnlyMigration(dataDir) {
13082
13780
  }
13083
13781
  }
13084
13782
  if (!overridesDir || overrideFiles.length === 0) return false;
13085
- const legacyKeyPath = join22(overridesDir, "..", "encryption-key.json");
13783
+ const legacyKeyPath = join17(overridesDir, "..", "encryption-key.json");
13086
13784
  try {
13087
13785
  await access2(legacyKeyPath);
13088
13786
  return false;
@@ -13100,7 +13798,7 @@ async function runOverrideOnlyMigration(dataDir) {
13100
13798
  const envNameToId = {};
13101
13799
  let envsRaw;
13102
13800
  try {
13103
- envsRaw = JSON.parse(await readFile16(join22(abDataDir, "environments.json"), "utf-8"));
13801
+ envsRaw = JSON.parse(await readFile17(join17(abDataDir, "environments.json"), "utf-8"));
13104
13802
  for (const _env of envsRaw) {
13105
13803
  const env2 = _env;
13106
13804
  if (env2["name"] && env2["id"]) {
@@ -13119,7 +13817,7 @@ async function runOverrideOnlyMigration(dataDir) {
13119
13817
  let servicesRaw = [];
13120
13818
  const serviceNameToId = {};
13121
13819
  try {
13122
- servicesRaw = JSON.parse(await readFile16(join22(abDataDir, "services.json"), "utf-8"));
13820
+ servicesRaw = JSON.parse(await readFile17(join17(abDataDir, "services.json"), "utf-8"));
13123
13821
  for (const _svc of servicesRaw) {
13124
13822
  const svc = _svc;
13125
13823
  if (svc["name"] && svc["id"]) serviceNameToId[svc["name"]] = svc["id"];
@@ -13136,7 +13834,7 @@ async function runOverrideOnlyMigration(dataDir) {
13136
13834
  const envName = baseName.slice(0, -"-environment".length);
13137
13835
  let data;
13138
13836
  try {
13139
- data = await decryptLegacyFile(join22(overridesDir, file), sodiumKey, sodium);
13837
+ data = await decryptLegacyFile(join17(overridesDir, file), sodiumKey, sodium);
13140
13838
  } catch (err) {
13141
13839
  console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
13142
13840
  continue;
@@ -13163,7 +13861,7 @@ async function runOverrideOnlyMigration(dataDir) {
13163
13861
  }
13164
13862
  }
13165
13863
  if (Object.keys(flatOverrides).length > 0) {
13166
- await atomicWrite(join22(abDataDir, "environment-overrides.json"), flatOverrides);
13864
+ await atomicWrite(join17(abDataDir, "environment-overrides.json"), flatOverrides);
13167
13865
  console.log(`[override-migration] Written ${Object.keys(flatOverrides).length} override(s) to environment-overrides.json`);
13168
13866
  }
13169
13867
  let envsModified = false;
@@ -13173,7 +13871,7 @@ async function runOverrideOnlyMigration(dataDir) {
13173
13871
  const serviceName = baseName.slice(0, -"-envs".length);
13174
13872
  let data;
13175
13873
  try {
13176
- data = await decryptLegacyFile(join22(overridesDir, file), sodiumKey, sodium);
13874
+ data = await decryptLegacyFile(join17(overridesDir, file), sodiumKey, sodium);
13177
13875
  } catch (err) {
13178
13876
  console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
13179
13877
  continue;
@@ -13232,7 +13930,7 @@ async function runOverrideOnlyMigration(dataDir) {
13232
13930
  }
13233
13931
  }
13234
13932
  if (envsModified) {
13235
- await atomicWrite(join22(abDataDir, "environments.json"), envsRaw);
13933
+ await atomicWrite(join17(abDataDir, "environments.json"), envsRaw);
13236
13934
  console.log("[override-migration] Merged service overrides into environments.json");
13237
13935
  }
13238
13936
  const migratedDir = overridesDir.replace(/overrides$/, "overrides-migrated");
@@ -13247,27 +13945,32 @@ async function runOverrideOnlyMigration(dataDir) {
13247
13945
  }
13248
13946
 
13249
13947
  // ../server/src/services/server-state.ts
13250
- import { join as join23 } from "path";
13948
+ import { join as join18 } from "path";
13251
13949
  import { stat as stat7 } from "fs/promises";
13252
13950
  var ENCRYPTED_KEY_FILENAME2 = ".encryption.key.enc";
13253
13951
  var VERIFY_FILENAME2 = ".runeya-verify";
13254
13952
  async function getServerState() {
13255
- const dataDir = env.DATA_DIR;
13953
+ const dataDir = await keyRoot();
13256
13954
  let hasEncFile = false;
13257
13955
  let hasVerify = false;
13258
13956
  let hasData = false;
13259
13957
  try {
13260
- await stat7(join23(dataDir, ENCRYPTED_KEY_FILENAME2));
13958
+ await stat7(join18(dataDir, ENCRYPTED_KEY_FILENAME2));
13261
13959
  hasEncFile = true;
13262
13960
  } catch {
13263
13961
  }
13264
13962
  try {
13265
- await stat7(join23(dataDir, VERIFY_FILENAME2));
13963
+ await stat7(join18(dataDir, VERIFY_FILENAME2));
13266
13964
  hasVerify = true;
13267
13965
  } catch {
13268
13966
  }
13269
13967
  try {
13270
- await stat7(join23(dataDir, "projects.json"));
13968
+ await stat7(join18(dataDir, "projects.json"));
13969
+ hasData = true;
13970
+ } catch {
13971
+ }
13972
+ try {
13973
+ await stat7(join18(dataDir, "project.json"));
13271
13974
  hasData = true;
13272
13975
  } catch {
13273
13976
  }
@@ -13559,7 +14262,6 @@ function getOpenApiDocument() {
13559
14262
  // ../server/src/create-server.ts
13560
14263
  import rateLimit from "express-rate-limit";
13561
14264
  import { existsSync as existsSync2, createReadStream } from "fs";
13562
- import { mkdir as mkdir18 } from "fs/promises";
13563
14265
  import multer from "multer";
13564
14266
 
13565
14267
  // ../server/src/services/image-cleanup.service.ts
@@ -13650,7 +14352,8 @@ async function createLocalServer() {
13650
14352
  generateJwtSecret();
13651
14353
  const ciMode = await loadFromEnv();
13652
14354
  await runLegacyMigration(env.DATA_DIR);
13653
- if (!ciMode && !hasPendingLegacyKey() && !await hasEncryptedKeyFile(env.DATA_DIR) && !await hasVerifyFile(env.DATA_DIR)) {
14355
+ const keyDir = await keyRoot();
14356
+ if (!ciMode && !hasPendingLegacyKey() && !await hasEncryptedKeyFile(keyDir) && !await hasVerifyFile(keyDir)) {
13654
14357
  const recoveredKey = await findLegacyKeyInBackup(env.DATA_DIR);
13655
14358
  if (recoveredKey) {
13656
14359
  setPendingLegacyKey(recoveredKey);
@@ -13665,10 +14368,12 @@ async function createLocalServer() {
13665
14368
  console.log("[server] Premier lancement \u2014 setup requis");
13666
14369
  }
13667
14370
  }
13668
- if (!existsSync2(env.DATA_DIR)) await mkdir18(env.DATA_DIR);
13669
14371
  const absoluteDataDir = resolve5(env.DATA_DIR);
13670
- console.log(`[server] Data directory: ${absoluteDataDir}`);
13671
- await ensureDataGitignore(env.DATA_DIR);
14372
+ console.log(`[server] Local project directory: ${absoluteDataDir}`);
14373
+ console.log(`[server] Machine directory: ${machineRoot()}`);
14374
+ if (existsSync2(env.DATA_DIR)) {
14375
+ await ensureDataGitignore(env.DATA_DIR);
14376
+ }
13672
14377
  await settingsManager.getSettings();
13673
14378
  await migrationEngine.run();
13674
14379
  await ensureFilePermissions();
@@ -13947,7 +14652,7 @@ async function createLocalServer() {
13947
14652
  res.status(400).json({ error: "masterPassword invalide" });
13948
14653
  return;
13949
14654
  }
13950
- const encKey = await readEncryptedKey(env.DATA_DIR, masterPassword);
14655
+ const encKey = await readEncryptedKey(await keyRoot(), masterPassword);
13951
14656
  setEncryptionKeyInMemory(encKey);
13952
14657
  await runPostUnlock();
13953
14658
  const token = await signJwt();
@@ -13985,7 +14690,7 @@ async function createLocalServer() {
13985
14690
  return;
13986
14691
  }
13987
14692
  encKey = Buffer.from(encryptionKeyHex, "hex");
13988
- await verifyEncryptionKey(env.DATA_DIR, encKey);
14693
+ await verifyEncryptionKey(await keyRoot(), encKey);
13989
14694
  } else {
13990
14695
  if (body.encryptionKey !== void 0) {
13991
14696
  const encryptionKeyHex = body.encryptionKey;
@@ -13999,14 +14704,14 @@ async function createLocalServer() {
13999
14704
  encKey = randomBytes6(32);
14000
14705
  }
14001
14706
  }
14002
- await writeEncryptedKey(env.DATA_DIR, masterPassword, encKey);
14003
- const readBack = await readEncryptedKey(env.DATA_DIR, masterPassword);
14707
+ await writeEncryptedKey(await keyRoot(), masterPassword, encKey);
14708
+ const readBack = await readEncryptedKey(await keyRoot(), masterPassword);
14004
14709
  if (!readBack.equals(encKey)) {
14005
14710
  res.status(500).json({ error: "Erreur de v\xE9rification round-trip \u2014 op\xE9ration annul\xE9e" });
14006
14711
  return;
14007
14712
  }
14008
14713
  if (!hasVerify) {
14009
- await writeVerifyFile(env.DATA_DIR, encKey);
14714
+ await writeVerifyFile(await keyRoot(), encKey);
14010
14715
  }
14011
14716
  setEncryptionKeyInMemory(encKey);
14012
14717
  if (isLegacyMigration) {
@@ -14429,4 +15134,4 @@ export {
14429
15134
  createLocalServer,
14430
15135
  pullEnv
14431
15136
  };
14432
- //# sourceMappingURL=src-A3WSWXEY.js.map
15137
+ //# sourceMappingURL=src-BCBD4MPB.js.map