@ricsam/r5d-worker 0.0.10 → 0.0.12
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/dist/cjs/main.cjs +508 -215
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +505 -214
- package/dist/mjs/package.json +1 -1
- package/dist/types/main.d.ts +9 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -29,14 +29,15 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
30
|
var main_exports = {};
|
|
31
31
|
__export(main_exports, {
|
|
32
|
-
ensureVisibleGitCheckout: () => ensureVisibleGitCheckout
|
|
32
|
+
ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
|
|
33
|
+
isArtifactEnvPath: () => isArtifactEnvPath,
|
|
34
|
+
syncSessionArtifacts: () => syncSessionArtifacts
|
|
33
35
|
});
|
|
34
36
|
module.exports = __toCommonJS(main_exports);
|
|
35
37
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
36
|
-
var import_node_os = __toESM(require("node:os"), 1);
|
|
37
38
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
38
39
|
var import_node_crypto = require("node:crypto");
|
|
39
|
-
var
|
|
40
|
+
var import_node_os = __toESM(require("node:os"), 1);
|
|
40
41
|
var import_node_child_process = require("node:child_process");
|
|
41
42
|
var import_git_identity = require("./git-identity.cjs");
|
|
42
43
|
const import_meta = {};
|
|
@@ -45,6 +46,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
|
45
46
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
46
47
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
47
48
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
49
|
+
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
48
50
|
const MAX_LINE_LENGTH = 2e3;
|
|
49
51
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
50
52
|
const R5D_REMOTE_NAME = "r5d";
|
|
@@ -64,6 +66,9 @@ function defaultProjectsRoot() {
|
|
|
64
66
|
function defaultSyncRoot() {
|
|
65
67
|
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "sync");
|
|
66
68
|
}
|
|
69
|
+
function defaultArtifactRoot() {
|
|
70
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "artifacts");
|
|
71
|
+
}
|
|
67
72
|
function printHelp() {
|
|
68
73
|
process.stdout.write(`Usage:
|
|
69
74
|
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
@@ -77,6 +82,7 @@ Options:
|
|
|
77
82
|
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
78
83
|
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
79
84
|
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
85
|
+
--artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
|
|
80
86
|
-v, --version Show r5d-worker version
|
|
81
87
|
-h, --help Show this help
|
|
82
88
|
`);
|
|
@@ -193,6 +199,11 @@ function parseArgs(argv) {
|
|
|
193
199
|
options.syncRoot = syncRoot;
|
|
194
200
|
continue;
|
|
195
201
|
}
|
|
202
|
+
const artifactRoot = readOption("--artifact-root");
|
|
203
|
+
if (artifactRoot !== void 0) {
|
|
204
|
+
options.artifactRoot = artifactRoot;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
196
207
|
rest.push(arg);
|
|
197
208
|
}
|
|
198
209
|
if (options.version) {
|
|
@@ -235,6 +246,170 @@ async function readResponseText(response) {
|
|
|
235
246
|
return "";
|
|
236
247
|
}
|
|
237
248
|
}
|
|
249
|
+
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
250
|
+
function validateArtifactSessionId(sessionId) {
|
|
251
|
+
if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
252
|
+
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
256
|
+
const relative = import_node_path.default.relative(rootPath, candidatePath);
|
|
257
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
258
|
+
throw new Error(`${label} escapes the artifact directory`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
262
|
+
validateArtifactSessionId(sessionId);
|
|
263
|
+
return import_node_path.default.join(artifactRoot, sessionId);
|
|
264
|
+
}
|
|
265
|
+
function artifactEnv(artifactRoot, sessionId) {
|
|
266
|
+
if (!sessionId) {
|
|
267
|
+
return {};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function isArtifactEnvPath(filePath) {
|
|
274
|
+
return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
|
|
275
|
+
}
|
|
276
|
+
function artifactApiBasePath(projectId, branchName, sessionId) {
|
|
277
|
+
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
|
|
278
|
+
}
|
|
279
|
+
function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
|
|
280
|
+
return new URL(artifactApiBasePath(projectId, branchName, sessionId), baseUrl);
|
|
281
|
+
}
|
|
282
|
+
function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
|
|
283
|
+
return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
284
|
+
}
|
|
285
|
+
function sha256Path(filePath) {
|
|
286
|
+
return (0, import_node_crypto.createHash)("sha256").update(import_node_fs.default.readFileSync(filePath)).digest("hex");
|
|
287
|
+
}
|
|
288
|
+
function parseSessionArtifactManifest(value) {
|
|
289
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.artifacts)) {
|
|
290
|
+
throw new Error("Artifact manifest response is invalid");
|
|
291
|
+
}
|
|
292
|
+
return value.artifacts.map((entry) => {
|
|
293
|
+
if (!entry || typeof entry !== "object") {
|
|
294
|
+
throw new Error("Artifact manifest entry is invalid");
|
|
295
|
+
}
|
|
296
|
+
const artifact = entry;
|
|
297
|
+
if (typeof artifact.filename !== "string" || artifact.filename.includes("/") || artifact.filename.includes("\\")) {
|
|
298
|
+
throw new Error("Artifact manifest entry has an invalid filename");
|
|
299
|
+
}
|
|
300
|
+
if (typeof artifact.size !== "number" || !Number.isFinite(artifact.size) || artifact.size < 0) {
|
|
301
|
+
throw new Error(`Artifact manifest entry has an invalid size: ${artifact.filename}`);
|
|
302
|
+
}
|
|
303
|
+
if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
|
|
304
|
+
throw new Error(`Artifact manifest entry has an invalid sha256: ${artifact.filename}`);
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
filename: artifact.filename,
|
|
308
|
+
size: artifact.size,
|
|
309
|
+
sha256: artifact.sha256.toLowerCase()
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async function fetchSessionArtifactManifest(input) {
|
|
314
|
+
const response = await fetch(artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId), {
|
|
315
|
+
headers: {
|
|
316
|
+
Authorization: `Bearer ${input.token}`
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
const detail = (await readResponseText(response)).trim();
|
|
321
|
+
throw new Error(`Failed to fetch artifact manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
322
|
+
}
|
|
323
|
+
return parseSessionArtifactManifest(await response.json());
|
|
324
|
+
}
|
|
325
|
+
async function downloadSessionArtifact(input) {
|
|
326
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
327
|
+
const targetPath = import_node_path.default.join(targetDir, input.artifact.filename);
|
|
328
|
+
assertInsideRoot(targetDir, targetPath, "Artifact path");
|
|
329
|
+
const response = await fetch(
|
|
330
|
+
artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, input.artifact.filename),
|
|
331
|
+
{
|
|
332
|
+
headers: {
|
|
333
|
+
Authorization: `Bearer ${input.token}`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
const detail = (await readResponseText(response)).trim();
|
|
339
|
+
throw new Error(`Failed to download artifact ${input.artifact.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
340
|
+
}
|
|
341
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
342
|
+
const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
343
|
+
if (receivedHash !== input.artifact.sha256) {
|
|
344
|
+
throw new Error(`Downloaded artifact ${input.artifact.filename} failed sha256 verification`);
|
|
345
|
+
}
|
|
346
|
+
if (buffer.length !== input.artifact.size) {
|
|
347
|
+
throw new Error(`Downloaded artifact ${input.artifact.filename} has unexpected size`);
|
|
348
|
+
}
|
|
349
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
350
|
+
import_node_fs.default.writeFileSync(tempPath, buffer);
|
|
351
|
+
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
352
|
+
}
|
|
353
|
+
async function syncSessionArtifacts(input) {
|
|
354
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
355
|
+
import_node_fs.default.mkdirSync(targetDir, { recursive: true });
|
|
356
|
+
const artifacts = await fetchSessionArtifactManifest(input);
|
|
357
|
+
const manifestNames = new Set(artifacts.map((artifact) => artifact.filename));
|
|
358
|
+
for (const artifact of artifacts) {
|
|
359
|
+
const localPath = import_node_path.default.join(targetDir, artifact.filename);
|
|
360
|
+
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
361
|
+
if (import_node_fs.default.existsSync(localPath)) {
|
|
362
|
+
const stats = import_node_fs.default.statSync(localPath);
|
|
363
|
+
if (stats.isFile() && stats.size === artifact.size && sha256Path(localPath) === artifact.sha256) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
|
|
367
|
+
}
|
|
368
|
+
await downloadSessionArtifact({ ...input, artifact });
|
|
369
|
+
}
|
|
370
|
+
for (const localName of import_node_fs.default.readdirSync(targetDir)) {
|
|
371
|
+
if (!manifestNames.has(localName)) {
|
|
372
|
+
const localPath = import_node_path.default.join(targetDir, localName);
|
|
373
|
+
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
374
|
+
import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return targetDir;
|
|
378
|
+
}
|
|
379
|
+
async function resolveArtifactEnvFilePath(input) {
|
|
380
|
+
if (!isArtifactEnvPath(input.filePath)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (!input.sessionId) {
|
|
384
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
|
|
385
|
+
}
|
|
386
|
+
if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
|
|
387
|
+
throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
|
|
388
|
+
}
|
|
389
|
+
const targetDir = await syncSessionArtifacts({
|
|
390
|
+
baseUrl: input.baseUrl,
|
|
391
|
+
token: input.token,
|
|
392
|
+
projectId: input.projectId,
|
|
393
|
+
branchName: input.branchName,
|
|
394
|
+
sessionId: input.sessionId,
|
|
395
|
+
artifactRoot: input.artifactRoot
|
|
396
|
+
});
|
|
397
|
+
const relativePath = import_node_path.default.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
|
|
398
|
+
if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
|
|
399
|
+
throw new Error(`Invalid artifact path: ${input.filePath}`);
|
|
400
|
+
}
|
|
401
|
+
const absolutePath = import_node_path.default.resolve(targetDir, ...relativePath.split("/"));
|
|
402
|
+
assertInsideRoot(targetDir, absolutePath, "Artifact path");
|
|
403
|
+
return {
|
|
404
|
+
absolutePath,
|
|
405
|
+
virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function rejectArtifactWritePath(filePath) {
|
|
409
|
+
if (isArtifactEnvPath(filePath)) {
|
|
410
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
238
413
|
async function verifyR5dctlAuth(baseUrl, token) {
|
|
239
414
|
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
240
415
|
let response;
|
|
@@ -746,16 +921,36 @@ function formatLineNumberedContent(content, offset, limit) {
|
|
|
746
921
|
const endLine = Math.min(startLine + readLimit - 1, totalLines);
|
|
747
922
|
const selectedLines = allLines.slice(startLine - 1, endLine);
|
|
748
923
|
const padWidth = String(endLine).length;
|
|
749
|
-
const formattedLines =
|
|
924
|
+
const formattedLines = [];
|
|
925
|
+
let actualEndLine = endLine;
|
|
926
|
+
let budgetTruncated = false;
|
|
927
|
+
for (let index = 0; index < selectedLines.length; index += 1) {
|
|
928
|
+
const line = selectedLines[index] ?? "";
|
|
750
929
|
const lineNumber = startLine + index;
|
|
751
930
|
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
752
|
-
|
|
753
|
-
|
|
931
|
+
const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
|
|
932
|
+
const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
|
|
933
|
+
const currentLength = formattedLines.join("\n").length;
|
|
934
|
+
if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
|
|
935
|
+
budgetTruncated = true;
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
formattedLines.push(formattedLine);
|
|
939
|
+
actualEndLine = lineNumber;
|
|
940
|
+
}
|
|
941
|
+
let formattedContent = formattedLines.join("\n");
|
|
942
|
+
if (budgetTruncated) {
|
|
943
|
+
formattedContent = `${formattedContent}
|
|
944
|
+
... (truncated by byte budget)`;
|
|
945
|
+
}
|
|
946
|
+
const truncated = budgetTruncated || actualEndLine < totalLines;
|
|
754
947
|
return {
|
|
755
|
-
content:
|
|
948
|
+
content: formattedContent,
|
|
756
949
|
totalLines,
|
|
757
950
|
startLine,
|
|
758
|
-
endLine
|
|
951
|
+
endLine: actualEndLine,
|
|
952
|
+
truncated,
|
|
953
|
+
continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
|
|
759
954
|
};
|
|
760
955
|
}
|
|
761
956
|
function readPngDimensions(bytes) {
|
|
@@ -822,7 +1017,51 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
822
1017
|
}
|
|
823
1018
|
return dimensions;
|
|
824
1019
|
}
|
|
1020
|
+
const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
1021
|
+
async function withFileMutationQueue(key, operation) {
|
|
1022
|
+
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
1023
|
+
let release;
|
|
1024
|
+
const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
|
|
1025
|
+
release = resolve;
|
|
1026
|
+
}));
|
|
1027
|
+
fileMutationQueues.set(key, current);
|
|
1028
|
+
await previous.catch(() => void 0);
|
|
1029
|
+
try {
|
|
1030
|
+
return await operation();
|
|
1031
|
+
} finally {
|
|
1032
|
+
release();
|
|
1033
|
+
if (fileMutationQueues.get(key) === current) {
|
|
1034
|
+
fileMutationQueues.delete(key);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function mutationQueueKey(projectId, branchName, filePath) {
|
|
1039
|
+
return `${projectId}:${branchName}:${import_node_path.default.posix.normalize(filePath)}`;
|
|
1040
|
+
}
|
|
825
1041
|
async function executeReadFileOperation(input) {
|
|
1042
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1043
|
+
filePath: input.message.filePath,
|
|
1044
|
+
baseUrl: input.baseUrl,
|
|
1045
|
+
token: input.token,
|
|
1046
|
+
projectId: input.projectId,
|
|
1047
|
+
branchName: input.message.branchName,
|
|
1048
|
+
sessionId: input.message.sessionId,
|
|
1049
|
+
artifactRoot: input.artifactRoot
|
|
1050
|
+
});
|
|
1051
|
+
if (artifact) {
|
|
1052
|
+
if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
|
|
1053
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1054
|
+
}
|
|
1055
|
+
const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1056
|
+
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
1057
|
+
return {
|
|
1058
|
+
type: "read",
|
|
1059
|
+
kind: "text",
|
|
1060
|
+
file: artifact.virtualPath,
|
|
1061
|
+
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
1062
|
+
...formatted2
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
826
1065
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
827
1066
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
828
1067
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -831,222 +1070,242 @@ async function executeReadFileOperation(input) {
|
|
|
831
1070
|
const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
832
1071
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
833
1072
|
return {
|
|
834
|
-
type: "
|
|
1073
|
+
type: "read",
|
|
1074
|
+
kind: "text",
|
|
835
1075
|
file: resolved.virtualPath,
|
|
836
1076
|
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
837
1077
|
...formatted
|
|
838
1078
|
};
|
|
839
1079
|
}
|
|
840
|
-
async function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
};
|
|
1080
|
+
async function executeWriteFileOperation(input) {
|
|
1081
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
1082
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1083
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1084
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1085
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
|
|
1086
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
1087
|
+
return {
|
|
1088
|
+
type: "write",
|
|
1089
|
+
file: resolved.virtualPath,
|
|
1090
|
+
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
853
1093
|
}
|
|
854
1094
|
async function executeEditFileOperation(input) {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
)
|
|
1095
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
1096
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1097
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1098
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1099
|
+
if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
|
|
1100
|
+
throw new Error("edit requires at least one replacement");
|
|
1101
|
+
}
|
|
1102
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
1103
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1104
|
+
}
|
|
1105
|
+
const originalContent = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
|
|
1106
|
+
const ranges = input.message.edits.map((edit, index) => {
|
|
1107
|
+
if (edit.oldText === edit.newText) {
|
|
1108
|
+
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
1109
|
+
}
|
|
1110
|
+
if (edit.oldText.length === 0) {
|
|
1111
|
+
throw new Error(`edits[${index}].oldText cannot be empty`);
|
|
1112
|
+
}
|
|
1113
|
+
const occurrences = originalContent.split(edit.oldText).length - 1;
|
|
1114
|
+
if (occurrences === 0) {
|
|
1115
|
+
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1116
|
+
}
|
|
1117
|
+
if (occurrences > 1) {
|
|
1118
|
+
throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
|
|
1119
|
+
}
|
|
1120
|
+
const start = originalContent.indexOf(edit.oldText);
|
|
1121
|
+
return { index, start, end: start + edit.oldText.length, edit };
|
|
1122
|
+
});
|
|
1123
|
+
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
|
|
1124
|
+
for (let index = 1; index < sortedRanges.length; index += 1) {
|
|
1125
|
+
const previous = sortedRanges[index - 1];
|
|
1126
|
+
const current = sortedRanges[index];
|
|
1127
|
+
if (current.start < previous.end) {
|
|
1128
|
+
throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
|
|
1129
|
+
}
|
|
875
1130
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1131
|
+
let nextContent = originalContent;
|
|
1132
|
+
for (const range of [...sortedRanges].reverse()) {
|
|
1133
|
+
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
1134
|
+
}
|
|
1135
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
1136
|
+
return {
|
|
1137
|
+
type: "edit",
|
|
1138
|
+
file: resolved.virtualPath,
|
|
1139
|
+
edits: input.message.edits.length
|
|
1140
|
+
};
|
|
1141
|
+
});
|
|
883
1142
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1143
|
+
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
1144
|
+
function normalizeToolLimit(value, fallback, max) {
|
|
1145
|
+
if (!Number.isFinite(value) || value === void 0) return fallback;
|
|
1146
|
+
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
1147
|
+
}
|
|
1148
|
+
function escapeRegex(value) {
|
|
1149
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1150
|
+
}
|
|
1151
|
+
function wildcardToRegex(pattern) {
|
|
1152
|
+
const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
1153
|
+
return new RegExp(`^${escaped}$`);
|
|
1154
|
+
}
|
|
1155
|
+
function normalizeFindPattern(pattern) {
|
|
1156
|
+
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
1157
|
+
}
|
|
1158
|
+
function toVirtualPath(branchPath, absolutePath) {
|
|
1159
|
+
const relative = import_node_path.default.relative(branchPath, absolutePath).split(import_node_path.default.sep).join(import_node_path.default.posix.sep);
|
|
1160
|
+
return relative ? `/${relative}` : "/";
|
|
1161
|
+
}
|
|
1162
|
+
function walkProjectEntries(branchPath, startPath) {
|
|
1163
|
+
const entries = [];
|
|
1164
|
+
const visit = (absolutePath) => {
|
|
1165
|
+
const stat = import_node_fs.default.statSync(absolutePath);
|
|
1166
|
+
const virtualPath = toVirtualPath(branchPath, absolutePath);
|
|
1167
|
+
entries.push({ absolutePath, virtualPath, stat });
|
|
1168
|
+
if (!stat.isDirectory()) return;
|
|
1169
|
+
const children = import_node_fs.default.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1170
|
+
for (const child of children) {
|
|
1171
|
+
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
1172
|
+
visit(import_node_path.default.join(absolutePath, child.name));
|
|
891
1173
|
}
|
|
892
|
-
}
|
|
893
|
-
|
|
1174
|
+
};
|
|
1175
|
+
visit(startPath);
|
|
1176
|
+
return entries;
|
|
894
1177
|
}
|
|
895
|
-
function
|
|
896
|
-
|
|
897
|
-
let patchPath = decodePatchPath(withoutMetadata);
|
|
898
|
-
if (patchPath === "/dev/null") {
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
902
|
-
patchPath = patchPath.slice(2);
|
|
903
|
-
}
|
|
904
|
-
if (!patchPath || import_node_path.default.isAbsolute(patchPath)) {
|
|
905
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
906
|
-
}
|
|
907
|
-
const normalized = import_node_path.default.posix.normalize(patchPath);
|
|
908
|
-
if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
909
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
910
|
-
}
|
|
911
|
-
return normalized;
|
|
1178
|
+
function isProbablyText(bytes) {
|
|
1179
|
+
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
912
1180
|
}
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1181
|
+
async function executeGrepOperation(input) {
|
|
1182
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1183
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1184
|
+
if (!import_node_fs.default.existsSync(start.absolutePath)) {
|
|
1185
|
+
throw new Error(`Path not found: ${start.virtualPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
|
|
1188
|
+
const flags = input.message.caseSensitive === false ? "i" : "";
|
|
1189
|
+
const regex = new RegExp(input.message.pattern, flags);
|
|
1190
|
+
const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
|
|
1191
|
+
const lines = [];
|
|
1192
|
+
let matchCount = 0;
|
|
1193
|
+
let truncated = false;
|
|
1194
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1195
|
+
if (!entry.stat.isFile()) continue;
|
|
1196
|
+
const repoRelative = entry.virtualPath.slice(1);
|
|
1197
|
+
if (globRegex && !globRegex.test(repoRelative)) continue;
|
|
1198
|
+
const bytes = import_node_fs.default.readFileSync(entry.absolutePath);
|
|
1199
|
+
if (!isProbablyText(bytes)) continue;
|
|
1200
|
+
const fileLines = bytes.toString("utf8").split(/\r?\n/);
|
|
1201
|
+
for (let index = 0; index < fileLines.length; index += 1) {
|
|
1202
|
+
const line = fileLines[index] ?? "";
|
|
1203
|
+
regex.lastIndex = 0;
|
|
1204
|
+
const match = regex.exec(line);
|
|
1205
|
+
if (!match) continue;
|
|
1206
|
+
matchCount += 1;
|
|
1207
|
+
if (lines.length < limit) {
|
|
1208
|
+
const column = Math.max(1, (match.index ?? 0) + 1);
|
|
1209
|
+
lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
|
|
1210
|
+
} else {
|
|
1211
|
+
truncated = true;
|
|
1212
|
+
}
|
|
940
1213
|
}
|
|
941
|
-
index += 1;
|
|
942
|
-
}
|
|
943
|
-
throw new Error("Unterminated quoted path in patch header");
|
|
944
|
-
}
|
|
945
|
-
function parseDiffGitPaths(line) {
|
|
946
|
-
if (!line.startsWith("diff --git ")) {
|
|
947
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
948
|
-
}
|
|
949
|
-
const rest = line.slice("diff --git ".length);
|
|
950
|
-
const first = readDiffPathToken(rest, 0);
|
|
951
|
-
const second = readDiffPathToken(rest, first.nextIndex);
|
|
952
|
-
if (rest.slice(second.nextIndex).trim()) {
|
|
953
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
954
1214
|
}
|
|
1215
|
+
const hint = truncated ? `
|
|
1216
|
+
... truncated. Narrow path/glob or increase limit to continue.` : "";
|
|
955
1217
|
return {
|
|
956
|
-
|
|
957
|
-
|
|
1218
|
+
type: "grep",
|
|
1219
|
+
content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
|
|
1220
|
+
matchCount,
|
|
1221
|
+
truncated
|
|
958
1222
|
};
|
|
959
1223
|
}
|
|
960
|
-
function
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}
|
|
986
|
-
if (line.startsWith("new file mode ")) {
|
|
987
|
-
current.operation = "add";
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
if (line.startsWith("deleted file mode ")) {
|
|
991
|
-
current.operation = "delete";
|
|
992
|
-
if (current.oldPath) {
|
|
993
|
-
current.path = current.oldPath;
|
|
994
|
-
delete current.oldPath;
|
|
995
|
-
}
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
if (line.startsWith("rename from ")) {
|
|
999
|
-
current.operation = "move";
|
|
1000
|
-
current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
if (line.startsWith("rename to ")) {
|
|
1004
|
-
current.operation = "move";
|
|
1005
|
-
current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
if (line.startsWith("@@")) {
|
|
1009
|
-
current.hunks += 1;
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1013
|
-
current.additions += 1;
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1017
|
-
current.deletions += 1;
|
|
1224
|
+
async function executeFindOperation(input) {
|
|
1225
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1226
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1227
|
+
if (!import_node_fs.default.existsSync(start.absolutePath) || !import_node_fs.default.statSync(start.absolutePath).isDirectory()) {
|
|
1228
|
+
throw new Error(`Directory not found: ${start.virtualPath}`);
|
|
1229
|
+
}
|
|
1230
|
+
const pattern = normalizeFindPattern(input.message.pattern ?? "*");
|
|
1231
|
+
const matcher = wildcardToRegex(pattern);
|
|
1232
|
+
const entryType = input.message.entryType ?? "file";
|
|
1233
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1234
|
+
const matches = [];
|
|
1235
|
+
let count = 0;
|
|
1236
|
+
let truncated = false;
|
|
1237
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1238
|
+
if (entry.virtualPath === start.virtualPath) continue;
|
|
1239
|
+
const isDirectory = entry.stat.isDirectory();
|
|
1240
|
+
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
1241
|
+
if (entryType === "directory" && !isDirectory) continue;
|
|
1242
|
+
const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
|
|
1243
|
+
if (!matcher.test(import_node_path.default.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
|
|
1244
|
+
count += 1;
|
|
1245
|
+
if (matches.length < limit) {
|
|
1246
|
+
matches.push(label);
|
|
1247
|
+
} else {
|
|
1248
|
+
truncated = true;
|
|
1018
1249
|
}
|
|
1019
1250
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1251
|
+
const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
|
|
1252
|
+
return {
|
|
1253
|
+
type: "find",
|
|
1254
|
+
content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
|
|
1255
|
+
count,
|
|
1256
|
+
truncated
|
|
1257
|
+
};
|
|
1024
1258
|
}
|
|
1025
|
-
async function
|
|
1259
|
+
async function executeLsOperation(input) {
|
|
1026
1260
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
import_node_fs.default.rmSync(patchPath, { force: true });
|
|
1037
|
-
}
|
|
1261
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1262
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isDirectory()) {
|
|
1263
|
+
throw new Error(`Directory not found: ${resolved.virtualPath}`);
|
|
1264
|
+
}
|
|
1265
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1266
|
+
const entries = import_node_fs.default.readdirSync(resolved.absolutePath, { withFileTypes: true }).filter((entry) => !IGNORED_ENTRY_NAMES.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
1267
|
+
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1268
|
+
const truncated = entries.length > displayed.length;
|
|
1269
|
+
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
1038
1270
|
return {
|
|
1039
|
-
type: "
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
1045
|
-
hunks: files.reduce((sum, file) => sum + file.hunks, 0)
|
|
1046
|
-
}
|
|
1271
|
+
type: "ls",
|
|
1272
|
+
path: resolved.virtualPath,
|
|
1273
|
+
content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
|
|
1274
|
+
count: entries.length,
|
|
1275
|
+
truncated
|
|
1047
1276
|
};
|
|
1048
1277
|
}
|
|
1049
1278
|
async function executeViewFileBytesOperation(input) {
|
|
1279
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1280
|
+
filePath: input.message.filePath,
|
|
1281
|
+
baseUrl: input.baseUrl,
|
|
1282
|
+
token: input.token,
|
|
1283
|
+
projectId: input.projectId,
|
|
1284
|
+
branchName: input.message.branchName,
|
|
1285
|
+
sessionId: input.message.sessionId,
|
|
1286
|
+
artifactRoot: input.artifactRoot
|
|
1287
|
+
});
|
|
1288
|
+
if (artifact) {
|
|
1289
|
+
if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
|
|
1290
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1291
|
+
}
|
|
1292
|
+
const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
|
|
1293
|
+
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1294
|
+
if (!mediaType2) {
|
|
1295
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1296
|
+
}
|
|
1297
|
+
const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1298
|
+
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
1299
|
+
return {
|
|
1300
|
+
type: "view_file_bytes",
|
|
1301
|
+
filePath: artifact.virtualPath,
|
|
1302
|
+
mediaType: mediaType2,
|
|
1303
|
+
base64: bytes2.toString("base64"),
|
|
1304
|
+
fileSizeBytes: bytes2.length,
|
|
1305
|
+
width: dimensions2.width,
|
|
1306
|
+
height: dimensions2.height
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1050
1309
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1051
1310
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1052
1311
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -1055,7 +1314,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1055
1314
|
const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
|
|
1056
1315
|
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1057
1316
|
if (!mediaType) {
|
|
1058
|
-
throw new Error("
|
|
1317
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1059
1318
|
}
|
|
1060
1319
|
const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
1061
1320
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
@@ -1161,14 +1420,18 @@ function pullBranch(input) {
|
|
|
1161
1420
|
}
|
|
1162
1421
|
async function executeOperation(input) {
|
|
1163
1422
|
switch (input.message.type) {
|
|
1164
|
-
case "
|
|
1423
|
+
case "read":
|
|
1165
1424
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
1166
|
-
case "
|
|
1167
|
-
return
|
|
1168
|
-
case "
|
|
1425
|
+
case "write":
|
|
1426
|
+
return executeWriteFileOperation({ ...input, message: input.message });
|
|
1427
|
+
case "edit":
|
|
1169
1428
|
return executeEditFileOperation({ ...input, message: input.message });
|
|
1170
|
-
case "
|
|
1171
|
-
return
|
|
1429
|
+
case "grep":
|
|
1430
|
+
return executeGrepOperation({ ...input, message: input.message });
|
|
1431
|
+
case "find":
|
|
1432
|
+
return executeFindOperation({ ...input, message: input.message });
|
|
1433
|
+
case "ls":
|
|
1434
|
+
return executeLsOperation({ ...input, message: input.message });
|
|
1172
1435
|
case "view_file_bytes":
|
|
1173
1436
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1174
1437
|
case "sync":
|
|
@@ -1223,6 +1486,18 @@ async function executeCommand(input) {
|
|
|
1223
1486
|
branchName: input.message.branchName,
|
|
1224
1487
|
manifest: input.manifest
|
|
1225
1488
|
});
|
|
1489
|
+
let artifactProcessEnv = {};
|
|
1490
|
+
if (input.message.sessionId) {
|
|
1491
|
+
await syncSessionArtifacts({
|
|
1492
|
+
baseUrl: input.baseUrl,
|
|
1493
|
+
token: input.token,
|
|
1494
|
+
projectId: input.projectId,
|
|
1495
|
+
branchName: input.message.branchName,
|
|
1496
|
+
sessionId: input.message.sessionId,
|
|
1497
|
+
artifactRoot: input.artifactRoot
|
|
1498
|
+
});
|
|
1499
|
+
artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1500
|
+
}
|
|
1226
1501
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1227
1502
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1228
1503
|
cwd,
|
|
@@ -1232,7 +1507,8 @@ async function executeCommand(input) {
|
|
|
1232
1507
|
...process.env,
|
|
1233
1508
|
...containerRegistryEnv(),
|
|
1234
1509
|
...input.message.env ?? {},
|
|
1235
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1510
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1511
|
+
...artifactProcessEnv
|
|
1236
1512
|
}
|
|
1237
1513
|
});
|
|
1238
1514
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1295,6 +1571,15 @@ async function executeStreamingCommand(input) {
|
|
|
1295
1571
|
branchName: input.message.branchName,
|
|
1296
1572
|
manifest: input.manifest
|
|
1297
1573
|
});
|
|
1574
|
+
await syncSessionArtifacts({
|
|
1575
|
+
baseUrl: input.baseUrl,
|
|
1576
|
+
token: input.token,
|
|
1577
|
+
projectId: input.projectId,
|
|
1578
|
+
branchName: input.message.branchName,
|
|
1579
|
+
sessionId: input.message.sessionId,
|
|
1580
|
+
artifactRoot: input.artifactRoot
|
|
1581
|
+
});
|
|
1582
|
+
const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1298
1583
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1299
1584
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1300
1585
|
cwd,
|
|
@@ -1304,7 +1589,8 @@ async function executeStreamingCommand(input) {
|
|
|
1304
1589
|
...process.env,
|
|
1305
1590
|
...containerRegistryEnv(),
|
|
1306
1591
|
...input.message.env ?? {},
|
|
1307
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1592
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1593
|
+
...artifactProcessEnv
|
|
1308
1594
|
}
|
|
1309
1595
|
});
|
|
1310
1596
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1679,11 +1965,14 @@ async function startWorker(options) {
|
|
|
1679
1965
|
validateLabel(label);
|
|
1680
1966
|
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1681
1967
|
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1968
|
+
const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
1682
1969
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1683
1970
|
`);
|
|
1684
1971
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1685
1972
|
`);
|
|
1686
1973
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1974
|
+
`);
|
|
1975
|
+
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
1687
1976
|
`);
|
|
1688
1977
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1689
1978
|
`);
|
|
@@ -1691,6 +1980,7 @@ async function startWorker(options) {
|
|
|
1691
1980
|
await verifyR5dctlAuth(baseUrl, token);
|
|
1692
1981
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
1693
1982
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
1983
|
+
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
1694
1984
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1695
1985
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1696
1986
|
headers: {
|
|
@@ -1701,12 +1991,13 @@ async function startWorker(options) {
|
|
|
1701
1991
|
const hello = {
|
|
1702
1992
|
type: "hello",
|
|
1703
1993
|
hostInfo: {
|
|
1704
|
-
hostname: (0,
|
|
1994
|
+
hostname: (0, import_node_os.hostname)(),
|
|
1705
1995
|
platform: process.platform,
|
|
1706
1996
|
arch: process.arch,
|
|
1707
1997
|
pid: process.pid,
|
|
1708
1998
|
version: getWorkerVersion(),
|
|
1709
|
-
projectRoot: projectsRoot
|
|
1999
|
+
projectRoot: projectsRoot,
|
|
2000
|
+
artifactRoot
|
|
1710
2001
|
}
|
|
1711
2002
|
};
|
|
1712
2003
|
ws.send(JSON.stringify(hello));
|
|
@@ -1803,7 +2094,7 @@ async function startWorker(options) {
|
|
|
1803
2094
|
const result = await withBranchLock(
|
|
1804
2095
|
message.projectId,
|
|
1805
2096
|
message.branchName,
|
|
1806
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2097
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1807
2098
|
);
|
|
1808
2099
|
ws.send(JSON.stringify(result));
|
|
1809
2100
|
return;
|
|
@@ -1822,7 +2113,7 @@ async function startWorker(options) {
|
|
|
1822
2113
|
void withBranchLock(
|
|
1823
2114
|
message.projectId,
|
|
1824
2115
|
message.branchName,
|
|
1825
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2116
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1826
2117
|
).catch((error) => {
|
|
1827
2118
|
sendWorkerMessage(ws, {
|
|
1828
2119
|
type: "exec_start_error",
|
|
@@ -1841,14 +2132,14 @@ async function startWorker(options) {
|
|
|
1841
2132
|
}
|
|
1842
2133
|
return;
|
|
1843
2134
|
}
|
|
1844
|
-
if (message.type === "
|
|
2135
|
+
if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
|
|
1845
2136
|
try {
|
|
1846
2137
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
1847
2138
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1848
2139
|
const result = await withBranchLock(
|
|
1849
2140
|
message.projectId,
|
|
1850
2141
|
message.branchName,
|
|
1851
|
-
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2142
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1852
2143
|
);
|
|
1853
2144
|
ws.send(
|
|
1854
2145
|
JSON.stringify({
|
|
@@ -1925,5 +2216,7 @@ if (isCliEntrypoint()) {
|
|
|
1925
2216
|
}
|
|
1926
2217
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1927
2218
|
0 && (module.exports = {
|
|
1928
|
-
ensureVisibleGitCheckout
|
|
2219
|
+
ensureVisibleGitCheckout,
|
|
2220
|
+
isArtifactEnvPath,
|
|
2221
|
+
syncSessionArtifacts
|
|
1929
2222
|
});
|