@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 +574 -144
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +574 -144
- package/dist/mjs/package.json +1 -1
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import { spawn as spawnChildProcess } from "node:child_process";
|
|
8
8
|
const DEFAULT_BASE_URL = "https://r5d.dev";
|
|
9
9
|
const LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/;
|
|
10
10
|
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]$|^[A-Za-z0-9]$/;
|
|
@@ -17,20 +17,24 @@ const branchLocks = /* @__PURE__ */ new Map();
|
|
|
17
17
|
function defaultConfigPath() {
|
|
18
18
|
return path.join(os.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
19
19
|
}
|
|
20
|
-
function
|
|
21
|
-
return path.join(os.homedir(), ".r5d", "projects"
|
|
20
|
+
function defaultProjectsRoot() {
|
|
21
|
+
return path.join(os.homedir(), ".r5d", "projects");
|
|
22
|
+
}
|
|
23
|
+
function defaultSyncRoot() {
|
|
24
|
+
return path.join(os.homedir(), ".r5d", "sync");
|
|
22
25
|
}
|
|
23
26
|
function printHelp() {
|
|
24
27
|
process.stdout.write(`Usage:
|
|
25
|
-
r5d-worker start
|
|
28
|
+
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
26
29
|
|
|
27
30
|
Options:
|
|
28
|
-
--label <label> Required unique worker label for this
|
|
31
|
+
--label <label> Required unique worker label for this user, e.g. macos or ec2-build
|
|
29
32
|
--base-url <url> r5d.dev base URL (default from r5d config, R5D_BASE_URL, or ${DEFAULT_BASE_URL})
|
|
30
33
|
--token <token> r5d worker token
|
|
31
34
|
--api-key <key> r5d API key
|
|
32
35
|
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
33
|
-
--project-root <dir> Override checkout root (default ~/.r5d/projects
|
|
36
|
+
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
37
|
+
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
34
38
|
-h, --help Show this help
|
|
35
39
|
`);
|
|
36
40
|
}
|
|
@@ -93,6 +97,11 @@ function parseArgs(argv) {
|
|
|
93
97
|
options.projectRoot = projectRoot;
|
|
94
98
|
continue;
|
|
95
99
|
}
|
|
100
|
+
const syncRoot = readOption("--sync-root");
|
|
101
|
+
if (syncRoot !== void 0) {
|
|
102
|
+
options.syncRoot = syncRoot;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
96
105
|
rest.push(arg);
|
|
97
106
|
}
|
|
98
107
|
if (options.help || rest.length === 0) {
|
|
@@ -104,7 +113,6 @@ function parseArgs(argv) {
|
|
|
104
113
|
}
|
|
105
114
|
return {
|
|
106
115
|
command: "start",
|
|
107
|
-
projectId: requireValue(rest[1], "Missing project id"),
|
|
108
116
|
options
|
|
109
117
|
};
|
|
110
118
|
}
|
|
@@ -182,17 +190,14 @@ function validateBranchName(branchName) {
|
|
|
182
190
|
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
183
191
|
}
|
|
184
192
|
}
|
|
185
|
-
function
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
function gitExtraHeaderConfigKey(baseUrl) {
|
|
189
|
-
return `http.${normalizeBaseUrl(baseUrl)}/.extraHeader`;
|
|
193
|
+
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
194
|
+
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
190
195
|
}
|
|
191
196
|
function gitAuthArgs(auth) {
|
|
192
197
|
if (!auth) {
|
|
193
198
|
return [];
|
|
194
199
|
}
|
|
195
|
-
return ["-c", `${gitExtraHeaderConfigKey(auth.
|
|
200
|
+
return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
|
|
196
201
|
}
|
|
197
202
|
function runGit(args, options = {}) {
|
|
198
203
|
const command = ["git", ...gitAuthArgs(options.auth), ...args];
|
|
@@ -220,8 +225,22 @@ function tryGit(args, options = {}) {
|
|
|
220
225
|
return false;
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
|
-
function
|
|
224
|
-
return runGit(["
|
|
228
|
+
function runInternalGit(context, args) {
|
|
229
|
+
return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
|
|
230
|
+
}
|
|
231
|
+
function runInternalGitDir(context, args) {
|
|
232
|
+
return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
|
|
233
|
+
}
|
|
234
|
+
function tryInternalGit(context, args) {
|
|
235
|
+
try {
|
|
236
|
+
runInternalGit(context, args);
|
|
237
|
+
return true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function getInternalCommitHash(context) {
|
|
243
|
+
return runInternalGit(context, ["rev-parse", "HEAD"]);
|
|
225
244
|
}
|
|
226
245
|
function getGitBlobHashForContent(content) {
|
|
227
246
|
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
@@ -230,6 +249,12 @@ function getGitBlobHashForContent(content) {
|
|
|
230
249
|
function hasWorktreeChanges(cwd) {
|
|
231
250
|
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
232
251
|
}
|
|
252
|
+
function getInternalStatus(context) {
|
|
253
|
+
return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
|
|
254
|
+
}
|
|
255
|
+
function hasInternalWorktreeChanges(context) {
|
|
256
|
+
return getInternalStatus(context).trim().length > 0;
|
|
257
|
+
}
|
|
233
258
|
function assertInsideBranch(branchPath, filePath) {
|
|
234
259
|
const relative = path.relative(branchPath, filePath);
|
|
235
260
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
@@ -252,11 +277,11 @@ function resolveProjectFilePath(branchPath, virtualPath) {
|
|
|
252
277
|
assertInsideBranch(branchPath, absolutePath);
|
|
253
278
|
return { absolutePath, virtualPath: `/${repoRelativePath}`, repoRelativePath };
|
|
254
279
|
}
|
|
255
|
-
function branchLockKey(
|
|
256
|
-
return `${
|
|
280
|
+
function branchLockKey(projectId, branchName) {
|
|
281
|
+
return `${projectId}:${branchName}`;
|
|
257
282
|
}
|
|
258
|
-
async function withBranchLock(
|
|
259
|
-
const key = branchLockKey(
|
|
283
|
+
async function withBranchLock(projectId, branchName, fn) {
|
|
284
|
+
const key = branchLockKey(projectId, branchName);
|
|
260
285
|
const previous = branchLocks.get(key) ?? Promise.resolve();
|
|
261
286
|
let release = () => {
|
|
262
287
|
};
|
|
@@ -277,60 +302,221 @@ async function withBranchLock(projectRoot, branchName, fn) {
|
|
|
277
302
|
}
|
|
278
303
|
}
|
|
279
304
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
runGit(["config", "user.name", "r5d.dev Worker"], { cwd: pathName });
|
|
286
|
-
runGit(["config", "user.email", "worker@r5d.dev"], { cwd: pathName });
|
|
305
|
+
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
306
|
+
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
307
|
+
return remoteUrl;
|
|
308
|
+
}
|
|
309
|
+
return new URL(remoteUrl, baseUrl).toString();
|
|
287
310
|
}
|
|
288
|
-
function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
311
|
+
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
312
|
+
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
313
|
+
}
|
|
314
|
+
function internalGitAuth(baseUrl, token) {
|
|
315
|
+
return {
|
|
316
|
+
extraHeaderUrl: baseUrl,
|
|
317
|
+
header: `Authorization: Bearer ${token}`
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function internalRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
321
|
+
return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
|
|
322
|
+
}
|
|
323
|
+
function githubRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
324
|
+
return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
|
|
325
|
+
}
|
|
326
|
+
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
327
|
+
try {
|
|
328
|
+
return new URL(remoteUrl).origin;
|
|
329
|
+
} catch {
|
|
330
|
+
return remoteUrl;
|
|
295
331
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (!hasWorktreeChanges(mainPath)) {
|
|
301
|
-
tryGit(["pull", "--ff-only", "build-it-now", "main"], { cwd: mainPath });
|
|
332
|
+
}
|
|
333
|
+
function gitAuthForRemote(remoteUrl, authHeader) {
|
|
334
|
+
if (!authHeader) {
|
|
335
|
+
return void 0;
|
|
302
336
|
}
|
|
303
|
-
return
|
|
337
|
+
return {
|
|
338
|
+
extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
|
|
339
|
+
header: authHeader
|
|
340
|
+
};
|
|
304
341
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
342
|
+
function branchGitDirName(branchName) {
|
|
343
|
+
return `${encodeURIComponent(branchName)}.git`;
|
|
344
|
+
}
|
|
345
|
+
function internalGitDirFor(syncRoot, projectId, branchName) {
|
|
346
|
+
return path.join(syncRoot, projectId, branchGitDirName(branchName));
|
|
347
|
+
}
|
|
348
|
+
function hasNormalVisibleGitDir(branchPath) {
|
|
349
|
+
const gitPath = path.join(branchPath, ".git");
|
|
350
|
+
return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
|
|
351
|
+
}
|
|
352
|
+
function ensureOriginRemote(branchPath, remoteUrl) {
|
|
353
|
+
if (tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) {
|
|
354
|
+
runGit(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
|
|
355
|
+
} else {
|
|
356
|
+
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
310
357
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
358
|
+
}
|
|
359
|
+
function shellQuote(value) {
|
|
360
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
361
|
+
}
|
|
362
|
+
function githubCredentialsPath() {
|
|
363
|
+
return path.join(os.homedir(), ".r5d", "github-credentials");
|
|
364
|
+
}
|
|
365
|
+
function decodeBasicAuthHeader(authHeader) {
|
|
366
|
+
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
367
|
+
if (!match) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const decoded = Buffer.from(match[1], "base64").toString("utf8");
|
|
371
|
+
const separatorIndex = decoded.indexOf(":");
|
|
372
|
+
if (separatorIndex <= 0) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
username: decoded.slice(0, separatorIndex),
|
|
377
|
+
password: decoded.slice(separatorIndex + 1)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function writeCredentialStoreEntry(remoteUrl, authHeader) {
|
|
381
|
+
const credentials = decodeBasicAuthHeader(authHeader);
|
|
382
|
+
if (!credentials) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
let remote;
|
|
386
|
+
try {
|
|
387
|
+
remote = new URL(remoteUrl);
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
if (remote.protocol !== "https:") {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const storePath = githubCredentialsPath();
|
|
395
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
396
|
+
const hostKey = `${remote.protocol}//${remote.host}`;
|
|
397
|
+
const existing = fs.existsSync(storePath) ? fs.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
|
|
398
|
+
const next = existing.filter((line) => {
|
|
399
|
+
try {
|
|
400
|
+
const parsed = new URL(line);
|
|
401
|
+
return `${parsed.protocol}//${parsed.host}` !== hostKey;
|
|
402
|
+
} catch {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
const credentialUrl = new URL(hostKey);
|
|
407
|
+
credentialUrl.username = credentials.username;
|
|
408
|
+
credentialUrl.password = credentials.password;
|
|
409
|
+
next.push(credentialUrl.toString());
|
|
410
|
+
fs.writeFileSync(storePath, `${next.join("\n")}
|
|
411
|
+
`, { mode: 384 });
|
|
412
|
+
fs.chmodSync(storePath, 384);
|
|
413
|
+
return storePath;
|
|
414
|
+
}
|
|
415
|
+
function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
|
|
416
|
+
if (!authHeader) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
|
|
420
|
+
if (credentialStore) {
|
|
421
|
+
runGit(["config", "credential.helper", `store --file=${shellQuote(credentialStore)}`], { cwd: branchPath });
|
|
422
|
+
runGit(["config", "credential.useHttpPath", "false"], { cwd: branchPath });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
|
|
426
|
+
}
|
|
427
|
+
function checkoutVisibleBranch(input) {
|
|
428
|
+
const branchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`], { cwd: input.branchPath });
|
|
429
|
+
const remoteBranchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], {
|
|
430
|
+
cwd: input.branchPath
|
|
315
431
|
});
|
|
316
|
-
|
|
432
|
+
const defaultRemoteExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.defaultBranch}`], {
|
|
433
|
+
cwd: input.branchPath
|
|
434
|
+
});
|
|
435
|
+
const clean = !hasWorktreeChanges(input.branchPath);
|
|
436
|
+
if (remoteBranchExists && clean) {
|
|
437
|
+
runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], { cwd: input.branchPath });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (branchExists) {
|
|
441
|
+
runGit(["checkout", input.branchName], { cwd: input.branchPath });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (defaultRemoteExists) {
|
|
445
|
+
runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], { cwd: input.branchPath });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
|
|
449
|
+
}
|
|
450
|
+
function ensureVisibleGitCheckout(input) {
|
|
451
|
+
validateBranchName(input.branchName);
|
|
452
|
+
fs.mkdirSync(input.projectRoot, { recursive: true });
|
|
453
|
+
const branchPath = path.join(input.projectRoot, input.branchName);
|
|
454
|
+
if (!hasNormalVisibleGitDir(branchPath)) {
|
|
317
455
|
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
} else {
|
|
323
|
-
runGit(["worktree", "add", "-b", input.branchName, branchPath, "build-it-now/main"], {
|
|
324
|
-
cwd: mainPath
|
|
325
|
-
});
|
|
456
|
+
fs.mkdirSync(path.dirname(branchPath), { recursive: true });
|
|
457
|
+
if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
|
|
458
|
+
fs.mkdirSync(branchPath, { recursive: true });
|
|
459
|
+
runGit(["init"], { cwd: branchPath });
|
|
326
460
|
}
|
|
327
461
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
462
|
+
ensureOriginRemote(branchPath, input.githubRemoteUrl);
|
|
463
|
+
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
464
|
+
tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
|
|
465
|
+
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
332
466
|
return branchPath;
|
|
333
467
|
}
|
|
468
|
+
function configureInternalRepo(context, remoteUrl) {
|
|
469
|
+
if (tryInternalGit(context, ["remote", "get-url", "build-it-now"])) {
|
|
470
|
+
runInternalGit(context, ["remote", "set-url", "build-it-now", remoteUrl]);
|
|
471
|
+
} else {
|
|
472
|
+
runInternalGit(context, ["remote", "add", "build-it-now", remoteUrl]);
|
|
473
|
+
}
|
|
474
|
+
runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
|
|
475
|
+
runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
|
|
476
|
+
}
|
|
477
|
+
function ensureInternalSyncGit(input) {
|
|
478
|
+
validateBranchName(input.branchName);
|
|
479
|
+
const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
|
|
480
|
+
fs.mkdirSync(path.dirname(gitDir), { recursive: true });
|
|
481
|
+
if (!fs.existsSync(gitDir)) {
|
|
482
|
+
runGit(["init", "--bare", gitDir]);
|
|
483
|
+
}
|
|
484
|
+
const auth = internalGitAuth(input.baseUrl, input.token);
|
|
485
|
+
const context = { gitDir, workTree: input.branchPath, auth };
|
|
486
|
+
const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
487
|
+
configureInternalRepo(context, remoteUrl);
|
|
488
|
+
runInternalGit(context, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
|
|
489
|
+
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/build-it-now/${input.branchName}`]);
|
|
490
|
+
const target = hasRemoteBranch ? `refs/remotes/build-it-now/${input.branchName}` : "refs/remotes/build-it-now/main";
|
|
491
|
+
const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
492
|
+
if (!hasHead || !hasInternalWorktreeChanges(context)) {
|
|
493
|
+
runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
|
|
494
|
+
}
|
|
495
|
+
return context;
|
|
496
|
+
}
|
|
497
|
+
function ensureBranchWorkspace(input) {
|
|
498
|
+
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
499
|
+
const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
500
|
+
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
|
|
501
|
+
const branchPath = ensureVisibleGitCheckout({
|
|
502
|
+
projectRoot: input.projectRoot,
|
|
503
|
+
branchName: input.branchName,
|
|
504
|
+
githubRemoteUrl,
|
|
505
|
+
githubAuth,
|
|
506
|
+
githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
|
|
507
|
+
defaultBranch
|
|
508
|
+
});
|
|
509
|
+
const internalGit = ensureInternalSyncGit({
|
|
510
|
+
projectId: input.projectId,
|
|
511
|
+
baseUrl: input.baseUrl,
|
|
512
|
+
token: input.token,
|
|
513
|
+
syncRoot: input.syncRoot,
|
|
514
|
+
branchPath,
|
|
515
|
+
branchName: input.branchName,
|
|
516
|
+
manifest: input.manifest
|
|
517
|
+
});
|
|
518
|
+
return { branchPath, internalGit };
|
|
519
|
+
}
|
|
334
520
|
function resolveCommandCwd(branchPath, cwd) {
|
|
335
521
|
if (!cwd) {
|
|
336
522
|
return branchPath;
|
|
@@ -439,8 +625,8 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
439
625
|
return dimensions;
|
|
440
626
|
}
|
|
441
627
|
async function executeReadFileOperation(input) {
|
|
442
|
-
const
|
|
443
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
628
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
629
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
444
630
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
445
631
|
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
446
632
|
}
|
|
@@ -454,8 +640,8 @@ async function executeReadFileOperation(input) {
|
|
|
454
640
|
};
|
|
455
641
|
}
|
|
456
642
|
async function executeCreateFileOperation(input) {
|
|
457
|
-
const
|
|
458
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
643
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
644
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
459
645
|
if (fs.existsSync(resolved.absolutePath)) {
|
|
460
646
|
throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
|
|
461
647
|
}
|
|
@@ -468,8 +654,8 @@ async function executeCreateFileOperation(input) {
|
|
|
468
654
|
};
|
|
469
655
|
}
|
|
470
656
|
async function executeEditFileOperation(input) {
|
|
471
|
-
const
|
|
472
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
657
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
658
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
473
659
|
if (input.message.oldString === input.message.newString) {
|
|
474
660
|
throw new Error("old_string and new_string must be different");
|
|
475
661
|
}
|
|
@@ -498,8 +684,8 @@ async function executeEditFileOperation(input) {
|
|
|
498
684
|
};
|
|
499
685
|
}
|
|
500
686
|
async function executeViewFileBytesOperation(input) {
|
|
501
|
-
const
|
|
502
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
687
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
688
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
503
689
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
504
690
|
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
505
691
|
}
|
|
@@ -520,12 +706,12 @@ async function executeViewFileBytesOperation(input) {
|
|
|
520
706
|
height: dimensions.height
|
|
521
707
|
};
|
|
522
708
|
}
|
|
523
|
-
function stagedBlobSizeBytes(
|
|
524
|
-
const names =
|
|
709
|
+
function stagedBlobSizeBytes(context) {
|
|
710
|
+
const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
|
|
525
711
|
let total = 0;
|
|
526
712
|
for (const name of names) {
|
|
527
713
|
try {
|
|
528
|
-
const sizeText =
|
|
714
|
+
const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
|
|
529
715
|
total += Number.parseInt(sizeText, 10) || 0;
|
|
530
716
|
} catch {
|
|
531
717
|
}
|
|
@@ -536,11 +722,11 @@ function stagedBlobSizeBytes(cwd) {
|
|
|
536
722
|
return total;
|
|
537
723
|
}
|
|
538
724
|
function syncBranch(input) {
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
const hasStagedChanges = !
|
|
542
|
-
const status =
|
|
543
|
-
const currentCommit =
|
|
725
|
+
const workspace = ensureOperationBranch(input);
|
|
726
|
+
runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
|
|
727
|
+
const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
|
|
728
|
+
const status = getInternalStatus(workspace.internalGit);
|
|
729
|
+
const currentCommit = getInternalCommitHash(workspace.internalGit);
|
|
544
730
|
if (!hasStagedChanges) {
|
|
545
731
|
return {
|
|
546
732
|
type: "sync",
|
|
@@ -550,7 +736,7 @@ function syncBranch(input) {
|
|
|
550
736
|
status
|
|
551
737
|
};
|
|
552
738
|
}
|
|
553
|
-
const diffSizeBytes = stagedBlobSizeBytes(
|
|
739
|
+
const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
|
|
554
740
|
if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
|
|
555
741
|
return {
|
|
556
742
|
type: "sync",
|
|
@@ -568,15 +754,15 @@ function syncBranch(input) {
|
|
|
568
754
|
parentNodeId: input.parentNodeId,
|
|
569
755
|
...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
|
|
570
756
|
});
|
|
571
|
-
|
|
572
|
-
const commitHash =
|
|
573
|
-
|
|
757
|
+
runInternalGit(workspace.internalGit, ["commit", "-m", message]);
|
|
758
|
+
const commitHash = getInternalCommitHash(workspace.internalGit);
|
|
759
|
+
runInternalGit(workspace.internalGit, ["push", "build-it-now", `HEAD:refs/heads/${input.branchName}`]);
|
|
574
760
|
return {
|
|
575
761
|
type: "sync",
|
|
576
762
|
changed: true,
|
|
577
763
|
commitHash,
|
|
578
764
|
diffSizeBytes,
|
|
579
|
-
status:
|
|
765
|
+
status: getInternalStatus(workspace.internalGit),
|
|
580
766
|
trigger: input.trigger
|
|
581
767
|
};
|
|
582
768
|
}
|
|
@@ -600,14 +786,14 @@ function confirmLargeDiff(input) {
|
|
|
600
786
|
};
|
|
601
787
|
}
|
|
602
788
|
function pullBranch(input) {
|
|
603
|
-
const
|
|
604
|
-
|
|
789
|
+
const workspace = ensureOperationBranch(input);
|
|
790
|
+
runInternalGit(workspace.internalGit, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
|
|
605
791
|
const target = input.commitHash ?? `build-it-now/${input.branchName}`;
|
|
606
|
-
|
|
607
|
-
|
|
792
|
+
runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
|
|
793
|
+
runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
608
794
|
return {
|
|
609
795
|
type: "pull_branch",
|
|
610
|
-
commitHash:
|
|
796
|
+
commitHash: getInternalCommitHash(workspace.internalGit)
|
|
611
797
|
};
|
|
612
798
|
}
|
|
613
799
|
async function executeOperation(input) {
|
|
@@ -626,10 +812,12 @@ async function executeOperation(input) {
|
|
|
626
812
|
baseUrl: input.baseUrl,
|
|
627
813
|
token: input.token,
|
|
628
814
|
projectRoot: input.projectRoot,
|
|
815
|
+
syncRoot: input.syncRoot,
|
|
629
816
|
branchName: input.message.branchName,
|
|
630
817
|
sessionId: input.message.sessionId,
|
|
631
818
|
parentNodeId: input.message.parentNodeId,
|
|
632
|
-
trigger: input.message.trigger
|
|
819
|
+
trigger: input.message.trigger,
|
|
820
|
+
manifest: input.manifest
|
|
633
821
|
});
|
|
634
822
|
case "confirm_large_diff":
|
|
635
823
|
return confirmLargeDiff({
|
|
@@ -637,10 +825,12 @@ async function executeOperation(input) {
|
|
|
637
825
|
baseUrl: input.baseUrl,
|
|
638
826
|
token: input.token,
|
|
639
827
|
projectRoot: input.projectRoot,
|
|
828
|
+
syncRoot: input.syncRoot,
|
|
640
829
|
branchName: input.message.branchName,
|
|
641
830
|
sessionId: input.message.sessionId,
|
|
642
831
|
parentNodeId: input.message.parentNodeId,
|
|
643
|
-
reason: input.message.reason
|
|
832
|
+
reason: input.message.reason,
|
|
833
|
+
manifest: input.manifest
|
|
644
834
|
});
|
|
645
835
|
case "pull_branch":
|
|
646
836
|
return pullBranch({
|
|
@@ -648,8 +838,10 @@ async function executeOperation(input) {
|
|
|
648
838
|
baseUrl: input.baseUrl,
|
|
649
839
|
token: input.token,
|
|
650
840
|
projectRoot: input.projectRoot,
|
|
841
|
+
syncRoot: input.syncRoot,
|
|
651
842
|
branchName: input.message.branchName,
|
|
652
|
-
commitHash: input.message.commitHash
|
|
843
|
+
commitHash: input.message.commitHash,
|
|
844
|
+
manifest: input.manifest
|
|
653
845
|
});
|
|
654
846
|
}
|
|
655
847
|
}
|
|
@@ -662,9 +854,11 @@ async function executeCommand(input) {
|
|
|
662
854
|
baseUrl: input.baseUrl,
|
|
663
855
|
token: input.token,
|
|
664
856
|
projectRoot: input.projectRoot,
|
|
665
|
-
|
|
857
|
+
syncRoot: input.syncRoot,
|
|
858
|
+
branchName: input.message.branchName,
|
|
859
|
+
manifest: input.manifest
|
|
666
860
|
});
|
|
667
|
-
const cwd = resolveCommandCwd(branchPath, input.message.cwd);
|
|
861
|
+
const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
|
|
668
862
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
669
863
|
cwd,
|
|
670
864
|
stdout: "pipe",
|
|
@@ -713,6 +907,182 @@ async function executeCommand(input) {
|
|
|
713
907
|
function sendWorkerMessage(ws, message) {
|
|
714
908
|
ws.send(JSON.stringify(message));
|
|
715
909
|
}
|
|
910
|
+
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
911
|
+
const readline = require("node:readline");
|
|
912
|
+
const nodePty = require("node-pty");
|
|
913
|
+
|
|
914
|
+
let ptyProcess = null;
|
|
915
|
+
|
|
916
|
+
function send(message, callback) {
|
|
917
|
+
process.stdout.write(JSON.stringify(message) + "\n", callback);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function decode(data) {
|
|
921
|
+
return Buffer.from(data, "base64").toString("utf8");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
925
|
+
|
|
926
|
+
rl.on("line", (line) => {
|
|
927
|
+
let message;
|
|
928
|
+
try {
|
|
929
|
+
message = JSON.parse(line);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
if (message.type === "open") {
|
|
937
|
+
ptyProcess = nodePty.spawn(message.file, message.args, message.options);
|
|
938
|
+
ptyProcess.onData((data) => {
|
|
939
|
+
send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
|
|
940
|
+
});
|
|
941
|
+
ptyProcess.onExit((event) => {
|
|
942
|
+
send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
|
|
943
|
+
});
|
|
944
|
+
send({ type: "opened" });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (!ptyProcess) {
|
|
949
|
+
send({ type: "error", error: "PTY has not been opened" });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (message.type === "input") {
|
|
954
|
+
ptyProcess.write(decode(message.data));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (message.type === "resize") {
|
|
959
|
+
ptyProcess.resize(message.cols, message.rows);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (message.type === "close") {
|
|
964
|
+
ptyProcess.kill();
|
|
965
|
+
}
|
|
966
|
+
} catch (error) {
|
|
967
|
+
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
`;
|
|
971
|
+
function nodePtyModulePaths() {
|
|
972
|
+
const candidates = [];
|
|
973
|
+
const entrypoint = process.argv[1];
|
|
974
|
+
if (entrypoint) {
|
|
975
|
+
try {
|
|
976
|
+
let current = path.dirname(fs.realpathSync(entrypoint));
|
|
977
|
+
for (let index = 0; index < 8; index += 1) {
|
|
978
|
+
candidates.push(path.join(current, "node_modules"));
|
|
979
|
+
const parent = path.dirname(current);
|
|
980
|
+
if (parent === current) {
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
current = parent;
|
|
984
|
+
}
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
candidates.push(path.join(process.cwd(), "node_modules"), "/usr/local/lib/node_modules", "/usr/lib/node_modules");
|
|
989
|
+
if (process.env.NODE_PATH) {
|
|
990
|
+
candidates.push(...process.env.NODE_PATH.split(path.delimiter));
|
|
991
|
+
}
|
|
992
|
+
return [...new Set(candidates.filter(Boolean))].join(path.delimiter);
|
|
993
|
+
}
|
|
994
|
+
function createNodePtyBridge(options) {
|
|
995
|
+
const child = spawnChildProcess("node", ["-e", PTY_BRIDGE_SCRIPT], {
|
|
996
|
+
env: {
|
|
997
|
+
...process.env,
|
|
998
|
+
NODE_PATH: nodePtyModulePaths()
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
let lineBuffer = "";
|
|
1002
|
+
let emittedTerminalEvent = false;
|
|
1003
|
+
let stderr = "";
|
|
1004
|
+
const sendToChild = (message) => {
|
|
1005
|
+
if (child.stdin.writable) {
|
|
1006
|
+
child.stdin.write(`${JSON.stringify(message)}
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const handleEvent = (event) => {
|
|
1011
|
+
if (event.type === "opened") {
|
|
1012
|
+
options.onOpened();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (event.type === "output") {
|
|
1016
|
+
options.onOutput(Buffer.from(event.data, "base64").toString("utf8"));
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (event.type === "exit") {
|
|
1020
|
+
emittedTerminalEvent = true;
|
|
1021
|
+
options.onExit({ exitCode: event.exitCode, signal: event.signal });
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (event.type === "error") {
|
|
1025
|
+
emittedTerminalEvent = true;
|
|
1026
|
+
options.onError(new Error(event.error));
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
child.stdout.setEncoding("utf8");
|
|
1030
|
+
child.stdout.on("data", (chunk) => {
|
|
1031
|
+
lineBuffer += chunk;
|
|
1032
|
+
let newlineIndex = lineBuffer.indexOf("\n");
|
|
1033
|
+
while (newlineIndex !== -1) {
|
|
1034
|
+
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
1035
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
1036
|
+
if (line) {
|
|
1037
|
+
try {
|
|
1038
|
+
handleEvent(JSON.parse(line));
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
emittedTerminalEvent = true;
|
|
1041
|
+
options.onError(new Error(error instanceof Error ? error.message : String(error)));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
newlineIndex = lineBuffer.indexOf("\n");
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
child.stderr.setEncoding("utf8");
|
|
1048
|
+
child.stderr.on("data", (chunk) => {
|
|
1049
|
+
stderr += chunk;
|
|
1050
|
+
process.stderr.write(`[r5d-worker] pty helper: ${chunk}`);
|
|
1051
|
+
});
|
|
1052
|
+
child.on("error", (error) => {
|
|
1053
|
+
emittedTerminalEvent = true;
|
|
1054
|
+
options.onError(error);
|
|
1055
|
+
});
|
|
1056
|
+
child.on("exit", (code, signal) => {
|
|
1057
|
+
if (emittedTerminalEvent) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const detail = stderr.trim() || `node pty helper exited with code ${code ?? "unknown"}${signal ? ` signal ${signal}` : ""}`;
|
|
1061
|
+
options.onError(new Error(detail));
|
|
1062
|
+
});
|
|
1063
|
+
sendToChild({
|
|
1064
|
+
type: "open",
|
|
1065
|
+
file: options.file,
|
|
1066
|
+
args: options.args,
|
|
1067
|
+
options: options.ptyOptions
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
write(data) {
|
|
1071
|
+
sendToChild({ type: "input", data: Buffer.from(data, "utf8").toString("base64") });
|
|
1072
|
+
},
|
|
1073
|
+
resize(cols, rows) {
|
|
1074
|
+
sendToChild({ type: "resize", cols, rows });
|
|
1075
|
+
},
|
|
1076
|
+
kill() {
|
|
1077
|
+
sendToChild({ type: "close" });
|
|
1078
|
+
setTimeout(() => {
|
|
1079
|
+
if (!child.killed) {
|
|
1080
|
+
child.kill();
|
|
1081
|
+
}
|
|
1082
|
+
}, 500);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
716
1086
|
function resolveHostShell() {
|
|
717
1087
|
if (process.platform === "win32") {
|
|
718
1088
|
return { file: process.env.COMSPEC || "powershell.exe", args: [] };
|
|
@@ -726,43 +1096,60 @@ function openPty(input) {
|
|
|
726
1096
|
baseUrl: input.baseUrl,
|
|
727
1097
|
token: input.token,
|
|
728
1098
|
projectRoot: input.projectRoot,
|
|
729
|
-
|
|
1099
|
+
syncRoot: input.syncRoot,
|
|
1100
|
+
branchName: input.message.branchName,
|
|
1101
|
+
manifest: input.manifest
|
|
730
1102
|
});
|
|
731
1103
|
const shell = resolveHostShell();
|
|
732
|
-
const ptyProcess =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1104
|
+
const ptyProcess = createNodePtyBridge({
|
|
1105
|
+
file: shell.file,
|
|
1106
|
+
args: shell.args,
|
|
1107
|
+
ptyOptions: {
|
|
1108
|
+
name: "xterm-256color",
|
|
1109
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
1110
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
1111
|
+
cwd: branchPath.branchPath,
|
|
1112
|
+
env: {
|
|
1113
|
+
...process.env,
|
|
1114
|
+
...input.message.env ?? {},
|
|
1115
|
+
BUILD_IT_NOW_PROJECT_ID: input.projectId,
|
|
1116
|
+
BUILD_IT_NOW_BRANCH_NAME: input.message.branchName
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
onOpened: () => {
|
|
1120
|
+
sendWorkerMessage(input.ws, {
|
|
1121
|
+
type: "pty_opened",
|
|
1122
|
+
requestId: input.message.requestId,
|
|
1123
|
+
ptyId: input.message.ptyId
|
|
1124
|
+
});
|
|
1125
|
+
},
|
|
1126
|
+
onOutput: (data) => {
|
|
1127
|
+
sendWorkerMessage(input.ws, {
|
|
1128
|
+
type: "pty_output",
|
|
1129
|
+
ptyId: input.message.ptyId,
|
|
1130
|
+
data
|
|
1131
|
+
});
|
|
1132
|
+
},
|
|
1133
|
+
onExit: (event) => {
|
|
1134
|
+
activePtys.delete(input.message.ptyId);
|
|
1135
|
+
sendWorkerMessage(input.ws, {
|
|
1136
|
+
type: "pty_exit",
|
|
1137
|
+
ptyId: input.message.ptyId,
|
|
1138
|
+
exitCode: event.exitCode,
|
|
1139
|
+
signal: event.signal
|
|
1140
|
+
});
|
|
1141
|
+
},
|
|
1142
|
+
onError: (error) => {
|
|
1143
|
+
activePtys.delete(input.message.ptyId);
|
|
1144
|
+
sendWorkerMessage(input.ws, {
|
|
1145
|
+
type: "pty_error",
|
|
1146
|
+
requestId: input.message.requestId,
|
|
1147
|
+
ptyId: input.message.ptyId,
|
|
1148
|
+
error: error.message
|
|
1149
|
+
});
|
|
742
1150
|
}
|
|
743
1151
|
});
|
|
744
1152
|
activePtys.set(input.message.ptyId, ptyProcess);
|
|
745
|
-
ptyProcess.onData((data) => {
|
|
746
|
-
sendWorkerMessage(input.ws, {
|
|
747
|
-
type: "pty_output",
|
|
748
|
-
ptyId: input.message.ptyId,
|
|
749
|
-
data
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
ptyProcess.onExit((event) => {
|
|
753
|
-
activePtys.delete(input.message.ptyId);
|
|
754
|
-
sendWorkerMessage(input.ws, {
|
|
755
|
-
type: "pty_exit",
|
|
756
|
-
ptyId: input.message.ptyId,
|
|
757
|
-
exitCode: event.exitCode,
|
|
758
|
-
signal: event.signal
|
|
759
|
-
});
|
|
760
|
-
});
|
|
761
|
-
sendWorkerMessage(input.ws, {
|
|
762
|
-
type: "pty_opened",
|
|
763
|
-
requestId: input.message.requestId,
|
|
764
|
-
ptyId: input.message.ptyId
|
|
765
|
-
});
|
|
766
1153
|
} catch (error) {
|
|
767
1154
|
sendWorkerMessage(input.ws, {
|
|
768
1155
|
type: "pty_error",
|
|
@@ -808,32 +1195,36 @@ function closeAllPtys() {
|
|
|
808
1195
|
}
|
|
809
1196
|
activePtys.clear();
|
|
810
1197
|
}
|
|
811
|
-
function websocketUrl(baseUrl,
|
|
1198
|
+
function websocketUrl(baseUrl, label) {
|
|
812
1199
|
const url = new URL("/worker/ws", baseUrl);
|
|
813
1200
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
814
|
-
url.searchParams.set("projectId", projectId);
|
|
815
1201
|
url.searchParams.set("label", label);
|
|
816
1202
|
return url.toString();
|
|
817
1203
|
}
|
|
818
|
-
|
|
1204
|
+
function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
|
|
1205
|
+
return path.join(projectsRoot, manifestByProjectId.get(projectId)?.repoSlug ?? projectId);
|
|
1206
|
+
}
|
|
1207
|
+
async function startWorker(options) {
|
|
819
1208
|
const config = readConfig(options.configPath);
|
|
820
1209
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
821
1210
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
822
1211
|
validateLabel(label);
|
|
823
|
-
const
|
|
824
|
-
process.
|
|
825
|
-
`);
|
|
1212
|
+
const projectsRoot = path.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1213
|
+
const syncRoot = path.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
826
1214
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
827
1215
|
`);
|
|
828
|
-
process.stdout.write(`[r5d-worker] root: ${
|
|
1216
|
+
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1217
|
+
`);
|
|
1218
|
+
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
829
1219
|
`);
|
|
830
1220
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
831
1221
|
`);
|
|
832
1222
|
runGit(["--version"]);
|
|
833
1223
|
await verifyBinctlAuth(baseUrl, token);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const
|
|
1224
|
+
fs.mkdirSync(projectsRoot, { recursive: true });
|
|
1225
|
+
fs.mkdirSync(syncRoot, { recursive: true });
|
|
1226
|
+
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1227
|
+
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
837
1228
|
headers: {
|
|
838
1229
|
Authorization: `Bearer ${token}`
|
|
839
1230
|
}
|
|
@@ -847,7 +1238,7 @@ async function startWorker(projectId, options) {
|
|
|
847
1238
|
arch: process.arch,
|
|
848
1239
|
pid: process.pid,
|
|
849
1240
|
version: "0.1.0",
|
|
850
|
-
projectRoot
|
|
1241
|
+
projectRoot: projectsRoot
|
|
851
1242
|
}
|
|
852
1243
|
};
|
|
853
1244
|
ws.send(JSON.stringify(hello));
|
|
@@ -859,6 +1250,39 @@ async function startWorker(projectId, options) {
|
|
|
859
1250
|
if (message.type === "connected") {
|
|
860
1251
|
return;
|
|
861
1252
|
}
|
|
1253
|
+
if (message.type === "project_manifest") {
|
|
1254
|
+
manifestByProjectId.clear();
|
|
1255
|
+
for (const project of message.projects) {
|
|
1256
|
+
manifestByProjectId.set(project.projectId, project);
|
|
1257
|
+
}
|
|
1258
|
+
process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
|
|
1259
|
+
`);
|
|
1260
|
+
for (const project of message.projects) {
|
|
1261
|
+
const projectRoot = projectRootFor(projectsRoot, project.projectId, manifestByProjectId);
|
|
1262
|
+
const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
|
|
1263
|
+
for (const branchName of branches) {
|
|
1264
|
+
try {
|
|
1265
|
+
await withBranchLock(project.projectId, branchName, async () => {
|
|
1266
|
+
ensureBranchWorkspace({
|
|
1267
|
+
projectId: project.projectId,
|
|
1268
|
+
baseUrl,
|
|
1269
|
+
token,
|
|
1270
|
+
projectRoot,
|
|
1271
|
+
syncRoot,
|
|
1272
|
+
branchName,
|
|
1273
|
+
manifest: project
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
process.stderr.write(
|
|
1278
|
+
`[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
1279
|
+
`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
862
1286
|
if (message.type === "ping") {
|
|
863
1287
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
864
1288
|
return;
|
|
@@ -880,9 +1304,11 @@ async function startWorker(projectId, options) {
|
|
|
880
1304
|
return;
|
|
881
1305
|
}
|
|
882
1306
|
if (message.type === "pty_open") {
|
|
883
|
-
|
|
1307
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1308
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1309
|
+
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
884
1310
|
`);
|
|
885
|
-
openPty({ ws, message, projectId, baseUrl, token, projectRoot });
|
|
1311
|
+
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
886
1312
|
return;
|
|
887
1313
|
}
|
|
888
1314
|
if (message.type === "pty_input") {
|
|
@@ -898,22 +1324,26 @@ async function startWorker(projectId, options) {
|
|
|
898
1324
|
return;
|
|
899
1325
|
}
|
|
900
1326
|
if (message.type === "exec") {
|
|
1327
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1328
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
901
1329
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
902
1330
|
`);
|
|
903
1331
|
const result = await withBranchLock(
|
|
904
|
-
|
|
1332
|
+
message.projectId,
|
|
905
1333
|
message.branchName,
|
|
906
|
-
() => executeCommand({ message, projectId, baseUrl, token, projectRoot })
|
|
1334
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
907
1335
|
);
|
|
908
1336
|
ws.send(JSON.stringify(result));
|
|
909
1337
|
return;
|
|
910
1338
|
}
|
|
911
|
-
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") {
|
|
1339
|
+
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") {
|
|
912
1340
|
try {
|
|
1341
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1342
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
913
1343
|
const result = await withBranchLock(
|
|
914
|
-
|
|
1344
|
+
message.projectId,
|
|
915
1345
|
message.branchName,
|
|
916
|
-
() => executeOperation({ message, projectId, baseUrl, token, projectRoot })
|
|
1346
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
917
1347
|
);
|
|
918
1348
|
ws.send(
|
|
919
1349
|
JSON.stringify({
|
|
@@ -958,7 +1388,7 @@ async function main() {
|
|
|
958
1388
|
printHelp();
|
|
959
1389
|
return;
|
|
960
1390
|
}
|
|
961
|
-
await startWorker(parsed.
|
|
1391
|
+
await startWorker(parsed.options);
|
|
962
1392
|
}
|
|
963
1393
|
main().catch((error) => {
|
|
964
1394
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|