@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/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
|
|
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
|
|
44
|
-
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
209
|
-
return
|
|
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.
|
|
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
|
|
247
|
-
return runGit(["
|
|
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(
|
|
279
|
-
return `${
|
|
303
|
+
function branchLockKey(projectId, branchName) {
|
|
304
|
+
return `${projectId}:${branchName}`;
|
|
280
305
|
}
|
|
281
|
-
async function withBranchLock(
|
|
282
|
-
const key = branchLockKey(
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
360
|
+
return {
|
|
361
|
+
extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
|
|
362
|
+
header: authHeader
|
|
363
|
+
};
|
|
327
364
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
547
|
-
const names =
|
|
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 =
|
|
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
|
|
563
|
-
|
|
564
|
-
const hasStagedChanges = !
|
|
565
|
-
const status =
|
|
566
|
-
const currentCommit =
|
|
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(
|
|
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
|
-
|
|
595
|
-
const commitHash =
|
|
596
|
-
|
|
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:
|
|
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
|
|
627
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1122
|
+
syncRoot: input.syncRoot,
|
|
1123
|
+
branchName: input.message.branchName,
|
|
1124
|
+
manifest: input.manifest
|
|
753
1125
|
});
|
|
754
1126
|
const shell = resolveHostShell();
|
|
755
|
-
const ptyProcess =
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
847
|
-
|
|
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: ${
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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)}
|