@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/mjs/main.mjs
CHANGED
|
@@ -18,7 +18,7 @@ const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
|
|
|
18
18
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
19
19
|
const cancelledProcessRuns = /* @__PURE__ */ new Set();
|
|
20
20
|
const activePtys = /* @__PURE__ */ new Map();
|
|
21
|
-
|
|
21
|
+
let currentWorkerSocket = null;
|
|
22
22
|
let containerRegistryCredential = null;
|
|
23
23
|
let visibleGitIdentity = null;
|
|
24
24
|
function defaultConfigPath() {
|
|
@@ -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
|
}
|
|
@@ -546,31 +650,6 @@ function resolveProjectFilePath(branchPath, virtualPath) {
|
|
|
546
650
|
assertInsideBranch(branchPath, absolutePath);
|
|
547
651
|
return { absolutePath, virtualPath: repoRelativePath ? `/${repoRelativePath}` : "/", repoRelativePath };
|
|
548
652
|
}
|
|
549
|
-
function branchLockKey(projectId, branchName) {
|
|
550
|
-
return `${projectId}:${branchName}`;
|
|
551
|
-
}
|
|
552
|
-
async function withBranchLock(projectId, branchName, fn) {
|
|
553
|
-
const key = branchLockKey(projectId, branchName);
|
|
554
|
-
const previous = branchLocks.get(key) ?? Promise.resolve();
|
|
555
|
-
let release = () => {
|
|
556
|
-
};
|
|
557
|
-
const next = new Promise((resolve) => {
|
|
558
|
-
release = resolve;
|
|
559
|
-
});
|
|
560
|
-
const queued = previous.catch(() => {
|
|
561
|
-
}).then(() => next);
|
|
562
|
-
branchLocks.set(key, queued);
|
|
563
|
-
await previous.catch(() => {
|
|
564
|
-
});
|
|
565
|
-
try {
|
|
566
|
-
return await fn();
|
|
567
|
-
} finally {
|
|
568
|
-
release();
|
|
569
|
-
if (branchLocks.get(key) === queued) {
|
|
570
|
-
branchLocks.delete(key);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
653
|
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
575
654
|
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
576
655
|
return remoteUrl;
|
|
@@ -1494,6 +1573,14 @@ async function executeCommand(input) {
|
|
|
1494
1573
|
sessionId: input.message.sessionId,
|
|
1495
1574
|
artifactRoot: input.artifactRoot
|
|
1496
1575
|
});
|
|
1576
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1577
|
+
baseUrl: input.baseUrl,
|
|
1578
|
+
token: input.token,
|
|
1579
|
+
projectId: input.projectId,
|
|
1580
|
+
branchName: input.message.branchName,
|
|
1581
|
+
planRoot: input.planRoot,
|
|
1582
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1583
|
+
});
|
|
1497
1584
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1498
1585
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1499
1586
|
cwd,
|
|
@@ -1504,10 +1591,20 @@ async function executeCommand(input) {
|
|
|
1504
1591
|
...containerRegistryEnv(),
|
|
1505
1592
|
...input.message.env ?? {},
|
|
1506
1593
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1507
|
-
...artifactProcessEnv
|
|
1594
|
+
...artifactProcessEnv,
|
|
1595
|
+
...planProcessEnv
|
|
1508
1596
|
}
|
|
1509
1597
|
});
|
|
1510
|
-
activeProcesses.set(input.message.runId, {
|
|
1598
|
+
activeProcesses.set(input.message.runId, {
|
|
1599
|
+
process: subprocess,
|
|
1600
|
+
projectId: input.projectId,
|
|
1601
|
+
sessionId: input.message.sessionId ?? "",
|
|
1602
|
+
branchName: input.message.branchName,
|
|
1603
|
+
argv: input.message.argv,
|
|
1604
|
+
command: input.message.argv.join(" "),
|
|
1605
|
+
cwd: input.message.cwd,
|
|
1606
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1607
|
+
});
|
|
1511
1608
|
if (input.message.timeoutMs) {
|
|
1512
1609
|
timeout = setTimeout(() => {
|
|
1513
1610
|
subprocess.kill("SIGTERM");
|
|
@@ -1545,9 +1642,7 @@ async function executeCommand(input) {
|
|
|
1545
1642
|
}
|
|
1546
1643
|
async function executeStreamingCommand(input) {
|
|
1547
1644
|
const startedAt = Date.now();
|
|
1548
|
-
let timeout;
|
|
1549
1645
|
let started = false;
|
|
1550
|
-
let timedOut = false;
|
|
1551
1646
|
try {
|
|
1552
1647
|
if (cancelledProcessRuns.delete(input.message.runId)) {
|
|
1553
1648
|
sendWorkerMessage(input.ws, {
|
|
@@ -1575,6 +1670,14 @@ async function executeStreamingCommand(input) {
|
|
|
1575
1670
|
sessionId: input.message.sessionId,
|
|
1576
1671
|
artifactRoot: input.artifactRoot
|
|
1577
1672
|
});
|
|
1673
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1674
|
+
baseUrl: input.baseUrl,
|
|
1675
|
+
token: input.token,
|
|
1676
|
+
projectId: input.projectId,
|
|
1677
|
+
branchName: input.message.branchName,
|
|
1678
|
+
planRoot: input.planRoot,
|
|
1679
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1680
|
+
});
|
|
1578
1681
|
const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
|
|
1579
1682
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
1580
1683
|
cwd,
|
|
@@ -1585,22 +1688,26 @@ async function executeStreamingCommand(input) {
|
|
|
1585
1688
|
...containerRegistryEnv(),
|
|
1586
1689
|
...input.message.env ?? {},
|
|
1587
1690
|
...internalGitProcessEnv(workspace, input.message.env),
|
|
1588
|
-
...artifactProcessEnv
|
|
1691
|
+
...artifactProcessEnv,
|
|
1692
|
+
...planProcessEnv
|
|
1589
1693
|
}
|
|
1590
1694
|
});
|
|
1591
|
-
activeProcesses.set(input.message.runId, {
|
|
1695
|
+
activeProcesses.set(input.message.runId, {
|
|
1696
|
+
process: subprocess,
|
|
1697
|
+
projectId: input.projectId,
|
|
1698
|
+
sessionId: input.message.sessionId,
|
|
1699
|
+
branchName: input.message.branchName,
|
|
1700
|
+
argv: input.message.argv,
|
|
1701
|
+
command: input.message.command,
|
|
1702
|
+
cwd: input.message.cwd,
|
|
1703
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1704
|
+
});
|
|
1592
1705
|
started = true;
|
|
1593
1706
|
sendWorkerMessage(input.ws, {
|
|
1594
1707
|
type: "exec_started",
|
|
1595
1708
|
requestId: input.message.requestId,
|
|
1596
1709
|
runId: input.message.runId
|
|
1597
1710
|
});
|
|
1598
|
-
if (input.message.timeoutMs) {
|
|
1599
|
-
timeout = setTimeout(() => {
|
|
1600
|
-
timedOut = true;
|
|
1601
|
-
subprocess.kill("SIGTERM");
|
|
1602
|
-
}, input.message.timeoutMs);
|
|
1603
|
-
}
|
|
1604
1711
|
const [exitCode] = await Promise.all([
|
|
1605
1712
|
subprocess.exited,
|
|
1606
1713
|
streamCommandOutput(subprocess.stdout, (data) => {
|
|
@@ -1624,8 +1731,7 @@ async function executeStreamingCommand(input) {
|
|
|
1624
1731
|
type: "exec_exit",
|
|
1625
1732
|
runId: input.message.runId,
|
|
1626
1733
|
exitCode,
|
|
1627
|
-
durationMs: Date.now() - startedAt
|
|
1628
|
-
...timedOut ? { timedOut: true } : {}
|
|
1734
|
+
durationMs: Date.now() - startedAt
|
|
1629
1735
|
});
|
|
1630
1736
|
} catch (error) {
|
|
1631
1737
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1645,15 +1751,36 @@ async function executeStreamingCommand(input) {
|
|
|
1645
1751
|
});
|
|
1646
1752
|
}
|
|
1647
1753
|
} finally {
|
|
1648
|
-
if (timeout) {
|
|
1649
|
-
clearTimeout(timeout);
|
|
1650
|
-
}
|
|
1651
1754
|
activeProcesses.delete(input.message.runId);
|
|
1652
1755
|
cancelledProcessRuns.delete(input.message.runId);
|
|
1653
1756
|
}
|
|
1654
1757
|
}
|
|
1655
1758
|
function sendWorkerMessage(ws, message) {
|
|
1656
|
-
ws
|
|
1759
|
+
const target = currentWorkerSocket ?? ws;
|
|
1760
|
+
try {
|
|
1761
|
+
target.send(JSON.stringify(message));
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
process.stderr.write(`[r5d-worker] failed to send ${message.type}: ${error instanceof Error ? error.message : String(error)}
|
|
1764
|
+
`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
function buildActiveProcessReports() {
|
|
1768
|
+
return Array.from(activeProcesses.entries()).filter(([, active]) => active.sessionId.length > 0).map(([runId, active]) => ({
|
|
1769
|
+
runId,
|
|
1770
|
+
projectId: active.projectId,
|
|
1771
|
+
sessionId: active.sessionId,
|
|
1772
|
+
branchName: active.branchName,
|
|
1773
|
+
argv: active.argv,
|
|
1774
|
+
command: active.command,
|
|
1775
|
+
...active.cwd ? { cwd: active.cwd } : {},
|
|
1776
|
+
startedAt: active.startedAt
|
|
1777
|
+
}));
|
|
1778
|
+
}
|
|
1779
|
+
function sendActiveProcessReport(ws) {
|
|
1780
|
+
sendWorkerMessage(ws, {
|
|
1781
|
+
type: "active_processes",
|
|
1782
|
+
processes: buildActiveProcessReports()
|
|
1783
|
+
});
|
|
1657
1784
|
}
|
|
1658
1785
|
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
1659
1786
|
const readline = require("node:readline");
|
|
@@ -1837,7 +1964,7 @@ function resolveHostShell() {
|
|
|
1837
1964
|
}
|
|
1838
1965
|
return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
|
|
1839
1966
|
}
|
|
1840
|
-
function openPty(input) {
|
|
1967
|
+
async function openPty(input) {
|
|
1841
1968
|
try {
|
|
1842
1969
|
const branchPath = ensureBranchWorkspace({
|
|
1843
1970
|
projectId: input.projectId,
|
|
@@ -1848,6 +1975,14 @@ function openPty(input) {
|
|
|
1848
1975
|
branchName: input.message.branchName,
|
|
1849
1976
|
manifest: input.manifest
|
|
1850
1977
|
});
|
|
1978
|
+
const planProcessEnv = await preparePlanEnvForShell({
|
|
1979
|
+
baseUrl: input.baseUrl,
|
|
1980
|
+
token: input.token,
|
|
1981
|
+
projectId: input.projectId,
|
|
1982
|
+
branchName: input.message.branchName,
|
|
1983
|
+
planRoot: input.planRoot,
|
|
1984
|
+
activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
|
|
1985
|
+
});
|
|
1851
1986
|
const shell = resolveHostShell();
|
|
1852
1987
|
const ptyProcess = createNodePtyBridge({
|
|
1853
1988
|
file: shell.file,
|
|
@@ -1861,6 +1996,7 @@ function openPty(input) {
|
|
|
1861
1996
|
...process.env,
|
|
1862
1997
|
...containerRegistryEnv(),
|
|
1863
1998
|
...input.message.env ?? {},
|
|
1999
|
+
...planProcessEnv,
|
|
1864
2000
|
R5D_PROJECT_ID: input.projectId,
|
|
1865
2001
|
R5D_BRANCH_NAME: input.message.branchName
|
|
1866
2002
|
}
|
|
@@ -1961,6 +2097,7 @@ async function startWorker(options) {
|
|
|
1961
2097
|
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1962
2098
|
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
1963
2099
|
const artifactRoot = path.resolve(options.artifactRoot ?? process.env.R5D_ARTIFACTS_ROOT ?? defaultArtifactRoot());
|
|
2100
|
+
const planRoot = path.resolve(options.planRoot ?? process.env.R5D_PLANS_ROOT ?? defaultPlanRoot());
|
|
1964
2101
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
1965
2102
|
`);
|
|
1966
2103
|
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
@@ -1968,6 +2105,8 @@ async function startWorker(options) {
|
|
|
1968
2105
|
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
1969
2106
|
`);
|
|
1970
2107
|
process.stdout.write(`[r5d-worker] artifact root: ${artifactRoot}
|
|
2108
|
+
`);
|
|
2109
|
+
process.stdout.write(`[r5d-worker] plan root: ${planRoot}
|
|
1971
2110
|
`);
|
|
1972
2111
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1973
2112
|
`);
|
|
@@ -1976,6 +2115,7 @@ async function startWorker(options) {
|
|
|
1976
2115
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1977
2116
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1978
2117
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2118
|
+
fs.mkdirSync(planRoot, { recursive: true });
|
|
1979
2119
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1980
2120
|
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
1981
2121
|
headers: {
|
|
@@ -1983,6 +2123,7 @@ async function startWorker(options) {
|
|
|
1983
2123
|
}
|
|
1984
2124
|
});
|
|
1985
2125
|
ws.addEventListener("open", () => {
|
|
2126
|
+
currentWorkerSocket = ws;
|
|
1986
2127
|
const hello = {
|
|
1987
2128
|
type: "hello",
|
|
1988
2129
|
hostInfo: {
|
|
@@ -1992,10 +2133,12 @@ async function startWorker(options) {
|
|
|
1992
2133
|
pid: process.pid,
|
|
1993
2134
|
version: getWorkerVersion(),
|
|
1994
2135
|
projectRoot: projectsRoot,
|
|
1995
|
-
artifactRoot
|
|
2136
|
+
artifactRoot,
|
|
2137
|
+
planRoot
|
|
1996
2138
|
}
|
|
1997
2139
|
};
|
|
1998
2140
|
ws.send(JSON.stringify(hello));
|
|
2141
|
+
sendActiveProcessReport(ws);
|
|
1999
2142
|
process.stdout.write("[r5d-worker] connected\n");
|
|
2000
2143
|
});
|
|
2001
2144
|
ws.addEventListener("message", (event) => {
|
|
@@ -2018,17 +2161,29 @@ async function startWorker(options) {
|
|
|
2018
2161
|
const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
|
|
2019
2162
|
for (const branchName of branches) {
|
|
2020
2163
|
try {
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2164
|
+
ensureBranchWorkspace({
|
|
2165
|
+
projectId: project.projectId,
|
|
2166
|
+
baseUrl,
|
|
2167
|
+
token,
|
|
2168
|
+
projectRoot,
|
|
2169
|
+
syncRoot,
|
|
2170
|
+
branchName,
|
|
2171
|
+
manifest: project
|
|
2172
|
+
});
|
|
2173
|
+
try {
|
|
2174
|
+
await syncProjectPlans({
|
|
2024
2175
|
baseUrl,
|
|
2025
2176
|
token,
|
|
2026
|
-
|
|
2027
|
-
syncRoot,
|
|
2177
|
+
projectId: project.projectId,
|
|
2028
2178
|
branchName,
|
|
2029
|
-
|
|
2179
|
+
planRoot
|
|
2030
2180
|
});
|
|
2031
|
-
})
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
process.stderr.write(
|
|
2183
|
+
`[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
2184
|
+
`
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2032
2187
|
} catch (error) {
|
|
2033
2188
|
process.stderr.write(
|
|
2034
2189
|
`[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -2056,7 +2211,7 @@ async function startWorker(options) {
|
|
|
2056
2211
|
requestId: message.requestId,
|
|
2057
2212
|
runId: message.runId,
|
|
2058
2213
|
cancelled: true,
|
|
2059
|
-
message: active ? `Sent SIGTERM to ${active.command
|
|
2214
|
+
message: active ? `Sent SIGTERM to ${active.command}` : "Cancellation queued before command start"
|
|
2060
2215
|
})
|
|
2061
2216
|
);
|
|
2062
2217
|
return;
|
|
@@ -2066,7 +2221,7 @@ async function startWorker(options) {
|
|
|
2066
2221
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2067
2222
|
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
2068
2223
|
`);
|
|
2069
|
-
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
2224
|
+
await openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, planRoot, manifest });
|
|
2070
2225
|
return;
|
|
2071
2226
|
}
|
|
2072
2227
|
if (message.type === "pty_input") {
|
|
@@ -2086,11 +2241,17 @@ async function startWorker(options) {
|
|
|
2086
2241
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2087
2242
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
2088
2243
|
`);
|
|
2089
|
-
const result = await
|
|
2090
|
-
message
|
|
2091
|
-
message.
|
|
2092
|
-
|
|
2093
|
-
|
|
2244
|
+
const result = await executeCommand({
|
|
2245
|
+
message,
|
|
2246
|
+
projectId: message.projectId,
|
|
2247
|
+
baseUrl,
|
|
2248
|
+
token,
|
|
2249
|
+
projectRoot,
|
|
2250
|
+
syncRoot,
|
|
2251
|
+
artifactRoot,
|
|
2252
|
+
planRoot,
|
|
2253
|
+
manifest
|
|
2254
|
+
});
|
|
2094
2255
|
ws.send(JSON.stringify(result));
|
|
2095
2256
|
return;
|
|
2096
2257
|
}
|
|
@@ -2105,11 +2266,18 @@ async function startWorker(options) {
|
|
|
2105
2266
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2106
2267
|
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
2107
2268
|
`);
|
|
2108
|
-
void
|
|
2109
|
-
|
|
2110
|
-
message
|
|
2111
|
-
|
|
2112
|
-
|
|
2269
|
+
void executeStreamingCommand({
|
|
2270
|
+
ws,
|
|
2271
|
+
message,
|
|
2272
|
+
projectId: message.projectId,
|
|
2273
|
+
baseUrl,
|
|
2274
|
+
token,
|
|
2275
|
+
projectRoot,
|
|
2276
|
+
syncRoot,
|
|
2277
|
+
artifactRoot,
|
|
2278
|
+
planRoot,
|
|
2279
|
+
manifest
|
|
2280
|
+
}).catch((error) => {
|
|
2113
2281
|
sendWorkerMessage(ws, {
|
|
2114
2282
|
type: "exec_start_error",
|
|
2115
2283
|
requestId: message.requestId,
|
|
@@ -2131,11 +2299,16 @@ async function startWorker(options) {
|
|
|
2131
2299
|
try {
|
|
2132
2300
|
const manifest = manifestByProjectId.get(message.projectId);
|
|
2133
2301
|
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
2134
|
-
const result = await
|
|
2135
|
-
message
|
|
2136
|
-
message.
|
|
2137
|
-
|
|
2138
|
-
|
|
2302
|
+
const result = await executeOperation({
|
|
2303
|
+
message,
|
|
2304
|
+
projectId: message.projectId,
|
|
2305
|
+
baseUrl,
|
|
2306
|
+
token,
|
|
2307
|
+
projectRoot,
|
|
2308
|
+
syncRoot,
|
|
2309
|
+
artifactRoot,
|
|
2310
|
+
manifest
|
|
2311
|
+
});
|
|
2139
2312
|
ws.send(
|
|
2140
2313
|
JSON.stringify({
|
|
2141
2314
|
type: "operation_result",
|
|
@@ -2170,10 +2343,31 @@ async function startWorker(options) {
|
|
|
2170
2343
|
});
|
|
2171
2344
|
});
|
|
2172
2345
|
ws.addEventListener("close", (event) => {
|
|
2346
|
+
if (currentWorkerSocket === ws) {
|
|
2347
|
+
currentWorkerSocket = null;
|
|
2348
|
+
}
|
|
2173
2349
|
closeAllPtys();
|
|
2174
2350
|
const reason = event.reason ? `: ${event.reason}` : "";
|
|
2175
2351
|
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
2176
2352
|
`);
|
|
2353
|
+
if (activeProcesses.size > 0) {
|
|
2354
|
+
const delayMs = 2e3;
|
|
2355
|
+
process.stderr.write(`[r5d-worker] ${activeProcesses.size} process(es) still active; reconnecting in ${delayMs}ms
|
|
2356
|
+
`);
|
|
2357
|
+
const reconnect = () => {
|
|
2358
|
+
void startWorker(options).catch((error) => {
|
|
2359
|
+
process.stderr.write(`[r5d-worker] reconnect failed: ${error instanceof Error ? error.message : String(error)}
|
|
2360
|
+
`);
|
|
2361
|
+
if (activeProcesses.size > 0) {
|
|
2362
|
+
setTimeout(reconnect, delayMs);
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
process.exit(1);
|
|
2366
|
+
});
|
|
2367
|
+
};
|
|
2368
|
+
setTimeout(reconnect, delayMs);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2177
2371
|
process.exit(event.code === 1e3 ? 0 : 1);
|
|
2178
2372
|
});
|
|
2179
2373
|
ws.addEventListener("error", (event) => {
|
|
@@ -2213,6 +2407,8 @@ export {
|
|
|
2213
2407
|
ensureVisibleGitCheckout,
|
|
2214
2408
|
isArtifactEnvPath,
|
|
2215
2409
|
prepareArtifactEnvForShell,
|
|
2410
|
+
preparePlanEnvForShell,
|
|
2216
2411
|
resolveProjectFilePath,
|
|
2412
|
+
syncProjectPlans,
|
|
2217
2413
|
syncSessionArtifacts
|
|
2218
2414
|
};
|