@ricsam/r5d-worker 0.0.2 → 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.
package/dist/cjs/main.cjs CHANGED
@@ -27,7 +27,7 @@ var import_node_os = __toESM(require("node:os"), 1);
27
27
  var import_node_path = __toESM(require("node:path"), 1);
28
28
  var import_node_crypto = require("node:crypto");
29
29
  var import_node_os2 = require("node:os");
30
- var nodePty = __toESM(require("node-pty"), 1);
30
+ var import_node_child_process = require("node:child_process");
31
31
  const DEFAULT_BASE_URL = "https://r5d.dev";
32
32
  const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
33
33
  const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
@@ -40,20 +40,24 @@ const branchLocks = /* @__PURE__ */ new Map();
40
40
  function defaultConfigPath() {
41
41
  return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
42
42
  }
43
- function defaultProjectRoot(projectId) {
44
- return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects", projectId);
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");
45
48
  }
46
49
  function printHelp() {
47
50
  process.stdout.write(`Usage:
48
- r5d-worker start <project-id> --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
51
+ r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
49
52
 
50
53
  Options:
51
- --label <label> Required unique worker label for this project, e.g. macos or ec2-build
54
+ --label <label> Required unique worker label for this user, e.g. macos or ec2-build
52
55
  --base-url <url> r5d.dev base URL (default from r5d config, R5D_BASE_URL, or ${DEFAULT_BASE_URL})
53
56
  --token <token> r5d worker token
54
57
  --api-key <key> r5d API key
55
58
  --config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
56
- --project-root <dir> Override checkout root (default ~/.r5d/projects/<project-id>)
59
+ --project-root <dir> Override projects checkout root (default ~/.r5d/projects)
60
+ --sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
57
61
  -h, --help Show this help
58
62
  `);
59
63
  }
@@ -116,6 +120,11 @@ function parseArgs(argv) {
116
120
  options.projectRoot = projectRoot;
117
121
  continue;
118
122
  }
123
+ const syncRoot = readOption("--sync-root");
124
+ if (syncRoot !== void 0) {
125
+ options.syncRoot = syncRoot;
126
+ continue;
127
+ }
119
128
  rest.push(arg);
120
129
  }
121
130
  if (options.help || rest.length === 0) {
@@ -127,7 +136,6 @@ function parseArgs(argv) {
127
136
  }
128
137
  return {
129
138
  command: "start",
130
- projectId: requireValue(rest[1], "Missing project id"),
131
139
  options
132
140
  };
133
141
  }
@@ -205,17 +213,14 @@ function validateBranchName(branchName) {
205
213
  throw new Error(`Invalid branch name from server: ${branchName}`);
206
214
  }
207
215
  }
208
- function gitRemoteUrl(baseUrl, projectId) {
209
- return `${baseUrl}/git/${projectId}.git`;
210
- }
211
- function gitExtraHeaderConfigKey(baseUrl) {
212
- return `http.${normalizeBaseUrl(baseUrl)}/.extraHeader`;
216
+ function gitExtraHeaderConfigKey(extraHeaderUrl) {
217
+ return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
213
218
  }
214
219
  function gitAuthArgs(auth) {
215
220
  if (!auth) {
216
221
  return [];
217
222
  }
218
- return ["-c", `${gitExtraHeaderConfigKey(auth.baseUrl)}=Authorization: Bearer ${auth.token}`];
223
+ return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
219
224
  }
220
225
  function runGit(args, options = {}) {
221
226
  const command = ["git", ...gitAuthArgs(options.auth), ...args];
@@ -243,8 +248,22 @@ function tryGit(args, options = {}) {
243
248
  return false;
244
249
  }
245
250
  }
246
- function getCurrentCommitHash(cwd) {
247
- return runGit(["rev-parse", "HEAD"], { cwd });
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"]);
248
267
  }
249
268
  function getGitBlobHashForContent(content) {
250
269
  const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
@@ -253,6 +272,12 @@ function getGitBlobHashForContent(content) {
253
272
  function hasWorktreeChanges(cwd) {
254
273
  return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
255
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
+ }
256
281
  function assertInsideBranch(branchPath, filePath) {
257
282
  const relative = import_node_path.default.relative(branchPath, filePath);
258
283
  if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
@@ -275,11 +300,11 @@ function resolveProjectFilePath(branchPath, virtualPath) {
275
300
  assertInsideBranch(branchPath, absolutePath);
276
301
  return { absolutePath, virtualPath: `/${repoRelativePath}`, repoRelativePath };
277
302
  }
278
- function branchLockKey(projectRoot, branchName) {
279
- return `${projectRoot}:${branchName}`;
303
+ function branchLockKey(projectId, branchName) {
304
+ return `${projectId}:${branchName}`;
280
305
  }
281
- async function withBranchLock(projectRoot, branchName, fn) {
282
- const key = branchLockKey(projectRoot, branchName);
306
+ async function withBranchLock(projectId, branchName, fn) {
307
+ const key = branchLockKey(projectId, branchName);
283
308
  const previous = branchLocks.get(key) ?? Promise.resolve();
284
309
  let release = () => {
285
310
  };
@@ -300,60 +325,221 @@ async function withBranchLock(projectRoot, branchName, fn) {
300
325
  }
301
326
  }
302
327
  }
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 });
328
+ function resolveRemoteUrl(baseUrl, remoteUrl) {
329
+ if (/^https?:\/\//i.test(remoteUrl)) {
330
+ return remoteUrl;
331
+ }
332
+ return new URL(remoteUrl, baseUrl).toString();
310
333
  }
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 });
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;
318
354
  }
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 });
355
+ }
356
+ function gitAuthForRemote(remoteUrl, authHeader) {
357
+ if (!authHeader) {
358
+ return void 0;
325
359
  }
326
- return mainPath;
360
+ return {
361
+ extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
362
+ header: authHeader
363
+ };
327
364
  }
