@ricsam/r5d-worker 0.0.24 → 0.0.25

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/README.md CHANGED
@@ -15,7 +15,7 @@ The worker uses the existing `r5dctl` config file and accepts `R5D_WORKER_TOKEN`
15
15
 
16
16
  Web shells and agent shell commands receive a server-issued `r5dctl` credential automatically. The credential is scoped to the signed-in user and revoked when the shell or command finishes, so commands such as `r5dctl auth status` do not require a separate login on the worker host.
17
17
 
18
- Visible branch checkouts are the user/agent workbench. The worker may clone the connected GitHub `origin` during first setup, but ongoing pull/push/fetch/reset behavior is manual Git work. r5d internal sync git metadata lives separately under `~/.r5d/sync`.
18
+ Visible branch checkouts are the user/agent workbench. Their `origin` remains the connected GitHub repository for manual Git work. r5d internal sync git metadata lives separately under `~/.r5d/sync`; when a worker connects, every idle registered branch is force-synchronized from that internal remote. The same destructive sync can be requested for a live worker from User Settings.
19
19
 
20
20
  Worker labels are mandatory and unique per user. Choose labels that describe host capabilities, such as `macos`, `linux`, `ios`, or `ec2-build`.
21
21
 
package/dist/cjs/main.cjs CHANGED
@@ -30,11 +30,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  var main_exports = {};
31
31
  __export(main_exports, {
32
32
  ensureVisibleGitCheckout: () => ensureVisibleGitCheckout,
33
+ findRepositorySyncBlockers: () => findRepositorySyncBlockers,
33
34
  isArtifactEnvPath: () => isArtifactEnvPath,
34
35
  prepareArtifactEnvForShell: () => prepareArtifactEnvForShell,
35
36
  preparePlanEnvForShell: () => preparePlanEnvForShell,
36
37
  resolveHostShell: () => resolveHostShell,
37
38
  resolveProjectFilePath: () => resolveProjectFilePath,
39
+ syncManifestProjectsFromInternal: () => syncManifestProjectsFromInternal,
38
40
  syncProjectPlans: () => syncProjectPlans,
39
41
  syncSessionArtifacts: () => syncSessionArtifacts
40
42
  });
@@ -627,6 +629,27 @@ function runGit(args, options = {}) {
627
629
  }
628
630
  return result.stdout.toString().trim();
629
631
  }
632
+ async function runGitAsync(args, options = {}) {
633
+ const command = ["git", ...gitAuthArgs(options.auth), ...args];
634
+ const subprocess = Bun.spawn(command, {
635
+ cwd: options.cwd,
636
+ stdout: "pipe",
637
+ stderr: "pipe",
638
+ env: {
639
+ ...process.env,
640
+ GIT_TERMINAL_PROMPT: "0"
641
+ }
642
+ });
643
+ const [stdout, stderr, exitCode] = await Promise.all([
644
+ collectStream(subprocess.stdout),
645
+ collectStream(subprocess.stderr),
646
+ subprocess.exited
647
+ ]);
648
+ if (exitCode !== 0) {
649
+ throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
650
+ }
651
+ return stdout.trim();
652
+ }
630
653
  function tryGit(args, options = {}) {
631
654
  try {
632
655
  runGit(args, options);
@@ -635,9 +658,20 @@ function tryGit(args, options = {}) {
635
658
  return false;
636
659
  }
637
660
  }
661
+ async function tryGitAsync(args, options = {}) {
662
+ try {
663
+ await runGitAsync(args, options);
664
+ return true;
665
+ } catch {
666
+ return false;
667
+ }
668
+ }
638
669
  function runInternalGit(context, args) {
639
670
  return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
640
671
  }
672
+ function runInternalGitAsync(context, args) {
673
+ return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
674
+ }
641
675
  function runInternalGitDir(context, args) {
642
676
  return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
643
677
  }
@@ -754,6 +788,13 @@ function ensureOriginRemote(branchPath, remoteUrl) {
754
788
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
755
789
  }
756
790
  }
791
+ async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
792
+ if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
793
+ await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
794
+ } else {
795
+ await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
796
+ }
797
+ }
757
798
  function shellQuote(value) {
758
799
  return `'${value.replace(/'/g, "'\\''")}'`;
759
800
  }
