@ricsam/r5d-worker 0.0.137 → 0.0.139

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 (236) hide show
  1. package/README.md +4 -94
  2. package/dist/cjs/package.json +1 -1
  3. package/dist/mjs/main.mjs +46 -7543
  4. package/dist/mjs/package.json +1 -1
  5. package/dist/mjs/personal/action-scheduler.mjs +31 -0
  6. package/dist/mjs/personal/client.mjs +215 -0
  7. package/dist/mjs/personal/runtime.mjs +359 -0
  8. package/dist/mjs/personal/tcp.mjs +217 -0
  9. package/dist/mjs/runtime/adapter.mjs +42 -0
  10. package/dist/mjs/runtime/client.mjs +95 -0
  11. package/dist/mjs/runtime/daemon.mjs +131 -0
  12. package/dist/mjs/runtime/executor.mjs +436 -0
  13. package/dist/mjs/runtime/index.mjs +15 -0
  14. package/dist/mjs/runtime/protocol.mjs +139 -0
  15. package/dist/mjs/runtime/pty-host.mjs +156 -0
  16. package/dist/mjs/runtime/pty.mjs +127 -0
  17. package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
  18. package/dist/mjs/runtime/releases/grants.mjs +93 -0
  19. package/dist/mjs/runtime/releases/main.mjs +101 -0
  20. package/dist/mjs/runtime/releases/manager.mjs +541 -0
  21. package/dist/mjs/runtime/releases/process.mjs +88 -0
  22. package/dist/mjs/runtime/releases/retirement.mjs +18 -0
  23. package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
  24. package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
  25. package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
  26. package/dist/mjs/runtime/storage.mjs +156 -0
  27. package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
  28. package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
  29. package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
  30. package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
  31. package/dist/mjs/runtime/workspace/files.mjs +308 -0
  32. package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
  33. package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
  34. package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
  35. package/dist/types/main.d.ts +0 -1167
  36. package/dist/types/personal/action-scheduler.d.ts +23 -0
  37. package/dist/types/personal/client.d.ts +14 -0
  38. package/dist/types/personal/runtime.d.ts +121 -0
  39. package/dist/types/personal/tcp.d.ts +14 -0
  40. package/dist/types/runtime/client.d.ts +3 -1
  41. package/dist/types/runtime/executor.d.ts +6 -1
  42. package/dist/types/runtime/protocol.d.ts +176 -4
  43. package/dist/types/runtime/pty.d.ts +1 -0
  44. package/dist/types/runtime/releases/artifacts.d.ts +51 -0
  45. package/dist/types/runtime/releases/grants.d.ts +16 -0
  46. package/dist/types/runtime/releases/manager.d.ts +145 -0
  47. package/dist/types/runtime/releases/process.d.ts +14 -0
  48. package/dist/types/runtime/releases/retirement.d.ts +15 -0
  49. package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
  50. package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
  51. package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
  52. package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
  53. package/dist/types/runtime/workspace/authority.d.ts +338 -0
  54. package/dist/types/runtime/workspace/contracts.d.ts +86 -0
  55. package/dist/types/runtime/workspace/file-write.d.ts +14 -0
  56. package/dist/types/runtime/workspace/files.d.ts +21 -0
  57. package/dist/types/runtime/workspace/http-body.d.ts +9 -0
  58. package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
  59. package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
  60. package/package.json +8 -6
  61. package/dist/cjs/atomic-rename.cjs +0 -303
  62. package/dist/cjs/browser-upload.cjs +0 -73
  63. package/dist/cjs/cli-update.cjs +0 -108
  64. package/dist/cjs/command-launcher.cjs +0 -423
  65. package/dist/cjs/control-command-policy.cjs +0 -177
  66. package/dist/cjs/git-blob-hash.cjs +0 -41
  67. package/dist/cjs/git-identity.cjs +0 -39
  68. package/dist/cjs/git-process-environment.cjs +0 -133
  69. package/dist/cjs/inline-output.cjs +0 -62
  70. package/dist/cjs/main.cjs +0 -7517
  71. package/dist/cjs/managed-paths.cjs +0 -215
  72. package/dist/cjs/plan-lint.cjs +0 -165
  73. package/dist/cjs/plan-parser.cjs +0 -127
  74. package/dist/cjs/plan-paths.cjs +0 -53
  75. package/dist/cjs/port-forward-client.cjs +0 -206
  76. package/dist/cjs/process-tree.cjs +0 -117
  77. package/dist/cjs/project-checkout-garbage.cjs +0 -219
  78. package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
  79. package/dist/cjs/project-mirror-refs-token.cjs +0 -85
  80. package/dist/cjs/project-workspace-state.cjs +0 -972
  81. package/dist/cjs/project-worktrees.cjs +0 -1713
  82. package/dist/cjs/pty-output-coalescer.cjs +0 -87
  83. package/dist/cjs/recovery-journal-protocol.cjs +0 -56
  84. package/dist/cjs/recovery-journal-runtime.cjs +0 -133
  85. package/dist/cjs/recovery-journal-thread.cjs +0 -9
  86. package/dist/cjs/recovery-journal.cjs +0 -742
  87. package/dist/cjs/recovery-output-window.cjs +0 -77
  88. package/dist/cjs/recovery-protocol.cjs +0 -31
  89. package/dist/cjs/recovery-store.cjs +0 -252
  90. package/dist/cjs/registry-auth.cjs +0 -310
  91. package/dist/cjs/repository-transition-policy.cjs +0 -49
  92. package/dist/cjs/runtime-version.cjs +0 -88
  93. package/dist/cjs/session-file-mutations.cjs +0 -61
  94. package/dist/cjs/supervisor.cjs +0 -147
  95. package/dist/cjs/three-way-merge.cjs +0 -346
  96. package/dist/cjs/working-tree-mirror.cjs +0 -1872
  97. package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
  98. package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
  99. package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
  100. package/dist/cjs/workspace-command-targets.cjs +0 -63
  101. package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
  102. package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
  103. package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
  104. package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
  105. package/dist/cjs/workspace-git-sync.cjs +0 -3595
  106. package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
  107. package/dist/cjs/workspace-hydration-merge.cjs +0 -448
  108. package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
  109. package/dist/cjs/workspace-incident-state.cjs +0 -37
  110. package/dist/cjs/workspace-merge-projection.cjs +0 -628
  111. package/dist/cjs/workspace-mount-boundary.cjs +0 -71
  112. package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
  113. package/dist/cjs/workspace-mutation-gate.cjs +0 -127
  114. package/dist/cjs/workspace-path-move.cjs +0 -195
  115. package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
  116. package/dist/cjs/workspace-project-config-policy.cjs +0 -53
  117. package/dist/cjs/workspace-projection-ledger.cjs +0 -81
  118. package/dist/cjs/workspace-publication-evidence.cjs +0 -46
  119. package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
  120. package/dist/mjs/atomic-rename.mjs +0 -261
  121. package/dist/mjs/browser-upload.mjs +0 -42
  122. package/dist/mjs/command-launcher.mjs +0 -393
  123. package/dist/mjs/control-command-policy.mjs +0 -146
  124. package/dist/mjs/git-blob-hash.mjs +0 -16
  125. package/dist/mjs/git-identity.mjs +0 -15
  126. package/dist/mjs/git-process-environment.mjs +0 -105
  127. package/dist/mjs/inline-output.mjs +0 -37
  128. package/dist/mjs/managed-paths.mjs +0 -174
  129. package/dist/mjs/plan-lint.mjs +0 -129
  130. package/dist/mjs/plan-parser.mjs +0 -94
  131. package/dist/mjs/plan-paths.mjs +0 -25
  132. package/dist/mjs/port-forward-client.mjs +0 -172
  133. package/dist/mjs/project-checkout-garbage.mjs +0 -178
  134. package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
  135. package/dist/mjs/project-mirror-refs-token.mjs +0 -53
  136. package/dist/mjs/project-workspace-state.mjs +0 -937
  137. package/dist/mjs/project-worktrees.mjs +0 -1657
  138. package/dist/mjs/pty-output-coalescer.mjs +0 -61
  139. package/dist/mjs/recovery-journal-protocol.mjs +0 -30
  140. package/dist/mjs/recovery-journal-runtime.mjs +0 -112
  141. package/dist/mjs/recovery-journal-thread.mjs +0 -8
  142. package/dist/mjs/recovery-journal.mjs +0 -694
  143. package/dist/mjs/recovery-output-window.mjs +0 -53
  144. package/dist/mjs/recovery-protocol.mjs +0 -6
  145. package/dist/mjs/recovery-store.mjs +0 -217
  146. package/dist/mjs/registry-auth.mjs +0 -269
  147. package/dist/mjs/repository-transition-policy.mjs +0 -22
  148. package/dist/mjs/runtime-version.mjs +0 -54
  149. package/dist/mjs/session-file-mutations.mjs +0 -37
  150. package/dist/mjs/supervisor.mjs +0 -114
  151. package/dist/mjs/three-way-merge.mjs +0 -318
  152. package/dist/mjs/working-tree-mirror.mjs +0 -1817
  153. package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
  154. package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
  155. package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
  156. package/dist/mjs/workspace-command-targets.mjs +0 -37
  157. package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
  158. package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
  159. package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
  160. package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
  161. package/dist/mjs/workspace-git-sync.mjs +0 -3553
  162. package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
  163. package/dist/mjs/workspace-hydration-merge.mjs +0 -414
  164. package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
  165. package/dist/mjs/workspace-incident-state.mjs +0 -12
  166. package/dist/mjs/workspace-merge-projection.mjs +0 -594
  167. package/dist/mjs/workspace-mount-boundary.mjs +0 -37
  168. package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
  169. package/dist/mjs/workspace-mutation-gate.mjs +0 -103
  170. package/dist/mjs/workspace-path-move.mjs +0 -160
  171. package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
  172. package/dist/mjs/workspace-project-config-policy.mjs +0 -27
  173. package/dist/mjs/workspace-projection-ledger.mjs +0 -57
  174. package/dist/mjs/workspace-publication-evidence.mjs +0 -21
  175. package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
  176. package/dist/types/atomic-rename.d.ts +0 -78
  177. package/dist/types/browser-upload.d.ts +0 -7
  178. package/dist/types/command-launcher.d.ts +0 -104
  179. package/dist/types/control-command-policy.d.ts +0 -53
  180. package/dist/types/git-blob-hash.d.ts +0 -10
  181. package/dist/types/git-identity.d.ts +0 -9
  182. package/dist/types/git-process-environment.d.ts +0 -9
  183. package/dist/types/inline-output.d.ts +0 -3
  184. package/dist/types/managed-paths.d.ts +0 -19
  185. package/dist/types/plan-lint.d.ts +0 -16
  186. package/dist/types/plan-parser.d.ts +0 -38
  187. package/dist/types/plan-paths.d.ts +0 -36
  188. package/dist/types/port-forward-client.d.ts +0 -8
  189. package/dist/types/project-checkout-garbage.d.ts +0 -86
  190. package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
  191. package/dist/types/project-mirror-refs-token.d.ts +0 -53
  192. package/dist/types/project-workspace-state.d.ts +0 -188
  193. package/dist/types/project-worktrees.d.ts +0 -397
  194. package/dist/types/pty-output-coalescer.d.ts +0 -20
  195. package/dist/types/recovery-journal-protocol.d.ts +0 -35
  196. package/dist/types/recovery-journal-runtime.d.ts +0 -12
  197. package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
  198. package/dist/types/recovery-journal-thread.d.ts +0 -1
  199. package/dist/types/recovery-journal.d.ts +0 -248
  200. package/dist/types/recovery-output-window.d.ts +0 -19
  201. package/dist/types/recovery-protocol.d.ts +0 -59
  202. package/dist/types/recovery-store.d.ts +0 -52
  203. package/dist/types/registry-auth.d.ts +0 -47
  204. package/dist/types/repository-transition-policy.d.ts +0 -26
  205. package/dist/types/runtime-version.d.ts +0 -1
  206. package/dist/types/session-file-mutations.d.ts +0 -22
  207. package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
  208. package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
  209. package/dist/types/supervisor-signal-fixture.d.ts +0 -1
  210. package/dist/types/supervisor.d.ts +0 -23
  211. package/dist/types/three-way-merge.d.ts +0 -77
  212. package/dist/types/working-tree-mirror.d.ts +0 -424
  213. package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
  214. package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
  215. package/dist/types/workspace-command-sync-policy.d.ts +0 -57
  216. package/dist/types/workspace-command-targets.d.ts +0 -37
  217. package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
  218. package/dist/types/workspace-filesystem-executor.d.ts +0 -123
  219. package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
  220. package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
  221. package/dist/types/workspace-git-sync.d.ts +0 -514
  222. package/dist/types/workspace-hydration-ledger.d.ts +0 -76
  223. package/dist/types/workspace-hydration-merge.d.ts +0 -103
  224. package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
  225. package/dist/types/workspace-incident-state.d.ts +0 -19
  226. package/dist/types/workspace-merge-projection.d.ts +0 -69
  227. package/dist/types/workspace-mount-boundary.d.ts +0 -10
  228. package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
  229. package/dist/types/workspace-mutation-gate.d.ts +0 -42
  230. package/dist/types/workspace-path-move.d.ts +0 -21
  231. package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
  232. package/dist/types/workspace-project-config-policy.d.ts +0 -48
  233. package/dist/types/workspace-projection-ledger.d.ts +0 -59
  234. package/dist/types/workspace-publication-evidence.d.ts +0 -17
  235. package/dist/types/workspace-sync-coalescer.d.ts +0 -28
  236. /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