328
- function ensureBranchWorkspace(input) {
329
- validateBranchName(input.branchName);
330
- const mainPath = ensureMainClone(input);
331
- if (input.branchName === "main") {
332
- return mainPath;
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 });
333
380
  }
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
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
338
454
  });
339
- if (!import_node_fs.default.existsSync(import_node_path.default.join(branchPath, ".git"))) {
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)) {
340
478
  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
- });
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 });
349
483
  }
350
484
  }
351
- configureRepo(branchPath, input);
352
- if (hasRemoteBranch && !hasWorktreeChanges(branchPath)) {
353
- tryGit(["pull", "--ff-only", "build-it-now", input.branchName], { cwd: branchPath });
354
- }
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 });
355
489
  return branchPath;
356
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
+ }
357
543
  function resolveCommandCwd(branchPath, cwd) {
358
544
  if (!cwd) {
359
545
  return branchPath;
@@ -462,8 +648,8 @@ function readImageDimensions(bytes, mediaType) {
462
648
  return dimensions;
463
649
  }
464
650
  async function executeReadFileOperation(input) {
465
- const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
466
- const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
651
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
652
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
467
653
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
468
654
  throw new Error(`File not found: ${resolved.virtualPath}`);
469
655
  }
@@ -477,8 +663,8 @@ async function executeReadFileOperation(input) {
477
663
  };
478
664
  }
479
665
  async function executeCreateFileOperation(input) {
480
- const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
481
- const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
666
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
667
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
482
668
  if (import_node_fs.default.existsSync(resolved.absolutePath)) {
483
669
  throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
484
670
  }
@@ -491,8 +677,8 @@ async function executeCreateFileOperation(input) {
491
677
  };
492
678
  }
493
679
  async function executeEditFileOperation(input) {
494
- const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
495
- const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
680
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
681
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
496
682
  if (input.message.oldString === input.message.newString) {
497
683
  throw new Error("old_string and new_string must be different");
498
684
  }
@@ -521,8 +707,8 @@ async function executeEditFileOperation(input) {
521
707
  };
522
708
  }
523
709
  async function executeViewFileBytesOperation(input) {
524
- const branchPath = ensureOperationBranch({ ...input, branchName: input.message.branchName });
525
- const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
710
+ const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
711
+ const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
526
712
  if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
527
713
  throw new Error(`File not found: ${resolved.virtualPath}`);
528
714
  }
@@ -543,12 +729,12 @@ async function executeViewFileBytesOperation(input) {
543
729
  height: dimensions.height
544
730
  };
545
731
  }
