@ricsam/r5d-worker 0.0.7 → 0.0.9

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
@@ -13,7 +13,7 @@ The worker uses the existing `r5dctl` config file and accepts `R5D_WORKER_TOKEN`
13
13
  ~/.r5d/projects/<namespace-repo>/<branch-name>
14
14
  ```
15
15
 
16
- Visible branch checkouts use GitHub `origin` remotes. r5d internal sync git metadata lives separately under `~/.r5d/sync`.
16
+ 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`.
17
17
 
18
18
  Worker labels are mandatory and unique per user. Choose labels that describe host capabilities, such as `macos`, `linux`, `ios`, or `ec2-build`.
19
19
 
package/dist/cjs/main.cjs CHANGED
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,6 +26,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+ var main_exports = {};
31
+ __export(main_exports, {
32
+ ensureVisibleGitCheckout: () => ensureVisibleGitCheckout
33
+ });
34
+ module.exports = __toCommonJS(main_exports);
25
35
  var import_node_fs = __toESM(require("node:fs"), 1);
26
36
  var import_node_os = __toESM(require("node:os"), 1);
27
37
  var import_node_path = __toESM(require("node:path"), 1);
@@ -29,6 +39,7 @@ var import_node_crypto = require("node:crypto");
29
39
  var import_node_os2 = require("node:os");
30
40
  var import_node_child_process = require("node:child_process");
31
41
  var import_git_identity = require("./git-identity.cjs");
42
+ const import_meta = {};
32
43
  const DEFAULT_BASE_URL = "https://r5d.dev";
33
44
  const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
34
45
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
@@ -39,6 +50,7 @@ const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
39
50
  const R5D_REMOTE_NAME = "r5d";
40
51
  const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
41
52
  const activeProcesses = /* @__PURE__ */ new Map();
53
+ const cancelledProcessRuns = /* @__PURE__ */ new Set();
42
54
  const activePtys = /* @__PURE__ */ new Map();
43
55
  const branchLocks = /* @__PURE__ */ new Map();
44
56
  let containerRegistryCredential = null;
