@ricsam/r5d-worker 0.0.123 → 0.0.125
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.
- package/README.md +1 -1
- package/dist/cjs/command-launcher.cjs +6 -2
- package/dist/cjs/main.cjs +496 -238
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-checkout-garbage.cjs +32 -3
- package/dist/cjs/project-workspace-state.cjs +51 -35
- package/dist/cjs/project-worktrees.cjs +79 -34
- package/dist/cjs/recovery-journal-protocol.cjs +56 -0
- package/dist/cjs/recovery-journal-runtime.cjs +133 -0
- package/dist/cjs/recovery-journal-thread.cjs +9 -0
- package/dist/cjs/recovery-journal.cjs +735 -0
- package/dist/cjs/recovery-store.cjs +8 -0
- package/dist/cjs/session-file-mutations.cjs +61 -0
- package/dist/cjs/working-tree-mirror.cjs +1 -0
- package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
- package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
- package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
- package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
- package/dist/cjs/workspace-git-sync.cjs +275 -201
- package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
- package/dist/mjs/command-launcher.mjs +6 -2
- package/dist/mjs/main.mjs +499 -244
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-checkout-garbage.mjs +30 -2
- package/dist/mjs/project-workspace-state.mjs +51 -35
- package/dist/mjs/project-worktrees.mjs +74 -34
- package/dist/mjs/recovery-journal-protocol.mjs +30 -0
- package/dist/mjs/recovery-journal-runtime.mjs +112 -0
- package/dist/mjs/recovery-journal-thread.mjs +8 -0
- package/dist/mjs/recovery-journal.mjs +687 -0
- package/dist/mjs/recovery-store.mjs +8 -0
- package/dist/mjs/session-file-mutations.mjs +37 -0
- package/dist/mjs/working-tree-mirror.mjs +1 -0
- package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
- package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
- package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
- package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
- package/dist/mjs/workspace-git-sync.mjs +264 -202
- package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
- package/dist/types/command-launcher.d.ts +2 -1
- package/dist/types/main.d.ts +30 -19
- package/dist/types/project-checkout-garbage.d.ts +19 -2
- package/dist/types/project-workspace-state.d.ts +9 -9
- package/dist/types/project-worktrees.d.ts +49 -5
- package/dist/types/recovery-journal-protocol.d.ts +35 -0
- package/dist/types/recovery-journal-runtime.d.ts +12 -0
- package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
- package/dist/types/recovery-journal-thread.d.ts +1 -0
- package/dist/types/recovery-journal.d.ts +246 -0
- package/dist/types/recovery-store.d.ts +6 -0
- package/dist/types/session-file-mutations.d.ts +22 -0
- package/dist/types/workspace-command-sync-policy.d.ts +22 -7
- package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
- package/dist/types/workspace-filesystem-executor.d.ts +123 -0
- package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
- package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
- package/dist/types/workspace-git-sync.d.ts +113 -7
- package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
- package/package.json +1 -1
- package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
- package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
- package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
package/dist/cjs/package.json
CHANGED
|
@@ -34,7 +34,8 @@ __export(project_checkout_garbage_exports, {
|
|
|
34
34
|
isStagedProjectCheckoutPath: () => isStagedProjectCheckoutPath,
|
|
35
35
|
ownedStagedPath: () => ownedStagedPath,
|
|
36
36
|
removeStagedProjectCheckout: () => removeStagedProjectCheckout,
|
|
37
|
-
stageProjectCheckoutForDeletion: () => stageProjectCheckoutForDeletion
|
|
37
|
+
stageProjectCheckoutForDeletion: () => stageProjectCheckoutForDeletion,
|
|
38
|
+
stageProjectForDeletion: () => stageProjectForDeletion
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(project_checkout_garbage_exports);
|
|
40
41
|
var import_node_crypto = require("node:crypto");
|
|
@@ -58,11 +59,33 @@ function stageProjectCheckoutForDeletion(projectRoot, branchName, checkoutPath)
|
|
|
58
59
|
import_node_fs.default.renameSync(checkoutPath, target);
|
|
59
60
|
return target;
|
|
60
61
|
}
|
|
62
|
+
function stageProjectForDeletion(projectsRoot, projectRoot) {
|
|
63
|
+
const root = import_node_path.default.resolve(projectsRoot);
|
|
64
|
+
const resolved = import_node_path.default.resolve(projectRoot);
|
|
65
|
+
const relative = import_node_path.default.relative(root, resolved);
|
|
66
|
+
const segments = relative.split(import_node_path.default.sep);
|
|
67
|
+
if (!relative || relative.startsWith("..") || import_node_path.default.isAbsolute(relative) || segments.length !== 2 || segments.some((segment) => !segment || segment.startsWith("."))) {
|
|
68
|
+
throw new Error(`Cannot stage ${projectRoot} for deletion: not a project root under ${projectsRoot}`);
|
|
69
|
+
}
|
|
70
|
+
if (!isRealDirectory(resolved)) throw new Error(`Cannot stage ${projectRoot} for deletion: not a directory`);
|
|
71
|
+
const garbageRoot = import_node_path.default.join(root, segments[0], PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
72
|
+
import_node_fs.default.mkdirSync(garbageRoot, { recursive: true });
|
|
73
|
+
if (!isRealDirectory(garbageRoot)) throw new Error(`Cannot stage ${projectRoot} for deletion: ${garbageRoot} is not a directory`);
|
|
74
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
75
|
+
const target = import_node_path.default.join(garbageRoot, `${segments[1]}-${timestamp}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`);
|
|
76
|
+
import_node_fs.default.renameSync(resolved, target);
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
61
79
|
function isStagedProjectCheckoutPath(projectsRoot, candidate) {
|
|
62
80
|
const relative = import_node_path.default.relative(import_node_path.default.resolve(projectsRoot), import_node_path.default.resolve(candidate));
|
|
63
81
|
if (!relative || relative.startsWith("..") || import_node_path.default.isAbsolute(relative)) return false;
|
|
64
82
|
const segments = relative.split(import_node_path.default.sep);
|
|
65
|
-
|
|
83
|
+
const entry = segments[segments.length - 1];
|
|
84
|
+
if (!entry || entry.startsWith(".")) return false;
|
|
85
|
+
if (segments.length === 4)
|
|
86
|
+
return segments[2] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && !segments[0].startsWith(".") && !segments[1].startsWith(".");
|
|
87
|
+
if (segments.length === 3) return segments[1] === PROJECT_DELETED_CHECKOUTS_DIRECTORY && !segments[0].startsWith(".");
|
|
88
|
+
return false;
|
|
66
89
|
}
|
|
67
90
|
function ownedStagedPath(projectsRoot, candidate) {
|
|
68
91
|
const root = import_node_path.default.resolve(projectsRoot);
|
|
@@ -95,6 +118,11 @@ function discoverStagedProjectCheckouts(projectsRoot) {
|
|
|
95
118
|
const staged = [];
|
|
96
119
|
for (const namespace of realDirectoryEntries(root)) {
|
|
97
120
|
if (namespace.startsWith(".")) continue;
|
|
121
|
+
const namespaceGarbageRoot = import_node_path.default.join(root, namespace, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
122
|
+
for (const entry of realDirectoryEntries(namespaceGarbageRoot)) {
|
|
123
|
+
const candidate = import_node_path.default.join(namespaceGarbageRoot, entry);
|
|
124
|
+
if (ownedStagedPath(root, candidate)) staged.push(candidate);
|
|
125
|
+
}
|
|
98
126
|
for (const project of realDirectoryEntries(import_node_path.default.join(root, namespace))) {
|
|
99
127
|
if (project.startsWith(".")) continue;
|
|
100
128
|
const garbageRoot = import_node_path.default.join(root, namespace, project, PROJECT_DELETED_CHECKOUTS_DIRECTORY);
|
|
@@ -186,5 +214,6 @@ class ProjectCheckoutGarbageCollector {
|
|
|
186
214
|
isStagedProjectCheckoutPath,
|
|
187
215
|
ownedStagedPath,
|
|
188
216
|
removeStagedProjectCheckout,
|
|
189
|
-
stageProjectCheckoutForDeletion
|
|
217
|
+
stageProjectCheckoutForDeletion,
|
|
218
|
+
stageProjectForDeletion
|
|
190
219
|
});
|
|
@@ -314,47 +314,63 @@ function lstatIfExists(targetPath) {
|
|
|
314
314
|
throw error;
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
-
function
|
|
317
|
+
async function fsyncDirectoryAsync(directory) {
|
|
318
|
+
const handle = await import_node_fs.default.promises.open(directory, "r");
|
|
319
|
+
try {
|
|
320
|
+
await handle.sync();
|
|
321
|
+
} finally {
|
|
322
|
+
await handle.close();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function lstatIfExistsAsync(targetPath) {
|
|
326
|
+
try {
|
|
327
|
+
return await import_node_fs.default.promises.lstat(targetPath);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error.code === "ENOENT") return null;
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function ensureDurableStateRoot(stateRoot) {
|
|
318
334
|
const resolvedStateRoot = import_node_path.default.resolve(stateRoot);
|
|
319
335
|
let existingAncestor = resolvedStateRoot;
|
|
320
|
-
while (!
|
|
336
|
+
while (!await lstatIfExistsAsync(existingAncestor)) {
|
|
321
337
|
const parent = import_node_path.default.dirname(existingAncestor);
|
|
322
338
|
if (parent === existingAncestor) throw new Error(`Project workspace state has no existing ancestor: ${resolvedStateRoot}`);
|
|
323
339
|
existingAncestor = parent;
|
|
324
340
|
}
|
|
325
|
-
const ancestorStat =
|
|
341
|
+
const ancestorStat = await lstatIfExistsAsync(existingAncestor);
|
|
326
342
|
if (!ancestorStat || ancestorStat.isSymbolicLink() || !ancestorStat.isDirectory()) {
|
|
327
343
|
throw new Error(`Project workspace state must descend from a real directory: ${resolvedStateRoot}`);
|
|
328
344
|
}
|
|
329
|
-
import_node_fs.default.
|
|
345
|
+
await import_node_fs.default.promises.mkdir(resolvedStateRoot, { recursive: true, mode: 448 });
|
|
330
346
|
let current = resolvedStateRoot;
|
|
331
347
|
while (true) {
|
|
332
|
-
const stat =
|
|
348
|
+
const stat = await lstatIfExistsAsync(current);
|
|
333
349
|
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
334
350
|
throw new Error(`Project workspace state path must be a real directory: ${current}`);
|
|
335
351
|
}
|
|
336
|
-
|
|
352
|
+
await fsyncDirectoryAsync(current);
|
|
337
353
|
if (current === existingAncestor) break;
|
|
338
354
|
current = import_node_path.default.dirname(current);
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
|
|
342
|
-
ensureDurableStateRoot(stateRoot);
|
|
357
|
+
async function writeStateAtomically(stateRoot, statePath, state, afterStateRootFsync) {
|
|
358
|
+
await ensureDurableStateRoot(stateRoot);
|
|
343
359
|
afterStateRootFsync?.();
|
|
344
360
|
const temporaryPath = import_node_path.default.join(stateRoot, `.${PROJECT_WORKSPACE_STATE_FILE}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
|
|
345
|
-
let
|
|
361
|
+
let handle;
|
|
346
362
|
try {
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
handle = await import_node_fs.default.promises.open(temporaryPath, "wx", 384);
|
|
364
|
+
await handle.writeFile(`${JSON.stringify(normalizeState(state), null, 2)}
|
|
349
365
|
`, "utf8");
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
import_node_fs.default.
|
|
354
|
-
|
|
366
|
+
await handle.sync();
|
|
367
|
+
await handle.close();
|
|
368
|
+
handle = void 0;
|
|
369
|
+
await import_node_fs.default.promises.rename(temporaryPath, statePath);
|
|
370
|
+
await fsyncDirectoryAsync(stateRoot);
|
|
355
371
|
} finally {
|
|
356
|
-
if (
|
|
357
|
-
import_node_fs.default.
|
|
372
|
+
if (handle !== void 0) await handle.close();
|
|
373
|
+
await import_node_fs.default.promises.rm(temporaryPath, { force: true });
|
|
358
374
|
}
|
|
359
375
|
}
|
|
360
376
|
function createBranchTombstone(project, branchName) {
|
|
@@ -730,7 +746,7 @@ class ProjectWorkspaceStateStore {
|
|
|
730
746
|
locallyPendingCreatedBranches: state.locallyPendingCreatedBranches
|
|
731
747
|
});
|
|
732
748
|
}
|
|
733
|
-
reconcile(input) {
|
|
749
|
+
async reconcile(input) {
|
|
734
750
|
const previous = readState(this.statePath);
|
|
735
751
|
const authoritativeDesiredProjects = normalizeDesiredProjects(input.desiredProjects);
|
|
736
752
|
for (const project of authoritativeDesiredProjects) {
|
|
@@ -806,11 +822,11 @@ class ProjectWorkspaceStateStore {
|
|
|
806
822
|
enabledRepositoryTransitions: previous.enabledRepositoryTransitions.filter(({ projectId }) => authoritativeProjectIds.has(projectId)),
|
|
807
823
|
tombstones
|
|
808
824
|
});
|
|
809
|
-
writeStateAtomically(this.stateRoot, this.statePath, nextState);
|
|
825
|
+
await writeStateAtomically(this.stateRoot, this.statePath, nextState);
|
|
810
826
|
return stateView(this.projectsRoot, nextState);
|
|
811
827
|
}
|
|
812
828
|
/** Record a successful enabled reseed without letting disabled config consume it. */
|
|
813
|
-
recordEnabledRepositoryTransition(input) {
|
|
829
|
+
async recordEnabledRepositoryTransition(input) {
|
|
814
830
|
const [transition] = normalizeEnabledRepositoryTransitions([input]);
|
|
815
831
|
const state = readState(this.statePath);
|
|
816
832
|
if (!state.desiredProjects.some(({ projectId }) => projectId === transition.projectId)) {
|
|
@@ -819,11 +835,11 @@ class ProjectWorkspaceStateStore {
|
|
|
819
835
|
state.enabledRepositoryTransitions = state.enabledRepositoryTransitions.filter(({ projectId }) => projectId !== transition.projectId);
|
|
820
836
|
state.enabledRepositoryTransitions.push(transition);
|
|
821
837
|
state.enabledRepositoryTransitions.sort((left, right) => left.projectId.localeCompare(right.projectId));
|
|
822
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
838
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
823
839
|
return stateView(this.projectsRoot, state);
|
|
824
840
|
}
|
|
825
841
|
/** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
|
|
826
|
-
recordPendingCreatedBranch(input) {
|
|
842
|
+
async recordPendingCreatedBranch(input) {
|
|
827
843
|
const [pending] = normalizePendingCreatedBranches([input]);
|
|
828
844
|
const state = readState(this.statePath);
|
|
829
845
|
const existingPending = state.locallyPendingCreatedBranches.find(
|
|
@@ -846,11 +862,11 @@ class ProjectWorkspaceStateStore {
|
|
|
846
862
|
state.locallyPendingCreatedBranches.sort(
|
|
847
863
|
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
848
864
|
);
|
|
849
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
865
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
850
866
|
return stateView(this.projectsRoot, state);
|
|
851
867
|
}
|
|
852
868
|
/** Roll back an intent only when Git failed before creating its branch. */
|
|
853
|
-
rollbackPendingCreatedBranch(input) {
|
|
869
|
+
async rollbackPendingCreatedBranch(input) {
|
|
854
870
|
const [pending] = normalizePendingCreatedBranches([input]);
|
|
855
871
|
const state = readState(this.statePath);
|
|
856
872
|
const key = pendingCreatedBranchKey(pending);
|
|
@@ -863,11 +879,11 @@ class ProjectWorkspaceStateStore {
|
|
|
863
879
|
);
|
|
864
880
|
const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
|
|
865
881
|
if (project) project.branches = project.branches.filter(({ branchName }) => branchName !== pending.branchName);
|
|
866
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
882
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
867
883
|
return stateView(this.projectsRoot, state);
|
|
868
884
|
}
|
|
869
885
|
/** Clear a create intent after an authoritative desired config contains it. */
|
|
870
|
-
clearPendingCreatedBranch(input) {
|
|
886
|
+
async clearPendingCreatedBranch(input) {
|
|
871
887
|
const [pending] = normalizePendingCreatedBranches([input]);
|
|
872
888
|
const state = readState(this.statePath);
|
|
873
889
|
const key = pendingCreatedBranchKey(pending);
|
|
@@ -877,11 +893,11 @@ class ProjectWorkspaceStateStore {
|
|
|
877
893
|
state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
|
|
878
894
|
(candidate) => pendingCreatedBranchKey(candidate) !== key
|
|
879
895
|
);
|
|
880
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
896
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
881
897
|
return stateView(this.projectsRoot, state);
|
|
882
898
|
}
|
|
883
899
|
/** Persist a branch tombstone before mutating its linked worktree or local ref. */
|
|
884
|
-
beginBranchDeletion(input) {
|
|
900
|
+
async beginBranchDeletion(input) {
|
|
885
901
|
const [target] = normalizePendingCreatedBranches([input]);
|
|
886
902
|
const state = readState(this.statePath);
|
|
887
903
|
const project = state.desiredProjects.find(({ projectId }) => projectId === target.projectId);
|
|
@@ -897,10 +913,10 @@ class ProjectWorkspaceStateStore {
|
|
|
897
913
|
state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
|
|
898
914
|
(candidate) => pendingCreatedBranchKey(candidate) !== key
|
|
899
915
|
);
|
|
900
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
916
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
901
917
|
return stateView(this.projectsRoot, state);
|
|
902
918
|
}
|
|
903
|
-
recordTreePublication(input) {
|
|
919
|
+
async recordTreePublication(input) {
|
|
904
920
|
if (!GIT_OBJECT_ID_PATTERN.test(input.publishedHead)) throw new Error("Invalid published workspace head");
|
|
905
921
|
const state = readState(this.statePath);
|
|
906
922
|
const ids = new Set(input.tombstoneIds);
|
|
@@ -913,10 +929,10 @@ class ProjectWorkspaceStateStore {
|
|
|
913
929
|
state.tombstones = state.tombstones.filter(
|
|
914
930
|
(tombstone) => !tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)
|
|
915
931
|
);
|
|
916
|
-
if (changed) writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
932
|
+
if (changed) await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
917
933
|
return stateView(this.projectsRoot, state);
|
|
918
934
|
}
|
|
919
|
-
recordMirrorRefDeletion(input) {
|
|
935
|
+
async recordMirrorRefDeletion(input) {
|
|
920
936
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
921
937
|
const state = readState(this.statePath);
|
|
922
938
|
const tombstone = state.tombstones.find(({ id }) => id === input.tombstoneId);
|
|
@@ -935,13 +951,13 @@ class ProjectWorkspaceStateStore {
|
|
|
935
951
|
if (branchNames.every((branchName) => tombstone.mirrorRefsDeleted.includes(branchName))) {
|
|
936
952
|
state.tombstones = state.tombstones.filter(({ id }) => id !== tombstone.id);
|
|
937
953
|
}
|
|
938
|
-
writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
954
|
+
await writeStateAtomically(this.stateRoot, this.statePath, state);
|
|
939
955
|
return stateView(this.projectsRoot, state);
|
|
940
956
|
}
|
|
941
957
|
}
|
|
942
958
|
const projectWorkspaceStateTestHarness = {
|
|
943
959
|
writeEmptyStateAtDurableRootBoundary(stateRoot, afterStateRootFsync) {
|
|
944
|
-
writeStateAtomically(stateRoot, import_node_path.default.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
|
|
960
|
+
return writeStateAtomically(stateRoot, import_node_path.default.join(stateRoot, PROJECT_WORKSPACE_STATE_FILE), EMPTY_STATE, afterStateRootFsync);
|
|
945
961
|
}
|
|
946
962
|
};
|
|
947
963
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var project_worktrees_exports = {};
|
|
30
30
|
__export(project_worktrees_exports, {
|
|
31
31
|
PROJECT_REMOVED_CHECKOUTS_DIRECTORY: () => PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
32
|
+
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID: () => PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
32
33
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX: () => PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
33
34
|
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
34
35
|
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
@@ -52,7 +53,11 @@ __export(project_worktrees_exports, {
|
|
|
52
53
|
pushProjectMirrorHeads: () => pushProjectMirrorHeads,
|
|
53
54
|
recoverStaleProjectWorktreeSnapshots: () => recoverStaleProjectWorktreeSnapshots,
|
|
54
55
|
removeProjectWorktrees: () => removeProjectWorktrees,
|
|
55
|
-
reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror
|
|
56
|
+
reseedProjectHeadsFromMirror: () => reseedProjectHeadsFromMirror,
|
|
57
|
+
runProjectSnapshotApplyJob: () => runProjectSnapshotApplyJob,
|
|
58
|
+
runProjectSnapshotCreateJob: () => runProjectSnapshotCreateJob,
|
|
59
|
+
runProjectSnapshotPersistJob: () => runProjectSnapshotPersistJob,
|
|
60
|
+
runProjectSnapshotRollbackJob: () => runProjectSnapshotRollbackJob
|
|
56
61
|
});
|
|
57
62
|
module.exports = __toCommonJS(project_worktrees_exports);
|
|
58
63
|
var import_node_crypto = require("node:crypto");
|
|
@@ -64,6 +69,7 @@ var import_managed_paths = require("./managed-paths.cjs");
|
|
|
64
69
|
var import_project_checkout_garbage = require("./project-checkout-garbage.cjs");
|
|
65
70
|
var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
|
|
66
71
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
72
|
+
var import_workspace_filesystem_executor = require("./workspace-filesystem-executor.cjs");
|
|
67
73
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
68
74
|
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
69
75
|
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION = 1;
|
|
@@ -479,7 +485,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
479
485
|
}
|
|
480
486
|
}
|
|
481
487
|
}
|
|
482
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope
|
|
488
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, scope, ownerSessionId) {
|
|
483
489
|
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
484
490
|
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
485
491
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -502,7 +508,7 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress, s
|
|
|
502
508
|
state: "building",
|
|
503
509
|
scope,
|
|
504
510
|
ownerProcessId: process.pid,
|
|
505
|
-
ownerSessionId
|
|
511
|
+
ownerSessionId,
|
|
506
512
|
projectRoot: resolvedProjectRoot,
|
|
507
513
|
branches: branchSnapshots,
|
|
508
514
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1025,7 +1031,7 @@ async function reconcileLinkedLayout(input) {
|
|
|
1025
1031
|
plan.push({ branchName: branch.branchName, checkoutPath, seed: resolved.seed, action: keep ? "keep" : "reset" });
|
|
1026
1032
|
}
|
|
1027
1033
|
const mutatedBranches = new Set(plan.filter(({ action }) => action !== "keep").map(({ branchName }) => branchName));
|
|
1028
|
-
if (mutatedBranches.size > 0) input.beforeDestructiveMutation(mutatedBranches);
|
|
1034
|
+
if (mutatedBranches.size > 0) await input.beforeDestructiveMutation(mutatedBranches);
|
|
1029
1035
|
for (const entry of plan) {
|
|
1030
1036
|
if (entry.action === "keep") continue;
|
|
1031
1037
|
if (entry.action === "reset") {
|
|
@@ -1066,13 +1072,19 @@ async function ensureProjectWorktrees(input) {
|
|
|
1066
1072
|
const snapshotStorageRoot = import_node_path.default.resolve(input.snapshotRoot ?? import_node_os.default.tmpdir());
|
|
1067
1073
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
1068
1074
|
let snapshot;
|
|
1069
|
-
const ensureSnapshot = (branchNames) => {
|
|
1070
|
-
snapshot ??=
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1075
|
+
const ensureSnapshot = async (branchNames) => {
|
|
1076
|
+
snapshot ??= await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run(
|
|
1077
|
+
"project_snapshot_create",
|
|
1078
|
+
{
|
|
1079
|
+
projectRoot,
|
|
1080
|
+
branches: (branchNames ? input.branches.filter(({ branchName }) => branchNames.has(branchName)) : input.branches).map((branch) => ({
|
|
1081
|
+
...branch
|
|
1082
|
+
})),
|
|
1083
|
+
temporaryRoot: snapshotStorageRoot,
|
|
1084
|
+
scope: branchNames ? "branches" : "project",
|
|
1085
|
+
ownerSessionId: PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
1086
|
+
},
|
|
1087
|
+
{ onProgress: input.onSnapshotProgress }
|
|
1076
1088
|
);
|
|
1077
1089
|
return snapshot;
|
|
1078
1090
|
};
|
|
@@ -1090,10 +1102,12 @@ async function ensureProjectWorktrees(input) {
|
|
|
1090
1102
|
...input,
|
|
1091
1103
|
projectRoot,
|
|
1092
1104
|
defaultBranch,
|
|
1093
|
-
beforeDestructiveMutation:
|
|
1105
|
+
beforeDestructiveMutation: async (branchNames) => {
|
|
1106
|
+
await ensureSnapshot(branchNames);
|
|
1107
|
+
}
|
|
1094
1108
|
}));
|
|
1095
1109
|
} else {
|
|
1096
|
-
ensureSnapshot();
|
|
1110
|
+
await ensureSnapshot();
|
|
1097
1111
|
mirrorHeads = await initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
|
|
1098
1112
|
mutatedBranches = new Set(input.branches.map(({ branchName }) => branchName));
|
|
1099
1113
|
}
|
|
@@ -1101,17 +1115,12 @@ async function ensureProjectWorktrees(input) {
|
|
|
1101
1115
|
if (mutatedBranches.size > 0 && !preparedSnapshot) {
|
|
1102
1116
|
throw new Error("Project worktree mutation started without a prepared recovery snapshot");
|
|
1103
1117
|
}
|
|
1104
|
-
|
|
1105
|
-
if (!mutatedBranches.has(branch.branchName))
|
|
1118
|
+
const snapshotMirrors = input.branches.flatMap((branch) => {
|
|
1119
|
+
if (!mutatedBranches.has(branch.branchName)) return [];
|
|
1106
1120
|
const snapshotPath = preparedSnapshot?.paths.get(branch.branchName);
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
targetRoot: branchPath(projectRoot, branch.branchName),
|
|
1111
|
-
sourceMode: "all",
|
|
1112
|
-
deletionMode: "git"
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1121
|
+
return snapshotPath ? [{ snapshotPath, targetPath: branchPath(projectRoot, branch.branchName) }] : [];
|
|
1122
|
+
});
|
|
1123
|
+
if (snapshotMirrors.length > 0) await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_apply", { mirrors: snapshotMirrors });
|
|
1115
1124
|
const states = [];
|
|
1116
1125
|
for (const branch of input.branches) {
|
|
1117
1126
|
const checkoutPath = branchPath(projectRoot, branch.branchName);
|
|
@@ -1127,24 +1136,23 @@ async function ensureProjectWorktrees(input) {
|
|
|
1127
1136
|
states.sort((left, right) => left.branchName.localeCompare(right.branchName));
|
|
1128
1137
|
if (snapshot) {
|
|
1129
1138
|
const primaryGitDirectory = import_node_path.default.join(branchPath(projectRoot, input.primaryBranchName), ".git");
|
|
1130
|
-
|
|
1139
|
+
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_persist", {
|
|
1131
1140
|
snapshot,
|
|
1132
|
-
|
|
1133
|
-
snapshot.manifest.scope === "branches" ? [
|
|
1141
|
+
durabilityPaths: snapshot.manifest.scope === "branches" ? [
|
|
1134
1142
|
...snapshot.manifest.branches.map(({ branchName }) => branchPath(projectRoot, branchName)),
|
|
1135
1143
|
// Branch refs and worktree registrations moved in the shared
|
|
1136
1144
|
// administrative directory; objects are fsynced by git itself.
|
|
1137
1145
|
import_node_path.default.join(primaryGitDirectory, "refs"),
|
|
1138
1146
|
import_node_path.default.join(primaryGitDirectory, "packed-refs"),
|
|
1139
1147
|
import_node_path.default.join(primaryGitDirectory, "worktrees")
|
|
1140
|
-
] :
|
|
1141
|
-
);
|
|
1148
|
+
] : null
|
|
1149
|
+
});
|
|
1142
1150
|
}
|
|
1143
1151
|
return states;
|
|
1144
1152
|
} catch (error) {
|
|
1145
1153
|
if (!snapshot) throw error;
|
|
1146
1154
|
try {
|
|
1147
|
-
|
|
1155
|
+
await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_snapshot_rollback", { snapshot });
|
|
1148
1156
|
} catch (recoveryError) {
|
|
1149
1157
|
throw new Error(
|
|
1150
1158
|
`Project worktree reconciliation failed and its snapshot could not be fully restored; recovery state is retained at ${snapshot.root}`,
|
|
@@ -1275,7 +1283,7 @@ function createLinkedProjectBranch(input) {
|
|
|
1275
1283
|
throw error;
|
|
1276
1284
|
}
|
|
1277
1285
|
}
|
|
1278
|
-
function createOrRetryLinkedProjectBranch(input) {
|
|
1286
|
+
async function createOrRetryLinkedProjectBranch(input) {
|
|
1279
1287
|
if (input.pendingRetry) {
|
|
1280
1288
|
deleteLinkedProjectBranch({
|
|
1281
1289
|
projectRoot: input.projectRoot,
|
|
@@ -1283,7 +1291,12 @@ function createOrRetryLinkedProjectBranch(input) {
|
|
|
1283
1291
|
branchName: input.branchName
|
|
1284
1292
|
});
|
|
1285
1293
|
}
|
|
1286
|
-
return
|
|
1294
|
+
return await (0, import_workspace_filesystem_executor.workspaceFilesystemExecutor)().run("project_branch_create", {
|
|
1295
|
+
projectRoot: input.projectRoot,
|
|
1296
|
+
sourceBranchName: input.sourceBranchName,
|
|
1297
|
+
branchName: input.branchName,
|
|
1298
|
+
workingTree: input.workingTree
|
|
1299
|
+
});
|
|
1287
1300
|
}
|
|
1288
1301
|
function classifyProjectCheckout(input) {
|
|
1289
1302
|
const checkoutPath = branchPath(input.projectRoot, input.branchName);
|
|
@@ -1377,7 +1390,8 @@ function removeProjectWorktrees(input) {
|
|
|
1377
1390
|
throw new Error(`Project branch ${branchName} has an in-progress Git operation`);
|
|
1378
1391
|
}
|
|
1379
1392
|
}
|
|
1380
|
-
import_node_fs.default.
|
|
1393
|
+
if (!import_node_fs.default.existsSync(projectRoot)) return {};
|
|
1394
|
+
return { stagedForCollectionPath: (0, import_project_checkout_garbage.stageProjectForDeletion)(input.projectsRoot, projectRoot) };
|
|
1381
1395
|
}
|
|
1382
1396
|
async function deleteProjectMirrorBranch(input) {
|
|
1383
1397
|
(0, import_managed_paths.validateManagedBranchName)(input.branchName);
|
|
@@ -1620,11 +1634,37 @@ async function reseedProjectHeadsFromMirror(input) {
|
|
|
1620
1634
|
reseeded: moved
|
|
1621
1635
|
}));
|
|
1622
1636
|
}
|
|
1637
|
+
function runProjectSnapshotCreateJob(input, context) {
|
|
1638
|
+
return snapshotBranchTrees(input.projectRoot, input.branches, input.temporaryRoot, context.progress, input.scope, input.ownerSessionId);
|
|
1639
|
+
}
|
|
1640
|
+
function runProjectSnapshotApplyJob(input) {
|
|
1641
|
+
for (const { snapshotPath, targetPath } of input.mirrors) {
|
|
1642
|
+
(0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotPath, targetRoot: targetPath, sourceMode: "all", deletionMode: "git" });
|
|
1643
|
+
}
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
function runProjectSnapshotPersistJob(input) {
|
|
1647
|
+
persistReconciledProjectAndConsumeSnapshot(input.snapshot, void 0, input.durabilityPaths ?? void 0);
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
function runProjectSnapshotRollbackJob(input) {
|
|
1651
|
+
rollbackProjectWorktreeSnapshot(input.snapshot);
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1623
1654
|
const projectWorktreesTestHarness = {
|
|
1624
1655
|
commandArgs: gitCommandArgs,
|
|
1625
1656
|
configureRepository,
|
|
1626
1657
|
createPreparedSnapshot(input) {
|
|
1627
|
-
return {
|
|
1658
|
+
return {
|
|
1659
|
+
root: snapshotBranchTrees(
|
|
1660
|
+
input.projectRoot,
|
|
1661
|
+
input.branches,
|
|
1662
|
+
input.temporaryRoot,
|
|
1663
|
+
void 0,
|
|
1664
|
+
input.scope ?? "project",
|
|
1665
|
+
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID
|
|
1666
|
+
).root
|
|
1667
|
+
};
|
|
1628
1668
|
},
|
|
1629
1669
|
consumeSnapshot(snapshotRoot) {
|
|
1630
1670
|
const manifest = readSnapshotManifest(snapshotRoot);
|
|
@@ -1641,6 +1681,7 @@ const projectWorktreesTestHarness = {
|
|
|
1641
1681
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1642
1682
|
0 && (module.exports = {
|
|
1643
1683
|
PROJECT_REMOVED_CHECKOUTS_DIRECTORY,
|
|
1684
|
+
PROJECT_WORKTREE_SNAPSHOT_OWNER_SESSION_ID,
|
|
1644
1685
|
PROJECT_WORKTREE_SNAPSHOT_PREFIX,
|
|
1645
1686
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1646
1687
|
applyObservedProjectMirrorHeads,
|
|
@@ -1664,5 +1705,9 @@ const projectWorktreesTestHarness = {
|
|
|
1664
1705
|
pushProjectMirrorHeads,
|
|
1665
1706
|
recoverStaleProjectWorktreeSnapshots,
|
|
1666
1707
|
removeProjectWorktrees,
|
|
1667
|
-
reseedProjectHeadsFromMirror
|
|
1708
|
+
reseedProjectHeadsFromMirror,
|
|
1709
|
+
runProjectSnapshotApplyJob,
|
|
1710
|
+
runProjectSnapshotCreateJob,
|
|
1711
|
+
runProjectSnapshotPersistJob,
|
|
1712
|
+
runProjectSnapshotRollbackJob
|
|
1668
1713
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var recovery_journal_protocol_exports = {};
|
|
20
|
+
__export(recovery_journal_protocol_exports, {
|
|
21
|
+
WORKER_RECOVERY_JOURNAL_OPS: () => WORKER_RECOVERY_JOURNAL_OPS,
|
|
22
|
+
isWorkerRecoveryJournalOp: () => isWorkerRecoveryJournalOp,
|
|
23
|
+
serializeWorkerRecoveryJournalError: () => serializeWorkerRecoveryJournalError
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(recovery_journal_protocol_exports);
|
|
26
|
+
const WORKER_RECOVERY_JOURNAL_OPS = [
|
|
27
|
+
"admit",
|
|
28
|
+
"readmitUnknownBranchDeletion",
|
|
29
|
+
"record",
|
|
30
|
+
"unknown",
|
|
31
|
+
"unknownRun",
|
|
32
|
+
"acknowledgeResult",
|
|
33
|
+
"acknowledgeOutput",
|
|
34
|
+
"acknowledgeTerminal",
|
|
35
|
+
"cancelSession",
|
|
36
|
+
"isRequestCancelled",
|
|
37
|
+
"operations",
|
|
38
|
+
"recoveryState",
|
|
39
|
+
"terminals",
|
|
40
|
+
"outputReplayBegin",
|
|
41
|
+
"outputReplayNext",
|
|
42
|
+
"outputReplayEnd"
|
|
43
|
+
];
|
|
44
|
+
function isWorkerRecoveryJournalOp(value) {
|
|
45
|
+
return typeof value === "string" && WORKER_RECOVERY_JOURNAL_OPS.includes(value);
|
|
46
|
+
}
|
|
47
|
+
function serializeWorkerRecoveryJournalError(error) {
|
|
48
|
+
if (!(error instanceof Error)) return { name: "Error", message: String(error) };
|
|
49
|
+
return { name: error.name, message: error.message, ...typeof error.stack === "string" ? { stack: error.stack } : {} };
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
WORKER_RECOVERY_JOURNAL_OPS,
|
|
54
|
+
isWorkerRecoveryJournalOp,
|
|
55
|
+
serializeWorkerRecoveryJournalError
|
|
56
|
+
});
|