@@ -926,6 +967,9 @@ function ensureVisibleGitCheckout(input) {
926
967
  checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
927
968
  return branchPath;
928
969
  }
970
+ if (input.reconcileExistingOrigin) {
971
+ ensureOriginRemote(branchPath, input.githubRemoteUrl);
972
+ }
929
973
  configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
930
974
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
931
975
  return branchPath;
@@ -972,7 +1016,8 @@ function ensureBranchWorkspace(input) {
972
1016
  githubRemoteUrl,
973
1017
  githubAuth,
974
1018
  githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
975
- defaultBranch
1019
+ defaultBranch,
1020
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
976
1021
  });
977
1022
  const internalGit = ensureInternalSyncGit({
978
1023
  projectId: input.projectId,
@@ -985,6 +1030,140 @@ function ensureBranchWorkspace(input) {
985
1030
  });
986
1031
  return { branchPath, internalGit };
987
1032
  }
1033
+ async function ensureVisibleGitCheckoutForRepositorySync(input) {
1034
+ validateBranchName(input.branchName);
1035
+ import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
1036
+ const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
1037
+ const createdCheckout = !hasNormalVisibleGitDir(branchPath);
1038
+ if (createdCheckout) {
1039
+ import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
1040
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
1041
+ if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
1042
+ import_node_fs.default.mkdirSync(branchPath, { recursive: true });
1043
+ await runGitAsync(["init"], { cwd: branchPath });
1044
+ }
1045
+ await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1046
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1047
+ (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1048
+ await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1049
+ checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1050
+ return branchPath;
1051
+ }
1052
+ if (input.reconcileExistingOrigin) {
1053
+ await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1054
+ }
1055
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1056
+ (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
1057
+ return branchPath;
1058
+ }
1059
+ async function forceSyncManifestBranchFromInternal(input) {
1060
+ const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1061
+ const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1062
+ const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1063
+ projectRoot: input.projectRoot,
1064
+ branchName: input.branchName,
1065
+ githubRemoteUrl,
1066
+ githubAuth,
1067
+ githubAuthHeader: input.manifest.repoAuthHeader,
1068
+ defaultBranch: input.manifest.defaultBranch || "main",
1069
+ reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1070
+ });
1071
+ const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1072
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
1073
+ if (!import_node_fs.default.existsSync(gitDir)) {
1074
+ await runGitAsync(["init", "--bare", gitDir]);
1075
+ }
1076
+ const context = {
1077
+ gitDir,
1078
+ workTree: branchPath,
1079
+ auth: internalGitAuth(input.baseUrl, input.token)
1080
+ };
1081
+ configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1082
+ await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1083
+ const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1084
+ const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1085
+ const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1086
+ if (!target) {
1087
+ throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1088
+ }
1089
+ await runInternalGitAsync(context, ["reset", "--hard", target]);
1090
+ await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1091
+ return getInternalCommitHash(context);
1092
+ }
1093
+ function findRepositorySyncBlockers(input) {
1094
+ const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1095
+ const blockedBy = [];
1096
+ for (const process2 of input.processes) {
1097
+ const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1098
+ if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1099
+ }
1100
+ for (const shell of input.shells) {
1101
+ const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1102
+ if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1103
+ }
1104
+ return blockedBy;
1105
+ }
1106
+ async function syncManifestProjectsFromInternal(input) {
1107
+ const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1108
+ const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1109
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1110
+ return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1111
+ });
1112
+ const blockedBy = findRepositorySyncBlockers({
1113
+ targets: targets.map(({ manifest, branchName }) => ({
1114
+ projectId: manifest.projectId,
1115
+ projectPath: manifest.projectPath,
1116
+ branchName
1117
+ })),
1118
+ processes: [...activeProcesses].map(([id, active]) => ({
1119
+ id,
1120
+ projectId: active.projectId,
1121
+ branchName: active.branchName
1122
+ })),
1123
+ shells: [...activePtys].map(([id, active]) => ({
1124
+ id,
1125
+ projectId: active.projectId,
1126
+ branchName: active.branchName
1127
+ }))
1128
+ });
1129
+ if (blockedBy.length > 0) {
1130
+ return { status: "blocked", results: [], blockedBy };
1131
+ }
1132
+ const results = [];
1133
+ for (const { manifest, branchName } of targets) {
1134
+ try {
1135
+ const commitHash = await forceSyncManifestBranchFromInternal({
1136
+ projectId: manifest.projectId,
1137
+ baseUrl: input.baseUrl,
1138
+ token: input.token,
1139
+ projectRoot: import_node_path.default.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1140
+ syncRoot: input.syncRoot,
1141
+ branchName,
1142
+ manifest
1143
+ });
1144
+ results.push({
1145
+ projectId: manifest.projectId,
1146
+ projectPath: manifest.projectPath,
1147
+ branchName,
1148
+ status: "synced",
1149
+ commitHash
1150
+ });
1151
+ } catch (error) {
1152
+ results.push({
1153
+ projectId: manifest.projectId,
1154
+ projectPath: manifest.projectPath,
1155
+ branchName,
1156
+ status: "failed",
1157
+ error: error instanceof Error ? error.message : String(error)
1158
+ });
1159
+ }
1160
+ }
1161
+ return {
1162
+ status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1163
+ results,
1164
+ blockedBy: []
1165
+ };
1166
+ }
988
1167
  function resolveCommandCwd(branchPath, cwd) {
989
1168
  if (!cwd) {
990
1169
  return branchPath;
@@ -2089,7 +2268,11 @@ async function openPty(input) {
2089
2268
  });
2090
2269
  }
