@ricsam/r5d-worker 0.0.80 → 0.0.82
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 +7 -1
- package/dist/cjs/main.cjs +331 -98
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +377 -122
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +329 -98
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +381 -122
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +63 -0
- package/dist/types/working-tree-mirror.d.ts +1 -2
- package/dist/types/workspace-command-sync-policy.d.ts +4 -3
- package/dist/types/workspace-git-sync.d.ts +6 -0
- package/dist/types/workspace-merge-projection.d.ts +42 -0
- package/package.json +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
4
|
+
import { inspectWorkingTree } from "./working-tree-mirror.mjs";
|
|
5
|
+
import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
|
|
6
|
+
const MINIMUM_MERGE_TREE_GIT_VERSION = { major: 2, minor: 40 };
|
|
7
|
+
const NON_RECURSIVE_GIT_CONFIG = [
|
|
8
|
+
"-c",
|
|
9
|
+
"submodule.recurse=false",
|
|
10
|
+
"-c",
|
|
11
|
+
"fetch.recurseSubmodules=false",
|
|
12
|
+
"-c",
|
|
13
|
+
"push.recurseSubmodules=false"
|
|
14
|
+
];
|
|
15
|
+
let cachedWorkspaceMergeProjectionSupport = null;
|
|
16
|
+
function gitCommandArgs(args) {
|
|
17
|
+
return ["git", ...NON_RECURSIVE_GIT_CONFIG, ...args];
|
|
18
|
+
}
|
|
19
|
+
function gitResult(cwd, args, options = {}) {
|
|
20
|
+
const result = Bun.spawnSync(gitCommandArgs(args), {
|
|
21
|
+
cwd,
|
|
22
|
+
...options.stdin !== void 0 ? { stdin: options.stdin } : {},
|
|
23
|
+
stdout: "pipe",
|
|
24
|
+
stderr: "pipe",
|
|
25
|
+
env: options.environment ?? workerGitProcessEnvironment()
|
|
26
|
+
});
|
|
27
|
+
return { exitCode: result.exitCode, stdout: Buffer.from(result.stdout), stderr: Buffer.from(result.stderr) };
|
|
28
|
+
}
|
|
29
|
+
function git(cwd, args, action, options = {}) {
|
|
30
|
+
const result = gitResult(cwd, args, options);
|
|
31
|
+
if (result.exitCode !== 0) {
|
|
32
|
+
const detail = result.stderr.toString().trim() || result.stdout.toString().trim() || `git exited ${result.exitCode}`;
|
|
33
|
+
throw new Error(`${action}: ${detail}`);
|
|
34
|
+
}
|
|
35
|
+
return result.stdout;
|
|
36
|
+
}
|
|
37
|
+
function gitText(cwd, args, action, options = {}) {
|
|
38
|
+
return git(cwd, args, action, options).toString().trim();
|
|
39
|
+
}
|
|
40
|
+
function gitVersionIsSupported(version) {
|
|
41
|
+
const match = /(?:^|\s)(\d+)\.(\d+)(?:\.\d+)?(?:\s|$)/u.exec(version);
|
|
42
|
+
if (!match) return false;
|
|
43
|
+
const major = Number(match[1]);
|
|
44
|
+
const minor = Number(match[2]);
|
|
45
|
+
return major > MINIMUM_MERGE_TREE_GIT_VERSION.major || major === MINIMUM_MERGE_TREE_GIT_VERSION.major && minor >= MINIMUM_MERGE_TREE_GIT_VERSION.minor;
|
|
46
|
+
}
|
|
47
|
+
function workspaceMergeProjectionSupport() {
|
|
48
|
+
if (cachedWorkspaceMergeProjectionSupport) return cachedWorkspaceMergeProjectionSupport;
|
|
49
|
+
if (process.env.R5D_WORKSPACE_MERGE_PROJECTION === "0") {
|
|
50
|
+
cachedWorkspaceMergeProjectionSupport = {
|
|
51
|
+
supported: false,
|
|
52
|
+
error: "Workspace merge projection is disabled by R5D_WORKSPACE_MERGE_PROJECTION=0; stale mounts will remain pinned until it is enabled"
|
|
53
|
+
};
|
|
54
|
+
return cachedWorkspaceMergeProjectionSupport;
|
|
55
|
+
}
|
|
56
|
+
const version = gitResult(void 0, ["--version"]);
|
|
57
|
+
const versionText = version.stdout.toString().trim();
|
|
58
|
+
if (version.exitCode !== 0 || !gitVersionIsSupported(versionText)) {
|
|
59
|
+
const observed = versionText || version.stderr.toString().trim() || `git exited ${version.exitCode}`;
|
|
60
|
+
cachedWorkspaceMergeProjectionSupport = {
|
|
61
|
+
supported: false,
|
|
62
|
+
error: `Workspace merge projection requires Git 2.40 or newer (found ${observed}); upgrade Git before synchronizing stale mounts`
|
|
63
|
+
};
|
|
64
|
+
return cachedWorkspaceMergeProjectionSupport;
|
|
65
|
+
}
|
|
66
|
+
cachedWorkspaceMergeProjectionSupport = { supported: true };
|
|
67
|
+
return cachedWorkspaceMergeProjectionSupport;
|
|
68
|
+
}
|
|
69
|
+
function normalizeWorkspaceRelativePath(value) {
|
|
70
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
71
|
+
if (!normalized || normalized === "." || normalized.includes("\0") || normalized.split("/").some((segment) => segment === ".." || segment === ".git")) {
|
|
72
|
+
throw new Error(`Invalid workspace merge-projection path: ${value}`);
|
|
73
|
+
}
|
|
74
|
+
return normalized;
|
|
75
|
+
}
|
|
76
|
+
function requireObjectId(value, label) {
|
|
77
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value)) throw new Error(`${label} is not a Git object id: ${value}`);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function requireCommit(workspacePath, revision, label) {
|
|
81
|
+
const head = gitText(workspacePath, ["rev-parse", "--verify", `${revision}^{commit}`], `resolve ${label}`);
|
|
82
|
+
return requireObjectId(head, label);
|
|
83
|
+
}
|
|
84
|
+
function workspacePrivateStateDirectory(workspacePath) {
|
|
85
|
+
const gitDirectory = path.join(workspacePath, ".git");
|
|
86
|
+
const gitStatus = fs.lstatSync(gitDirectory);
|
|
87
|
+
if (!gitStatus.isDirectory() || gitStatus.isSymbolicLink()) {
|
|
88
|
+
throw new Error(`Workspace Git directory is not a regular directory: ${gitDirectory}`);
|
|
89
|
+
}
|
|
90
|
+
const stateDirectory = path.join(gitDirectory, "r5d");
|
|
91
|
+
let stateStatus = null;
|
|
92
|
+
try {
|
|
93
|
+
stateStatus = fs.lstatSync(stateDirectory);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code !== "ENOENT") throw error;
|
|
96
|
+
}
|
|
97
|
+
if (!stateStatus) fs.mkdirSync(stateDirectory, { mode: 448 });
|
|
98
|
+
else if (!stateStatus.isDirectory() || stateStatus.isSymbolicLink()) {
|
|
99
|
+
throw new Error(`Workspace Git private state is not a regular directory: ${stateDirectory}`);
|
|
100
|
+
}
|
|
101
|
+
return stateDirectory;
|
|
102
|
+
}
|
|
103
|
+
function temporaryIndexPath(workspacePath) {
|
|
104
|
+
return path.join(workspacePrivateStateDirectory(workspacePath), `tmp-index-${crypto.randomUUID()}`);
|
|
105
|
+
}
|
|
106
|
+
function temporaryIndexEnvironment(indexPath) {
|
|
107
|
+
return { ...workerGitProcessEnvironment(), GIT_INDEX_FILE: indexPath };
|
|
108
|
+
}
|
|
109
|
+
function removeTemporaryIndex(indexPath) {
|
|
110
|
+
fs.rmSync(indexPath, { force: true });
|
|
111
|
+
fs.rmSync(`${indexPath}.lock`, { force: true });
|
|
112
|
+
}
|
|
113
|
+
function sourceEntryPath(sourceRoot, relativePath) {
|
|
114
|
+
const candidate = path.resolve(sourceRoot, ...relativePath.split("/"));
|
|
115
|
+
const relative = path.relative(sourceRoot, candidate);
|
|
116
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
117
|
+
throw new Error(`Workspace merge-projection source escapes its root: ${relativePath}`);
|
|
118
|
+
}
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
function assertSameOpenFile(before, after, sourcePath) {
|
|
122
|
+
if (!after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mode !== after.mode || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
123
|
+
throw new Error(`Working-tree source changed while its merge projection was being synthesized: ${sourcePath}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function hashRegularFile(workspacePath, sourcePath, entry) {
|
|
127
|
+
const descriptor = fs.openSync(sourcePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
128
|
+
try {
|
|
129
|
+
const before = fs.fstatSync(descriptor);
|
|
130
|
+
if (!before.isFile() || before.size !== entry.size || (before.mode & 511) !== (entry.mode & 511)) {
|
|
131
|
+
throw new Error(`Working-tree source changed while its merge projection was being synthesized: ${sourcePath}`);
|
|
132
|
+
}
|
|
133
|
+
const objectId = requireObjectId(
|
|
134
|
+
gitText(workspacePath, ["hash-object", "-w", "--stdin"], `hash workspace projection file ${sourcePath}`, {
|
|
135
|
+
stdin: descriptor
|
|
136
|
+
}),
|
|
137
|
+
`Workspace projection blob for ${sourcePath}`
|
|
138
|
+
);
|
|
139
|
+
const after = fs.fstatSync(descriptor);
|
|
140
|
+
assertSameOpenFile(before, after, sourcePath);
|
|
141
|
+
const pathStatus = fs.lstatSync(sourcePath);
|
|
142
|
+
if (pathStatus.dev !== after.dev || pathStatus.ino !== after.ino || !pathStatus.isFile() || pathStatus.isSymbolicLink()) {
|
|
143
|
+
throw new Error(`Working-tree source changed while its merge projection was being synthesized: ${sourcePath}`);
|
|
144
|
+
}
|
|
145
|
+
return objectId;
|
|
146
|
+
} finally {
|
|
147
|
+
fs.closeSync(descriptor);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function hashSymlink(workspacePath, sourcePath, entry) {
|
|
151
|
+
const status = fs.lstatSync(sourcePath);
|
|
152
|
+
if (!status.isSymbolicLink() || fs.readlinkSync(sourcePath) !== entry.target) {
|
|
153
|
+
throw new Error(`Working-tree source changed while its merge projection was being synthesized: ${sourcePath}`);
|
|
154
|
+
}
|
|
155
|
+
const objectId = requireObjectId(
|
|
156
|
+
gitText(workspacePath, ["hash-object", "-w", "--stdin"], `hash workspace projection symlink ${sourcePath}`, {
|
|
157
|
+
stdin: Buffer.from(entry.target)
|
|
158
|
+
}),
|
|
159
|
+
`Workspace projection symlink blob for ${sourcePath}`
|
|
160
|
+
);
|
|
161
|
+
if (!fs.lstatSync(sourcePath).isSymbolicLink() || fs.readlinkSync(sourcePath) !== entry.target) {
|
|
162
|
+
throw new Error(`Working-tree source changed while its merge projection was being synthesized: ${sourcePath}`);
|
|
163
|
+
}
|
|
164
|
+
return objectId;
|
|
165
|
+
}
|
|
166
|
+
function indexMode(entry) {
|
|
167
|
+
if (entry.kind === "symlink") return "120000";
|
|
168
|
+
return (entry.mode & 73) !== 0 ? "100755" : "100644";
|
|
169
|
+
}
|
|
170
|
+
function indexInfoRecord(mode, objectId, relativePath) {
|
|
171
|
+
return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
|
|
172
|
+
}
|
|
173
|
+
function synthesizeMountTree(input) {
|
|
174
|
+
const sourceRoot = path.resolve(input.sourcePath);
|
|
175
|
+
const entries = inspectWorkingTree(sourceRoot, input.sourceMode);
|
|
176
|
+
const indexPath = temporaryIndexPath(input.workspacePath);
|
|
177
|
+
const environment = temporaryIndexEnvironment(indexPath);
|
|
178
|
+
try {
|
|
179
|
+
git(input.workspacePath, ["read-tree", "--empty"], "initialize workspace projection index", { environment });
|
|
180
|
+
const records = [];
|
|
181
|
+
for (const [relativePath, entry] of [...entries.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
182
|
+
if (entry.kind === "directory") continue;
|
|
183
|
+
const sourcePath = sourceEntryPath(sourceRoot, relativePath);
|
|
184
|
+
const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
|
|
185
|
+
records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
|
|
186
|
+
}
|
|
187
|
+
if (records.length > 0) {
|
|
188
|
+
git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
|
|
189
|
+
environment,
|
|
190
|
+
stdin: Buffer.concat(records)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return requireObjectId(
|
|
194
|
+
gitText(input.workspacePath, ["write-tree"], "write workspace projection mount tree", { environment }),
|
|
195
|
+
"Workspace projection mount tree"
|
|
196
|
+
);
|
|
197
|
+
} finally {
|
|
198
|
+
removeTemporaryIndex(indexPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function nulRecords(content) {
|
|
202
|
+
const records = [];
|
|
203
|
+
let start = 0;
|
|
204
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
205
|
+
if (content[index] !== 0) continue;
|
|
206
|
+
if (index > start) records.push(content.subarray(start, index));
|
|
207
|
+
start = index + 1;
|
|
208
|
+
}
|
|
209
|
+
if (start < content.length) records.push(content.subarray(start));
|
|
210
|
+
return records;
|
|
211
|
+
}
|
|
212
|
+
function indexedSubtreePaths(content, workspaceRelativePath) {
|
|
213
|
+
const prefix = Buffer.from(workspaceRelativePath);
|
|
214
|
+
const descendantPrefix = Buffer.from(`${workspaceRelativePath}/`);
|
|
215
|
+
const records = nulRecords(content).filter(
|
|
216
|
+
(record) => record.equals(prefix) || record.length > descendantPrefix.length && record.subarray(0, descendantPrefix.length).equals(descendantPrefix)
|
|
217
|
+
);
|
|
218
|
+
return records.length > 0 ? Buffer.concat(records.flatMap((record) => [record, Buffer.from([0])])) : Buffer.alloc(0);
|
|
219
|
+
}
|
|
220
|
+
function graftMountTree(input) {
|
|
221
|
+
const indexPath = temporaryIndexPath(input.workspacePath);
|
|
222
|
+
const environment = temporaryIndexEnvironment(indexPath);
|
|
223
|
+
try {
|
|
224
|
+
git(input.workspacePath, ["read-tree", input.basisHead], "read workspace projection basis", { environment });
|
|
225
|
+
const indexedPaths = git(input.workspacePath, ["ls-files", "-z"], "enumerate workspace projection basis", { environment });
|
|
226
|
+
const removedPaths = indexedSubtreePaths(indexedPaths, input.workspaceRelativePath);
|
|
227
|
+
if (removedPaths.length > 0) {
|
|
228
|
+
git(input.workspacePath, ["update-index", "--force-remove", "-z", "--stdin"], "remove prior workspace projection subtree", {
|
|
229
|
+
environment,
|
|
230
|
+
stdin: removedPaths
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
git(
|
|
234
|
+
input.workspacePath,
|
|
235
|
+
["read-tree", "-i", `--prefix=${input.workspaceRelativePath}/`, input.mountTree],
|
|
236
|
+
"graft workspace projection mount tree",
|
|
237
|
+
{ environment }
|
|
238
|
+
);
|
|
239
|
+
return requireObjectId(
|
|
240
|
+
gitText(input.workspacePath, ["write-tree"], "write grafted workspace projection tree", { environment }),
|
|
241
|
+
"Grafted workspace projection tree"
|
|
242
|
+
);
|
|
243
|
+
} finally {
|
|
244
|
+
removeTemporaryIndex(indexPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function synthesizeOursCommit(input) {
|
|
248
|
+
const mountTree = synthesizeMountTree({
|
|
249
|
+
workspacePath: input.workspacePath,
|
|
250
|
+
sourcePath: input.mount.sourcePath,
|
|
251
|
+
sourceMode: input.mount.sourceMode
|
|
252
|
+
});
|
|
253
|
+
const rootTree = graftMountTree({
|
|
254
|
+
workspacePath: input.workspacePath,
|
|
255
|
+
workspaceRelativePath: input.workspaceRelativePath,
|
|
256
|
+
basisHead: input.basisHead,
|
|
257
|
+
mountTree
|
|
258
|
+
});
|
|
259
|
+
const message = JSON.stringify({
|
|
260
|
+
type: "workspace_projection_basis",
|
|
261
|
+
mountId: input.mount.id,
|
|
262
|
+
attemptId: input.attemptId
|
|
263
|
+
});
|
|
264
|
+
return requireObjectId(
|
|
265
|
+
gitText(
|
|
266
|
+
input.workspacePath,
|
|
267
|
+
["commit-tree", rootTree, "-p", input.basisHead, "-m", message],
|
|
268
|
+
`commit synthesized workspace projection for mount ${input.mount.id}`
|
|
269
|
+
),
|
|
270
|
+
`Synthesized workspace projection commit for mount ${input.mount.id}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
function mergeWorkspaceProjectionMount(input) {
|
|
274
|
+
const support = workspaceMergeProjectionSupport();
|
|
275
|
+
if (!support.supported) throw new Error(support.error);
|
|
276
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
277
|
+
const workspaceRelativePath = normalizeWorkspaceRelativePath(input.mount.workspaceRelativePath);
|
|
278
|
+
const basisHead = requireCommit(workspacePath, input.basisHead, `workspace projection basis for mount ${input.mount.id}`);
|
|
279
|
+
const currentHead = requireCommit(workspacePath, input.currentHead, "current workspace projection head");
|
|
280
|
+
const oursCommit = synthesizeOursCommit({
|
|
281
|
+
workspacePath,
|
|
282
|
+
mount: input.mount,
|
|
283
|
+
workspaceRelativePath,
|
|
284
|
+
basisHead,
|
|
285
|
+
attemptId: input.attemptId
|
|
286
|
+
});
|
|
287
|
+
const merged = gitResult(workspacePath, [
|
|
288
|
+
"merge-tree",
|
|
289
|
+
"--write-tree",
|
|
290
|
+
`--merge-base=${basisHead}`,
|
|
291
|
+
"--name-only",
|
|
292
|
+
"-z",
|
|
293
|
+
"--no-messages",
|
|
294
|
+
oursCommit,
|
|
295
|
+
currentHead
|
|
296
|
+
]);
|
|
297
|
+
if (merged.exitCode !== 0 && merged.exitCode !== 1) {
|
|
298
|
+
const detail = merged.stderr.toString().trim() || merged.stdout.toString().trim() || `git exited ${merged.exitCode}`;
|
|
299
|
+
throw new Error(`Merge workspace projection for mount ${input.mount.id}: ${detail}`);
|
|
300
|
+
}
|
|
301
|
+
const records = nulRecords(merged.stdout);
|
|
302
|
+
const resultTree = records.shift()?.toString() ?? "";
|
|
303
|
+
requireObjectId(resultTree, `Merged workspace projection tree for mount ${input.mount.id}`);
|
|
304
|
+
if (merged.exitCode === 0) return { kind: "clean", resultTree, oursCommit };
|
|
305
|
+
const conflictPaths = records.map((record) => record.toString()).sort();
|
|
306
|
+
return {
|
|
307
|
+
kind: "conflict",
|
|
308
|
+
oursCommit,
|
|
309
|
+
conflictPaths,
|
|
310
|
+
error: `Workspace projection for mount ${input.mount.id} conflicted with the current workspace head`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function materializeWorkspaceProjectionTree(input) {
|
|
314
|
+
const workspacePath = path.resolve(input.workspacePath);
|
|
315
|
+
const workspaceRelativePath = normalizeWorkspaceRelativePath(input.workspaceRelativePath);
|
|
316
|
+
const resultTree = requireObjectId(input.resultTree, "Workspace projection result tree");
|
|
317
|
+
const targetPath = path.join(workspacePath, ...workspaceRelativePath.split("/"));
|
|
318
|
+
assertManagedDirectoryPath({
|
|
319
|
+
trustedRoot: workspacePath,
|
|
320
|
+
candidate: targetPath,
|
|
321
|
+
label: "Workspace merge-projection target"
|
|
322
|
+
});
|
|
323
|
+
git(workspacePath, ["cat-file", "-e", `${resultTree}^{tree}`], "resolve workspace merge-projection result tree");
|
|
324
|
+
const subtree = gitResult(workspacePath, ["cat-file", "-e", `${resultTree}:${workspaceRelativePath}`]);
|
|
325
|
+
if (subtree.exitCode !== 0) {
|
|
326
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const subtreeType = gitText(
|
|
330
|
+
workspacePath,
|
|
331
|
+
["cat-file", "-t", `${resultTree}:${workspaceRelativePath}`],
|
|
332
|
+
"inspect workspace merge-projection result subtree"
|
|
333
|
+
);
|
|
334
|
+
if (subtreeType !== "tree") {
|
|
335
|
+
throw new Error(`Workspace merge-projection result is not a directory at ${workspaceRelativePath}`);
|
|
336
|
+
}
|
|
337
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
338
|
+
git(
|
|
339
|
+
workspacePath,
|
|
340
|
+
["checkout", "--no-recurse-submodules", resultTree, "--", `:(literal)${workspaceRelativePath}`],
|
|
341
|
+
"materialize merged workspace projection subtree"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const workspaceMergeProjectionTestHarness = {
|
|
345
|
+
gitVersionIsSupported,
|
|
346
|
+
resetSupportProbe() {
|
|
347
|
+
cachedWorkspaceMergeProjectionSupport = null;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
export {
|
|
351
|
+
materializeWorkspaceProjectionTree,
|
|
352
|
+
mergeWorkspaceProjectionMount,
|
|
353
|
+
workspaceMergeProjectionSupport,
|
|
354
|
+
workspaceMergeProjectionTestHarness
|
|
355
|
+
};
|
package/dist/types/main.d.ts
CHANGED
|
@@ -114,6 +114,46 @@ declare function assertWorkerChildAdmission(input: {
|
|
|
114
114
|
workspaceConfigured: boolean;
|
|
115
115
|
runtimeHealthy: boolean;
|
|
116
116
|
}): void;
|
|
117
|
+
type WorkerPty = {
|
|
118
|
+
write(data: string): void;
|
|
119
|
+
resize(cols: number, rows: number): void;
|
|
120
|
+
kill(): void;
|
|
121
|
+
terminate(): Promise<void>;
|
|
122
|
+
};
|
|
123
|
+
type ActiveWorkerPty = WorkerPty & {
|
|
124
|
+
target: WorkerSessionTarget;
|
|
125
|
+
foregroundBusy: boolean;
|
|
126
|
+
lastInputAt: number;
|
|
127
|
+
releaseWorkspaceMutation?: () => void;
|
|
128
|
+
};
|
|
129
|
+
type StagedPtyEnvFiles = {
|
|
130
|
+
paths: string[];
|
|
131
|
+
resolvedByRequestedPath: Map<string, string>;
|
|
132
|
+
};
|
|
133
|
+
declare function resolvePtyEnvFilePath(requestedPath: string, temporaryRoot?: string): string;
|
|
134
|
+
declare function removePtyEnvFiles(paths: readonly string[]): void;
|
|
135
|
+
declare function stagePtyEnvFiles(envFiles: readonly {
|
|
136
|
+
path: string;
|
|
137
|
+
content: string;
|
|
138
|
+
mode?: number;
|
|
139
|
+
}[] | undefined, temporaryRoot?: string): StagedPtyEnvFiles;
|
|
140
|
+
declare function resolvePtyEnvFileReferences(env: Record<string, string>, staged: StagedPtyEnvFiles): Record<string, string>;
|
|
141
|
+
declare function workerCommandHasWorkspaceEffect(message: {
|
|
142
|
+
workspaceEffect?: "none";
|
|
143
|
+
}): boolean;
|
|
144
|
+
declare function workerPtyIsWorkspaceBusy(pty: Pick<ActiveWorkerPty, "foregroundBusy" | "lastInputAt">, now?: number, foregroundIdleEnabled?: boolean): boolean;
|
|
145
|
+
declare function parseLinuxPtyForegroundBusy(stat: string): boolean | null;
|
|
146
|
+
export declare const workerPtyTestHarness: {
|
|
147
|
+
temporaryPathPrefix: string;
|
|
148
|
+
inputBusyGraceMs: number;
|
|
149
|
+
resolveEnvFilePath: typeof resolvePtyEnvFilePath;
|
|
150
|
+
stageEnvFiles: typeof stagePtyEnvFiles;
|
|
151
|
+
removeEnvFiles: typeof removePtyEnvFiles;
|
|
152
|
+
resolveEnvFileReferences: typeof resolvePtyEnvFileReferences;
|
|
153
|
+
commandHasWorkspaceEffect: typeof workerCommandHasWorkspaceEffect;
|
|
154
|
+
ptyIsWorkspaceBusy: typeof workerPtyIsWorkspaceBusy;
|
|
155
|
+
parseLinuxForegroundBusy: typeof parseLinuxPtyForegroundBusy;
|
|
156
|
+
};
|
|
117
157
|
export type WorkerBuiltInToolPaths = {
|
|
118
158
|
artifactsDir?: string;
|
|
119
159
|
plansDir?: string;
|
|
@@ -375,6 +415,29 @@ export declare function prepareShellEnvForTarget(input: {
|
|
|
375
415
|
artifactRoot: string;
|
|
376
416
|
planRoot: string;
|
|
377
417
|
}): Promise<Record<string, string>>;
|
|
418
|
+
declare function createNodePtyBridge(options: {
|
|
419
|
+
file: string;
|
|
420
|
+
args: string[];
|
|
421
|
+
ptyOptions: {
|
|
422
|
+
name: string;
|
|
423
|
+
cols: number;
|
|
424
|
+
rows: number;
|
|
425
|
+
cwd: string;
|
|
426
|
+
env: Record<string, string | undefined>;
|
|
427
|
+
};
|
|
428
|
+
onOpened: (pid: number) => void;
|
|
429
|
+
onForeground: (busy: boolean) => void;
|
|
430
|
+
onOutput: (data: string) => void;
|
|
431
|
+
onExit: (event: {
|
|
432
|
+
exitCode: number;
|
|
433
|
+
signal?: number | string;
|
|
434
|
+
}) => void;
|
|
435
|
+
onError: (error: Error) => void;
|
|
436
|
+
onTerminationError: (error: Error) => void;
|
|
437
|
+
}): WorkerPty;
|
|
438
|
+
export declare const workerPtyBridgeTestHarness: {
|
|
439
|
+
create: typeof createNodePtyBridge;
|
|
440
|
+
};
|
|
378
441
|
export declare function resolveHostShell(command?: string, platform?: NodeJS.Platform): {
|
|
379
442
|
file: string;
|
|
380
443
|
args: string[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type WorkingTreeSourceMode = "all" | "git";
|
|
2
2
|
export type WorkingTreeDeletionMode = "all" | "git";
|
|
3
3
|
export type WorkingTreeMirrorDurability = "per_entry" | "deferred_private_staging";
|
|
4
|
-
type TreeEntry = {
|
|
4
|
+
export type TreeEntry = {
|
|
5
5
|
kind: "directory";
|
|
6
6
|
mode: number;
|
|
7
7
|
} | {
|
|
@@ -35,4 +35,3 @@ export declare function mirrorWorkingTree(input: {
|
|
|
35
35
|
}): {
|
|
36
36
|
paths: string[];
|
|
37
37
|
};
|
|
38
|
-
export {};
|
|
@@ -12,9 +12,10 @@ type WorkspaceSyncCompletionBarrier = {
|
|
|
12
12
|
afterCurrent(): Promise<void>;
|
|
13
13
|
};
|
|
14
14
|
/**
|
|
15
|
-
* A
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* A project command may reserve immediately: per-mount hydration rechecks its
|
|
16
|
+
* busy predicate immediately before touching visible bytes. A visible-projects
|
|
17
|
+
* workspace command spans every project, so it keeps the whole-cycle barrier.
|
|
18
|
+
* Canonical remediation is serialized separately by the mutation gate.
|
|
18
19
|
*/
|
|
19
20
|
export declare function reserveWorkspaceCommandAfterCurrentSync(target: WorkspaceCommandTarget, coordinator: WorkspaceSyncCompletionBarrier, reserve: () => void): Promise<void>;
|
|
20
21
|
/**
|
|
@@ -29,6 +29,8 @@ export type WorkspaceGitMount = {
|
|
|
29
29
|
/** Remove the outer-workspace subtree when the mounted source was intentionally deleted. */
|
|
30
30
|
deleteWhenSourceMissing?: boolean;
|
|
31
31
|
busy?: () => boolean;
|
|
32
|
+
/** Monotonic/opaque visible-mutation token used to catch short writes that begin and end between busy checks. */
|
|
33
|
+
mutationToken?: () => string;
|
|
32
34
|
/** Activity-only fence used before checkout readiness is established on reconnect. */
|
|
33
35
|
busyForRecovery?: () => boolean;
|
|
34
36
|
};
|
|
@@ -50,6 +52,7 @@ export type WorkspaceGitSyncResult = {
|
|
|
50
52
|
local: string;
|
|
51
53
|
remote: string;
|
|
52
54
|
};
|
|
55
|
+
conflictKind?: "integration_rebase" | "projection_merge";
|
|
53
56
|
error?: string;
|
|
54
57
|
};
|
|
55
58
|
declare function gitCommandArgs(args: string[]): string[];
|
|
@@ -74,6 +77,7 @@ export declare function ensureWorkspaceGitClone(input: {
|
|
|
74
77
|
name: string;
|
|
75
78
|
email: string;
|
|
76
79
|
};
|
|
80
|
+
preserveResolutionInProgress?: boolean;
|
|
77
81
|
}): {
|
|
78
82
|
localHead: string | null;
|
|
79
83
|
remoteHead: string | null;
|
|
@@ -88,6 +92,8 @@ export declare function hydrateWorkspaceGitMounts(workspacePath: string, mounts:
|
|
|
88
92
|
export declare function recoverWorkspaceGitHydration(workspacePath: string, mounts: readonly WorkspaceGitMount[], options?: {
|
|
89
93
|
ignoreBusy?: boolean;
|
|
90
94
|
deferMountIds?: ReadonlySet<string>;
|
|
95
|
+
preserveResolutionInProgress?: boolean;
|
|
96
|
+
preserveStaleBases?: boolean;
|
|
91
97
|
}): void;
|
|
92
98
|
export declare function resetWorkspaceGit(input: {
|
|
93
99
|
workspacePath: string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type WorkingTreeSourceMode } from "./working-tree-mirror";
|
|
2
|
+
export type WorkspaceMergeProjectionSupport = {
|
|
3
|
+
supported: true;
|
|
4
|
+
} | {
|
|
5
|
+
supported: false;
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
export type WorkspaceMergeProjectionMount = {
|
|
9
|
+
id: string;
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
workspaceRelativePath: string;
|
|
12
|
+
sourceMode: WorkingTreeSourceMode;
|
|
13
|
+
};
|
|
14
|
+
export type WorkspaceMergeProjectionResult = {
|
|
15
|
+
kind: "clean";
|
|
16
|
+
resultTree: string;
|
|
17
|
+
oursCommit: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "conflict";
|
|
20
|
+
oursCommit: string;
|
|
21
|
+
conflictPaths: string[];
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
declare function gitVersionIsSupported(version: string): boolean;
|
|
25
|
+
export declare function workspaceMergeProjectionSupport(): WorkspaceMergeProjectionSupport;
|
|
26
|
+
export declare function mergeWorkspaceProjectionMount(input: {
|
|
27
|
+
workspacePath: string;
|
|
28
|
+
mount: WorkspaceMergeProjectionMount;
|
|
29
|
+
basisHead: string;
|
|
30
|
+
currentHead: string;
|
|
31
|
+
attemptId: string;
|
|
32
|
+
}): WorkspaceMergeProjectionResult;
|
|
33
|
+
export declare function materializeWorkspaceProjectionTree(input: {
|
|
34
|
+
workspacePath: string;
|
|
35
|
+
workspaceRelativePath: string;
|
|
36
|
+
resultTree: string;
|
|
37
|
+
}): void;
|
|
38
|
+
export declare const workspaceMergeProjectionTestHarness: {
|
|
39
|
+
gitVersionIsSupported: typeof gitVersionIsSupported;
|
|
40
|
+
resetSupportProbe(): void;
|
|
41
|
+
};
|
|
42
|
+
export {};
|