@ricsam/r5d-worker 0.0.14 → 0.0.15
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 +211 -57
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +209 -57
- package/dist/mjs/package.json +1 -1
- package/dist/types/main.d.ts +15 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -32,7 +32,9 @@ __export(main_exports, {
|
|
|
32
32
|
ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
|
|
33
33
|
isArtifactEnvPath: () => isArtifactEnvPath,
|
|
34
34
|
prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
|
|
35
|
+
preparePlanEnvForShell: () => preparePlanEnvForShell,
|
|
35
36
|
resolveProjectFilePath: () => resolveProjectFilePath,
|
|
37
|
+
syncProjectPlans: () => syncProjectPlans,
|
|
36
38
|
syncSessionArtifacts: () => syncSessionArtifacts
|
|
37
39
|
});
|
|
38
40
|
module.exports = __toCommonJS(main_exports);
|
|
@@ -71,6 +73,9 @@ function defaultSyncRoot() {
|
|
|
71
73
|
function defaultArtifactRoot() {
|
|
72
74
|
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "artifacts");
|
|
73
75
|
}
|
|
76
|
+
function defaultPlanRoot() {
|
|
77
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "plans");
|
|
78
|
+
}
|
|
74
79
|
function printHelp() {
|
|
75
80
|
process.stdout.write(`Usage:
|
|
76
81
|
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
@@ -85,6 +90,7 @@ Options:
|
|
|
85
90
|
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
86
91
|
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
87
92
|
--artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
|
|
93
|
+
--plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
|
|
88
94
|
-v, --version Show r5d-worker version
|
|
89
95
|
-h, --help Show this help
|
|
90
96
|
`);
|
|
@@ -206,6 +212,11 @@ function parseArgs(argv) {
|
|
|
206
212
|
options.artifactRoot = artifactRoot;
|
|
207
213
|
continue;
|
|
208
214
|
}
|
|
215
|
+
const planRoot = readOption("--plan-root");
|
|
216
|
+
if (planRoot !== void 0) {
|
|
217
|
+
options.planRoot = planRoot;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
209
220
|
rest.push(arg);
|
|
210
221
|
}
|
|
211
222
|
if (options.version) {
|
|
@@ -249,6 +260,7 @@ async function readResponseText(response) {
|
|
|
249
260
|
}
|
|
250
261
|
}
|
|
251
262
|
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
263
|
+
const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
|
|
252
264
|
function validateArtifactSessionId(sessionId) {
|
|
253
265
|
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)) {
|
|
254
266
|
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
@@ -257,7 +269,7 @@ function validateArtifactSessionId(sessionId) {
|
|
|
257
269
|
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
258
270
|
const relative = import_node_path.default.relative(rootPath, candidatePath);
|
|
259
271
|
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
260
|
-
throw new Error(`${label} escapes
|
|
272
|
+
throw new Error(`${label} escapes sync directory`);
|
|
261
273
|
}
|
|
262
274
|
}
|
|
263
275
|
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
@@ -272,6 +284,23 @@ function artifactEnv(artifactRoot, sessionId) {
|
|
|
272
284
|
R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
|
|
273
285
|
};
|
|
274
286
|
}
|
|
287
|
+
function projectPlanDir(planRoot, projectId, branchName) {
|
|
288
|
+
validateProjectId(projectId);
|
|
289
|
+
validateBranchName(branchName);
|
|
290
|
+
return import_node_path.default.join(planRoot, projectId, branchName);
|
|
291
|
+
}
|
|
292
|
+
function planEnv(planRoot, projectId, branchName, activePlanId) {
|
|
293
|
+
const plansDir = projectPlanDir(planRoot, projectId, branchName);
|
|
294
|
+
const env = {
|
|
295
|
+
[R5D_PLANS_DIR_ENV]: plansDir
|
|
296
|
+
};
|
|
297
|
+
if (activePlanId) {
|
|
298
|
+
validatePlanId(activePlanId);
|
|
299
|
+
env.R5D_ACTIVE_PLAN_ID = activePlanId;
|
|
300
|
+
env.R5D_ACTIVE_PLAN_FILE = import_node_path.default.join(plansDir, `${activePlanId}.plan.md`);
|
|
301
|
+
}
|
|
302
|
+
return env;
|
|
303
|
+
}
|
|
275
304
|
function isArtifactEnvPath(filePath) {
|
|
276
305
|
return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
|
|
277
306
|
}
|
|
@@ -284,99 +313,150 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
|
|
|
284
313
|
function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
|
|
285
314
|
return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
286
315
|
}
|
|
316
|
+
function planApiBasePath(projectId, branchName) {
|
|
317
|
+
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
|
|
318
|
+
}
|
|
319
|
+
function planManifestUrl(baseUrl, projectId, branchName) {
|
|
320
|
+
return new URL(planApiBasePath(projectId, branchName), baseUrl);
|
|
321
|
+
}
|
|
322
|
+
function planDownloadUrl(baseUrl, projectId, branchName, filename) {
|
|
323
|
+
return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
324
|
+
}
|
|
287
325
|
function sha256Path(filePath) {
|
|
288
326
|
return (0, import_node_crypto.createHash)("sha256").update(import_node_fs.default.readFileSync(filePath)).digest("hex");
|
|
289
327
|
}
|
|
290
|
-
function
|
|
291
|
-
if (!value || typeof value !== "object" || !Array.isArray(value
|
|
292
|
-
throw new Error(
|
|
328
|
+
function parseRemoteFileManifest(value, collectionKey, label) {
|
|
329
|
+
if (!value || typeof value !== "object" || !Array.isArray(value[collectionKey])) {
|
|
330
|
+
throw new Error(`${label} manifest response is invalid`);
|
|
293
331
|
}
|
|
294
|
-
return value.
|
|
332
|
+
return value[collectionKey].map((entry) => {
|
|
295
333
|
if (!entry || typeof entry !== "object") {
|
|
296
|
-
throw new Error(
|
|
334
|
+
throw new Error(`${label} manifest entry is invalid`);
|
|
297
335
|
}
|
|
298
|
-
const
|
|
299
|
-
if (typeof
|
|
300
|
-
throw new Error(
|
|
336
|
+
const file = entry;
|
|
337
|
+
if (typeof file.filename !== "string" || file.filename.includes("/") || file.filename.includes("\\")) {
|
|
338
|
+
throw new Error(`${label} manifest entry has an invalid filename`);
|
|
301
339
|
}
|
|
302
|
-
if (typeof
|
|
303
|
-
throw new Error(
|
|
340
|
+
if (typeof file.size !== "number" || !Number.isFinite(file.size) || file.size < 0) {
|
|
341
|
+
throw new Error(`${label} manifest entry has an invalid size: ${file.filename}`);
|
|
304
342
|
}
|
|
305
|
-
if (typeof
|
|
306
|
-
throw new Error(
|
|
343
|
+
if (typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(file.sha256)) {
|
|
344
|
+
throw new Error(`${label} manifest entry has an invalid sha256: ${file.filename}`);
|
|
307
345
|
}
|
|
308
346
|
return {
|
|
309
|
-
filename:
|
|
310
|
-
size:
|
|
311
|
-
sha256:
|
|
347
|
+
filename: file.filename,
|
|
348
|
+
size: file.size,
|
|
349
|
+
sha256: file.sha256.toLowerCase()
|
|
312
350
|
};
|
|
313
351
|
});
|
|
314
352
|
}
|
|
315
|
-
async function
|
|
316
|
-
const response = await fetch(
|
|
353
|
+
async function fetchRemoteFileManifest(input) {
|
|
354
|
+
const response = await fetch(input.url, {
|
|
317
355
|
headers: {
|
|
318
356
|
Authorization: `Bearer ${input.token}`
|
|
319
357
|
}
|
|
320
358
|
});
|
|
321
359
|
if (!response.ok) {
|
|
322
360
|
const detail = (await readResponseText(response)).trim();
|
|
323
|
-
throw new Error(`Failed to fetch
|
|
361
|
+
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
324
362
|
}
|
|
325
|
-
return
|
|
363
|
+
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
326
364
|
}
|
|
327
|
-
async function
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
{
|
|
334
|
-
headers: {
|
|
335
|
-
Authorization: `Bearer ${input.token}`
|
|
336
|
-
}
|
|
365
|
+
async function downloadRemoteSyncedFile(input) {
|
|
366
|
+
const targetPath = import_node_path.default.join(input.targetDir, input.file.filename);
|
|
367
|
+
assertInsideRoot(input.targetDir, targetPath, input.label);
|
|
368
|
+
const response = await fetch(input.url, {
|
|
369
|
+
headers: {
|
|
370
|
+
Authorization: `Bearer ${input.token}`
|
|
337
371
|
}
|
|
338
|
-
);
|
|
372
|
+
});
|
|
339
373
|
if (!response.ok) {
|
|
340
374
|
const detail = (await readResponseText(response)).trim();
|
|
341
|
-
throw new Error(`Failed to download
|
|
375
|
+
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
342
376
|
}
|
|
343
377
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
344
378
|
const receivedHash = (0, import_node_crypto.createHash)("sha256").update(buffer).digest("hex");
|
|
345
|
-
if (receivedHash !== input.
|
|
346
|
-
throw new Error(`Downloaded
|
|
379
|
+
if (receivedHash !== input.file.sha256) {
|
|
380
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
347
381
|
}
|
|
348
|
-
if (buffer.length !== input.
|
|
349
|
-
throw new Error(`Downloaded
|
|
382
|
+
if (buffer.length !== input.file.size) {
|
|
383
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
350
384
|
}
|
|
351
385
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
352
386
|
import_node_fs.default.writeFileSync(tempPath, buffer);
|
|
353
387
|
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
354
388
|
}
|
|
355
|
-
async function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const localPath = import_node_path.default.join(targetDir, artifact.filename);
|
|
362
|
-
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
389
|
+
async function syncRemoteFileSet(input) {
|
|
390
|
+
import_node_fs.default.mkdirSync(input.targetDir, { recursive: true });
|
|
391
|
+
const manifestNames = new Set(input.files.map((file) => file.filename));
|
|
392
|
+
for (const file of input.files) {
|
|
393
|
+
const localPath = import_node_path.default.join(input.targetDir, file.filename);
|
|
394
|
+
assertInsideRoot(input.targetDir, localPath, input.label);
|
|
363
395
|
if (import_node_fs.default.existsSync(localPath)) {
|
|
364
396
|
const stats = import_node_fs.default.statSync(localPath);
|
|
365
|
-
if (stats.isFile() && stats.size ===
|
|
397
|
+
if (stats.isFile() && stats.size === file.size && sha256Path(localPath) === file.sha256) {
|
|
366
398
|
continue;
|
|
367
399
|
}
|
|
368
400
|
import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
|
|
369
401
|
}
|
|
370
|
-
await
|
|
402
|
+
await input.download(file);
|
|
371
403
|
}
|
|
372
|
-
for (const localName of import_node_fs.default.readdirSync(targetDir)) {
|
|
404
|
+
for (const localName of import_node_fs.default.readdirSync(input.targetDir)) {
|
|
373
405
|
if (!manifestNames.has(localName)) {
|
|
374
|
-
const localPath = import_node_path.default.join(targetDir, localName);
|
|
375
|
-
assertInsideRoot(targetDir, localPath,
|
|
406
|
+
const localPath = import_node_path.default.join(input.targetDir, localName);
|
|
407
|
+
assertInsideRoot(input.targetDir, localPath, input.label);
|
|
376
408
|
import_node_fs.default.rmSync(localPath, { recursive: true, force: true });
|
|
377
409
|
}
|
|
378
410
|
}
|
|
379
|
-
return targetDir;
|
|
411
|
+
return input.targetDir;
|
|
412
|
+
}
|
|
413
|
+
async function fetchSessionArtifactManifest(input) {
|
|
414
|
+
return fetchRemoteFileManifest({
|
|
415
|
+
url: artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId),
|
|
416
|
+
token: input.token,
|
|
417
|
+
collectionKey: "artifacts",
|
|
418
|
+
label: "artifact"
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async function syncSessionArtifacts(input) {
|
|
422
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
423
|
+
const artifacts = await fetchSessionArtifactManifest(input);
|
|
424
|
+
return syncRemoteFileSet({
|
|
425
|
+
targetDir,
|
|
426
|
+
files: artifacts,
|
|
427
|
+
label: "Artifact path",
|
|
428
|
+
download: (artifact) => downloadRemoteSyncedFile({
|
|
429
|
+
url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
|
|
430
|
+
token: input.token,
|
|
431
|
+
targetDir,
|
|
432
|
+
file: artifact,
|
|
433
|
+
label: "artifact"
|
|
434
|
+
})
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
async function fetchProjectPlanManifest(input) {
|
|
438
|
+
return fetchRemoteFileManifest({
|
|
439
|
+
url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
|
|
440
|
+
token: input.token,
|
|
441
|
+
collectionKey: "plans",
|
|
442
|
+
label: "plan"
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async function syncProjectPlans(input) {
|
|
446
|
+
const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
447
|
+
const plans = await fetchProjectPlanManifest(input);
|
|
448
|
+
return syncRemoteFileSet({
|
|
449
|
+
targetDir,
|
|
450
|
+
files: plans,
|
|
451
|
+
label: "Plan path",
|
|
452
|
+
download: (plan) => downloadRemoteSyncedFile({
|
|
453
|
+
url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
|
|
454
|
+
token: input.token,
|
|
455
|
+
targetDir,
|
|
456
|
+
file: plan,
|
|
457
|
+
label: "plan"
|
|
458
|
+
})
|
|
459
|
+
});
|
|
380
460
|
}
|
|
381
461
|
async function resolveArtifactEnvFilePath(input) {
|
|
382
462
|
if (!isArtifactEnvPath(input.filePath)) {
|
|
@@ -435,6 +515,19 @@ async function prepareArtifactEnvForShell(input) {
|
|
|
435
515
|
R5D_ARTIFACTS_DIR: artifactsDir
|
|
436
516
|
};
|
|
437
517
|
}
|
|
518
|
+
async function preparePlanEnvForShell(input) {
|
|
519
|
+
try {
|
|
520
|
+
await syncProjectPlans(input);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
process.stderr.write(
|
|
523
|
+
`[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
524
|
+
`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
528
|
+
import_node_fs.default.mkdirSync(plansDir, { recursive: true });
|
|
529
|
+
return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
|
|
530
|
+
}
|
|
438
531
|
async function verifyR5dctlAuth(baseUrl, token) {
|
|
439
532
|
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
440
533
|
let response;
|
|
@@ -479,11 +572,24 @@ function validateLabel(label) {
|
|
|
479
572
|
throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
|
|
480
573
|
}
|
|
481
574
|
}
|
|
575
|
+
function validateProjectId(projectId) {
|
|
576
|
+
if (projectId === "local" || projectId === "local-code") {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) {
|
|
580
|
+
throw new Error(`Invalid project id from server: ${projectId}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
482
583
|
function validateBranchName(branchName) {
|
|
483
584
|
if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
|
|
484
585
|
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
485
586
|
}
|
|
486
587
|
}
|
|
588
|
+
function validatePlanId(planId) {
|
|
589
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
590
|
+
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
487
593
|
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
488
594
|
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
489
595
|
}
|
|
@@ -1532,6 +1638,14 @@ async function executeCommand(input) {
|
|
|
1532
1638
|
sessionId: input.message.sessionId,
|
|
1533
1639
|
artifactRoot: input.artifactRoot
|
|
1534
1640
|
});
|
|
1641
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1642
|
+
baseUrl: input.baseUrl,
|
|
1643
|
+
token: input.token,
|
|
1644
|
+
projectId: input.projectId,
|
|
1645
|
+
branchName: input.message.branchName,
|
|
1646
|
+
planRoot: input.planRoot,
|
|
1647
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1648
|
+
});
|
|
1535
1649
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1536
1650
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1537
1651
|
cwd,
|
|
@@ -1542,7 +1656,8 @@ async function executeCommand(input) {
|
|
|
1542
1656
|
...containerRegistryEnv(),
|
|
1543
1657
|
...input.message.env ?? {},
|
|
1544
1658
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1545
|
-
...artifactProcessEnv
|
|
1659
|
+
...artifactProcessEnv,
|
|
1660
|
+
...planProcessEnv
|
|
1546
1661
|
}
|
|
1547
1662
|
});
|
|
1548
1663
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1613,6 +1728,14 @@ async function executeStreamingCommand(input) {
|
|
|
1613
1728
|
sessionId: input.message.sessionId,
|
|
1614
1729
|
artifactRoot: input.artifactRoot
|
|
1615
1730
|
});
|
|
1731
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1732
|
+
baseUrl: input.baseUrl,
|
|
1733
|
+
token: input.token,
|
|
1734
|
+
projectId: input.projectId,
|
|
1735
|
+
branchName: input.message.branchName,
|
|
1736
|
+
planRoot: input.planRoot,
|
|
1737
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1738
|
+
});
|
|
1616
1739
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1617
1740
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1618
1741
|
cwd,
|
|
@@ -1623,7 +1746,8 @@ async function executeStreamingCommand(input) {
|
|
|
1623
1746
|
...containerRegistryEnv(),
|
|
1624
1747
|
...input.message.env ?? {},
|
|
1625
1748
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1626
|
-
...artifactProcessEnv
|
|
1749
|
+
...artifactProcessEnv,
|
|
1750
|
+
...planProcessEnv
|
|
1627
1751
|
}
|
|
1628
1752
|
});
|
|
1629
1753
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1875,7 +1999,7 @@ function resolveHostShell() {
|
|
|
1875
1999
|
}
|
|
1876
2000
|
return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
|
|
1877
2001
|
}
|
|
1878
|
-
function openPty(input) {
|
|
2002
|
+
async function openPty(input) {
|
|
1879
2003
|
try {
|
|
1880
2004
|
const branchPath = ensureBranchWorkspace({
|
|
1881
2005
|
projectId: input.projectId,
|
|
@@ -1886,6 +2010,14 @@ function openPty(input) {
|
|
|
1886
2010
|
branchName: input.message.branchName,
|
|
1887
2011
|
manifest: input.manifest
|
|
1888
2012
|
});
|
|
2013
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
2014
|
+
baseUrl: input.baseUrl,
|
|
2015
|
+
token: input.token,
|
|
2016
|
+
projectId: input.projectId,
|
|
2017
|
+
branchName: input.message.branchName,
|
|
2018
|
+
planRoot: input.planRoot,
|
|
2019
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2020
|
+
});
|
|
1889
2021
|
const shell = resolveHostShell();
|
|
1890
2022
|
const ptyProcess = createNodePtyBridge({
|
|
1891
2023
|
file: shell.file,
|
|
@@ -1899,6 +2031,7 @@ function openPty(input) {
|
|
|
1899
2031
|
...process.env,
|
|
1900
2032
|
...containerRegistryEnv(),
|
|
1901
2033
|
...input.message.env ?? {},
|
|
2034
|
+
...planProcessEnv,
|
|
1902
2035
|
R5D_PROJECT_ID: input.projectId,
|
|
1903
2036
|
R5D_BRANCH_NAME: input.message.branchName
|
|
1904
2037
|
}
|
|
@@ -1999,6 +2132,7 @@ async function startWorker(options) {
|
|
|
1999
2132
|
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2000
2133
|
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2001
2134
|
const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2135
|
+
const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
2002
2136
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
2003
2137
|
`);
|
|
2004
2138
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -2006,6 +2140,8 @@ async function startWorker(options) {
|
|
|
2006
2140
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
2007
2141
|
`);
|
|
2008
2142
|
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
2143
|
+
`);
|
|
2144
|
+
process.stdout.write(`[r5d-worker] plan root: ${planRoot}
|
|
2009
2145
|
`);
|
|
2010
2146
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
2011
2147
|
`);
|
|
@@ -2014,6 +2150,7 @@ async function startWorker(options) {
|
|
|
2014
2150
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
2015
2151
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
2016
2152
|
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
2153
|
+
import_node_fs.default.mkdirSync(planRoot, { recursive: true });
|
|
2017
2154
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2018
2155
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2019
2156
|
headers: {
|
|
@@ -2030,7 +2167,8 @@ async function startWorker(options) {
|
|
|
2030
2167
|
pid: process.pid,
|
|
2031
2168
|
version: getWorkerVersion(),
|
|
2032
2169
|
projectRoot: projectsRoot,
|
|
2033
|
-
artifactRoot
|
|
2170
|
+
artifactRoot,
|
|
2171
|
+
planRoot
|
|
2034
2172
|
}
|
|
2035
2173
|
};
|
|
2036
2174
|
ws.send(JSON.stringify(hello));
|
|
@@ -2066,6 +2204,20 @@ async function startWorker(options) {
|
|
|
2066
2204
|
branchName,
|
|
2067
2205
|
manifest: project
|
|
2068
2206
|
});
|
|
2207
|
+
try {
|
|
2208
|
+
await syncProjectPlans({
|
|
2209
|
+
baseUrl,
|
|
2210
|
+
token,
|
|
2211
|
+
projectId: project.projectId,
|
|
2212
|
+
branchName,
|
|
2213
|
+
planRoot
|
|
2214
|
+
});
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
process.stderr.write(
|
|
2217
|
+
`[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2218
|
+
`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2069
2221
|
});
|
|
2070
2222
|
} catch (error) {
|
|
2071
2223
|
process.stderr.write(
|
|
@@ -2104,7 +2256,7 @@ async function startWorker(options) {
|
|
|
2104
2256
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2105
2257
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
2106
2258
|
`);
|
|
2107
|
-
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
2259
|
+
await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
|
|
2108
2260
|
return;
|
|
2109
2261
|
}
|
|
2110
2262
|
if (message.type === "pty_input") {
|
|
@@ -2127,7 +2279,7 @@ async function startWorker(options) {
|
|
|
2127
2279
|
const result = await withBranchLock(
|
|
2128
2280
|
message.projectId,
|
|
2129
2281
|
message.branchName,
|
|
2130
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
2282
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
|
|
2131
2283
|
);
|
|
2132
2284
|
ws.send(JSON.stringify(result));
|
|
2133
2285
|
return;
|
|
@@ -2146,7 +2298,7 @@ async function startWorker(options) {
|
|
|
2146
2298
|
void withBranchLock(
|
|
2147
2299
|
message.projectId,
|
|
2148
2300
|
message.branchName,
|
|
2149
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
2301
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
|
|
2150
2302
|
).catch((error) => {
|
|
2151
2303
|
sendWorkerMessage(ws, {
|
|
2152
2304
|
type: "exec_start_error",
|
|
@@ -2252,6 +2404,8 @@ if (isCliEntrypoint()) {
|
|
|
2252
2404
|
ensureVisibleGitCheckout,
|
|
2253
2405
|
isArtifactEnvPath,
|
|
2254
2406
|
prepareArtifactEnvForShell,
|
|
2407
|
+
preparePlanEnvForShell,
|
|
2255
2408
|
resolveProjectFilePath,
|
|
2409
|
+
syncProjectPlans,
|
|
2256
2410
|
syncSessionArtifacts
|
|
2257
2411
|
});
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -33,6 +33,9 @@ function defaultSyncRoot() {
|
|
|
33
33
|
function defaultArtifactRoot() {
|
|
34
34
|
return path.join(os.homedir(), ".r5d", "artifacts");
|
|
35
35
|
}
|
|
36
|
+
function defaultPlanRoot() {
|
|
37
|
+
return path.join(os.homedir(), ".r5d", "plans");
|
|
38
|
+
}
|
|
36
39
|
function printHelp() {
|
|
37
40
|
process.stdout.write(`Usage:
|
|
38
41
|
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
@@ -47,6 +50,7 @@ Options:
|
|
|
47
50
|
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
48
51
|
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
49
52
|
--artifact-root <dir> Override session artifact root (default from R5D_ARTIFACTS_ROOT or ~/.r5d/artifacts)
|
|
53
|
+
--plan-root <dir> Override synced plan root (default from R5D_PLANS_ROOT or ~/.r5d/plans)
|
|
50
54
|
-v, --version Show r5d-worker version
|
|
51
55
|
-h, --help Show this help
|
|
52
56
|
`);
|
|
@@ -168,6 +172,11 @@ function parseArgs(argv) {
|
|
|
168
172
|
options.artifactRoot = artifactRoot;
|
|
169
173
|
continue;
|
|
170
174
|
}
|
|
175
|
+
const planRoot = readOption("--plan-root");
|
|
176
|
+
if (planRoot !== void 0) {
|
|
177
|
+
options.planRoot = planRoot;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
171
180
|
rest.push(arg);
|
|
172
181
|
}
|
|
173
182
|
if (options.version) {
|
|
@@ -211,6 +220,7 @@ async function readResponseText(response) {
|
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
223
|
+
const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
|
|
214
224
|
function validateArtifactSessionId(sessionId) {
|
|
215
225
|
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
226
|
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
@@ -219,7 +229,7 @@ function validateArtifactSessionId(sessionId) {
|
|
|
219
229
|
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
220
230
|
const relative = path.relative(rootPath, candidatePath);
|
|
221
231
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
222
|
-
throw new Error(`${label} escapes
|
|
232
|
+
throw new Error(`${label} escapes sync directory`);
|
|
223
233
|
}
|
|
224
234
|
}
|
|
225
235
|
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
@@ -234,6 +244,23 @@ function artifactEnv(artifactRoot, sessionId) {
|
|
|
234
244
|
R5D_ARTIFACTS_DIR: sessionArtifactDir(artifactRoot, sessionId)
|
|
235
245
|
};
|
|
236
246
|
}
|
|
247
|
+
function projectPlanDir(planRoot, projectId, branchName) {
|
|
248
|
+
validateProjectId(projectId);
|
|
249
|
+
validateBranchName(branchName);
|
|
250
|
+
return path.join(planRoot, projectId, branchName);
|
|
251
|
+
}
|
|
252
|
+
function planEnv(planRoot, projectId, branchName, activePlanId) {
|
|
253
|
+
const plansDir = projectPlanDir(planRoot, projectId, branchName);
|
|
254
|
+
const env = {
|
|
255
|
+
[R5D_PLANS_DIR_ENV]: plansDir
|
|
256
|
+
};
|
|
257
|
+
if (activePlanId) {
|
|
258
|
+
validatePlanId(activePlanId);
|
|
259
|
+
env.R5D_ACTIVE_PLAN_ID = activePlanId;
|
|
260
|
+
env.R5D_ACTIVE_PLAN_FILE = path.join(plansDir, `${activePlanId}.plan.md`);
|
|
261
|
+
}
|
|
262
|
+
return env;
|
|
263
|
+
}
|
|
237
264
|
function isArtifactEnvPath(filePath) {
|
|
238
265
|
return filePath === R5D_ARTIFACTS_DIR_REF || filePath.startsWith(`${R5D_ARTIFACTS_DIR_REF}/`);
|
|
239
266
|
}
|
|
@@ -246,99 +273,150 @@ function artifactManifestUrl(baseUrl, projectId, branchName, sessionId) {
|
|
|
246
273
|
function artifactDownloadUrl(baseUrl, projectId, branchName, sessionId, filename) {
|
|
247
274
|
return new URL(`${artifactApiBasePath(projectId, branchName, sessionId)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
248
275
|
}
|
|
276
|
+
function planApiBasePath(projectId, branchName) {
|
|
277
|
+
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/plans`;
|
|
278
|
+
}
|
|
279
|
+
function planManifestUrl(baseUrl, projectId, branchName) {
|
|
280
|
+
return new URL(planApiBasePath(projectId, branchName), baseUrl);
|
|
281
|
+
}
|
|
282
|
+
function planDownloadUrl(baseUrl, projectId, branchName, filename) {
|
|
283
|
+
return new URL(`${planApiBasePath(projectId, branchName)}/${encodeURIComponent(filename)}`, baseUrl);
|
|
284
|
+
}
|
|
249
285
|
function sha256Path(filePath) {
|
|
250
286
|
return createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
251
287
|
}
|
|
252
|
-
function
|
|
253
|
-
if (!value || typeof value !== "object" || !Array.isArray(value
|
|
254
|
-
throw new Error(
|
|
288
|
+
function parseRemoteFileManifest(value, collectionKey, label) {
|
|
289
|
+
if (!value || typeof value !== "object" || !Array.isArray(value[collectionKey])) {
|
|
290
|
+
throw new Error(`${label} manifest response is invalid`);
|
|
255
291
|
}
|
|
256
|
-
return value.
|
|
292
|
+
return value[collectionKey].map((entry) => {
|
|
257
293
|
if (!entry || typeof entry !== "object") {
|
|
258
|
-
throw new Error(
|
|
294
|
+
throw new Error(`${label} manifest entry is invalid`);
|
|
259
295
|
}
|
|
260
|
-
const
|
|
261
|
-
if (typeof
|
|
262
|
-
throw new Error(
|
|
296
|
+
const file = entry;
|
|
297
|
+
if (typeof file.filename !== "string" || file.filename.includes("/") || file.filename.includes("\\")) {
|
|
298
|
+
throw new Error(`${label} manifest entry has an invalid filename`);
|
|
263
299
|
}
|
|
264
|
-
if (typeof
|
|
265
|
-
throw new Error(
|
|
300
|
+
if (typeof file.size !== "number" || !Number.isFinite(file.size) || file.size < 0) {
|
|
301
|
+
throw new Error(`${label} manifest entry has an invalid size: ${file.filename}`);
|
|
266
302
|
}
|
|
267
|
-
if (typeof
|
|
268
|
-
throw new Error(
|
|
303
|
+
if (typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/i.test(file.sha256)) {
|
|
304
|
+
throw new Error(`${label} manifest entry has an invalid sha256: ${file.filename}`);
|
|
269
305
|
}
|
|
270
306
|
return {
|
|
271
|
-
filename:
|
|
272
|
-
size:
|
|
273
|
-
sha256:
|
|
307
|
+
filename: file.filename,
|
|
308
|
+
size: file.size,
|
|
309
|
+
sha256: file.sha256.toLowerCase()
|
|
274
310
|
};
|
|
275
311
|
});
|
|
276
312
|
}
|
|
277
|
-
async function
|
|
278
|
-
const response = await fetch(
|
|
313
|
+
async function fetchRemoteFileManifest(input) {
|
|
314
|
+
const response = await fetch(input.url, {
|
|
279
315
|
headers: {
|
|
280
316
|
Authorization: `Bearer ${input.token}`
|
|
281
317
|
}
|
|
282
318
|
});
|
|
283
319
|
if (!response.ok) {
|
|
284
320
|
const detail = (await readResponseText(response)).trim();
|
|
285
|
-
throw new Error(`Failed to fetch
|
|
321
|
+
throw new Error(`Failed to fetch ${input.label} manifest (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
286
322
|
}
|
|
287
|
-
return
|
|
323
|
+
return parseRemoteFileManifest(await response.json(), input.collectionKey, input.label);
|
|
288
324
|
}
|
|
289
|
-
async function
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
headers: {
|
|
297
|
-
Authorization: `Bearer ${input.token}`
|
|
298
|
-
}
|
|
325
|
+
async function downloadRemoteSyncedFile(input) {
|
|
326
|
+
const targetPath = path.join(input.targetDir, input.file.filename);
|
|
327
|
+
assertInsideRoot(input.targetDir, targetPath, input.label);
|
|
328
|
+
const response = await fetch(input.url, {
|
|
329
|
+
headers: {
|
|
330
|
+
Authorization: `Bearer ${input.token}`
|
|
299
331
|
}
|
|
300
|
-
);
|
|
332
|
+
});
|
|
301
333
|
if (!response.ok) {
|
|
302
334
|
const detail = (await readResponseText(response)).trim();
|
|
303
|
-
throw new Error(`Failed to download
|
|
335
|
+
throw new Error(`Failed to download ${input.label} ${input.file.filename} (${response.status})${detail ? `: ${detail}` : ""}`);
|
|
304
336
|
}
|
|
305
337
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
306
338
|
const receivedHash = createHash("sha256").update(buffer).digest("hex");
|
|
307
|
-
if (receivedHash !== input.
|
|
308
|
-
throw new Error(`Downloaded
|
|
339
|
+
if (receivedHash !== input.file.sha256) {
|
|
340
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} failed sha256 verification`);
|
|
309
341
|
}
|
|
310
|
-
if (buffer.length !== input.
|
|
311
|
-
throw new Error(`Downloaded
|
|
342
|
+
if (buffer.length !== input.file.size) {
|
|
343
|
+
throw new Error(`Downloaded ${input.label} ${input.file.filename} has unexpected size`);
|
|
312
344
|
}
|
|
313
345
|
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
|
|
314
346
|
fs.writeFileSync(tempPath, buffer);
|
|
315
347
|
fs.renameSync(tempPath, targetPath);
|
|
316
348
|
}
|
|
317
|
-
async function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const localPath = path.join(targetDir, artifact.filename);
|
|
324
|
-
assertInsideRoot(targetDir, localPath, "Artifact path");
|
|
349
|
+
async function syncRemoteFileSet(input) {
|
|
350
|
+
fs.mkdirSync(input.targetDir, { recursive: true });
|
|
351
|
+
const manifestNames = new Set(input.files.map((file) => file.filename));
|
|
352
|
+
for (const file of input.files) {
|
|
353
|
+
const localPath = path.join(input.targetDir, file.filename);
|
|
354
|
+
assertInsideRoot(input.targetDir, localPath, input.label);
|
|
325
355
|
if (fs.existsSync(localPath)) {
|
|
326
356
|
const stats = fs.statSync(localPath);
|
|
327
|
-
if (stats.isFile() && stats.size ===
|
|
357
|
+
if (stats.isFile() && stats.size === file.size && sha256Path(localPath) === file.sha256) {
|
|
328
358
|
continue;
|
|
329
359
|
}
|
|
330
360
|
fs.rmSync(localPath, { recursive: true, force: true });
|
|
331
361
|
}
|
|
332
|
-
await
|
|
362
|
+
await input.download(file);
|
|
333
363
|
}
|
|
334
|
-
for (const localName of fs.readdirSync(targetDir)) {
|
|
364
|
+
for (const localName of fs.readdirSync(input.targetDir)) {
|
|
335
365
|
if (!manifestNames.has(localName)) {
|
|
336
|
-
const localPath = path.join(targetDir, localName);
|
|
337
|
-
assertInsideRoot(targetDir, localPath,
|
|
366
|
+
const localPath = path.join(input.targetDir, localName);
|
|
367
|
+
assertInsideRoot(input.targetDir, localPath, input.label);
|
|
338
368
|
fs.rmSync(localPath, { recursive: true, force: true });
|
|
339
369
|
}
|
|
340
370
|
}
|
|
341
|
-
return targetDir;
|
|
371
|
+
return input.targetDir;
|
|
372
|
+
}
|
|
373
|
+
async function fetchSessionArtifactManifest(input) {
|
|
374
|
+
return fetchRemoteFileManifest({
|
|
375
|
+
url: artifactManifestUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId),
|
|
376
|
+
token: input.token,
|
|
377
|
+
collectionKey: "artifacts",
|
|
378
|
+
label: "artifact"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async function syncSessionArtifacts(input) {
|
|
382
|
+
const targetDir = sessionArtifactDir(input.artifactRoot, input.sessionId);
|
|
383
|
+
const artifacts = await fetchSessionArtifactManifest(input);
|
|
384
|
+
return syncRemoteFileSet({
|
|
385
|
+
targetDir,
|
|
386
|
+
files: artifacts,
|
|
387
|
+
label: "Artifact path",
|
|
388
|
+
download: (artifact) => downloadRemoteSyncedFile({
|
|
389
|
+
url: artifactDownloadUrl(input.baseUrl, input.projectId, input.branchName, input.sessionId, artifact.filename),
|
|
390
|
+
token: input.token,
|
|
391
|
+
targetDir,
|
|
392
|
+
file: artifact,
|
|
393
|
+
label: "artifact"
|
|
394
|
+
})
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async function fetchProjectPlanManifest(input) {
|
|
398
|
+
return fetchRemoteFileManifest({
|
|
399
|
+
url: planManifestUrl(input.baseUrl, input.projectId, input.branchName),
|
|
400
|
+
token: input.token,
|
|
401
|
+
collectionKey: "plans",
|
|
402
|
+
label: "plan"
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
async function syncProjectPlans(input) {
|
|
406
|
+
const targetDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
407
|
+
const plans = await fetchProjectPlanManifest(input);
|
|
408
|
+
return syncRemoteFileSet({
|
|
409
|
+
targetDir,
|
|
410
|
+
files: plans,
|
|
411
|
+
label: "Plan path",
|
|
412
|
+
download: (plan) => downloadRemoteSyncedFile({
|
|
413
|
+
url: planDownloadUrl(input.baseUrl, input.projectId, input.branchName, plan.filename),
|
|
414
|
+
token: input.token,
|
|
415
|
+
targetDir,
|
|
416
|
+
file: plan,
|
|
417
|
+
label: "plan"
|
|
418
|
+
})
|
|
419
|
+
});
|
|
342
420
|
}
|
|
343
421
|
async function resolveArtifactEnvFilePath(input) {
|
|
344
422
|
if (!isArtifactEnvPath(input.filePath)) {
|
|
@@ -397,6 +475,19 @@ async function prepareArtifactEnvForShell(input) {
|
|
|
397
475
|
R5D_ARTIFACTS_DIR: artifactsDir
|
|
398
476
|
};
|
|
399
477
|
}
|
|
478
|
+
async function preparePlanEnvForShell(input) {
|
|
479
|
+
try {
|
|
480
|
+
await syncProjectPlans(input);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
process.stderr.write(
|
|
483
|
+
`[r5d-worker] plan sync skipped for ${input.projectId}/${input.branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
484
|
+
`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
488
|
+
fs.mkdirSync(plansDir, { recursive: true });
|
|
489
|
+
return planEnv(input.planRoot, input.projectId, input.branchName, input.activePlanId);
|
|
490
|
+
}
|
|
400
491
|
async function verifyR5dctlAuth(baseUrl, token) {
|
|
401
492
|
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
402
493
|
let response;
|
|
@@ -441,11 +532,24 @@ function validateLabel(label) {
|
|
|
441
532
|
throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
|
|
442
533
|
}
|
|
443
534
|
}
|
|
535
|
+
function validateProjectId(projectId) {
|
|
536
|
+
if (projectId === "local" || projectId === "local-code") {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) {
|
|
540
|
+
throw new Error(`Invalid project id from server: ${projectId}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
444
543
|
function validateBranchName(branchName) {
|
|
445
544
|
if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
|
|
446
545
|
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
447
546
|
}
|
|
448
547
|
}
|
|
548
|
+
function validatePlanId(planId) {
|
|
549
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(planId)) {
|
|
550
|
+
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
449
553
|
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
450
554
|
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
451
555
|
}
|
|
@@ -1494,6 +1598,14 @@ async function executeCommand(input) {
|
|
|
1494
1598
|
sessionId: input.message.sessionId,
|
|
1495
1599
|
artifactRoot: input.artifactRoot
|
|
1496
1600
|
});
|
|
1601
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1602
|
+
baseUrl: input.baseUrl,
|
|
1603
|
+
token: input.token,
|
|
1604
|
+
projectId: input.projectId,
|
|
1605
|
+
branchName: input.message.branchName,
|
|
1606
|
+
planRoot: input.planRoot,
|
|
1607
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1608
|
+
});
|
|
1497
1609
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1498
1610
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1499
1611
|
cwd,
|
|
@@ -1504,7 +1616,8 @@ async function executeCommand(input) {
|
|
|
1504
1616
|
...containerRegistryEnv(),
|
|
1505
1617
|
...input.message.env ?? {},
|
|
1506
1618
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1507
|
-
...artifactProcessEnv
|
|
1619
|
+
...artifactProcessEnv,
|
|
1620
|
+
...planProcessEnv
|
|
1508
1621
|
}
|
|
1509
1622
|
});
|
|
1510
1623
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1575,6 +1688,14 @@ async function executeStreamingCommand(input) {
|
|
|
1575
1688
|
sessionId: input.message.sessionId,
|
|
1576
1689
|
artifactRoot: input.artifactRoot
|
|
1577
1690
|
});
|
|
1691
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1692
|
+
baseUrl: input.baseUrl,
|
|
1693
|
+
token: input.token,
|
|
1694
|
+
projectId: input.projectId,
|
|
1695
|
+
branchName: input.message.branchName,
|
|
1696
|
+
planRoot: input.planRoot,
|
|
1697
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1698
|
+
});
|
|
1578
1699
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1579
1700
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1580
1701
|
cwd,
|
|
@@ -1585,7 +1706,8 @@ async function executeStreamingCommand(input) {
|
|
|
1585
1706
|
...containerRegistryEnv(),
|
|
1586
1707
|
...input.message.env ?? {},
|
|
1587
1708
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1588
|
-
...artifactProcessEnv
|
|
1709
|
+
...artifactProcessEnv,
|
|
1710
|
+
...planProcessEnv
|
|
1589
1711
|
}
|
|
1590
1712
|
});
|
|
1591
1713
|
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
@@ -1837,7 +1959,7 @@ function resolveHostShell() {
|
|
|
1837
1959
|
}
|
|
1838
1960
|
return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
|
|
1839
1961
|
}
|
|
1840
|
-
function openPty(input) {
|
|
1962
|
+
async function openPty(input) {
|
|
1841
1963
|
try {
|
|
1842
1964
|
const branchPath = ensureBranchWorkspace({
|
|
1843
1965
|
projectId: input.projectId,
|
|
@@ -1848,6 +1970,14 @@ function openPty(input) {
|
|
|
1848
1970
|
branchName: input.message.branchName,
|
|
1849
1971
|
manifest: input.manifest
|
|
1850
1972
|
});
|
|
1973
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1974
|
+
baseUrl: input.baseUrl,
|
|
1975
|
+
token: input.token,
|
|
1976
|
+
projectId: input.projectId,
|
|
1977
|
+
branchName: input.message.branchName,
|
|
1978
|
+
planRoot: input.planRoot,
|
|
1979
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1980
|
+
});
|
|
1851
1981
|
const shell = resolveHostShell();
|
|
1852
1982
|
const ptyProcess = createNodePtyBridge({
|
|
1853
1983
|
file: shell.file,
|
|
@@ -1861,6 +1991,7 @@ function openPty(input) {
|
|
|
1861
1991
|
...process.env,
|
|
1862
1992
|
...containerRegistryEnv(),
|
|
1863
1993
|
...input.message.env ?? {},
|
|
1994
|
+
...planProcessEnv,
|
|
1864
1995
|
R5D_PROJECT_ID: input.projectId,
|
|
1865
1996
|
R5D_BRANCH_NAME: input.message.branchName
|
|
1866
1997
|
}
|
|
@@ -1961,6 +2092,7 @@ async function startWorker(options) {
|
|
|
1961
2092
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1962
2093
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1963
2094
|
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2095
|
+
const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
1964
2096
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1965
2097
|
`);
|
|
1966
2098
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -1968,6 +2100,8 @@ async function startWorker(options) {
|
|
|
1968
2100
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1969
2101
|
`);
|
|
1970
2102
|
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
2103
|
+
`);
|
|
2104
|
+
process.stdout.write(`[r5d-worker] plan root: ${planRoot}
|
|
1971
2105
|
`);
|
|
1972
2106
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1973
2107
|
`);
|
|
@@ -1976,6 +2110,7 @@ async function startWorker(options) {
|
|
|
1976
2110
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1977
2111
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1978
2112
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2113
|
+
fs.mkdirSync(planRoot, { recursive: true });
|
|
1979
2114
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1980
2115
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1981
2116
|
headers: {
|
|
@@ -1992,7 +2127,8 @@ async function startWorker(options) {
|
|
|
1992
2127
|
pid: process.pid,
|
|
1993
2128
|
version: getWorkerVersion(),
|
|
1994
2129
|
projectRoot: projectsRoot,
|
|
1995
|
-
artifactRoot
|
|
2130
|
+
artifactRoot,
|
|
2131
|
+
planRoot
|
|
1996
2132
|
}
|
|
1997
2133
|
};
|
|
1998
2134
|
ws.send(JSON.stringify(hello));
|
|
@@ -2028,6 +2164,20 @@ async function startWorker(options) {
|
|
|
2028
2164
|
branchName,
|
|
2029
2165
|
manifest: project
|
|
2030
2166
|
});
|
|
2167
|
+
try {
|
|
2168
|
+
await syncProjectPlans({
|
|
2169
|
+
baseUrl,
|
|
2170
|
+
token,
|
|
2171
|
+
projectId: project.projectId,
|
|
2172
|
+
branchName,
|
|
2173
|
+
planRoot
|
|
2174
|
+
});
|
|
2175
|
+
} catch (error) {
|
|
2176
|
+
process.stderr.write(
|
|
2177
|
+
`[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2178
|
+
`
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2031
2181
|
});
|
|
2032
2182
|
} catch (error) {
|
|
2033
2183
|
process.stderr.write(
|
|
@@ -2066,7 +2216,7 @@ async function startWorker(options) {
|
|
|
2066
2216
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2067
2217
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
2068
2218
|
`);
|
|
2069
|
-
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
2219
|
+
await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
|
|
2070
2220
|
return;
|
|
2071
2221
|
}
|
|
2072
2222
|
if (message.type === "pty_input") {
|
|
@@ -2089,7 +2239,7 @@ async function startWorker(options) {
|
|
|
2089
2239
|
const result = await withBranchLock(
|
|
2090
2240
|
message.projectId,
|
|
2091
2241
|
message.branchName,
|
|
2092
|
-
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
2242
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
|
|
2093
2243
|
);
|
|
2094
2244
|
ws.send(JSON.stringify(result));
|
|
2095
2245
|
return;
|
|
@@ -2108,7 +2258,7 @@ async function startWorker(options) {
|
|
|
2108
2258
|
void withBranchLock(
|
|
2109
2259
|
message.projectId,
|
|
2110
2260
|
message.branchName,
|
|
2111
|
-
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, manifest })
|
|
2261
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, artifactRoot, planRoot, manifest })
|
|
2112
2262
|
).catch((error) => {
|
|
2113
2263
|
sendWorkerMessage(ws, {
|
|
2114
2264
|
type: "exec_start_error",
|
|
@@ -2213,6 +2363,8 @@ export {
|
|
|
2213
2363
|
ensureVisibleGitCheckout,
|
|
2214
2364
|
isArtifactEnvPath,
|
|
2215
2365
|
prepareArtifactEnvForShell,
|
|
2366
|
+
preparePlanEnvForShell,
|
|
2216
2367
|
resolveProjectFilePath,
|
|
2368
|
+
syncProjectPlans,
|
|
2217
2369
|
syncSessionArtifacts
|
|
2218
2370
|
};
|
package/dist/mjs/package.json
CHANGED
package/dist/types/main.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export declare function syncSessionArtifacts(input: {
|
|
|
8
8
|
sessionId: string;
|
|
9
9
|
artifactRoot: string;
|
|
10
10
|
}): Promise<string>;
|
|
11
|
+
export declare function syncProjectPlans(input: {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
token: string;
|
|
14
|
+
projectId: string;
|
|
15
|
+
branchName: string;
|
|
16
|
+
planRoot: string;
|
|
17
|
+
}): Promise<string>;
|
|
11
18
|
export declare function prepareArtifactEnvForShell(input: {
|
|
12
19
|
baseUrl: string;
|
|
13
20
|
token: string;
|
|
@@ -16,6 +23,14 @@ export declare function prepareArtifactEnvForShell(input: {
|
|
|
16
23
|
sessionId?: string;
|
|
17
24
|
artifactRoot: string;
|
|
18
25
|
}): Promise<Record<string, string>>;
|
|
26
|
+
export declare function preparePlanEnvForShell(input: {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
token: string;
|
|
29
|
+
projectId: string;
|
|
30
|
+
branchName: string;
|
|
31
|
+
planRoot: string;
|
|
32
|
+
activePlanId?: string;
|
|
33
|
+
}): Promise<Record<string, string>>;
|
|
19
34
|
type GitAuth = {
|
|
20
35
|
extraHeaderUrl: string;
|
|
21
36
|
header: string;
|