2091
2270
  });
2092
- activePtys.set(input.message.ptyId, ptyProcess);
2271
+ activePtys.set(input.message.ptyId, {
2272
+ ...ptyProcess,
2273
+ projectId: input.projectId,
2274
+ branchName: input.message.branchName
2275
+ });
2093
2276
  } catch (error) {
2094
2277
  sendWorkerMessage(input.ws, {
2095
2278
  type: "pty_error",
@@ -2172,6 +2355,7 @@ async function startWorker(options) {
2172
2355
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2173
2356
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2174
2357
  const manifestByProjectId = /* @__PURE__ */ new Map();
2358
+ let repositorySyncQueue = Promise.resolve();
2175
2359
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2176
2360
  headers: {
2177
2361
  Authorization: `Bearer ${token}`
@@ -2229,42 +2413,64 @@ async function startWorker(options) {
2229
2413
  }
2230
2414
  process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2231
2415
  `);
2232
- for (const project of message.projects) {
2233
- const projectRoot = projectRootFor(projectsRoot, project.projectId, manifestByProjectId);
2234
- const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
2235
- for (const branchName of branches) {
2416
+ return;
2417
+ }
2418
+ if (message.type === "sync_projects") {
2419
+ let syncResult = { status: "completed", results: [], blockedBy: [] };
2420
+ const executeSync = async () => {
2421
+ syncResult = await syncManifestProjectsFromInternal({
2422
+ baseUrl,
2423
+ token,
2424
+ projectsRoot,
2425
+ syncRoot,
2426
+ manifests: [...manifestByProjectId.values()],
2427
+ projectIds: message.projectIds
2428
+ });
2429
+ for (const result of syncResult.results) {
2430
+ if (result.status !== "synced") continue;
2236
2431
  try {
2237
- ensureBranchWorkspace({
2238
- projectId: project.projectId,
2432
+ await syncProjectPlans({
2239
2433
  baseUrl,
2240
2434
  token,
2241
- projectRoot,
2242
- syncRoot,
2243
- branchName,
2244
- manifest: project
2435
+ projectId: result.projectId,
2436
+ branchName: result.branchName,
2437
+ planRoot
2245
2438
  });
2246
- try {
2247
- await syncProjectPlans({
2248
- baseUrl,
2249
- token,
2250
- projectId: project.projectId,
2251
- branchName,
2252
- planRoot
2253
- });
2254
- } catch (error) {
2255
- process.stderr.write(
2256
- `[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2257
- `
2258
- );
2259
- }
2260
2439
  } catch (error) {
2261
2440
  process.stderr.write(
2262
- `[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2441
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2263
2442
  `
2264
2443
  );
2265
2444
  }
2266
2445
  }
2446
+ };
2447
+ const queued = repositorySyncQueue.then(executeSync, executeSync);
2448
+ repositorySyncQueue = queued.then(
2449
+ () => void 0,
2450
+ () => void 0
2451
+ );
2452
+ try {
2453
+ await queued;
2454
+ } catch (error) {
2455
+ syncResult = {
2456
+ status: "partial",
2457
+ results: [
2458
+ {
2459
+ projectId: "worker",
2460
+ projectPath: "worker",
2461
+ branchName: "all",
2462
+ status: "failed",
2463
+ error: error instanceof Error ? error.message : String(error)
2464
+ }
2465
+ ],
2466
+ blockedBy: []
2467
+ };
2267
2468
  }
2469
+ sendWorkerMessage(ws, {
2470
+ type: "sync_projects_result",
2471
+ requestId: message.requestId,
2472
+ result: syncResult
2473
+ });
2268
2474
  return;
2269
2475
  }
2270
2476
  if (message.type === "ping") {
@@ -2291,6 +2497,7 @@ async function startWorker(options) {
2291
2497
  return;
2292
2498
  }
2293
2499
  if (message.type === "pty_open") {
2500
+ await repositorySyncQueue;
2294
2501
  const manifest = manifestByProjectId.get(message.projectId);
2295
2502
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2296
2503
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2311,6 +2518,7 @@ async function startWorker(options) {
2311
2518
  return;
2312
2519
  }
2313
2520
  if (message.type === "exec") {
2521
+ await repositorySyncQueue;
2314
2522
  const manifest = manifestByProjectId.get(message.projectId);
2315
2523
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2316
2524
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2330,6 +2538,7 @@ async function startWorker(options) {
2330
2538
  return;
2331
2539
  }
2332
2540
  if (message.type === "exec_start") {
2541
+ await repositorySyncQueue;
2333
2542
  sendWorkerMessage(ws, {
2334
2543
  type: "exec_accepted",
2335
2544
  requestId: message.requestId,
@@ -2371,6 +2580,7 @@ async function startWorker(options) {
2371
2580
  }
2372
2581
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2373
2582
  try {
2583
+ await repositorySyncQueue;
2374
2584
  const manifest = manifestByProjectId.get(message.projectId);
2375
2585
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2376
2586
  const result = await executeOperation({
@@ -2481,11 +2691,13 @@ if (isCliEntrypoint()) {
2481
2691
  // Annotate the CommonJS export names for ESM import in node:
2482
2692
  0 && (module.exports = {
2483
2693
  ensureVisibleGitCheckout,
2694
+ findRepositorySyncBlockers,
2484
2695
  isArtifactEnvPath,
2485
2696
  prepareArtifactEnvForShell,
2486
2697
  preparePlanEnvForShell,
2487
2698
  resolveHostShell,
2488
2699
  resolveProjectFilePath,
2700
+ syncManifestProjectsFromInternal,
2489
2701
  syncProjectPlans,
2490
2702
  syncSessionArtifacts
2491
2703
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -586,6 +586,27 @@ function runGit(args, options = {}) {
586
586
  }
587
587
  return result.stdout.toString().trim();
588
588
  }
589
+ async function runGitAsync(args, options = {}) {
590
+ const command = ["git", ...gitAuthArgs(options.auth), ...args];
591
+ const subprocess = Bun.spawn(command, {
592
+ cwd: options.cwd,
593
+ stdout: "pipe",
594
+ stderr: "pipe",
595
+ env: {
596
+ ...process.env,
597
+ GIT_TERMINAL_PROMPT: "0"
598
+ }
599
+ });
600
+ const [stdout, stderr, exitCode] = await Promise.all([
601
+ collectStream(subprocess.stdout),
602
+ collectStream(subprocess.stderr),
603
+ subprocess.exited
604
+ ]);
605
+ if (exitCode !== 0) {
606
+ throw new Error(`git ${args.join(" ")} failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`);
607
+ }
608
+ return stdout.trim();
609
+ }
589
610
  function tryGit(args, options = {}) {
590
611
  try {
591
612
  runGit(args, options);
@@ -594,9 +615,20 @@ function tryGit(args, options = {}) {
594
615
  return false;
595
616
  }
596
617
  }
618
+ async function tryGitAsync(args, options = {}) {
619
+ try {
620
+ await runGitAsync(args, options);
621
+ return true;
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
597
626
  function runInternalGit(context, args) {
598
627
  return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
599
628
  }
629
+ function runInternalGitAsync(context, args) {
630
+ return runGitAsync(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
631
+ }
600
632
  function runInternalGitDir(context, args) {
601
633
  return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
602
634
  }
@@ -713,6 +745,13 @@ function ensureOriginRemote(branchPath, remoteUrl) {
713
745
  runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
714
746
  }
715
747
  }
748
+ async function ensureOriginRemoteAsync(branchPath, remoteUrl) {
749
+ if (await tryGitAsync(["remote", "get-url", "origin"], { cwd: branchPath })) {
750
+ await runGitAsync(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
751
+ } else {
752
+ await runGitAsync(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
753
+ }
754
+ }
716
755
  function shellQuote(value) {
717
756
  return `'${value.replace(/'/g, "'\\''")}'`;
718
757
  }
@@ -885,6 +924,9 @@ function ensureVisibleGitCheckout(input) {
885
924
  checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
886
925
  return branchPath;
887
926
  }
927
+ if (input.reconcileExistingOrigin) {
928
+ ensureOriginRemote(branchPath, input.githubRemoteUrl);
929
+ }
888
930
  configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
889
931
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
890
932
  return branchPath;
@@ -931,7 +973,8 @@ function ensureBranchWorkspace(input) {
931
973
  githubRemoteUrl,
932
974
  githubAuth,
933
975
  githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
934
- defaultBranch
976
+ defaultBranch,
977
+ reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl)
935
978
  });
936
979
  const internalGit = ensureInternalSyncGit({
937
980
  projectId: input.projectId,
@@ -944,6 +987,140 @@ function ensureBranchWorkspace(input) {
944
987
  });
945
988
  return { branchPath, internalGit };
946
989
  }
990
+ async function ensureVisibleGitCheckoutForRepositorySync(input) {
991
+ validateBranchName(input.branchName);
992
+ fs.mkdirSync(input.projectRoot, { recursive: true });
993
+ const branchPath = path.join(input.projectRoot, input.branchName);
994
+ const createdCheckout = !hasNormalVisibleGitDir(branchPath);
995
+ if (createdCheckout) {
996
+ fs.rmSync(branchPath, { recursive: true, force: true });
997
+ fs.mkdirSync(path.dirname(branchPath), { recursive: true });
998
+ if (!await tryGitAsync(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
999
+ fs.mkdirSync(branchPath, { recursive: true });
1000
+ await runGitAsync(["init"], { cwd: branchPath });
1001
+ }
1002
+ await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1003
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1004
+ configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1005
+ await tryGitAsync(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
1006
+ checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
1007
+ return branchPath;
1008
+ }
1009
+ if (input.reconcileExistingOrigin) {
1010
+ await ensureOriginRemoteAsync(branchPath, input.githubRemoteUrl);
1011
+ }
1012
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
1013
+ configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
1014
+ return branchPath;
1015
+ }
1016
+ async function forceSyncManifestBranchFromInternal(input) {
1017
+ const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
1018
+ const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest.repoAuthHeader);
1019
+ const branchPath = await ensureVisibleGitCheckoutForRepositorySync({
1020
+ projectRoot: input.projectRoot,
1021
+ branchName: input.branchName,
1022
+ githubRemoteUrl,
1023
+ githubAuth,
1024
+ githubAuthHeader: input.manifest.repoAuthHeader,
1025
+ defaultBranch: input.manifest.defaultBranch || "main",
1026
+ reconcileExistingOrigin: Boolean(input.manifest.repoHttpUrl)
1027
+ });
1028
+ const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
1029
+ fs.mkdirSync(path.dirname(gitDir), { recursive: true });
1030
+ if (!fs.existsSync(gitDir)) {
1031
+ await runGitAsync(["init", "--bare", gitDir]);
1032
+ }
1033
+ const context = {
1034
+ gitDir,
1035
+ workTree: branchPath,
1036
+ auth: internalGitAuth(input.baseUrl, input.token)
1037
+ };
1038
+ configureInternalRepo(context, internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest));
1039
+ await runInternalGitAsync(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
1040
+ const branchTarget = `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`;
1041
+ const mainTarget = `refs/remotes/${R5D_REMOTE_NAME}/main`;
1042
+ const target = tryInternalGit(context, ["show-ref", "--verify", "--quiet", branchTarget]) ? branchTarget : tryInternalGit(context, ["show-ref", "--verify", "--quiet", mainTarget]) ? mainTarget : null;
1043
+ if (!target) {
1044
+ throw new Error(`Internal remote has neither ${input.branchName} nor main`);
1045
+ }
1046
+ await runInternalGitAsync(context, ["reset", "--hard", target]);
1047
+ await runInternalGitAsync(context, ["clean", "-fd", "--", ".", ":(exclude).git"]);
1048
+ return getInternalCommitHash(context);
1049
+ }
1050
+ function findRepositorySyncBlockers(input) {
1051
+ const targetByKey = new Map(input.targets.map((target) => [`${target.projectId}:${target.branchName}`, target]));
1052
+ const blockedBy = [];
1053
+ for (const process2 of input.processes) {
1054
+ const target = targetByKey.get(`${process2.projectId}:${process2.branchName}`);
1055
+ if (target) blockedBy.push({ ...target, kind: "process", id: process2.id });
1056
+ }
1057
+ for (const shell of input.shells) {
1058
+ const target = targetByKey.get(`${shell.projectId}:${shell.branchName}`);
1059
+ if (target) blockedBy.push({ ...target, kind: "shell", id: shell.id });
1060
+ }
1061
+ return blockedBy;
1062
+ }
1063
+ async function syncManifestProjectsFromInternal(input) {
1064
+ const requestedProjectIds = input.projectIds ? new Set(input.projectIds) : null;
1065
+ const targets = input.manifests.filter((manifest) => !requestedProjectIds || requestedProjectIds.has(manifest.projectId)).flatMap((manifest) => {
1066
+ const branches = manifest.branches.length > 0 ? manifest.branches : [manifest.defaultBranch || "main"];
1067
+ return [...new Set(branches)].map((branchName) => ({ manifest, branchName }));
1068
+ });
1069
+ const blockedBy = findRepositorySyncBlockers({
1070
+ targets: targets.map(({ manifest, branchName }) => ({
1071
+ projectId: manifest.projectId,
1072
+ projectPath: manifest.projectPath,
1073
+ branchName
1074
+ })),
1075
+ processes: [...activeProcesses].map(([id, active]) => ({
1076
+ id,
1077
+ projectId: active.projectId,
1078
+ branchName: active.branchName
1079
+ })),
1080
+ shells: [...activePtys].map(([id, active]) => ({
1081
+ id,
1082
+ projectId: active.projectId,
1083
+ branchName: active.branchName
1084
+ }))
1085
+ });
1086
+ if (blockedBy.length > 0) {
1087
+ return { status: "blocked", results: [], blockedBy };
1088
+ }
1089
+ const results = [];
1090
+ for (const { manifest, branchName } of targets) {
1091
+ try {
1092
+ const commitHash = await forceSyncManifestBranchFromInternal({
1093
+ projectId: manifest.projectId,
1094
+ baseUrl: input.baseUrl,
1095
+ token: input.token,
1096
+ projectRoot: path.join(input.projectsRoot, manifest.repoSlug || manifest.projectId),
1097
+ syncRoot: input.syncRoot,
1098
+ branchName,
1099
+ manifest
1100
+ });
1101
+ results.push({
1102
+ projectId: manifest.projectId,
1103
+ projectPath: manifest.projectPath,
1104
+ branchName,
1105
+ status: "synced",
1106
+ commitHash
1107
+ });
1108
+ } catch (error) {
1109
+ results.push({
1110
+ projectId: manifest.projectId,
1111
+ projectPath: manifest.projectPath,
1112
+ branchName,
1113
+ status: "failed",
1114
+ error: error instanceof Error ? error.message : String(error)
1115
+ });
1116
+ }
1117
+ }
1118
+ return {
1119
+ status: results.some((result) => result.status === "failed") ? "partial" : "completed",
1120
+ results,
1121
+ blockedBy: []
1122
+ };
1123
+ }
947
1124
  function resolveCommandCwd(branchPath, cwd) {
948
1125
  if (!cwd) {
949
1126
  return branchPath;
@@ -2048,7 +2225,11 @@ async function openPty(input) {
2048
2225
  });
2049
2226
  }
2050
2227
  });
2051
- activePtys.set(input.message.ptyId, ptyProcess);
2228
+ activePtys.set(input.message.ptyId, {
2229
+ ...ptyProcess,
2230
+ projectId: input.projectId,
2231
+ branchName: input.message.branchName
2232
+ });
2052
2233
  } catch (error) {
2053
2234
  sendWorkerMessage(input.ws, {
2054
2235
  type: "pty_error",
@@ -2131,6 +2312,7 @@ async function startWorker(options) {
2131
2312
  fs.mkdirSync(artifactRoot, { recursive: true });
2132
2313
  fs.mkdirSync(planRoot, { recursive: true });
2133
2314
  const manifestByProjectId = /* @__PURE__ */ new Map();
2315
+ let repositorySyncQueue = Promise.resolve();
2134
2316
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2135
2317
  headers: {
2136
2318
  Authorization: `Bearer ${token}`
@@ -2188,42 +2370,64 @@ async function startWorker(options) {
2188
2370
  }
2189
2371
  process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2190
2372
  `);
2191
- for (const project of message.projects) {
2192
- const projectRoot = projectRootFor(projectsRoot, project.projectId, manifestByProjectId);
2193
- const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
2194
- for (const branchName of branches) {
2373
+ return;
2374
+ }
2375
+ if (message.type === "sync_projects") {
2376
+ let syncResult = { status: "completed", results: [], blockedBy: [] };
2377
+ const executeSync = async () => {
2378
+ syncResult = await syncManifestProjectsFromInternal({
2379
+ baseUrl,
2380
+ token,
2381
+ projectsRoot,
2382
+ syncRoot,
2383
+ manifests: [...manifestByProjectId.values()],
2384
+ projectIds: message.projectIds
2385
+ });
2386
+ for (const result of syncResult.results) {
2387
+ if (result.status !== "synced") continue;
2195
2388
  try {
2196
- ensureBranchWorkspace({
2197
- projectId: project.projectId,
2389
+ await syncProjectPlans({
2198
2390
  baseUrl,
2199
2391
  token,
2200
- projectRoot,
2201
- syncRoot,
2202
- branchName,
2203
- manifest: project
2392
+ projectId: result.projectId,
2393
+ branchName: result.branchName,
2394
+ planRoot
2204
2395
  });
2205
- try {
2206
- await syncProjectPlans({
2207
- baseUrl,
2208
- token,
2209
- projectId: project.projectId,
2210
- branchName,
2211
- planRoot
2212
- });
2213
- } catch (error) {
2214
- process.stderr.write(
2215
- `[r5d-worker] plan sync skipped for ${project.projectId}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2216
- `
2217
- );
2218
- }
2219
2396
  } catch (error) {
2220
2397
  process.stderr.write(
2221
- `[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2398
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2222
2399
  `
2223
2400
  );
2224
2401
  }
2225
2402
  }
2403
+ };
2404
+ const queued = repositorySyncQueue.then(executeSync, executeSync);
2405
+ repositorySyncQueue = queued.then(
2406
+ () => void 0,
2407
+ () => void 0
2408
+ );
2409
+ try {
2410
+ await queued;
2411
+ } catch (error) {
2412
+ syncResult = {
2413
+ status: "partial",
2414
+ results: [
2415
+ {
2416
+ projectId: "worker",
2417
+ projectPath: "worker",
2418
+ branchName: "all",
2419
+ status: "failed",
2420
+ error: error instanceof Error ? error.message : String(error)
2421
+ }
2422
+ ],
2423
+ blockedBy: []
2424
+ };
2226
2425
  }
2426
+ sendWorkerMessage(ws, {
2427
+ type: "sync_projects_result",
2428
+ requestId: message.requestId,
2429
+ result: syncResult
2430
+ });
2227
2431
  return;
2228
2432
  }
2229
2433
  if (message.type === "ping") {
@@ -2250,6 +2454,7 @@ async function startWorker(options) {
2250
2454
  return;
2251
2455
  }
2252
2456
  if (message.type === "pty_open") {
2457
+ await repositorySyncQueue;
2253
2458
  const manifest = manifestByProjectId.get(message.projectId);
2254
2459
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2255
2460
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2270,6 +2475,7 @@ async function startWorker(options) {
2270
2475
  return;
2271
2476
  }
2272
2477
  if (message.type === "exec") {
2478
+ await repositorySyncQueue;
2273
2479
  const manifest = manifestByProjectId.get(message.projectId);
2274
2480
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2275
2481
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2289,6 +2495,7 @@ async function startWorker(options) {
2289
2495
  return;
2290
2496
  }
2291
2497
  if (message.type === "exec_start") {
2498
+ await repositorySyncQueue;
2292
2499
  sendWorkerMessage(ws, {
2293
2500
  type: "exec_accepted",
2294
2501
  requestId: message.requestId,
@@ -2330,6 +2537,7 @@ async function startWorker(options) {
2330
2537
  }
2331
2538
  if (message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
2332
2539
  try {
2540
+ await repositorySyncQueue;
2333
2541
  const manifest = manifestByProjectId.get(message.projectId);
2334
2542
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2335
2543
  const result = await executeOperation({
@@ -2439,11 +2647,13 @@ if (isCliEntrypoint()) {
2439
2647
  }
2440
2648
  export {
2441
2649
  ensureVisibleGitCheckout,
2650
+ findRepositorySyncBlockers,
2442
2651
  isArtifactEnvPath,
2443
2652
  prepareArtifactEnvForShell,
2444
2653
  preparePlanEnvForShell,
2445
2654
  resolveHostShell,
2446
2655
  resolveProjectFilePath,
2656
+ syncManifestProjectsFromInternal,
2447
2657
  syncProjectPlans,
2448
2658
  syncSessionArtifacts
2449
2659
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "type": "module"
5
5
  }
@@ -1,4 +1,33 @@
1
1
  #!/usr/bin/env bun
2
+ type WorkerProjectManifestEntry = {
3
+ projectId: string;
4
+ repoSlug: string;
5
+ projectPath: string;
6
+ repoHttpUrl: string | null;
7
+ repoAuthHeader: string | null;
8
+ defaultBranch: string;
9
+ branches: string[];
10
+ internalRemoteUrl: string;
11
+ };
12
+ type WorkerRepositorySyncTarget = {
13
+ projectId: string;
14
+ projectPath: string;
15
+ branchName: string;
16
+ };
17
+ type WorkerRepositorySyncResult = {
18
+ status: "completed" | "partial" | "blocked";
19
+ results: Array<WorkerRepositorySyncTarget & ({
20
+ status: "synced";
21
+ commitHash: string;
22
+ } | {
23
+ status: "failed";
24
+ error: string;
25
+ })>;
26
+ blockedBy: Array<WorkerRepositorySyncTarget & {
27
+ kind: "process" | "shell";
28
+ id: string;
29
+ }>;
30
+ };
2
31
  export declare function isArtifactEnvPath(filePath: string): boolean;
3
32
  export declare function syncSessionArtifacts(input: {
4
33
  baseUrl: string;
@@ -47,7 +76,29 @@ export declare function ensureVisibleGitCheckout(input: {
47
76
  githubAuth?: GitAuth;
48
77
  githubAuthHeader?: string | null;
49
78
  defaultBranch: string;
79
+ reconcileExistingOrigin?: boolean;
50
80
  }): string;
81
+ export declare function findRepositorySyncBlockers(input: {
82
+ targets: WorkerRepositorySyncTarget[];
83
+ processes: Array<{
84
+ id: string;
85
+ projectId: string;
86
+ branchName: string;
87
+ }>;
88
+ shells: Array<{
89
+ id: string;
90
+ projectId: string;
91
+ branchName: string;
92
+ }>;
93
+ }): WorkerRepositorySyncResult["blockedBy"];
94
+ export declare function syncManifestProjectsFromInternal(input: {
95
+ baseUrl: string;
96
+ token: string;
97
+ projectsRoot: string;
98
+ syncRoot: string;
99
+ manifests: WorkerProjectManifestEntry[];
100
+ projectIds?: string[];
101
+ }): Promise<WorkerRepositorySyncResult>;
51
102
  export declare function resolveHostShell(command?: string, platform?: NodeJS.Platform): {
52
103
  file: string;
53
104
  args: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",