@@ -1,937 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import {
5
- managedBranchPath,
6
- managedProjectRoot,
7
- validateCheckoutPathSegments,
8
- validateManagedBranchName
9
- } from "./managed-paths.mjs";
10
- const PROJECT_WORKSPACE_STATE_FILE = "project-workspace-state.json";
11
- const PROJECT_WORKSPACE_STATE_VERSION = 1;
12
- const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
- const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
14
- class ProjectWorkspacePendingBranchPathChangeError extends Error {
15
- projectId;
16
- branchName;
17
- currentCheckoutPathSegments;
18
- incomingCheckoutPathSegments;
19
- constructor(input) {
20
- super(`Cannot move project ${input.projectId} while creator-local branch ${input.branchName} is pending publication`);
21
- this.name = "ProjectWorkspacePendingBranchPathChangeError";
22
- this.projectId = input.projectId;
23
- this.branchName = input.branchName;
24
- this.currentCheckoutPathSegments = [...input.currentCheckoutPathSegments];
25
- this.incomingCheckoutPathSegments = [...input.incomingCheckoutPathSegments];
26
- }
27
- }
28
- class ProjectWorkspaceCheckoutPathCollisionError extends Error {
29
- projectId;
30
- occupyingProjectId;
31
- checkoutPathSegments;
32
- constructor(input) {
33
- super(
34
- `Cannot assign checkout path ${input.checkoutPathSegments.join("/")} to project ${input.projectId} while project ${input.occupyingProjectId} still owns a durable checkout or deletion tombstone there`
35
- );
36
- this.name = "ProjectWorkspaceCheckoutPathCollisionError";
37
- this.projectId = input.projectId;
38
- this.occupyingProjectId = input.occupyingProjectId;
39
- this.checkoutPathSegments = [...input.checkoutPathSegments];
40
- }
41
- }
42
- const EMPTY_STATE = {
43
- version: PROJECT_WORKSPACE_STATE_VERSION,
44
- desiredProjects: [],
45
- locallyPendingCreatedBranches: [],
46
- enabledRepositoryTransitions: [],
47
- tombstones: []
48
- };
49
- function isRecord(value) {
50
- return typeof value === "object" && value !== null && !Array.isArray(value);
51
- }
52
- function validateProjectId(projectId) {
53
- if (typeof projectId !== "string" || projectId !== "local" && projectId !== "local-code" && !PROJECT_ID_PATTERN.test(projectId)) {
54
- throw new Error(`Invalid project workspace state project id: ${String(projectId)}`);
55
- }
56
- return projectId;
57
- }
58
- function comparePathSegments(left, right) {
59
- return left[0].localeCompare(right[0]) || left[1].localeCompare(right[1]);
60
- }
61
- function samePathSegments(left, right) {
62
- return left[0] === right[0] && left[1] === right[1];
63
- }
64
- function pathsOverlap(left, right) {
65
- const relative = path.relative(path.resolve(left), path.resolve(right));
66
- const reverse = path.relative(path.resolve(right), path.resolve(left));
67
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) || reverse !== ".." && !reverse.startsWith(`..${path.sep}`) && !path.isAbsolute(reverse);
68
- }
69
- function normalizeBranchNames(branches, label) {
70
- const branchNames = branches.map(({ branchName }) => {
71
- validateManagedBranchName(branchName);
72
- return branchName;
73
- });
74
- if (new Set(branchNames).size !== branchNames.length) throw new Error(`${label} contains duplicate branches`);
75
- const sorted = [...branchNames].sort();
76
- for (const [index, branchName] of sorted.entries()) {
77
- const overlapping = sorted.slice(index + 1).find((candidate) => candidate.startsWith(`${branchName}/`));
78
- if (overlapping) throw new Error(`${label} contains overlapping branch paths ${branchName} and ${overlapping}`);
79
- }
80
- return sorted;
81
- }
82
- function normalizeDesiredProjects(projects) {
83
- const normalized = projects.map((project) => {
84
- const projectId = validateProjectId(project.projectId);
85
- const checkoutPathSegments = validateCheckoutPathSegments(project.checkoutPathSegments);
86
- validateManagedBranchName(project.defaultBranch);
87
- const branchNames = normalizeBranchNames(project.branches, `Project ${projectId}`);
88
- return {
89
- projectId,
90
- checkoutPathSegments,
91
- defaultBranch: project.defaultBranch,
92
- branches: branchNames.map((branchName) => ({ branchName }))
93
- };
94
- });
95
- if (new Set(normalized.map(({ projectId }) => projectId)).size !== normalized.length) {
96
- throw new Error("Project workspace state contains duplicate project ids");
97
- }
98
- const checkoutPaths = /* @__PURE__ */ new Map();
99
- for (const project of normalized) {
100
- const checkoutPath = project.checkoutPathSegments.join("/");
101
- const existing = checkoutPaths.get(checkoutPath);
102
- if (existing) throw new Error(`Projects ${existing} and ${project.projectId} share managed checkout path ${checkoutPath}`);
103
- checkoutPaths.set(checkoutPath, project.projectId);
104
- }
105
- return normalized.sort(
106
- (left, right) => left.projectId.localeCompare(right.projectId) || comparePathSegments(left.checkoutPathSegments, right.checkoutPathSegments)
107
- );
108
- }
109
- function normalizePendingCreatedBranches(value) {
110
- if (!Array.isArray(value)) throw new Error("Invalid durable locally pending project branches");
111
- const pending = value.map((entry) => {
112
- if (!isRecord(entry) || typeof entry.branchName !== "string") {
113
- throw new Error("Invalid durable locally pending project branch");
114
- }
115
- const projectId = validateProjectId(entry.projectId);
116
- validateManagedBranchName(entry.branchName);
117
- if (entry.branchId !== void 0 && (typeof entry.branchId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(entry.branchId))) {
118
- throw new Error("Invalid durable locally pending project branch id");
119
- }
120
- return { ...entry.branchId ? { branchId: entry.branchId } : {}, projectId, branchName: entry.branchName };
121
- });
122
- const keys = pending.map(({ projectId, branchName }) => `${projectId}\0${branchName}`);
123
- if (new Set(keys).size !== keys.length) throw new Error("Duplicate durable locally pending project branch");
124
- return pending.sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
125
- }
126
- function normalizePreserveOnlyBranches(value) {
127
- const normalized = normalizePendingCreatedBranches(value);
128
- if (normalized.some(({ branchId }) => !branchId)) throw new Error("Preserve-only branch is missing its durable branch id");
129
- return normalized;
130
- }
131
- function normalizeEnabledRepositoryTransitions(value) {
132
- if (value === void 0) return [];
133
- if (!Array.isArray(value)) throw new Error("Invalid durable enabled repository transitions");
134
- const transitions = value.map((entry) => {
135
- if (!isRecord(entry)) throw new Error("Invalid durable enabled repository transition");
136
- const projectId = validateProjectId(entry.projectId);
137
- if (entry.transitionId !== null && (typeof entry.transitionId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(entry.transitionId))) {
138
- throw new Error("Invalid durable enabled repository transition id");
139
- }
140
- return { projectId, transitionId: entry.transitionId };
141
- });
142
- if (new Set(transitions.map(({ projectId }) => projectId)).size !== transitions.length) {
143
- throw new Error("Duplicate durable enabled repository transition project");
144
- }
145
- return transitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
146
- }
147
- function tombstoneBranchNames(tombstone) {
148
- return tombstone.kind === "branch" ? [tombstone.branchName] : tombstone.branchNames;
149
- }
150
- function deletionId(input) {
151
- const digest = createHash("sha256").update(
152
- JSON.stringify([input.kind, input.projectId, input.checkoutPathSegments[0], input.checkoutPathSegments[1], input.branchName ?? null])
153
- ).digest("hex");
154
- return `${input.kind}:${digest}`;
155
- }
156
- function normalizeDeletedMirrorRefs(value, branchNames, label) {
157
- if (!Array.isArray(value) || value.some((branchName) => typeof branchName !== "string")) {
158
- throw new Error(`${label} has invalid deleted mirror refs`);
159
- }
160
- const normalized = [...new Set(value)].sort();
161
- if (normalized.some((branchName) => !branchNames.includes(branchName))) {
162
- throw new Error(`${label} records a mirror ref outside its branch set`);
163
- }
164
- return normalized;
165
- }
166
- function normalizeTreePublishedHead(value, label) {
167
- if (value === null) return null;
168
- if (typeof value !== "string" || !GIT_OBJECT_ID_PATTERN.test(value)) {
169
- throw new Error(`${label} has an invalid published workspace head`);
170
- }
171
- return value;
172
- }
173
- function normalizeTombstone(value) {
174
- if (!isRecord(value)) throw new Error("Invalid project workspace deletion tombstone");
175
- const projectId = validateProjectId(value.projectId);
176
- const checkoutPathSegments = validateCheckoutPathSegments(value.checkoutPathSegments);
177
- if (value.kind === "branch") {
178
- if (typeof value.branchName !== "string") throw new Error("Invalid project workspace branch deletion tombstone");
179
- validateManagedBranchName(value.branchName);
180
- const expectedId2 = deletionId({ kind: "branch", projectId, checkoutPathSegments, branchName: value.branchName });
181
- if (value.id !== expectedId2) throw new Error("Project workspace branch deletion tombstone id does not match its target");
182
- return {
183
- id: expectedId2,
184
- kind: "branch",
185
- projectId,
186
- checkoutPathSegments,
187
- branchName: value.branchName,
188
- treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId2}`),
189
- mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, [value.branchName], `Deletion ${expectedId2}`)
190
- };
191
- }
192
- if (value.kind !== "project" || !Array.isArray(value.branchNames)) {
193
- throw new Error("Invalid project workspace project deletion tombstone");
194
- }
195
- const branchNames = normalizeBranchNames(
196
- value.branchNames.map((branchName) => ({ branchName: String(branchName) })),
197
- `Deletion ${String(value.id)}`
198
- );
199
- const expectedId = deletionId({ kind: "project", projectId, checkoutPathSegments });
200
- if (value.id !== expectedId) throw new Error("Project workspace project deletion tombstone id does not match its target");
201
- return {
202
- id: expectedId,
203
- kind: "project",
204
- projectId,
205
- checkoutPathSegments,
206
- branchNames,
207
- treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId}`),
208
- mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, branchNames, `Deletion ${expectedId}`)
209
- };
210
- }
211
- function normalizeState(value) {
212
- if (!isRecord(value) || value.version !== PROJECT_WORKSPACE_STATE_VERSION) {
213
- throw new Error("Unsupported or invalid project workspace state file");
214
- }
215
- if (!Array.isArray(value.desiredProjects) || !Array.isArray(value.locallyPendingCreatedBranches) || !Array.isArray(value.tombstones)) {
216
- throw new Error("Invalid project workspace state file");
217
- }
218
- const desiredProjects = normalizeDesiredProjects(value.desiredProjects);
219
- const locallyPendingCreatedBranches = normalizePendingCreatedBranches(value.locallyPendingCreatedBranches);
220
- const enabledRepositoryTransitions = normalizeEnabledRepositoryTransitions(value.enabledRepositoryTransitions);
221
- const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
222
- for (const pending of locallyPendingCreatedBranches) {
223
- const project = desiredById.get(pending.projectId);
224
- if (!project || !project.branches.some(({ branchName }) => branchName === pending.branchName)) {
225
- throw new Error(`Durable pending branch ${pending.projectId}/${pending.branchName} is missing from desired project state`);
226
- }
227
- }
228
- const tombstones = value.tombstones.map(normalizeTombstone);
229
- if (new Set(tombstones.map(({ id }) => id)).size !== tombstones.length) {
230
- throw new Error("Project workspace state contains duplicate deletion tombstones");
231
- }
232
- return {
233
- version: PROJECT_WORKSPACE_STATE_VERSION,
234
- desiredProjects,
235
- locallyPendingCreatedBranches,
236
- enabledRepositoryTransitions,
237
- tombstones: tombstones.sort((left, right) => left.id.localeCompare(right.id))
238
- };
239
- }
240
- function cloneDesiredProjects(projects) {
241
- return projects.map((project) => ({
242
- ...project,
243
- checkoutPathSegments: [...project.checkoutPathSegments],
244
- branches: project.branches.map((branch) => ({ ...branch }))
245
- }));
246
- }
247
- function cloneTombstones(tombstones) {
248
- return tombstones.map((tombstone) => ({
249
- ...tombstone,
250
- checkoutPathSegments: [...tombstone.checkoutPathSegments],
251
- mirrorRefsDeleted: [...tombstone.mirrorRefsDeleted],
252
- ...tombstone.kind === "project" ? { branchNames: [...tombstone.branchNames] } : {}
253
- }));
254
- }
255
- function readState(statePath) {
256
- try {
257
- return normalizeState(JSON.parse(fs.readFileSync(statePath, "utf8")));
258
- } catch (error) {
259
- if (error.code === "ENOENT") return normalizeState(EMPTY_STATE);
260
- throw new Error(
261
- `Could not read durable project workspace state at ${statePath}: ${error instanceof Error ? error.message : String(error)}`,
262
- { cause: error }
263
- );
264
- }
265
- }
266
- function fsyncDirectory(directory) {
267
- let descriptor;
268
- try {
269
- descriptor = fs.openSync(directory, "r");
270
- fs.fsyncSync(descriptor);
271
- } finally {
272
- if (descriptor !== void 0) fs.closeSync(descriptor);
273
- }
274
- }
275
- function lstatIfExists(targetPath) {
276
- try {
277
- return fs.lstatSync(targetPath);
278
- } catch (error) {
279
- if (error.code === "ENOENT") return null;
280
- throw error;
281
- }
282
- }
283
- async function fsyncDirectoryAsync(directory) {
284
- const handle = await fs.promises.open(directory, "r");
285
- try {
286
- await handle.sync();
287
- } finally {
288
- await handle.close();
289
- }
290
- }
291
- async function lstatIfExistsAsync(targetPath) {
292
- try {
293
- return await fs.promises.lstat(targetPath);
294
- } catch (error) {
295
- if (error.code === "ENOENT") return null;
296
- throw error;
297
- }
298
- }
299
- async function ensureDurableStateRoot(stateRoot) {
300
- const resolvedStateRoot = path.resolve(stateRoot);
301
- let existingAncestor = resolvedStateRoot;
302
- while (!await lstatIfExistsAsync(existingAncestor)) {
303
- const parent = path.dirname(existingAncestor);
304
- if (parent === existingAncestor) throw new Error(`Project workspace state has no existing ancestor: ${resolvedStateRoot}`);
305
- existingAncestor = parent;
306
- }
307
- const ancestorStat = await lstatIfExistsAsync(existingAncestor);
308
- if (!ancestorStat || ancestorStat.isSymbolicLink() || !ancestorStat.isDirectory()) {
309
- throw new Error(`Project workspace state must descend from a real directory: ${resolvedStateRoot}`);
310
- }
311
- await fs.promises.mkdir(resolvedStateRoot, { recursive: true, mode: 448 });
312
- let current = resolvedStateRoot;
313
- while (true) {
314
- const stat = await lstatIfExistsAsync(current);
315
- if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
316
- throw new Error(`Project workspace state path must be a real directory: ${current}`);
317
- }
318
- await fsyncDirectoryAsync(current);
319
- if (current === existingAncestor) break;
320
- current = path.dirname(current);
321
- }
322
- }
323
- async function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
324
- await ensureDurableStateRoot(stateRoot);
325
- afterStateRootFsync?.();
326
- const temporaryPath = path.join(stateRoot, `.${PROJECT_WORKSPACE_STATE_FILE}.${process.pid}.${randomUUID()}.tmp`);
327
- let handle;
328
- try {
329
- handle = await fs.promises.open(temporaryPath, "wx", 384);
330
- await handle.writeFile(`${JSON.stringify(normalizeState(state), null, 2)}
331
- `, "utf8");
332
- await handle.sync();
333
- await handle.close();
334
- handle = void 0;
335
- await fs.promises.rename(temporaryPath, statePath);
336
- await fsyncDirectoryAsync(stateRoot);
337
- } finally {
338
- if (handle !== void 0) await handle.close();
339
- await fs.promises.rm(temporaryPath, { force: true });
340
- }
341
- }
342
- function createBranchTombstone(project, branchName) {
343
- return {
344
- id: deletionId({ kind: "branch", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments, branchName }),
345
- kind: "branch",
346
- projectId: project.projectId,
347
- checkoutPathSegments: [...project.checkoutPathSegments],
348
- branchName,
349
- treePublishedHead: null,
350
- mirrorRefsDeleted: []
351
- };
352
- }
353
- function createProjectTombstone(project) {
354
- return {
355
- id: deletionId({ kind: "project", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments }),
356
- kind: "project",
357
- projectId: project.projectId,
358
- checkoutPathSegments: [...project.checkoutPathSegments],
359
- branchNames: project.branches.map(({ branchName }) => branchName).sort(),
360
- treePublishedHead: null,
361
- mirrorRefsDeleted: []
362
- };
363
- }
364
- function addBranchTombstone(tombstones, project, branchName) {
365
- const projectTombstone = tombstones.find(
366
- (tombstone) => tombstone.kind === "project" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
367
- );
368
- if (projectTombstone) {
369
- if (!projectTombstone.branchNames.includes(branchName)) {
370
- projectTombstone.branchNames.push(branchName);
371
- projectTombstone.branchNames.sort();
372
- projectTombstone.treePublishedHead = null;
373
- }
374
- return;
375
- }
376
- const created = createBranchTombstone(project, branchName);
377
- if (!tombstones.some(({ id }) => id === created.id)) tombstones.push(created);
378
- }
379
- function addProjectTombstone(tombstones, project) {
380
- const created = createProjectTombstone(project);
381
- const existing = tombstones.find(({ id }) => id === created.id);
382
- if (existing) return;
383
- const supersededBranches = tombstones.filter(
384
- (tombstone) => tombstone.kind === "branch" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
385
- );
386
- const alreadyDeleted = new Set(supersededBranches.flatMap(({ mirrorRefsDeleted }) => mirrorRefsDeleted));
387
- created.mirrorRefsDeleted = created.branchNames.filter((branchName) => alreadyDeleted.has(branchName));
388
- tombstones.splice(
389
- 0,
390
- tombstones.length,
391
- ...tombstones.filter((tombstone) => !supersededBranches.some(({ id }) => id === tombstone.id)),
392
- created
393
- );
394
- }
395
- function applyPendingCreatedBranches(desiredProjects, pendingCreatedBranches) {
396
- const seen = /* @__PURE__ */ new Set();
397
- for (const pending of pendingCreatedBranches) {
398
- const projectId = validateProjectId(pending.projectId);
399
- validateManagedBranchName(pending.branchName);
400
- const key = `${projectId}\0${pending.branchName}`;
401
- if (seen.has(key)) continue;
402
- seen.add(key);
403
- const project = desiredProjects.find((candidate) => candidate.projectId === projectId);
404
- if (!project) throw new Error(`Pending local branch ${projectId}/${pending.branchName} has no desired project`);
405
- if (!project.branches.some(({ branchName }) => branchName === pending.branchName)) {
406
- project.branches.push({ branchName: pending.branchName });
407
- project.branches = normalizeBranchNames(project.branches, `Project ${projectId}`).map((branchName) => ({ branchName }));
408
- }
409
- }
410
- }
411
- function pendingCreatedBranchKey(input) {
412
- return `${input.projectId}\0${input.branchName}`;
413
- }
414
- function pruneAuthoritativelyDesiredBranchDeletions(pendingDeletions, desiredProjects) {
415
- const desiredKeys = new Set(
416
- desiredProjects.flatMap(
417
- (project) => project.branches.map(({ branchName }) => pendingCreatedBranchKey({ projectId: project.projectId, branchName }))
418
- )
419
- );
420
- for (const [key, deletion] of pendingDeletions) {
421
- if (key === pendingCreatedBranchKey(deletion) && desiredKeys.has(key)) pendingDeletions.delete(key);
422
- }
423
- }
424
- function desiredProjectHasBranch(desiredProjects, pending) {
425
- return Boolean(
426
- desiredProjects.find(({ projectId }) => projectId === pending.projectId)?.branches.some(({ branchName }) => branchName === pending.branchName)
427
- );
428
- }
429
- function mergePendingCreatedBranches(...groups) {
430
- const byKey = /* @__PURE__ */ new Map();
431
- for (const pending of groups.flat()) {
432
- const normalized = normalizePendingCreatedBranches([pending])[0];
433
- const key = pendingCreatedBranchKey(normalized);
434
- const existing = byKey.get(key);
435
- if (existing?.branchId && normalized.branchId && existing.branchId !== normalized.branchId) {
436
- throw new Error(`Pending local branch ${normalized.projectId}/${normalized.branchName} changed durable branch id`);
437
- }
438
- byKey.set(key, normalized.branchId ? normalized : existing ?? normalized);
439
- }
440
- return [...byKey.values()].sort(
441
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
442
- );
443
- }
444
- function primaryBranch(project) {
445
- if (project.branches.some(({ branchName }) => branchName === "main")) return "main";
446
- if (project.branches.some(({ branchName }) => branchName === project.defaultBranch)) return project.defaultBranch;
447
- return project.branches[0]?.branchName ?? null;
448
- }
449
- function branchIsCurrentlyDesired(desiredProjects, projectId, branchName) {
450
- return Boolean(
451
- desiredProjects.find((project) => project.projectId === projectId)?.branches.some((branch) => branch.branchName === branchName)
452
- );
453
- }
454
- function tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) {
455
- const project = desiredProjects.find(
456
- (candidate) => candidate.projectId === tombstone.projectId && samePathSegments(candidate.checkoutPathSegments, tombstone.checkoutPathSegments)
457
- );
458
- if (!project) return false;
459
- return tombstone.kind === "project" || project.branches.some(({ branchName }) => branchName === tombstone.branchName);
460
- }
461
- function tombstoneManagedPath(projectsRoot, tombstone) {
462
- return tombstone.kind === "branch" ? managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName) : managedProjectRoot(projectsRoot, tombstone.checkoutPathSegments);
463
- }
464
- function effectiveDesiredProjects(projectsRoot, desiredProjects, tombstones) {
465
- const blockedPaths = tombstones.map((tombstone) => tombstoneManagedPath(projectsRoot, tombstone));
466
- return desiredProjects.flatMap((project) => {
467
- const projectRoot = managedProjectRoot(projectsRoot, project.checkoutPathSegments);
468
- if (blockedPaths.some((blockedPath) => blockedPath === projectRoot)) return [];
469
- return [
470
- {
471
- ...project,
472
- checkoutPathSegments: [...project.checkoutPathSegments],
473
- branches: project.branches.filter(({ branchName }) => {
474
- const branchPath = managedBranchPath(projectsRoot, project.checkoutPathSegments, branchName);
475
- return !blockedPaths.some((blockedPath) => pathsOverlap(blockedPath, branchPath));
476
- })
477
- }
478
- ];
479
- });
480
- }
481
- function stateView(projectsRoot, state) {
482
- const effectiveProjects = effectiveDesiredProjects(projectsRoot, state.desiredProjects, state.tombstones);
483
- const effectiveByPath = new Map(effectiveProjects.map((project) => [project.checkoutPathSegments.join("/"), project]));
484
- const pendingTreeDeletions = state.tombstones.flatMap((tombstone) => {
485
- if (tombstone.treePublishedHead) return [];
486
- if (tombstone.kind === "project") {
487
- return [
488
- {
489
- tombstoneId: tombstone.id,
490
- kind: "project",
491
- projectId: tombstone.projectId,
492
- checkoutPathSegments: [...tombstone.checkoutPathSegments],
493
- branchNames: [...tombstone.branchNames],
494
- managedPath: managedProjectRoot(projectsRoot, tombstone.checkoutPathSegments),
495
- projectDeleted: !state.desiredProjects.some(({ projectId }) => projectId === tombstone.projectId)
496
- }
497
- ];
498
- }
499
- return [
500
- {
501
- tombstoneId: tombstone.id,
502
- kind: "branch",
503
- projectId: tombstone.projectId,
504
- checkoutPathSegments: [...tombstone.checkoutPathSegments],
505
- branchName: tombstone.branchName,
506
- managedPath: managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName),
507
- primaryBranchName: primaryBranch(
508
- effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
509
- projectId: tombstone.projectId,
510
- checkoutPathSegments: tombstone.checkoutPathSegments,
511
- defaultBranch: "main",
512
- branches: []
513
- }
514
- )
515
- }
516
- ];
517
- });
518
- const pendingMirrorRefDeletions = state.tombstones.flatMap((tombstone) => {
519
- if (!tombstone.treePublishedHead || tombstone.kind === "project" || branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)) {
520
- return [];
521
- }
522
- return tombstoneBranchNames(tombstone).filter((branchName) => !tombstone.mirrorRefsDeleted.includes(branchName)).map((branchName) => ({
523
- tombstoneId: tombstone.id,
524
- projectId: tombstone.projectId,
525
- branchName,
526
- checkoutPathSegments: [...tombstone.checkoutPathSegments],
527
- managedPath: managedBranchPath(projectsRoot, tombstone.checkoutPathSegments, branchName),
528
- primaryBranchName: primaryBranch(
529
- effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
530
- projectId: tombstone.projectId,
531
- checkoutPathSegments: tombstone.checkoutPathSegments,
532
- defaultBranch: "main",
533
- branches: []
534
- }
535
- ),
536
- treePublishedHead: tombstone.treePublishedHead
537
- }));
538
- });
539
- return {
540
- desiredProjects: cloneDesiredProjects(state.desiredProjects),
541
- effectiveDesiredProjects: effectiveProjects,
542
- locallyPendingCreatedBranches: state.locallyPendingCreatedBranches.map((pending) => ({ ...pending })),
543
- enabledRepositoryTransitions: state.enabledRepositoryTransitions.map((transition) => ({ ...transition })),
544
- tombstones: cloneTombstones(state.tombstones),
545
- pendingTreeDeletions,
546
- pendingMirrorRefDeletions
547
- };
548
- }
549
- function isRealDirectory(candidate) {
550
- try {
551
- return fs.lstatSync(candidate).isDirectory();
552
- } catch {
553
- return false;
554
- }
555
- }
556
- function directoryEntries(directory) {
557
- if (!isRealDirectory(directory)) return null;
558
- try {
559
- return fs.readdirSync(directory, { withFileTypes: true });
560
- } catch {
561
- return null;
562
- }
563
- }
564
- function canonicalEncodedBranchName(entryName) {
565
- try {
566
- const branchName = decodeURIComponent(entryName);
567
- validateManagedBranchName(branchName);
568
- return encodeURIComponent(branchName) === entryName ? branchName : null;
569
- } catch {
570
- return null;
571
- }
572
- }
573
- function validOuterProjectId(entryName) {
574
- try {
575
- return validateProjectId(entryName);
576
- } catch {
577
- return null;
578
- }
579
- }
580
- function discoveredOuterProject(projects, projectId) {
581
- const existing = projects.get(projectId);
582
- if (existing) return existing;
583
- const created = {
584
- projectId,
585
- projectTreePath: null,
586
- projectTreeRootIsWhollyManaged: false,
587
- planTreePath: null,
588
- planTreeRootIsWhollyManaged: false,
589
- branches: /* @__PURE__ */ new Map()
590
- };
591
- projects.set(projectId, created);
592
- return created;
593
- }
594
- function discoveredOuterBranch(project, branchName) {
595
- const existing = project.branches.get(branchName);
596
- if (existing) return existing;
597
- const created = { branchName, projectTreePath: null, planTreePath: null };
598
- project.branches.set(branchName, created);
599
- return created;
600
- }
601
- function discoverStaleOuterProjectWorkspaceEntries(input) {
602
- const workspaceShadowRoot = path.resolve(input.workspaceShadowRoot);
603
- const desiredProjects = normalizeDesiredProjects(input.desiredProjects);
604
- applyPendingCreatedBranches(desiredProjects, input.locallyPendingCreatedBranches ?? []);
605
- const desiredProjectIds = new Set(desiredProjects.map(({ projectId }) => projectId));
606
- const desiredBranchKeys = new Set(
607
- desiredProjects.flatMap((project) => project.branches.map(({ branchName }) => `${project.projectId}\0${branchName}`))
608
- );
609
- const discovered = /* @__PURE__ */ new Map();
610
- const projectsContainer = path.join(workspaceShadowRoot, "projects");
611
- for (const projectEntry of directoryEntries(projectsContainer) ?? []) {
612
- const projectId = validOuterProjectId(projectEntry.name);
613
- const projectTreePath = path.join(projectsContainer, projectEntry.name);
614
- if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(projectTreePath)) continue;
615
- const project = discoveredOuterProject(discovered, projectId);
616
- project.projectTreePath = projectTreePath;
617
- const projectEntries = directoryEntries(projectTreePath);
618
- const branchesPath = path.join(projectTreePath, "branches");
619
- const branchEntries = directoryEntries(branchesPath);
620
- project.projectTreeRootIsWhollyManaged = Boolean(
621
- projectEntries && projectEntries.every((entry) => entry.name === "branches" && entry.isDirectory()) && branchEntries
622
- );
623
- if (!branchEntries) continue;
624
- for (const branchEntry of branchEntries) {
625
- const branchName = canonicalEncodedBranchName(branchEntry.name);
626
- const branchTreePath = path.join(branchesPath, branchEntry.name);
627
- if (!branchName || !branchEntry.isDirectory() || !isRealDirectory(branchTreePath)) {
628
- project.projectTreeRootIsWhollyManaged = false;
629
- continue;
630
- }
631
- discoveredOuterBranch(project, branchName).projectTreePath = branchTreePath;
632
- }
633
- }
634
- const plansContainer = path.join(workspaceShadowRoot, "plans");
635
- for (const projectEntry of directoryEntries(plansContainer) ?? []) {
636
- const projectId = validOuterProjectId(projectEntry.name);
637
- const planTreePath = path.join(plansContainer, projectEntry.name);
638
- if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(planTreePath)) continue;
639
- const project = discoveredOuterProject(discovered, projectId);
640
- project.planTreePath = planTreePath;
641
- const planEntries = directoryEntries(planTreePath);
642
- project.planTreeRootIsWhollyManaged = Boolean(planEntries);
643
- if (!planEntries) continue;
644
- const desiredBranchNames = desiredProjects.find((desiredProject) => desiredProject.projectId === projectId)?.branches.map(({ branchName }) => branchName) ?? [];
645
- for (const planEntry of planEntries) {
646
- const branchPlanPath = path.join(planTreePath, planEntry.name);
647
- const containsDesiredLegacyNestedPlan = desiredBranchNames.some((desiredBranchName) => {
648
- const segments = desiredBranchName.split("/");
649
- return segments.length > 1 && segments[0] === planEntry.name && isRealDirectory(path.join(planTreePath, ...segments));
650
- });
651
- if (containsDesiredLegacyNestedPlan) continue;
652
- const branchName = canonicalEncodedBranchName(planEntry.name);
653
- if (!branchName || !planEntry.isDirectory() || !isRealDirectory(branchPlanPath)) {
654
- project.planTreeRootIsWhollyManaged = false;
655
- continue;
656
- }
657
- discoveredOuterBranch(project, branchName).planTreePath = branchPlanPath;
658
- }
659
- }
660
- const stale = [];
661
- for (const project of [...discovered.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
662
- const branches = [...project.branches.values()].sort((left, right) => left.branchName.localeCompare(right.branchName));
663
- if (!desiredProjectIds.has(project.projectId)) {
664
- const projectTreePath = project.projectTreeRootIsWhollyManaged ? project.projectTreePath : null;
665
- const planTreePath = project.planTreeRootIsWhollyManaged ? project.planTreePath : null;
666
- const individuallySafeBranches = branches.map((branch) => ({
667
- branchName: branch.branchName,
668
- projectTreePath: projectTreePath ? null : branch.projectTreePath,
669
- planTreePath: planTreePath ? null : branch.planTreePath
670
- }));
671
- if (projectTreePath || planTreePath || individuallySafeBranches.some((branch) => branch.projectTreePath || branch.planTreePath)) {
672
- stale.push({
673
- kind: "project",
674
- projectId: project.projectId,
675
- projectTreePath,
676
- planTreePath,
677
- branches: individuallySafeBranches
678
- });
679
- }
680
- continue;
681
- }
682
- for (const branch of branches) {
683
- if (desiredBranchKeys.has(`${project.projectId}\0${branch.branchName}`)) continue;
684
- stale.push({
685
- kind: "branch",
686
- projectId: project.projectId,
687
- branchName: branch.branchName,
688
- projectTreePath: branch.projectTreePath,
689
- planTreePath: branch.planTreePath
690
- });
691
- }
692
- }
693
- return stale;
694
- }
695
- class ProjectWorkspaceStateStore {
696
- stateRoot;
697
- projectsRoot;
698
- statePath;
699
- constructor(input) {
700
- this.stateRoot = path.resolve(input.stateRoot);
701
- this.projectsRoot = path.resolve(input.projectsRoot);
702
- this.statePath = path.join(this.stateRoot, PROJECT_WORKSPACE_STATE_FILE);
703
- }
704
- read() {
705
- return stateView(this.projectsRoot, readState(this.statePath));
706
- }
707
- discoverStaleOuterEntries(workspaceShadowRoot) {
708
- const state = readState(this.statePath);
709
- return discoverStaleOuterProjectWorkspaceEntries({
710
- workspaceShadowRoot,
711
- desiredProjects: state.desiredProjects,
712
- locallyPendingCreatedBranches: state.locallyPendingCreatedBranches
713
- });
714
- }
715
- async reconcile(input) {
716
- const previous = readState(this.statePath);
717
- const authoritativeDesiredProjects = normalizeDesiredProjects(input.desiredProjects);
718
- for (const project of authoritativeDesiredProjects) {
719
- const occupyingProjectId = previous.desiredProjects.find(
720
- (candidate) => candidate.projectId !== project.projectId && samePathSegments(candidate.checkoutPathSegments, project.checkoutPathSegments)
721
- )?.projectId ?? previous.tombstones.find(
722
- (candidate) => candidate.projectId !== project.projectId && samePathSegments(candidate.checkoutPathSegments, project.checkoutPathSegments)
723
- )?.projectId;
724
- if (occupyingProjectId) {
725
- throw new ProjectWorkspaceCheckoutPathCollisionError({
726
- projectId: project.projectId,
727
- occupyingProjectId,
728
- checkoutPathSegments: project.checkoutPathSegments
729
- });
730
- }
731
- }
732
- const authoritativeProjectIds = new Set(authoritativeDesiredProjects.map(({ projectId }) => projectId));
733
- const preserveOnlyBranches = normalizePreserveOnlyBranches(input.preserveOnlyBranches ?? []);
734
- for (const preserved of preserveOnlyBranches) {
735
- const project = authoritativeDesiredProjects.find(({ projectId }) => projectId === preserved.projectId);
736
- if (!project) {
737
- throw new Error(`Preserve-only branch ${preserved.projectId}/${preserved.branchName} has no desired project`);
738
- }
739
- if (project.branches.some(({ branchName }) => branchName === preserved.branchName)) {
740
- throw new Error(`Preserve-only branch ${preserved.projectId}/${preserved.branchName} is already active`);
741
- }
742
- }
743
- const locallyPendingCreatedBranches = mergePendingCreatedBranches(
744
- previous.locallyPendingCreatedBranches,
745
- input.locallyPendingCreatedBranches ?? []
746
- ).filter(
747
- (pending) => authoritativeProjectIds.has(pending.projectId) && !desiredProjectHasBranch(authoritativeDesiredProjects, pending)
748
- );
749
- for (const pending of locallyPendingCreatedBranches) {
750
- const currentProject = previous.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
751
- const incomingProject = authoritativeDesiredProjects.find(({ projectId }) => projectId === pending.projectId);
752
- if (currentProject && incomingProject && !samePathSegments(currentProject.checkoutPathSegments, incomingProject.checkoutPathSegments)) {
753
- throw new ProjectWorkspacePendingBranchPathChangeError({
754
- projectId: pending.projectId,
755
- branchName: pending.branchName,
756
- currentCheckoutPathSegments: currentProject.checkoutPathSegments,
757
- incomingCheckoutPathSegments: incomingProject.checkoutPathSegments
758
- });
759
- }
760
- }
761
- const desiredProjects = cloneDesiredProjects(authoritativeDesiredProjects);
762
- applyPendingCreatedBranches(desiredProjects, preserveOnlyBranches);
763
- applyPendingCreatedBranches(desiredProjects, locallyPendingCreatedBranches);
764
- const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
765
- const tombstones = cloneTombstones(previous.tombstones);
766
- for (const previousProject of previous.desiredProjects) {
767
- const desiredProject = desiredById.get(previousProject.projectId);
768
- if (!desiredProject || !samePathSegments(previousProject.checkoutPathSegments, desiredProject.checkoutPathSegments)) {
769
- addProjectTombstone(tombstones, previousProject);
770
- continue;
771
- }
772
- const desiredBranchNames = new Set(desiredProject.branches.map(({ branchName }) => branchName));
773
- for (const { branchName } of previousProject.branches) {
774
- if (!desiredBranchNames.has(branchName)) addBranchTombstone(tombstones, previousProject, branchName);
775
- }
776
- }
777
- tombstones.splice(
778
- 0,
779
- tombstones.length,
780
- ...tombstones.filter(
781
- (tombstone) => !tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) && (!tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(desiredProjects, tombstone.projectId, tombstone.branchName))
782
- )
783
- );
784
- const nextState = normalizeState({
785
- version: PROJECT_WORKSPACE_STATE_VERSION,
786
- desiredProjects,
787
- locallyPendingCreatedBranches,
788
- enabledRepositoryTransitions: previous.enabledRepositoryTransitions.filter(({ projectId }) => authoritativeProjectIds.has(projectId)),
789
- tombstones
790
- });
791
- await writeStateAtomically(this.stateRoot, this.statePath, nextState);
792
- return stateView(this.projectsRoot, nextState);
793
- }
794
- /** Record a successful enabled reseed without letting disabled config consume it. */
795
- async recordEnabledRepositoryTransition(input) {
796
- const [transition] = normalizeEnabledRepositoryTransitions([input]);
797
- const state = readState(this.statePath);
798
- if (!state.desiredProjects.some(({ projectId }) => projectId === transition.projectId)) {
799
- throw new Error(`Cannot record an enabled repository transition for missing project ${transition.projectId}`);
800
- }
801
- state.enabledRepositoryTransitions = state.enabledRepositoryTransitions.filter(({ projectId }) => projectId !== transition.projectId);
802
- state.enabledRepositoryTransitions.push(transition);
803
- state.enabledRepositoryTransitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
804
- await writeStateAtomically(this.stateRoot, this.statePath, state);
805
- return stateView(this.projectsRoot, state);
806
- }
807
- /** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
808
- async recordPendingCreatedBranch(input) {
809
- const [pending] = normalizePendingCreatedBranches([input]);
810
- const state = readState(this.statePath);
811
- const existingPending = state.locallyPendingCreatedBranches.find(
812
- (candidate) => pendingCreatedBranchKey(candidate) === pendingCreatedBranchKey(pending)
813
- );
814
- if (existingPending) {
815
- if (existingPending.branchId !== pending.branchId) {
816
- throw new Error(`Pending project branch ${pending.branchName} belongs to another durable branch incarnation`);
817
- }
818
- return stateView(this.projectsRoot, state);
819
- }
820
- const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
821
- if (!project) throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName} without a desired project`);
822
- if (project.branches.some(({ branchName }) => branchName === pending.branchName)) {
823
- throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName}: branch is already desired`);
824
- }
825
- project.branches.push({ branchName: pending.branchName });
826
- project.branches = normalizeBranchNames(project.branches, `Project ${project.projectId}`).map((branchName) => ({ branchName }));
827
- state.locallyPendingCreatedBranches.push(pending);
828
- state.locallyPendingCreatedBranches.sort(
829
- (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
830
- );
831
- await writeStateAtomically(this.stateRoot, this.statePath, state);
832
- return stateView(this.projectsRoot, state);
833
- }
834
- /** Roll back an intent only when Git failed before creating its branch. */
835
- async rollbackPendingCreatedBranch(input) {
836
- const [pending] = normalizePendingCreatedBranches([input]);
837
- const state = readState(this.statePath);
838
- const key = pendingCreatedBranchKey(pending);
839
- const existing = state.locallyPendingCreatedBranches.find((candidate) => pendingCreatedBranchKey(candidate) === key);
840
- if (!existing || pending.branchId && existing.branchId !== pending.branchId) {
841
- return stateView(this.projectsRoot, state);
842
- }
843
- state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
844
- (candidate) => pendingCreatedBranchKey(candidate) !== key
845
- );
846
- const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
847
- if (project) project.branches = project.branches.filter(({ branchName }) => branchName !== pending.branchName);
848
- await writeStateAtomically(this.stateRoot, this.statePath, state);
849
- return stateView(this.projectsRoot, state);
850
- }
851
- /** Clear a create intent after an authoritative desired config contains it. */
852
- async clearPendingCreatedBranch(input) {
853
- const [pending] = normalizePendingCreatedBranches([input]);
854
- const state = readState(this.statePath);
855
- const key = pendingCreatedBranchKey(pending);
856
- if (!state.locallyPendingCreatedBranches.some((candidate) => pendingCreatedBranchKey(candidate) === key)) {
857
- return stateView(this.projectsRoot, state);
858
- }
859
- state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
860
- (candidate) => pendingCreatedBranchKey(candidate) !== key
861
- );
862
- await writeStateAtomically(this.stateRoot, this.statePath, state);
863
- return stateView(this.projectsRoot, state);
864
- }
865
- /** Persist a branch tombstone before mutating its linked worktree or local ref. */
866
- async beginBranchDeletion(input) {
867
- const [target] = normalizePendingCreatedBranches([input]);
868
- const state = readState(this.statePath);
869
- const project = state.desiredProjects.find(({ projectId }) => projectId === target.projectId);
870
- if (!project)
871
- throw new Error(`Cannot begin branch deletion for ${target.projectId}/${target.branchName}: desired project is missing`);
872
- const existingTombstone = state.tombstones.find(
873
- (tombstone) => tombstone.kind === "branch" && tombstone.projectId === target.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments) && tombstone.branchName === target.branchName
874
- );
875
- if (existingTombstone) return stateView(this.projectsRoot, state);
876
- addBranchTombstone(state.tombstones, project, target.branchName);
877
- project.branches = project.branches.filter(({ branchName }) => branchName !== target.branchName);
878
- const key = pendingCreatedBranchKey(target);
879
- state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
880
- (candidate) => pendingCreatedBranchKey(candidate) !== key
881
- );
882
- await writeStateAtomically(this.stateRoot, this.statePath, state);
883
- return stateView(this.projectsRoot, state);
884
- }
885
- async recordTreePublication(input) {
886
- if (!GIT_OBJECT_ID_PATTERN.test(input.publishedHead)) throw new Error("Invalid published workspace head");
887
- const state = readState(this.statePath);
888
- const ids = new Set(input.tombstoneIds);
889
- let changed = false;
890
- for (const tombstone of state.tombstones) {
891
- if (!ids.has(tombstone.id) || tombstone.treePublishedHead) continue;
892
- tombstone.treePublishedHead = input.publishedHead;
893
- changed = true;
894
- }
895
- state.tombstones = state.tombstones.filter(
896
- (tombstone) => !tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)
897
- );
898
- if (changed) await writeStateAtomically(this.stateRoot, this.statePath, state);
899
- return stateView(this.projectsRoot, state);
900
- }
901
- async recordMirrorRefDeletion(input) {
902
- validateManagedBranchName(input.branchName);
903
- const state = readState(this.statePath);
904
- const tombstone = state.tombstones.find(({ id }) => id === input.tombstoneId);
905
- if (!tombstone) return stateView(this.projectsRoot, state);
906
- const branchNames = tombstoneBranchNames(tombstone);
907
- if (!branchNames.includes(input.branchName)) {
908
- throw new Error(`Branch ${input.branchName} does not belong to deletion ${input.tombstoneId}`);
909
- }
910
- if (!tombstone.treePublishedHead) {
911
- throw new Error(`Cannot record mirror deletion before workspace tree publication for ${input.tombstoneId}`);
912
- }
913
- if (!tombstone.mirrorRefsDeleted.includes(input.branchName)) {
914
- tombstone.mirrorRefsDeleted.push(input.branchName);
915
- tombstone.mirrorRefsDeleted.sort();
916
- }
917
- if (branchNames.every((branchName) => tombstone.mirrorRefsDeleted.includes(branchName))) {
918
- state.tombstones = state.tombstones.filter(({ id }) => id !== tombstone.id);
919
- }
920
- await writeStateAtomically(this.stateRoot, this.statePath, state);
921
- return stateView(this.projectsRoot, state);
922
- }
923
- }
924
- const projectWorkspaceStateTestHarness = {
925
- writeEmptyStateAtDurableRootBoundary(stateRoot, afterStateRootFsync) {
926
- return writeStateAtomically(stateRoot, path.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
927
- }
928
- };
929
- export {
930
- PROJECT_WORKSPACE_STATE_FILE,
931
- ProjectWorkspaceCheckoutPathCollisionError,
932
- ProjectWorkspacePendingBranchPathChangeError,
933
- ProjectWorkspaceStateStore,
934
- discoverStaleOuterProjectWorkspaceEntries,
935
- projectWorkspaceStateTestHarness,
936
- pruneAuthoritativelyDesiredBranchDeletions
937
- };