@ricsam/r5d-worker 0.0.24 → 0.0.26

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;
@@ -1747,6 +1926,7 @@ async function executeStreamingCommand(input) {
1747
1926
  projectId: input.projectId,
1748
1927
  sessionId: input.message.sessionId,
1749
1928
  branchName: input.message.branchName,
1929
+ credentialId: input.message.credentialId,
1750
1930
  argv: input.message.argv,
1751
1931
  command: input.message.command,
1752
1932
  cwd: input.message.cwd,
@@ -1820,6 +2000,7 @@ function buildActiveProcessReports() {
1820
2000
  projectId: active.projectId,
1821
2001
  sessionId: active.sessionId,
1822
2002
  branchName: active.branchName,
2003
+ ...active.credentialId ? { credentialId: active.credentialId } : {},
1823
2004
  argv: active.argv,
1824
2005
  command: active.command,
1825
2006
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2089,7 +2270,11 @@ async function openPty(input) {
2089
2270
  });
2090
2271
  }
2091
2272
  });
2092
- activePtys.set(input.message.ptyId, ptyProcess);
2273
+ activePtys.set(input.message.ptyId, {
2274
+ ...ptyProcess,
2275
+ projectId: input.projectId,
2276
+ branchName: input.message.branchName
2277
+ });
2093
2278
  } catch (error) {
2094
2279
  sendWorkerMessage(input.ws, {
2095
2280
  type: "pty_error",
@@ -2172,6 +2357,7 @@ async function startWorker(options) {
2172
2357
  import_node_fs.default.mkdirSync(artifactRoot, { recursive: true });
2173
2358
  import_node_fs.default.mkdirSync(planRoot, { recursive: true });
2174
2359
  const manifestByProjectId = /* @__PURE__ */ new Map();
2360
+ let repositorySyncQueue = Promise.resolve();
2175
2361
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2176
2362
  headers: {
2177
2363
  Authorization: `Bearer ${token}`
@@ -2229,42 +2415,64 @@ async function startWorker(options) {
2229
2415
  }
2230
2416
  process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2231
2417
  `);
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) {
2418
+ return;
2419
+ }
2420
+ if (message.type === "sync_projects") {
2421
+ let syncResult = { status: "completed", results: [], blockedBy: [] };
2422
+ const executeSync = async () => {
2423
+ syncResult = await syncManifestProjectsFromInternal({
2424
+ baseUrl,
2425
+ token,
2426
+ projectsRoot,
2427
+ syncRoot,
2428
+ manifests: [...manifestByProjectId.values()],
2429
+ projectIds: message.projectIds
2430
+ });
2431
+ for (const result of syncResult.results) {
2432
+ if (result.status !== "synced") continue;
2236
2433
  try {
2237
- ensureBranchWorkspace({
2238
- projectId: project.projectId,
2434
+ await syncProjectPlans({
2239
2435
  baseUrl,
2240
2436
  token,
2241
- projectRoot,
2242
- syncRoot,
2243
- branchName,
2244
- manifest: project
2437
+ projectId: result.projectId,
2438
+ branchName: result.branchName,
2439
+ planRoot
2245
2440
  });
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
2441
  } catch (error) {
2261
2442
  process.stderr.write(
2262
- `[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2443
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2263
2444
  `
2264
2445
  );
2265
2446
  }
2266
2447
  }
2448
+ };
2449
+ const queued = repositorySyncQueue.then(executeSync, executeSync);
2450
+ repositorySyncQueue = queued.then(
2451
+ () => void 0,
2452
+ () => void 0
2453
+ );
2454
+ try {
2455
+ await queued;
2456
+ } catch (error) {
2457
+ syncResult = {
2458
+ status: "partial",
2459
+ results: [
2460
+ {
2461
+ projectId: "worker",
2462
+ projectPath: "worker",
2463
+ branchName: "all",
2464
+ status: "failed",
2465
+ error: error instanceof Error ? error.message : String(error)
2466
+ }
2467
+ ],
2468
+ blockedBy: []
2469
+ };
2267
2470
  }
2471
+ sendWorkerMessage(ws, {
2472
+ type: "sync_projects_result",
2473
+ requestId: message.requestId,
2474
+ result: syncResult
2475
+ });
2268
2476
  return;
2269
2477
  }
2270
2478
  if (message.type === "ping") {
@@ -2291,6 +2499,7 @@ async function startWorker(options) {
2291
2499
  return;
2292
2500
  }
2293
2501
  if (message.type === "pty_open") {
2502
+ await repositorySyncQueue;
2294
2503
  const manifest = manifestByProjectId.get(message.projectId);
2295
2504
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2296
2505
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2311,6 +2520,7 @@ async function startWorker(options) {
2311
2520
  return;
2312
2521
  }
2313
2522
  if (message.type === "exec") {
2523
+ await repositorySyncQueue;
2314
2524
  const manifest = manifestByProjectId.get(message.projectId);
2315
2525
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2316
2526
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2330,6 +2540,7 @@ async function startWorker(options) {
2330
2540
  return;
2331
2541
  }
2332
2542
  if (message.type === "exec_start") {
2543
+ await repositorySyncQueue;
2333
2544
  sendWorkerMessage(ws, {
2334
2545
  type: "exec_accepted",
2335
2546
  requestId: message.requestId,
@@ -2371,6 +2582,7 @@ async function startWorker(options) {
2371
2582
  }
2372
2583
  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
2584
  try {
2585
+ await repositorySyncQueue;
2374
2586
  const manifest = manifestByProjectId.get(message.projectId);
2375
2587
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2376
2588
  const result = await executeOperation({
@@ -2481,11 +2693,13 @@ if (isCliEntrypoint()) {
2481
2693
  // Annotate the CommonJS export names for ESM import in node:
2482
2694
  0 && (module.exports = {
2483
2695
  ensureVisibleGitCheckout,
2696
+ findRepositorySyncBlockers,
2484
2697
  isArtifactEnvPath,
2485
2698
  prepareArtifactEnvForShell,
2486
2699
  preparePlanEnvForShell,
2487
2700
  resolveHostShell,
2488
2701
  resolveProjectFilePath,
2702
+ syncManifestProjectsFromInternal,
2489
2703
  syncProjectPlans,
2490
2704
  syncSessionArtifacts
2491
2705
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
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;
@@ -1706,6 +1883,7 @@ async function executeStreamingCommand(input) {
1706
1883
  projectId: input.projectId,
1707
1884
  sessionId: input.message.sessionId,
1708
1885
  branchName: input.message.branchName,
1886
+ credentialId: input.message.credentialId,
1709
1887
  argv: input.message.argv,
1710
1888
  command: input.message.command,
1711
1889
  cwd: input.message.cwd,
@@ -1779,6 +1957,7 @@ function buildActiveProcessReports() {
1779
1957
  projectId: active.projectId,
1780
1958
  sessionId: active.sessionId,
1781
1959
  branchName: active.branchName,
1960
+ ...active.credentialId ? { credentialId: active.credentialId } : {},
1782
1961
  argv: active.argv,
1783
1962
  command: active.command,
1784
1963
  ...active.cwd ? { cwd: active.cwd } : {},
@@ -2048,7 +2227,11 @@ async function openPty(input) {
2048
2227
  });
2049
2228
  }
2050
2229
  });
2051
- activePtys.set(input.message.ptyId, ptyProcess);
2230
+ activePtys.set(input.message.ptyId, {
2231
+ ...ptyProcess,
2232
+ projectId: input.projectId,
2233
+ branchName: input.message.branchName
2234
+ });
2052
2235
  } catch (error) {
2053
2236
  sendWorkerMessage(input.ws, {
2054
2237
  type: "pty_error",
@@ -2131,6 +2314,7 @@ async function startWorker(options) {
2131
2314
  fs.mkdirSync(artifactRoot, { recursive: true });
2132
2315
  fs.mkdirSync(planRoot, { recursive: true });
2133
2316
  const manifestByProjectId = /* @__PURE__ */ new Map();
2317
+ let repositorySyncQueue = Promise.resolve();
2134
2318
  const ws = new WebSocket(websocketUrl(baseUrl, label), {
2135
2319
  headers: {
2136
2320
  Authorization: `Bearer ${token}`
@@ -2188,42 +2372,64 @@ async function startWorker(options) {
2188
2372
  }
2189
2373
  process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
2190
2374
  `);
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) {
2375
+ return;
2376
+ }
2377
+ if (message.type === "sync_projects") {
2378
+ let syncResult = { status: "completed", results: [], blockedBy: [] };
2379
+ const executeSync = async () => {
2380
+ syncResult = await syncManifestProjectsFromInternal({
2381
+ baseUrl,
2382
+ token,
2383
+ projectsRoot,
2384
+ syncRoot,
2385
+ manifests: [...manifestByProjectId.values()],
2386
+ projectIds: message.projectIds
2387
+ });
2388
+ for (const result of syncResult.results) {
2389
+ if (result.status !== "synced") continue;
2195
2390
  try {
2196
- ensureBranchWorkspace({
2197
- projectId: project.projectId,
2391
+ await syncProjectPlans({
2198
2392
  baseUrl,
2199
2393
  token,
2200
- projectRoot,
2201
- syncRoot,
2202
- branchName,
2203
- manifest: project
2394
+ projectId: result.projectId,
2395
+ branchName: result.branchName,
2396
+ planRoot
2204
2397
  });
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
2398
  } catch (error) {
2220
2399
  process.stderr.write(
2221
- `[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
2400
+ `[r5d-worker] plan sync skipped for ${result.projectId}/${result.branchName}: ${error instanceof Error ? error.message : String(error)}
2222
2401
  `
2223
2402
  );
2224
2403
  }
2225
2404
  }
2405
+ };
2406
+ const queued = repositorySyncQueue.then(executeSync, executeSync);
2407
+ repositorySyncQueue = queued.then(
2408
+ () => void 0,
2409
+ () => void 0
2410
+ );
2411
+ try {
2412
+ await queued;
2413
+ } catch (error) {
2414
+ syncResult = {
2415
+ status: "partial",
2416
+ results: [
2417
+ {
2418
+ projectId: "worker",
2419
+ projectPath: "worker",
2420
+ branchName: "all",
2421
+ status: "failed",
2422
+ error: error instanceof Error ? error.message : String(error)
2423
+ }
2424
+ ],
2425
+ blockedBy: []
2426
+ };
2226
2427
  }
2428
+ sendWorkerMessage(ws, {
2429
+ type: "sync_projects_result",
2430
+ requestId: message.requestId,
2431
+ result: syncResult
2432
+ });
2227
2433
  return;
2228
2434
  }
2229
2435
  if (message.type === "ping") {
@@ -2250,6 +2456,7 @@ async function startWorker(options) {
2250
2456
  return;
2251
2457
  }
2252
2458
  if (message.type === "pty_open") {
2459
+ await repositorySyncQueue;
2253
2460
  const manifest = manifestByProjectId.get(message.projectId);
2254
2461
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2255
2462
  process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
@@ -2270,6 +2477,7 @@ async function startWorker(options) {
2270
2477
  return;
2271
2478
  }
2272
2479
  if (message.type === "exec") {
2480
+ await repositorySyncQueue;
2273
2481
  const manifest = manifestByProjectId.get(message.projectId);
2274
2482
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2275
2483
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
@@ -2289,6 +2497,7 @@ async function startWorker(options) {
2289
2497
  return;
2290
2498
  }
2291
2499
  if (message.type === "exec_start") {
2500
+ await repositorySyncQueue;
2292
2501
  sendWorkerMessage(ws, {
2293
2502
  type: "exec_accepted",
2294
2503
  requestId: message.requestId,
@@ -2330,6 +2539,7 @@ async function startWorker(options) {
2330
2539
  }
2331
2540
  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
2541
  try {
2542
+ await repositorySyncQueue;
2333
2543
  const manifest = manifestByProjectId.get(message.projectId);
2334
2544
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
2335
2545
  const result = await executeOperation({
@@ -2439,11 +2649,13 @@ if (isCliEntrypoint()) {
2439
2649
  }
2440
2650
  export {
2441
2651
  ensureVisibleGitCheckout,
2652
+ findRepositorySyncBlockers,
2442
2653
  isArtifactEnvPath,
2443
2654
  prepareArtifactEnvForShell,
2444
2655
  preparePlanEnvForShell,
2445
2656
  resolveHostShell,
2446
2657
  resolveProjectFilePath,
2658
+ syncManifestProjectsFromInternal,
2447
2659
  syncProjectPlans,
2448
2660
  syncSessionArtifacts
2449
2661
  };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
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.26",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",