@ricsam/r5d-worker 0.0.1 → 0.0.2
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 +17 -37
- package/dist/cjs/main.cjs +990 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/main.mjs +967 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/main.d.ts +2 -0
- package/package.json +35 -7
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
26
|
+
var import_node_os = __toESM(require("node:os"), 1);
|
|
27
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
28
|
+
var import_node_crypto = require("node:crypto");
|
|
29
|
+
var import_node_os2 = require("node:os");
|
|
30
|
+
var nodePty = __toESM(require("node-pty"), 1);
|
|
31
|
+
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
32
|
+
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
33
|
+
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
34
|
+
const DEFAULT_READ_LIMIT = 2e3;
|
|
35
|
+
const MAX_LINE_LENGTH = 2e3;
|
|
36
|
+
const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
37
|
+
const activeProcesses = /* @__PURE__ */ new Map();
|
|
38
|
+
const activePtys = /* @__PURE__ */ new Map();
|
|
39
|
+
const branchLocks = /* @__PURE__ */ new Map();
|
|
40
|
+
function defaultConfigPath() {
|
|
41
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
42
|
+
}
|
|
43
|
+
function defaultProjectRoot(projectId) {
|
|
44
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects", projectId);
|
|
45
|
+
}
|
|
46
|
+
function printHelp() {
|
|
47
|
+
process.stdout.write(`Usage:
|
|
48
|
+
r5d-worker start <project-id> --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--label <label> Required unique worker label for this project, e.g. macos or ec2-build
|
|
52
|
+
--base-url <url> r5d.dev base URL (default from r5d config, R5D_BASE_URL, or ${DEFAULT_BASE_URL})
|
|
53
|
+
--token <token> r5d worker token
|
|
54
|
+
--api-key <key> r5d API key
|
|
55
|
+
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
56
|
+
--project-root <dir> Override checkout root (default ~/.r5d/projects/<project-id>)
|
|
57
|
+
-h, --help Show this help
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
function requireValue(value, message) {
|
|
61
|
+
if (!value) {
|
|
62
|
+
throw new Error(message);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function parseArgs(argv) {
|
|
67
|
+
const options = {
|
|
68
|
+
configPath: defaultConfigPath(),
|
|
69
|
+
help: false
|
|
70
|
+
};
|
|
71
|
+
const rest = [];
|
|
72
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
73
|
+
const arg = argv[index];
|
|
74
|
+
if (!arg) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "-h" || arg === "--help") {
|
|
78
|
+
options.help = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const readOption = (name) => {
|
|
82
|
+
if (arg === name) {
|
|
83
|
+
index += 1;
|
|
84
|
+
return requireValue(argv[index], `Missing value for ${name}`);
|
|
85
|
+
}
|
|
86
|
+
const prefix = `${name}=`;
|
|
87
|
+
return arg.startsWith(prefix) ? arg.slice(prefix.length) : void 0;
|
|
88
|
+
};
|
|
89
|
+
const baseUrl = readOption("--base-url");
|
|
90
|
+
if (baseUrl !== void 0) {
|
|
91
|
+
options.baseUrl = baseUrl;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const token = readOption("--token");
|
|
95
|
+
if (token !== void 0) {
|
|
96
|
+
options.token = token;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const apiKey = readOption("--api-key");
|
|
100
|
+
if (apiKey !== void 0) {
|
|
101
|
+
options.apiKey = apiKey;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const configPath = readOption("--config");
|
|
105
|
+
if (configPath !== void 0) {
|
|
106
|
+
options.configPath = configPath;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const label = readOption("--label");
|
|
110
|
+
if (label !== void 0) {
|
|
111
|
+
options.label = label;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const projectRoot = readOption("--project-root");
|
|
115
|
+
if (projectRoot !== void 0) {
|
|
116
|
+
options.projectRoot = projectRoot;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
rest.push(arg);
|
|
120
|
+
}
|
|
121
|
+
if (options.help || rest.length === 0) {
|
|
122
|
+
return { command: "help", options };
|
|
123
|
+
}
|
|
124
|
+
const command = rest[0];
|
|
125
|
+
if (command !== "start") {
|
|
126
|
+
throw new Error(`Unknown command: ${command}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
command: "start",
|
|
130
|
+
projectId: requireValue(rest[1], "Missing project id"),
|
|
131
|
+
options
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function readConfig(configPath) {
|
|
135
|
+
if (!import_node_fs.default.existsSync(configPath)) {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
const raw = import_node_fs.default.readFileSync(configPath, "utf8").trim();
|
|
139
|
+
if (!raw) {
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
return {
|
|
144
|
+
baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
|
|
145
|
+
token: typeof parsed.token === "string" ? parsed.token : void 0,
|
|
146
|
+
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function normalizeBaseUrl(value) {
|
|
150
|
+
return value.replace(/\/+$/, "");
|
|
151
|
+
}
|
|
152
|
+
async function readResponseText(response) {
|
|
153
|
+
try {
|
|
154
|
+
return await response.text();
|
|
155
|
+
} catch {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function verifyBinctlAuth(baseUrl, token) {
|
|
160
|
+
const statusUrl = new URL("/api/binctl/auth/status", baseUrl);
|
|
161
|
+
let response;
|
|
162
|
+
try {
|
|
163
|
+
response = await fetch(statusUrl, {
|
|
164
|
+
headers: {
|
|
165
|
+
Authorization: `Bearer ${token}`
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new Error(`Could not reach ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
}
|
|
171
|
+
if (response.ok) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const responseText = await readResponseText(response);
|
|
175
|
+
let serverMessage = responseText.trim();
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(responseText);
|
|
178
|
+
if (typeof parsed.error === "string" && parsed.error.trim()) {
|
|
179
|
+
serverMessage = parsed.error.trim();
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
const detail = serverMessage ? `: ${serverMessage}` : "";
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Authentication failed against ${baseUrl} (${response.status})${detail}. Run \`r5dctl auth login --base-url ${baseUrl}\` or pass --token/--api-key for this server.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
function resolveCredentials(options, config) {
|
|
189
|
+
const token = options.token ?? process.env.R5D_WORKER_TOKEN ?? process.env.R5D_TOKEN ?? process.env.BINCTL_TOKEN ?? config.token ?? options.apiKey ?? process.env.R5D_API_KEY ?? process.env.BINCTL_API_KEY ?? config.apiKey;
|
|
190
|
+
if (!token) {
|
|
191
|
+
throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.BINCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
|
|
195
|
+
token
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function validateLabel(label) {
|
|
199
|
+
if (!LABEL_RE.test(label)) {
|
|
200
|
+
throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function validateBranchName(branchName) {
|
|
204
|
+
if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
|
|
205
|
+
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function gitRemoteUrl(baseUrl, projectId) {
|
|
209
|
+
return `${baseUrl}/git/${projectId}.git`;
|
|
210
|
+
}
|
|
211
|
+
function gitExtraHeaderConfigKey(baseUrl) {
|
|
212
|
+
return `http.${normalizeBaseUrl(baseUrl)}/.extraHeader`;
|
|
213
|
+
}
|
|
214
|
+
function gitAuthArgs(auth) {
|
|
215
|
+
if (!auth) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
return ["-c", `${gitExtraHeaderConfigKey(auth.baseUrl)}=Authorization: Bearer ${auth.token}`];
|
|
219
|
+
}
|
|
220
|
+
function runGit(args, options = {}) {
|
|
221
|
+
const command = ["git", ...gitAuthArgs(options.auth), ...args];
|
|
222
|
+
const result = Bun.spawnSync(command, {
|
|
223
|
+
cwd: options.cwd,
|
|
224
|
+
stdout: "pipe",
|
|
225
|
+
stderr: "pipe",
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
if (result.exitCode !== 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`git ${args.join(" ")} failed: ${result.stderr.toString().trim() || result.stdout.toString().trim() || `exit ${result.exitCode}`}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return result.stdout.toString().trim();
|
|
237
|
+
}
|
|
238
|
+
function tryGit(args, options = {}) {
|
|
239
|
+
try {
|
|
240
|
+
runGit(args, options);
|
|
241
|
+
return true;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function getCurrentCommitHash(cwd) {
|
|
247
|
+
return runGit(["rev-parse", "HEAD"], { cwd });
|
|
248
|
+
}
|
|
249
|
+
function getGitBlobHashForContent(content) {
|
|
250
|
+
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
251
|
+
return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
|
|
252
|
+
}
|
|
253
|
+
function hasWorktreeChanges(cwd) {
|
|
254
|
+
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
255
|
+
}
|
|
256
|
+
function assertInsideBranch(branchPath, filePath) {
|
|
257
|
+
const relative = import_node_path.default.relative(branchPath, filePath);
|
|
258
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
259
|
+
throw new Error("File path escapes the project branch");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function resolveProjectFilePath(branchPath, virtualPath) {
|
|
263
|
+
if (typeof virtualPath !== "string" || !virtualPath.startsWith("/") || virtualPath.includes("\0")) {
|
|
264
|
+
throw new Error(`File path must be an absolute project path starting with /. Got: ${JSON.stringify(virtualPath)}`);
|
|
265
|
+
}
|
|
266
|
+
const normalized = import_node_path.default.posix.normalize(virtualPath);
|
|
267
|
+
if (normalized === "/" || normalized.startsWith("/../")) {
|
|
268
|
+
throw new Error(`Invalid project file path: ${virtualPath}`);
|
|
269
|
+
}
|
|
270
|
+
const repoRelativePath = normalized.slice(1);
|
|
271
|
+
if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
|
|
272
|
+
throw new Error("Refusing to access .git through worker file tools");
|
|
273
|
+
}
|
|
274
|
+
const absolutePath = import_node_path.default.resolve(branchPath, ...repoRelativePath.split("/"));
|
|
275
|
+
assertInsideBranch(branchPath, absolutePath);
|
|
276
|
+
return { absolutePath, virtualPath: `/${repoRelativePath}`, repoRelativePath };
|
|
277
|
+
}
|
|
278
|
+
function branchLockKey(projectRoot, branchName) {
|
|
279
|
+
return `${projectRoot}:${branchName}`;
|
|
280
|
+
}
|
|
281
|
+
async function withBranchLock(projectRoot, branchName, fn) {
|
|
282
|
+
const key = branchLockKey(projectRoot, branchName);
|
|
283
|
+
const previous = branchLocks.get(key) ?? Promise.resolve();
|
|
284
|
+
let release = () => {
|
|
285
|
+
};
|
|
286
|
+
const next = new Promise((resolve) => {
|
|
287
|
+
release = resolve;
|
|
288
|
+
});
|
|
289
|
+
const queued = previous.catch(() => {
|
|
290
|
+
}).then(() => next);
|
|
291
|
+
branchLocks.set(key, queued);
|
|
292
|
+
await previous.catch(() => {
|
|
293
|
+
});
|
|
294
|
+
try {
|
|
295
|
+
return await fn();
|
|
296
|
+
} finally {
|
|
297
|
+
release();
|
|
298
|
+
if (branchLocks.get(key) === queued) {
|
|
299
|
+
branchLocks.delete(key);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function configureRepo(pathName, auth) {
|
|
304
|
+
const authHeaderKey = gitExtraHeaderConfigKey(auth.baseUrl);
|
|
305
|
+
tryGit(["config", "--unset-all", "http.extraHeader"], { cwd: pathName });
|
|
306
|
+
tryGit(["config", "--unset-all", authHeaderKey], { cwd: pathName });
|
|
307
|
+
runGit(["config", "--add", authHeaderKey, `Authorization: Bearer ${auth.token}`], { cwd: pathName });
|
|
308
|
+
runGit(["config", "user.name", "r5d.dev Worker"], { cwd: pathName });
|
|
309
|
+
runGit(["config", "user.email", "worker@r5d.dev"], { cwd: pathName });
|
|
310
|
+
}
|
|
311
|
+
function ensureMainClone(input) {
|
|
312
|
+
const mainPath = import_node_path.default.join(input.projectRoot, "main");
|
|
313
|
+
const remoteUrl = gitRemoteUrl(input.baseUrl, input.projectId);
|
|
314
|
+
import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
|
|
315
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.join(mainPath, ".git"))) {
|
|
316
|
+
import_node_fs.default.rmSync(mainPath, { recursive: true, force: true });
|
|
317
|
+
runGit(["clone", "--origin", "build-it-now", remoteUrl, mainPath], { auth: input });
|
|
318
|
+
}
|
|
319
|
+
configureRepo(mainPath, input);
|
|
320
|
+
runGit(["remote", "set-url", "build-it-now", remoteUrl], { cwd: mainPath });
|
|
321
|
+
runGit(["fetch", "build-it-now", "--prune"], { cwd: mainPath });
|
|
322
|
+
runGit(["checkout", "main"], { cwd: mainPath });
|
|
323
|
+
if (!hasWorktreeChanges(mainPath)) {
|
|
324
|
+
tryGit(["pull", "--ff-only", "build-it-now", "main"], { cwd: mainPath });
|
|
325
|
+
}
|
|
326
|
+
return mainPath;
|
|
327
|
+
}
|
|
328
|
+
function ensureBranchWorkspace(input) {
|
|
329
|
+
validateBranchName(input.branchName);
|
|
330
|
+
const mainPath = ensureMainClone(input);
|
|
331
|
+
if (input.branchName === "main") {
|
|
332
|
+
return mainPath;
|
|
333
|
+
}
|
|
334
|
+
const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
|
|
335
|
+
runGit(["fetch", "build-it-now", "--prune"], { cwd: mainPath });
|
|
336
|
+
const hasRemoteBranch = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/build-it-now/${input.branchName}`], {
|
|
337
|
+
cwd: mainPath
|
|
338
|
+
});
|
|
339
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"))) {
|
|
340
|
+
import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
|
|
341
|
+
if (hasRemoteBranch) {
|
|
342
|
+
runGit(["worktree", "add", "-B", input.branchName, branchPath, `build-it-now/${input.branchName}`], {
|
|
343
|
+
cwd: mainPath
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
runGit(["worktree", "add", "-b", input.branchName, branchPath, "build-it-now/main"], {
|
|
347
|
+
cwd: mainPath
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
configureRepo(branchPath, input);
|
|
352
|
+
if (hasRemoteBranch && !hasWorktreeChanges(branchPath)) {
|
|
353
|
+
tryGit(["pull", "--ff-only", "build-it-now", input.branchName], { cwd: branchPath });
|
|
354
|
+
}
|
|
355
|
+
return branchPath;
|
|
356
|
+
}
|
|
357
|
+
function resolveCommandCwd(branchPath, cwd) {
|
|
358
|
+
if (!cwd) {
|
|
359
|
+
return branchPath;
|
|
360
|
+
}
|
|
361
|
+
if (import_node_path.default.isAbsolute(cwd)) {
|
|
362
|
+
return cwd;
|
|
363
|
+
}
|
|
364
|
+
const resolved = import_node_path.default.resolve(branchPath, cwd);
|
|
365
|
+
const relative = import_node_path.default.relative(branchPath, resolved);
|
|
366
|
+
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
367
|
+
throw new Error(`Relative cwd escapes the project branch: ${cwd}`);
|
|
368
|
+
}
|
|
369
|
+
return resolved;
|
|
370
|
+
}
|
|
371
|
+
async function collectStream(stream) {
|
|
372
|
+
if (!stream) {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
return await new Response(stream).text();
|
|
376
|
+
}
|
|
377
|
+
function ensureOperationBranch(input) {
|
|
378
|
+
return ensureBranchWorkspace(input);
|
|
379
|
+
}
|
|
380
|
+
function formatLineNumberedContent(content, offset, limit) {
|
|
381
|
+
const allLines = content.split("\n");
|
|
382
|
+
const totalLines = allLines.length;
|
|
383
|
+
const startLine = Math.max(1, Math.floor(offset ?? 1));
|
|
384
|
+
const readLimit = Math.max(1, Math.floor(limit ?? DEFAULT_READ_LIMIT));
|
|
385
|
+
const endLine = Math.min(startLine + readLimit - 1, totalLines);
|
|
386
|
+
const selectedLines = allLines.slice(startLine - 1, endLine);
|
|
387
|
+
const padWidth = String(endLine).length;
|
|
388
|
+
const formattedLines = selectedLines.map((line, index) => {
|
|
389
|
+
const lineNumber = startLine + index;
|
|
390
|
+
const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
|
|
391
|
+
return `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
content: formattedLines.join("\n"),
|
|
395
|
+
totalLines,
|
|
396
|
+
startLine,
|
|
397
|
+
endLine
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function readPngDimensions(bytes) {
|
|
401
|
+
if (bytes.length < 24) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const pngSignature = "89504e470d0a1a0a";
|
|
405
|
+
if (bytes.subarray(0, 8).toString("hex") !== pngSignature || bytes.subarray(12, 16).toString("ascii") !== "IHDR") {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
width: bytes.readUInt32BE(16),
|
|
410
|
+
height: bytes.readUInt32BE(20)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function readJpegDimensions(bytes) {
|
|
414
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const startOfFrameMarkers = /* @__PURE__ */ new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
418
|
+
let offset = 2;
|
|
419
|
+
while (offset < bytes.length) {
|
|
420
|
+
while (offset < bytes.length && bytes[offset] !== 255) {
|
|
421
|
+
offset += 1;
|
|
422
|
+
}
|
|
423
|
+
while (offset < bytes.length && bytes[offset] === 255) {
|
|
424
|
+
offset += 1;
|
|
425
|
+
}
|
|
426
|
+
if (offset >= bytes.length) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const marker = bytes[offset];
|
|
430
|
+
offset += 1;
|
|
431
|
+
if (marker === 217 || marker === 218) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
if (marker >= 208 && marker <= 215) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (offset + 2 > bytes.length) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const length = bytes.readUInt16BE(offset);
|
|
441
|
+
if (length < 2 || offset + length > bytes.length) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
if (startOfFrameMarkers.has(marker)) {
|
|
445
|
+
if (length < 7) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
height: bytes.readUInt16BE(offset + 3),
|
|
450
|
+
width: bytes.readUInt16BE(offset + 5)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
offset += length;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function readImageDimensions(bytes, mediaType) {
|
|
458
|
+
const dimensions = mediaType === "image/png" ? readPngDimensions(bytes) : readJpegDimensions(bytes);
|
|
459
|
+
if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {
|
|
460
|
+
throw new Error("Could not determine image dimensions");
|
|
461
|
+
}
|
|
462
|
+
return dimensions;
|
|
463
|
+
}
|
|
464
|
+
async function executeReadFileOperation(input) {
|
|
465
|
+
const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
466
|
+
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
467
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
468
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
469
|
+
}
|
|
470
|
+
const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
471
|
+
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
472
|
+
return {
|
|
473
|
+
type: "read_file",
|
|
474
|
+
file: resolved.virtualPath,
|
|
475
|
+
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
476
|
+
...formatted
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function executeCreateFileOperation(input) {
|
|
480
|
+
const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
481
|
+
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
482
|
+
if (import_node_fs.default.existsSync(resolved.absolutePath)) {
|
|
483
|
+
throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
|
|
484
|
+
}
|
|
485
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
|
|
486
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
|
|
487
|
+
return {
|
|
488
|
+
type: "create_file",
|
|
489
|
+
file: resolved.virtualPath,
|
|
490
|
+
gitBlobHash: getGitBlobHashForContent(input.message.content)
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function executeEditFileOperation(input) {
|
|
494
|
+
const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
495
|
+
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
496
|
+
if (input.message.oldString === input.message.newString) {
|
|
497
|
+
throw new Error("old_string and new_string must be different");
|
|
498
|
+
}
|
|
499
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
500
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
501
|
+
}
|
|
502
|
+
const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
|
|
503
|
+
if (!content.includes(input.message.oldString)) {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`Could not find old_string: "${input.message.oldString.slice(0, 100)}${input.message.oldString.length > 100 ? "..." : ""}"`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (!input.message.replaceAll) {
|
|
509
|
+
const occurrences = content.split(input.message.oldString).length - 1;
|
|
510
|
+
if (occurrences > 1) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`old_string is not unique in the file (found ${occurrences} occurrences). Provide more context or use replace_all: true.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const nextContent = input.message.replaceAll ? content.split(input.message.oldString).join(input.message.newString) : content.replace(input.message.oldString, input.message.newString);
|
|
517
|
+
import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
|
|
518
|
+
return {
|
|
519
|
+
type: "edit_file",
|
|
520
|
+
file: resolved.virtualPath
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function executeViewFileBytesOperation(input) {
|
|
524
|
+
const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
525
|
+
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
526
|
+
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
527
|
+
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
528
|
+
}
|
|
529
|
+
const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
|
|
530
|
+
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
531
|
+
if (!mediaType) {
|
|
532
|
+
throw new Error("view_image supports PNG and JPEG files only");
|
|
533
|
+
}
|
|
534
|
+
const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
|
|
535
|
+
const dimensions = readImageDimensions(bytes, mediaType);
|
|
536
|
+
return {
|
|
537
|
+
type: "view_file_bytes",
|
|
538
|
+
filePath: resolved.virtualPath,
|
|
539
|
+
mediaType,
|
|
540
|
+
base64: bytes.toString("base64"),
|
|
541
|
+
fileSizeBytes: bytes.length,
|
|
542
|
+
width: dimensions.width,
|
|
543
|
+
height: dimensions.height
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function stagedBlobSizeBytes(cwd) {
|
|
547
|
+
const names = runGit(["diff", "--cached", "--name-only", "-z"], { cwd }).split("\0").filter(Boolean);
|
|
548
|
+
let total = 0;
|
|
549
|
+
for (const name of names) {
|
|
550
|
+
try {
|
|
551
|
+
const sizeText = runGit(["cat-file", "-s", `:${name}`], { cwd });
|
|
552
|
+
total += Number.parseInt(sizeText, 10) || 0;
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
if (total > MAX_SYNC_DIFF_BYTES) {
|
|
556
|
+
return MAX_SYNC_DIFF_BYTES + 1;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return total;
|
|
560
|
+
}
|
|
561
|
+
function syncBranch(input) {
|
|
562
|
+
const branchPath = ensureOperationBranch(input);
|
|
563
|
+
runGit(["add", "-A"], { cwd: branchPath });
|
|
564
|
+
const hasStagedChanges = !tryGit(["diff", "--cached", "--quiet"], { cwd: branchPath });
|
|
565
|
+
const status = runGit(["status", "--porcelain"], { cwd: branchPath });
|
|
566
|
+
const currentCommit = getCurrentCommitHash(branchPath);
|
|
567
|
+
if (!hasStagedChanges) {
|
|
568
|
+
return {
|
|
569
|
+
type: "sync",
|
|
570
|
+
changed: false,
|
|
571
|
+
commitHash: currentCommit,
|
|
572
|
+
diffSizeBytes: 0,
|
|
573
|
+
status
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const diffSizeBytes = stagedBlobSizeBytes(branchPath);
|
|
577
|
+
if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
|
|
578
|
+
return {
|
|
579
|
+
type: "sync",
|
|
580
|
+
changed: true,
|
|
581
|
+
commitHash: currentCommit,
|
|
582
|
+
diffSizeBytes,
|
|
583
|
+
status,
|
|
584
|
+
largeDiffBlocked: true,
|
|
585
|
+
trigger: input.trigger
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
const message = JSON.stringify({
|
|
589
|
+
type: "agent_changes",
|
|
590
|
+
sessionId: input.sessionId,
|
|
591
|
+
parentNodeId: input.parentNodeId,
|
|
592
|
+
...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
|
|
593
|
+
});
|
|
594
|
+
runGit(["commit", "-m", message], { cwd: branchPath });
|
|
595
|
+
const commitHash = getCurrentCommitHash(branchPath);
|
|
596
|
+
runGit(["push", "build-it-now", `HEAD:refs/heads/${input.branchName}`], { cwd: branchPath });
|
|
597
|
+
return {
|
|
598
|
+
type: "sync",
|
|
599
|
+
changed: true,
|
|
600
|
+
commitHash,
|
|
601
|
+
diffSizeBytes,
|
|
602
|
+
status: runGit(["status", "--porcelain"], { cwd: branchPath }),
|
|
603
|
+
trigger: input.trigger
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function confirmLargeDiff(input) {
|
|
607
|
+
const reason = input.reason.trim();
|
|
608
|
+
if (!reason) {
|
|
609
|
+
throw new Error("confirm_large_diff requires a non-empty reason");
|
|
610
|
+
}
|
|
611
|
+
const result = syncBranch({
|
|
612
|
+
...input,
|
|
613
|
+
confirmedLargeDiff: true,
|
|
614
|
+
confirmationReason: reason
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
type: "confirm_large_diff",
|
|
618
|
+
changed: result.changed,
|
|
619
|
+
commitHash: result.commitHash,
|
|
620
|
+
diffSizeBytes: result.diffSizeBytes,
|
|
621
|
+
status: result.status,
|
|
622
|
+
reason
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function pullBranch(input) {
|
|
626
|
+
const branchPath = ensureOperationBranch(input);
|
|
627
|
+
runGit(["fetch", "build-it-now", "--prune"], { cwd: branchPath });
|
|
628
|
+
const target = input.commitHash ?? `build-it-now/${input.branchName}`;
|
|
629
|
+
runGit(["reset", "--hard", target], { cwd: branchPath });
|
|
630
|
+
runGit(["clean", "-fd"], { cwd: branchPath });
|
|
631
|
+
return {
|
|
632
|
+
type: "pull_branch",
|
|
633
|
+
commitHash: getCurrentCommitHash(branchPath)
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
async function executeOperation(input) {
|
|
637
|
+
switch (input.message.type) {
|
|
638
|
+
case "read_file":
|
|
639
|
+
return executeReadFileOperation({ ...input, message: input.message });
|
|
640
|
+
case "create_file":
|
|
641
|
+
return executeCreateFileOperation({ ...input, message: input.message });
|
|
642
|
+
case "edit_file":
|
|
643
|
+
return executeEditFileOperation({ ...input, message: input.message });
|
|
644
|
+
case "view_file_bytes":
|
|
645
|
+
return executeViewFileBytesOperation({ ...input, message: input.message });
|
|
646
|
+
case "sync":
|
|
647
|
+
return syncBranch({
|
|
648
|
+
projectId: input.projectId,
|
|
649
|
+
baseUrl: input.baseUrl,
|
|
650
|
+
token: input.token,
|
|
651
|
+
projectRoot: input.projectRoot,
|
|
652
|
+
branchName: input.message.branchName,
|
|
653
|
+
sessionId: input.message.sessionId,
|
|
654
|
+
parentNodeId: input.message.parentNodeId,
|
|
655
|
+
trigger: input.message.trigger
|
|
656
|
+
});
|
|
657
|
+
case "confirm_large_diff":
|
|
658
|
+
return confirmLargeDiff({
|
|
659
|
+
projectId: input.projectId,
|
|
660
|
+
baseUrl: input.baseUrl,
|
|
661
|
+
token: input.token,
|
|
662
|
+
projectRoot: input.projectRoot,
|
|
663
|
+
branchName: input.message.branchName,
|
|
664
|
+
sessionId: input.message.sessionId,
|
|
665
|
+
parentNodeId: input.message.parentNodeId,
|
|
666
|
+
reason: input.message.reason
|
|
667
|
+
});
|
|
668
|
+
case "pull_branch":
|
|
669
|
+
return pullBranch({
|
|
670
|
+
projectId: input.projectId,
|
|
671
|
+
baseUrl: input.baseUrl,
|
|
672
|
+
token: input.token,
|
|
673
|
+
projectRoot: input.projectRoot,
|
|
674
|
+
branchName: input.message.branchName,
|
|
675
|
+
commitHash: input.message.commitHash
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function executeCommand(input) {
|
|
680
|
+
const startedAt = Date.now();
|
|
681
|
+
let timeout;
|
|
682
|
+
try {
|
|
683
|
+
const branchPath = ensureBranchWorkspace({
|
|
684
|
+
projectId: input.projectId,
|
|
685
|
+
baseUrl: input.baseUrl,
|
|
686
|
+
token: input.token,
|
|
687
|
+
projectRoot: input.projectRoot,
|
|
688
|
+
branchName: input.message.branchName
|
|
689
|
+
});
|
|
690
|
+
const cwd = resolveCommandCwd(branchPath, input.message.cwd);
|
|
691
|
+
const subprocess = Bun.spawn(input.message.argv, {
|
|
692
|
+
cwd,
|
|
693
|
+
stdout: "pipe",
|
|
694
|
+
stderr: "pipe",
|
|
695
|
+
env: {
|
|
696
|
+
...process.env,
|
|
697
|
+
...input.message.env ?? {}
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
|
|
701
|
+
if (input.message.timeoutMs) {
|
|
702
|
+
timeout = setTimeout(() => {
|
|
703
|
+
subprocess.kill("SIGTERM");
|
|
704
|
+
}, input.message.timeoutMs);
|
|
705
|
+
}
|
|
706
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
707
|
+
collectStream(subprocess.stdout),
|
|
708
|
+
collectStream(subprocess.stderr),
|
|
709
|
+
subprocess.exited
|
|
710
|
+
]);
|
|
711
|
+
return {
|
|
712
|
+
type: "exec_result",
|
|
713
|
+
requestId: input.message.requestId,
|
|
714
|
+
stdout,
|
|
715
|
+
stderr,
|
|
716
|
+
exitCode,
|
|
717
|
+
durationMs: Date.now() - startedAt
|
|
718
|
+
};
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return {
|
|
721
|
+
type: "exec_result",
|
|
722
|
+
requestId: input.message.requestId,
|
|
723
|
+
stdout: "",
|
|
724
|
+
stderr: "",
|
|
725
|
+
exitCode: 1,
|
|
726
|
+
durationMs: Date.now() - startedAt,
|
|
727
|
+
error: error instanceof Error ? error.message : String(error)
|
|
728
|
+
};
|
|
729
|
+
} finally {
|
|
730
|
+
if (timeout) {
|
|
731
|
+
clearTimeout(timeout);
|
|
732
|
+
}
|
|
733
|
+
activeProcesses.delete(input.message.runId);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function sendWorkerMessage(ws, message) {
|
|
737
|
+
ws.send(JSON.stringify(message));
|
|
738
|
+
}
|
|
739
|
+
function resolveHostShell() {
|
|
740
|
+
if (process.platform === "win32") {
|
|
741
|
+
return { file: process.env.COMSPEC || "powershell.exe", args: [] };
|
|
742
|
+
}
|
|
743
|
+
return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
|
|
744
|
+
}
|
|
745
|
+
function openPty(input) {
|
|
746
|
+
try {
|
|
747
|
+
const branchPath = ensureBranchWorkspace({
|
|
748
|
+
projectId: input.projectId,
|
|
749
|
+
baseUrl: input.baseUrl,
|
|
750
|
+
token: input.token,
|
|
751
|
+
projectRoot: input.projectRoot,
|
|
752
|
+
branchName: input.message.branchName
|
|
753
|
+
});
|
|
754
|
+
const shell = resolveHostShell();
|
|
755
|
+
const ptyProcess = nodePty.spawn(shell.file, shell.args, {
|
|
756
|
+
name: "xterm-256color",
|
|
757
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
758
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
759
|
+
cwd: branchPath,
|
|
760
|
+
env: {
|
|
761
|
+
...process.env,
|
|
762
|
+
...input.message.env ?? {},
|
|
763
|
+
BUILD_IT_NOW_PROJECT_ID: input.projectId,
|
|
764
|
+
BUILD_IT_NOW_BRANCH_NAME: input.message.branchName
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
activePtys.set(input.message.ptyId, ptyProcess);
|
|
768
|
+
ptyProcess.onData((data) => {
|
|
769
|
+
sendWorkerMessage(input.ws, {
|
|
770
|
+
type: "pty_output",
|
|
771
|
+
ptyId: input.message.ptyId,
|
|
772
|
+
data
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
ptyProcess.onExit((event) => {
|
|
776
|
+
activePtys.delete(input.message.ptyId);
|
|
777
|
+
sendWorkerMessage(input.ws, {
|
|
778
|
+
type: "pty_exit",
|
|
779
|
+
ptyId: input.message.ptyId,
|
|
780
|
+
exitCode: event.exitCode,
|
|
781
|
+
signal: event.signal
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
sendWorkerMessage(input.ws, {
|
|
785
|
+
type: "pty_opened",
|
|
786
|
+
requestId: input.message.requestId,
|
|
787
|
+
ptyId: input.message.ptyId
|
|
788
|
+
});
|
|
789
|
+
} catch (error) {
|
|
790
|
+
sendWorkerMessage(input.ws, {
|
|
791
|
+
type: "pty_error",
|
|
792
|
+
requestId: input.message.requestId,
|
|
793
|
+
ptyId: input.message.ptyId,
|
|
794
|
+
error: error instanceof Error ? error.message : String(error)
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function writePty(ws, message) {
|
|
799
|
+
const ptyProcess = activePtys.get(message.ptyId);
|
|
800
|
+
if (!ptyProcess) {
|
|
801
|
+
sendWorkerMessage(ws, {
|
|
802
|
+
type: "pty_error",
|
|
803
|
+
ptyId: message.ptyId,
|
|
804
|
+
error: "Shell is not open"
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
ptyProcess.write(message.data);
|
|
809
|
+
}
|
|
810
|
+
function resizePty(message) {
|
|
811
|
+
const ptyProcess = activePtys.get(message.ptyId);
|
|
812
|
+
if (!ptyProcess) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
ptyProcess.resize(
|
|
816
|
+
Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
|
|
817
|
+
Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
function closePty(message) {
|
|
821
|
+
const ptyProcess = activePtys.get(message.ptyId);
|
|
822
|
+
if (!ptyProcess) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
activePtys.delete(message.ptyId);
|
|
826
|
+
ptyProcess.kill();
|
|
827
|
+
}
|
|
828
|
+
function closeAllPtys() {
|
|
829
|
+
for (const ptyProcess of activePtys.values()) {
|
|
830
|
+
ptyProcess.kill();
|
|
831
|
+
}
|
|
832
|
+
activePtys.clear();
|
|
833
|
+
}
|
|
834
|
+
function websocketUrl(baseUrl, projectId, label) {
|
|
835
|
+
const url = new URL("/worker/ws", baseUrl);
|
|
836
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
837
|
+
url.searchParams.set("projectId", projectId);
|
|
838
|
+
url.searchParams.set("label", label);
|
|
839
|
+
return url.toString();
|
|
840
|
+
}
|
|
841
|
+
async function startWorker(projectId, options) {
|
|
842
|
+
const config = readConfig(options.configPath);
|
|
843
|
+
const { baseUrl, token } = resolveCredentials(options, config);
|
|
844
|
+
const label = requireValue(options.label, "Missing required --label <label>");
|
|
845
|
+
validateLabel(label);
|
|
846
|
+
const projectRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectRoot(projectId));
|
|
847
|
+
process.stdout.write(`[r5d-worker] project: ${projectId}
|
|
848
|
+
`);
|
|
849
|
+
process.stdout.write(`[r5d-worker] label: ${label}
|
|
850
|
+
`);
|
|
851
|
+
process.stdout.write(`[r5d-worker] root: ${projectRoot}
|
|
852
|
+
`);
|
|
853
|
+
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
854
|
+
`);
|
|
855
|
+
runGit(["--version"]);
|
|
856
|
+
await verifyBinctlAuth(baseUrl, token);
|
|
857
|
+
process.stdout.write("[r5d-worker] preparing local checkout...\n");
|
|
858
|
+
ensureMainClone({ projectId, baseUrl, token, projectRoot });
|
|
859
|
+
const ws = new WebSocket(websocketUrl(baseUrl, projectId, label), {
|
|
860
|
+
headers: {
|
|
861
|
+
Authorization: `Bearer ${token}`
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
ws.addEventListener("open", () => {
|
|
865
|
+
const hello = {
|
|
866
|
+
type: "hello",
|
|
867
|
+
hostInfo: {
|
|
868
|
+
hostname: (0, import_node_os2.hostname)(),
|
|
869
|
+
platform: process.platform,
|
|
870
|
+
arch: process.arch,
|
|
871
|
+
pid: process.pid,
|
|
872
|
+
version: "0.1.0",
|
|
873
|
+
projectRoot
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
ws.send(JSON.stringify(hello));
|
|
877
|
+
process.stdout.write("[r5d-worker] connected\n");
|
|
878
|
+
});
|
|
879
|
+
ws.addEventListener("message", (event) => {
|
|
880
|
+
void (async () => {
|
|
881
|
+
const message = JSON.parse(String(event.data));
|
|
882
|
+
if (message.type === "connected") {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (message.type === "ping") {
|
|
886
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (message.type === "cancel") {
|
|
890
|
+
const active = activeProcesses.get(message.runId);
|
|
891
|
+
if (active) {
|
|
892
|
+
active.process.kill("SIGTERM");
|
|
893
|
+
}
|
|
894
|
+
ws.send(
|
|
895
|
+
JSON.stringify({
|
|
896
|
+
type: "cancel_result",
|
|
897
|
+
requestId: message.requestId,
|
|
898
|
+
runId: message.runId,
|
|
899
|
+
cancelled: !!active,
|
|
900
|
+
message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "No active command with that run id"
|
|
901
|
+
})
|
|
902
|
+
);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (message.type === "pty_open") {
|
|
906
|
+
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.branchName}
|
|
907
|
+
`);
|
|
908
|
+
openPty({ ws, message, projectId, baseUrl, token, projectRoot });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (message.type === "pty_input") {
|
|
912
|
+
writePty(ws, message);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (message.type === "pty_resize") {
|
|
916
|
+
resizePty(message);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (message.type === "pty_close") {
|
|
920
|
+
closePty(message);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (message.type === "exec") {
|
|
924
|
+
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
925
|
+
`);
|
|
926
|
+
const result = await withBranchLock(
|
|
927
|
+
projectRoot,
|
|
928
|
+
message.branchName,
|
|
929
|
+
() => executeCommand({ message, projectId, baseUrl, token, projectRoot })
|
|
930
|
+
);
|
|
931
|
+
ws.send(JSON.stringify(result));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (message.type === "read_file" || message.type === "create_file" || message.type === "edit_file" || message.type === "view_file_bytes" || message.type === "sync" || message.type === "pull_branch") {
|
|
935
|
+
try {
|
|
936
|
+
const result = await withBranchLock(
|
|
937
|
+
projectRoot,
|
|
938
|
+
message.branchName,
|
|
939
|
+
() => executeOperation({ message, projectId, baseUrl, token, projectRoot })
|
|
940
|
+
);
|
|
941
|
+
ws.send(
|
|
942
|
+
JSON.stringify({
|
|
943
|
+
type: "operation_result",
|
|
944
|
+
requestId: message.requestId,
|
|
945
|
+
result
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
ws.send(
|
|
950
|
+
JSON.stringify({
|
|
951
|
+
type: "operation_result",
|
|
952
|
+
requestId: message.requestId,
|
|
953
|
+
error: error instanceof Error ? error.message : String(error)
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
})().catch((error) => {
|
|
959
|
+
process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
|
|
960
|
+
`);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
ws.addEventListener("close", (event) => {
|
|
964
|
+
closeAllPtys();
|
|
965
|
+
const reason = event.reason ? `: ${event.reason}` : "";
|
|
966
|
+
process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
|
|
967
|
+
`);
|
|
968
|
+
process.exit(event.code === 1e3 ? 0 : 1);
|
|
969
|
+
});
|
|
970
|
+
ws.addEventListener("error", (event) => {
|
|
971
|
+
const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
|
|
972
|
+
process.stderr.write(`[r5d-worker] websocket error${message}
|
|
973
|
+
`);
|
|
974
|
+
});
|
|
975
|
+
await new Promise(() => {
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
async function main() {
|
|
979
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
980
|
+
if (parsed.command === "help") {
|
|
981
|
+
printHelp();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
await startWorker(parsed.projectId, parsed.options);
|
|
985
|
+
}
|
|
986
|
+
main().catch((error) => {
|
|
987
|
+
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|
|
988
|
+
`);
|
|
989
|
+
process.exit(1);
|
|
990
|
+
});
|