@rudderhq/server 0.7.18 → 0.7.19-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/dist/agent-runtimes/registry.d.ts.map +1 -1
  2. package/dist/agent-runtimes/registry.js +2 -1
  3. package/dist/agent-runtimes/registry.js.map +1 -1
  4. package/dist/file-system-durability.d.ts +5 -0
  5. package/dist/file-system-durability.d.ts.map +1 -0
  6. package/dist/file-system-durability.js +39 -0
  7. package/dist/file-system-durability.js.map +1 -0
  8. package/dist/home-paths.d.ts +1 -0
  9. package/dist/home-paths.d.ts.map +1 -1
  10. package/dist/home-paths.js +705 -102
  11. package/dist/home-paths.js.map +1 -1
  12. package/dist/routes/chats.d.ts.map +1 -1
  13. package/dist/routes/chats.js +2 -0
  14. package/dist/routes/chats.js.map +1 -1
  15. package/dist/routes/issues.d.ts.map +1 -1
  16. package/dist/routes/issues.js +27 -5
  17. package/dist/routes/issues.js.map +1 -1
  18. package/dist/routes/issues.mutations.d.ts.map +1 -1
  19. package/dist/routes/issues.mutations.js +10 -6
  20. package/dist/routes/issues.mutations.js.map +1 -1
  21. package/dist/routes/orgs.d.ts.map +1 -1
  22. package/dist/routes/orgs.js +28 -7
  23. package/dist/routes/orgs.js.map +1 -1
  24. package/dist/routes/rudder-plugins.d.ts +5 -0
  25. package/dist/routes/rudder-plugins.d.ts.map +1 -1
  26. package/dist/routes/rudder-plugins.js +21 -1
  27. package/dist/routes/rudder-plugins.js.map +1 -1
  28. package/dist/routes/rudder-plugins.test.d.ts +2 -0
  29. package/dist/routes/rudder-plugins.test.d.ts.map +1 -0
  30. package/dist/routes/rudder-plugins.test.js +42 -0
  31. package/dist/routes/rudder-plugins.test.js.map +1 -0
  32. package/dist/routes/run-intelligence.d.ts.map +1 -1
  33. package/dist/routes/run-intelligence.js +8 -0
  34. package/dist/routes/run-intelligence.js.map +1 -1
  35. package/dist/services/agent-issue-creation.d.ts.map +1 -1
  36. package/dist/services/agent-issue-creation.js +2 -0
  37. package/dist/services/agent-issue-creation.js.map +1 -1
  38. package/dist/services/chat-assistant.d.ts.map +1 -1
  39. package/dist/services/chat-assistant.helpers.d.ts +4 -2
  40. package/dist/services/chat-assistant.helpers.d.ts.map +1 -1
  41. package/dist/services/chat-assistant.helpers.js +5 -0
  42. package/dist/services/chat-assistant.helpers.js.map +1 -1
  43. package/dist/services/chat-assistant.js +31 -8
  44. package/dist/services/chat-assistant.js.map +1 -1
  45. package/dist/services/chat-inline-annotations.test.js +6 -3
  46. package/dist/services/chat-inline-annotations.test.js.map +1 -1
  47. package/dist/services/index.d.ts +1 -1
  48. package/dist/services/index.d.ts.map +1 -1
  49. package/dist/services/index.js.map +1 -1
  50. package/dist/services/issue-references.d.ts +10 -0
  51. package/dist/services/issue-references.d.ts.map +1 -0
  52. package/dist/services/issue-references.js +141 -0
  53. package/dist/services/issue-references.js.map +1 -0
  54. package/dist/services/issues.d.ts +1 -1
  55. package/dist/services/issues.d.ts.map +1 -1
  56. package/dist/services/issues.helpers.d.ts +4 -0
  57. package/dist/services/issues.helpers.d.ts.map +1 -1
  58. package/dist/services/issues.helpers.js.map +1 -1
  59. package/dist/services/issues.js +164 -97
  60. package/dist/services/issues.js.map +1 -1
  61. package/dist/services/organization-workspace-browser.d.ts +6 -2
  62. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  63. package/dist/services/organization-workspace-browser.js +212 -47
  64. package/dist/services/organization-workspace-browser.js.map +1 -1
  65. package/dist/services/plugin-archive-native.d.ts +10 -0
  66. package/dist/services/plugin-archive-native.d.ts.map +1 -0
  67. package/dist/services/plugin-archive-native.js +170 -0
  68. package/dist/services/plugin-archive-native.js.map +1 -0
  69. package/dist/services/plugin-archive-node.d.ts +3 -0
  70. package/dist/services/plugin-archive-node.d.ts.map +1 -0
  71. package/dist/services/plugin-archive-node.js +97 -0
  72. package/dist/services/plugin-archive-node.js.map +1 -0
  73. package/dist/services/rudder-plugins.d.ts +1 -1
  74. package/dist/services/rudder-plugins.d.ts.map +1 -1
  75. package/dist/services/rudder-plugins.js +54 -93
  76. package/dist/services/rudder-plugins.js.map +1 -1
  77. package/dist/services/rudder-plugins.test.js +51 -0
  78. package/dist/services/rudder-plugins.test.js.map +1 -1
  79. package/dist/services/run-intelligence.d.ts +1 -0
  80. package/dist/services/run-intelligence.d.ts.map +1 -1
  81. package/dist/services/run-intelligence.js +1 -1
  82. package/dist/services/run-intelligence.js.map +1 -1
  83. package/dist/services/run-log-store.d.ts +1 -0
  84. package/dist/services/run-log-store.d.ts.map +1 -1
  85. package/dist/services/run-log-store.js +123 -4
  86. package/dist/services/run-log-store.js.map +1 -1
  87. package/dist/services/runtime-kernel/heartbeat.d.ts +1 -0
  88. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  89. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  90. package/dist/services/runtime-kernel/model-fallback.d.ts.map +1 -1
  91. package/dist/services/runtime-kernel/model-fallback.js +56 -11
  92. package/dist/services/runtime-kernel/model-fallback.js.map +1 -1
  93. package/dist/services/workspace-backup-v2.d.ts +1 -0
  94. package/dist/services/workspace-backup-v2.d.ts.map +1 -1
  95. package/dist/services/workspace-backup-v2.js +16 -0
  96. package/dist/services/workspace-backup-v2.js.map +1 -1
  97. package/dist/services/workspace-backups.d.ts.map +1 -1
  98. package/dist/services/workspace-backups.js +4 -2
  99. package/dist/services/workspace-backups.js.map +1 -1
  100. package/dist/services/workspace-files-native.d.ts +16 -0
  101. package/dist/services/workspace-files-native.d.ts.map +1 -0
  102. package/dist/services/workspace-files-native.js +212 -0
  103. package/dist/services/workspace-files-native.js.map +1 -0
  104. package/dist/services/workspace-manifest-native.d.ts +1 -1
  105. package/dist/services/workspace-manifest-native.d.ts.map +1 -1
  106. package/dist/services/workspace-manifest-native.js +49 -6
  107. package/dist/services/workspace-manifest-native.js.map +1 -1
  108. package/dist/services/workspace-runtime.helpers.d.ts.map +1 -1
  109. package/dist/services/workspace-runtime.helpers.js +13 -11
  110. package/dist/services/workspace-runtime.helpers.js.map +1 -1
  111. package/package.json +14 -14
  112. package/resources/bundled-skills/rudder-docs/SKILL.md +12 -12
  113. package/resources/bundled-skills/rudder-docs/references/source-map.md +7 -31
  114. package/skills/rudder-docs/SKILL.md +12 -12
  115. package/skills/rudder-docs/references/source-map.md +7 -31
  116. package/ui-dist/assets/{LibraryLiveSurface-BHneB__U.js → LibraryLiveSurface-D-6Puw4N.js} +1 -1
  117. package/ui-dist/assets/{abnfDiagram-VRR7QNED-GUoY9rTt.js → abnfDiagram-VRR7QNED-DjgQ9iD_.js} +1 -1
  118. package/ui-dist/assets/{addon-fit-CqXXE0-1.js → addon-fit-OOCvZY14.js} +1 -1
  119. package/ui-dist/assets/{arc-BD7m_r6l.js → arc-D6yqUjH4.js} +1 -1
  120. package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-nEufXAY_.js → architectureDiagram-ZJ3FMSHR-eF-Es2a-.js} +1 -1
  121. package/ui-dist/assets/{blockDiagram-677ZJIJ3-D3qUEbL5.js → blockDiagram-677ZJIJ3-B7G2uxR7.js} +1 -1
  122. package/ui-dist/assets/{c4Diagram-LMCZKHZV-7SPUxKgx.js → c4Diagram-LMCZKHZV-YhY4bz85.js} +1 -1
  123. package/ui-dist/assets/channel-DISxdJMI.js +1 -0
  124. package/ui-dist/assets/{chunk-2Q5K7J3B-6o3ETzOY.js → chunk-2Q5K7J3B-D7QxbIUK.js} +1 -1
  125. package/ui-dist/assets/{chunk-32BRIVSS-Efd566e8.js → chunk-32BRIVSS-CArPDsLy.js} +1 -1
  126. package/ui-dist/assets/{chunk-5VM5RSS4-BA4X4gzU.js → chunk-5VM5RSS4-ByyAjdOI.js} +1 -1
  127. package/ui-dist/assets/{chunk-EX3LRPZG-BD7K-mPD.js → chunk-EX3LRPZG-B6Z9cEKa.js} +1 -1
  128. package/ui-dist/assets/{chunk-JWPE2WC7-DXv6iHxr.js → chunk-JWPE2WC7-L5jYy4CI.js} +1 -1
  129. package/ui-dist/assets/{chunk-MOJQB5TN-dQ6gk-_u.js → chunk-MOJQB5TN-BBTZZsOQ.js} +1 -1
  130. package/ui-dist/assets/{chunk-RYQCIY6F-xEk6bTb7.js → chunk-RYQCIY6F-DyV9CaPT.js} +1 -1
  131. package/ui-dist/assets/{chunk-V7JOEXUC-zlvz5Y5q.js → chunk-V7JOEXUC-CNztgBrF.js} +1 -1
  132. package/ui-dist/assets/{chunk-VR4S4FIN--zCSWk5F.js → chunk-VR4S4FIN-Bv62Xmto.js} +1 -1
  133. package/ui-dist/assets/{chunk-XXDRQBXY-CZ2QWPRe.js → chunk-XXDRQBXY-B5AzJDaz.js} +1 -1
  134. package/ui-dist/assets/classDiagram-OUVF2IWQ-BssC8UiF.js +1 -0
  135. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-BssC8UiF.js +1 -0
  136. package/ui-dist/assets/{cose-bilkent-JH36ORCC-DrRNgFzR.js → cose-bilkent-JH36ORCC-DrzyjyO_.js} +1 -1
  137. package/ui-dist/assets/{cynefin-VYW2F7L2-T9PONuTh.js → cynefin-VYW2F7L2-ChoChVKW.js} +1 -1
  138. package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-Dkop4mS3.js → cynefinDiagram-TSTJHNR4-Fv4ecj_f.js} +1 -1
  139. package/ui-dist/assets/{dagre-VKFMJZFB-Chc8Imxx.js → dagre-VKFMJZFB-Zd466k0O.js} +1 -1
  140. package/ui-dist/assets/{diagram-FQU43EPY-n4pgnJYP.js → diagram-FQU43EPY-BgL3Uu7z.js} +1 -1
  141. package/ui-dist/assets/{diagram-G47NLZAW-DnjvVsj5.js → diagram-G47NLZAW-Bcbg2TZF.js} +1 -1
  142. package/ui-dist/assets/{diagram-NH7WQ7WH-kHZ9astC.js → diagram-NH7WQ7WH-YS4gl6dx.js} +1 -1
  143. package/ui-dist/assets/{diagram-OA4YK3LP-BAMly3yv.js → diagram-OA4YK3LP-C-A8A6Xi.js} +1 -1
  144. package/ui-dist/assets/{diagram-WEI45ONY-BTFQhBrp.js → diagram-WEI45ONY-mqkaxwes.js} +1 -1
  145. package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-c2T4DORg.js → ebnfDiagram-CCIWWBDH-CvH1cI4K.js} +1 -1
  146. package/ui-dist/assets/{erDiagram-Q63AITRT-BXLi1JHT.js → erDiagram-Q63AITRT-Duhw1MUB.js} +1 -1
  147. package/ui-dist/assets/{flowDiagram-23GEKE2U-CHzX7naH.js → flowDiagram-23GEKE2U-DUkAECUs.js} +1 -1
  148. package/ui-dist/assets/{ganttDiagram-NO4QXBWP-BqUKeoDF.js → ganttDiagram-NO4QXBWP-B3cINaqn.js} +1 -1
  149. package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-DM4K-DoE.js → gitGraphDiagram-IHSO6WYX-BMEHvYy5.js} +1 -1
  150. package/ui-dist/assets/{graph-BDejhZe_.js → graph-B1-dUGRC.js} +1 -1
  151. package/ui-dist/assets/{index-BJ-hTdyx.js → index-BFmYzGL9.js} +1 -1
  152. package/ui-dist/assets/{index-DSAl1a-M.js → index-BGAEBJze.js} +1 -1
  153. package/ui-dist/assets/{index-CH8tOJZ9.js → index-BMfPEMCH.js} +1 -1
  154. package/ui-dist/assets/{index-BuCCxdIU.js → index-BglV2AAc.js} +1 -1
  155. package/ui-dist/assets/{index-CV0k_zvK.js → index-Blix7pGb.js} +1 -1
  156. package/ui-dist/assets/{index-XnCv8P5f.js → index-CBrXXiQ_.js} +1 -1
  157. package/ui-dist/assets/{index-CIbvFYbV.js → index-CSMCUMYP.js} +1 -1
  158. package/ui-dist/assets/index-CUW-AUBO.css +32 -0
  159. package/ui-dist/assets/{index-Bni14R_J.js → index-CaveK_TY.js} +1 -1
  160. package/ui-dist/assets/{index-WbyFmgaS.js → index-DHcvu8nk.js} +1 -1
  161. package/ui-dist/assets/{index-DAM3EGbv.js → index-DSz4Tjbh.js} +1 -1
  162. package/ui-dist/assets/{index-DVDu5Nwd.js → index-DgNZ8Oto.js} +1 -1
  163. package/ui-dist/assets/{index-DZBZIqJI.js → index-Drmy6GaQ.js} +1 -1
  164. package/ui-dist/assets/{index-B4zWD5-i.js → index-DvrowL98.js} +1 -1
  165. package/ui-dist/assets/{index-BN90mN5Z.js → index-DxHCQu-4.js} +1 -1
  166. package/ui-dist/assets/{index-DHzaZSPM.js → index-IJhIHwGp.js} +546 -546
  167. package/ui-dist/assets/{infoDiagram-FWYZ7A6U-PcFBrZTu.js → infoDiagram-FWYZ7A6U-BJ7smuOx.js} +1 -1
  168. package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-BF2qBweH.js → ishikawaDiagram-FXEZZL3T-Bn7jPYRy.js} +1 -1
  169. package/ui-dist/assets/{journeyDiagram-5HDEW3XC-D3TqAoEB.js → journeyDiagram-5HDEW3XC-D5NYECBN.js} +1 -1
  170. package/ui-dist/assets/{kanban-definition-HUTT4EX6-CUiHrckR.js → kanban-definition-HUTT4EX6-CcrYvDYt.js} +1 -1
  171. package/ui-dist/assets/{layout-DMKAzTyC.js → layout-32M7EWAw.js} +1 -1
  172. package/ui-dist/assets/{linear-D8C0WdTA.js → linear-DT9kJUfH.js} +1 -1
  173. package/ui-dist/assets/{map-DUCHGq8e.js → map-C2crE4Bv.js} +1 -1
  174. package/ui-dist/assets/{mermaid.core-C-eoqr77.js → mermaid.core-B9y8ebhy.js} +4 -4
  175. package/ui-dist/assets/{mindmap-definition-LN4V7U3C-B54nV_Hu.js → mindmap-definition-LN4V7U3C-CaFsY4LT.js} +1 -1
  176. package/ui-dist/assets/{pegDiagram-2B236MQR-CcIDNzTo.js → pegDiagram-2B236MQR-BMTtqHzV.js} +1 -1
  177. package/ui-dist/assets/{pieDiagram-ENE6RG2P-BU4swrSE.js → pieDiagram-ENE6RG2P-DRl32AMI.js} +1 -1
  178. package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-CnniDL18.js → quadrantDiagram-ABIIQ3AL-pXeC4qE5.js} +1 -1
  179. package/ui-dist/assets/{railroadDiagram-RFXS5EU6-CiBY7wpV.js → railroadDiagram-RFXS5EU6-tGzWZNPI.js} +1 -1
  180. package/ui-dist/assets/{requirementDiagram-TGXJPOKE-C7c_tTqS.js → requirementDiagram-TGXJPOKE-DEqEN1a0.js} +1 -1
  181. package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DJuTS99d.js → sankeyDiagram-HTMAVEWB-DjVDCNO4.js} +1 -1
  182. package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-BxhtY70W.js → sequenceDiagram-DBY2YBRQ-CRKMhKhd.js} +1 -1
  183. package/ui-dist/assets/{sizeCapture-X5ZJPWSS-0A0Kww03.js → sizeCapture-X5ZJPWSS-CWl0gYed.js} +1 -1
  184. package/ui-dist/assets/{stateDiagram-2N3HPSRC-DPw4bytW.js → stateDiagram-2N3HPSRC-BH6FVEBD.js} +1 -1
  185. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-DFWkP3y3.js +1 -0
  186. package/ui-dist/assets/{swimlanes-5IMT3BWC-D6MMDWrs.js → swimlanes-5IMT3BWC-BZdn1cQ5.js} +2 -2
  187. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BwA91U1K.js +8 -0
  188. package/ui-dist/assets/{timeline-definition-FHXFAJF6-gNKDUxQx.js → timeline-definition-FHXFAJF6-CMofE9DN.js} +1 -1
  189. package/ui-dist/assets/{vennDiagram-L72KCM5P-BpZCM-ST.js → vennDiagram-L72KCM5P-CszussGd.js} +1 -1
  190. package/ui-dist/assets/{wardleyDiagram-EHGQE667-CvR0dYqn.js → wardleyDiagram-EHGQE667-C1Op8a5q.js} +1 -1
  191. package/ui-dist/assets/{xterm-CEksOm0P.js → xterm-BM41mI-I.js} +1 -1
  192. package/ui-dist/assets/{xychartDiagram-FW5EYKEG-C6_4sxeI.js → xychartDiagram-FW5EYKEG-zi-IHNrX.js} +1 -1
  193. package/ui-dist/index.html +2 -2
  194. package/ui-dist/assets/channel-Czz1OwXA.js +0 -1
  195. package/ui-dist/assets/classDiagram-OUVF2IWQ-C_ooAkuA.js +0 -1
  196. package/ui-dist/assets/classDiagram-v2-EOCWNBFH-C_ooAkuA.js +0 -1
  197. package/ui-dist/assets/index-BsB7Sc7m.css +0 -32
  198. package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-ANI7JZ72.js +0 -1
  199. package/ui-dist/assets/swimlanesDiagram-G3AALYLV-CxjGCcz5.js +0 -8
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { resolveStoredOrDerivedAgentWorkspaceKey } from "./agent-workspace-key.js";
8
+ import { syncDirectory, syncFileHandle } from "./file-system-durability.js";
8
9
  const DEFAULT_INSTANCE_ID = "default";