546
- function stagedBlobSizeBytes(cwd) {
547
- const names = runGit(["diff", "--cached", "--name-only", "-z"], { cwd }).split("\0").filter(Boolean);
732
+ function stagedBlobSizeBytes(context) {
733
+ const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
548
734
  let total = 0;
549
735
  for (const name of names) {
550
736
  try {
551
- const sizeText = runGit(["cat-file", "-s", `:${name}`], { cwd });
737
+ const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
552
738
  total += Number.parseInt(sizeText, 10) || 0;
553
739
  } catch {
554
740
  }
@@ -559,11 +745,11 @@ function stagedBlobSizeBytes(cwd) {
559
745
  return total;
560
746
  }
561
747
  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);
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);
567
753
  if (!hasStagedChanges) {
568
754
  return {
569
755
  type: "sync",
@@ -573,7 +759,7 @@ function syncBranch(input) {
573
759
  status
574
760
  };
575
761
  }
576
- const diffSizeBytes = stagedBlobSizeBytes(branchPath);
762
+ const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
577
763
  if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
578
764
  return {
579
765
  type: "sync",
@@ -591,15 +777,15 @@ function syncBranch(input) {
591
777
  parentNodeId: input.parentNodeId,
592
778
  ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
593
779
  });
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 });
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}`]);
597
783
  return {
598
784
  type: "sync",
599
785
  changed: true,
600
786
  commitHash,
601
787
  diffSizeBytes,
602
- status: runGit(["status", "--porcelain"], { cwd: branchPath }),
788
+ status: getInternalStatus(workspace.internalGit),
603
789
  trigger: input.trigger
604
790
  };
605
791
  }
@@ -623,14 +809,14 @@ function confirmLargeDiff(input) {
623
809
  };
624
810
  }
625
811
  function pullBranch(input) {
626
- const branchPath = ensureOperationBranch(input);
627
- runGit(["fetch", "build-it-now", "--prune"], { cwd: branchPath });
812
+ const workspace = ensureOperationBranch(input);
813
+ runInternalGit(workspace.internalGit, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
628
814
  const target = input.commitHash ?? `build-it-now/${input.branchName}`;
629
- runGit(["reset", "--hard", target], { cwd: branchPath });
630
- runGit(["clean", "-fd"], { cwd: branchPath });
815
+ runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
816
+ runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
631
817
  return {
632
818
  type: "pull_branch",
633
- commitHash: getCurrentCommitHash(branchPath)
819
+ commitHash: getInternalCommitHash(workspace.internalGit)
634
820
  };
635
821
  }
636
822
  async function executeOperation(input) {
@@ -649,10 +835,12 @@ async function executeOperation(input) {
649
835
  baseUrl: input.baseUrl,
650
836
  token: input.token,
651
837
  projectRoot: input.projectRoot,
838
+ syncRoot: input.syncRoot,
652
839
  branchName: input.message.branchName,
653
840
  sessionId: input.message.sessionId,
654
841
  parentNodeId: input.message.parentNodeId,
655
- trigger: input.message.trigger
842
+ trigger: input.message.trigger,
843
+ manifest: input.manifest
656
844
  });
657
845
  case "confirm_large_diff":
658
846
  return confirmLargeDiff({
@@ -660,10 +848,12 @@ async function executeOperation(input) {
660
848
  baseUrl: input.baseUrl,
661
849
  token: input.token,
662
850
  projectRoot: input.projectRoot,
851
+ syncRoot: input.syncRoot,
663
852
  branchName: input.message.branchName,
664
853
  sessionId: input.message.sessionId,
665
854
  parentNodeId: input.message.parentNodeId,
666
- reason: input.message.reason
855
+ reason: input.message.reason,
856
+ manifest: input.manifest
667
857
  });
668
858
  case "pull_branch":
669
859
  return pullBranch({
@@ -671,8 +861,10 @@ async function executeOperation(input) {
671
861
  baseUrl: input.baseUrl,
672
862
  token: input.token,
673
863
  projectRoot: input.projectRoot,
864
+ syncRoot: input.syncRoot,
674
865
  branchName: input.message.branchName,
675
- commitHash: input.message.commitHash
866
+ commitHash: input.message.commitHash,
867
+ manifest: input.manifest
676
868
  });
677
869
  }
678
870
  }
@@ -685,9 +877,11 @@ async function executeCommand(input) {
685
877
  baseUrl: input.baseUrl,
686
878
  token: input.token,
687
879
  projectRoot: input.projectRoot,
688
- branchName: input.message.branchName
880
+ syncRoot: input.syncRoot,
881
+ branchName: input.message.branchName,
882
+ manifest: input.manifest
689
883
  });
690
- const cwd = resolveCommandCwd(branchPath, input.message.cwd);
884
+ const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
691
885
  const subprocess = Bun.spawn(input.message.argv, {
692
886
  cwd,
693
887
  stdout: "pipe",
@@ -736,6 +930,182 @@ async function executeCommand(input) {
736
930
  function sendWorkerMessage(ws, message) {
737
931
  ws.send(JSON.stringify(message));
738
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
+ }
739
1109
  function resolveHostShell() {
740
1110
  if (process.platform === "win32") {
741
1111
  return { file: process.env.COMSPEC || "powershell.exe", args: [] };
@@ -749,43 +1119,60 @@ function openPty(input) {
749
1119
  baseUrl: input.baseUrl,
750
1120
  token: input.token,
751
1121
  projectRoot: input.projectRoot,
752
- branchName: input.message.branchName
1122
+ syncRoot: input.syncRoot,
1123
+ branchName: input.message.branchName,
1124
+ manifest: input.manifest
753
1125
  });
754
1126
  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
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
+ });
765
1173
  }
766
1174
  });
767
1175
  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
1176
  } catch (error) {
790
1177
  sendWorkerMessage(input.ws, {
791
1178
  type: "pty_error",
@@ -831,32 +1218,36 @@ function closeAllPtys() {
831
1218
  }
832
1219
  activePtys.clear();
833
1220
  }
834
- function websocketUrl(baseUrl, projectId, label) {
1221
+ function websocketUrl(baseUrl, label) {
835
1222
  const url = new URL("/worker/ws", baseUrl);
836
1223
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
837
- url.searchParams.set("projectId", projectId);
838
1224
  url.searchParams.set("label", label);
839
1225
  return url.toString();
840
1226
  }
841
- async function startWorker(projectId, options) {
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) {
842
1231
  const config = readConfig(options.configPath);
843
1232
  const { baseUrl, token } = resolveCredentials(options, config);
844
1233
  const label = requireValue(options.label, "Missing required --label <label>");
845
1234
  validateLabel(label);
846
- const projectRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectRoot(projectId));
847
- process.stdout.write(`[r5d-worker] project: ${projectId}
848
- `);
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());
849
1237
  process.stdout.write(`[r5d-worker] label: ${label}
