@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/mjs/main.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { createHash
|
|
6
|
-
import { hostname } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import os, { hostname } from "node:os";
|
|
7
6
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
8
7
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
8
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
@@ -11,6 +10,7 @@ const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
|
11
10
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
12
11
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
13
12
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
13
|
+
const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
14
14
|
const MAX_LINE_LENGTH = 2e3;
|
|
15
15
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
16
16
|
const R5D_REMOTE_NAME = "r5d";
|
|
@@ -30,6 +30,9 @@ function defaultProjectsRoot() {
|
|
|
30
30
|
function defaultSyncRoot() {
|
|
31
31
|
return path.join(os.homedir(), ".r5d", "sync");
|
|
32
32
|
}
|
|
33
|
+
function defaultArtifactRoot() {
|
|
34
|
+
return path.join(os.homedir(), ".r5d", "artifacts");
|
|
35
|
+
}
|
|
33
36
|
function printHelp() {
|
|
34
37
|
process.stdout.write(`Usage:
|
|
35
38
|
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
@@ -43,6 +46,7 @@ Options:
|
|
|
43
46
|
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
44
47
|
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
45
48
|
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
49
|
+
--artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
|
|
46
50
|
-v, --version Show r5d-worker version
|
|
47
51
|
-h, --help Show this help
|
|
48
52
|
`);
|
|
@@ -159,6 +163,11 @@ function parseArgs(argv) {
|
|
|
159
163
|
options.syncRoot = syncRoot;
|
|
160
164
|
continue;
|
|
161
165
|
}
|
|
166
|
+
const artifactRoot = readOption("--artifact-root");
|
|
167
|
+
if (artifactRoot !== void 0) {
|
|
168
|
+
options.artifactRoot = artifactRoot;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
162
171
|
rest.push(arg);
|
|
163
172
|
}
|
|
164
173
|
if (options.version) {
|
|
@@ -201,6 +210,170 @@ async function readResponseText(response) {
|
|
|
201
210
|
return "";
|
|
202
211
|
}
|
|
203
212
|
}
|
|
213
|
+
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
214
|
+
function validateArtifactSessionId(sessionId) {
|
|
215
|
+
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)) {
|
|
216
|
+
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
220
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
221
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
222
|
+
throw new Error(`${label} escapes the artifact directory`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
226
|
+
validateArtifactSessionId(sessionId);
|
|
227
|
+
return path.join(artifactRoot, sessionId);
|
|
228
|
+
}
|
|
229
|
+
function artifactEnv(artifactRoot, sessionId) {
|
|
230
|
+
if (!sessionId) {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function isArtifactEnvPath(filePath) {
|
|
238
|
+
return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
|
|
239
|
+
}
|
|
240
|
+
function artifactApiBasePath(projectId, branchName, sessionId) {
|
|
241
|
+
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
|
|
242
|
+
}
|
|
243
|
+
function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
|
|
244
|
+
return new URL(artifactApiBasePath(projectId, branchName, sessionId), baseUrl);
|
|
245
|
+
}
|
|
246
|
+
function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
|
|
247
|
+
return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
248
|
+
}
|
|
249
|
+
function sha256Path(filePath) {
|
|
250
|
+
return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
251
|
+
}
|
|
252
|
+
function parseSessionArtifactManifest(value) {
|
|
253
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.artifacts)) {
|
|
254
|
+
throw new Error("Artifact manifest response is invalid");
|
|
255
|
+
}
|
|
256
|
+
return value.artifacts.map((entry) => {
|
|
257
|
+
if (!entry || typeof entry !== "object") {
|
|
258
|
+
throw new Error("Artifact manifest entry is invalid");
|
|
259
|
+
}
|
|
260
|
+
const artifact = entry;
|
|
261
|
+
if (typeof artifact.filename !== "string" || artifact.filename.includes("/") || artifact.filename.includes("\\")) {
|
|
262
|
+
throw new Error("Artifact manifest entry has an invalid filename");
|
|
263
|
+
}
|
|
264
|
+
if (typeof artifact.size !== "number" || !Number.isFinite(artifact.size) || artifact.size < 0) {
|
|
265
|
+
throw new Error(`Artifact manifest entry has an invalid size: ${artifact.filename}`);
|
|
266
|
+
}
|
|
267
|
+
if (typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
|
|
268
|
+
throw new Error(`Artifact manifest entry has an invalid sha256: ${artifact.filename}`);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
filename: artifact.filename,
|
|
272
|
+
size: artifact.size,
|
|
273
|
+
sha256: artifact.sha256.toLowerCase()
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function fetchSessionArtifactManifest(input) {
|
|
278
|
+
const response = await fetch(artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId), {
|
|
279
|
+
headers: {
|
|
280
|
+
Authorization: `Bearer ${input.token}`
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
const detail = (await readResponseText(response)).trim();
|
|
285
|
+
throw new Error(`Failed to fetch artifact manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
286
|
+
}
|
|
287
|
+
return parseSessionArtifactManifest(await response.json());
|
|
288
|
+
}
|
|
289
|
+
async function downloadSessionArtifact(input) {
|
|
290
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
291
|
+
const targetPath = path.join(targetDir, input.artifact.filename);
|
|
292
|
+
assertInsideRoot(targetDir, targetPath, "Artifact path");
|
|
293
|
+
const response = await fetch(
|
|
294
|
+
artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, input.artifact.filename),
|
|
295
|
+
{
|
|
296
|
+
headers: {
|
|
297
|
+
Authorization: `Bearer ${input.token}`
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const detail = (await readResponseText(response)).trim();
|
|
303
|
+
throw new Error(`Failed to download artifact ${input.artifact.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
304
|
+
}
|
|
305
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
306
|
+
const receivedHash = createHash("sha256").update(buffer).digest("hex");
|
|
307
|
+
if (receivedHash !== input.artifact.sha256) {
|
|
308
|
+
throw new Error(`Downloaded artifact ${input.artifact.filename} failed sha256 verification`);
|
|
309
|
+
}
|
|
310
|
+
if (buffer.length !== input.artifact.size) {
|
|
311
|
+
throw new Error(`Downloaded artifact ${input.artifact.filename} has unexpected size`);
|
|
312
|
+
}
|
|
313
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
314
|
+
fs.writeFileSync(tempPath, buffer);
|
|
315
|
+
fs.renameSync(tempPath, targetPath);
|
|
316
|
+
}
|
|
317
|
+
async function syncSessionArtifacts(input) {
|
|
318
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
319
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
320
|
+
const artifacts = await fetchSessionArtifactManifest(input);
|
|
321
|
+
const manifestNames = new Set(artifacts.map((artifact) => artifact.filename));
|
|
322
|
+
for (const artifact of artifacts) {
|
|
323
|
+
const localPath = path.join(targetDir, artifact.filename);
|
|
324
|
+
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
325
|
+
if (fs.existsSync(localPath)) {
|
|
326
|
+
const stats = fs.statSync(localPath);
|
|
327
|
+
if (stats.isFile() && stats.size === artifact.size && sha256Path(localPath) === artifact.sha256) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
fs.rmSync(localPath, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
await downloadSessionArtifact({ ...input, artifact });
|
|
333
|
+
}
|
|
334
|
+
for (const localName of fs.readdirSync(targetDir)) {
|
|
335
|
+
if (!manifestNames.has(localName)) {
|
|
336
|
+
const localPath = path.join(targetDir, localName);
|
|
337
|
+
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
338
|
+
fs.rmSync(localPath, { recursive: true, force: true });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return targetDir;
|
|
342
|
+
}
|
|
343
|
+
async function resolveArtifactEnvFilePath(input) {
|
|
344
|
+
if (!isArtifactEnvPath(input.filePath)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
if (!input.sessionId) {
|
|
348
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
|
|
349
|
+
}
|
|
350
|
+
if (!input.filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`)) {
|
|
351
|
+
throw new Error(`Artifact path must reference a file under ${R5D_ARTIFACTS_DIR_REF}/`);
|
|
352
|
+
}
|
|
353
|
+
const targetDir = await syncSessionArtifacts({
|
|
354
|
+
baseUrl: input.baseUrl,
|
|
355
|
+
token: input.token,
|
|
356
|
+
projectId: input.projectId,
|
|
357
|
+
branchName: input.branchName,
|
|
358
|
+
sessionId: input.sessionId,
|
|
359
|
+
artifactRoot: input.artifactRoot
|
|
360
|
+
});
|
|
361
|
+
const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
|
|
362
|
+
if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
|
|
363
|
+
throw new Error(`Invalid artifact path: ${input.filePath}`);
|
|
364
|
+
}
|
|
365
|
+
const absolutePath = path.resolve(targetDir, ...relativePath.split("/"));
|
|
366
|
+
assertInsideRoot(targetDir, absolutePath, "Artifact path");
|
|
367
|
+
return {
|
|
368
|
+
absolutePath,
|
|
369
|
+
virtualPath: `${R5D_ARTIFACTS_DIR_REF}/${relativePath}`
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function rejectArtifactWritePath(filePath) {
|
|
373
|
+
if (isArtifactEnvPath(filePath)) {
|
|
374
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
204
377
|
async function verifyR5dctlAuth(baseUrl, token) {
|
|
205
378
|
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
206
379
|
let response;
|
|
@@ -712,16 +885,36 @@ function formatLineNumberedContent(content, offset, limit) {
|
|
|
712
885
|
const endLine = Math.min(startLine + readLimit - 1, totalLines);
|
|
713
886
|
const selectedLines = allLines.slice(startLine - 1, endLine);
|
|
714
887
|
const padWidth = String(endLine).length;
|
|
715
|
-
const formattedLines =
|
|
888
|
+
const formattedLines = [];
|
|
889
|
+
let actualEndLine = endLine;
|
|
890
|
+
let budgetTruncated = false;
|
|
891
|
+
for (let index = 0; index < selectedLines.length; index += 1) {
|
|
892
|
+
const line = selectedLines[index] ?? "";
|
|
716
893
|
const lineNumber = startLine + index;
|
|
717
894
|
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
718
|
-
|
|
719
|
-
|
|
895
|
+
const formattedLine = `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
|
|
896
|
+
const candidateLength = formattedLines.length === 0 ? formattedLine.length : formattedLine.length + 1;
|
|
897
|
+
const currentLength = formattedLines.join("\n").length;
|
|
898
|
+
if (currentLength + candidateLength > DEFAULT_READ_MAX_BYTES) {
|
|
899
|
+
budgetTruncated = true;
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
formattedLines.push(formattedLine);
|
|
903
|
+
actualEndLine = lineNumber;
|
|
904
|
+
}
|
|
905
|
+
let formattedContent = formattedLines.join("\n");
|
|
906
|
+
if (budgetTruncated) {
|
|
907
|
+
formattedContent = `${formattedContent}
|
|
908
|
+
... (truncated by byte budget)`;
|
|
909
|
+
}
|
|
910
|
+
const truncated = budgetTruncated || actualEndLine < totalLines;
|
|
720
911
|
return {
|
|
721
|
-
content:
|
|
912
|
+
content: formattedContent,
|
|
722
913
|
totalLines,
|
|
723
914
|
startLine,
|
|
724
|
-
endLine
|
|
915
|
+
endLine: actualEndLine,
|
|
916
|
+
truncated,
|
|
917
|
+
continuation: truncated ? `Call read with offset ${actualEndLine + 1} to continue.` : void 0
|
|
725
918
|
};
|
|
726
919
|
}
|
|
727
920
|
function readPngDimensions(bytes) {
|
|
@@ -788,7 +981,51 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
788
981
|
}
|
|
789
982
|
return dimensions;
|
|
790
983
|
}
|
|
984
|
+
const fileMutationQueues = /* @__PURE__ */ new Map();
|
|
985
|
+
async function withFileMutationQueue(key, operation) {
|
|
986
|
+
const previous = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
987
|
+
let release;
|
|
988
|
+
const current = previous.catch(() => void 0).then(() => new Promise((resolve) => {
|
|
989
|
+
release = resolve;
|
|
990
|
+
}));
|
|
991
|
+
fileMutationQueues.set(key, current);
|
|
992
|
+
await previous.catch(() => void 0);
|
|
993
|
+
try {
|
|
994
|
+
return await operation();
|
|
995
|
+
} finally {
|
|
996
|
+
release();
|
|
997
|
+
if (fileMutationQueues.get(key) === current) {
|
|
998
|
+
fileMutationQueues.delete(key);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function mutationQueueKey(projectId, branchName, filePath) {
|
|
1003
|
+
return `${projectId}:${branchName}:${path.posix.normalize(filePath)}`;
|
|
1004
|
+
}
|
|
791
1005
|
async function executeReadFileOperation(input) {
|
|
1006
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1007
|
+
filePath: input.message.filePath,
|
|
1008
|
+
baseUrl: input.baseUrl,
|
|
1009
|
+
token: input.token,
|
|
1010
|
+
projectId: input.projectId,
|
|
1011
|
+
branchName: input.message.branchName,
|
|
1012
|
+
sessionId: input.message.sessionId,
|
|
1013
|
+
artifactRoot: input.artifactRoot
|
|
1014
|
+
});
|
|
1015
|
+
if (artifact) {
|
|
1016
|
+
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1017
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1018
|
+
}
|
|
1019
|
+
const buffer2 = fs.readFileSync(artifact.absolutePath);
|
|
1020
|
+
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
1021
|
+
return {
|
|
1022
|
+
type: "read",
|
|
1023
|
+
kind: "text",
|
|
1024
|
+
file: artifact.virtualPath,
|
|
1025
|
+
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
1026
|
+
...formatted2
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
792
1029
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
793
1030
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
794
1031
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -797,222 +1034,242 @@ async function executeReadFileOperation(input) {
|
|
|
797
1034
|
const buffer = fs.readFileSync(resolved.absolutePath);
|
|
798
1035
|
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
799
1036
|
return {
|
|
800
|
-
type: "
|
|
1037
|
+
type: "read",
|
|
1038
|
+
kind: "text",
|
|
801
1039
|
file: resolved.virtualPath,
|
|
802
1040
|
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
803
1041
|
...formatted
|
|
804
1042
|
};
|
|
805
1043
|
}
|
|
806
|
-
async function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
};
|
|
1044
|
+
async function executeWriteFileOperation(input) {
|
|
1045
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
1046
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1047
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1048
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1049
|
+
fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
|
|
1050
|
+
fs.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
1051
|
+
return {
|
|
1052
|
+
type: "write",
|
|
1053
|
+
file: resolved.virtualPath,
|
|
1054
|
+
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
1055
|
+
};
|
|
1056
|
+
});
|
|
819
1057
|
}
|
|
820
1058
|
async function executeEditFileOperation(input) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
)
|
|
1059
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
1060
|
+
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, input.message.filePath), async () => {
|
|
1061
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1062
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1063
|
+
if (!Array.isArray(input.message.edits) || input.message.edits.length === 0) {
|
|
1064
|
+
throw new Error("edit requires at least one replacement");
|
|
1065
|
+
}
|
|
1066
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1067
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
1068
|
+
}
|
|
1069
|
+
const originalContent = fs.readFileSync(resolved.absolutePath, "utf8");
|
|
1070
|
+
const ranges = input.message.edits.map((edit, index) => {
|
|
1071
|
+
if (edit.oldText === edit.newText) {
|
|
1072
|
+
throw new Error(`edits[${index}].oldText and edits[${index}].newText must be different`);
|
|
1073
|
+
}
|
|
1074
|
+
if (edit.oldText.length === 0) {
|
|
1075
|
+
throw new Error(`edits[${index}].oldText cannot be empty`);
|
|
1076
|
+
}
|
|
1077
|
+
const occurrences = originalContent.split(edit.oldText).length - 1;
|
|
1078
|
+
if (occurrences === 0) {
|
|
1079
|
+
throw new Error(`Could not find edits[${index}].oldText: "${edit.oldText.slice(0, 100)}${edit.oldText.length > 100 ? "..." : ""}"`);
|
|
1080
|
+
}
|
|
1081
|
+
if (occurrences > 1) {
|
|
1082
|
+
throw new Error(`edits[${index}].oldText is not unique in the file (found ${occurrences} occurrences). Include more surrounding context.`);
|
|
1083
|
+
}
|
|
1084
|
+
const start = originalContent.indexOf(edit.oldText);
|
|
1085
|
+
return { index, start, end: start + edit.oldText.length, edit };
|
|
1086
|
+
});
|
|
1087
|
+
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
|
|
1088
|
+
for (let index = 1; index < sortedRanges.length; index += 1) {
|
|
1089
|
+
const previous = sortedRanges[index - 1];
|
|
1090
|
+
const current = sortedRanges[index];
|
|
1091
|
+
if (current.start < previous.end) {
|
|
1092
|
+
throw new Error(`edits[${current.index}] overlaps edits[${previous.index}]. Merge nearby changes into one replacement.`);
|
|
1093
|
+
}
|
|
841
1094
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1095
|
+
let nextContent = originalContent;
|
|
1096
|
+
for (const range of [...sortedRanges].reverse()) {
|
|
1097
|
+
nextContent = nextContent.slice(0, range.start) + range.edit.newText + nextContent.slice(range.end);
|
|
1098
|
+
}
|
|
1099
|
+
fs.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
1100
|
+
return {
|
|
1101
|
+
type: "edit",
|
|
1102
|
+
file: resolved.virtualPath,
|
|
1103
|
+
edits: input.message.edits.length
|
|
1104
|
+
};
|
|
1105
|
+
});
|
|
849
1106
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1107
|
+
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
1108
|
+
function normalizeToolLimit(value, fallback, max) {
|
|
1109
|
+
if (!Number.isFinite(value) || value === void 0) return fallback;
|
|
1110
|
+
return Math.max(1, Math.min(max, Math.floor(value)));
|
|
1111
|
+
}
|
|
1112
|
+
function escapeRegex(value) {
|
|
1113
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1114
|
+
}
|
|
1115
|
+
function wildcardToRegex(pattern) {
|
|
1116
|
+
const escaped = escapeRegex(pattern).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
1117
|
+
return new RegExp(`^${escaped}$`);
|
|
1118
|
+
}
|
|
1119
|
+
function normalizeFindPattern(pattern) {
|
|
1120
|
+
return /[*?]/.test(pattern) ? pattern : `*${pattern}*`;
|
|
1121
|
+
}
|
|
1122
|
+
function toVirtualPath(branchPath, absolutePath) {
|
|
1123
|
+
const relative = path.relative(branchPath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1124
|
+
return relative ? `/${relative}` : "/";
|
|
1125
|
+
}
|
|
1126
|
+
function walkProjectEntries(branchPath, startPath) {
|
|
1127
|
+
const entries = [];
|
|
1128
|
+
const visit = (absolutePath) => {
|
|
1129
|
+
const stat = fs.statSync(absolutePath);
|
|
1130
|
+
const virtualPath = toVirtualPath(branchPath, absolutePath);
|
|
1131
|
+
entries.push({ absolutePath, virtualPath, stat });
|
|
1132
|
+
if (!stat.isDirectory()) return;
|
|
1133
|
+
const children = fs.readdirSync(absolutePath, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1134
|
+
for (const child of children) {
|
|
1135
|
+
if (IGNORED_ENTRY_NAMES.has(child.name)) continue;
|
|
1136
|
+
visit(path.join(absolutePath, child.name));
|
|
857
1137
|
}
|
|
858
|
-
}
|
|
859
|
-
|
|
1138
|
+
};
|
|
1139
|
+
visit(startPath);
|
|
1140
|
+
return entries;
|
|
860
1141
|
}
|
|
861
|
-
function
|
|
862
|
-
|
|
863
|
-
let patchPath = decodePatchPath(withoutMetadata);
|
|
864
|
-
if (patchPath === "/dev/null") {
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
868
|
-
patchPath = patchPath.slice(2);
|
|
869
|
-
}
|
|
870
|
-
if (!patchPath || path.isAbsolute(patchPath)) {
|
|
871
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
872
|
-
}
|
|
873
|
-
const normalized = path.posix.normalize(patchPath);
|
|
874
|
-
if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
875
|
-
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
876
|
-
}
|
|
877
|
-
return normalized;
|
|
1142
|
+
function isProbablyText(bytes) {
|
|
1143
|
+
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
878
1144
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1145
|
+
async function executeGrepOperation(input) {
|
|
1146
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1147
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1148
|
+
if (!fs.existsSync(start.absolutePath)) {
|
|
1149
|
+
throw new Error(`Path not found: ${start.virtualPath}`);
|
|
1150
|
+
}
|
|
1151
|
+
const limit = normalizeToolLimit(input.message.limit, 100, 1e3);
|
|
1152
|
+
const flags = input.message.caseSensitive === false ? "i" : "";
|
|
1153
|
+
const regex = new RegExp(input.message.pattern, flags);
|
|
1154
|
+
const globRegex = input.message.glob ? wildcardToRegex(input.message.glob) : null;
|
|
1155
|
+
const lines = [];
|
|
1156
|
+
let matchCount = 0;
|
|
1157
|
+
let truncated = false;
|
|
1158
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1159
|
+
if (!entry.stat.isFile()) continue;
|
|
1160
|
+
const repoRelative = entry.virtualPath.slice(1);
|
|
1161
|
+
if (globRegex && !globRegex.test(repoRelative)) continue;
|
|
1162
|
+
const bytes = fs.readFileSync(entry.absolutePath);
|
|
1163
|
+
if (!isProbablyText(bytes)) continue;
|
|
1164
|
+
const fileLines = bytes.toString("utf8").split(/\r?\n/);
|
|
1165
|
+
for (let index = 0; index < fileLines.length; index += 1) {
|
|
1166
|
+
const line = fileLines[index] ?? "";
|
|
1167
|
+
regex.lastIndex = 0;
|
|
1168
|
+
const match = regex.exec(line);
|
|
1169
|
+
if (!match) continue;
|
|
1170
|
+
matchCount += 1;
|
|
1171
|
+
if (lines.length < limit) {
|
|
1172
|
+
const column = Math.max(1, (match.index ?? 0) + 1);
|
|
1173
|
+
lines.push(`${entry.virtualPath}:${index + 1}:${column}: ${line.slice(0, MAX_LINE_LENGTH)}`);
|
|
1174
|
+
} else {
|
|
1175
|
+
truncated = true;
|
|
1176
|
+
}
|
|
906
1177
|
}
|
|
907
|
-
index += 1;
|
|
908
|
-
}
|
|
909
|
-
throw new Error("Unterminated quoted path in patch header");
|
|
910
|
-
}
|
|
911
|
-
function parseDiffGitPaths(line) {
|
|
912
|
-
if (!line.startsWith("diff --git ")) {
|
|
913
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
914
|
-
}
|
|
915
|
-
const rest = line.slice("diff --git ".length);
|
|
916
|
-
const first = readDiffPathToken(rest, 0);
|
|
917
|
-
const second = readDiffPathToken(rest, first.nextIndex);
|
|
918
|
-
if (rest.slice(second.nextIndex).trim()) {
|
|
919
|
-
throw new Error(`Invalid patch header: ${line}`);
|
|
920
1178
|
}
|
|
1179
|
+
const hint = truncated ? `
|
|
1180
|
+
... truncated. Narrow path/glob or increase limit to continue.` : "";
|
|
921
1181
|
return {
|
|
922
|
-
|
|
923
|
-
|
|
1182
|
+
type: "grep",
|
|
1183
|
+
content: lines.length > 0 ? `${lines.join("\n")}${hint}` : "No matches.",
|
|
1184
|
+
matchCount,
|
|
1185
|
+
truncated
|
|
924
1186
|
};
|
|
925
1187
|
}
|
|
926
|
-
function
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
if (line.startsWith("new file mode ")) {
|
|
953
|
-
current.operation = "add";
|
|
954
|
-
continue;
|
|
955
|
-
}
|
|
956
|
-
if (line.startsWith("deleted file mode ")) {
|
|
957
|
-
current.operation = "delete";
|
|
958
|
-
if (current.oldPath) {
|
|
959
|
-
current.path = current.oldPath;
|
|
960
|
-
delete current.oldPath;
|
|
961
|
-
}
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
if (line.startsWith("rename from ")) {
|
|
965
|
-
current.operation = "move";
|
|
966
|
-
current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
if (line.startsWith("rename to ")) {
|
|
970
|
-
current.operation = "move";
|
|
971
|
-
current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
if (line.startsWith("@@")) {
|
|
975
|
-
current.hunks += 1;
|
|
976
|
-
continue;
|
|
977
|
-
}
|
|
978
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
979
|
-
current.additions += 1;
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
983
|
-
current.deletions += 1;
|
|
1188
|
+
async function executeFindOperation(input) {
|
|
1189
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1190
|
+
const start = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1191
|
+
if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
|
|
1192
|
+
throw new Error(`Directory not found: ${start.virtualPath}`);
|
|
1193
|
+
}
|
|
1194
|
+
const pattern = normalizeFindPattern(input.message.pattern ?? "*");
|
|
1195
|
+
const matcher = wildcardToRegex(pattern);
|
|
1196
|
+
const entryType = input.message.entryType ?? "file";
|
|
1197
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1198
|
+
const matches = [];
|
|
1199
|
+
let count = 0;
|
|
1200
|
+
let truncated = false;
|
|
1201
|
+
for (const entry of walkProjectEntries(workspace.branchPath, start.absolutePath)) {
|
|
1202
|
+
if (entry.virtualPath === start.virtualPath) continue;
|
|
1203
|
+
const isDirectory = entry.stat.isDirectory();
|
|
1204
|
+
if (entryType === "file" && !entry.stat.isFile()) continue;
|
|
1205
|
+
if (entryType === "directory" && !isDirectory) continue;
|
|
1206
|
+
const label = isDirectory ? `${entry.virtualPath}/` : entry.virtualPath;
|
|
1207
|
+
if (!matcher.test(path.posix.basename(entry.virtualPath)) && !matcher.test(entry.virtualPath.slice(1))) continue;
|
|
1208
|
+
count += 1;
|
|
1209
|
+
if (matches.length < limit) {
|
|
1210
|
+
matches.push(label);
|
|
1211
|
+
} else {
|
|
1212
|
+
truncated = true;
|
|
984
1213
|
}
|
|
985
1214
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1215
|
+
const hint = truncated ? "\n... truncated. Narrow path/pattern or increase limit to continue." : "";
|
|
1216
|
+
return {
|
|
1217
|
+
type: "find",
|
|
1218
|
+
content: matches.length > 0 ? `${matches.join("\n")}${hint}` : "No paths found.",
|
|
1219
|
+
count,
|
|
1220
|
+
truncated
|
|
1221
|
+
};
|
|
990
1222
|
}
|
|
991
|
-
async function
|
|
1223
|
+
async function executeLsOperation(input) {
|
|
992
1224
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
fs.rmSync(patchPath, { force: true });
|
|
1003
|
-
}
|
|
1225
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.path ?? "/");
|
|
1226
|
+
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1227
|
+
throw new Error(`Directory not found: ${resolved.virtualPath}`);
|
|
1228
|
+
}
|
|
1229
|
+
const limit = normalizeToolLimit(input.message.limit, 200, 1e3);
|
|
1230
|
+
const entries = fs.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));
|
|
1231
|
+
const displayed = entries.slice(0, limit).map((entry) => `${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1232
|
+
const truncated = entries.length > displayed.length;
|
|
1233
|
+
const hint = truncated ? "\n... truncated. Increase limit to continue." : "";
|
|
1004
1234
|
return {
|
|
1005
|
-
type: "
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
1011
|
-
hunks: files.reduce((sum, file) => sum + file.hunks, 0)
|
|
1012
|
-
}
|
|
1235
|
+
type: "ls",
|
|
1236
|
+
path: resolved.virtualPath,
|
|
1237
|
+
content: displayed.length > 0 ? `${displayed.join("\n")}${hint}` : "(empty directory)",
|
|
1238
|
+
count: entries.length,
|
|
1239
|
+
truncated
|
|
1013
1240
|
};
|
|
1014
1241
|
}
|
|
1015
1242
|
async function executeViewFileBytesOperation(input) {
|
|
1243
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1244
|
+
filePath: input.message.filePath,
|
|
1245
|
+
baseUrl: input.baseUrl,
|
|
1246
|
+
token: input.token,
|
|
1247
|
+
projectId: input.projectId,
|
|
1248
|
+
branchName: input.message.branchName,
|
|
1249
|
+
sessionId: input.message.sessionId,
|
|
1250
|
+
artifactRoot: input.artifactRoot
|
|
1251
|
+
});
|
|
1252
|
+
if (artifact) {
|
|
1253
|
+
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1254
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1255
|
+
}
|
|
1256
|
+
const extension2 = path.extname(artifact.absolutePath).toLowerCase();
|
|
1257
|
+
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1258
|
+
if (!mediaType2) {
|
|
1259
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1260
|
+
}
|
|
1261
|
+
const bytes2 = fs.readFileSync(artifact.absolutePath);
|
|
1262
|
+
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
1263
|
+
return {
|
|
1264
|
+
type: "view_file_bytes",
|
|
1265
|
+
filePath: artifact.virtualPath,
|
|
1266
|
+
mediaType: mediaType2,
|
|
1267
|
+
base64: bytes2.toString("base64"),
|
|
1268
|
+
fileSizeBytes: bytes2.length,
|
|
1269
|
+
width: dimensions2.width,
|
|
1270
|
+
height: dimensions2.height
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1016
1273
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1017
1274
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1018
1275
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -1021,7 +1278,7 @@ async function executeViewFileBytesOperation(input) {
|
|
|
1021
1278
|
const extension = path.extname(resolved.absolutePath).toLowerCase();
|
|
1022
1279
|
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1023
1280
|
if (!mediaType) {
|
|
1024
|
-
throw new Error("
|
|
1281
|
+
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1025
1282
|
}
|
|
1026
1283
|
const bytes = fs.readFileSync(resolved.absolutePath);
|
|
1027
1284
|
const dimensions = readImageDimensions(bytes, mediaType);
|
|
@@ -1127,14 +1384,18 @@ function pullBranch(input) {
|
|
|
1127
1384
|
}
|
|
1128
1385
|
async function executeOperation(input) {
|
|
1129
1386
|
switch (input.message.type) {
|
|
1130
|
-
case "
|
|
1387
|
+
case "read":
|
|
1131
1388
|
return executeReadFileOperation({ ...input, message: input.message });
|
|
1132
|
-
case "
|
|
1133
|
-
return
|
|
1134
|
-
case "
|
|
1389
|
+
case "write":
|
|
1390
|
+
return executeWriteFileOperation({ ...input, message: input.message });
|
|
1391
|
+
case "edit":
|
|
1135
1392
|
return executeEditFileOperation({ ...input, message: input.message });
|
|
1136
|
-
case "
|
|
1137
|
-
return
|
|
1393
|
+
case "grep":
|
|
1394
|
+
return executeGrepOperation({ ...input, message: input.message });
|
|
1395
|
+
case "find":
|
|
1396
|
+
return executeFindOperation({ ...input, message: input.message });
|
|
1397
|
+
case "ls":
|
|
1398
|
+
return executeLsOperation({ ...input, message: input.message });
|
|
1138
1399
|
case "view_file_bytes":
|
|
1139
1400
|
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
1140
1401
|
case "sync":
|
|
@@ -1189,6 +1450,18 @@ async function executeCommand(input) {
|
|
|
1189
1450
|
branchName: input.message.branchName,
|
|
1190
1451
|
manifest: input.manifest
|
|
1191
1452
|
});
|
|
1453
|
+
let artifactProcessEnv = {};
|
|
1454
|
+
if (input.message.sessionId) {
|
|
1455
|
+
await syncSessionArtifacts({
|
|
1456
|
+
baseUrl: input.baseUrl,
|
|
1457
|
+
token: input.token,
|
|
1458
|
+
projectId: input.projectId,
|
|
1459
|
+
branchName: input.message.branchName,
|
|
1460
|
+
sessionId: input.message.sessionId,
|
|
1461
|
+
artifactRoot: input.artifactRoot
|
|
1462
|
+
});
|
|
1463
|
+
artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1464
|
+
}
|
|
1192
1465
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1193
1466
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1194
1467
|
cwd,
|
|
@@ -1198,7 +1471,8 @@ async function executeCommand(input) {
|
|
|
1198
1471
|
...process.env,
|
|
1199
1472
|
...containerRegistryEnv(),
|
|
1200
1473
|
...input.message.env ?? {},
|
|
1201
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1474
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1475
|
+
...artifactProcessEnv
|
|
1202
1476
|
}
|
|
1203
1477
|
});
|
|
1204
1478
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1261,6 +1535,15 @@ async function executeStreamingCommand(input) {
|
|
|
1261
1535
|
branchName: input.message.branchName,
|
|
1262
1536
|
manifest: input.manifest
|
|
1263
1537
|
});
|
|
1538
|
+
await syncSessionArtifacts({
|
|
1539
|
+
baseUrl: input.baseUrl,
|
|
1540
|
+
token: input.token,
|
|
1541
|
+
projectId: input.projectId,
|
|
1542
|
+
branchName: input.message.branchName,
|
|
1543
|
+
sessionId: input.message.sessionId,
|
|
1544
|
+
artifactRoot: input.artifactRoot
|
|
1545
|
+
});
|
|
1546
|
+
const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1264
1547
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1265
1548
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1266
1549
|
cwd,
|
|
@@ -1270,7 +1553,8 @@ async function executeStreamingCommand(input) {
|
|
|
1270
1553
|
...process.env,
|
|
1271
1554
|
...containerRegistryEnv(),
|
|
1272
1555
|
...input.message.env ?? {},
|
|
1273
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1556
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1557
|
+
...artifactProcessEnv
|
|
1274
1558
|
}
|
|
1275
1559
|
});
|
|
1276
1560
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1645,11 +1929,14 @@ async function startWorker(options) {
|
|
|
1645
1929
|
validateLabel(label);
|
|
1646
1930
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1647
1931
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1932
|
+
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
1648
1933
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1649
1934
|
`);
|
|
1650
1935
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1651
1936
|
`);
|
|
1652
1937
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1938
|
+
`);
|
|
1939
|
+
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
1653
1940
|
`);
|
|
1654
1941
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1655
1942
|
`);
|
|
@@ -1657,6 +1944,7 @@ async function startWorker(options) {
|
|
|
1657
1944
|
await verifyR5dctlAuth(baseUrl, token);
|
|
1658
1945
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1659
1946
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1947
|
+
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
1660
1948
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1661
1949
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1662
1950
|
headers: {
|
|
@@ -1672,7 +1960,8 @@ async function startWorker(options) {
|
|
|
1672
1960
|
arch: process.arch,
|
|
1673
1961
|
pid: process.pid,
|
|
1674
1962
|
version: getWorkerVersion(),
|
|
1675
|
-
projectRoot: projectsRoot
|
|
1963
|
+
projectRoot: projectsRoot,
|
|
1964
|
+
artifactRoot
|
|
1676
1965
|
}
|
|
1677
1966
|
};
|
|
1678
1967
|
ws.send(JSON.stringify(hello));
|
|
@@ -1769,7 +2058,7 @@ async function startWorker(options) {
|
|
|
1769
2058
|
const result = await withBranchLock(
|
|
1770
2059
|
message.projectId,
|
|
1771
2060
|
message.branchName,
|
|
1772
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2061
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1773
2062
|
);
|
|
1774
2063
|
ws.send(JSON.stringify(result));
|
|
1775
2064
|
return;
|
|
@@ -1788,7 +2077,7 @@ async function startWorker(options) {
|
|
|
1788
2077
|
void withBranchLock(
|
|
1789
2078
|
message.projectId,
|
|
1790
2079
|
message.branchName,
|
|
1791
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2080
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1792
2081
|
).catch((error) => {
|
|
1793
2082
|
sendWorkerMessage(ws, {
|
|
1794
2083
|
type: "exec_start_error",
|
|
@@ -1807,14 +2096,14 @@ async function startWorker(options) {
|
|
|
1807
2096
|
}
|
|
1808
2097
|
return;
|
|
1809
2098
|
}
|
|
1810
|
-
if (message.type === "
|
|
2099
|
+
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") {
|
|
1811
2100
|
try {
|
|
1812
2101
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
1813
2102
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1814
2103
|
const result = await withBranchLock(
|
|
1815
2104
|
message.projectId,
|
|
1816
2105
|
message.branchName,
|
|
1817
|
-
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2106
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1818
2107
|
);
|
|
1819
2108
|
ws.send(
|
|
1820
2109
|
JSON.stringify({
|
|
@@ -1890,5 +2179,7 @@ if (isCliEntrypoint()) {
|
|
|
1890
2179
|
});
|
|
1891
2180
|
}
|
|
1892
2181
|
export {
|
|
1893
|
-
ensureVisibleGitCheckout
|
|
2182
|
+
ensureVisibleGitCheckout,
|
|
2183
|
+
isArtifactEnvPath,
|
|
2184
|
+
syncSessionArtifacts
|
|
1894
2185
|
};
|