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