850
1238
  `);
851
- process.stdout.write(`[r5d-worker] root: ${projectRoot}
1239
+ process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
1240
+ `);
1241
+ process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
852
1242
  `);
853
1243
  process.stdout.write(`[r5d-worker] server: ${baseUrl}
854
1244
  `);
855
1245
  runGit(["--version"]);
856
1246
  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), {
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), {
860
1251
  headers: {
861
1252
  Authorization: `Bearer ${token}`
862
1253
  }
@@ -870,7 +1261,7 @@ async function startWorker(projectId, options) {
870
1261
  arch: process.arch,
871
1262
  pid: process.pid,
872
1263
  version: "0.1.0",
873
- projectRoot
1264
+ projectRoot: projectsRoot
874
1265
  }
875
1266
  };
876
1267
  ws.send(JSON.stringify(hello));
@@ -882,6 +1273,39 @@ async function startWorker(projectId, options) {
882
1273
  if (message.type === "connected") {
883
1274
  return;
884
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
+ }
885
1309
  if (message.type === "ping") {
886
1310
  ws.send(JSON.stringify({ type: "pong" }));
887
1311
  return;
@@ -903,9 +1327,11 @@ async function startWorker(projectId, options) {
903
1327
  return;
904
1328
  }
905
1329
  if (message.type === "pty_open") {
906
- process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.branchName}
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}
907
1333
  `);
908
- openPty({ ws, message, projectId, baseUrl, token, projectRoot });
1334
+ openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
909
1335
  return;
910
1336
  }
911
1337
  if (message.type === "pty_input") {
@@ -921,22 +1347,26 @@ async function startWorker(projectId, options) {
921
1347
  return;
922
1348
  }
923
1349
  if (message.type === "exec") {
1350
+ const manifest = manifestByProjectId.get(message.projectId);
1351
+ const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
924
1352
  process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
925
1353
  `);
926
1354
  const result = await withBranchLock(
927
- projectRoot,
1355
+ message.projectId,
928
1356
  message.branchName,
929
- () => executeCommand({ message, projectId, baseUrl, token, projectRoot })
1357
+ () => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
930
1358
  );
931
1359
  ws.send(JSON.stringify(result));
932
1360
  return;
933
1361
  }
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") {
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") {
935
1363
  try {
1364
+ const manifest = manifestByProjectId.get(message.projectId);
1365
+ const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
936
1366
  const result = await withBranchLock(
937
- projectRoot,
1367
+ message.projectId,
938
1368
  message.branchName,
939
- () => executeOperation({ message, projectId, baseUrl, token, projectRoot })
1369
+ () => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
940
1370
  );
941
1371
  ws.send(
942
1372
  JSON.stringify({
@@ -981,7 +1411,7 @@ async function main() {
981
1411
  printHelp();
982
1412
  return;
983
1413
  }
984
- await startWorker(parsed.projectId, parsed.options);
1414
+ await startWorker(parsed.options);
985
1415
  }
986
1416
  main().catch((error) => {
987
1417
  process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}