@ricsam/r5d-worker 0.0.9 → 0.0.11
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 +275 -9
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +272 -8
- 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,7 +29,9 @@ 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);
|
|
@@ -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;
|
|
@@ -823,6 +998,28 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
823
998
|
return dimensions;
|
|
824
999
|
}
|
|
825
1000
|
async function executeReadFileOperation(input) {
|
|
1001
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1002
|
+
filePath: input.message.filePath,
|
|
1003
|
+
baseUrl: input.baseUrl,
|
|
1004
|
+
token: input.token,
|
|
1005
|
+
projectId: input.projectId,
|
|
1006
|
+
branchName: input.message.branchName,
|
|
1007
|
+
sessionId: input.message.sessionId,
|
|
1008
|
+
artifactRoot: input.artifactRoot
|
|
1009
|
+
});
|
|
1010
|
+
if (artifact) {
|
|
1011
|
+
if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
|
|
1012
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1013
|
+
}
|
|
1014
|
+
const buffer2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1015
|
+
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
1016
|
+
return {
|
|
1017
|
+
type: "read_file",
|
|
1018
|
+
file: artifact.virtualPath,
|
|
1019
|
+
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
1020
|
+
...formatted2
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
826
1023
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
827
1024
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
828
1025
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -838,6 +1035,7 @@ async function executeReadFileOperation(input) {
|
|
|
838
1035
|
};
|
|
839
1036
|
}
|
|
840
1037
|
async function executeCreateFileOperation(input) {
|
|
1038
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
841
1039
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
842
1040
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
843
1041
|
if (import_node_fs.default.existsSync(resolved.absolutePath)) {
|
|
@@ -852,6 +1050,7 @@ async function executeCreateFileOperation(input) {
|
|
|
852
1050
|
};
|
|
853
1051
|
}
|
|
854
1052
|
async function executeEditFileOperation(input) {
|
|
1053
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
855
1054
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
856
1055
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
857
1056
|
if (input.message.oldString === input.message.newString) {
|
|
@@ -901,6 +1100,7 @@ function normalizePatchPath(rawPath) {
|
|
|
901
1100
|
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
902
1101
|
patchPath = patchPath.slice(2);
|
|
903
1102
|
}
|
|
1103
|
+
rejectArtifactWritePath(patchPath);
|
|
904
1104
|
if (!patchPath || import_node_path.default.isAbsolute(patchPath)) {
|
|
905
1105
|
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
906
1106
|
}
|
|
@@ -1047,6 +1247,36 @@ async function executeApplyPatchOperation(input) {
|
|
|
1047
1247
|
};
|
|
1048
1248
|
}
|
|
1049
1249
|
async function executeViewFileBytesOperation(input) {
|
|
1250
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1251
|
+
filePath: input.message.filePath,
|
|
1252
|
+
baseUrl: input.baseUrl,
|
|
1253
|
+
token: input.token,
|
|
1254
|
+
projectId: input.projectId,
|
|
1255
|
+
branchName: input.message.branchName,
|
|
1256
|
+
sessionId: input.message.sessionId,
|
|
1257
|
+
artifactRoot: input.artifactRoot
|
|
1258
|
+
});
|
|
1259
|
+
if (artifact) {
|
|
1260
|
+
if (!import_node_fs.default.existsSync(artifact.absolutePath) || !import_node_fs.default.statSync(artifact.absolutePath).isFile()) {
|
|
1261
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1262
|
+
}
|
|
1263
|
+
const extension2 = import_node_path.default.extname(artifact.absolutePath).toLowerCase();
|
|
1264
|
+
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1265
|
+
if (!mediaType2) {
|
|
1266
|
+
throw new Error("view_image supports PNG and JPEG files only");
|
|
1267
|
+
}
|
|
1268
|
+
const bytes2 = import_node_fs.default.readFileSync(artifact.absolutePath);
|
|
1269
|
+
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
1270
|
+
return {
|
|
1271
|
+
type: "view_file_bytes",
|
|
1272
|
+
filePath: artifact.virtualPath,
|
|
1273
|
+
mediaType: mediaType2,
|
|
1274
|
+
base64: bytes2.toString("base64"),
|
|
1275
|
+
fileSizeBytes: bytes2.length,
|
|
1276
|
+
width: dimensions2.width,
|
|
1277
|
+
height: dimensions2.height
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1050
1280
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1051
1281
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1052
1282
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -1223,6 +1453,18 @@ async function executeCommand(input) {
|
|
|
1223
1453
|
branchName: input.message.branchName,
|
|
1224
1454
|
manifest: input.manifest
|
|
1225
1455
|
});
|
|
1456
|
+
let artifactProcessEnv = {};
|
|
1457
|
+
if (input.message.sessionId) {
|
|
1458
|
+
await syncSessionArtifacts({
|
|
1459
|
+
baseUrl: input.baseUrl,
|
|
1460
|
+
token: input.token,
|
|
1461
|
+
projectId: input.projectId,
|
|
1462
|
+
branchName: input.message.branchName,
|
|
1463
|
+
sessionId: input.message.sessionId,
|
|
1464
|
+
artifactRoot: input.artifactRoot
|
|
1465
|
+
});
|
|
1466
|
+
artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1467
|
+
}
|
|
1226
1468
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1227
1469
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1228
1470
|
cwd,
|
|
@@ -1232,7 +1474,8 @@ async function executeCommand(input) {
|
|
|
1232
1474
|
...process.env,
|
|
1233
1475
|
...containerRegistryEnv(),
|
|
1234
1476
|
...input.message.env ?? {},
|
|
1235
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1477
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1478
|
+
...artifactProcessEnv
|
|
1236
1479
|
}
|
|
1237
1480
|
});
|
|
1238
1481
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1295,6 +1538,15 @@ async function executeStreamingCommand(input) {
|
|
|
1295
1538
|
branchName: input.message.branchName,
|
|
1296
1539
|
manifest: input.manifest
|
|
1297
1540
|
});
|
|
1541
|
+
await syncSessionArtifacts({
|
|
1542
|
+
baseUrl: input.baseUrl,
|
|
1543
|
+
token: input.token,
|
|
1544
|
+
projectId: input.projectId,
|
|
1545
|
+
branchName: input.message.branchName,
|
|
1546
|
+
sessionId: input.message.sessionId,
|
|
1547
|
+
artifactRoot: input.artifactRoot
|
|
1548
|
+
});
|
|
1549
|
+
const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1298
1550
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1299
1551
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1300
1552
|
cwd,
|
|
@@ -1304,7 +1556,8 @@ async function executeStreamingCommand(input) {
|
|
|
1304
1556
|
...process.env,
|
|
1305
1557
|
...containerRegistryEnv(),
|
|
1306
1558
|
...input.message.env ?? {},
|
|
1307
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1559
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1560
|
+
...artifactProcessEnv
|
|
1308
1561
|
}
|
|
1309
1562
|
});
|
|
1310
1563
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1679,11 +1932,14 @@ async function startWorker(options) {
|
|
|
1679
1932
|
validateLabel(label);
|
|
1680
1933
|
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1681
1934
|
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1935
|
+
const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
1682
1936
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1683
1937
|
`);
|
|
1684
1938
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1685
1939
|
`);
|
|
1686
1940
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1941
|
+
`);
|
|
1942
|
+
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
1687
1943
|
`);
|
|
1688
1944
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1689
1945
|
`);
|
|
@@ -1691,6 +1947,7 @@ async function startWorker(options) {
|
|
|
1691
1947
|
await verifyR5dctlAuth(baseUrl, token);
|
|
1692
1948
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
1693
1949
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
1950
|
+
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
1694
1951
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1695
1952
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1696
1953
|
headers: {
|
|
@@ -1706,7 +1963,8 @@ async function startWorker(options) {
|
|
|
1706
1963
|
arch: process.arch,
|
|
1707
1964
|
pid: process.pid,
|
|
1708
1965
|
version: getWorkerVersion(),
|
|
1709
|
-
projectRoot: projectsRoot
|
|
1966
|
+
projectRoot: projectsRoot,
|
|
1967
|
+
artifactRoot
|
|
1710
1968
|
}
|
|
1711
1969
|
};
|
|
1712
1970
|
ws.send(JSON.stringify(hello));
|
|
@@ -1803,7 +2061,7 @@ async function startWorker(options) {
|
|
|
1803
2061
|
const result = await withBranchLock(
|
|
1804
2062
|
message.projectId,
|
|
1805
2063
|
message.branchName,
|
|
1806
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2064
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1807
2065
|
);
|
|
1808
2066
|
ws.send(JSON.stringify(result));
|
|
1809
2067
|
return;
|
|
@@ -1822,7 +2080,7 @@ async function startWorker(options) {
|
|
|
1822
2080
|
void withBranchLock(
|
|
1823
2081
|
message.projectId,
|
|
1824
2082
|
message.branchName,
|
|
1825
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2083
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1826
2084
|
).catch((error) => {
|
|
1827
2085
|
sendWorkerMessage(ws, {
|
|
1828
2086
|
type: "exec_start_error",
|
|
@@ -1848,7 +2106,7 @@ async function startWorker(options) {
|
|
|
1848
2106
|
const result = await withBranchLock(
|
|
1849
2107
|
message.projectId,
|
|
1850
2108
|
message.branchName,
|
|
1851
|
-
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2109
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1852
2110
|
);
|
|
1853
2111
|
ws.send(
|
|
1854
2112
|
JSON.stringify({
|
|
@@ -1910,7 +2168,13 @@ async function main() {
|
|
|
1910
2168
|
}
|
|
1911
2169
|
await startWorker(parsed.options);
|
|
1912
2170
|
}
|
|
1913
|
-
|
|
2171
|
+
function isCliEntrypoint() {
|
|
2172
|
+
if (import_meta.main) {
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
2175
|
+
return typeof require === "function" && typeof module !== "undefined" && require.main === module;
|
|
2176
|
+
}
|
|
2177
|
+
if (isCliEntrypoint()) {
|
|
1914
2178
|
main().catch((error) => {
|
|
1915
2179
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
1916
2180
|
`);
|
|
@@ -1919,5 +2183,7 @@ if (import_meta.main) {
|
|
|
1919
2183
|
}
|
|
1920
2184
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1921
2185
|
0 && (module.exports = {
|
|
1922
|
-
ensureVisibleGitCheckout
|
|
2186
|
+
ensureVisibleGitCheckout,
|
|
2187
|
+
isArtifactEnvPath,
|
|
2188
|
+
syncSessionArtifacts
|
|
1923
2189
|
});
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -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;
|
|
@@ -789,6 +962,28 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
789
962
|
return dimensions;
|
|
790
963
|
}
|
|
791
964
|
async function executeReadFileOperation(input) {
|
|
965
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
966
|
+
filePath: input.message.filePath,
|
|
967
|
+
baseUrl: input.baseUrl,
|
|
968
|
+
token: input.token,
|
|
969
|
+
projectId: input.projectId,
|
|
970
|
+
branchName: input.message.branchName,
|
|
971
|
+
sessionId: input.message.sessionId,
|
|
972
|
+
artifactRoot: input.artifactRoot
|
|
973
|
+
});
|
|
974
|
+
if (artifact) {
|
|
975
|
+
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
976
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
977
|
+
}
|
|
978
|
+
const buffer2 = fs.readFileSync(artifact.absolutePath);
|
|
979
|
+
const formatted2 = formatLineNumberedContent(buffer2.toString("utf8"), input.message.offset, input.message.limit);
|
|
980
|
+
return {
|
|
981
|
+
type: "read_file",
|
|
982
|
+
file: artifact.virtualPath,
|
|
983
|
+
gitBlobHash: getGitBlobHashForContent(buffer2),
|
|
984
|
+
...formatted2
|
|
985
|
+
};
|
|
986
|
+
}
|
|
792
987
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
793
988
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
794
989
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -804,6 +999,7 @@ async function executeReadFileOperation(input) {
|
|
|
804
999
|
};
|
|
805
1000
|
}
|
|
806
1001
|
async function executeCreateFileOperation(input) {
|
|
1002
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
807
1003
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
808
1004
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
809
1005
|
if (fs.existsSync(resolved.absolutePath)) {
|
|
@@ -818,6 +1014,7 @@ async function executeCreateFileOperation(input) {
|
|
|
818
1014
|
};
|
|
819
1015
|
}
|
|
820
1016
|
async function executeEditFileOperation(input) {
|
|
1017
|
+
rejectArtifactWritePath(input.message.filePath);
|
|
821
1018
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
822
1019
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
823
1020
|
if (input.message.oldString === input.message.newString) {
|
|
@@ -867,6 +1064,7 @@ function normalizePatchPath(rawPath) {
|
|
|
867
1064
|
if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
|
|
868
1065
|
patchPath = patchPath.slice(2);
|
|
869
1066
|
}
|
|
1067
|
+
rejectArtifactWritePath(patchPath);
|
|
870
1068
|
if (!patchPath || path.isAbsolute(patchPath)) {
|
|
871
1069
|
throw new Error(`Invalid patch path: ${rawPath}`);
|
|
872
1070
|
}
|
|
@@ -1013,6 +1211,36 @@ async function executeApplyPatchOperation(input) {
|
|
|
1013
1211
|
};
|
|
1014
1212
|
}
|
|
1015
1213
|
async function executeViewFileBytesOperation(input) {
|
|
1214
|
+
const artifact = await resolveArtifactEnvFilePath({
|
|
1215
|
+
filePath: input.message.filePath,
|
|
1216
|
+
baseUrl: input.baseUrl,
|
|
1217
|
+
token: input.token,
|
|
1218
|
+
projectId: input.projectId,
|
|
1219
|
+
branchName: input.message.branchName,
|
|
1220
|
+
sessionId: input.message.sessionId,
|
|
1221
|
+
artifactRoot: input.artifactRoot
|
|
1222
|
+
});
|
|
1223
|
+
if (artifact) {
|
|
1224
|
+
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1225
|
+
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1226
|
+
}
|
|
1227
|
+
const extension2 = path.extname(artifact.absolutePath).toLowerCase();
|
|
1228
|
+
const mediaType2 = extension2 === ".jpg" || extension2 === ".jpeg" ? "image/jpeg" : extension2 === ".png" ? "image/png" : null;
|
|
1229
|
+
if (!mediaType2) {
|
|
1230
|
+
throw new Error("view_image supports PNG and JPEG files only");
|
|
1231
|
+
}
|
|
1232
|
+
const bytes2 = fs.readFileSync(artifact.absolutePath);
|
|
1233
|
+
const dimensions2 = readImageDimensions(bytes2, mediaType2);
|
|
1234
|
+
return {
|
|
1235
|
+
type: "view_file_bytes",
|
|
1236
|
+
filePath: artifact.virtualPath,
|
|
1237
|
+
mediaType: mediaType2,
|
|
1238
|
+
base64: bytes2.toString("base64"),
|
|
1239
|
+
fileSizeBytes: bytes2.length,
|
|
1240
|
+
width: dimensions2.width,
|
|
1241
|
+
height: dimensions2.height
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1016
1244
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1017
1245
|
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
1018
1246
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
@@ -1189,6 +1417,18 @@ async function executeCommand(input) {
|
|
|
1189
1417
|
branchName: input.message.branchName,
|
|
1190
1418
|
manifest: input.manifest
|
|
1191
1419
|
});
|
|
1420
|
+
let artifactProcessEnv = {};
|
|
1421
|
+
if (input.message.sessionId) {
|
|
1422
|
+
await syncSessionArtifacts({
|
|
1423
|
+
baseUrl: input.baseUrl,
|
|
1424
|
+
token: input.token,
|
|
1425
|
+
projectId: input.projectId,
|
|
1426
|
+
branchName: input.message.branchName,
|
|
1427
|
+
sessionId: input.message.sessionId,
|
|
1428
|
+
artifactRoot: input.artifactRoot
|
|
1429
|
+
});
|
|
1430
|
+
artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1431
|
+
}
|
|
1192
1432
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1193
1433
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1194
1434
|
cwd,
|
|
@@ -1198,7 +1438,8 @@ async function executeCommand(input) {
|
|
|
1198
1438
|
...process.env,
|
|
1199
1439
|
...containerRegistryEnv(),
|
|
1200
1440
|
...input.message.env ?? {},
|
|
1201
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1441
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1442
|
+
...artifactProcessEnv
|
|
1202
1443
|
}
|
|
1203
1444
|
});
|
|
1204
1445
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1261,6 +1502,15 @@ async function executeStreamingCommand(input) {
|
|
|
1261
1502
|
branchName: input.message.branchName,
|
|
1262
1503
|
manifest: input.manifest
|
|
1263
1504
|
});
|
|
1505
|
+
await syncSessionArtifacts({
|
|
1506
|
+
baseUrl: input.baseUrl,
|
|
1507
|
+
token: input.token,
|
|
1508
|
+
projectId: input.projectId,
|
|
1509
|
+
branchName: input.message.branchName,
|
|
1510
|
+
sessionId: input.message.sessionId,
|
|
1511
|
+
artifactRoot: input.artifactRoot
|
|
1512
|
+
});
|
|
1513
|
+
const artifactProcessEnv = artifactEnv(input.artifactRoot, input.message.sessionId);
|
|
1264
1514
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1265
1515
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1266
1516
|
cwd,
|
|
@@ -1270,7 +1520,8 @@ async function executeStreamingCommand(input) {
|
|
|
1270
1520
|
...process.env,
|
|
1271
1521
|
...containerRegistryEnv(),
|
|
1272
1522
|
...input.message.env ?? {},
|
|
1273
|
-
...internalGitProcessEnv(workspace, input.message.env)
|
|
1523
|
+
...internalGitProcessEnv(workspace, input.message.env),
|
|
1524
|
+
...artifactProcessEnv
|
|
1274
1525
|
}
|
|
1275
1526
|
});
|
|
1276
1527
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1645,11 +1896,14 @@ async function startWorker(options) {
|
|
|
1645
1896
|
validateLabel(label);
|
|
1646
1897
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1647
1898
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1899
|
+
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
1648
1900
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1649
1901
|
`);
|
|
1650
1902
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1651
1903
|
`);
|
|
1652
1904
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1905
|
+
`);
|
|
1906
|
+
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
1653
1907
|
`);
|
|
1654
1908
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1655
1909
|
`);
|
|
@@ -1657,6 +1911,7 @@ async function startWorker(options) {
|
|
|
1657
1911
|
await verifyR5dctlAuth(baseUrl, token);
|
|
1658
1912
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1659
1913
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1914
|
+
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
1660
1915
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1661
1916
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1662
1917
|
headers: {
|
|
@@ -1672,7 +1927,8 @@ async function startWorker(options) {
|
|
|
1672
1927
|
arch: process.arch,
|
|
1673
1928
|
pid: process.pid,
|
|
1674
1929
|
version: getWorkerVersion(),
|
|
1675
|
-
projectRoot: projectsRoot
|
|
1930
|
+
projectRoot: projectsRoot,
|
|
1931
|
+
artifactRoot
|
|
1676
1932
|
}
|
|
1677
1933
|
};
|
|
1678
1934
|
ws.send(JSON.stringify(hello));
|
|
@@ -1769,7 +2025,7 @@ async function startWorker(options) {
|
|
|
1769
2025
|
const result = await withBranchLock(
|
|
1770
2026
|
message.projectId,
|
|
1771
2027
|
message.branchName,
|
|
1772
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2028
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1773
2029
|
);
|
|
1774
2030
|
ws.send(JSON.stringify(result));
|
|
1775
2031
|
return;
|
|
@@ -1788,7 +2044,7 @@ async function startWorker(options) {
|
|
|
1788
2044
|
void withBranchLock(
|
|
1789
2045
|
message.projectId,
|
|
1790
2046
|
message.branchName,
|
|
1791
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2047
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1792
2048
|
).catch((error) => {
|
|
1793
2049
|
sendWorkerMessage(ws, {
|
|
1794
2050
|
type: "exec_start_error",
|
|
@@ -1814,7 +2070,7 @@ async function startWorker(options) {
|
|
|
1814
2070
|
const result = await withBranchLock(
|
|
1815
2071
|
message.projectId,
|
|
1816
2072
|
message.branchName,
|
|
1817
|
-
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
2073
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
1818
2074
|
);
|
|
1819
2075
|
ws.send(
|
|
1820
2076
|
JSON.stringify({
|
|
@@ -1876,7 +2132,13 @@ async function main() {
|
|
|
1876
2132
|
}
|
|
1877
2133
|
await startWorker(parsed.options);
|
|
1878
2134
|
}
|
|
1879
|
-
|
|
2135
|
+
function isCliEntrypoint() {
|
|
2136
|
+
if (import.meta.main) {
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
return typeof require === "function" && typeof module !== "undefined" && require.main === module;
|
|
2140
|
+
}
|
|
2141
|
+
if (isCliEntrypoint()) {
|
|
1880
2142
|
main().catch((error) => {
|
|
1881
2143
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
1882
2144
|
`);
|
|
@@ -1884,5 +2146,7 @@ if (import.meta.main) {
|
|
|
1884
2146
|
});
|
|
1885
2147
|
}
|
|
1886
2148
|
export {
|
|
1887
|
-
ensureVisibleGitCheckout
|
|
2149
|
+
ensureVisibleGitCheckout,
|
|
2150
|
+
isArtifactEnvPath,
|
|
2151
|
+
syncSessionArtifacts
|
|
1888
2152
|
};
|
package/dist/mjs/package.json
CHANGED
package/dist/types/main.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
export declare function isArtifactEnvPath(filePath: string): boolean;
|
|
3
|
+
export declare function syncSessionArtifacts(input: {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
token: string;
|
|
6
|
+
projectId: string;
|
|
7
|
+
branchName: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
artifactRoot: string;
|
|
10
|
+
}): Promise<string>;
|
|
2
11
|
type GitAuth = {
|
|
3
12
|
extraHeaderUrl: string;
|
|
4
13
|
header: string;
|