@@ -595,19 +607,23 @@ function ensureVisibleGitCheckout(input) {
595
607
  validateBranchName(input.branchName);
596
608
  import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
597
609
  const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
598
- if (!hasNormalVisibleGitDir(branchPath)) {
610
+ const createdCheckout = !hasNormalVisibleGitDir(branchPath);
611
+ if (createdCheckout) {
599
612
  import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
600
613
  import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
601
614
  if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
602
615
  import_node_fs.default.mkdirSync(branchPath, { recursive: true });
603
616
  runGit(["init"], { cwd: branchPath });
604
617
  }
618
+ ensureOriginRemote(branchPath, input.githubRemoteUrl);
619
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
620
+ (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
621
+ tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
622
+ checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
623
+ return branchPath;
605
624
  }
606
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
607
625
  configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
608
626
  (0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
609
- tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
610
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
611
627
  return branchPath;
612
628
  }
613
629
  function configureInternalRepo(context, remoteUrl) {
@@ -713,6 +729,15 @@ async function streamCommandOutput(stream, onData) {
713
729
  function ensureOperationBranch(input) {
714
730
  return ensureBranchWorkspace(input);
715
731
  }
732
+ function internalGitProcessEnv(workspace, env) {
733
+ if (env?.R5D_USE_INTERNAL_GIT !== "1") {
734
+ return {};
735
+ }
736
+ return {
737
+ GIT_DIR: workspace.internalGit.gitDir,
738
+ GIT_WORK_TREE: workspace.branchPath
739
+ };
740
+ }
716
741
  function formatLineNumberedContent(content, offset, limit) {
717
742
  const allLines = content.split("\n");
718
743
  const totalLines = allLines.length;
@@ -856,6 +881,171 @@ async function executeEditFileOperation(input) {
856
881
  file: resolved.virtualPath
857
882
  };
858
883
  }
884
+ function decodePatchPath(rawPath) {
885
+ const trimmed = rawPath.trim();
886
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
887
+ try {
888
+ return JSON.parse(trimmed);
889
+ } catch {
890
+ return trimmed.slice(1, -1);
891
+ }
892
+ }
893
+ return trimmed;
894
+ }
895
+ function normalizePatchPath(rawPath) {
896
+ const withoutMetadata = rawPath.split(" ")[0] ?? rawPath;
897
+ let patchPath = decodePatchPath(withoutMetadata);
898
+ if (patchPath === "/dev/null") {
899
+ return null;
900
+ }
901
+ if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
902
+ patchPath = patchPath.slice(2);
903
+ }
904
+ if (!patchPath || import_node_path.default.isAbsolute(patchPath)) {
905
+ throw new Error(`Invalid patch path: ${rawPath}`);
906
+ }
907
+ const normalized = import_node_path.default.posix.normalize(patchPath);
908
+ if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
909
+ throw new Error(`Invalid patch path: ${rawPath}`);
910
+ }
911
+ return normalized;
912
+ }
913
+ function readDiffPathToken(value, startIndex) {
914
+ let index = startIndex;
915
+ while (index < value.length && /\s/.test(value[index] ?? "")) {
916
+ index += 1;
917
+ }
918
+ if (index >= value.length) {
919
+ throw new Error("Missing path in patch header");
920
+ }
921
+ if (value[index] !== '"') {
922
+ const tokenStart2 = index;
923
+ while (index < value.length && !/\s/.test(value[index] ?? "")) {
924
+ index += 1;
925
+ }
926
+ return { token: value.slice(tokenStart2, index), nextIndex: index };
927
+ }
928
+ const tokenStart = index;
929
+ index += 1;
930
+ let escaped = false;
931
+ while (index < value.length) {
932
+ const character = value[index];
933
+ if (escaped) {
934
+ escaped = false;
935
+ } else if (character === "\\") {
936
+ escaped = true;
937
+ } else if (character === '"') {
938
+ index += 1;
939
+ return { token: value.slice(tokenStart, index), nextIndex: index };
940
+ }
941
+ index += 1;
942
+ }
943
+ throw new Error("Unterminated quoted path in patch header");
944
+ }
945
+ function parseDiffGitPaths(line) {
946
+ if (!line.startsWith("diff --git ")) {
947
+ throw new Error(`Invalid patch header: ${line}`);
948
+ }
949
+ const rest = line.slice("diff --git ".length);
950
+ const first = readDiffPathToken(rest, 0);
951
+ const second = readDiffPathToken(rest, first.nextIndex);
952
+ if (rest.slice(second.nextIndex).trim()) {
953
+ throw new Error(`Invalid patch header: ${line}`);
954
+ }
955
+ return {
956
+ oldPath: normalizePatchPath(first.token),
957
+ newPath: normalizePatchPath(second.token)
958
+ };
959
+ }
960
+ function summarizePatch(patch) {
961
+ if (!patch.trim()) {
962
+ throw new Error("Patch cannot be empty");
963
+ }
964
+ const files = [];
965
+ let current;
966
+ for (const line of patch.split(/\r?\n/)) {
967
+ if (line.startsWith("diff --git ")) {
968
+ const { oldPath, newPath } = parseDiffGitPaths(line);
969
+ if (!oldPath && !newPath) {
970
+ throw new Error(`Invalid patch header: ${line}`);
971
+ }
972
+ current = {
973
+ path: newPath ?? oldPath,
974
+ ...oldPath && newPath && oldPath !== newPath ? { oldPath } : {},
975
+ operation: oldPath && newPath && oldPath !== newPath ? "move" : "update",
976
+ additions: 0,
977
+ deletions: 0,
978
+ hunks: 0
979
+ };
980
+ files.push(current);
981
+ continue;
982
+ }
983
+ if (!current) {
984
+ continue;
985
+ }
986
+ if (line.startsWith("new file mode ")) {
987
+ current.operation = "add";
988
+ continue;
989
+ }
990
+ if (line.startsWith("deleted file mode ")) {
991
+ current.operation = "delete";
992
+ if (current.oldPath) {
993
+ current.path = current.oldPath;
994
+ delete current.oldPath;
995
+ }
996
+ continue;
997
+ }
998
+ if (line.startsWith("rename from ")) {
999
+ current.operation = "move";
1000
+ current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
1001
+ continue;
1002
+ }
1003
+ if (line.startsWith("rename to ")) {
1004
+ current.operation = "move";
1005
+ current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
1006
+ continue;
1007
+ }
1008
+ if (line.startsWith("@@")) {
1009
+ current.hunks += 1;
1010
+ continue;
1011
+ }
1012
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1013
+ current.additions += 1;
1014
+ continue;
1015
+ }
1016
+ if (line.startsWith("-") && !line.startsWith("---")) {
1017
+ current.deletions += 1;
1018
+ }
1019
+ }
1020
+ if (files.length === 0) {
1021
+ throw new Error("Patch must contain at least one diff --git file header");
1022
+ }
1023
+ return files;
1024
+ }
1025
+ async function executeApplyPatchOperation(input) {
1026
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
1027
+ const files = summarizePatch(input.message.patch);
1028
+ const patchPath = import_node_path.default.join(import_node_os.default.tmpdir(), `r5d-apply-patch-${(0, import_node_crypto.randomUUID)()}.patch`);
1029
+ try {
1030
+ import_node_fs.default.writeFileSync(patchPath, input.message.patch, "utf8");
1031
+ runInternalGit(workspace.internalGit, ["apply", "--check", "--whitespace=nowarn", patchPath]);
1032
+ runInternalGit(workspace.internalGit, ["apply", "--whitespace=nowarn", patchPath]);
1033
+ } catch (error) {
1034
+ throw new Error(`Patch did not apply: ${error instanceof Error ? error.message : String(error)}`);
1035
+ } finally {
1036
+ import_node_fs.default.rmSync(patchPath, { force: true });
1037
+ }
1038
+ return {
1039
+ type: "apply_patch",
1040
+ files,
1041
+ summary: {
1042
+ filesChanged: files.length,
1043
+ additions: files.reduce((sum, file) => sum + file.additions, 0),
1044
+ deletions: files.reduce((sum, file) => sum + file.deletions, 0),
1045
+ hunks: files.reduce((sum, file) => sum + file.hunks, 0)
1046
+ }
1047
+ };
1048
+ }
859
1049
  async function executeViewFileBytesOperation(input) {
860
1050
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
861
1051
  const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
@@ -977,6 +1167,8 @@ async function executeOperation(input) {
977
1167
  return executeCreateFileOperation({ ...input, message: input.message });
978
1168
  case "edit_file":
979
1169
  return executeEditFileOperation({ ...input, message: input.message });
1170
+ case "apply_patch":
1171
+ return executeApplyPatchOperation({ ...input, message: input.message });
980
1172
  case "view_file_bytes":
981
1173
  return executeViewFileBytesOperation({ ...input, message: input.message });
982
1174
  case "sync":
@@ -1022,7 +1214,7 @@ async function executeCommand(input) {
1022
1214
  const startedAt = Date.now();
1023
1215
  let timeout;
1024
1216
  try {
1025
- const branchPath = ensureBranchWorkspace({
1217
+ const workspace = ensureBranchWorkspace({
1026
1218
  projectId: input.projectId,
1027
1219
  baseUrl: input.baseUrl,
1028
1220
  token: input.token,
@@ -1031,7 +1223,7 @@ async function executeCommand(input) {
1031
1223
  branchName: input.message.branchName,
1032
1224
  manifest: input.manifest
1033
1225
  });
1034
- const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
1226
+ const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1035
1227
  const subprocess = Bun.spawn(input.message.argv, {
1036
1228
  cwd,
1037
1229
  stdout: "pipe",
@@ -1039,7 +1231,8 @@ async function executeCommand(input) {
1039
1231
  env: {
1040
1232
  ...process.env,
1041
1233
  ...containerRegistryEnv(),
1042
- ...input.message.env ?? {}
1234
+ ...input.message.env ?? {},
1235
+ ...internalGitProcessEnv(workspace, input.message.env)
1043
1236
  }
1044
1237
  });
1045
1238
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1084,7 +1277,16 @@ async function executeStreamingCommand(input) {
1084
1277
  let started = false;
1085
1278
  let timedOut = false;
1086
1279
  try {
1087
- const branchPath = ensureBranchWorkspace({
1280
+ if (cancelledProcessRuns.delete(input.message.runId)) {
1281
+ sendWorkerMessage(input.ws, {
1282
+ type: "exec_start_error",
1283
+ requestId: input.message.requestId,
1284
+ runId: input.message.runId,
1285
+ error: "Command cancelled before it started"
1286
+ });
1287
+ return;
1288
+ }
1289
+ const workspace = ensureBranchWorkspace({
1088
1290
  projectId: input.projectId,
1089
1291
  baseUrl: input.baseUrl,
1090
1292
  token: input.token,
@@ -1093,7 +1295,7 @@ async function executeStreamingCommand(input) {
1093
1295
  branchName: input.message.branchName,
1094
1296
  manifest: input.manifest
1095
1297
  });
1096
- const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
1298
+ const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1097
1299
  const subprocess = Bun.spawn(input.message.argv, {
1098
1300
  cwd,
1099
1301
  stdout: "pipe",
@@ -1101,7 +1303,8 @@ async function executeStreamingCommand(input) {
1101
1303
  env: {
1102
1304
  ...process.env,
1103
1305
  ...containerRegistryEnv(),
1104
- ...input.message.env ?? {}
1306
+ ...input.message.env ?? {},
1307
+ ...internalGitProcessEnv(workspace, input.message.env)
1105
1308
  }
1106
1309
  });
1107
1310
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1165,6 +1368,7 @@ async function executeStreamingCommand(input) {
1165
1368
  clearTimeout(timeout);
1166
1369
  }
1167
1370
  activeProcesses.delete(input.message.runId);
1371
+ cancelledProcessRuns.delete(input.message.runId);
1168
1372
  }
1169
1373
  }
1170
1374
  function sendWorkerMessage(ws, message) {
@@ -1557,14 +1761,16 @@ async function startWorker(options) {
1557
1761
  const active = activeProcesses.get(message.runId);
1558
1762
  if (active) {
1559
1763
  active.process.kill("SIGTERM");
1764
+ } else {
1765
+ cancelledProcessRuns.add(message.runId);
1560
1766
  }
1561
1767
  ws.send(
1562
1768
  JSON.stringify({
1563
1769
  type: "cancel_result",
1564
1770
  requestId: message.requestId,
1565
1771
  runId: message.runId,
1566
- cancelled: !!active,
1567
- message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "No active command with that run id"
1772
+ cancelled: true,
1773
+ message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "Cancellation queued before command start"
1568
1774
  })
1569
1775
  );
1570
1776
  return;
@@ -1603,6 +1809,11 @@ async function startWorker(options) {
1603
1809
  return;
1604
1810
  }
1605
1811
  if (message.type === "exec_start") {
1812
+ sendWorkerMessage(ws, {
1813
+ type: "exec_accepted",
1814
+ requestId: message.requestId,
1815
+ runId: message.runId
1816
+ });
1606
1817
  try {
1607
1818
  const manifest = manifestByProjectId.get(message.projectId);
1608
1819
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -1630,7 +1841,7 @@ async function startWorker(options) {
1630
1841
  }
1631
1842
  return;
1632
1843
  }
1633
- if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
1844
+ if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "apply_patch" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
1634
1845
  try {
1635
1846
  const manifest = manifestByProjectId.get(message.projectId);
1636
1847
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -1655,6 +1866,17 @@ async function startWorker(options) {
1655
1866
  })
1656
1867
  );
1657
1868
  }
1869
+ return;
1870
+ }
1871
+ const unhandledMessage = message;
1872
+ if (typeof unhandledMessage.requestId === "string") {
1873
+ ws.send(
1874
+ JSON.stringify({
1875
+ type: "operation_result",
1876
+ requestId: unhandledMessage.requestId,
1877
+ error: `Unsupported worker message type: ${String(unhandledMessage.type)}`
1878
+ })
1879
+ );
1658
1880
  }
1659
1881
  })().catch((error) => {
1660
1882
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
@@ -1688,8 +1910,14 @@ async function main() {
1688
1910
  }
1689
1911
  await startWorker(parsed.options);
1690
1912
  }
1691
- main().catch((error) => {
1692
- process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
1913
+ if (import_meta.main) {
1914
+ main().catch((error) => {
1915
+ process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
1693
1916
  `);
1694
- process.exit(1);
1917
+ process.exit(1);
1918
+ });
1919
+ }
1920
+ // Annotate the CommonJS export names for ESM import in node:
1921
+ 0 && (module.exports = {
1922
+ ensureVisibleGitCheckout
1695
1923
  });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { createHash } from "node:crypto";
5
+ import { createHash, randomUUID } from "node:crypto";
6
6
  import { hostname } from "node:os";
7
7
  import { spawn as spawnChildProcess } from "node:child_process";
8
8
  import { configureVisibleGitIdentity } from "./git-identity.mjs";
@@ -16,6 +16,7 @@ const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
16
16
  const R5D_REMOTE_NAME = "r5d";
17
17
  const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
18
18
  const activeProcesses = /* @__PURE__ */ new Map();
19
+ const cancelledProcessRuns = /* @__PURE__ */ new Set();
19
20
  const activePtys = /* @__PURE__ */ new Map();
20
21
  const branchLocks = /* @__PURE__ */ new Map();
21
22
  let containerRegistryCredential = null;
@@ -572,19 +573,23 @@ function ensureVisibleGitCheckout(input) {
572
573
  validateBranchName(input.branchName);
573
574
  fs.mkdirSync(input.projectRoot, { recursive: true });
574
575
  const branchPath = path.join(input.projectRoot, input.branchName);
575
- if (!hasNormalVisibleGitDir(branchPath)) {
576
+ const createdCheckout = !hasNormalVisibleGitDir(branchPath);
577
+ if (createdCheckout) {
576
578
  fs.rmSync(branchPath, { recursive: true, force: true });
577
579
  fs.mkdirSync(path.dirname(branchPath), { recursive: true });
578
580
  if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
579
581
  fs.mkdirSync(branchPath, { recursive: true });
580
582
  runGit(["init"], { cwd: branchPath });
581
583
  }
584
+ ensureOriginRemote(branchPath, input.githubRemoteUrl);
585
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
586
+ configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
587
+ tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
588
+ checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
589
+ return branchPath;
582
590
  }
583
- ensureOriginRemote(branchPath, input.githubRemoteUrl);
584
591
  configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
585
592
  configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
586
- tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
587
- checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
588
593
  return branchPath;
589
594
  }
590
595
  function configureInternalRepo(context, remoteUrl) {
@@ -690,6 +695,15 @@ async function streamCommandOutput(stream, onData) {
690
695
  function ensureOperationBranch(input) {
691
696
  return ensureBranchWorkspace(input);
692
697
  }
698
+ function internalGitProcessEnv(workspace, env) {
699
+ if (env?.R5D_USE_INTERNAL_GIT !== "1") {
700
+ return {};
701
+ }
702
+ return {
703
+ GIT_DIR: workspace.internalGit.gitDir,
704
+ GIT_WORK_TREE: workspace.branchPath
705
+ };
706
+ }
693
707
  function formatLineNumberedContent(content, offset, limit) {
694
708
  const allLines = content.split("\n");
695
709
  const totalLines = allLines.length;
@@ -833,6 +847,171 @@ async function executeEditFileOperation(input) {
833
847
  file: resolved.virtualPath
834
848
  };
835
849
  }
850
+ function decodePatchPath(rawPath) {
851
+ const trimmed = rawPath.trim();
852
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
853
+ try {
854
+ return JSON.parse(trimmed);
855
+ } catch {
856
+ return trimmed.slice(1, -1);
857
+ }
858
+ }
859
+ return trimmed;
860
+ }
861
+ function normalizePatchPath(rawPath) {
862
+ const withoutMetadata = rawPath.split(" ")[0] ?? rawPath;
863
+ let patchPath = decodePatchPath(withoutMetadata);
864
+ if (patchPath === "/dev/null") {
865
+ return null;
866
+ }
867
+ if (patchPath.startsWith("a/") || patchPath.startsWith("b/")) {
868
+ patchPath = patchPath.slice(2);
869
+ }
870
+ if (!patchPath || path.isAbsolute(patchPath)) {
871
+ throw new Error(`Invalid patch path: ${rawPath}`);
872
+ }
873
+ const normalized = path.posix.normalize(patchPath);
874
+ if (normalized === "." || normalized.startsWith("../") || normalized === "..") {
875
+ throw new Error(`Invalid patch path: ${rawPath}`);
876
+ }
877
+ return normalized;
878
+ }
879
+ function readDiffPathToken(value, startIndex) {
880
+ let index = startIndex;
881
+ while (index < value.length && /\s/.test(value[index] ?? "")) {
882
+ index += 1;
883
+ }
884
+ if (index >= value.length) {
885
+ throw new Error("Missing path in patch header");
886
+ }
887
+ if (value[index] !== '"') {
888
+ const tokenStart2 = index;
889
+ while (index < value.length && !/\s/.test(value[index] ?? "")) {
890
+ index += 1;
891
+ }
892
+ return { token: value.slice(tokenStart2, index), nextIndex: index };
893
+ }
894
+ const tokenStart = index;
895
+ index += 1;
896
+ let escaped = false;
897
+ while (index < value.length) {
898
+ const character = value[index];
899
+ if (escaped) {
900
+ escaped = false;
901
+ } else if (character === "\\") {
902
+ escaped = true;
903
+ } else if (character === '"') {
904
+ index += 1;
905
+ return { token: value.slice(tokenStart, index), nextIndex: index };
906
+ }
907
+ index += 1;
908
+ }
909
+ throw new Error("Unterminated quoted path in patch header");
910
+ }
911
+ function parseDiffGitPaths(line) {
912
+ if (!line.startsWith("diff --git ")) {
913
+ throw new Error(`Invalid patch header: ${line}`);
914
+ }
915
+ const rest = line.slice("diff --git ".length);
916
+ const first = readDiffPathToken(rest, 0);
917
+ const second = readDiffPathToken(rest, first.nextIndex);
918
+ if (rest.slice(second.nextIndex).trim()) {
919
+ throw new Error(`Invalid patch header: ${line}`);
920
+ }
921
+ return {
922
+ oldPath: normalizePatchPath(first.token),
923
+ newPath: normalizePatchPath(second.token)
924
+ };
925
+ }
926
+ function summarizePatch(patch) {
927
+ if (!patch.trim()) {
928
+ throw new Error("Patch cannot be empty");
929
+ }
930
+ const files = [];
931
+ let current;
932
+ for (const line of patch.split(/\r?\n/)) {
933
+ if (line.startsWith("diff --git ")) {
934
+ const { oldPath, newPath } = parseDiffGitPaths(line);
935
+ if (!oldPath && !newPath) {
936
+ throw new Error(`Invalid patch header: ${line}`);
937
+ }
938
+ current = {
939
+ path: newPath ?? oldPath,
940
+ ...oldPath && newPath && oldPath !== newPath ? { oldPath } : {},
941
+ operation: oldPath && newPath && oldPath !== newPath ? "move" : "update",
942
+ additions: 0,
943
+ deletions: 0,
944
+ hunks: 0
945
+ };
946
+ files.push(current);
947
+ continue;
948
+ }
949
+ if (!current) {
950
+ continue;
951
+ }
952
+ if (line.startsWith("new file mode ")) {
953
+ current.operation = "add";
954
+ continue;
955
+ }
956
+ if (line.startsWith("deleted file mode ")) {
957
+ current.operation = "delete";
958
+ if (current.oldPath) {
959
+ current.path = current.oldPath;
960
+ delete current.oldPath;
961
+ }
962
+ continue;
963
+ }
964
+ if (line.startsWith("rename from ")) {
965
+ current.operation = "move";
966
+ current.oldPath = normalizePatchPath(line.slice("rename from ".length)) ?? void 0;
967
+ continue;
968
+ }
969
+ if (line.startsWith("rename to ")) {
970
+ current.operation = "move";
971
+ current.path = normalizePatchPath(line.slice("rename to ".length)) ?? current.path;
972
+ continue;
973
+ }
974
+ if (line.startsWith("@@")) {
975
+ current.hunks += 1;
976
+ continue;
977
+ }
978
+ if (line.startsWith("+") && !line.startsWith("+++")) {
979
+ current.additions += 1;
980
+ continue;
981
+ }
982
+ if (line.startsWith("-") && !line.startsWith("---")) {
983
+ current.deletions += 1;
984
+ }
985
+ }
986
+ if (files.length === 0) {
987
+ throw new Error("Patch must contain at least one diff --git file header");
988
+ }
989
+ return files;
990
+ }
991
+ async function executeApplyPatchOperation(input) {
992
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
993
+ const files = summarizePatch(input.message.patch);
994
+ const patchPath = path.join(os.tmpdir(), `r5d-apply-patch-${randomUUID()}.patch`);
995
+ try {
996
+ fs.writeFileSync(patchPath, input.message.patch, "utf8");
997
+ runInternalGit(workspace.internalGit, ["apply", "--check", "--whitespace=nowarn", patchPath]);
998
+ runInternalGit(workspace.internalGit, ["apply", "--whitespace=nowarn", patchPath]);
999
+ } catch (error) {
1000
+ throw new Error(`Patch did not apply: ${error instanceof Error ? error.message : String(error)}`);
1001
+ } finally {
1002
+ fs.rmSync(patchPath, { force: true });
1003
+ }
1004
+ return {
1005
+ type: "apply_patch",
1006
+ files,
1007
+ summary: {
1008
+ filesChanged: files.length,
1009
+ additions: files.reduce((sum, file) => sum + file.additions, 0),
1010
+ deletions: files.reduce((sum, file) => sum + file.deletions, 0),
1011
+ hunks: files.reduce((sum, file) => sum + file.hunks, 0)
1012
+ }
1013
+ };
1014
+ }
836
1015
  async function executeViewFileBytesOperation(input) {
837
1016
  const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
838
1017
  const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
@@ -954,6 +1133,8 @@ async function executeOperation(input) {
954
1133
  return executeCreateFileOperation({ ...input, message: input.message });
955
1134
  case "edit_file":
956
1135
  return executeEditFileOperation({ ...input, message: input.message });
1136
+ case "apply_patch":
1137
+ return executeApplyPatchOperation({ ...input, message: input.message });
957
1138
  case "view_file_bytes":
958
1139
  return executeViewFileBytesOperation({ ...input, message: input.message });
959
1140
  case "sync":
@@ -999,7 +1180,7 @@ async function executeCommand(input) {
999
1180
  const startedAt = Date.now();
1000
1181
  let timeout;
1001
1182
  try {
1002
- const branchPath = ensureBranchWorkspace({
1183
+ const workspace = ensureBranchWorkspace({
1003
1184
  projectId: input.projectId,
1004
1185
  baseUrl: input.baseUrl,
1005
1186
  token: input.token,
@@ -1008,7 +1189,7 @@ async function executeCommand(input) {
1008
1189
  branchName: input.message.branchName,
1009
1190
  manifest: input.manifest
1010
1191
  });
1011
- const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
1192
+ const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1012
1193
  const subprocess = Bun.spawn(input.message.argv, {
1013
1194
  cwd,
1014
1195
  stdout: "pipe",
@@ -1016,7 +1197,8 @@ async function executeCommand(input) {
1016
1197
  env: {
1017
1198
  ...process.env,
1018
1199
  ...containerRegistryEnv(),
1019
- ...input.message.env ?? {}
1200
+ ...input.message.env ?? {},
1201
+ ...internalGitProcessEnv(workspace, input.message.env)
1020
1202
  }
1021
1203
  });
1022
1204
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1061,7 +1243,16 @@ async function executeStreamingCommand(input) {
1061
1243
  let started = false;
1062
1244
  let timedOut = false;
1063
1245
  try {
1064
- const branchPath = ensureBranchWorkspace({
1246
+ if (cancelledProcessRuns.delete(input.message.runId)) {
1247
+ sendWorkerMessage(input.ws, {
1248
+ type: "exec_start_error",
1249
+ requestId: input.message.requestId,
1250
+ runId: input.message.runId,
1251
+ error: "Command cancelled before it started"
1252
+ });
1253
+ return;
1254
+ }
1255
+ const workspace = ensureBranchWorkspace({
1065
1256
  projectId: input.projectId,
1066
1257
  baseUrl: input.baseUrl,
1067
1258
  token: input.token,
@@ -1070,7 +1261,7 @@ async function executeStreamingCommand(input) {
1070
1261
  branchName: input.message.branchName,
1071
1262
  manifest: input.manifest
1072
1263
  });
1073
- const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
1264
+ const cwd = resolveCommandCwd(workspace.branchPath, input.message.cwd);
1074
1265
  const subprocess = Bun.spawn(input.message.argv, {
1075
1266
  cwd,
1076
1267
  stdout: "pipe",
@@ -1078,7 +1269,8 @@ async function executeStreamingCommand(input) {
1078
1269
  env: {
1079
1270
  ...process.env,
1080
1271
  ...containerRegistryEnv(),
1081
- ...input.message.env ?? {}
1272
+ ...input.message.env ?? {},
1273
+ ...internalGitProcessEnv(workspace, input.message.env)
1082
1274
  }
1083
1275
  });
1084
1276
  activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
@@ -1142,6 +1334,7 @@ async function executeStreamingCommand(input) {
1142
1334
  clearTimeout(timeout);
1143
1335
  }
1144
1336
  activeProcesses.delete(input.message.runId);
1337
+ cancelledProcessRuns.delete(input.message.runId);
1145
1338
  }
1146
1339
  }
1147
1340
  function sendWorkerMessage(ws, message) {
@@ -1534,14 +1727,16 @@ async function startWorker(options) {
1534
1727
  const active = activeProcesses.get(message.runId);
1535
1728
  if (active) {
1536
1729
  active.process.kill("SIGTERM");
1730
+ } else {
1731
+ cancelledProcessRuns.add(message.runId);
1537
1732
  }
1538
1733
  ws.send(
1539
1734
  JSON.stringify({
1540
1735
  type: "cancel_result",
1541
1736
  requestId: message.requestId,
1542
1737
  runId: message.runId,
1543
- cancelled: !!active,
1544
- message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "No active command with that run id"
1738
+ cancelled: true,
1739
+ message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "Cancellation queued before command start"
1545
1740
  })
1546
1741
  );
1547
1742
  return;
@@ -1580,6 +1775,11 @@ async function startWorker(options) {
1580
1775
  return;
1581
1776
  }
1582
1777
  if (message.type === "exec_start") {
1778
+ sendWorkerMessage(ws, {
1779
+ type: "exec_accepted",
1780
+ requestId: message.requestId,
1781
+ runId: message.runId
1782
+ });
1583
1783
  try {
1584
1784
  const manifest = manifestByProjectId.get(message.projectId);
1585
1785
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -1607,7 +1807,7 @@ async function startWorker(options) {
1607
1807
  }
1608
1808
  return;
1609
1809
  }
1610
- if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
1810
+ if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "apply_patch" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "confirm_large_diff" || message.type === "pull_branch") {
1611
1811
  try {
1612
1812
  const manifest = manifestByProjectId.get(message.projectId);
1613
1813
  const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
@@ -1632,6 +1832,17 @@ async function startWorker(options) {
1632
1832
  })
1633
1833
  );
1634
1834
  }
1835
+ return;
1836
+ }
1837
+ const unhandledMessage = message;
1838
+ if (typeof unhandledMessage.requestId === "string") {
1839
+ ws.send(
1840
+ JSON.stringify({
1841
+ type: "operation_result",
1842
+ requestId: unhandledMessage.requestId,
1843
+ error: `Unsupported worker message type: ${String(unhandledMessage.type)}`
1844
+ })
1845
+ );
1635
1846
  }
1636
1847
  })().catch((error) => {
1637
1848
  process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
@@ -1665,8 +1876,13 @@ async function main() {
1665
1876
  }
1666
1877
  await startWorker(parsed.options);
1667
1878
  }
1668
- main().catch((error) => {
1669
- process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
1879
+ if (import.meta.main) {
1880
+ main().catch((error) => {
1881
+ process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
1670
1882
  `);
1671
- process.exit(1);
1672
- });
1883
+ process.exit(1);
1884
+ });
1885
+ }
1886
+ export {
1887
+ ensureVisibleGitCheckout
1888
+ };
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module"
5
5
  }
@@ -1,2 +1,14 @@
1
1
  #!/usr/bin/env bun
2
+ type GitAuth = {
3
+ extraHeaderUrl: string;
4
+ header: string;
5
+ };
6
+ export declare function ensureVisibleGitCheckout(input: {
7
+ projectRoot: string;
8
+ branchName: string;
9
+ githubRemoteUrl: string;
10
+ githubAuth?: GitAuth;
11
+ githubAuthHeader?: string | null;
12
+ defaultBranch: string;
13
+ }): string;
2
14
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",