@ricsam/r5d-worker 0.0.5 → 0.0.7
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 +1 -1
- package/dist/cjs/git-identity.cjs +39 -0
- package/dist/cjs/main.cjs +168 -17
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/git-identity.mjs +15 -0
- package/dist/mjs/main.mjs +168 -17
- package/dist/mjs/package.json +1 -1
- package/dist/types/git-identity.d.ts +9 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ r5d-worker --version
|
|
|
10
10
|
The worker uses the existing `r5dctl` config file and accepts `R5D_WORKER_TOKEN` or `R5D_API_KEY`. Project checkouts are managed under:
|
|
11
11
|
|
|
12
12
|
```text
|
|
13
|
-
~/.r5d/projects/<repo
|
|
13
|
+
~/.r5d/projects/<namespace-repo>/<branch-name>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Visible branch checkouts use GitHub `origin` remotes. r5d internal sync git metadata lives separately under `~/.r5d/sync`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var git_identity_exports = {};
|
|
20
|
+
__export(git_identity_exports, {
|
|
21
|
+
configureVisibleGitIdentity: () => configureVisibleGitIdentity
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(git_identity_exports);
|
|
24
|
+
function configureVisibleGitIdentity(branchPath, identity, runGit) {
|
|
25
|
+
if (!identity) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const name = identity.name.trim();
|
|
29
|
+
const email = identity.email.trim();
|
|
30
|
+
if (!name || !email) {
|
|
31
|
+
throw new Error("Git identity must include name and email");
|
|
32
|
+
}
|
|
33
|
+
runGit(["config", "user.name", name], { cwd: branchPath });
|
|
34
|
+
runGit(["config", "user.email", email], { cwd: branchPath });
|
|
35
|
+
}
|
|
36
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
37
|
+
0 && (module.exports = {
|
|
38
|
+
configureVisibleGitIdentity
|
|
39
|
+
});
|
package/dist/cjs/main.cjs
CHANGED
|
@@ -28,6 +28,7 @@ var import_node_path = __toESM(require("node:path"), 1);
|
|
|
28
28
|
var import_node_crypto = require("node:crypto");
|
|
29
29
|
var import_node_os2 = require("node:os");
|
|
30
30
|
var import_node_child_process = require("node:child_process");
|
|
31
|
+
var import_git_identity = require("./git-identity.cjs");
|
|
31
32
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
32
33
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
33
34
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
@@ -35,10 +36,13 @@ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
|
35
36
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
36
37
|
const MAX_LINE_LENGTH = 2e3;
|
|
37
38
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
39
|
+
const R5D_REMOTE_NAME = "r5d";
|
|
40
|
+
const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
|
|
38
41
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
39
42
|
const activePtys = /* @__PURE__ */ new Map();
|
|
40
43
|
const branchLocks = /* @__PURE__ */ new Map();
|
|
41
44
|
let containerRegistryCredential = null;
|
|
45
|
+
let visibleGitIdentity = null;
|
|
42
46
|
function defaultConfigPath() {
|
|
43
47
|
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
44
48
|
}
|
|
@@ -93,7 +97,7 @@ function getWorkerVersion() {
|
|
|
93
97
|
return packageVersion;
|
|
94
98
|
}
|
|
95
99
|
const sourceVersion = readVersionFile(import_node_path.default.join(current, "VERSION.txt"));
|
|
96
|
-
if (sourceVersion && import_node_path.default.basename(current) === "
|
|
100
|
+
if (sourceVersion && import_node_path.default.basename(current) === "packages") {
|
|
97
101
|
return sourceVersion;
|
|
98
102
|
}
|
|
99
103
|
const parent = import_node_path.default.dirname(current);
|
|
@@ -219,8 +223,8 @@ async function readResponseText(response) {
|
|
|
219
223
|
return "";
|
|
220
224
|
}
|
|
221
225
|
}
|
|
222
|
-
async function
|
|
223
|
-
const statusUrl = new URL("/api/
|
|
226
|
+
async function verifyR5dctlAuth(baseUrl, token) {
|
|
227
|
+
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
224
228
|
let response;
|
|
225
229
|
try {
|
|
226
230
|
response = await fetch(statusUrl, {
|
|
@@ -249,12 +253,12 @@ async function verifyBinctlAuth(baseUrl, token) {
|
|
|
249
253
|
);
|
|
250
254
|
}
|
|
251
255
|
function resolveCredentials(options, config) {
|
|
252
|
-
const token = options.token ?? process.env.R5D_WORKER_TOKEN ?? process.env.R5D_TOKEN ?? process.env.
|
|
256
|
+
const token = options.token ?? process.env.R5D_WORKER_TOKEN ?? process.env.R5D_TOKEN ?? process.env.R5DCTL_TOKEN ?? config.token ?? options.apiKey ?? process.env.R5D_API_KEY ?? process.env.R5DCTL_API_KEY ?? config.apiKey;
|
|
253
257
|
if (!token) {
|
|
254
258
|
throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
|
|
255
259
|
}
|
|
256
260
|
return {
|
|
257
|
-
baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.
|
|
261
|
+
baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
|
|
258
262
|
token
|
|
259
263
|
};
|
|
260
264
|
}
|
|
@@ -601,15 +605,19 @@ function ensureVisibleGitCheckout(input) {
|
|
|
601
605
|
}
|
|
602
606
|
ensureOriginRemote(branchPath, input.githubRemoteUrl);
|
|
603
607
|
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
608
|
+
(0, import_git_identity.configureVisibleGitIdentity)(branchPath, visibleGitIdentity, runGit);
|
|
604
609
|
tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
|
|
605
610
|
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
606
611
|
return branchPath;
|
|
607
612
|
}
|
|
608
613
|
function configureInternalRepo(context, remoteUrl) {
|
|
609
|
-
if (tryInternalGit(context, ["remote", "get-url",
|
|
610
|
-
runInternalGit(context, ["remote", "set-url",
|
|
614
|
+
if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
|
|
615
|
+
runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
|
|
611
616
|
} else {
|
|
612
|
-
runInternalGit(context, ["remote", "add",
|
|
617
|
+
runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
|
|
618
|
+
}
|
|
619
|
+
if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
|
|
620
|
+
runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
|
|
613
621
|
}
|
|
614
622
|
runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
|
|
615
623
|
runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
|
|
@@ -625,9 +633,9 @@ function ensureInternalSyncGit(input) {
|
|
|
625
633
|
const context = { gitDir, workTree: input.branchPath, auth };
|
|
626
634
|
const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
627
635
|
configureInternalRepo(context, remoteUrl);
|
|
628
|
-
runInternalGit(context, ["fetch",
|
|
629
|
-
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes
|
|
630
|
-
const target = hasRemoteBranch ? `refs/remotes
|
|
636
|
+
runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
637
|
+
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
|
|
638
|
+
const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
|
|
631
639
|
const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
632
640
|
if (!hasHead || !hasInternalWorktreeChanges(context)) {
|
|
633
641
|
runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
|
|
@@ -677,6 +685,31 @@ async function collectStream(stream) {
|
|
|
677
685
|
}
|
|
678
686
|
return await new Response(stream).text();
|
|
679
687
|
}
|
|
688
|
+
async function streamCommandOutput(stream, onData) {
|
|
689
|
+
if (!stream) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const reader = stream.getReader();
|
|
693
|
+
const decoder = new TextDecoder();
|
|
694
|
+
try {
|
|
695
|
+
while (true) {
|
|
696
|
+
const chunk = await reader.read();
|
|
697
|
+
if (chunk.done) {
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
const text = decoder.decode(chunk.value, { stream: true });
|
|
701
|
+
if (text.length > 0) {
|
|
702
|
+
onData(text);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const trailing = decoder.decode();
|
|
706
|
+
if (trailing.length > 0) {
|
|
707
|
+
onData(trailing);
|
|
708
|
+
}
|
|
709
|
+
} finally {
|
|
710
|
+
reader.releaseLock();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
680
713
|
function ensureOperationBranch(input) {
|
|
681
714
|
return ensureBranchWorkspace(input);
|
|
682
715
|
}
|
|
@@ -896,7 +929,7 @@ function syncBranch(input) {
|
|
|
896
929
|
});
|
|
897
930
|
runInternalGit(workspace.internalGit, ["commit", "-m", message]);
|
|
898
931
|
const commitHash = getInternalCommitHash(workspace.internalGit);
|
|
899
|
-
runInternalGit(workspace.internalGit, ["push",
|
|
932
|
+
runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
|
|
900
933
|
return {
|
|
901
934
|
type: "sync",
|
|
902
935
|
changed: true,
|
|
@@ -927,8 +960,8 @@ function confirmLargeDiff(input) {
|
|
|
927
960
|
}
|
|
928
961
|
function pullBranch(input) {
|
|
929
962
|
const workspace = ensureOperationBranch(input);
|
|
930
|
-
runInternalGit(workspace.internalGit, ["fetch",
|
|
931
|
-
const target = input.commitHash ??
|
|
963
|
+
runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
964
|
+
const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
|
|
932
965
|
runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
|
|
933
966
|
runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
934
967
|
return {
|
|
@@ -1045,6 +1078,95 @@ async function executeCommand(input) {
|
|
|
1045
1078
|
activeProcesses.delete(input.message.runId);
|
|
1046
1079
|
}
|
|
1047
1080
|
}
|
|
1081
|
+
async function executeStreamingCommand(input) {
|
|
1082
|
+
const startedAt = Date.now();
|
|
1083
|
+
let timeout;
|
|
1084
|
+
let started = false;
|
|
1085
|
+
let timedOut = false;
|
|
1086
|
+
try {
|
|
1087
|
+
const branchPath = ensureBranchWorkspace({
|
|
1088
|
+
projectId: input.projectId,
|
|
1089
|
+
baseUrl: input.baseUrl,
|
|
1090
|
+
token: input.token,
|
|
1091
|
+
projectRoot: input.projectRoot,
|
|
1092
|
+
syncRoot: input.syncRoot,
|
|
1093
|
+
branchName: input.message.branchName,
|
|
1094
|
+
manifest: input.manifest
|
|
1095
|
+
});
|
|
1096
|
+
const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
|
|
1097
|
+
const subprocess = Bun.spawn(input.message.argv, {
|
|
1098
|
+
cwd,
|
|
1099
|
+
stdout: "pipe",
|
|
1100
|
+
stderr: "pipe",
|
|
1101
|
+
env: {
|
|
1102
|
+
...process.env,
|
|
1103
|
+
...containerRegistryEnv(),
|
|
1104
|
+
...input.message.env ?? {}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
1108
|
+
started = true;
|
|
1109
|
+
sendWorkerMessage(input.ws, {
|
|
1110
|
+
type: "exec_started",
|
|
1111
|
+
requestId: input.message.requestId,
|
|
1112
|
+
runId: input.message.runId
|
|
1113
|
+
});
|
|
1114
|
+
if (input.message.timeoutMs) {
|
|
1115
|
+
timeout = setTimeout(() => {
|
|
1116
|
+
timedOut = true;
|
|
1117
|
+
subprocess.kill("SIGTERM");
|
|
1118
|
+
}, input.message.timeoutMs);
|
|
1119
|
+
}
|
|
1120
|
+
const [exitCode] = await Promise.all([
|
|
1121
|
+
subprocess.exited,
|
|
1122
|
+
streamCommandOutput(subprocess.stdout, (data) => {
|
|
1123
|
+
sendWorkerMessage(input.ws, {
|
|
1124
|
+
type: "exec_output",
|
|
1125
|
+
runId: input.message.runId,
|
|
1126
|
+
stream: "stdout",
|
|
1127
|
+
data
|
|
1128
|
+
});
|
|
1129
|
+
}),
|
|
1130
|
+
streamCommandOutput(subprocess.stderr, (data) => {
|
|
1131
|
+
sendWorkerMessage(input.ws, {
|
|
1132
|
+
type: "exec_output",
|
|
1133
|
+
runId: input.message.runId,
|
|
1134
|
+
stream: "stderr",
|
|
1135
|
+
data
|
|
1136
|
+
});
|
|
1137
|
+
})
|
|
1138
|
+
]);
|
|
1139
|
+
sendWorkerMessage(input.ws, {
|
|
1140
|
+
type: "exec_exit",
|
|
1141
|
+
runId: input.message.runId,
|
|
1142
|
+
exitCode,
|
|
1143
|
+
durationMs: Date.now() - startedAt,
|
|
1144
|
+
...timedOut ? { timedOut: true } : {}
|
|
1145
|
+
});
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1148
|
+
if (started) {
|
|
1149
|
+
sendWorkerMessage(input.ws, {
|
|
1150
|
+
type: "exec_error",
|
|
1151
|
+
runId: input.message.runId,
|
|
1152
|
+
error: message,
|
|
1153
|
+
durationMs: Date.now() - startedAt
|
|
1154
|
+
});
|
|
1155
|
+
} else {
|
|
1156
|
+
sendWorkerMessage(input.ws, {
|
|
1157
|
+
type: "exec_start_error",
|
|
1158
|
+
requestId: input.message.requestId,
|
|
1159
|
+
runId: input.message.runId,
|
|
1160
|
+
error: message
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
} finally {
|
|
1164
|
+
if (timeout) {
|
|
1165
|
+
clearTimeout(timeout);
|
|
1166
|
+
}
|
|
1167
|
+
activeProcesses.delete(input.message.runId);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1048
1170
|
function sendWorkerMessage(ws, message) {
|
|
1049
1171
|
ws.send(JSON.stringify(message));
|
|
1050
1172
|
}
|
|
@@ -1254,8 +1376,8 @@ function openPty(input) {
|
|
|
1254
1376
|
...process.env,
|
|
1255
1377
|
...containerRegistryEnv(),
|
|
1256
1378
|
...input.message.env ?? {},
|
|
1257
|
-
|
|
1258
|
-
|
|
1379
|
+
R5D_PROJECT_ID: input.projectId,
|
|
1380
|
+
R5D_BRANCH_NAME: input.message.branchName
|
|
1259
1381
|
}
|
|
1260
1382
|
},
|
|
1261
1383
|
onOpened: () => {
|
|
@@ -1362,7 +1484,7 @@ async function startWorker(options) {
|
|
|
1362
1484
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1363
1485
|
`);
|
|
1364
1486
|
runGit(["--version"]);
|
|
1365
|
-
await
|
|
1487
|
+
await verifyR5dctlAuth(baseUrl, token);
|
|
1366
1488
|
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
1367
1489
|
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
1368
1490
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
@@ -1394,6 +1516,7 @@ async function startWorker(options) {
|
|
|
1394
1516
|
}
|
|
1395
1517
|
if (message.type === "project_manifest") {
|
|
1396
1518
|
configureContainerRegistryAuth(message.githubContainerRegistry);
|
|
1519
|
+
visibleGitIdentity = message.gitIdentity;
|
|
1397
1520
|
manifestByProjectId.clear();
|
|
1398
1521
|
for (const project of message.projects) {
|
|
1399
1522
|
manifestByProjectId.set(project.projectId, project);
|
|
@@ -1479,6 +1602,34 @@ async function startWorker(options) {
|
|
|
1479
1602
|
ws.send(JSON.stringify(result));
|
|
1480
1603
|
return;
|
|
1481
1604
|
}
|
|
1605
|
+
if (message.type === "exec_start") {
|
|
1606
|
+
try {
|
|
1607
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1608
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1609
|
+
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
1610
|
+
`);
|
|
1611
|
+
void withBranchLock(
|
|
1612
|
+
message.projectId,
|
|
1613
|
+
message.branchName,
|
|
1614
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
1615
|
+
).catch((error) => {
|
|
1616
|
+
sendWorkerMessage(ws, {
|
|
1617
|
+
type: "exec_start_error",
|
|
1618
|
+
requestId: message.requestId,
|
|
1619
|
+
runId: message.runId,
|
|
1620
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
sendWorkerMessage(ws, {
|
|
1625
|
+
type: "exec_start_error",
|
|
1626
|
+
requestId: message.requestId,
|
|
1627
|
+
runId: message.runId,
|
|
1628
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1482
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") {
|
|
1483
1634
|
try {
|
|
1484
1635
|
const manifest = manifestByProjectId.get(message.projectId);
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function configureVisibleGitIdentity(branchPath, identity, runGit) {
|
|
2
|
+
if (!identity) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
const name = identity.name.trim();
|
|
6
|
+
const email = identity.email.trim();
|
|
7
|
+
if (!name || !email) {
|
|
8
|
+
throw new Error("Git identity must include name and email");
|
|
9
|
+
}
|
|
10
|
+
runGit(["config", "user.name", name], { cwd: branchPath });
|
|
11
|
+
runGit(["config", "user.email", email], { cwd: branchPath });
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
configureVisibleGitIdentity
|
|
15
|
+
};
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
7
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
8
|
+
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
8
9
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
9
10
|
const WORKER_PACKAGE_NAME = "@ricsam/r5d-worker";
|
|
10
11
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
@@ -12,10 +13,13 @@ const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
|
12
13
|
const DEFAULT_READ_LIMIT = 2e3;
|
|
13
14
|
const MAX_LINE_LENGTH = 2e3;
|
|
14
15
|
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
16
|
+
const R5D_REMOTE_NAME = "r5d";
|
|
17
|
+
const LEGACY_BUILD_IT_NOW_REMOTE_NAME = "build-it-now";
|
|
15
18
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
16
19
|
const activePtys = /* @__PURE__ */ new Map();
|
|
17
20
|
const branchLocks = /* @__PURE__ */ new Map();
|
|
18
21
|
let containerRegistryCredential = null;
|
|
22
|
+
let visibleGitIdentity = null;
|
|
19
23
|
function defaultConfigPath() {
|
|
20
24
|
return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
21
25
|
}
|
|
@@ -70,7 +74,7 @@ function getWorkerVersion() {
|
|
|
70
74
|
return packageVersion;
|
|
71
75
|
}
|
|
72
76
|
const sourceVersion = readVersionFile(path.join(current, "VERSION.txt"));
|
|
73
|
-
if (sourceVersion && path.basename(current) === "
|
|
77
|
+
if (sourceVersion && path.basename(current) === "packages") {
|
|
74
78
|
return sourceVersion;
|
|
75
79
|
}
|
|
76
80
|
const parent = path.dirname(current);
|
|
@@ -196,8 +200,8 @@ async function readResponseText(response) {
|
|
|
196
200
|
return "";
|
|
197
201
|
}
|
|
198
202
|
}
|
|
199
|
-
async function
|
|
200
|
-
const statusUrl = new URL("/api/
|
|
203
|
+
async function verifyR5dctlAuth(baseUrl, token) {
|
|
204
|
+
const statusUrl = new URL("/api/r5dctl/auth/status", baseUrl);
|
|
201
205
|
let response;
|
|
202
206
|
try {
|
|
203
207
|
response = await fetch(statusUrl, {
|
|
@@ -226,12 +230,12 @@ async function verifyBinctlAuth(baseUrl, token) {
|
|
|
226
230
|
);
|
|
227
231
|
}
|
|
228
232
|
function resolveCredentials(options, config) {
|
|
229
|
-
const token = options.token ?? process.env.R5D_WORKER_TOKEN ?? process.env.R5D_TOKEN ?? process.env.
|
|
233
|
+
const token = options.token ?? process.env.R5D_WORKER_TOKEN ?? process.env.R5D_TOKEN ?? process.env.R5DCTL_TOKEN ?? config.token ?? options.apiKey ?? process.env.R5D_API_KEY ?? process.env.R5DCTL_API_KEY ?? config.apiKey;
|
|
230
234
|
if (!token) {
|
|
231
235
|
throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
|
|
232
236
|
}
|
|
233
237
|
return {
|
|
234
|
-
baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.
|
|
238
|
+
baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.R5DCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
|
|
235
239
|
token
|
|
236
240
|
};
|
|
237
241
|
}
|
|
@@ -578,15 +582,19 @@ function ensureVisibleGitCheckout(input) {
|
|
|
578
582
|
}
|
|
579
583
|
ensureOriginRemote(branchPath, input.githubRemoteUrl);
|
|
580
584
|
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
585
|
+
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
581
586
|
tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
|
|
582
587
|
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
583
588
|
return branchPath;
|
|
584
589
|
}
|
|
585
590
|
function configureInternalRepo(context, remoteUrl) {
|
|
586
|
-
if (tryInternalGit(context, ["remote", "get-url",
|
|
587
|
-
runInternalGit(context, ["remote", "set-url",
|
|
591
|
+
if (tryInternalGit(context, ["remote", "get-url", R5D_REMOTE_NAME])) {
|
|
592
|
+
runInternalGit(context, ["remote", "set-url", R5D_REMOTE_NAME, remoteUrl]);
|
|
588
593
|
} else {
|
|
589
|
-
runInternalGit(context, ["remote", "add",
|
|
594
|
+
runInternalGit(context, ["remote", "add", R5D_REMOTE_NAME, remoteUrl]);
|
|
595
|
+
}
|
|
596
|
+
if (tryInternalGit(context, ["remote", "get-url", LEGACY_BUILD_IT_NOW_REMOTE_NAME])) {
|
|
597
|
+
runInternalGit(context, ["remote", "remove", LEGACY_BUILD_IT_NOW_REMOTE_NAME]);
|
|
590
598
|
}
|
|
591
599
|
runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
|
|
592
600
|
runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
|
|
@@ -602,9 +610,9 @@ function ensureInternalSyncGit(input) {
|
|
|
602
610
|
const context = { gitDir, workTree: input.branchPath, auth };
|
|
603
611
|
const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
604
612
|
configureInternalRepo(context, remoteUrl);
|
|
605
|
-
runInternalGit(context, ["fetch",
|
|
606
|
-
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes
|
|
607
|
-
const target = hasRemoteBranch ? `refs/remotes
|
|
613
|
+
runInternalGit(context, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
614
|
+
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}`]);
|
|
615
|
+
const target = hasRemoteBranch ? `refs/remotes/${R5D_REMOTE_NAME}/${input.branchName}` : `refs/remotes/${R5D_REMOTE_NAME}/main`;
|
|
608
616
|
const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
609
617
|
if (!hasHead || !hasInternalWorktreeChanges(context)) {
|
|
610
618
|
runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
|
|
@@ -654,6 +662,31 @@ async function collectStream(stream) {
|
|
|
654
662
|
}
|
|
655
663
|
return await new Response(stream).text();
|
|
656
664
|
}
|
|
665
|
+
async function streamCommandOutput(stream, onData) {
|
|
666
|
+
if (!stream) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const reader = stream.getReader();
|
|
670
|
+
const decoder = new TextDecoder();
|
|
671
|
+
try {
|
|
672
|
+
while (true) {
|
|
673
|
+
const chunk = await reader.read();
|
|
674
|
+
if (chunk.done) {
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
const text = decoder.decode(chunk.value, { stream: true });
|
|
678
|
+
if (text.length > 0) {
|
|
679
|
+
onData(text);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const trailing = decoder.decode();
|
|
683
|
+
if (trailing.length > 0) {
|
|
684
|
+
onData(trailing);
|
|
685
|
+
}
|
|
686
|
+
} finally {
|
|
687
|
+
reader.releaseLock();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
657
690
|
function ensureOperationBranch(input) {
|
|
658
691
|
return ensureBranchWorkspace(input);
|
|
659
692
|
}
|
|
@@ -873,7 +906,7 @@ function syncBranch(input) {
|
|
|
873
906
|
});
|
|
874
907
|
runInternalGit(workspace.internalGit, ["commit", "-m", message]);
|
|
875
908
|
const commitHash = getInternalCommitHash(workspace.internalGit);
|
|
876
|
-
runInternalGit(workspace.internalGit, ["push",
|
|
909
|
+
runInternalGit(workspace.internalGit, ["push", R5D_REMOTE_NAME, `HEAD:refs/heads/${input.branchName}`]);
|
|
877
910
|
return {
|
|
878
911
|
type: "sync",
|
|
879
912
|
changed: true,
|
|
@@ -904,8 +937,8 @@ function confirmLargeDiff(input) {
|
|
|
904
937
|
}
|
|
905
938
|
function pullBranch(input) {
|
|
906
939
|
const workspace = ensureOperationBranch(input);
|
|
907
|
-
runInternalGit(workspace.internalGit, ["fetch",
|
|
908
|
-
const target = input.commitHash ??
|
|
940
|
+
runInternalGit(workspace.internalGit, ["fetch", R5D_REMOTE_NAME, "--prune", `+refs/heads/*:refs/remotes/${R5D_REMOTE_NAME}/*`]);
|
|
941
|
+
const target = input.commitHash ?? `${R5D_REMOTE_NAME}/${input.branchName}`;
|
|
909
942
|
runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
|
|
910
943
|
runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
911
944
|
return {
|
|
@@ -1022,6 +1055,95 @@ async function executeCommand(input) {
|
|
|
1022
1055
|
activeProcesses.delete(input.message.runId);
|
|
1023
1056
|
}
|
|
1024
1057
|
}
|
|
1058
|
+
async function executeStreamingCommand(input) {
|
|
1059
|
+
const startedAt = Date.now();
|
|
1060
|
+
let timeout;
|
|
1061
|
+
let started = false;
|
|
1062
|
+
let timedOut = false;
|
|
1063
|
+
try {
|
|
1064
|
+
const branchPath = ensureBranchWorkspace({
|
|
1065
|
+
projectId: input.projectId,
|
|
1066
|
+
baseUrl: input.baseUrl,
|
|
1067
|
+
token: input.token,
|
|
1068
|
+
projectRoot: input.projectRoot,
|
|
1069
|
+
syncRoot: input.syncRoot,
|
|
1070
|
+
branchName: input.message.branchName,
|
|
1071
|
+
manifest: input.manifest
|
|
1072
|
+
});
|
|
1073
|
+
const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
|
|
1074
|
+
const subprocess = Bun.spawn(input.message.argv, {
|
|
1075
|
+
cwd,
|
|
1076
|
+
stdout: "pipe",
|
|
1077
|
+
stderr: "pipe",
|
|
1078
|
+
env: {
|
|
1079
|
+
...process.env,
|
|
1080
|
+
...containerRegistryEnv(),
|
|
1081
|
+
...input.message.env ?? {}
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
1085
|
+
started = true;
|
|
1086
|
+
sendWorkerMessage(input.ws, {
|
|
1087
|
+
type: "exec_started",
|
|
1088
|
+
requestId: input.message.requestId,
|
|
1089
|
+
runId: input.message.runId
|
|
1090
|
+
});
|
|
1091
|
+
if (input.message.timeoutMs) {
|
|
1092
|
+
timeout = setTimeout(() => {
|
|
1093
|
+
timedOut = true;
|
|
1094
|
+
subprocess.kill("SIGTERM");
|
|
1095
|
+
}, input.message.timeoutMs);
|
|
1096
|
+
}
|
|
1097
|
+
const [exitCode] = await Promise.all([
|
|
1098
|
+
subprocess.exited,
|
|
1099
|
+
streamCommandOutput(subprocess.stdout, (data) => {
|
|
1100
|
+
sendWorkerMessage(input.ws, {
|
|
1101
|
+
type: "exec_output",
|
|
1102
|
+
runId: input.message.runId,
|
|
1103
|
+
stream: "stdout",
|
|
1104
|
+
data
|
|
1105
|
+
});
|
|
1106
|
+
}),
|
|
1107
|
+
streamCommandOutput(subprocess.stderr, (data) => {
|
|
1108
|
+
sendWorkerMessage(input.ws, {
|
|
1109
|
+
type: "exec_output",
|
|
1110
|
+
runId: input.message.runId,
|
|
1111
|
+
stream: "stderr",
|
|
1112
|
+
data
|
|
1113
|
+
});
|
|
1114
|
+
})
|
|
1115
|
+
]);
|
|
1116
|
+
sendWorkerMessage(input.ws, {
|
|
1117
|
+
type: "exec_exit",
|
|
1118
|
+
runId: input.message.runId,
|
|
1119
|
+
exitCode,
|
|
1120
|
+
durationMs: Date.now() - startedAt,
|
|
1121
|
+
...timedOut ? { timedOut: true } : {}
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1125
|
+
if (started) {
|
|
1126
|
+
sendWorkerMessage(input.ws, {
|
|
1127
|
+
type: "exec_error",
|
|
1128
|
+
runId: input.message.runId,
|
|
1129
|
+
error: message,
|
|
1130
|
+
durationMs: Date.now() - startedAt
|
|
1131
|
+
});
|
|
1132
|
+
} else {
|
|
1133
|
+
sendWorkerMessage(input.ws, {
|
|
1134
|
+
type: "exec_start_error",
|
|
1135
|
+
requestId: input.message.requestId,
|
|
1136
|
+
runId: input.message.runId,
|
|
1137
|
+
error: message
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
} finally {
|
|
1141
|
+
if (timeout) {
|
|
1142
|
+
clearTimeout(timeout);
|
|
1143
|
+
}
|
|
1144
|
+
activeProcesses.delete(input.message.runId);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1025
1147
|
function sendWorkerMessage(ws, message) {
|
|
1026
1148
|
ws.send(JSON.stringify(message));
|
|
1027
1149
|
}
|
|
@@ -1231,8 +1353,8 @@ function openPty(input) {
|
|
|
1231
1353
|
...process.env,
|
|
1232
1354
|
...containerRegistryEnv(),
|
|
1233
1355
|
...input.message.env ?? {},
|
|
1234
|
-
|
|
1235
|
-
|
|
1356
|
+
R5D_PROJECT_ID: input.projectId,
|
|
1357
|
+
R5D_BRANCH_NAME: input.message.branchName
|
|
1236
1358
|
}
|
|
1237
1359
|
},
|
|
1238
1360
|
onOpened: () => {
|
|
@@ -1339,7 +1461,7 @@ async function startWorker(options) {
|
|
|
1339
1461
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
1340
1462
|
`);
|
|
1341
1463
|
runGit(["--version"]);
|
|
1342
|
-
await
|
|
1464
|
+
await verifyR5dctlAuth(baseUrl, token);
|
|
1343
1465
|
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1344
1466
|
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1345
1467
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
@@ -1371,6 +1493,7 @@ async function startWorker(options) {
|
|
|
1371
1493
|
}
|
|
1372
1494
|
if (message.type === "project_manifest") {
|
|
1373
1495
|
configureContainerRegistryAuth(message.githubContainerRegistry);
|
|
1496
|
+
visibleGitIdentity = message.gitIdentity;
|
|
1374
1497
|
manifestByProjectId.clear();
|
|
1375
1498
|
for (const project of message.projects) {
|
|
1376
1499
|
manifestByProjectId.set(project.projectId, project);
|
|
@@ -1456,6 +1579,34 @@ async function startWorker(options) {
|
|
|
1456
1579
|
ws.send(JSON.stringify(result));
|
|
1457
1580
|
return;
|
|
1458
1581
|
}
|
|
1582
|
+
if (message.type === "exec_start") {
|
|
1583
|
+
try {
|
|
1584
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1585
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1586
|
+
process.stdout.write(`[r5d-worker] exec_start ${message.runId}: ${message.argv.join(" ")}
|
|
1587
|
+
`);
|
|
1588
|
+
void withBranchLock(
|
|
1589
|
+
message.projectId,
|
|
1590
|
+
message.branchName,
|
|
1591
|
+
() => executeStreamingCommand({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
1592
|
+
).catch((error) => {
|
|
1593
|
+
sendWorkerMessage(ws, {
|
|
1594
|
+
type: "exec_start_error",
|
|
1595
|
+
requestId: message.requestId,
|
|
1596
|
+
runId: message.runId,
|
|
1597
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1598
|
+
});
|
|
1599
|
+
});
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
sendWorkerMessage(ws, {
|
|
1602
|
+
type: "exec_start_error",
|
|
1603
|
+
requestId: message.requestId,
|
|
1604
|
+
runId: message.runId,
|
|
1605
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1459
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") {
|
|
1460
1611
|
try {
|
|
1461
1612
|
const manifest = manifestByProjectId.get(message.projectId);
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type WorkerGitIdentity = {
|
|
2
|
+
name: string;
|
|
3
|
+
email: string;
|
|
4
|
+
};
|
|
5
|
+
type RunGit = (args: string[], options?: {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}) => string | void;
|
|
8
|
+
export declare function configureVisibleGitIdentity(branchPath: string, identity: WorkerGitIdentity | null | undefined, runGit: RunGit): void;
|
|
9
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/main.cjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "git+https://github.com/ricsam/r5d-dev.git",
|
|
18
|
-
"directory": "
|
|
18
|
+
"directory": "packages/r5d-worker"
|
|
19
19
|
},
|
|
20
20
|
"bin": {
|
|
21
21
|
"r5d-worker": "dist/cjs/main.cjs"
|