@ricsam/r5d-worker 0.0.1 → 0.0.3

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.
@@ -0,0 +1,1420 @@
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 import_node_child_process = require("node:child_process");
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 defaultProjectsRoot() {
44
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects");
45
+ }
46
+ function defaultSyncRoot() {
47
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "sync");
48
+ }
49
+ function printHelp() {
50
+ process.stdout.write(`Usage:
51
+ r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
52
+
53
+ Options:
54
+ --label <label> Required unique worker label for this user, e.g. macos or ec2-build
55
+ --base-url <url> r5d.dev base URL (default from r5d config, R5D_BASE_URL, or ${DEFAULT_BASE_URL})
56
+ --token <token> r5d worker token
57
+ --api-key <key> r5d API key
58
+ --config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
59
+ --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
60
+ --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
61
+ -h, --help Show this help
62
+ `);
63
+ }
64
+ function requireValue(value, message) {
65
+ if (!value) {
66
+ throw new Error(message);
67
+ }
68
+ return value;
69
+ }
70
+ function parseArgs(argv) {
71
+ const options = {
72
+ configPath: defaultConfigPath(),
73
+ help: false
74
+ };
75
+ const rest = [];
76
+ for (let index = 0; index < argv.length; index += 1) {
77
+ const arg = argv[index];
78
+ if (!arg) {
79
+ continue;
80
+ }
81
+ if (arg === "-h" || arg === "--help") {
82
+ options.help = true;
83
+ continue;
84
+ }
85
+ const readOption = (name) => {
86
+ if (arg === name) {
87
+ index += 1;
88
+ return requireValue(argv[index], `Missing value for ${name}`);
89
+ }
90
+ const prefix = `${name}=`;
91
+ return arg.startsWith(prefix) ? arg.slice(prefix.length) : void 0;
92
+ };
93
+ const baseUrl = readOption("--base-url");
94
+ if (baseUrl !== void 0) {
95
+ options.baseUrl = baseUrl;
96
+ continue;
97
+ }
98
+ const token = readOption("--token");
99
+ if (token !== void 0) {
100
+ options.token = token;
101
+ continue;
102
+ }
103
+ const apiKey = readOption("--api-key");
104
+ if (apiKey !== void 0) {
105
+ options.apiKey = apiKey;
106
+ continue;
107
+ }
108
+ const configPath = readOption("--config");
109
+ if (configPath !== void 0) {
110
+ options.configPath = configPath;
111
+ continue;
112
+ }
113
+ const label = readOption("--label");
114
+ if (label !== void 0) {
115
+ options.label = label;
116
+ continue;
117
+ }
118
+ const projectRoot = readOption("--project-root");
119
+ if (projectRoot !== void 0) {
120
+ options.projectRoot = projectRoot;
121
+ continue;
122
+ }
123
+ const syncRoot = readOption("--sync-root");
124
+ if (syncRoot !== void 0) {
125
+ options.syncRoot = syncRoot;
126
+ continue;
127
+ }
128
+ rest.push(arg);
129
+ }
130
+ if (options.help || rest.length === 0) {
131
+ return { command: "help", options };
132
+ }
133
+ const command = rest[0];
134
+ if (command !== "start") {
135
+ throw new Error(`Unknown command: ${command}`);
136
+ }
137
+ return {
138
+ command: "start",
139
+ options
140
+ };
141
+ }
142
+ function readConfig(configPath) {
143
+ if (!import_node_fs.default.existsSync(configPath)) {
144
+ return {};
145
+ }
146
+ const raw = import_node_fs.default.readFileSync(configPath, "utf8").trim();
147
+ if (!raw) {
148
+ return {};
149
+ }
150
+ const parsed = JSON.parse(raw);
151
+ return {
152
+ baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
153
+ token: typeof parsed.token === "string" ? parsed.token : void 0,
154
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0
155
+ };
156
+ }
157
+ function normalizeBaseUrl(value) {
158
+ return value.replace(/\/+$/, "");
159
+ }
160
+ async function readResponseText(response) {
161
+ try {
162
+ return await response.text();
163
+ } catch {
164
+ return "";
165
+ }
166
+ }
167
+ async function verifyBinctlAuth(baseUrl, token) {
168
+ const statusUrl = new URL("/api/binctl/auth/status", baseUrl);
169
+ let response;
170
+ try {
171
+ response = await fetch(statusUrl, {
172
+ headers: {
173
+ Authorization: `Bearer ${token}`
174
+ }
175
+ });
176
+ } catch (error) {
177
+ throw new Error(`Could not reach ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
178
+ }
179
+ if (response.ok) {
180
+ return;
181
+ }
182
+ const responseText = await readResponseText(response);
183
+ let serverMessage = responseText.trim();
184
+ try {
185
+ const parsed = JSON.parse(responseText);
186
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
187
+ serverMessage = parsed.error.trim();
188
+ }
189
+ } catch {
190
+ }
191
+ const detail = serverMessage ? `: ${serverMessage}` : "";
192
+ throw new Error(
193
+ `Authentication failed against ${baseUrl} (${response.status})${detail}. Run \`r5dctl auth login --base-url ${baseUrl}\` or pass --token/--api-key for this server.`
194
+ );
195
+ }
196
+ function resolveCredentials(options, config) {
197
+ 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;
198
+ if (!token) {
199
+ throw new Error("Authentication required. Run `r5dctl auth login` or set R5D_WORKER_TOKEN/R5D_API_KEY.");
200
+ }
201
+ return {
202
+ baseUrl: normalizeBaseUrl(options.baseUrl ?? process.env.R5D_BASE_URL ?? process.env.BINCTL_BASE_URL ?? config.baseUrl ?? DEFAULT_BASE_URL),
203
+ token
204
+ };
205
+ }
206
+ function validateLabel(label) {
207
+ if (!LABEL_RE.test(label)) {
208
+ throw new Error("Worker label must start with a letter or number and may contain letters, numbers, dots, underscores, and hyphens");
209
+ }
210
+ }
211
+ function validateBranchName(branchName) {
212
+ if (branchName !== "main" && !BRANCH_RE.test(branchName)) {
213
+ throw new Error(`Invalid branch name from server: ${branchName}`);
214
+ }
215
+ }
216
+ function gitExtraHeaderConfigKey(extraHeaderUrl) {
217
+ return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
218
+ }
219
+ function gitAuthArgs(auth) {
220
+ if (!auth) {
221
+ return [];
222
+ }
223
+ return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
224
+ }
225
+ function runGit(args, options = {}) {
226
+ const command = ["git", ...gitAuthArgs(options.auth), ...args];
227
+ const result = Bun.spawnSync(command, {
228
+ cwd: options.cwd,
229
+ stdout: "pipe",
230
+ stderr: "pipe",
231
+ env: {
232
+ ...process.env,
233
+ GIT_TERMINAL_PROMPT: "0"
234
+ }
235
+ });
236
+ if (result.exitCode !== 0) {
237
+ throw new Error(
238
+ `git ${args.join(" ")} failed: ${result.stderr.toString().trim() || result.stdout.toString().trim() || `exit ${result.exitCode}`}`
239
+ );
240
+ }
241
+ return result.stdout.toString().trim();
242
+ }
243
+ function tryGit(args, options = {}) {
244
+ try {
245
+ runGit(args, options);
246
+ return true;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ function runInternalGit(context, args) {
252
+ return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
253
+ }
254
+ function runInternalGitDir(context, args) {
255
+ return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
256
+ }
257
+ function tryInternalGit(context, args) {
258
+ try {
259
+ runInternalGit(context, args);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ function getInternalCommitHash(context) {
266
+ return runInternalGit(context, ["rev-parse", "HEAD"]);
267
+ }
268
+ function getGitBlobHashForContent(content) {
269
+ const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
270
+ return (0, import_node_crypto.createHash)("sha1").update(`blob ${buffer.length}\0`).update(buffer).digest("hex");
271
+ }
272
+ function hasWorktreeChanges(cwd) {
273
+ return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
274
+ }
275
+ function getInternalStatus(context) {
276
+ return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
277
+ }
278
+ function hasInternalWorktreeChanges(context) {
279
+ return getInternalStatus(context).trim().length > 0;
280
+ }
281
+ function assertInsideBranch(branchPath, filePath) {
282
+ const relative = import_node_path.default.relative(branchPath, filePath);
283
+ if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
284
+ throw new Error("File path escapes the project branch");
285
+ }
286
+ }
287
+ function resolveProjectFilePath(branchPath, virtualPath) {
288
+ if (typeof virtualPath !== "string" || !virtualPath.startsWith("/") || virtualPath.includes("\0")) {
289
+ throw new Error(`File path must be an absolute project path starting with /. Got: ${JSON.stringify(virtualPath)}`);
290
+ }
291
+ const normalized = import_node_path.default.posix.normalize(virtualPath);
292
+ if (normalized === "/" || normalized.startsWith("/../")) {
293
+ throw new Error(`Invalid project file path: ${virtualPath}`);
294
+ }
295
+ const repoRelativePath = normalized.slice(1);
296
+ if (repoRelativePath === ".git" || repoRelativePath.startsWith(".git/")) {
297
+ throw new Error("Refusing to access .git through worker file tools");
298
+ }
299
+ const absolutePath = import_node_path.default.resolve(branchPath, ...repoRelativePath.split("/"));
300
+ assertInsideBranch(branchPath, absolutePath);
301
+ return { absolutePath, virtualPath: `/${repoRelativePath}`, repoRelativePath };
302
+ }
303
+ function branchLockKey(projectId, branchName) {
304
+ return `${projectId}:${branchName}`;
305
+ }
306
+ async function withBranchLock(projectId, branchName, fn) {
307
+ const key = branchLockKey(projectId, branchName);
308
+ const previous = branchLocks.get(key) ?? Promise.resolve();
309
+ let release = () => {
310
+ };
311
+ const next = new Promise((resolve) => {
312
+ release = resolve;
313
+ });
314
+ const queued = previous.catch(() => {
315
+ }).then(() => next);
316
+ branchLocks.set(key, queued);
317
+ await previous.catch(() => {
318
+ });
319
+ try {
320
+ return await fn();
321
+ } finally {
322
+ release();
323
+ if (branchLocks.get(key) === queued) {
324
+ branchLocks.delete(key);
325
+ }
326
+ }
327
+ }
328
+ function resolveRemoteUrl(baseUrl, remoteUrl) {
329
+ if (/^https?:\/\//i.test(remoteUrl)) {
330
+ return remoteUrl;
331
+ }
332
+ return new URL(remoteUrl, baseUrl).toString();
333
+ }
334
+ function fallbackInternalRemoteUrl(baseUrl, projectId) {
335
+ return new URL(`/git/${projectId}.git`, baseUrl).toString();
336
+ }
337
+ function internalGitAuth(baseUrl, token) {
338
+ return {
339
+ extraHeaderUrl: baseUrl,
340
+ header: `Authorization: Bearer ${token}`
341
+ };
342
+ }
343
+ function internalRemoteUrlFor(baseUrl, projectId, manifest) {
344
+ return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
345
+ }
346
+ function githubRemoteUrlFor(baseUrl, projectId, manifest) {
347
+ return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
348
+ }
349
+ function gitExtraHeaderUrlForRemote(remoteUrl) {
350
+ try {
351
+ return new URL(remoteUrl).origin;
352
+ } catch {
353
+ return remoteUrl;
354
+ }
355
+ }
356
+ function gitAuthForRemote(remoteUrl, authHeader) {
357
+ if (!authHeader) {
358
+ return void 0;
359
+ }
360
+ return {
361
+ extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
362
+ header: authHeader
363
+ };
364
+ }
365
+ function branchGitDirName(branchName) {
366
+ return `${encodeURIComponent(branchName)}.git`;
367
+ }
368
+ function internalGitDirFor(syncRoot, projectId, branchName) {
369
+ return import_node_path.default.join(syncRoot, projectId, branchGitDirName(branchName));
370
+ }
371
+ function hasNormalVisibleGitDir(branchPath) {
372
+ const gitPath = import_node_path.default.join(branchPath, ".git");
373
+ return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
374
+ }
375
+ function ensureOriginRemote(branchPath, remoteUrl) {
376
+ if (tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) {
377
+ runGit(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
378
+ } else {
379
+ runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
380
+ }
381
+ }
382
+ function shellQuote(value) {
383
+ return `'${value.replace(/'/g, "'\\''")}'`;
384
+ }
385
+ function githubCredentialsPath() {
386
+ return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
387
+ }
388
+ function decodeBasicAuthHeader(authHeader) {
389
+ const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
390
+ if (!match) {
391
+ return null;
392
+ }
393
+ const decoded = Buffer.from(match[1], "base64").toString("utf8");
394
+ const separatorIndex = decoded.indexOf(":");
395
+ if (separatorIndex <= 0) {
396
+ return null;
397
+ }
398
+ return {
399
+ username: decoded.slice(0, separatorIndex),
400
+ password: decoded.slice(separatorIndex + 1)
401
+ };
402
+ }
403
+ function writeCredentialStoreEntry(remoteUrl, authHeader) {
404
+ const credentials = decodeBasicAuthHeader(authHeader);
405
+ if (!credentials) {
406
+ return null;
407
+ }
408
+ let remote;
409
+ try {
410
+ remote = new URL(remoteUrl);
411
+ } catch {
412
+ return null;
413
+ }
414
+ if (remote.protocol !== "https:") {
415
+ return null;
416
+ }
417
+ const storePath = githubCredentialsPath();
418
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(storePath), { recursive: true });
419
+ const hostKey = `${remote.protocol}//${remote.host}`;
420
+ const existing = import_node_fs.default.existsSync(storePath) ? import_node_fs.default.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
421
+ const next = existing.filter((line) => {
422
+ try {
423
+ const parsed = new URL(line);
424
+ return `${parsed.protocol}//${parsed.host}` !== hostKey;
425
+ } catch {
426
+ return true;
427
+ }
428
+ });
429
+ const credentialUrl = new URL(hostKey);
430
+ credentialUrl.username = credentials.username;
431
+ credentialUrl.password = credentials.password;
432
+ next.push(credentialUrl.toString());
433
+ import_node_fs.default.writeFileSync(storePath, `${next.join("\n")}
434
+ `, { mode: 384 });
435
+ import_node_fs.default.chmodSync(storePath, 384);
436
+ return storePath;
437
+ }
438
+ function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
439
+ if (!authHeader) {
440
+ return;
441
+ }
442
+ const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
443
+ if (credentialStore) {
444
+ runGit(["config", "credential.helper", `store --file=${shellQuote(credentialStore)}`], { cwd: branchPath });
445
+ runGit(["config", "credential.useHttpPath", "false"], { cwd: branchPath });
446
+ return;
447
+ }
448
+ runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
449
+ }
450
+ function checkoutVisibleBranch(input) {
451
+ const branchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`], { cwd: input.branchPath });
452
+ const remoteBranchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], {
453
+ cwd: input.branchPath
454
+ });
455
+ const defaultRemoteExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.defaultBranch}`], {
456
+ cwd: input.branchPath
457
+ });
458
+ const clean = !hasWorktreeChanges(input.branchPath);
459
+ if (remoteBranchExists && clean) {
460
+ runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], { cwd: input.branchPath });
461
+ return;
462
+ }
463
+ if (branchExists) {
464
+ runGit(["checkout", input.branchName], { cwd: input.branchPath });
465
+ return;
466
+ }
467
+ if (defaultRemoteExists) {
468
+ runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], { cwd: input.branchPath });
469
+ return;
470
+ }
471
+ runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
472
+ }
473
+ function ensureVisibleGitCheckout(input) {
474
+ validateBranchName(input.branchName);
475
+ import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
476
+ const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
477
+ if (!hasNormalVisibleGitDir(branchPath)) {
478
+ import_node_fs.default.rmSync(branchPath, { recursive: true, force: true });
479
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
480
+ if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
481
+ import_node_fs.default.mkdirSync(branchPath, { recursive: true });
482
+ runGit(["init"], { cwd: branchPath });
483
+ }
484
+ }
485
+ ensureOriginRemote(branchPath, input.githubRemoteUrl);
486
+ configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
487
+ tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
488
+ checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
489
+ return branchPath;
490
+ }
491
+ function configureInternalRepo(context, remoteUrl) {
492
+ if (tryInternalGit(context, ["remote", "get-url", "build-it-now"])) {
493
+ runInternalGit(context, ["remote", "set-url", "build-it-now", remoteUrl]);
494
+ } else {
495
+ runInternalGit(context, ["remote", "add", "build-it-now", remoteUrl]);
496
+ }
497
+ runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
498
+ runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
499
+ }
500
+ function ensureInternalSyncGit(input) {
501
+ validateBranchName(input.branchName);
502
+ const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
503
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
504
+ if (!import_node_fs.default.existsSync(gitDir)) {
505
+ runGit(["init", "--bare", gitDir]);
506
+ }
507
+ const auth = internalGitAuth(input.baseUrl, input.token);
508
+ const context = { gitDir, workTree: input.branchPath, auth };
509
+ const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
510
+ configureInternalRepo(context, remoteUrl);
511
+ runInternalGit(context, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
512
+ const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/build-it-now/${input.branchName}`]);
513
+ const target = hasRemoteBranch ? `refs/remotes/build-it-now/${input.branchName}` : "refs/remotes/build-it-now/main";
514
+ const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
515
+ if (!hasHead || !hasInternalWorktreeChanges(context)) {
516
+ runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
517
+ }
518
+ return context;
519
+ }
520
+ function ensureBranchWorkspace(input) {
521
+ const defaultBranch = input.manifest?.defaultBranch || "main";
522
+ const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
523
+ const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
524
+ const branchPath = ensureVisibleGitCheckout({
525
+ projectRoot: input.projectRoot,
526
+ branchName: input.branchName,
527
+ githubRemoteUrl,
528
+ githubAuth,
529
+ githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
530
+ defaultBranch
531
+ });
532
+ const internalGit = ensureInternalSyncGit({
533
+ projectId: input.projectId,
534
+ baseUrl: input.baseUrl,
535
+ token: input.token,
536
+ syncRoot: input.syncRoot,
537
+ branchPath,
538
+ branchName: input.branchName,
539
+ manifest: input.manifest
540
+ });
541
+ return { branchPath, internalGit };
542
+ }
543
+ function resolveCommandCwd(branchPath, cwd) {
544
+ if (!cwd) {
545
+ return branchPath;
546
+ }
547
+ if (import_node_path.default.isAbsolute(cwd)) {
548
+ return cwd;
549
+ }
550
+ const resolved = import_node_path.default.resolve(branchPath, cwd);
551
+ const relative = import_node_path.default.relative(branchPath, resolved);
552
+ if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
553
+ throw new Error(`Relative cwd escapes the project branch: ${cwd}`);
554
+ }
555
+ return resolved;
556
+ }
557
+ async function collectStream(stream) {
558
+ if (!stream) {
559
+ return "";
560
+ }
561
+ return await new Response(stream).text();
562
+ }
563
+ function ensureOperationBranch(input) {
564
+ return ensureBranchWorkspace(input);
565
+ }
566
+ function formatLineNumberedContent(content, offset, limit) {
567
+ const allLines = content.split("\n");
568
+ const totalLines = allLines.length;
569
+ const startLine = Math.max(1, Math.floor(offset ?? 1));
570
+ const readLimit = Math.max(1, Math.floor(limit ?? DEFAULT_READ_LIMIT));
571
+ const endLine = Math.min(startLine + readLimit - 1, totalLines);
572
+ const selectedLines = allLines.slice(startLine - 1, endLine);
573
+ const padWidth = String(endLine).length;
574
+ const formattedLines = selectedLines.map((line, index) => {
575
+ const lineNumber = startLine + index;
576
+ const truncatedLine = line.length > MAX_LINE_LENGTH ? `${line.slice(0, MAX_LINE_LENGTH)}...` : line;
577
+ return `${String(lineNumber).padStart(padWidth, " ")}\u2192${truncatedLine}`;
578
+ });
579
+ return {
580
+ content: formattedLines.join("\n"),
581
+ totalLines,
582
+ startLine,
583
+ endLine
584
+ };
585
+ }
586
+ function readPngDimensions(bytes) {
587
+ if (bytes.length < 24) {
588
+ return null;
589
+ }
590
+ const pngSignature = "89504e470d0a1a0a";
591
+ if (bytes.subarray(0, 8).toString("hex") !== pngSignature || bytes.subarray(12, 16).toString("ascii") !== "IHDR") {
592
+ return null;
593
+ }
594
+ return {
595
+ width: bytes.readUInt32BE(16),
596
+ height: bytes.readUInt32BE(20)
597
+ };
598
+ }
599
+ function readJpegDimensions(bytes) {
600
+ if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216) {
601
+ return null;
602
+ }
603
+ const startOfFrameMarkers = /* @__PURE__ */ new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
604
+ let offset = 2;
605
+ while (offset < bytes.length) {
606
+ while (offset < bytes.length && bytes[offset] !== 255) {
607
+ offset += 1;
608
+ }
609
+ while (offset < bytes.length && bytes[offset] === 255) {
610
+ offset += 1;
611
+ }
612
+ if (offset >= bytes.length) {
613
+ return null;
614
+ }
615
+ const marker = bytes[offset];
616
+ offset += 1;
617
+ if (marker === 217 || marker === 218) {
618
+ return null;
619
+ }
620
+ if (marker >= 208 && marker <= 215) {
621
+ continue;
622
+ }
623
+ if (offset + 2 > bytes.length) {
624
+ return null;
625
+ }
626
+ const length = bytes.readUInt16BE(offset);
627
+ if (length < 2 || offset + length > bytes.length) {
628
+ return null;
629
+ }
630
+ if (startOfFrameMarkers.has(marker)) {
631
+ if (length < 7) {
632
+ return null;
633
+ }
634
+ return {
635
+ height: bytes.readUInt16BE(offset + 3),
636
+ width: bytes.readUInt16BE(offset + 5)
637
+ };
638
+ }
639
+ offset += length;
640
+ }
641
+ return null;
642
+ }
643
+ function readImageDimensions(bytes, mediaType) {
644
+ const dimensions = mediaType === "image/png" ? readPngDimensions(bytes) : readJpegDimensions(bytes);
645
+ if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) {
646
+ throw new Error("Could not determine image dimensions");
647
+ }
648
+ return dimensions;
649
+ }
650
+ async function executeReadFileOperation(input) {
651
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
652
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
653
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
654
+ throw new Error(`File not found: ${resolved.virtualPath}`);
655
+ }
656
+ const buffer = import_node_fs.default.readFileSync(resolved.absolutePath);
657
+ const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
658
+ return {
659
+ type: "read_file",
660
+ file: resolved.virtualPath,
661
+ gitBlobHash: getGitBlobHashForContent(buffer),
662
+ ...formatted
663
+ };
664
+ }
665
+ async function executeCreateFileOperation(input) {
666
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
667
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
668
+ if (import_node_fs.default.existsSync(resolved.absolutePath)) {
669
+ throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
670
+ }
671
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(resolved.absolutePath), { recursive: true });
672
+ import_node_fs.default.writeFileSync(resolved.absolutePath, input.message.content, "utf8");
673
+ return {
674
+ type: "create_file",
675
+ file: resolved.virtualPath,
676
+ gitBlobHash: getGitBlobHashForContent(input.message.content)
677
+ };
678
+ }
679
+ async function executeEditFileOperation(input) {
680
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
681
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
682
+ if (input.message.oldString === input.message.newString) {
683
+ throw new Error("old_string and new_string must be different");
684
+ }
685
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
686
+ throw new Error(`File not found: ${resolved.virtualPath}`);
687
+ }
688
+ const content = import_node_fs.default.readFileSync(resolved.absolutePath, "utf8");
689
+ if (!content.includes(input.message.oldString)) {
690
+ throw new Error(
691
+ `Could not find old_string: "${input.message.oldString.slice(0, 100)}${input.message.oldString.length > 100 ? "..." : ""}"`
692
+ );
693
+ }
694
+ if (!input.message.replaceAll) {
695
+ const occurrences = content.split(input.message.oldString).length - 1;
696
+ if (occurrences > 1) {
697
+ throw new Error(
698
+ `old_string is not unique in the file (found ${occurrences} occurrences). Provide more context or use replace_all: true.`
699
+ );
700
+ }
701
+ }
702
+ const nextContent = input.message.replaceAll ? content.split(input.message.oldString).join(input.message.newString) : content.replace(input.message.oldString, input.message.newString);
703
+ import_node_fs.default.writeFileSync(resolved.absolutePath, nextContent, "utf8");
704
+ return {
705
+ type: "edit_file",
706
+ file: resolved.virtualPath
707
+ };
708
+ }
709
+ async function executeViewFileBytesOperation(input) {
710
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
711
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
712
+ if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
713
+ throw new Error(`File not found: ${resolved.virtualPath}`);
714
+ }
715
+ const extension = import_node_path.default.extname(resolved.absolutePath).toLowerCase();
716
+ const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
717
+ if (!mediaType) {
718
+ throw new Error("view_image supports PNG and JPEG files only");
719
+ }
720
+ const bytes = import_node_fs.default.readFileSync(resolved.absolutePath);
721
+ const dimensions = readImageDimensions(bytes, mediaType);
722
+ return {
723
+ type: "view_file_bytes",
724
+ filePath: resolved.virtualPath,
725
+ mediaType,
726
+ base64: bytes.toString("base64"),
727
+ fileSizeBytes: bytes.length,
728
+ width: dimensions.width,
729
+ height: dimensions.height
730
+ };
731
+ }
732
+ function stagedBlobSizeBytes(context) {
733
+ const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
734
+ let total = 0;
735
+ for (const name of names) {
736
+ try {
737
+ const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
738
+ total += Number.parseInt(sizeText, 10) || 0;
739
+ } catch {
740
+ }
741
+ if (total > MAX_SYNC_DIFF_BYTES) {
742
+ return MAX_SYNC_DIFF_BYTES + 1;
743
+ }
744
+ }
745
+ return total;
746
+ }
747
+ function syncBranch(input) {
748
+ const workspace = ensureOperationBranch(input);
749
+ runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
750
+ const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
751
+ const status = getInternalStatus(workspace.internalGit);
752
+ const currentCommit = getInternalCommitHash(workspace.internalGit);
753
+ if (!hasStagedChanges) {
754
+ return {
755
+ type: "sync",
756
+ changed: false,
757
+ commitHash: currentCommit,
758
+ diffSizeBytes: 0,
759
+ status
760
+ };
761
+ }
762
+ const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
763
+ if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
764
+ return {
765
+ type: "sync",
766
+ changed: true,
767
+ commitHash: currentCommit,
768
+ diffSizeBytes,
769
+ status,
770
+ largeDiffBlocked: true,
771
+ trigger: input.trigger
772
+ };
773
+ }
774
+ const message = JSON.stringify({
775
+ type: "agent_changes",
776
+ sessionId: input.sessionId,
777
+ parentNodeId: input.parentNodeId,
778
+ ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
779
+ });
780
+ runInternalGit(workspace.internalGit, ["commit", "-m", message]);
781
+ const commitHash = getInternalCommitHash(workspace.internalGit);
782
+ runInternalGit(workspace.internalGit, ["push", "build-it-now", `HEAD:refs/heads/${input.branchName}`]);
783
+ return {
784
+ type: "sync",
785
+ changed: true,
786
+ commitHash,
787
+ diffSizeBytes,
788
+ status: getInternalStatus(workspace.internalGit),
789
+ trigger: input.trigger
790
+ };
791
+ }
792
+ function confirmLargeDiff(input) {
793
+ const reason = input.reason.trim();
794
+ if (!reason) {
795
+ throw new Error("confirm_large_diff requires a non-empty reason");
796
+ }
797
+ const result = syncBranch({
798
+ ...input,
799
+ confirmedLargeDiff: true,
800
+ confirmationReason: reason
801
+ });
802
+ return {
803
+ type: "confirm_large_diff",
804
+ changed: result.changed,
805
+ commitHash: result.commitHash,
806
+ diffSizeBytes: result.diffSizeBytes,
807
+ status: result.status,
808
+ reason
809
+ };
810
+ }
811
+ function pullBranch(input) {
812
+ const workspace = ensureOperationBranch(input);
813
+ runInternalGit(workspace.internalGit, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
814
+ const target = input.commitHash ?? `build-it-now/${input.branchName}`;
815
+ runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
816
+ runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
817
+ return {
818
+ type: "pull_branch",
819
+ commitHash: getInternalCommitHash(workspace.internalGit)
820
+ };
821
+ }
822
+ async function executeOperation(input) {
823
+ switch (input.message.type) {
824
+ case "read_file":
825
+ return executeReadFileOperation({ ...input, message: input.message });
826
+ case "create_file":
827
+ return executeCreateFileOperation({ ...input, message: input.message });
828
+ case "edit_file":
829
+ return executeEditFileOperation({ ...input, message: input.message });
830
+ case "view_file_bytes":
831
+ return executeViewFileBytesOperation({ ...input, message: input.message });
832
+ case "sync":
833
+ return syncBranch({
834
+ projectId: input.projectId,
835
+ baseUrl: input.baseUrl,
836
+ token: input.token,
837
+ projectRoot: input.projectRoot,
838
+ syncRoot: input.syncRoot,
839
+ branchName: input.message.branchName,
840
+ sessionId: input.message.sessionId,
841
+ parentNodeId: input.message.parentNodeId,
842
+ trigger: input.message.trigger,
843
+ manifest: input.manifest
844
+ });
845
+ case "confirm_large_diff":
846
+ return confirmLargeDiff({
847
+ projectId: input.projectId,
848
+ baseUrl: input.baseUrl,
849
+ token: input.token,
850
+ projectRoot: input.projectRoot,
851
+ syncRoot: input.syncRoot,
852
+ branchName: input.message.branchName,
853
+ sessionId: input.message.sessionId,
854
+ parentNodeId: input.message.parentNodeId,
855
+ reason: input.message.reason,
856
+ manifest: input.manifest
857
+ });
858
+ case "pull_branch":
859
+ return pullBranch({
860
+ projectId: input.projectId,
861
+ baseUrl: input.baseUrl,
862
+ token: input.token,
863
+ projectRoot: input.projectRoot,
864
+ syncRoot: input.syncRoot,
865
+ branchName: input.message.branchName,
866
+ commitHash: input.message.commitHash,
867
+ manifest: input.manifest
868
+ });
869
+ }
870
+ }
871
+ async function executeCommand(input) {
872
+ const startedAt = Date.now();
873
+ let timeout;
874
+ try {
875
+ const branchPath = ensureBranchWorkspace({
876
+ projectId: input.projectId,
877
+ baseUrl: input.baseUrl,
878
+ token: input.token,
879
+ projectRoot: input.projectRoot,
880
+ syncRoot: input.syncRoot,
881
+ branchName: input.message.branchName,
882
+ manifest: input.manifest
883
+ });
884
+ const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
885
+ const subprocess = Bun.spawn(input.message.argv, {
886
+ cwd,
887
+ stdout: "pipe",
888
+ stderr: "pipe",
889
+ env: {
890
+ ...process.env,
891
+ ...input.message.env ?? {}
892
+ }
893
+ });
894
+ activeProcesses.set(input.message.runId, { process: subprocess, command: input.message.argv });
895
+ if (input.message.timeoutMs) {
896
+ timeout = setTimeout(() => {
897
+ subprocess.kill("SIGTERM");
898
+ }, input.message.timeoutMs);
899
+ }
900
+ const [stdout, stderr, exitCode] = await Promise.all([
901
+ collectStream(subprocess.stdout),
902
+ collectStream(subprocess.stderr),
903
+ subprocess.exited
904
+ ]);
905
+ return {
906
+ type: "exec_result",
907
+ requestId: input.message.requestId,
908
+ stdout,
909
+ stderr,
910
+ exitCode,
911
+ durationMs: Date.now() - startedAt
912
+ };
913
+ } catch (error) {
914
+ return {
915
+ type: "exec_result",
916
+ requestId: input.message.requestId,
917
+ stdout: "",
918
+ stderr: "",
919
+ exitCode: 1,
920
+ durationMs: Date.now() - startedAt,
921
+ error: error instanceof Error ? error.message : String(error)
922
+ };
923
+ } finally {
924
+ if (timeout) {
925
+ clearTimeout(timeout);
926
+ }
927
+ activeProcesses.delete(input.message.runId);
928
+ }
929
+ }
930
+ function sendWorkerMessage(ws, message) {
931
+ ws.send(JSON.stringify(message));
932
+ }
933
+ const PTY_BRIDGE_SCRIPT = String.raw`
934
+ const readline = require("node:readline");
935
+ const nodePty = require("node-pty");
936
+
937
+ let ptyProcess = null;
938
+
939
+ function send(message, callback) {
940
+ process.stdout.write(JSON.stringify(message) + "\n", callback);
941
+ }
942
+
943
+ function decode(data) {
944
+ return Buffer.from(data, "base64").toString("utf8");
945
+ }
946
+
947
+ const rl = readline.createInterface({ input: process.stdin });
948
+
949
+ rl.on("line", (line) => {
950
+ let message;
951
+ try {
952
+ message = JSON.parse(line);
953
+ } catch (error) {
954
+ send({ type: "error", error: error instanceof Error ? error.message : String(error) });
955
+ return;
956
+ }
957
+
958
+ try {
959
+ if (message.type === "open") {
960
+ ptyProcess = nodePty.spawn(message.file, message.args, message.options);
961
+ ptyProcess.onData((data) => {
962
+ send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
963
+ });
964
+ ptyProcess.onExit((event) => {
965
+ send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
966
+ });
967
+ send({ type: "opened" });
968
+ return;
969
+ }
970
+
971
+ if (!ptyProcess) {
972
+ send({ type: "error", error: "PTY has not been opened" });
973
+ return;
974
+ }
975
+
976
+ if (message.type === "input") {
977
+ ptyProcess.write(decode(message.data));
978
+ return;
979
+ }
980
+
981
+ if (message.type === "resize") {
982
+ ptyProcess.resize(message.cols, message.rows);
983
+ return;
984
+ }
985
+
986
+ if (message.type === "close") {
987
+ ptyProcess.kill();
988
+ }
989
+ } catch (error) {
990
+ send({ type: "error", error: error instanceof Error ? error.message : String(error) });
991
+ }
992
+ });
993
+ `;
994
+ function nodePtyModulePaths() {
995
+ const candidates = [];
996
+ const entrypoint = process.argv[1];
997
+ if (entrypoint) {
998
+ try {
999
+ let current = import_node_path.default.dirname(import_node_fs.default.realpathSync(entrypoint));
1000
+ for (let index = 0; index < 8; index += 1) {
1001
+ candidates.push(import_node_path.default.join(current, "node_modules"));
1002
+ const parent = import_node_path.default.dirname(current);
1003
+ if (parent === current) {
1004
+ break;
1005
+ }
1006
+ current = parent;
1007
+ }
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ candidates.push(import_node_path.default.join(process.cwd(), "node_modules"), "/usr/local/lib/node_modules", "/usr/lib/node_modules");
1012
+ if (process.env.NODE_PATH) {
1013
+ candidates.push(...process.env.NODE_PATH.split(import_node_path.default.delimiter));
1014
+ }
1015
+ return [...new Set(candidates.filter(Boolean))].join(import_node_path.default.delimiter);
1016
+ }
1017
+ function createNodePtyBridge(options) {
1018
+ const child = (0, import_node_child_process.spawn)("node", ["-e", PTY_BRIDGE_SCRIPT], {
1019
+ env: {
1020
+ ...process.env,
1021
+ NODE_PATH: nodePtyModulePaths()
1022
+ }
1023
+ });
1024
+ let lineBuffer = "";
1025
+ let emittedTerminalEvent = false;
1026
+ let stderr = "";
1027
+ const sendToChild = (message) => {
1028
+ if (child.stdin.writable) {
1029
+ child.stdin.write(`${JSON.stringify(message)}
1030
+ `);
1031
+ }
1032
+ };
1033
+ const handleEvent = (event) => {
1034
+ if (event.type === "opened") {
1035
+ options.onOpened();
1036
+ return;
1037
+ }
1038
+ if (event.type === "output") {
1039
+ options.onOutput(Buffer.from(event.data, "base64").toString("utf8"));
1040
+ return;
1041
+ }
1042
+ if (event.type === "exit") {
1043
+ emittedTerminalEvent = true;
1044
+ options.onExit({ exitCode: event.exitCode, signal: event.signal });
1045
+ return;
1046
+ }
1047
+ if (event.type === "error") {
1048
+ emittedTerminalEvent = true;
1049
+ options.onError(new Error(event.error));
1050
+ }
1051
+ };
1052
+ child.stdout.setEncoding("utf8");
1053
+ child.stdout.on("data", (chunk) => {
1054
+ lineBuffer += chunk;
1055
+ let newlineIndex = lineBuffer.indexOf("\n");
1056
+ while (newlineIndex !== -1) {
1057
+ const line = lineBuffer.slice(0, newlineIndex).trim();
1058
+ lineBuffer = lineBuffer.slice(newlineIndex + 1);
1059
+ if (line) {
1060
+ try {
1061
+ handleEvent(JSON.parse(line));
1062
+ } catch (error) {
1063
+ emittedTerminalEvent = true;
1064
+ options.onError(new Error(error instanceof Error ? error.message : String(error)));
1065
+ }
1066
+ }
1067
+ newlineIndex = lineBuffer.indexOf("\n");
1068
+ }
1069
+ });
1070
+ child.stderr.setEncoding("utf8");
1071
+ child.stderr.on("data", (chunk) => {
1072
+ stderr += chunk;
1073
+ process.stderr.write(`[r5d-worker] pty helper: ${chunk}`);
1074
+ });
1075
+ child.on("error", (error) => {
1076
+ emittedTerminalEvent = true;
1077
+ options.onError(error);
1078
+ });
1079
+ child.on("exit", (code, signal) => {
1080
+ if (emittedTerminalEvent) {
1081
+ return;
1082
+ }
1083
+ const detail = stderr.trim() || `node pty helper exited with code ${code ?? "unknown"}${signal ? ` signal ${signal}` : ""}`;
1084
+ options.onError(new Error(detail));
1085
+ });
1086
+ sendToChild({
1087
+ type: "open",
1088
+ file: options.file,
1089
+ args: options.args,
1090
+ options: options.ptyOptions
1091
+ });
1092
+ return {
1093
+ write(data) {
1094
+ sendToChild({ type: "input", data: Buffer.from(data, "utf8").toString("base64") });
1095
+ },
1096
+ resize(cols, rows) {
1097
+ sendToChild({ type: "resize", cols, rows });
1098
+ },
1099
+ kill() {
1100
+ sendToChild({ type: "close" });
1101
+ setTimeout(() => {
1102
+ if (!child.killed) {
1103
+ child.kill();
1104
+ }
1105
+ }, 500);
1106
+ }
1107
+ };
1108
+ }
1109
+ function resolveHostShell() {
1110
+ if (process.platform === "win32") {
1111
+ return { file: process.env.COMSPEC || "powershell.exe", args: [] };
1112
+ }
1113
+ return { file: process.env.SHELL || "/bin/bash", args: ["-l"] };
1114
+ }
1115
+ function openPty(input) {
1116
+ try {
1117
+ const branchPath = ensureBranchWorkspace({
1118
+ projectId: input.projectId,
1119
+ baseUrl: input.baseUrl,
1120
+ token: input.token,
1121
+ projectRoot: input.projectRoot,
1122
+ syncRoot: input.syncRoot,
1123
+ branchName: input.message.branchName,
1124
+ manifest: input.manifest
1125
+ });
1126
+ const shell = resolveHostShell();
1127
+ const ptyProcess = createNodePtyBridge({
1128
+ file: shell.file,
1129
+ args: shell.args,
1130
+ ptyOptions: {
1131
+ name: "xterm-256color",
1132
+ cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
1133
+ rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
1134
+ cwd: branchPath.branchPath,
1135
+ env: {
1136
+ ...process.env,
1137
+ ...input.message.env ?? {},
1138
+ BUILD_IT_NOW_PROJECT_ID: input.projectId,
1139
+ BUILD_IT_NOW_BRANCH_NAME: input.message.branchName
1140
+ }
1141
+ },
1142
+ onOpened: () => {
1143
+ sendWorkerMessage(input.ws, {
1144
+ type: "pty_opened",
1145
+ requestId: input.message.requestId,
1146
+ ptyId: input.message.ptyId
1147
+ });
1148
+ },
1149
+ onOutput: (data) => {
1150
+ sendWorkerMessage(input.ws, {
1151
+ type: "pty_output",
1152
+ ptyId: input.message.ptyId,
1153
+ data
1154
+ });
1155
+ },
1156
+ onExit: (event) => {
1157
+ activePtys.delete(input.message.ptyId);
1158
+ sendWorkerMessage(input.ws, {
1159
+ type: "pty_exit",
1160
+ ptyId: input.message.ptyId,
1161
+ exitCode: event.exitCode,
1162
+ signal: event.signal
1163
+ });
1164
+ },
1165
+ onError: (error) => {
1166
+ activePtys.delete(input.message.ptyId);
1167
+ sendWorkerMessage(input.ws, {
1168
+ type: "pty_error",
1169
+ requestId: input.message.requestId,
1170
+ ptyId: input.message.ptyId,
1171
+ error: error.message
1172
+ });
1173
+ }
1174
+ });
1175
+ activePtys.set(input.message.ptyId, ptyProcess);
1176
+ } catch (error) {
1177
+ sendWorkerMessage(input.ws, {
1178
+ type: "pty_error",
1179
+ requestId: input.message.requestId,
1180
+ ptyId: input.message.ptyId,
1181
+ error: error instanceof Error ? error.message : String(error)
1182
+ });
1183
+ }
1184
+ }
1185
+ function writePty(ws, message) {
1186
+ const ptyProcess = activePtys.get(message.ptyId);
1187
+ if (!ptyProcess) {
1188
+ sendWorkerMessage(ws, {
1189
+ type: "pty_error",
1190
+ ptyId: message.ptyId,
1191
+ error: "Shell is not open"
1192
+ });
1193
+ return;
1194
+ }
1195
+ ptyProcess.write(message.data);
1196
+ }
1197
+ function resizePty(message) {
1198
+ const ptyProcess = activePtys.get(message.ptyId);
1199
+ if (!ptyProcess) {
1200
+ return;
1201
+ }
1202
+ ptyProcess.resize(
1203
+ Math.max(1, Math.min(Math.floor(message.cols || 80), 500)),
1204
+ Math.max(1, Math.min(Math.floor(message.rows || 24), 500))
1205
+ );
1206
+ }
1207
+ function closePty(message) {
1208
+ const ptyProcess = activePtys.get(message.ptyId);
1209
+ if (!ptyProcess) {
1210
+ return;
1211
+ }
1212
+ activePtys.delete(message.ptyId);
1213
+ ptyProcess.kill();
1214
+ }
1215
+ function closeAllPtys() {
1216
+ for (const ptyProcess of activePtys.values()) {
1217
+ ptyProcess.kill();
1218
+ }
1219
+ activePtys.clear();
1220
+ }
1221
+ function websocketUrl(baseUrl, label) {
1222
+ const url = new URL("/worker/ws", baseUrl);
1223
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
1224
+ url.searchParams.set("label", label);
1225
+ return url.toString();
1226
+ }
1227
+ function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
1228
+ return import_node_path.default.join(projectsRoot, manifestByProjectId.get(projectId)?.repoSlug ?? projectId);
1229
+ }
1230
+ async function startWorker(options) {
1231
+ const config = readConfig(options.configPath);
1232
+ const { baseUrl, token } = resolveCredentials(options, config);
1233
+ const label = requireValue(options.label, "Missing required --label <label>");
1234
+ validateLabel(label);
1235
+ const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
1236
+ const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
1237
+ process.stdout.write(`[r5d-worker] label: ${label}
1238
+ `);
1239
+ process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
1240
+ `);
1241
+ process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
1242
+ `);
1243
+ process.stdout.write(`[r5d-worker] server: ${baseUrl}
1244
+ `);
1245
+ runGit(["--version"]);
1246
+ await verifyBinctlAuth(baseUrl, token);
1247
+ import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
1248
+ import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
1249
+ const manifestByProjectId = /* @__PURE__ */ new Map();
1250
+ const ws = new WebSocket(websocketUrl(baseUrl, label), {
1251
+ headers: {
1252
+ Authorization: `Bearer ${token}`
1253
+ }
1254
+ });
1255
+ ws.addEventListener("open", () => {
1256
+ const hello = {
1257
+ type: "hello",
1258
+ hostInfo: {
1259
+ hostname: (0, import_node_os2.hostname)(),
1260
+ platform: process.platform,
1261
+ arch: process.arch,
1262
+ pid: process.pid,
1263
+ version: "0.1.0",
1264
+ projectRoot: projectsRoot
1265
+ }
1266
+ };
1267
+ ws.send(JSON.stringify(hello));
1268
+ process.stdout.write("[r5d-worker] connected\n");
1269
+ });
1270
+ ws.addEventListener("message", (event) => {
1271
+ void (async () => {
1272
+ const message = JSON.parse(String(event.data));
1273
+ if (message.type === "connected") {
1274
+ return;
1275
+ }
1276
+ if (message.type === "project_manifest") {
1277
+ manifestByProjectId.clear();
1278
+ for (const project of message.projects) {
1279
+ manifestByProjectId.set(project.projectId, project);
1280
+ }
1281
+ process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
1282
+ `);
1283
+ for (const project of message.projects) {
1284
+ const projectRoot = projectRootFor(projectsRoot, project.projectId, manifestByProjectId);
1285
+ const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
1286
+ for (const branchName of branches) {
1287
+ try {
1288
+ await withBranchLock(project.projectId, branchName, async () => {
1289
+ ensureBranchWorkspace({
1290
+ projectId: project.projectId,
1291
+ baseUrl,
1292
+ token,
1293
+ projectRoot,
1294
+ syncRoot,
1295
+ branchName,
1296
+ manifest: project
1297
+ });
1298
+ });
1299
+ } catch (error) {
1300
+ process.stderr.write(
1301
+ `[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
1302
+ `
1303
+ );
1304
+ }
1305
+ }
1306
+ }
1307
+ return;
1308
+ }
1309
+ if (message.type === "ping") {
1310
+ ws.send(JSON.stringify({ type: "pong" }));
1311
+ return;
1312
+ }
1313
+ if (message.type === "cancel") {
1314
+ const active = activeProcesses.get(message.runId);
1315
+ if (active) {
1316
+ active.process.kill("SIGTERM");
1317
+ }
1318
+ ws.send(
1319
+ JSON.stringify({
1320
+ type: "cancel_result",
1321
+ requestId: message.requestId,
1322
+ runId: message.runId,
1323
+ cancelled: !!active,
1324
+ message: active ? `Sent SIGTERM to ${active.command.join(" ")}` : "No active command with that run id"
1325
+ })
1326
+ );
1327
+ return;
1328
+ }
1329
+ if (message.type === "pty_open") {
1330
+ const manifest = manifestByProjectId.get(message.projectId);
1331
+ const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
1332
+ process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
1333
+ `);
1334
+ openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
1335
+ return;
1336
+ }
1337
+ if (message.type === "pty_input") {
1338
+ writePty(ws, message);
1339
+ return;
1340
+ }
1341
+ if (message.type === "pty_resize") {
1342
+ resizePty(message);
1343
+ return;
1344
+ }
1345
+ if (message.type === "pty_close") {
1346
+ closePty(message);
1347
+ return;
1348
+ }
1349
+ if (message.type === "exec") {
1350
+ const manifest = manifestByProjectId.get(message.projectId);
1351
+ const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
1352
+ process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
1353
+ `);
1354
+ const result = await withBranchLock(
1355
+ message.projectId,
1356
+ message.branchName,
1357
+ () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
1358
+ );
1359
+ ws.send(JSON.stringify(result));
1360
+ return;
1361
+ }
1362
+ 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") {
1363
+ try {
1364
+ const manifest = manifestByProjectId.get(message.projectId);
1365
+ const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
1366
+ const result = await withBranchLock(
1367
+ message.projectId,
1368
+ message.branchName,
1369
+ () => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
1370
+ );
1371
+ ws.send(
1372
+ JSON.stringify({
1373
+ type: "operation_result",
1374
+ requestId: message.requestId,
1375
+ result
1376
+ })
1377
+ );
1378
+ } catch (error) {
1379
+ ws.send(
1380
+ JSON.stringify({
1381
+ type: "operation_result",
1382
+ requestId: message.requestId,
1383
+ error: error instanceof Error ? error.message : String(error)
1384
+ })
1385
+ );
1386
+ }
1387
+ }
1388
+ })().catch((error) => {
1389
+ process.stderr.write(`[r5d-worker] message handling failed: ${error instanceof Error ? error.message : String(error)}
1390
+ `);
1391
+ });
1392
+ });
1393
+ ws.addEventListener("close", (event) => {
1394
+ closeAllPtys();
1395
+ const reason = event.reason ? `: ${event.reason}` : "";
1396
+ process.stderr.write(`[r5d-worker] disconnected (${event.code}${reason})
1397
+ `);
1398
+ process.exit(event.code === 1e3 ? 0 : 1);
1399
+ });
1400
+ ws.addEventListener("error", (event) => {
1401
+ const message = "message" in event && typeof event.message === "string" && event.message.trim() ? `: ${event.message.trim()}` : "";
1402
+ process.stderr.write(`[r5d-worker] websocket error${message}
1403
+ `);
1404
+ });
1405
+ await new Promise(() => {
1406
+ });
1407
+ }
1408
+ async function main() {
1409
+ const parsed = parseArgs(process.argv.slice(2));
1410
+ if (parsed.command === "help") {
1411
+ printHelp();
1412
+ return;
1413
+ }
1414
+ await startWorker(parsed.options);
1415
+ }
1416
+ main().catch((error) => {
1417
+ process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
1418
+ `);
1419
+ process.exit(1);
1420
+ });