9
10
  const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
10
11
  const FRIENDLY_PATH_SEGMENT_RE = /[^a-zA-Z0-9._-]+/g;
@@ -13,8 +14,23 @@ const EXECUTABLE_MODE_BITS = 0o111;
13
14
  const MIGRATION_BACKUP_DIR_NAME = ".rudder-migration-backups";
14
15
  const WORKSPACE_PERMISSION_ERROR_CODES = new Set(["EACCES", "EPERM", "EROFS"]);
15
16
  const ORGANIZATION_WORKSPACE_MAP_FILE = ".rudder-organizations.json";
17
+ const ORGANIZATION_WORKSPACE_MAP_LOCK = ".rudder-organizations.lock";
18
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX = ".rudder-lock-owner-";
19
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX = ".rudder-organizations.lock.acquire-";
20
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX = ".rudder-organizations.lock.recovered-";
21
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_KIND = "rudder-organization-workspace-map-lock";
22
+ const ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX = ".rudder-lock-reclaim-";
23
+ const ORGANIZATION_WORKSPACE_IDENTITY_FILE = ".rudder-workspace.json";
24
+ const ORGANIZATION_WORKSPACE_MIGRATION_FILE = ".rudder-workspace-migrations.json";
25
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS = 10_000;
26
+ const ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS = 60_000;
27
+ const ORGANIZATION_WORKSPACE_MAP_LOCK_TARGET_EXISTS_CODES = new Set(["EEXIST", "ENOTEMPTY", "EISDIR"]);
16
28
  const RESERVED_ORGANIZATION_WORKSPACE_NAMES = new Set([
17
29
  ORGANIZATION_WORKSPACE_MAP_FILE,
30
+ ORGANIZATION_WORKSPACE_MAP_LOCK,
31
+ MIGRATION_BACKUP_DIR_NAME,
32
+ "organizations",
33
+ "projects",
18
34
  ".rudder",
19
35
  "backups",
20
36
  "data",
@@ -83,7 +99,9 @@ function resolveAgentWorkspacePathSegment(agent) {
83
99
  export function resolveOrganizationWorkspaceRoot(orgId) {
84
100
  const normalizedOrgId = resolveOrganizationStorageKey(orgId);
85
101
  if (usesFriendlyOrganizationWorkspaceHome()) {
86
- return path.resolve(resolveOrganizationWorkspaceHomeDir(), readOrganizationWorkspaceFolderName(orgId) ?? normalizedOrgId);
102
+ const mappedFolderName = readOrganizationWorkspaceFolderName(orgId);
103
+ const folderName = mappedFolderName ?? sanitizeOrganizationWorkspaceFolderName(normalizedOrgId, normalizedOrgId);
104
+ return path.resolve(resolveOrganizationWorkspaceHomeDir(), folderName);
87
105
  }
88
106
  return path.resolve(resolveOrganizationWorkspaceHomeDir(), normalizedOrgId, "workspaces");
89
107
  }
@@ -155,11 +173,14 @@ export function resolveManagedOrganizationCodebaseDir(input) {
155
173
  return path.resolve(resolveOrganizationWorkspaceRoot(input.orgId), "codebase", sanitizeFriendlyPathSegment(input.repoName, "_default"));
156
174
  }
157
175
  export async function ensureOrganizationWorkspaceLayout(org) {
176
+ return await updateOrganizationWorkspaceMap(() => ensureOrganizationWorkspaceLayoutUnlocked(org));
177
+ }
178
+ async function ensureOrganizationWorkspaceLayoutUnlocked(org) {
158
179
  const orgId = typeof org === "string" ? org : org.id;
159
- await migrateOrganizationStorageRoot(orgId);
180
+ await migrateOrganizationStorageRootUnlocked(orgId);
160
181
  let mappingState = null;
161
182
  if (typeof org !== "string") {
162
- mappingState = await ensureOrganizationWorkspaceMapping(org);
183
+ mappingState = await ensureOrganizationWorkspaceMappingUnlocked(org);
163
184
  }
164
185
  else if (usesFriendlyOrganizationWorkspaceHome()) {
165
186
  const map = await readOrganizationWorkspaceMapFile();
@@ -168,20 +189,24 @@ export async function ensureOrganizationWorkspaceLayout(org) {
168
189
  mappingState = { record: existing, created: false };
169
190
  }
170
191
  }
171
- await migrateOrganizationWorkspaceRoot(orgId, {
192
+ const root = resolveOrganizationWorkspaceRoot(orgId);
193
+ await assertOrganizationWorkspaceRootIsOwned(root);
194
+ await migrateOrganizationWorkspaceRootUnlocked(orgId, {
172
195
  failIfMappedFolderMissing: Boolean(mappingState && !mappingState.created),
173
196
  });
174
- const root = resolveOrganizationWorkspaceRoot(orgId);
197
+ await assertOrganizationWorkspaceRootIsOwned(root);
175
198
  const agentsDir = resolveOrganizationAgentsDir(orgId);
176
199
  const skillsDir = resolveOrganizationSkillsDir(orgId);
177
200
  const projectsDir = resolveOrganizationProjectsDir(orgId);
178
201
  try {
202
+ await fs.mkdir(root, { recursive: true });
203
+ await assertOrganizationWorkspaceRootIsOwned(root);
179
204
  await Promise.all([
180
- fs.mkdir(root, { recursive: true }),
181
205
  fs.mkdir(agentsDir, { recursive: true }),
182
206
  fs.mkdir(skillsDir, { recursive: true }),
183
207
  fs.mkdir(projectsDir, { recursive: true }),
184
208
  ]);
209
+ await ensureOrganizationWorkspaceIdentity(root, orgId);
185
210
  }
186
211
  catch (error) {
187
212
  if (isPermissionError(error)) {
@@ -196,52 +221,330 @@ export async function ensureOrganizationWorkspaceLayout(org) {
196
221
  }
197
222
  return { root, agentsDir, skillsDir, projectsDir };
198
223
  }
199
- async function ensureOrganizationWorkspaceMapping(org) {
224
+ async function ensureOrganizationWorkspaceMappingUnlocked(org) {
200
225
  if (!usesFriendlyOrganizationWorkspaceHome())
201
226
  return null;
202
- return await updateOrganizationWorkspaceMap(async () => {
203
- const orgId = validatePathSegment(org.id, "org id");
204
- const homeDir = resolveOrganizationWorkspaceHomeDir();
205
- const mapPath = resolveOrganizationWorkspaceMapPath();
206
- await fs.mkdir(homeDir, { recursive: true });
207
- const now = new Date().toISOString();
208
- const mapState = await readOrganizationWorkspaceMapFileState();
209
- const map = mapState.map;
210
- const existing = findOrganizationWorkspaceMapRecord(map, orgId);
211
- if (existing) {
212
- existing.orgName = org.name ?? existing.orgName ?? null;
213
- existing.updatedAt = now;
214
- await writeOrganizationWorkspaceMapFile(mapPath, map);
215
- return { record: existing, created: false };
216
- }
217
- const folderName = await allocateOrganizationWorkspaceFolderName({
218
- homeDir,
219
- map,
220
- orgId,
221
- orgName: org.name,
222
- orgUrlKey: org.urlKey,
223
- allowExistingBaseDirectory: !mapState.exists,
224
- });
225
- const record = {
226
- instanceId: resolveRudderInstanceId(),
227
- orgId,
228
- folderName,
229
- orgName: org.name ?? null,
230
- createdAt: now,
231
- updatedAt: now,
232
- };
233
- map.organizations.push(record);
227
+ const orgId = validatePathSegment(org.id, "org id");
228
+ const homeDir = resolveOrganizationWorkspaceHomeDir();
229
+ const mapPath = resolveOrganizationWorkspaceMapPath();
230
+ await fs.mkdir(homeDir, { recursive: true });
231
+ const now = new Date().toISOString();
232
+ const mapState = await readOrganizationWorkspaceMapFileState();
233
+ const map = mapState.map;
234
+ const existing = findOrganizationWorkspaceMapRecord(map, orgId);
235
+ if (existing) {
236
+ existing.orgName = org.name ?? existing.orgName ?? null;
237
+ existing.updatedAt = now;
234
238
  await writeOrganizationWorkspaceMapFile(mapPath, map);
235
- return { record, created: true };
239
+ return { record: existing, created: false };
240
+ }
241
+ const folderName = await allocateOrganizationWorkspaceFolderName({
242
+ homeDir,
243
+ map,
244
+ orgId,
245
+ orgName: org.name,
246
+ orgUrlKey: org.urlKey,
236
247
  });
248
+ const folderPath = path.resolve(homeDir, folderName);
249
+ // Materialize the identity before publishing the map record so a crash can
250
+ // reclaim a directory that was created just before the map write.
251
+ await fs.mkdir(folderPath, { recursive: true });
252
+ await ensureOrganizationWorkspaceIdentity(folderPath, orgId);
253
+ const record = {
254
+ instanceId: resolveRudderInstanceId(),
255
+ orgId,
256
+ folderName,
257
+ orgName: org.name ?? null,
258
+ createdAt: now,
259
+ updatedAt: now,
260
+ };
261
+ map.organizations.push(record);
262
+ await writeOrganizationWorkspaceMapFile(mapPath, map);
263
+ return { record, created: true };
237
264
  }
238
265
  async function updateOrganizationWorkspaceMap(fn) {
239
266
  const previous = organizationWorkspaceMapUpdateQueue.catch(() => { });
240
- const current = previous.then(fn, fn);
267
+ const current = previous.then(() => withOrganizationWorkspaceMapLock(fn), () => withOrganizationWorkspaceMapLock(fn));
241
268
  organizationWorkspaceMapUpdateQueue = current.catch(() => { });
242
269
  return await current;
243
270
  }
271
+ async function withOrganizationWorkspaceMapLock(fn) {
272
+ const lockPath = path.resolve(resolveOrganizationWorkspaceHomeDir(), ORGANIZATION_WORKSPACE_MAP_LOCK);
273
+ const lockParentPath = path.dirname(lockPath);
274
+ const startedAt = Date.now();
275
+ const token = randomUUID();
276
+ const ownerFileName = `${ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX}${token}.json`;
277
+ const acquisitionPath = path.join(lockParentPath, `${ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX}${token}`);
278
+ const acquisitionOwnerPath = path.join(acquisitionPath, ownerFileName);
279
+ let acquisitionCreated = false;
280
+ let lockAcquired = false;
281
+ await fs.mkdir(lockParentPath, { recursive: true });
282
+ try {
283
+ // Build a complete owner directory before the final rename. This keeps an
284
+ // acquired lock from ever being observable without its owner record.
285
+ await fs.mkdir(acquisitionPath, { mode: 0o700 });
286
+ acquisitionCreated = true;
287
+ const owner = {
288
+ kind: ORGANIZATION_WORKSPACE_MAP_LOCK_KIND,
289
+ version: 1,
290
+ token,
291
+ pid: process.pid,
292
+ hostname: os.hostname(),
293
+ createdAt: new Date().toISOString(),
294
+ };
295
+ const handle = await fs.open(acquisitionOwnerPath, "wx", 0o600);
296
+ try {
297
+ await handle.writeFile(`${JSON.stringify(owner, null, 2)}\n`, "utf8");
298
+ await syncFileHandle(handle);
299
+ }
300
+ finally {
301
+ await handle.close();
302
+ }
303
+ await syncDirectory(acquisitionPath);
304
+ while (true) {
305
+ if (Date.now() - startedAt >= ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS) {
306
+ throw Object.assign(new Error(`Timed out waiting for the organization workspace mapping lock at ${lockPath}.`), { code: "RUDDER_WORKSPACE_MAP_LOCK_TIMEOUT" });
307
+ }
308
+ const existingLock = await lstatIfExists(lockPath);
309
+ if (existingLock) {
310
+ const existingOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
311
+ if (!existingOwner) {
312
+ const entries = await fs.readdir(lockPath).catch(() => []);
313
+ if (entries.length === 0
314
+ && await quarantineStaleEmptyOrganizationWorkspaceMapLock(lockPath)) {
315
+ continue;
316
+ }
317
+ if (entries.length === 0 && Date.now() - startedAt < ORGANIZATION_WORKSPACE_MAP_LOCK_TIMEOUT_MS) {
318
+ await new Promise((resolve) => setTimeout(resolve, 25));
319
+ continue;
320
+ }
321
+ throw organizationWorkspaceMapLockCollision(lockPath);
322
+ }
323
+ const ownerAgeMs = Date.now() - Date.parse(existingOwner.createdAt);
324
+ if (existingOwner.hostname === os.hostname()
325
+ && ownerAgeMs > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
326
+ && !isProcessAlive(existingOwner.pid)) {
327
+ await reclaimStaleOrganizationWorkspaceMapLock(lockPath, existingOwner);
328
+ continue;
329
+ }
330
+ await new Promise((resolve) => setTimeout(resolve, 25));
331
+ continue;
332
+ }
333
+ try {
334
+ await fs.rename(acquisitionPath, lockPath);
335
+ lockAcquired = true;
336
+ break;
337
+ }
338
+ catch (error) {
339
+ if (!ORGANIZATION_WORKSPACE_MAP_LOCK_TARGET_EXISTS_CODES.has(errorCode(error) ?? ""))
340
+ throw error;
341
+ // Another process appeared between the existence probe and rename;
342
+ // inspect it through the non-destructive path on the next iteration.
343
+ }
344
+ }
345
+ return await fn();
346
+ }
347
+ finally {
348
+ if (lockAcquired) {
349
+ await releaseOrganizationWorkspaceMapLock(lockPath, token);
350
+ }
351
+ else if (acquisitionCreated) {
352
+ await fs.rm(acquisitionPath, { recursive: true, force: true });
353
+ }
354
+ }
355
+ }
356
+ function organizationWorkspaceMapLockCollision(lockPath) {
357
+ return Object.assign(new Error(`Refusing to replace the unrecognized organization workspace mapping lock path at ${lockPath}. `
358
+ + "Rename or restore that directory before starting Rudder."), { code: "RUDDER_WORKSPACE_MAP_LOCK_COLLISION" });
359
+ }
360
+ async function quarantineStaleEmptyOrganizationWorkspaceMapLock(lockPath) {
361
+ const lockStat = await lstatIfExists(lockPath);
362
+ if (!lockStat
363
+ || !lockStat.isDirectory()
364
+ || lockStat.isSymbolicLink()
365
+ || (lockStat.mode & 0o777) !== 0o700
366
+ || Date.now() - lockStat.mtimeMs <= ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS)
367
+ return false;
368
+ const entries = await fs.readdir(lockPath).catch((error) => {
369
+ if (errorCode(error) === "ENOENT")
370
+ return [];
371
+ throw error;
372
+ });
373
+ if (entries.length > 0)
374
+ return false;
375
+ const quarantinePath = path.join(path.dirname(lockPath), `${ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX}${randomUUID()}`);
376
+ try {
377
+ await fs.rename(lockPath, quarantinePath);
378
+ }
379
+ catch (error) {
380
+ if (errorCode(error) === "ENOENT")
381
+ return true;
382
+ throw error;
383
+ }
384
+ await syncDirectory(path.dirname(lockPath));
385
+ return true;
386
+ }
387
+ async function reclaimStaleOrganizationWorkspaceMapLock(lockPath, observedOwner) {
388
+ const claim = {
389
+ version: 1,
390
+ token: randomUUID(),
391
+ ownerToken: observedOwner.token,
392
+ pid: process.pid,
393
+ hostname: os.hostname(),
394
+ createdAt: new Date().toISOString(),
395
+ };
396
+ const reclaimPath = path.join(lockPath, `${ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX}${claim.token}.json`);
397
+ let handle;
398
+ try {
399
+ handle = await fs.open(reclaimPath, "wx", 0o600);
400
+ }
401
+ catch (error) {
402
+ if (errorCode(error) === "ENOENT")
403
+ return;
404
+ throw error;
405
+ }
406
+ try {
407
+ await handle.writeFile(`${JSON.stringify(claim)}\n`, "utf8");
408
+ await syncFileHandle(handle);
409
+ }
410
+ finally {
411
+ await handle.close();
412
+ }
413
+ // Let concurrently-starting reclaimers publish before electing the oldest live claim.
414
+ await new Promise((resolve) => setTimeout(resolve, 25));
415
+ const claims = await readOrganizationWorkspaceMapReclaimClaims(lockPath);
416
+ await Promise.all(claims.map(async (candidate) => {
417
+ if (candidate.hostname === os.hostname()
418
+ && Date.now() - Date.parse(candidate.createdAt) > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
419
+ && !isProcessAlive(candidate.pid)) {
420
+ await fs.rm(candidate.path, { force: true });
421
+ }
422
+ }));
423
+ const liveClaims = (await readOrganizationWorkspaceMapReclaimClaims(lockPath))
424
+ .filter((candidate) => candidate.ownerToken === observedOwner.token)
425
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.token.localeCompare(right.token));
426
+ if (liveClaims[0]?.token !== claim.token) {
427
+ await fs.rm(reclaimPath, { force: true });
428
+ return;
429
+ }
430
+ const currentOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
431
+ const stillStale = currentOwner?.token === observedOwner.token
432
+ && currentOwner.hostname === os.hostname()
433
+ && Date.now() - Date.parse(currentOwner.createdAt) > ORGANIZATION_WORKSPACE_MAP_STALE_LOCK_MS
434
+ && !isProcessAlive(currentOwner.pid);
435
+ if (!stillStale) {
436
+ await fs.rm(reclaimPath, { force: true });
437
+ return;
438
+ }
439
+ const tombstonePath = `${lockPath}.reclaimed-${observedOwner.token}-${randomUUID()}`;
440
+ try {
441
+ await fs.rename(lockPath, tombstonePath);
442
+ }
443
+ catch (error) {
444
+ if (errorCode(error) !== "ENOENT")
445
+ throw error;
446
+ return;
447
+ }
448
+ // Only the positively identified directory captured by the atomic rename is removed.
449
+ await fs.rm(tombstonePath, { recursive: true, force: true });
450
+ }
451
+ async function readOrganizationWorkspaceMapReclaimClaims(lockPath) {
452
+ try {
453
+ const entries = await fs.readdir(lockPath, { withFileTypes: true });
454
+ const claims = await Promise.all(entries.flatMap((entry) => entry.isFile()
455
+ && entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX)
456
+ && entry.name.endsWith(".json")
457
+ ? [readOrganizationWorkspaceMapReclaimClaim(path.join(lockPath, entry.name), entry.name)]
458
+ : []));
459
+ return claims.flatMap((claim) => claim ? [claim] : []);
460
+ }
461
+ catch (error) {
462
+ if (errorCode(error) === "ENOENT")
463
+ return [];
464
+ throw error;
465
+ }
466
+ }
467
+ async function readOrganizationWorkspaceMapReclaimClaim(claimPath, fileName) {
468
+ try {
469
+ const claim = JSON.parse(await fs.readFile(claimPath, "utf8"));
470
+ if (claim.version !== 1
471
+ || typeof claim.token !== "string"
472
+ || fileName !== `${ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX}${claim.token}.json`
473
+ || typeof claim.ownerToken !== "string"
474
+ || !Number.isSafeInteger(claim.pid)
475
+ || (claim.pid ?? 0) <= 0
476
+ || typeof claim.hostname !== "string"
477
+ || typeof claim.createdAt !== "string"
478
+ || !Number.isFinite(Date.parse(claim.createdAt)))
479
+ return null;
480
+ return { ...claim, path: claimPath };
481
+ }
482
+ catch {
483
+ return null;
484
+ }
485
+ }
486
+ async function releaseOrganizationWorkspaceMapLock(lockPath, token) {
487
+ const currentOwner = await readOrganizationWorkspaceMapLockOwner(lockPath);
488
+ if (currentOwner?.token !== token)
489
+ return;
490
+ const tombstonePath = `${lockPath}.released-${token}-${randomUUID()}`;
491
+ try {
492
+ await fs.rename(lockPath, tombstonePath);
493
+ }
494
+ catch (error) {
495
+ if (errorCode(error) !== "ENOENT")
496
+ throw error;
497
+ return;
498
+ }
499
+ await fs.rm(tombstonePath, { recursive: true, force: true });
500
+ }
501
+ async function readOrganizationWorkspaceMapLockOwner(lockPath) {
502
+ try {
503
+ const entries = await fs.readdir(lockPath, { withFileTypes: true });
504
+ const ownerEntries = entries.filter((entry) => entry.isFile()
505
+ && entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX)
506
+ && entry.name.endsWith(".json"));
507
+ if (ownerEntries.length !== 1)
508
+ return null;
509
+ if (entries.some((entry) => entry.name !== ownerEntries[0].name
510
+ && !(entry.isFile()
511
+ && entry.name.startsWith(ORGANIZATION_WORKSPACE_MAP_RECLAIM_PREFIX)
512
+ && entry.name.endsWith(".json"))))
513
+ return null;
514
+ const fileName = ownerEntries[0].name;
515
+ if (!fileName.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX) || !fileName.endsWith(".json")) {
516
+ return null;
517
+ }
518
+ const owner = JSON.parse(await fs.readFile(path.join(lockPath, fileName), "utf8"));
519
+ if (owner.kind !== ORGANIZATION_WORKSPACE_MAP_LOCK_KIND
520
+ || owner.version !== 1
521
+ || typeof owner.token !== "string"
522
+ || fileName !== `${ORGANIZATION_WORKSPACE_MAP_LOCK_OWNER_PREFIX}${owner.token}.json`
523
+ || !Number.isSafeInteger(owner.pid)
524
+ || (owner.pid ?? 0) <= 0
525
+ || typeof owner.hostname !== "string"
526
+ || typeof owner.createdAt !== "string"
527
+ || !Number.isFinite(Date.parse(owner.createdAt)))
528
+ return null;
529
+ return owner;
530
+ }
531
+ catch {
532
+ return null;
533
+ }
534
+ }
535
+ function isProcessAlive(pid) {
536
+ try {
537
+ process.kill(pid, 0);
538
+ return true;
539
+ }
540
+ catch (error) {
541
+ return errorCode(error) !== "ESRCH";
542
+ }
543
+ }
244
544
  export async function migrateOrganizationStorageRoot(orgId) {
545
+ return await updateOrganizationWorkspaceMap(() => migrateOrganizationStorageRootUnlocked(orgId));
546
+ }
547
+ async function migrateOrganizationStorageRootUnlocked(orgId) {
245
548
  const canonicalRootPath = resolveOrganizationRoot(orgId);
246
549
  const legacyRootPath = resolveLegacyOrganizationRoot(orgId);
247
550
  if (canonicalRootPath === legacyRootPath) {
@@ -316,6 +619,9 @@ export async function migrateOrganizationStorageRoot(orgId) {
316
619
  };
317
620
  }
318
621
  export async function migrateOrganizationWorkspaceRoot(orgId, options) {
622
+ return await updateOrganizationWorkspaceMap(() => migrateOrganizationWorkspaceRootUnlocked(orgId, options));
623
+ }
624
+ async function migrateOrganizationWorkspaceRootUnlocked(orgId, options) {
319
625
  const canonicalRootPath = resolveOrganizationWorkspaceRoot(orgId);
320
626
  const legacyRootPath = resolveLegacyOrganizationWorkspaceRoot(orgId);
321
627
  const previousDocumentsRootPath = resolvePreviousDocumentsOrganizationWorkspaceRoot(orgId);
@@ -337,24 +643,53 @@ export async function migrateOrganizationWorkspaceRoot(orgId, options) {
337
643
  for (const candidateLegacyRootPath of legacyRootPaths) {
338
644
  firstLegacyRootPath = firstLegacyRootPath || candidateLegacyRootPath;
339
645
  const legacyExists = await directoryExists(candidateLegacyRootPath);
340
- if (!legacyExists)
646
+ if (!legacyExists) {
647
+ if (await directoryExists(canonicalRootPath)
648
+ && await workspaceIdentityMatches(canonicalRootPath, orgId)
649
+ && await workspaceMigrationIncludesAlias(canonicalRootPath, candidateLegacyRootPath)) {
650
+ await fs.mkdir(path.dirname(candidateLegacyRootPath), { recursive: true });
651
+ await createDirectoryCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
652
+ migrated = true;
653
+ migratedFromRootPath = candidateLegacyRootPath;
654
+ }
655
+ continue;
656
+ }
657
+ if (await pathsReferenceSameDirectory(candidateLegacyRootPath, canonicalRootPath))
341
658
  continue;
342
659
  const canonicalExists = await directoryExists(canonicalRootPath);
343
660
  try {
344
661
  if (canonicalExists) {
345
- await assertCanMergeDirectoryContents(candidateLegacyRootPath, canonicalRootPath);
346
- const retainedDuplicates = await mergeDirectoryContents(candidateLegacyRootPath, canonicalRootPath);
347
- if (retainedDuplicates)
348
- await archiveRetainedMigrationSource(candidateLegacyRootPath);
662
+ await assertWorkspaceRootMoveCanBeAtomic(candidateLegacyRootPath, canonicalRootPath);
663
+ await ensureOrganizationWorkspaceIdentity(canonicalRootPath, orgId);
664
+ await ensureOrganizationWorkspaceIdentity(candidateLegacyRootPath, orgId);
665
+ const compatibilityAliases = new Set([
666
+ ...(await readWorkspaceMigrationAliases(canonicalRootPath)),
667
+ ...(await readWorkspaceMigrationAliases(candidateLegacyRootPath)),
668
+ path.resolve(candidateLegacyRootPath),
669
+ ]);
670
+ // Preserve the legacy root inode while merging. A Run may already have
671
+ // a cwd anywhere below it; renaming this root to the canonical path
672
+ // keeps that cwd attached to the live workspace after the rekey.
673
+ await writeWorkspaceMigrationState(candidateLegacyRootPath, [...compatibilityAliases]);
674
+ await assertCanMergeDirectoryContents(canonicalRootPath, candidateLegacyRootPath);
675
+ const retainedDuplicates = await mergeDirectoryContents(canonicalRootPath, candidateLegacyRootPath);
676
+ if (retainedDuplicates) {
677
+ await archiveRetainedMigrationSource(canonicalRootPath, path.dirname(candidateLegacyRootPath));
678
+ }
349
679
  else
350
- await fs.rmdir(candidateLegacyRootPath);
680
+ await fs.rmdir(canonicalRootPath);
681
+ await moveWorkspacePathWithCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
351
682
  migrated = true;
352
683
  migratedFromRootPath = candidateLegacyRootPath;
353
684
  mergedIntoExistingTarget = true;
354
685
  continue;
355
686
  }
356
687
  await fs.mkdir(path.dirname(canonicalRootPath), { recursive: true });
357
- await movePath(candidateLegacyRootPath, canonicalRootPath);
688
+ await assertWorkspaceRootMoveCanBeAtomic(candidateLegacyRootPath, canonicalRootPath);
689
+ // The identity moves with the directory and makes a post-rename crash repairable.
690
+ await ensureOrganizationWorkspaceIdentity(candidateLegacyRootPath, orgId);
691
+ await recordWorkspaceCompatibilityAlias(candidateLegacyRootPath, candidateLegacyRootPath);
692
+ await moveWorkspacePathWithCompatibilityAlias(candidateLegacyRootPath, canonicalRootPath);
358
693
  migrated = true;
359
694
  migratedFromRootPath = candidateLegacyRootPath;
360
695
  }
@@ -490,33 +825,49 @@ function sanitizeFriendlyPathSegment(value, fallback = "_default") {
490
825
  return sanitized || fallback;
491
826
  }
492
827
  function sanitizeOrganizationWorkspaceFolderName(value, fallback) {
828
+ const safeFallback = sanitizeFriendlyPathSegment(fallback, "_default")
829
+ .replace(/[.]+$/g, "")
830
+ .toLowerCase();
831
+ const normalizedFallback = RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(safeFallback)
832
+ ? `organization-${safeFallback}`
833
+ : safeFallback;
493
834
  const sanitized = sanitizeFriendlyPathSegment(value, fallback)
494
835
  .replace(/[.]+$/g, "")
495
836
  .toLowerCase();
496
837
  const normalized = sanitized && !RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(sanitized)
497
838
  ? sanitized
498
- : fallback;
499
- return normalized || fallback;
839
+ : normalizedFallback;
840
+ return normalized || normalizedFallback;
841
+ }
842
+ function isOrganizationWorkspaceMapInternalDirectoryName(name) {
843
+ return name === ORGANIZATION_WORKSPACE_MAP_LOCK
844
+ || name.startsWith(`${ORGANIZATION_WORKSPACE_MAP_LOCK}.`)
845
+ || name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_ACQUIRE_PREFIX)
846
+ || name.startsWith(ORGANIZATION_WORKSPACE_MAP_LOCK_RECOVERY_PREFIX);
847
+ }
848
+ function isOrganizationWorkspaceMapRecord(value) {
849
+ return typeof value === "object"
850
+ && value !== null
851
+ && typeof value.instanceId === "string"
852
+ && typeof value.orgId === "string"
853
+ && typeof value.folderName === "string"
854
+ && typeof value.createdAt === "string"
855
+ && typeof value.updatedAt === "string";
500
856
  }
501
857
  async function readOrganizationWorkspaceMapFileState() {
502
858
  const mapPath = resolveOrganizationWorkspaceMapPath();
503
859
  try {
504
860
  const raw = await fs.readFile(mapPath, "utf8");
505
861
  const parsed = JSON.parse(raw);
506
- if (parsed.version !== 1 || !Array.isArray(parsed.organizations)) {
862
+ if (parsed.version !== 1 || !Array.isArray(parsed.organizations)
863
+ || !parsed.organizations.every(isOrganizationWorkspaceMapRecord)) {
507
864
  throw new Error(`Invalid organization workspace mapping file at ${mapPath}. Restore the mapping file or restore organizations from workspace backups before starting Rudder.`);
508
865
  }
509
866
  return {
510
867
  exists: true,
511
868
  map: {
512
869
  version: 1,
513
- organizations: parsed.organizations.filter((entry) => typeof entry === "object"
514
- && entry !== null
515
- && typeof entry.instanceId === "string"
516
- && typeof entry.orgId === "string"
517
- && typeof entry.folderName === "string"
518
- && typeof entry.createdAt === "string"
519
- && typeof entry.updatedAt === "string"),
870
+ organizations: parsed.organizations,
520
871
  },
521
872
  };
522
873
  }
@@ -547,47 +898,215 @@ function findOrganizationWorkspaceMapRecord(map, orgId) {
547
898
  function readOrganizationWorkspaceFolderName(orgId) {
548
899
  if (!usesFriendlyOrganizationWorkspaceHome())
549
900
  return null;
901
+ const mapPath = resolveOrganizationWorkspaceMapPath();
902
+ let raw;
550
903
  try {
551
- const raw = fsSync.readFileSync(resolveOrganizationWorkspaceMapPath(), "utf8");
552
- const parsed = JSON.parse(raw);
553
- if (parsed.version !== 1 || !Array.isArray(parsed.organizations))
904
+ raw = fsSync.readFileSync(mapPath, "utf8");
905
+ }
906
+ catch (error) {
907
+ if (errorCode(error) === "ENOENT")
554
908
  return null;
555
- const record = parsed.organizations.find((entry) => typeof entry === "object"
556
- && entry !== null
557
- && entry.instanceId === resolveRudderInstanceId()
558
- && entry.orgId === orgId
559
- && typeof entry.folderName === "string");
560
- return record?.folderName ?? null;
909
+ throw new Error(`Rudder could not read the organization workspace mapping at ${mapPath}.`, { cause: error });
561
910
  }
562
- catch {
911
+ let parsed;
912
+ try {
913
+ parsed = JSON.parse(raw);
914
+ }
915
+ catch (error) {
916
+ throw new Error(`Invalid organization workspace mapping file at ${mapPath}.`, { cause: error });
917
+ }
918
+ if (parsed.version !== 1 || !Array.isArray(parsed.organizations)
919
+ || !parsed.organizations.every(isOrganizationWorkspaceMapRecord)) {
920
+ throw new Error(`Invalid organization workspace mapping file at ${mapPath}.`);
921
+ }
922
+ const record = parsed.organizations.find((entry) => entry.instanceId === resolveRudderInstanceId() && entry.orgId === orgId);
923
+ if (!record)
563
924
  return null;
925
+ try {
926
+ if (typeof record.folderName !== "string") {
927
+ throw new Error("Organization workspace folder mapping must contain a folder name.");
928
+ }
929
+ const folderName = validatePathSegment(record.folderName, "organization workspace folder");
930
+ if (folderName !== record.folderName) {
931
+ throw new Error("Organization workspace folder mapping must not contain leading or trailing whitespace.");
932
+ }
933
+ if (RESERVED_ORGANIZATION_WORKSPACE_NAMES.has(folderName.toLowerCase())) {
934
+ throw new Error(`Organization workspace folder mapping uses reserved folder '${folderName}'.`);
935
+ }
936
+ return folderName;
937
+ }
938
+ catch (error) {
939
+ const detail = error instanceof Error && error.message ? ` ${error.message}` : "";
940
+ throw new Error(`Invalid organization workspace folder mapping for '${orgId}'.${detail}`, { cause: error });
564
941
  }
565
942
  }
566
943
  async function writeOrganizationWorkspaceMapFile(mapPath, map) {
567
944
  const tempPath = `${mapPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
568
945
  await fs.mkdir(path.dirname(mapPath), { recursive: true });
569
- await fs.writeFile(tempPath, `${JSON.stringify(map, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
946
+ const handle = await fs.open(tempPath, "wx", 0o600);
947
+ try {
948
+ await handle.writeFile(`${JSON.stringify(map, null, 2)}\n`, "utf8");
949
+ await syncFileHandle(handle);
950
+ }
951
+ finally {
952
+ await handle.close();
953
+ }
570
954
  await fs.rename(tempPath, mapPath);
955
+ await syncDirectory(path.dirname(mapPath));
956
+ }
957
+ async function ensureOrganizationWorkspaceIdentity(root, orgId) {
958
+ const identityPath = path.join(root, ORGANIZATION_WORKSPACE_IDENTITY_FILE);
959
+ const expectedOrgId = validatePathSegment(orgId, "org id");
960
+ try {
961
+ const existing = JSON.parse(await fs.readFile(identityPath, "utf8"));
962
+ if (existing.orgId !== expectedOrgId) {
963
+ throw new Error(`Organization workspace identity mismatch at ${identityPath}.`);
964
+ }
965
+ return;
966
+ }
967
+ catch (error) {
968
+ if (errorCode(error) === "ENOENT") {
969
+ // Continue below and publish a fresh identity atomically.
970
+ }
971
+ else if (error instanceof SyntaxError) {
972
+ const corruptPath = `${identityPath}.corrupt-${randomUUID()}`;
973
+ await fs.rename(identityPath, corruptPath);
974
+ await syncDirectory(root);
975
+ }
976
+ else {
977
+ throw error;
978
+ }
979
+ }
980
+ const tempPath = `${identityPath}.${process.pid}.${randomUUID()}.tmp`;
981
+ try {
982
+ const handle = await fs.open(tempPath, "wx", 0o600);
983
+ try {
984
+ await handle.writeFile(`${JSON.stringify({ version: 1, orgId: expectedOrgId }, null, 2)}\n`, "utf8");
985
+ await syncFileHandle(handle);
986
+ }
987
+ finally {
988
+ await handle.close();
989
+ }
990
+ await fs.rename(tempPath, identityPath);
991
+ await syncDirectory(root);
992
+ }
993
+ catch (error) {
994
+ if (errorCode(error) !== "EEXIST" || !(await workspaceIdentityMatches(root, expectedOrgId))) {
995
+ throw error;
996
+ }
997
+ }
998
+ finally {
999
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
1000
+ }
1001
+ }
1002
+ async function workspaceIdentityMatches(root, orgId) {
1003
+ try {
1004
+ const identity = JSON.parse(await fs.readFile(path.join(root, ORGANIZATION_WORKSPACE_IDENTITY_FILE), "utf8"));
1005
+ return identity.orgId === validatePathSegment(orgId, "org id");
1006
+ }
1007
+ catch {
1008
+ return false;
1009
+ }
1010
+ }
1011
+ async function assertFriendlyWorkspaceRemovalOwnership(orgId, workspaceRootPath, map) {
1012
+ if (!usesFriendlyOrganizationWorkspaceHome())
1013
+ return;
1014
+ const record = findOrganizationWorkspaceMapRecord(map, orgId);
1015
+ if (record) {
1016
+ const normalizedFolderName = record.folderName.toLowerCase();
1017
+ const duplicate = map.organizations.find((entry) => entry.instanceId === resolveRudderInstanceId()
1018
+ && entry.orgId !== orgId
1019
+ && entry.folderName.toLowerCase() === normalizedFolderName);
1020
+ if (duplicate) {
1021
+ throw new Error([
1022
+ `Refusing to remove organization workspace '${workspaceRootPath}'.`,
1023
+ `The folder is also mapped to organization '${duplicate.orgId}'.`,
1024
+ ].join(" "));
1025
+ }
1026
+ }
1027
+ const existing = await pathExists(workspaceRootPath);
1028
+ if (!existing)
1029
+ return;
1030
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
1031
+ throw new Error(`Refusing to remove organization workspace '${workspaceRootPath}' because it is not a regular directory.`);
1032
+ }
1033
+ if (!await workspaceIdentityMatches(workspaceRootPath, orgId)) {
1034
+ throw new Error(`Refusing to remove organization workspace '${workspaceRootPath}' because its identity does not match '${orgId}'.`);
1035
+ }
1036
+ }
1037
+ async function recordWorkspaceCompatibilityAlias(root, aliasPath) {
1038
+ const aliases = await readWorkspaceMigrationAliases(root);
1039
+ const normalizedAliasPath = path.resolve(aliasPath);
1040
+ if (aliases.includes(normalizedAliasPath))
1041
+ return;
1042
+ aliases.push(normalizedAliasPath);
1043
+ await writeWorkspaceMigrationState(root, aliases);
1044
+ }
1045
+ async function readWorkspaceMigrationAliases(root) {
1046
+ const migrationPath = path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE);
1047
+ try {
1048
+ const parsed = JSON.parse(await fs.readFile(migrationPath, "utf8"));
1049
+ if (parsed.version !== 1 || !Array.isArray(parsed.compatibilityAliases)) {
1050
+ throw new Error(`Invalid organization workspace migration state at ${migrationPath}.`);
1051
+ }
1052
+ return parsed.compatibilityAliases.filter((entry) => typeof entry === "string");
1053
+ }
1054
+ catch (error) {
1055
+ if (errorCode(error) === "ENOENT")
1056
+ return [];
1057
+ throw error;
1058
+ }
1059
+ }
1060
+ async function writeWorkspaceMigrationState(root, aliases) {
1061
+ const migrationPath = path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE);
1062
+ const state = {
1063
+ version: 1,
1064
+ compatibilityAliases: [...new Set(aliases.map((alias) => path.resolve(alias)))].sort(),
1065
+ };
1066
+ const tempPath = `${migrationPath}.${process.pid}.${randomUUID()}.tmp`;
1067
+ const handle = await fs.open(tempPath, "wx", 0o600);
1068
+ try {
1069
+ await handle.writeFile(`${JSON.stringify(state, null, 2)}\n`, "utf8");
1070
+ await syncFileHandle(handle);
1071
+ }
1072
+ finally {
1073
+ await handle.close();
1074
+ }
1075
+ await fs.rename(tempPath, migrationPath);
1076
+ await syncDirectory(root);
1077
+ }
1078
+ async function workspaceMigrationIncludesAlias(root, aliasPath) {
1079
+ try {
1080
+ const state = JSON.parse(await fs.readFile(path.join(root, ORGANIZATION_WORKSPACE_MIGRATION_FILE), "utf8"));
1081
+ return state.version === 1
1082
+ && Array.isArray(state.compatibilityAliases)
1083
+ && state.compatibilityAliases.includes(path.resolve(aliasPath));
1084
+ }
1085
+ catch {
1086
+ return false;
1087
+ }
571
1088
  }
572
1089
  async function allocateOrganizationWorkspaceFolderName(input) {
573
1090
  const fallback = resolveOrganizationStorageKey(input.orgId);
574
1091
  const base = sanitizeOrganizationWorkspaceFolderName(input.orgName ?? input.orgUrlKey, fallback);
575
1092
  const usedFolders = new Set(input.map.organizations
576
1093
  .filter((entry) => entry.instanceId === resolveRudderInstanceId() && entry.orgId !== input.orgId)
577
- .map((entry) => entry.folderName));
1094
+ .map((entry) => entry.folderName.toLowerCase()));
578
1095
  for (let attempt = 1; attempt < 10000; attempt += 1) {
579
1096
  const folderName = attempt === 1 ? base : `${base}-${attempt}`;
580
- if (usedFolders.has(folderName))
1097
+ if (usedFolders.has(folderName.toLowerCase()))
581
1098
  continue;
582
1099
  const existing = await pathExists(path.resolve(input.homeDir, folderName));
583
1100
  if (existing && !existing.isDirectory())
584
1101
  continue;
585
- if (existing && input.allowExistingBaseDirectory && folderName === base)
1102
+ if (existing
1103
+ && folderName === base
1104
+ && await workspaceIdentityMatches(path.resolve(input.homeDir, folderName), input.orgId))
586
1105
  return folderName;
587
1106
  if (existing) {
588
1107
  const owned = input.map.organizations.some((entry) => entry.instanceId === resolveRudderInstanceId()
589
1108
  && entry.orgId === input.orgId
590
- && entry.folderName === folderName);
1109
+ && entry.folderName.toLowerCase() === folderName.toLowerCase());
591
1110
  if (!owned)
592
1111
  continue;
593
1112
  }
@@ -596,22 +1115,38 @@ async function allocateOrganizationWorkspaceFolderName(input) {
596
1115
  throw new Error("Unable to allocate organization workspace folder name");
597
1116
  }
598
1117
  export async function removeOrganizationStorage(orgId) {
599
- const normalizedOrgId = validatePathSegment(orgId, "org id");
600
- const organizationRootPath = resolveOrganizationRoot(normalizedOrgId);
601
- const legacyOrganizationRootPath = resolveLegacyOrganizationRoot(normalizedOrgId);
602
- const workspaceRootPath = resolveOrganizationWorkspaceRoot(normalizedOrgId);
603
- const legacyProjectsRootPath = path.resolve(resolveRudderInstanceRoot(), "projects", normalizedOrgId);
604
- const removeLegacyOrganizationRoot = legacyOrganizationRootPath === organizationRootPath
605
- ? []
606
- : [fs.rm(legacyOrganizationRootPath, { recursive: true, force: true })];
607
- await Promise.all([
608
- fs.rm(organizationRootPath, { recursive: true, force: true }),
609
- fs.rm(workspaceRootPath, { recursive: true, force: true }),
610
- ...removeLegacyOrganizationRoot,
611
- // Best-effort cleanup for legacy pre-org-workspace managed project paths.
612
- fs.rm(legacyProjectsRootPath, { recursive: true, force: true }),
613
- ]);
614
- return { organizationRootPath, legacyOrganizationRootPath, workspaceRootPath, legacyProjectsRootPath };
1118
+ return await updateOrganizationWorkspaceMap(async () => {
1119
+ const normalizedOrgId = validatePathSegment(orgId, "org id");
1120
+ const organizationRootPath = resolveOrganizationRoot(normalizedOrgId);
1121
+ const legacyOrganizationRootPath = resolveLegacyOrganizationRoot(normalizedOrgId);
1122
+ const workspaceRootPath = resolveOrganizationWorkspaceRoot(normalizedOrgId);
1123
+ const previousDocumentsWorkspaceRootPath = resolvePreviousDocumentsOrganizationWorkspaceRoot(normalizedOrgId);
1124
+ const legacyProjectsRootPath = path.resolve(resolveRudderInstanceRoot(), "projects", normalizedOrgId);
1125
+ const mapState = await readOrganizationWorkspaceMapFileState();
1126
+ await assertFriendlyWorkspaceRemovalOwnership(normalizedOrgId, workspaceRootPath, mapState.map);
1127
+ const pathsToRemove = new Set([
1128
+ organizationRootPath,
1129
+ workspaceRootPath,
1130
+ legacyOrganizationRootPath,
1131
+ previousDocumentsWorkspaceRootPath,
1132
+ legacyProjectsRootPath,
1133
+ ]);
1134
+ await Promise.all([...pathsToRemove].map((targetPath) => fs.rm(targetPath, { recursive: true, force: true })));
1135
+ const nextOrganizations = mapState.map.organizations.filter((entry) => !(entry.instanceId === resolveRudderInstanceId() && entry.orgId === normalizedOrgId));
1136
+ if (nextOrganizations.length !== mapState.map.organizations.length) {
1137
+ await writeOrganizationWorkspaceMapFile(resolveOrganizationWorkspaceMapPath(), {
1138
+ ...mapState.map,
1139
+ organizations: nextOrganizations,
1140
+ });
1141
+ }
1142
+ return {
1143
+ organizationRootPath,
1144
+ legacyOrganizationRootPath,
1145
+ workspaceRootPath,
1146
+ previousDocumentsWorkspaceRootPath,
1147
+ legacyProjectsRootPath,
1148
+ };
1149
+ });
615
1150
  }
616
1151
  async function listDirectoryNames(rootPath) {
617
1152
  try {
@@ -679,6 +1214,17 @@ function formatOrganizationWorkspacePermissionMessage(input) {
679
1214
  "Grant Rudder permission to access Documents, choose a writable folder with RUDDER_ORGANIZATION_WORKSPACE_HOME, or on Windows try running Rudder as administrator if the folder policy requires it.",
680
1215
  ].join(" ");
681
1216
  }
1217
+ async function assertOrganizationWorkspaceRootIsOwned(rootPath) {
1218
+ const stat = await lstatIfExists(rootPath);
1219
+ if (!stat)
1220
+ return;
1221
+ if (stat.isSymbolicLink()) {
1222
+ throw new Error("The organization workspace root cannot be a symbolic link.");
1223
+ }
1224
+ if (!stat.isDirectory()) {
1225
+ throw new Error("The organization workspace root must be a directory.");
1226
+ }
1227
+ }
682
1228
  async function lstatIfExists(targetPath) {
683
1229
  try {
684
1230
  return await fs.lstat(targetPath);
@@ -696,16 +1242,64 @@ async function movePath(sourcePath, targetPath) {
696
1242
  return;
697
1243
  }
698
1244
  catch (error) {
699
- if (errorCode(error) !== "EXDEV")
700
- throw error;
1245
+ if (errorCode(error) === "EXDEV") {
1246
+ throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
1247
+ }
1248
+ throw error;
1249
+ }
1250
+ }
1251
+ async function pathsReferenceSameDirectory(first, second) {
1252
+ try {
1253
+ const [firstRealPath, secondRealPath] = await Promise.all([fs.realpath(first), fs.realpath(second)]);
1254
+ return firstRealPath === secondRealPath;
1255
+ }
1256
+ catch {
1257
+ return false;
1258
+ }
1259
+ }
1260
+ async function createDirectoryCompatibilityAlias(aliasPath, targetPath) {
1261
+ await fs.symlink(targetPath, aliasPath, process.platform === "win32" ? "junction" : "dir");
1262
+ await syncDirectory(path.dirname(aliasPath));
1263
+ }
1264
+ function organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath) {
1265
+ return Object.assign(new Error(`Cannot atomically migrate organization workspace across filesystems from ${sourcePath} to ${targetPath}.`), { code: "RUDDER_WORKSPACE_CROSS_DEVICE_MIGRATION" });
1266
+ }
1267
+ async function assertWorkspaceRootMoveCanBeAtomic(sourcePath, targetPath) {
1268
+ await assertPathMoveCanBeAtomic(sourcePath, targetPath);
1269
+ }
1270
+ async function assertPathMoveCanBeAtomic(sourcePath, targetPath) {
1271
+ const sourceParentPath = path.dirname(sourcePath);
1272
+ const targetParentPath = path.dirname(targetPath);
1273
+ const [sourceParentStat, targetParentStat] = await Promise.all([
1274
+ fs.stat(sourceParentPath),
1275
+ fs.stat(targetParentPath),
1276
+ ]);
1277
+ const sourceVolumeRoot = path.parse(sourceParentPath).root;
1278
+ const targetVolumeRoot = path.parse(targetParentPath).root;
1279
+ const sameVolumeRoot = process.platform === "win32"
1280
+ ? sourceVolumeRoot.toLowerCase() === targetVolumeRoot.toLowerCase()
1281
+ : sourceVolumeRoot === targetVolumeRoot;
1282
+ if (sourceParentStat.dev !== targetParentStat.dev || !sameVolumeRoot) {
1283
+ throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
1284
+ }
1285
+ }
1286
+ async function moveWorkspacePathWithCompatibilityAlias(sourcePath, targetPath) {
1287
+ try {
1288
+ await fs.rename(sourcePath, targetPath);
1289
+ }
1290
+ catch (error) {
1291
+ if (errorCode(error) === "EXDEV") {
1292
+ throw organizationWorkspaceCrossDeviceMigrationError(sourcePath, targetPath);
1293
+ }
1294
+ throw error;
1295
+ }
1296
+ try {
1297
+ await createDirectoryCompatibilityAlias(sourcePath, targetPath);
1298
+ }
1299
+ catch (error) {
1300
+ await fs.rename(targetPath, sourcePath).catch(() => { });
1301
+ throw error;
701
1302
  }
702
- await fs.cp(sourcePath, targetPath, {
703
- recursive: true,
704
- errorOnExist: true,
705
- force: false,
706
- preserveTimestamps: true,
707
- });
708
- await fs.rm(sourcePath, { recursive: true, force: false });
709
1303
  }
710
1304
  async function regularFilesHaveIdenticalContents(sourcePath, targetPath) {
711
1305
  const sourceFile = await fs.open(sourcePath, "r");
@@ -745,8 +1339,8 @@ async function regularFilesHaveIdenticalContents(sourcePath, targetPath) {
745
1339
  await sourceFile.close();
746
1340
  }
747
1341
  }
748
- async function archiveRetainedMigrationSource(sourceRoot) {
749
- const backupHome = path.join(path.dirname(sourceRoot), MIGRATION_BACKUP_DIR_NAME);
1342
+ async function archiveRetainedMigrationSource(sourceRoot, backupParentPath = path.dirname(sourceRoot)) {
1343
+ const backupHome = path.join(backupParentPath, MIGRATION_BACKUP_DIR_NAME);
750
1344
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
751
1345
  const backupPath = path.join(backupHome, `${path.basename(sourceRoot)}-${timestamp}-${randomUUID()}`);
752
1346
  await fs.mkdir(backupHome, { recursive: true });
@@ -758,10 +1352,13 @@ async function mergeDirectoryContents(sourceRoot, targetRoot) {
758
1352
  const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
759
1353
  let retainedDuplicates = false;
760
1354
  for (const entry of entries) {
1355
+ if (entry.name === ORGANIZATION_WORKSPACE_MIGRATION_FILE)
1356
+ continue;
761
1357
  const sourcePath = path.join(sourceRoot, entry.name);
762
1358
  const targetPath = path.join(targetRoot, entry.name);
763
1359
  const targetStat = await lstatIfExists(targetPath);
764
1360
  if (!targetStat) {
1361
+ await assertPathMoveCanBeAtomic(sourcePath, targetPath);
765
1362
  await movePath(sourcePath, targetPath);
766
1363
  continue;
767
1364
  }
@@ -787,11 +1384,15 @@ async function mergeDirectoryContents(sourceRoot, targetRoot) {
787
1384
  async function assertCanMergeDirectoryContents(sourceRoot, targetRoot) {
788
1385
  const entries = await fs.readdir(sourceRoot, { withFileTypes: true });
789
1386
  for (const entry of entries) {
1387
+ if (entry.name === ORGANIZATION_WORKSPACE_MIGRATION_FILE)
1388
+ continue;
790
1389
  const sourcePath = path.join(sourceRoot, entry.name);
791
1390
  const targetPath = path.join(targetRoot, entry.name);
792
1391
  const targetStat = await lstatIfExists(targetPath);
793
- if (!targetStat)
1392
+ if (!targetStat) {
1393
+ await assertPathMoveCanBeAtomic(sourcePath, targetPath);
794
1394
  continue;
1395
+ }
795
1396
  if (entry.isDirectory() && targetStat.isDirectory()) {
796
1397
  await assertCanMergeDirectoryContents(sourcePath, targetPath);
797
1398
  continue;
@@ -814,7 +1415,9 @@ export async function pruneOrphanedOrganizationStorage(liveOrgIds) {
814
1415
  const organizationDirNames = await listDirectoryNames(organizationRoot);
815
1416
  const legacyProjectDirNames = await listDirectoryNames(legacyProjectsRoot);
816
1417
  const legacyProjectsRootExists = await directoryExists(legacyProjectsRoot);
817
- const removedOrganizationDirNames = organizationDirNames.filter((dirName) => dirName !== MIGRATION_BACKUP_DIR_NAME && !liveOrgIdSet.has(dirName));
1418
+ const removedOrganizationDirNames = organizationDirNames.filter((dirName) => dirName !== MIGRATION_BACKUP_DIR_NAME
1419
+ && !isOrganizationWorkspaceMapInternalDirectoryName(dirName)
1420
+ && !liveOrgIdSet.has(dirName));
818
1421
  const removedLegacyProjectDirNames = legacyProjectDirNames;
819
1422
  await Promise.all([
820
1423
  ...removedOrganizationDirNames.map((dirName) => fs.rm(path.resolve(organizationRoot, dirName), { recursive: true, force: true })),