@ricsam/r5d-worker 0.0.2 → 0.0.4
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 +640 -144
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +640 -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]$/;
|
|
@@ -37,23 +37,28 @@ const MAX_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
|
37
37
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
38
38
|
const activePtys = /* @__PURE__ */ new Map();
|
|
39
39
|
const branchLocks = /* @__PURE__ */ new Map();
|
|
40
|
+
let containerRegistryCredential = null;
|
|
40
41
|
function defaultConfigPath() {
|
|
41
42
|
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
42
43
|
}
|
|
43
|
-
function
|
|
44
|
-
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects"
|
|
44
|
+
function defaultProjectsRoot() {
|
|
45
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "projects");
|
|
46
|
+
}
|
|
47
|
+
function defaultSyncRoot() {
|
|
48
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "sync");
|
|
45
49
|
}
|
|
46
50
|
function printHelp() {
|
|
47
51
|
process.stdout.write(`Usage:
|
|
48
|
-
r5d-worker start
|
|
52
|
+
r5d-worker start --label <label> [--base-url <url>] [--token <token>] [--api-key <key>]
|
|
49
53
|
|
|
50
54
|
Options:
|
|
51
|
-
--label <label> Required unique worker label for this
|
|
55
|
+
--label <label> Required unique worker label for this user, e.g. macos or ec2-build
|
|
52
56
|
--base-url <url> r5d.dev base URL (default from r5d config, R5D_BASE_URL, or ${DEFAULT_BASE_URL})
|
|
53
57
|
--token <token> r5d worker token
|
|
54
58
|
--api-key <key> r5d API key
|
|
55
59
|
--config <path> Config path (default ~/.config/r5d/r5dctl/config.json)
|
|
56
|
-
--project-root <dir> Override checkout root (default ~/.r5d/projects
|
|
60
|
+
--project-root <dir> Override projects checkout root (default ~/.r5d/projects)
|
|
61
|
+
--sync-root <dir> Override r5d internal sync git root (default ~/.r5d/sync)
|
|
57
62
|
-h, --help Show this help
|
|
58
63
|
`);
|
|
59
64
|
}
|
|
@@ -116,6 +121,11 @@ function parseArgs(argv) {
|
|
|
116
121
|
options.projectRoot = projectRoot;
|
|
117
122
|
continue;
|
|
118
123
|
}
|
|
124
|
+
const syncRoot = readOption("--sync-root");
|
|
125
|
+
if (syncRoot !== void 0) {
|
|
126
|
+
options.syncRoot = syncRoot;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
119
129
|
rest.push(arg);
|
|
120
130
|
}
|
|
121
131
|
if (options.help || rest.length === 0) {
|
|
@@ -127,7 +137,6 @@ function parseArgs(argv) {
|
|
|
127
137
|
}
|
|
128
138
|
return {
|
|
129
139
|
command: "start",
|
|
130
|
-
projectId: requireValue(rest[1], "Missing project id"),
|
|
131
140
|
options
|
|
132
141
|
};
|
|
133
142
|
}
|
|
@@ -205,17 +214,14 @@ function validateBranchName(branchName) {
|
|
|
205
214
|
throw new Error(`Invalid branch name from server: ${branchName}`);
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
|
-
function
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
function gitExtraHeaderConfigKey(baseUrl) {
|
|
212
|
-
return `http.${normalizeBaseUrl(baseUrl)}/.extraHeader`;
|
|
217
|
+
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
218
|
+
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
213
219
|
}
|
|
214
220
|
function gitAuthArgs(auth) {
|
|
215
221
|
if (!auth) {
|
|
216
222
|
return [];
|
|
217
223
|
}
|
|
218
|
-
return ["-c", `${gitExtraHeaderConfigKey(auth.
|
|
224
|
+
return ["-c", `${gitExtraHeaderConfigKey(auth.extraHeaderUrl)}=${auth.header}`];
|
|
219
225
|
}
|
|
220
226
|
function runGit(args, options = {}) {
|
|
221
227
|
const command = ["git", ...gitAuthArgs(options.auth), ...args];
|
|
@@ -243,8 +249,22 @@ function tryGit(args, options = {}) {
|
|
|
243
249
|
return false;
|
|
244
250
|
}
|
|
245
251
|
}
|
|
246
|
-
function
|
|
247
|
-
return runGit(["
|
|
252
|
+
function runInternalGit(context, args) {
|
|
253
|
+
return runGit(["--git-dir", context.gitDir, "--work-tree", context.workTree, ...args], { auth: context.auth });
|
|
254
|
+
}
|
|
255
|
+
function runInternalGitDir(context, args) {
|
|
256
|
+
return runGit(["--git-dir", context.gitDir, ...args], { auth: context.auth });
|
|
257
|
+
}
|
|
258
|
+
function tryInternalGit(context, args) {
|
|
259
|
+
try {
|
|
260
|
+
runInternalGit(context, args);
|
|
261
|
+
return true;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function getInternalCommitHash(context) {
|
|
267
|
+
return runInternalGit(context, ["rev-parse", "HEAD"]);
|
|
248
268
|
}
|
|
249
269
|
function getGitBlobHashForContent(content) {
|
|
250
270
|
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
@@ -253,6 +273,12 @@ function getGitBlobHashForContent(content) {
|
|
|
253
273
|
function hasWorktreeChanges(cwd) {
|
|
254
274
|
return runGit(["status", "--porcelain"], { cwd }).trim().length > 0;
|
|
255
275
|
}
|
|
276
|
+
function getInternalStatus(context) {
|
|
277
|
+
return runInternalGit(context, ["status", "--porcelain", "--", ".", ":(exclude).git"]);
|
|
278
|
+
}
|
|
279
|
+
function hasInternalWorktreeChanges(context) {
|
|
280
|
+
return getInternalStatus(context).trim().length > 0;
|
|
281
|
+
}
|
|
256
282
|
function assertInsideBranch(branchPath, filePath) {
|
|
257
283
|
const relative = import_node_path.default.relative(branchPath, filePath);
|
|
258
284
|
if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
|
|
@@ -275,11 +301,11 @@ function resolveProjectFilePath(branchPath, virtualPath) {
|
|
|
275
301
|
assertInsideBranch(branchPath, absolutePath);
|
|
276
302
|
return { absolutePath, virtualPath: `/${repoRelativePath}`, repoRelativePath };
|
|
277
303
|
}
|
|
278
|
-
function branchLockKey(
|
|
279
|
-
return `${
|
|
304
|
+
function branchLockKey(projectId, branchName) {
|
|
305
|
+
return `${projectId}:${branchName}`;
|
|
280
306
|
}
|
|
281
|
-
async function withBranchLock(
|
|
282
|
-
const key = branchLockKey(
|
|
307
|
+
async function withBranchLock(projectId, branchName, fn) {
|
|
308
|
+
const key = branchLockKey(projectId, branchName);
|
|
283
309
|
const previous = branchLocks.get(key) ?? Promise.resolve();
|
|
284
310
|
let release = () => {
|
|
285
311
|
};
|
|
@@ -300,60 +326,283 @@ async function withBranchLock(projectRoot, branchName, fn) {
|
|
|
300
326
|
}
|
|
301
327
|
}
|
|
302
328
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
329
|
+
function resolveRemoteUrl(baseUrl, remoteUrl) {
|
|
330
|
+
if (/^https?:\/\//i.test(remoteUrl)) {
|
|
331
|
+
return remoteUrl;
|
|
332
|
+
}
|
|
333
|
+
return new URL(remoteUrl, baseUrl).toString();
|
|
334
|
+
}
|
|
335
|
+
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
336
|
+
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
310
337
|
}
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
function internalGitAuth(baseUrl, token) {
|
|
339
|
+
return {
|
|
340
|
+
extraHeaderUrl: baseUrl,
|
|
341
|
+
header: `Authorization: Bearer ${token}`
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function internalRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
345
|
+
return resolveRemoteUrl(baseUrl, manifest?.internalRemoteUrl ?? fallbackInternalRemoteUrl(baseUrl, projectId));
|
|
346
|
+
}
|
|
347
|
+
function githubRemoteUrlFor(baseUrl, projectId, manifest) {
|
|
348
|
+
return manifest?.repoHttpUrl ?? internalRemoteUrlFor(baseUrl, projectId, manifest);
|
|
349
|
+
}
|
|
350
|
+
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
351
|
+
try {
|
|
352
|
+
return new URL(remoteUrl).origin;
|
|
353
|
+
} catch {
|
|
354
|
+
return remoteUrl;
|
|
318
355
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (!hasWorktreeChanges(mainPath)) {
|
|
324
|
-
tryGit(["pull", "--ff-only", "build-it-now", "main"], { cwd: mainPath });
|
|
356
|
+
}
|
|
357
|
+
function gitAuthForRemote(remoteUrl, authHeader) {
|
|
358
|
+
if (!authHeader) {
|
|
359
|
+
return void 0;
|
|
325
360
|
}
|
|
326
|
-
return
|
|
361
|
+
return {
|
|
362
|
+
extraHeaderUrl: gitExtraHeaderUrlForRemote(remoteUrl),
|
|
363
|
+
header: authHeader
|
|
364
|
+
};
|
|
327
365
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
366
|
+
function branchGitDirName(branchName) {
|
|
367
|
+
return `${encodeURIComponent(branchName)}.git`;
|
|
368
|
+
}
|
|
369
|
+
function internalGitDirFor(syncRoot, projectId, branchName) {
|
|
370
|
+
return import_node_path.default.join(syncRoot, projectId, branchGitDirName(branchName));
|
|
371
|
+
}
|
|
372
|
+
function hasNormalVisibleGitDir(branchPath) {
|
|
373
|
+
const gitPath = import_node_path.default.join(branchPath, ".git");
|
|
374
|
+
return import_node_fs.default.existsSync(gitPath) && import_node_fs.default.statSync(gitPath).isDirectory();
|
|
375
|
+
}
|
|
376
|
+
function ensureOriginRemote(branchPath, remoteUrl) {
|
|
377
|
+
if (tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) {
|
|
378
|
+
runGit(["remote", "set-url", "origin", remoteUrl], { cwd: branchPath });
|
|
379
|
+
} else {
|
|
380
|
+
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
333
381
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
382
|
+
}
|
|
383
|
+
function shellQuote(value) {
|
|
384
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
385
|
+
}
|
|
386
|
+
function githubCredentialsPath() {
|
|
387
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".r5d", "github-credentials");
|
|
388
|
+
}
|
|
389
|
+
function decodeBasicAuthHeader(authHeader) {
|
|
390
|
+
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(authHeader.trim());
|
|
391
|
+
if (!match) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const decoded = Buffer.from(match[1], "base64").toString("utf8");
|
|
395
|
+
const separatorIndex = decoded.indexOf(":");
|
|
396
|
+
if (separatorIndex <= 0) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
username: decoded.slice(0, separatorIndex),
|
|
401
|
+
password: decoded.slice(separatorIndex + 1)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function writeCredentialStoreEntry(remoteUrl, authHeader) {
|
|
405
|
+
const credentials = decodeBasicAuthHeader(authHeader);
|
|
406
|
+
if (!credentials) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
let remote;
|
|
410
|
+
try {
|
|
411
|
+
remote = new URL(remoteUrl);
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
if (remote.protocol !== "https:") {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const storePath = githubCredentialsPath();
|
|
419
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(storePath), { recursive: true });
|
|
420
|
+
const hostKey = `${remote.protocol}//${remote.host}`;
|
|
421
|
+
const existing = import_node_fs.default.existsSync(storePath) ? import_node_fs.default.readFileSync(storePath, "utf8").split(/\r?\n/).filter(Boolean) : [];
|
|
422
|
+
const next = existing.filter((line) => {
|
|
423
|
+
try {
|
|
424
|
+
const parsed = new URL(line);
|
|
425
|
+
return `${parsed.protocol}//${parsed.host}` !== hostKey;
|
|
426
|
+
} catch {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
338
429
|
});
|
|
339
|
-
|
|
430
|
+
const credentialUrl = new URL(hostKey);
|
|
431
|
+
credentialUrl.username = credentials.username;
|
|
432
|
+
credentialUrl.password = credentials.password;
|
|
433
|
+
next.push(credentialUrl.toString());
|
|
434
|
+
import_node_fs.default.writeFileSync(storePath, `${next.join("\n")}
|
|
435
|
+
`, { mode: 384 });
|
|
436
|
+
import_node_fs.default.chmodSync(storePath, 384);
|
|
437
|
+
return storePath;
|
|
438
|
+
}
|
|
439
|
+
function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
|
|
440
|
+
if (!authHeader) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const credentialStore = writeCredentialStoreEntry(remoteUrl, authHeader);
|
|
444
|
+
if (credentialStore) {
|
|
445
|
+
runGit(["config", "credential.helper", `store --file=${shellQuote(credentialStore)}`], { cwd: branchPath });
|
|
446
|
+
runGit(["config", "credential.useHttpPath", "false"], { cwd: branchPath });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
|
|
450
|
+
}
|
|
451
|
+
function dockerConfigPath() {
|
|
452
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".docker", "config.json");
|
|
453
|
+
}
|
|
454
|
+
function containersAuthPath() {
|
|
455
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "containers", "auth.json");
|
|
456
|
+
}
|
|
457
|
+
function readJsonFile(filePath) {
|
|
458
|
+
if (!import_node_fs.default.existsSync(filePath)) {
|
|
459
|
+
return {};
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
const parsed = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf8"));
|
|
463
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
464
|
+
} catch {
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function writeRegistryAuthFile(filePath, credential) {
|
|
469
|
+
const config = readJsonFile(filePath);
|
|
470
|
+
const existingAuths = config.auths && typeof config.auths === "object" && !Array.isArray(config.auths) ? config.auths : {};
|
|
471
|
+
const auths = existingAuths;
|
|
472
|
+
auths[credential.registry] = {
|
|
473
|
+
username: credential.username,
|
|
474
|
+
password: credential.password,
|
|
475
|
+
auth: Buffer.from(`${credential.username}:${credential.password}`).toString("base64")
|
|
476
|
+
};
|
|
477
|
+
config.auths = auths;
|
|
478
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(filePath), { recursive: true });
|
|
479
|
+
import_node_fs.default.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
|
|
480
|
+
`, { mode: 384 });
|
|
481
|
+
import_node_fs.default.chmodSync(filePath, 384);
|
|
482
|
+
}
|
|
483
|
+
function configureContainerRegistryAuth(credential) {
|
|
484
|
+
containerRegistryCredential = credential;
|
|
485
|
+
if (!credential) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
writeRegistryAuthFile(dockerConfigPath(), credential);
|
|
489
|
+
writeRegistryAuthFile(containersAuthPath(), credential);
|
|
490
|
+
if (credential.missingScopes.length > 0) {
|
|
491
|
+
process.stderr.write(
|
|
492
|
+
`[r5d-worker] GitHub token is missing GHCR scope(s): ${credential.missingScopes.join(", ")}. Sign in with GitHub again if GHCR push/pull fails.
|
|
493
|
+
`
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
process.stdout.write(`[r5d-worker] configured GHCR auth for ${credential.username}
|
|
497
|
+
`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function containerRegistryEnv() {
|
|
501
|
+
if (!containerRegistryCredential) {
|
|
502
|
+
return {};
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
R5D_GHCR_REGISTRY: containerRegistryCredential.registry,
|
|
506
|
+
R5D_GHCR_NAMESPACE: containerRegistryCredential.username,
|
|
507
|
+
R5D_GHCR_AUTH_FILE: containersAuthPath(),
|
|
508
|
+
REGISTRY_AUTH_FILE: containersAuthPath(),
|
|
509
|
+
GHCR_REGISTRY: containerRegistryCredential.registry,
|
|
510
|
+
GHCR_NAMESPACE: containerRegistryCredential.username
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function checkoutVisibleBranch(input) {
|
|
514
|
+
const branchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/heads/${input.branchName}`], { cwd: input.branchPath });
|
|
515
|
+
const remoteBranchExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], {
|
|
516
|
+
cwd: input.branchPath
|
|
517
|
+
});
|
|
518
|
+
const defaultRemoteExists = tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.defaultBranch}`], {
|
|
519
|
+
cwd: input.branchPath
|
|
520
|
+
});
|
|
521
|
+
const clean = !hasWorktreeChanges(input.branchPath);
|
|
522
|
+
if (remoteBranchExists && clean) {
|
|
523
|
+
runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], { cwd: input.branchPath });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (branchExists) {
|
|
527
|
+
runGit(["checkout", input.branchName], { cwd: input.branchPath });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (defaultRemoteExists) {
|
|
531
|
+
runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], { cwd: input.branchPath });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
|
|
535
|
+
}
|
|
536
|
+
function ensureVisibleGitCheckout(input) {
|
|
537
|
+
validateBranchName(input.branchName);
|
|
538
|
+
import_node_fs.default.mkdirSync(input.projectRoot, { recursive: true });
|
|
539
|
+
const branchPath = import_node_path.default.join(input.projectRoot, input.branchName);
|
|
540
|
+
if (!hasNormalVisibleGitDir(branchPath)) {
|
|
340
541
|
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
|
-
});
|
|
542
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(branchPath), { recursive: true });
|
|
543
|
+
if (!tryGit(["clone", "--origin", "origin", input.githubRemoteUrl, branchPath], { auth: input.githubAuth })) {
|
|
544
|
+
import_node_fs.default.mkdirSync(branchPath, { recursive: true });
|
|
545
|
+
runGit(["init"], { cwd: branchPath });
|
|
349
546
|
}
|
|
350
547
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
548
|
+
ensureOriginRemote(branchPath, input.githubRemoteUrl);
|
|
549
|
+
configureVisibleGitHubAuth(branchPath, input.githubRemoteUrl, input.githubAuthHeader);
|
|
550
|
+
tryGit(["fetch", "origin", "--prune"], { cwd: branchPath, auth: input.githubAuth });
|
|
551
|
+
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
355
552
|
return branchPath;
|
|
356
553
|
}
|
|
554
|
+
function configureInternalRepo(context, remoteUrl) {
|
|
555
|
+
if (tryInternalGit(context, ["remote", "get-url", "build-it-now"])) {
|
|
556
|
+
runInternalGit(context, ["remote", "set-url", "build-it-now", remoteUrl]);
|
|
557
|
+
} else {
|
|
558
|
+
runInternalGit(context, ["remote", "add", "build-it-now", remoteUrl]);
|
|
559
|
+
}
|
|
560
|
+
runInternalGitDir(context, ["config", "user.name", "r5d.dev Worker"]);
|
|
561
|
+
runInternalGitDir(context, ["config", "user.email", "worker@r5d.dev"]);
|
|
562
|
+
}
|
|
563
|
+
function ensureInternalSyncGit(input) {
|
|
564
|
+
validateBranchName(input.branchName);
|
|
565
|
+
const gitDir = internalGitDirFor(input.syncRoot, input.projectId, input.branchName);
|
|
566
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(gitDir), { recursive: true });
|
|
567
|
+
if (!import_node_fs.default.existsSync(gitDir)) {
|
|
568
|
+
runGit(["init", "--bare", gitDir]);
|
|
569
|
+
}
|
|
570
|
+
const auth = internalGitAuth(input.baseUrl, input.token);
|
|
571
|
+
const context = { gitDir, workTree: input.branchPath, auth };
|
|
572
|
+
const remoteUrl = internalRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
573
|
+
configureInternalRepo(context, remoteUrl);
|
|
574
|
+
runInternalGit(context, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
|
|
575
|
+
const hasRemoteBranch = tryInternalGit(context, ["show-ref", "--verify", "--quiet", `refs/remotes/build-it-now/${input.branchName}`]);
|
|
576
|
+
const target = hasRemoteBranch ? `refs/remotes/build-it-now/${input.branchName}` : "refs/remotes/build-it-now/main";
|
|
577
|
+
const hasHead = tryInternalGit(context, ["rev-parse", "--verify", "HEAD"]);
|
|
578
|
+
if (!hasHead || !hasInternalWorktreeChanges(context)) {
|
|
579
|
+
runInternalGit(context, ["checkout", "-f", "-B", input.branchName, target]);
|
|
580
|
+
}
|
|
581
|
+
return context;
|
|
582
|
+
}
|
|
583
|
+
function ensureBranchWorkspace(input) {
|
|
584
|
+
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
585
|
+
const githubRemoteUrl = githubRemoteUrlFor(input.baseUrl, input.projectId, input.manifest);
|
|
586
|
+
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
|
|
587
|
+
const branchPath = ensureVisibleGitCheckout({
|
|
588
|
+
projectRoot: input.projectRoot,
|
|
589
|
+
branchName: input.branchName,
|
|
590
|
+
githubRemoteUrl,
|
|
591
|
+
githubAuth,
|
|
592
|
+
githubAuthHeader: input.manifest?.repoAuthHeader ?? null,
|
|
593
|
+
defaultBranch
|
|
594
|
+
});
|
|
595
|
+
const internalGit = ensureInternalSyncGit({
|
|
596
|
+
projectId: input.projectId,
|
|
597
|
+
baseUrl: input.baseUrl,
|
|
598
|
+
token: input.token,
|
|
599
|
+
syncRoot: input.syncRoot,
|
|
600
|
+
branchPath,
|
|
601
|
+
branchName: input.branchName,
|
|
602
|
+
manifest: input.manifest
|
|
603
|
+
});
|
|
604
|
+
return { branchPath, internalGit };
|
|
605
|
+
}
|
|
357
606
|
function resolveCommandCwd(branchPath, cwd) {
|
|
358
607
|
if (!cwd) {
|
|
359
608
|
return branchPath;
|
|
@@ -462,8 +711,8 @@ function readImageDimensions(bytes, mediaType) {
|
|
|
462
711
|
return dimensions;
|
|
463
712
|
}
|
|
464
713
|
async function executeReadFileOperation(input) {
|
|
465
|
-
const
|
|
466
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
714
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
715
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
467
716
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
468
717
|
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
469
718
|
}
|
|
@@ -477,8 +726,8 @@ async function executeReadFileOperation(input) {
|
|
|
477
726
|
};
|
|
478
727
|
}
|
|
479
728
|
async function executeCreateFileOperation(input) {
|
|
480
|
-
const
|
|
481
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
729
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
730
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
482
731
|
if (import_node_fs.default.existsSync(resolved.absolutePath)) {
|
|
483
732
|
throw new Error(`File "${resolved.virtualPath}" already exists. Use edit_file to modify existing files.`);
|
|
484
733
|
}
|
|
@@ -491,8 +740,8 @@ async function executeCreateFileOperation(input) {
|
|
|
491
740
|
};
|
|
492
741
|
}
|
|
493
742
|
async function executeEditFileOperation(input) {
|
|
494
|
-
const
|
|
495
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
743
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
744
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
496
745
|
if (input.message.oldString === input.message.newString) {
|
|
497
746
|
throw new Error("old_string and new_string must be different");
|
|
498
747
|
}
|
|
@@ -521,8 +770,8 @@ async function executeEditFileOperation(input) {
|
|
|
521
770
|
};
|
|
522
771
|
}
|
|
523
772
|
async function executeViewFileBytesOperation(input) {
|
|
524
|
-
const
|
|
525
|
-
const resolved = resolveProjectFilePath(branchPath, input.message.filePath);
|
|
773
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
774
|
+
const resolved = resolveProjectFilePath(workspace.branchPath, input.message.filePath);
|
|
526
775
|
if (!import_node_fs.default.existsSync(resolved.absolutePath) || !import_node_fs.default.statSync(resolved.absolutePath).isFile()) {
|
|
527
776
|
throw new Error(`File not found: ${resolved.virtualPath}`);
|
|
528
777
|
}
|
|
@@ -543,12 +792,12 @@ async function executeViewFileBytesOperation(input) {
|
|
|
543
792
|
height: dimensions.height
|
|
544
793
|
};
|
|
545
794
|
}
|
|
546
|
-
function stagedBlobSizeBytes(
|
|
547
|
-
const names =
|
|
795
|
+
function stagedBlobSizeBytes(context) {
|
|
796
|
+
const names = runInternalGit(context, ["diff", "--cached", "--name-only", "-z", "--", ".", ":(exclude).git"]).split("\0").filter(Boolean);
|
|
548
797
|
let total = 0;
|
|
549
798
|
for (const name of names) {
|
|
550
799
|
try {
|
|
551
|
-
const sizeText =
|
|
800
|
+
const sizeText = runInternalGit(context, ["cat-file", "-s", `:${name}`]);
|
|
552
801
|
total += Number.parseInt(sizeText, 10) || 0;
|
|
553
802
|
} catch {
|
|
554
803
|
}
|
|
@@ -559,11 +808,11 @@ function stagedBlobSizeBytes(cwd) {
|
|
|
559
808
|
return total;
|
|
560
809
|
}
|
|
561
810
|
function syncBranch(input) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
const hasStagedChanges = !
|
|
565
|
-
const status =
|
|
566
|
-
const currentCommit =
|
|
811
|
+
const workspace = ensureOperationBranch(input);
|
|
812
|
+
runInternalGit(workspace.internalGit, ["add", "-A", "--", ".", ":(exclude).git"]);
|
|
813
|
+
const hasStagedChanges = !tryInternalGit(workspace.internalGit, ["diff", "--cached", "--quiet", "--", ".", ":(exclude).git"]);
|
|
814
|
+
const status = getInternalStatus(workspace.internalGit);
|
|
815
|
+
const currentCommit = getInternalCommitHash(workspace.internalGit);
|
|
567
816
|
if (!hasStagedChanges) {
|
|
568
817
|
return {
|
|
569
818
|
type: "sync",
|
|
@@ -573,7 +822,7 @@ function syncBranch(input) {
|
|
|
573
822
|
status
|
|
574
823
|
};
|
|
575
824
|
}
|
|
576
|
-
const diffSizeBytes = stagedBlobSizeBytes(
|
|
825
|
+
const diffSizeBytes = stagedBlobSizeBytes(workspace.internalGit);
|
|
577
826
|
if (diffSizeBytes > MAX_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
|
|
578
827
|
return {
|
|
579
828
|
type: "sync",
|
|
@@ -591,15 +840,15 @@ function syncBranch(input) {
|
|
|
591
840
|
parentNodeId: input.parentNodeId,
|
|
592
841
|
...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
|
|
593
842
|
});
|
|
594
|
-
|
|
595
|
-
const commitHash =
|
|
596
|
-
|
|
843
|
+
runInternalGit(workspace.internalGit, ["commit", "-m", message]);
|
|
844
|
+
const commitHash = getInternalCommitHash(workspace.internalGit);
|
|
845
|
+
runInternalGit(workspace.internalGit, ["push", "build-it-now", `HEAD:refs/heads/${input.branchName}`]);
|
|
597
846
|
return {
|
|
598
847
|
type: "sync",
|
|
599
848
|
changed: true,
|
|
600
849
|
commitHash,
|
|
601
850
|
diffSizeBytes,
|
|
602
|
-
status:
|
|
851
|
+
status: getInternalStatus(workspace.internalGit),
|
|
603
852
|
trigger: input.trigger
|
|
604
853
|
};
|
|
605
854
|
}
|
|
@@ -623,14 +872,14 @@ function confirmLargeDiff(input) {
|
|
|
623
872
|
};
|
|
624
873
|
}
|
|
625
874
|
function pullBranch(input) {
|
|
626
|
-
const
|
|
627
|
-
|
|
875
|
+
const workspace = ensureOperationBranch(input);
|
|
876
|
+
runInternalGit(workspace.internalGit, ["fetch", "build-it-now", "--prune", "+refs/heads/*:refs/remotes/build-it-now/*"]);
|
|
628
877
|
const target = input.commitHash ?? `build-it-now/${input.branchName}`;
|
|
629
|
-
|
|
630
|
-
|
|
878
|
+
runInternalGit(workspace.internalGit, ["reset", "--hard", target]);
|
|
879
|
+
runInternalGit(workspace.internalGit, ["clean", "-fd", "--", ".", ":(exclude).git"]);
|
|
631
880
|
return {
|
|
632
881
|
type: "pull_branch",
|
|
633
|
-
commitHash:
|
|
882
|
+
commitHash: getInternalCommitHash(workspace.internalGit)
|
|
634
883
|
};
|
|
635
884
|
}
|
|
636
885
|
async function executeOperation(input) {
|
|
@@ -649,10 +898,12 @@ async function executeOperation(input) {
|
|
|
649
898
|
baseUrl: input.baseUrl,
|
|
650
899
|
token: input.token,
|
|
651
900
|
projectRoot: input.projectRoot,
|
|
901
|
+
syncRoot: input.syncRoot,
|
|
652
902
|
branchName: input.message.branchName,
|
|
653
903
|
sessionId: input.message.sessionId,
|
|
654
904
|
parentNodeId: input.message.parentNodeId,
|
|
655
|
-
trigger: input.message.trigger
|
|
905
|
+
trigger: input.message.trigger,
|
|
906
|
+
manifest: input.manifest
|
|
656
907
|
});
|
|
657
908
|
case "confirm_large_diff":
|
|
658
909
|
return confirmLargeDiff({
|
|
@@ -660,10 +911,12 @@ async function executeOperation(input) {
|
|
|
660
911
|
baseUrl: input.baseUrl,
|
|
661
912
|
token: input.token,
|
|
662
913
|
projectRoot: input.projectRoot,
|
|
914
|
+
syncRoot: input.syncRoot,
|
|
663
915
|
branchName: input.message.branchName,
|
|
664
916
|
sessionId: input.message.sessionId,
|
|
665
917
|
parentNodeId: input.message.parentNodeId,
|
|
666
|
-
reason: input.message.reason
|
|
918
|
+
reason: input.message.reason,
|
|
919
|
+
manifest: input.manifest
|
|
667
920
|
});
|
|
668
921
|
case "pull_branch":
|
|
669
922
|
return pullBranch({
|
|
@@ -671,8 +924,10 @@ async function executeOperation(input) {
|
|
|
671
924
|
baseUrl: input.baseUrl,
|
|
672
925
|
token: input.token,
|
|
673
926
|
projectRoot: input.projectRoot,
|
|
927
|
+
syncRoot: input.syncRoot,
|
|
674
928
|
branchName: input.message.branchName,
|
|
675
|
-
commitHash: input.message.commitHash
|
|
929
|
+
commitHash: input.message.commitHash,
|
|
930
|
+
manifest: input.manifest
|
|
676
931
|
});
|
|
677
932
|
}
|
|
678
933
|
}
|
|
@@ -685,15 +940,18 @@ async function executeCommand(input) {
|
|
|
685
940
|
baseUrl: input.baseUrl,
|
|
686
941
|
token: input.token,
|
|
687
942
|
projectRoot: input.projectRoot,
|
|
688
|
-
|
|
943
|
+
syncRoot: input.syncRoot,
|
|
944
|
+
branchName: input.message.branchName,
|
|
945
|
+
manifest: input.manifest
|
|
689
946
|
});
|
|
690
|
-
const cwd = resolveCommandCwd(branchPath, input.message.cwd);
|
|
947
|
+
const cwd = resolveCommandCwd(branchPath.branchPath, input.message.cwd);
|
|
691
948
|
const subprocess = Bun.spawn(input.message.argv, {
|
|
692
949
|
cwd,
|
|
693
950
|
stdout: "pipe",
|
|
694
951
|
stderr: "pipe",
|
|
695
952
|
env: {
|
|
696
953
|
...process.env,
|
|
954
|
+
...containerRegistryEnv(),
|
|
697
955
|
...input.message.env ?? {}
|
|
698
956
|
}
|
|
699
957
|
});
|
|
@@ -736,6 +994,182 @@ async function executeCommand(input) {
|
|
|
736
994
|
function sendWorkerMessage(ws, message) {
|
|
737
995
|
ws.send(JSON.stringify(message));
|
|
738
996
|
}
|
|
997
|
+
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
998
|
+
const readline = require("node:readline");
|
|
999
|
+
const nodePty = require("node-pty");
|
|
1000
|
+
|
|
1001
|
+
let ptyProcess = null;
|
|
1002
|
+
|
|
1003
|
+
function send(message, callback) {
|
|
1004
|
+
process.stdout.write(JSON.stringify(message) + "\n", callback);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function decode(data) {
|
|
1008
|
+
return Buffer.from(data, "base64").toString("utf8");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
1012
|
+
|
|
1013
|
+
rl.on("line", (line) => {
|
|
1014
|
+
let message;
|
|
1015
|
+
try {
|
|
1016
|
+
message = JSON.parse(line);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
if (message.type === "open") {
|
|
1024
|
+
ptyProcess = nodePty.spawn(message.file, message.args, message.options);
|
|
1025
|
+
ptyProcess.onData((data) => {
|
|
1026
|
+
send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
|
|
1027
|
+
});
|
|
1028
|
+
ptyProcess.onExit((event) => {
|
|
1029
|
+
send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
|
|
1030
|
+
});
|
|
1031
|
+
send({ type: "opened" });
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (!ptyProcess) {
|
|
1036
|
+
send({ type: "error", error: "PTY has not been opened" });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (message.type === "input") {
|
|
1041
|
+
ptyProcess.write(decode(message.data));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (message.type === "resize") {
|
|
1046
|
+
ptyProcess.resize(message.cols, message.rows);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (message.type === "close") {
|
|
1051
|
+
ptyProcess.kill();
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
send({ type: "error", error: error instanceof Error ? error.message : String(error) });
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
`;
|
|
1058
|
+
function nodePtyModulePaths() {
|
|
1059
|
+
const candidates = [];
|
|
1060
|
+
const entrypoint = process.argv[1];
|
|
1061
|
+
if (entrypoint) {
|
|
1062
|
+
try {
|
|
1063
|
+
let current = import_node_path.default.dirname(import_node_fs.default.realpathSync(entrypoint));
|
|
1064
|
+
for (let index = 0; index < 8; index += 1) {
|
|
1065
|
+
candidates.push(import_node_path.default.join(current, "node_modules"));
|
|
1066
|
+
const parent = import_node_path.default.dirname(current);
|
|
1067
|
+
if (parent === current) {
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
current = parent;
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
candidates.push(import_node_path.default.join(process.cwd(), "node_modules"), "/usr/local/lib/node_modules", "/usr/lib/node_modules");
|
|
1076
|
+
if (process.env.NODE_PATH) {
|
|
1077
|
+
candidates.push(...process.env.NODE_PATH.split(import_node_path.default.delimiter));
|
|
1078
|
+
}
|
|
1079
|
+
return [...new Set(candidates.filter(Boolean))].join(import_node_path.default.delimiter);
|
|
1080
|
+
}
|
|
1081
|
+
function createNodePtyBridge(options) {
|
|
1082
|
+
const child = (0, import_node_child_process.spawn)("node", ["-e", PTY_BRIDGE_SCRIPT], {
|
|
1083
|
+
env: {
|
|
1084
|
+
...process.env,
|
|
1085
|
+
NODE_PATH: nodePtyModulePaths()
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
let lineBuffer = "";
|
|
1089
|
+
let emittedTerminalEvent = false;
|
|
1090
|
+
let stderr = "";
|
|
1091
|
+
const sendToChild = (message) => {
|
|
1092
|
+
if (child.stdin.writable) {
|
|
1093
|
+
child.stdin.write(`${JSON.stringify(message)}
|
|
1094
|
+
`);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const handleEvent = (event) => {
|
|
1098
|
+
if (event.type === "opened") {
|
|
1099
|
+
options.onOpened();
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (event.type === "output") {
|
|
1103
|
+
options.onOutput(Buffer.from(event.data, "base64").toString("utf8"));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (event.type === "exit") {
|
|
1107
|
+
emittedTerminalEvent = true;
|
|
1108
|
+
options.onExit({ exitCode: event.exitCode, signal: event.signal });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (event.type === "error") {
|
|
1112
|
+
emittedTerminalEvent = true;
|
|
1113
|
+
options.onError(new Error(event.error));
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
child.stdout.setEncoding("utf8");
|
|
1117
|
+
child.stdout.on("data", (chunk) => {
|
|
1118
|
+
lineBuffer += chunk;
|
|
1119
|
+
let newlineIndex = lineBuffer.indexOf("\n");
|
|
1120
|
+
while (newlineIndex !== -1) {
|
|
1121
|
+
const line = lineBuffer.slice(0, newlineIndex).trim();
|
|
1122
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
1123
|
+
if (line) {
|
|
1124
|
+
try {
|
|
1125
|
+
handleEvent(JSON.parse(line));
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
emittedTerminalEvent = true;
|
|
1128
|
+
options.onError(new Error(error instanceof Error ? error.message : String(error)));
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
newlineIndex = lineBuffer.indexOf("\n");
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
child.stderr.setEncoding("utf8");
|
|
1135
|
+
child.stderr.on("data", (chunk) => {
|
|
1136
|
+
stderr += chunk;
|
|
1137
|
+
process.stderr.write(`[r5d-worker] pty helper: ${chunk}`);
|
|
1138
|
+
});
|
|
1139
|
+
child.on("error", (error) => {
|
|
1140
|
+
emittedTerminalEvent = true;
|
|
1141
|
+
options.onError(error);
|
|
1142
|
+
});
|
|
1143
|
+
child.on("exit", (code, signal) => {
|
|
1144
|
+
if (emittedTerminalEvent) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const detail = stderr.trim() || `node pty helper exited with code ${code ?? "unknown"}${signal ? ` signal ${signal}` : ""}`;
|
|
1148
|
+
options.onError(new Error(detail));
|
|
1149
|
+
});
|
|
1150
|
+
sendToChild({
|
|
1151
|
+
type: "open",
|
|
1152
|
+
file: options.file,
|
|
1153
|
+
args: options.args,
|
|
1154
|
+
options: options.ptyOptions
|
|
1155
|
+
});
|
|
1156
|
+
return {
|
|
1157
|
+
write(data) {
|
|
1158
|
+
sendToChild({ type: "input", data: Buffer.from(data, "utf8").toString("base64") });
|
|
1159
|
+
},
|
|
1160
|
+
resize(cols, rows) {
|
|
1161
|
+
sendToChild({ type: "resize", cols, rows });
|
|
1162
|
+
},
|
|
1163
|
+
kill() {
|
|
1164
|
+
sendToChild({ type: "close" });
|
|
1165
|
+
setTimeout(() => {
|
|
1166
|
+
if (!child.killed) {
|
|
1167
|
+
child.kill();
|
|
1168
|
+
}
|
|
1169
|
+
}, 500);
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
739
1173
|
function resolveHostShell() {
|
|
740
1174
|
if (process.platform === "win32") {
|
|
741
1175
|
return { file: process.env.COMSPEC || "powershell.exe", args: [] };
|
|
@@ -749,43 +1183,61 @@ function openPty(input) {
|
|
|
749
1183
|
baseUrl: input.baseUrl,
|
|
750
1184
|
token: input.token,
|
|
751
1185
|
projectRoot: input.projectRoot,
|
|
752
|
-
|
|
1186
|
+
syncRoot: input.syncRoot,
|
|
1187
|
+
branchName: input.message.branchName,
|
|
1188
|
+
manifest: input.manifest
|
|
753
1189
|
});
|
|
754
1190
|
const shell = resolveHostShell();
|
|
755
|
-
const ptyProcess =
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1191
|
+
const ptyProcess = createNodePtyBridge({
|
|
1192
|
+
file: shell.file,
|
|
1193
|
+
args: shell.args,
|
|
1194
|
+
ptyOptions: {
|
|
1195
|
+
name: "xterm-256color",
|
|
1196
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
1197
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
1198
|
+
cwd: branchPath.branchPath,
|
|
1199
|
+
env: {
|
|
1200
|
+
...process.env,
|
|
1201
|
+
...containerRegistryEnv(),
|
|
1202
|
+
...input.message.env ?? {},
|
|
1203
|
+
BUILD_IT_NOW_PROJECT_ID: input.projectId,
|
|
1204
|
+
BUILD_IT_NOW_BRANCH_NAME: input.message.branchName
|
|
1205
|
+
}
|
|
1206
|
+
},
|
|
1207
|
+
onOpened: () => {
|
|
1208
|
+
sendWorkerMessage(input.ws, {
|
|
1209
|
+
type: "pty_opened",
|
|
1210
|
+
requestId: input.message.requestId,
|
|
1211
|
+
ptyId: input.message.ptyId
|
|
1212
|
+
});
|
|
1213
|
+
},
|
|
1214
|
+
onOutput: (data) => {
|
|
1215
|
+
sendWorkerMessage(input.ws, {
|
|
1216
|
+
type: "pty_output",
|
|
1217
|
+
ptyId: input.message.ptyId,
|
|
1218
|
+
data
|
|
1219
|
+
});
|
|
1220
|
+
},
|
|
1221
|
+
onExit: (event) => {
|
|
1222
|
+
activePtys.delete(input.message.ptyId);
|
|
1223
|
+
sendWorkerMessage(input.ws, {
|
|
1224
|
+
type: "pty_exit",
|
|
1225
|
+
ptyId: input.message.ptyId,
|
|
1226
|
+
exitCode: event.exitCode,
|
|
1227
|
+
signal: event.signal
|
|
1228
|
+
});
|
|
1229
|
+
},
|
|
1230
|
+
onError: (error) => {
|
|
1231
|
+
activePtys.delete(input.message.ptyId);
|
|
1232
|
+
sendWorkerMessage(input.ws, {
|
|
1233
|
+
type: "pty_error",
|
|
1234
|
+
requestId: input.message.requestId,
|
|
1235
|
+
ptyId: input.message.ptyId,
|
|
1236
|
+
error: error.message
|
|
1237
|
+
});
|
|
765
1238
|
}
|
|
766
1239
|
});
|
|
767
1240
|
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
1241
|
} catch (error) {
|
|
790
1242
|
sendWorkerMessage(input.ws, {
|
|
791
1243
|
type: "pty_error",
|
|
@@ -831,32 +1283,36 @@ function closeAllPtys() {
|
|
|
831
1283
|
}
|
|
832
1284
|
activePtys.clear();
|
|
833
1285
|
}
|
|
834
|
-
function websocketUrl(baseUrl,
|
|
1286
|
+
function websocketUrl(baseUrl, label) {
|
|
835
1287
|
const url = new URL("/worker/ws", baseUrl);
|
|
836
1288
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
837
|
-
url.searchParams.set("projectId", projectId);
|
|
838
1289
|
url.searchParams.set("label", label);
|
|
839
1290
|
return url.toString();
|
|
840
1291
|
}
|
|
841
|
-
|
|
1292
|
+
function projectRootFor(projectsRoot, projectId, manifestByProjectId) {
|
|
1293
|
+
return import_node_path.default.join(projectsRoot, manifestByProjectId.get(projectId)?.repoSlug ?? projectId);
|
|
1294
|
+
}
|
|
1295
|
+
async function startWorker(options) {
|
|
842
1296
|
const config = readConfig(options.configPath);
|
|
843
1297
|
const { baseUrl, token } = resolveCredentials(options, config);
|
|
844
1298
|
const label = requireValue(options.label, "Missing required --label <label>");
|
|
845
1299
|
validateLabel(label);
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
`);
|
|
1300
|
+
const projectsRoot = import_node_path.default.resolve(options.projectRoot ?? defaultProjectsRoot());
|
|
1301
|
+
const syncRoot = import_node_path.default.resolve(options.syncRoot ?? process.env.R5D_SYNC_ROOT ?? defaultSyncRoot());
|
|
849
1302
|
process.stdout.write(`[r5d-worker] label: ${label}
|
|
850
1303
|
`);
|
|
851
|
-
process.stdout.write(`[r5d-worker] root: ${
|
|
1304
|
+
process.stdout.write(`[r5d-worker] projects root: ${projectsRoot}
|
|
1305
|
+
`);
|
|
1306
|
+
process.stdout.write(`[r5d-worker] sync root: ${syncRoot}
|
|
852
1307
|
`);
|
|
853
1308
|
process.stdout.write(`[r5d-worker] server: ${baseUrl}
|
|
854
1309
|
`);
|
|
855
1310
|
runGit(["--version"]);
|
|
856
1311
|
await verifyBinctlAuth(baseUrl, token);
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
1312
|
+
import_node_fs.default.mkdirSync(projectsRoot, { recursive: true });
|
|
1313
|
+
import_node_fs.default.mkdirSync(syncRoot, { recursive: true });
|
|
1314
|
+
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
1315
|
+
const ws = new WebSocket(websocketUrl(baseUrl, label), {
|
|
860
1316
|
headers: {
|
|
861
1317
|
Authorization: `Bearer ${token}`
|
|
862
1318
|
}
|
|
@@ -870,7 +1326,7 @@ async function startWorker(projectId, options) {
|
|
|
870
1326
|
arch: process.arch,
|
|
871
1327
|
pid: process.pid,
|
|
872
1328
|
version: "0.1.0",
|
|
873
|
-
projectRoot
|
|
1329
|
+
projectRoot: projectsRoot
|
|
874
1330
|
}
|
|
875
1331
|
};
|
|
876
1332
|
ws.send(JSON.stringify(hello));
|
|
@@ -882,6 +1338,40 @@ async function startWorker(projectId, options) {
|
|
|
882
1338
|
if (message.type === "connected") {
|
|
883
1339
|
return;
|
|
884
1340
|
}
|
|
1341
|
+
if (message.type === "project_manifest") {
|
|
1342
|
+
configureContainerRegistryAuth(message.githubContainerRegistry);
|
|
1343
|
+
manifestByProjectId.clear();
|
|
1344
|
+
for (const project of message.projects) {
|
|
1345
|
+
manifestByProjectId.set(project.projectId, project);
|
|
1346
|
+
}
|
|
1347
|
+
process.stdout.write(`[r5d-worker] project manifest: ${message.projects.length} projects
|
|
1348
|
+
`);
|
|
1349
|
+
for (const project of message.projects) {
|
|
1350
|
+
const projectRoot = projectRootFor(projectsRoot, project.projectId, manifestByProjectId);
|
|
1351
|
+
const branches = project.branches.length > 0 ? project.branches : [project.defaultBranch || "main"];
|
|
1352
|
+
for (const branchName of branches) {
|
|
1353
|
+
try {
|
|
1354
|
+
await withBranchLock(project.projectId, branchName, async () => {
|
|
1355
|
+
ensureBranchWorkspace({
|
|
1356
|
+
projectId: project.projectId,
|
|
1357
|
+
baseUrl,
|
|
1358
|
+
token,
|
|
1359
|
+
projectRoot,
|
|
1360
|
+
syncRoot,
|
|
1361
|
+
branchName,
|
|
1362
|
+
manifest: project
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
process.stderr.write(
|
|
1367
|
+
`[r5d-worker] failed to sync ${project.repoSlug}/${branchName}: ${error instanceof Error ? error.message : String(error)}
|
|
1368
|
+
`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
885
1375
|
if (message.type === "ping") {
|
|
886
1376
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
887
1377
|
return;
|
|
@@ -903,9 +1393,11 @@ async function startWorker(projectId, options) {
|
|
|
903
1393
|
return;
|
|
904
1394
|
}
|
|
905
1395
|
if (message.type === "pty_open") {
|
|
906
|
-
|
|
1396
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1397
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
1398
|
+
process.stdout.write(`[r5d-worker] pty ${message.ptyId}: ${message.projectId}/${message.branchName}
|
|
907
1399
|
`);
|
|
908
|
-
openPty({ ws, message, projectId, baseUrl, token, projectRoot });
|
|
1400
|
+
openPty({ ws, message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest });
|
|
909
1401
|
return;
|
|
910
1402
|
}
|
|
911
1403
|
if (message.type === "pty_input") {
|
|
@@ -921,22 +1413,26 @@ async function startWorker(projectId, options) {
|
|
|
921
1413
|
return;
|
|
922
1414
|
}
|
|
923
1415
|
if (message.type === "exec") {
|
|
1416
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1417
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
924
1418
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
925
1419
|
`);
|
|
926
1420
|
const result = await withBranchLock(
|
|
927
|
-
|
|
1421
|
+
message.projectId,
|
|
928
1422
|
message.branchName,
|
|
929
|
-
() => executeCommand({ message, projectId, baseUrl, token, projectRoot })
|
|
1423
|
+
() => executeCommand({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
930
1424
|
);
|
|
931
1425
|
ws.send(JSON.stringify(result));
|
|
932
1426
|
return;
|
|
933
1427
|
}
|
|
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") {
|
|
1428
|
+
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
1429
|
try {
|
|
1430
|
+
const manifest = manifestByProjectId.get(message.projectId);
|
|
1431
|
+
const projectRoot = projectRootFor(projectsRoot, message.projectId, manifestByProjectId);
|
|
936
1432
|
const result = await withBranchLock(
|
|
937
|
-
|
|
1433
|
+
message.projectId,
|
|
938
1434
|
message.branchName,
|
|
939
|
-
() => executeOperation({ message, projectId, baseUrl, token, projectRoot })
|
|
1435
|
+
() => executeOperation({ message, projectId: message.projectId, baseUrl, token, projectRoot, syncRoot, manifest })
|
|
940
1436
|
);
|
|
941
1437
|
ws.send(
|
|
942
1438
|
JSON.stringify({
|
|
@@ -981,7 +1477,7 @@ async function main() {
|
|
|
981
1477
|
printHelp();
|
|
982
1478
|
return;
|
|
983
1479
|
}
|
|
984
|
-
await startWorker(parsed.
|
|
1480
|
+
await startWorker(parsed.options);
|
|
985
1481
|
}
|
|
986
1482
|
main().catch((error) => {
|
|
987
1483
|
process.stderr.write(`[r5d-worker] ${error instanceof Error ? error.message : String(error)}
|