@ricsam/r5d-worker 0.0.14 → 0.0.16
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 +318 -120
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +316 -120
- 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);
|
|
@@ -56,7 +58,7 @@ const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
|
|
|
56
58
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
57
59
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
58
60
|
const activePtys = /* @__PURE__ */ new Map();
|
|
59
|
-
|
|
61
|
+
let currentWorkerSocket = null;
|
|
60
62
|
let containerRegistryCredential = null;
|
|
61
63
|
let visibleGitIdentity = null;
|
|
62
64
|
function defaultConfigPath() {
|
|
@@ -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
|
}
|
|
@@ -584,31 +690,6 @@ function resolveProjectFilePath(branchPath, virtualPath) {
|
|
|
584
690
|
assertInsideBranch(branchPath, absolutePath);
|
|
585
691
|
return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
|
|
586
692
|
}
|
|
587
|
-
function branchLockKey(projectId, branchName) {
|
|
588
|
-
return `${projectId}:${branchName}`;
|
|
589
|
-
}
|
|
590
|
-
async function withBranchLock(projectId, branchName, fn) {
|
|
591
|
-
const key = branchLockKey(projectId, branchName);
|
|
592
|
-
const previous = branchLocks.get(key) ?? Promise.resolve();
|
|
593
|
-
let release = () => {
|
|
594
|
-
};
|
|
595
|
-
const next = new Promise((resolve) => {
|
|
596
|
-
release = resolve;
|
|
597
|
-
});
|
|
598
|
-
const queued = previous.catch(() => {
|
|
599
|
-
}).then(() => next);
|
|
600
|
-
branchLocks.set(key, queued);
|
|
601
|
-
await previous.catch(() => {
|
|
602
|
-
});
|
|
603
|
-
try {
|
|
604
|
-
return await fn();
|
|
605
|
-
} finally {
|
|
606
|
-
release();
|
|
607
|
-
if (branchLocks.get(key) === queued) {
|
|
608
|
-
branchLocks.delete(key);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
693
|
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
613
694
|
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
614
695
|
return remoteUrl;
|
|
@@ -1532,6 +1613,14 @@ async function executeCommand(input) {
|
|
|
1532
1613
|
sessionId: input.message.sessionId,
|
|
1533
1614
|
artifactRoot: input.artifactRoot
|
|
1534
1615
|
});
|
|
1616
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1617
|
+
baseUrl: input.baseUrl,
|
|
1618
|
+
token: input.token,
|
|
1619
|
+
projectId: input.projectId,
|
|
1620
|
+
branchName: input.message.branchName,
|
|
1621
|
+
planRoot: input.planRoot,
|
|
1622
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1623
|
+
});
|
|
1535
1624
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1536
1625
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1537
1626
|
cwd,
|
|
@@ -1542,10 +1631,20 @@ async function executeCommand(input) {
|
|
|
1542
1631
|
...containerRegistryEnv(),
|
|
1543
1632
|
...input.message.env ?? {},
|
|
1544
1633
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1545
|
-
...artifactProcessEnv
|
|
1634
|
+
...artifactProcessEnv,
|
|
1635
|
+
...planProcessEnv
|
|
1546
1636
|
}
|
|
1547
1637
|
});
|
|
1548
|
-
activeProcesses.set(input.message.runId, {
|
|
1638
|
+
activeProcesses.set(input.message.runId, {
|
|
1639
|
+
process: subprocess,
|
|
1640
|
+
projectId: input.projectId,
|
|
1641
|
+
sessionId: input.message.sessionId ?? "",
|
|
1642
|
+
branchName: input.message.branchName,
|
|
1643
|
+
argv: input.message.argv,
|
|
1644
|
+
command: input.message.argv.join(" "),
|
|
1645
|
+
cwd: input.message.cwd,
|
|
1646
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1647
|
+
});
|
|
1549
1648
|
if (input.message.timeoutMs) {
|
|
1550
1649
|
timeout = setTimeout(() => {
|
|
1551
1650
|
subprocess.kill("SIGTERM");
|
|
@@ -1583,9 +1682,7 @@ async function executeCommand(input) {
|
|
|
1583
1682
|
}
|
|
1584
1683
|
async function executeStreamingCommand(input) {
|
|
1585
1684
|
const startedAt = Date.now();
|
|
1586
|
-
let timeout;
|
|
1587
1685
|
let started = false;
|
|
1588
|
-
let timedOut = false;
|
|
1589
1686
|
try {
|
|
1590
1687
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1591
1688
|
sendWorkerMessage(input.ws, {
|
|
@@ -1613,6 +1710,14 @@ async function executeStreamingCommand(input) {
|
|
|
1613
1710
|
sessionId: input.message.sessionId,
|
|
1614
1711
|
artifactRoot: input.artifactRoot
|
|
1615
1712
|
});
|
|
1713
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1714
|
+
baseUrl: input.baseUrl,
|
|
1715
|
+
token: input.token,
|
|
1716
|
+
projectId: input.projectId,
|
|
1717
|
+
branchName: input.message.branchName,
|
|
1718
|
+
planRoot: input.planRoot,
|
|
1719
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1720
|
+
});
|
|
1616
1721
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1617
1722
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1618
1723
|
cwd,
|
|
@@ -1623,22 +1728,26 @@ async function executeStreamingCommand(input) {
|
|
|
1623
1728
|
...containerRegistryEnv(),
|
|
1624
1729
|
...input.message.env ?? {},
|
|
1625
1730
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1626
|
-
...artifactProcessEnv
|
|
1731
|
+
...artifactProcessEnv,
|
|
1732
|
+
...planProcessEnv
|
|
1627
1733
|
}
|
|
1628
1734
|
});
|
|
1629
|
-
activeProcesses.set(input.message.runId, {
|
|
1735
|
+
activeProcesses.set(input.message.runId, {
|
|
1736
|
+
process: subprocess,
|
|
1737
|
+
projectId: input.projectId,
|
|
1738
|
+
sessionId: input.message.sessionId,
|
|
1739
|
+
branchName: input.message.branchName,
|
|
1740
|
+
argv: input.message.argv,
|
|
1741
|
+
command: input.message.command,
|
|
1742
|
+
cwd: input.message.cwd,
|
|
1743
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1744
|
+
});
|
|
1630
1745
|
started = true;
|
|
1631
1746
|
sendWorkerMessage(input.ws, {
|
|
1632
1747
|
type: "exec_started",
|
|
1633
1748
|
requestId: input.message.requestId,
|
|
1634
1749
|
runId: input.message.runId
|
|
1635
1750
|
});
|
|
1636
|
-
if (input.message.timeoutMs) {
|
|
1637
|
-
timeout = setTimeout(() => {
|
|
1638
|
-
timedOut = true;
|
|
1639
|
-
subprocess.kill("SIGTERM");
|
|
1640
|
-
}, input.message.timeoutMs);
|
|
1641
|
-
}
|
|
1642
1751
|
const [exitCode] = await Promise.all([
|
|
1643
1752
|
subprocess.exited,
|
|
1644
1753
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1662,8 +1771,7 @@ async function executeStreamingCommand(input) {
|
|
|
1662
1771
|
type: "exec_exit",
|
|
1663
1772
|
runId: input.message.runId,
|
|
1664
1773
|
exitCode,
|
|
1665
|
-
durationMs: Date.now() - startedAt
|
|
1666
|
-
...timedOut ? { timedOut: true } : {}
|
|
1774
|
+
durationMs: Date.now() - startedAt
|
|
1667
1775
|
});
|
|
1668
1776
|
} catch (error) {
|
|
1669
1777
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1683,15 +1791,36 @@ async function executeStreamingCommand(input) {
|
|
|
1683
1791
|
});
|
|
1684
1792
|
}
|
|
1685
1793
|
} finally {
|
|
1686
|
-
if (timeout) {
|
|
1687
|
-
clearTimeout(timeout);
|
|
1688
|
-
}
|
|
1689
1794
|
activeProcesses.delete(input.message.runId);
|
|
1690
1795
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1691
1796
|
}
|
|
1692
1797
|
}
|
|
1693
1798
|
function sendWorkerMessage(ws, message) {
|
|
1694
|
-
ws
|
|
1799
|
+
const target = currentWorkerSocket ?? ws;
|
|
1800
|
+
try {
|
|
1801
|
+
target.send(JSON.stringify(message));
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
1804
|
+
`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function buildActiveProcessReports() {
|
|
1808
|
+
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
1809
|
+
runId,
|
|
1810
|
+
projectId: active.projectId,
|
|
1811
|
+
sessionId: active.sessionId,
|
|
1812
|
+
branchName: active.branchName,
|
|
1813
|
+
argv: active.argv,
|
|
1814
|
+
command: active.command,
|
|
1815
|
+
...active.cwd ? { cwd: active.cwd } : {},
|
|
1816
|
+
startedAt: active.startedAt
|
|
1817
|
+
}));
|
|
1818
|
+
}
|
|
1819
|
+
function sendActiveProcessReport(ws) {
|
|
1820
|
+
sendWorkerMessage(ws, {
|
|
1821
|
+
type: "active_processes",
|
|
1822
|
+
processes: buildActiveProcessReports()
|
|
1823
|
+
});
|
|
1695
1824
|
}
|
|
1696
1825
|
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
1697
1826
|
const readline = require("node:readline");
|
|
@@ -1875,7 +2004,7 @@ function resolveHostShell() {
|
|
|
1875
2004
|
}
|
|
1876
2005
|
return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
|
|
1877
2006
|
}
|
|
1878
|
-
function openPty(input) {
|
|
2007
|
+
async function openPty(input) {
|
|
1879
2008
|
try {
|
|
1880
2009
|
const branchPath = ensureBranchWorkspace({
|
|
1881
2010
|
projectId: input.projectId,
|
|
@@ -1886,6 +2015,14 @@ function openPty(input) {
|
|
|
1886
2015
|
branchName: input.message.branchName,
|
|
1887
2016
|
manifest: input.manifest
|
|
1888
2017
|
});
|
|
2018
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
2019
|
+
baseUrl: input.baseUrl,
|
|
2020
|
+
token: input.token,
|
|
2021
|
+
projectId: input.projectId,
|
|
2022
|
+
branchName: input.message.branchName,
|
|
2023
|
+
planRoot: input.planRoot,
|
|
2024
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
2025
|
+
});
|
|
1889
2026
|
const shell = resolveHostShell();
|
|
1890
2027
|
const ptyProcess = createNodePtyBridge({
|
|
1891
2028
|
file: shell.file,
|
|
@@ -1899,6 +2036,7 @@ function openPty(input) {
|
|
|
1899
2036
|
...process.env,
|
|
1900
2037
|
...containerRegistryEnv(),
|
|
1901
2038
|
...input.message.env ?? {},
|
|
2039
|
+
...planProcessEnv,
|
|
1902
2040
|
R5D_PROJECT_ID: input.projectId,
|
|
1903
2041
|
R5D_BRANCH_NAME: input.message.branchName
|
|
1904
2042
|
}
|
|
@@ -1999,6 +2137,7 @@ async function startWorker(options) {
|
|
|
1999
2137
|
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
2000
2138
|
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
2001
2139
|
const artifactRoot = import_node_path.default.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2140
|
+
const planRoot = import_node_path.default.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
2002
2141
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
2003
2142
|
`);
|
|
2004
2143
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -2006,6 +2145,8 @@ async function startWorker(options) {
|
|
|
2006
2145
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
2007
2146
|
`);
|
|
2008
2147
|
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
2148
|
+
`);
|
|
2149
|
+
process.stdout.write(`[r5d-worker] plan root: ${planRoot}
|
|
2009
2150
|
`);
|
|
2010
2151
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
2011
2152
|
`);
|
|
@@ -2014,6 +2155,7 @@ async function startWorker(options) {
|
|
|
2014
2155
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
2015
2156
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
2016
2157
|
import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
|
|
2158
|
+
import_node_fs.default.mkdirSync(planRoot, { recursive: true });
|
|
2017
2159
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2018
2160
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
2019
2161
|
headers: {
|
|
@@ -2021,6 +2163,7 @@ async function startWorker(options) {
|
|
|
2021
2163
|
}
|
|
2022
2164
|
});
|
|
2023
2165
|
ws.addEventListener("open", () => {
|
|
2166
|
+
currentWorkerSocket = ws;
|
|
2024
2167
|
const hello = {
|
|
2025
2168
|
type: "hello",
|
|
2026
2169
|
hostInfo: {
|
|
@@ -2030,10 +2173,12 @@ async function startWorker(options) {
|
|
|
2030
2173
|
pid: process.pid,
|
|
2031
2174
|
version: getWorkerVersion(),
|
|
2032
2175
|
projectRoot: projectsRoot,
|
|
2033
|
-
artifactRoot
|
|
2176
|
+
artifactRoot,
|
|
2177
|
+
planRoot
|
|
2034
2178
|
}
|
|
2035
2179
|
};
|
|
2036
2180
|
ws.send(JSON.stringify(hello));
|
|
2181
|
+
sendActiveProcessReport(ws);
|
|
2037
2182
|
process.stdout.write("[r5d-worker] connected\n");
|
|
2038
2183
|
});
|
|
2039
2184
|
ws.addEventListener("message", (event) => {
|
|
@@ -2056,17 +2201,29 @@ async function startWorker(options) {
|
|
|
2056
2201
|
const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
|
|
2057
2202
|
for (const branchName of branches) {
|
|
2058
2203
|
try {
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2204
|
+
ensureBranchWorkspace({
|
|
2205
|
+
projectId: project.projectId,
|
|
2206
|
+
baseUrl,
|
|
2207
|
+
token,
|
|
2208
|
+
projectRoot,
|
|
2209
|
+
syncRoot,
|
|
2210
|
+
branchName,
|
|
2211
|
+
manifest: project
|
|
2212
|
+
});
|
|
2213
|
+
try {
|
|
2214
|
+
await syncProjectPlans({
|
|
2062
2215
|
baseUrl,
|
|
2063
2216
|
token,
|
|
2064
|
-
|
|
2065
|
-
syncRoot,
|
|
2217
|
+
projectId: project.projectId,
|
|
2066
2218
|
branchName,
|
|
2067
|
-
|
|
2219
|
+
planRoot
|
|
2068
2220
|
});
|
|
2069
|
-
})
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
process.stderr.write(
|
|
2223
|
+
`[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2224
|
+
`
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2070
2227
|
} catch (error) {
|
|
2071
2228
|
process.stderr.write(
|
|
2072
2229
|
`[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2094,7 +2251,7 @@ async function startWorker(options) {
|
|
|
2094
2251
|
requestId: message.requestId,
|
|
2095
2252
|
runId: message.runId,
|
|
2096
2253
|
cancelled: true,
|
|
2097
|
-
message: active ? `Sent SIGTERM to ${active.command
|
|
2254
|
+
message: active ? `Sent SIGTERM to ${active.command}` : "Cancellation queued before command start"
|
|
2098
2255
|
})
|
|
2099
2256
|
);
|
|
2100
2257
|
return;
|
|
@@ -2104,7 +2261,7 @@ async function startWorker(options) {
|
|
|
2104
2261
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2105
2262
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
2106
2263
|
`);
|
|
2107
|
-
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
2264
|
+
await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
|
|
2108
2265
|
return;
|
|
2109
2266
|
}
|
|
2110
2267
|
if (message.type === "pty_input") {
|
|
@@ -2124,11 +2281,17 @@ async function startWorker(options) {
|
|
|
2124
2281
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2125
2282
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
2126
2283
|
`);
|
|
2127
|
-
const result = await
|
|
2128
|
-
message
|
|
2129
|
-
message.
|
|
2130
|
-
|
|
2131
|
-
|
|
2284
|
+
const result = await executeCommand({
|
|
2285
|
+
message,
|
|
2286
|
+
projectId: message.projectId,
|
|
2287
|
+
baseUrl,
|
|
2288
|
+
token,
|
|
2289
|
+
projectRoot,
|
|
2290
|
+
syncRoot,
|
|
2291
|
+
artifactRoot,
|
|
2292
|
+
planRoot,
|
|
2293
|
+
manifest
|
|
2294
|
+
});
|
|
2132
2295
|
ws.send(JSON.stringify(result));
|
|
2133
2296
|
return;
|
|
2134
2297
|
}
|
|
@@ -2143,11 +2306,18 @@ async function startWorker(options) {
|
|
|
2143
2306
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2144
2307
|
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
2145
2308
|
`);
|
|
2146
|
-
void
|
|
2147
|
-
|
|
2148
|
-
message
|
|
2149
|
-
|
|
2150
|
-
|
|
2309
|
+
void executeStreamingCommand({
|
|
2310
|
+
ws,
|
|
2311
|
+
message,
|
|
2312
|
+
projectId: message.projectId,
|
|
2313
|
+
baseUrl,
|
|
2314
|
+
token,
|
|
2315
|
+
projectRoot,
|
|
2316
|
+
syncRoot,
|
|
2317
|
+
artifactRoot,
|
|
2318
|
+
planRoot,
|
|
2319
|
+
manifest
|
|
2320
|
+
}).catch((error) => {
|
|
2151
2321
|
sendWorkerMessage(ws, {
|
|
2152
2322
|
type: "exec_start_error",
|
|
2153
2323
|
requestId: message.requestId,
|
|
@@ -2169,11 +2339,16 @@ async function startWorker(options) {
|
|
|
2169
2339
|
try {
|
|
2170
2340
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2171
2341
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2172
|
-
const result = await
|
|
2173
|
-
message
|
|
2174
|
-
message.
|
|
2175
|
-
|
|
2176
|
-
|
|
2342
|
+
const result = await executeOperation({
|
|
2343
|
+
message,
|
|
2344
|
+
projectId: message.projectId,
|
|
2345
|
+
baseUrl,
|
|
2346
|
+
token,
|
|
2347
|
+
projectRoot,
|
|
2348
|
+
syncRoot,
|
|
2349
|
+
artifactRoot,
|
|
2350
|
+
manifest
|
|
2351
|
+
});
|
|
2177
2352
|
ws.send(
|
|
2178
2353
|
JSON.stringify({
|
|
2179
2354
|
type: "operation_result",
|
|
@@ -2208,10 +2383,31 @@ async function startWorker(options) {
|
|
|
2208
2383
|
});
|
|
2209
2384
|
});
|
|
2210
2385
|
ws.addEventListener("close", (event) => {
|
|
2386
|
+
if (currentWorkerSocket === ws) {
|
|
2387
|
+
currentWorkerSocket = null;
|
|
2388
|
+
}
|
|
2211
2389
|
closeAllPtys();
|
|
2212
2390
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
2213
2391
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
2214
2392
|
`);
|
|
2393
|
+
if (activeProcesses.size > 0) {
|
|
2394
|
+
const delayMs = 2e3;
|
|
2395
|
+
process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
|
|
2396
|
+
`);
|
|
2397
|
+
const reconnect = () => {
|
|
2398
|
+
void startWorker(options).catch((error) => {
|
|
2399
|
+
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2400
|
+
`);
|
|
2401
|
+
if (activeProcesses.size > 0) {
|
|
2402
|
+
setTimeout(reconnect, delayMs);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
process.exit(1);
|
|
2406
|
+
});
|
|
2407
|
+
};
|
|
2408
|
+
setTimeout(reconnect, delayMs);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2215
2411
|
process.exit(event.code === 1e3 ? 0 : 1);
|
|
2216
2412
|
});
|
|
2217
2413
|
ws.addEventListener("error", (event) => {
|
|
@@ -2252,6 +2448,8 @@ if (isCliEntrypoint()) {
|
|
|
2252
2448
|
ensureVisibleGitCheckout,
|
|
2253
2449
|
isArtifactEnvPath,
|
|
2254
2450
|
prepareArtifactEnvForShell,
|
|
2451
|
+
preparePlanEnvForShell,
|
|
2255
2452
|
resolveProjectFilePath,
|
|
2453
|
+
syncProjectPlans,
|
|
2256
2454
|
syncSessionArtifacts
|
|
2257
2455
|
});
|