@ricsam/r5d-worker 0.0.45 → 0.0.47
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 +542 -153
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-sync.cjs +790 -90
- package/dist/mjs/main.mjs +541 -154
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-sync.mjs +787 -89
- package/dist/types/main.d.ts +59 -11
- package/dist/types/workspace-sync.d.ts +33 -3
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -21,7 +21,8 @@ import {
|
|
|
21
21
|
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
22
22
|
WorkspaceSyncSingleFlight,
|
|
23
23
|
calculateWorkspaceDiffFingerprint,
|
|
24
|
-
synchronizeWorkspace
|
|
24
|
+
synchronizeWorkspace,
|
|
25
|
+
workspaceProjectsForSync
|
|
25
26
|
} from "./workspace-sync.mjs";
|
|
26
27
|
class WorkerServerUnavailableError extends Error {
|
|
27
28
|
name = "WorkerServerUnavailableError";
|
|
@@ -246,7 +247,25 @@ async function readResponseText(response) {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
const R5D_ARTIFACTS_DIR_REF = "$R5D_ARTIFACTS_DIR";
|
|
250
|
+
const R5D_PLANS_DIR_REF = "$R5D_PLANS_DIR";
|
|
251
|
+
const R5D_ACTIVE_PLAN_FILE_REF = "$R5D_ACTIVE_PLAN_FILE";
|
|
249
252
|
const R5D_PLANS_DIR_ENV = "R5D_PLANS_DIR";
|
|
253
|
+
const BUILT_IN_TOOL_PATH_REFS = [R5D_ARTIFACTS_DIR_REF, R5D_PLANS_DIR_REF, R5D_ACTIVE_PLAN_FILE_REF];
|
|
254
|
+
function parseBuiltInToolPath(inputPath) {
|
|
255
|
+
const normalized = inputPath.replace(/\\/g, "/");
|
|
256
|
+
const match = /^\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))(?:\/(.*))?$/.exec(normalized);
|
|
257
|
+
if (!match) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const ref = `$${match[1] ?? match[2]}`;
|
|
261
|
+
if (!BUILT_IN_TOOL_PATH_REFS.includes(ref)) {
|
|
262
|
+
throw new Error(`Unsupported tool path variable "${ref}". Supported variables: ${BUILT_IN_TOOL_PATH_REFS.join(", ")}.`);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ref,
|
|
266
|
+
relativePath: match[3] ?? ""
|
|
267
|
+
};
|
|
268
|
+
}
|
|
250
269
|
function validateArtifactSessionId(sessionId) {
|
|
251
270
|
if (!/^(migration-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
|
|
252
271
|
throw new Error(`Invalid artifact session id: ${sessionId}`);
|
|
@@ -255,7 +274,7 @@ function validateArtifactSessionId(sessionId) {
|
|
|
255
274
|
function assertInsideRoot(rootPath, candidatePath, label) {
|
|
256
275
|
const relative = path.relative(rootPath, candidatePath);
|
|
257
276
|
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
258
|
-
throw new Error(`${label} escapes
|
|
277
|
+
throw new Error(`${label} escapes its allowed root`);
|
|
259
278
|
}
|
|
260
279
|
}
|
|
261
280
|
function sessionArtifactDir(artifactRoot, sessionId) {
|
|
@@ -288,7 +307,11 @@ function planEnv(planRoot, projectId, branchName, activePlanId) {
|
|
|
288
307
|
return env;
|
|
289
308
|
}
|
|
290
309
|
function isArtifactEnvPath(filePath) {
|
|
291
|
-
|
|
310
|
+
try {
|
|
311
|
+
return parseBuiltInToolPath(filePath)?.ref === R5D_ARTIFACTS_DIR_REF;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
292
315
|
}
|
|
293
316
|
function artifactApiBasePath(projectId, branchName, sessionId) {
|
|
294
317
|
return `/preview-api/${encodeURIComponent(projectId)}/${encodeURIComponent(branchName)}/artifacts/${encodeURIComponent(sessionId)}`;
|
|
@@ -422,40 +445,45 @@ async function syncSessionArtifacts(input) {
|
|
|
422
445
|
void queued.then(release, release);
|
|
423
446
|
return queued;
|
|
424
447
|
}
|
|
425
|
-
async function
|
|
426
|
-
|
|
427
|
-
|
|
448
|
+
async function prepareBuiltInToolPaths(input) {
|
|
449
|
+
const parsed = parseBuiltInToolPath(input.filePath);
|
|
450
|
+
if (!parsed) {
|
|
451
|
+
return void 0;
|
|
428
452
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
453
|
+
if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
|
|
454
|
+
if (input.access === "write") {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (!input.sessionId) {
|
|
460
|
+
throw new Error(`${R5D_ARTIFACTS_DIR_REF} paths require an active chat session`);
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
artifactsDir: await syncSessionArtifacts({
|
|
464
|
+
baseUrl: input.baseUrl,
|
|
465
|
+
token: input.token,
|
|
466
|
+
projectId: input.projectId,
|
|
467
|
+
branchName: input.branchName,
|
|
468
|
+
sessionId: input.sessionId,
|
|
469
|
+
artifactRoot: input.artifactRoot
|
|
470
|
+
})
|
|
471
|
+
};
|
|
431
472
|
}
|
|
432
|
-
|
|
433
|
-
|
|
473
|
+
const plansDir = projectPlanDir(input.planRoot, input.projectId, input.branchName);
|
|
474
|
+
fs.mkdirSync(plansDir, { recursive: true });
|
|
475
|
+
if (parsed.ref === R5D_PLANS_DIR_REF) {
|
|
476
|
+
return { plansDir };
|
|
434
477
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
token: input.token,
|
|
438
|
-
projectId: input.projectId,
|
|
439
|
-
branchName: input.branchName,
|
|
440
|
-
sessionId: input.sessionId,
|
|
441
|
-
artifactRoot: input.artifactRoot
|
|
442
|
-
});
|
|
443
|
-
const relativePath = path.posix.normalize(input.filePath.slice(`${R5D_ARTIFACTS_DIR_REF}/`.length));
|
|
444
|
-
if (!relativePath || relativePath === "." || relativePath.startsWith("../") || relativePath.includes("\0")) {
|
|
445
|
-
throw new Error(`Invalid artifact path: ${input.filePath}`);
|
|
478
|
+
if (!input.activePlanId) {
|
|
479
|
+
throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} paths require an active plan`);
|
|
446
480
|
}
|
|
447
|
-
|
|
448
|
-
assertInsideRoot(targetDir, absolutePath, "Artifact path");
|
|
481
|
+
validatePlanId(input.activePlanId);
|
|
449
482
|
return {
|
|
450
|
-
|
|
451
|
-
|
|
483
|
+
plansDir,
|
|
484
|
+
activePlanFile: path.join(plansDir, `${input.activePlanId}.plan.md`)
|
|
452
485
|
};
|
|
453
486
|
}
|
|
454
|
-
function rejectArtifactWritePath(filePath) {
|
|
455
|
-
if (isArtifactEnvPath(filePath)) {
|
|
456
|
-
throw new Error(`${R5D_ARTIFACTS_DIR_REF} attachments are read-only. Copy them into a project path before editing or creating files.`);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
487
|
async function prepareArtifactEnvForShell(input) {
|
|
460
488
|
if (!input.sessionId) {
|
|
461
489
|
return {};
|
|
@@ -551,6 +579,14 @@ function validatePlanId(planId) {
|
|
|
551
579
|
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
552
580
|
}
|
|
553
581
|
}
|
|
582
|
+
const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
583
|
+
"-c",
|
|
584
|
+
"submodule.recurse=false",
|
|
585
|
+
"-c",
|
|
586
|
+
"fetch.recurseSubmodules=false",
|
|
587
|
+
"-c",
|
|
588
|
+
"push.recurseSubmodules=false"
|
|
589
|
+
];
|
|
554
590
|
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
555
591
|
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
556
592
|
}
|
|
@@ -558,10 +594,23 @@ function gitAuthArgs(auth) {
|
|
|
558
594
|
if (!auth) {
|
|
559
595
|
return [];
|
|
560
596
|
}
|
|
561
|
-
|
|
597
|
+
const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
|
|
598
|
+
return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
|
|
599
|
+
}
|
|
600
|
+
const visibleCheckoutGitTestHarness = {
|
|
601
|
+
gitAuthArgs,
|
|
602
|
+
commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
|
|
603
|
+
cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
|
|
604
|
+
fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
|
|
605
|
+
};
|
|
606
|
+
function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
|
|
607
|
+
return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
|
|
608
|
+
}
|
|
609
|
+
function visibleCheckoutFetchArgs(...args) {
|
|
610
|
+
return visibleCheckoutGitTestHarness.fetchArgs(...args);
|
|
562
611
|
}
|
|
563
612
|
function runGit(args, options = {}) {
|
|
564
|
-
const command =
|
|
613
|
+
const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
|
|
565
614
|
const result = Bun.spawnSync(command, {
|
|
566
615
|
cwd: options.cwd,
|
|
567
616
|
stdout: "pipe",
|
|
@@ -606,10 +655,63 @@ function assertAllowedProjectPath(repoRelativePath, inputPath) {
|
|
|
606
655
|
throw new Error(`Invalid project file path: ${inputPath}`);
|
|
607
656
|
}
|
|
608
657
|
}
|
|
609
|
-
function
|
|
658
|
+
function assertPathInsideRealRoot(rootPath, candidatePath, label) {
|
|
659
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
660
|
+
const resolvedCandidate = path.resolve(candidatePath);
|
|
661
|
+
assertInsideRoot(resolvedRoot, resolvedCandidate, label);
|
|
662
|
+
let existingPath = resolvedCandidate;
|
|
663
|
+
while (!fs.existsSync(existingPath)) {
|
|
664
|
+
const parentPath = path.dirname(existingPath);
|
|
665
|
+
if (parentPath === existingPath) {
|
|
666
|
+
throw new Error(`${label} root does not exist: ${rootPath}`);
|
|
667
|
+
}
|
|
668
|
+
existingPath = parentPath;
|
|
669
|
+
}
|
|
670
|
+
const realRoot = fs.realpathSync(resolvedRoot);
|
|
671
|
+
const realExistingPath = fs.realpathSync(existingPath);
|
|
672
|
+
assertInsideRoot(realRoot, realExistingPath, label);
|
|
673
|
+
}
|
|
674
|
+
function resolveVirtualWorkerFilePath(inputPath, parsed, builtInPaths) {
|
|
675
|
+
if (parsed.relativePath.split("/").includes("..")) {
|
|
676
|
+
throw new Error(`Invalid ${parsed.ref} path: ${inputPath}`);
|
|
677
|
+
}
|
|
678
|
+
let rootPath;
|
|
679
|
+
let absolutePath;
|
|
680
|
+
if (parsed.ref === R5D_ARTIFACTS_DIR_REF) {
|
|
681
|
+
rootPath = builtInPaths?.artifactsDir;
|
|
682
|
+
} else if (parsed.ref === R5D_PLANS_DIR_REF) {
|
|
683
|
+
rootPath = builtInPaths?.plansDir;
|
|
684
|
+
} else {
|
|
685
|
+
if (parsed.relativePath) {
|
|
686
|
+
throw new Error(`${R5D_ACTIVE_PLAN_FILE_REF} must reference the active plan file directly`);
|
|
687
|
+
}
|
|
688
|
+
rootPath = builtInPaths?.plansDir;
|
|
689
|
+
absolutePath = builtInPaths?.activePlanFile;
|
|
690
|
+
}
|
|
691
|
+
if (!rootPath || parsed.ref === R5D_ACTIVE_PLAN_FILE_REF && !absolutePath) {
|
|
692
|
+
const requirement = parsed.ref === R5D_ARTIFACTS_DIR_REF ? "an active chat session" : "an active plan";
|
|
693
|
+
throw new Error(`${parsed.ref} paths require ${requirement}`);
|
|
694
|
+
}
|
|
695
|
+
const normalizedPath = parsed.relativePath ? path.posix.normalize(parsed.relativePath).replace(/^\/+|\/+$/g, "") : "";
|
|
696
|
+
const normalizedRelativePath = normalizedPath === "." ? "" : normalizedPath;
|
|
697
|
+
absolutePath ??= normalizedRelativePath ? path.resolve(rootPath, ...normalizedRelativePath.split("/")) : path.resolve(rootPath);
|
|
698
|
+
assertPathInsideRealRoot(rootPath, absolutePath, `${parsed.ref} path`);
|
|
699
|
+
return {
|
|
700
|
+
absolutePath,
|
|
701
|
+
displayPath: normalizedRelativePath ? `${parsed.ref}/${normalizedRelativePath}` : parsed.ref,
|
|
702
|
+
repoRelativePath: null,
|
|
703
|
+
scope: "virtual",
|
|
704
|
+
virtualRootPath: path.resolve(rootPath)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
610
708
|
if (typeof inputPath !== "string" || inputPath.length === 0 || inputPath.includes("\0")) {
|
|
611
709
|
throw new Error(`File path must be a non-empty string. Got: ${JSON.stringify(inputPath)}`);
|
|
612
710
|
}
|
|
711
|
+
const parsedBuiltInPath = parseBuiltInToolPath(inputPath);
|
|
712
|
+
if (parsedBuiltInPath) {
|
|
713
|
+
return resolveVirtualWorkerFilePath(inputPath, parsedBuiltInPath, builtInPaths);
|
|
714
|
+
}
|
|
613
715
|
const resolvedBranchPath = path.resolve(branchPath);
|
|
614
716
|
if (path.isAbsolute(inputPath)) {
|
|
615
717
|
const absolutePath2 = path.resolve(inputPath);
|
|
@@ -652,8 +754,28 @@ function resolveWorkerFilePath(branchPath, inputPath) {
|
|
|
652
754
|
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
653
755
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
654
756
|
}
|
|
655
|
-
function
|
|
656
|
-
|
|
757
|
+
function visibleCheckoutRemoteFor(input) {
|
|
758
|
+
const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
|
|
759
|
+
const isCanonicalManifestBranch = Boolean(canonicalCheckout);
|
|
760
|
+
const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
|
|
761
|
+
if (usesCanonicalRemote) {
|
|
762
|
+
return {
|
|
763
|
+
remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
764
|
+
authHeader: `Authorization: Bearer ${input.token}`,
|
|
765
|
+
persistAuth: false,
|
|
766
|
+
reconcileExistingOrigin: isCanonicalManifestBranch,
|
|
767
|
+
requireRemoteBranch: isCanonicalManifestBranch,
|
|
768
|
+
requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
773
|
+
authHeader: input.manifest?.repoAuthHeader ?? null,
|
|
774
|
+
persistAuth: true,
|
|
775
|
+
reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
|
|
776
|
+
requireRemoteBranch: false,
|
|
777
|
+
requiredBaseCommit: null
|
|
778
|
+
};
|
|
657
779
|
}
|
|
658
780
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
659
781
|
try {
|
|
@@ -682,6 +804,10 @@ function ensureOriginRemote(branchPath, remoteUrl) {
|
|
|
682
804
|
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
683
805
|
}
|
|
684
806
|
}
|
|
807
|
+
function originRemoteUrl(branchPath) {
|
|
808
|
+
if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
|
|
809
|
+
return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
|
|
810
|
+
}
|
|
685
811
|
function shellQuote(value) {
|
|
686
812
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
687
813
|
}
|
|
@@ -750,6 +876,18 @@ function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
|
|
|
750
876
|
}
|
|
751
877
|
runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
|
|
752
878
|
}
|
|
879
|
+
function clearVisibleGitAuthentication(branchPath) {
|
|
880
|
+
let extraHeaderKeys = [];
|
|
881
|
+
try {
|
|
882
|
+
extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
for (const key of extraHeaderKeys) {
|
|
886
|
+
tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
|
|
887
|
+
}
|
|
888
|
+
runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
|
|
889
|
+
tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
|
|
890
|
+
}
|
|
753
891
|
function dockerConfigPath() {
|
|
754
892
|
return path.join(os.homedir(), ".docker", "config.json");
|
|
755
893
|
}
|
|
@@ -838,18 +976,58 @@ function checkoutVisibleBranch(input) {
|
|
|
838
976
|
});
|
|
839
977
|
const clean = !hasWorktreeChanges(input.branchPath);
|
|
840
978
|
if (remoteBranchExists && clean) {
|
|
841
|
-
runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
979
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
980
|
+
cwd: input.branchPath
|
|
981
|
+
});
|
|
842
982
|
return;
|
|
843
983
|
}
|
|
844
984
|
if (branchExists) {
|
|
845
|
-
runGit(["checkout", input.branchName], { cwd: input.branchPath });
|
|
985
|
+
runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
|
|
846
986
|
return;
|
|
847
987
|
}
|
|
848
988
|
if (defaultRemoteExists) {
|
|
849
|
-
runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], {
|
|
989
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
|
|
990
|
+
cwd: input.branchPath
|
|
991
|
+
});
|
|
850
992
|
return;
|
|
851
993
|
}
|
|
852
|
-
runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
|
|
994
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
|
|
995
|
+
}
|
|
996
|
+
function migrateExistingCheckoutToRequiredRemote(input) {
|
|
997
|
+
if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
|
|
998
|
+
if (hasWorktreeChanges(input.branchPath)) {
|
|
999
|
+
throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
|
|
1000
|
+
}
|
|
1001
|
+
runGit(
|
|
1002
|
+
visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
|
|
1003
|
+
{
|
|
1004
|
+
cwd: input.branchPath,
|
|
1005
|
+
auth: input.remoteAuth
|
|
1006
|
+
}
|
|
1007
|
+
);
|
|
1008
|
+
const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
|
|
1009
|
+
if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
|
|
1010
|
+
throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
|
|
1011
|
+
}
|
|
1012
|
+
if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
|
|
1013
|
+
throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
|
|
1016
|
+
const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
|
|
1017
|
+
runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
|
|
1018
|
+
}
|
|
1019
|
+
ensureOriginRemote(input.branchPath, input.remoteUrl);
|
|
1020
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
1021
|
+
cwd: input.branchPath
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function assertCheckoutDerivedFromRequiredBase(input) {
|
|
1025
|
+
const remoteRevision = `refs/remotes/origin/${input.branchName}`;
|
|
1026
|
+
for (const revision of [remoteRevision, "HEAD"]) {
|
|
1027
|
+
if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
|
|
1028
|
+
throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
853
1031
|
}
|
|
854
1032
|
function ensureVisibleGitCheckout(input) {
|
|
855
1033
|
validateBranchName(input.branchName);
|
|
@@ -859,40 +1037,114 @@ function ensureVisibleGitCheckout(input) {
|
|
|
859
1037
|
if (createdCheckout) {
|
|
860
1038
|
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
861
1039
|
fs.mkdirSync(path.dirname(branchPath), { recursive: true });
|
|
862
|
-
|
|
1040
|
+
const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
|
|
1041
|
+
auth: input.remoteAuth
|
|
1042
|
+
});
|
|
1043
|
+
if (!cloned && input.requireRemoteBranch) {
|
|
1044
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1045
|
+
throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (!cloned) {
|
|
863
1048
|
fs.mkdirSync(branchPath, { recursive: true });
|
|
864
1049
|
runGit(["init"], { cwd: branchPath });
|
|
865
1050
|
}
|
|
866
|
-
ensureOriginRemote(branchPath, input.
|
|
867
|
-
|
|
1051
|
+
ensureOriginRemote(branchPath, input.remoteUrl);
|
|
1052
|
+
if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
|
|
1053
|
+
else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
|
|
868
1054
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
869
|
-
tryGit(
|
|
1055
|
+
tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
|
|
1056
|
+
if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
|
|
1057
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1058
|
+
throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
|
|
1059
|
+
}
|
|
870
1060
|
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
1061
|
+
if (input.requiredBaseCommit) {
|
|
1062
|
+
try {
|
|
1063
|
+
assertCheckoutDerivedFromRequiredBase({
|
|
1064
|
+
branchPath,
|
|
1065
|
+
branchName: input.branchName,
|
|
1066
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1067
|
+
});
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
871
1073
|
return branchPath;
|
|
872
1074
|
}
|
|
1075
|
+
if (input.requireRemoteBranch) {
|
|
1076
|
+
if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
|
|
1077
|
+
throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
|
|
1078
|
+
}
|
|
1079
|
+
migrateExistingCheckoutToRequiredRemote({
|
|
1080
|
+
branchPath,
|
|
1081
|
+
branchName: input.branchName,
|
|
1082
|
+
remoteUrl: input.remoteUrl,
|
|
1083
|
+
remoteAuth: input.remoteAuth,
|
|
1084
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1085
|
+
});
|
|
1086
|
+
runGit(
|
|
1087
|
+
visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
|
|
1088
|
+
{
|
|
1089
|
+
cwd: branchPath,
|
|
1090
|
+
auth: input.remoteAuth
|
|
1091
|
+
}
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
873
1094
|
if (input.reconcileExistingOrigin) {
|
|
874
|
-
ensureOriginRemote(branchPath, input.
|
|
1095
|
+
ensureOriginRemote(branchPath, input.remoteUrl);
|
|
1096
|
+
}
|
|
1097
|
+
if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
|
|
1098
|
+
throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
|
|
875
1099
|
}
|
|
876
|
-
|
|
1100
|
+
if (input.requiredBaseCommit) {
|
|
1101
|
+
assertCheckoutDerivedFromRequiredBase({
|
|
1102
|
+
branchPath,
|
|
1103
|
+
branchName: input.branchName,
|
|
1104
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
|
|
1108
|
+
else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
|
|
877
1109
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
878
1110
|
return branchPath;
|
|
879
1111
|
}
|
|
880
1112
|
function ensureBranchWorkspace(input) {
|
|
881
1113
|
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
882
|
-
const
|
|
883
|
-
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
|
|
1114
|
+
const remote = visibleCheckoutRemoteFor(input);
|
|
884
1115
|
return {
|
|
885
1116
|
branchPath: ensureVisibleGitCheckout({
|
|
886
1117
|
projectRoot: input.projectRoot,
|
|
887
1118
|
branchName: input.branchName,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1119
|
+
remoteUrl: remote.remoteUrl,
|
|
1120
|
+
remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
|
|
1121
|
+
persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
|
|
891
1122
|
defaultBranch,
|
|
892
|
-
reconcileExistingOrigin:
|
|
1123
|
+
reconcileExistingOrigin: remote.reconcileExistingOrigin,
|
|
1124
|
+
requireRemoteBranch: remote.requireRemoteBranch,
|
|
1125
|
+
requiredBaseCommit: remote.requiredBaseCommit,
|
|
1126
|
+
clearPersistentAuth: !remote.persistAuth
|
|
893
1127
|
})
|
|
894
1128
|
};
|
|
895
1129
|
}
|
|
1130
|
+
function workspaceSyncCheckoutTargets(input) {
|
|
1131
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1132
|
+
const add = (target) => {
|
|
1133
|
+
selected.set(`${target.projectId}\0${target.branchName}`, target);
|
|
1134
|
+
};
|
|
1135
|
+
if (input.canonicalCheckoutOnly) {
|
|
1136
|
+
add(input.canonicalCheckoutOnly);
|
|
1137
|
+
} else if (input.includeAllManifestCheckouts) {
|
|
1138
|
+
for (const project of input.projects) {
|
|
1139
|
+
for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
|
|
1140
|
+
}
|
|
1141
|
+
} else {
|
|
1142
|
+
for (const target of input.pendingTargets) add(target);
|
|
1143
|
+
}
|
|
1144
|
+
return [...selected.values()].sort(
|
|
1145
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
896
1148
|
function resolveCommandCwd(branchPath, cwd) {
|
|
897
1149
|
if (!cwd) {
|
|
898
1150
|
return branchPath;
|
|
@@ -1066,13 +1318,13 @@ async function withFileMutationQueue(key, operation) {
|
|
|
1066
1318
|
}
|
|
1067
1319
|
}
|
|
1068
1320
|
function mutationQueueKey(projectId, branchName, resolved) {
|
|
1069
|
-
if (resolved.scope
|
|
1070
|
-
return
|
|
1321
|
+
if (resolved.scope !== "project") {
|
|
1322
|
+
return `${resolved.scope}:${path.normalize(resolved.absolutePath)}`;
|
|
1071
1323
|
}
|
|
1072
1324
|
return `project:${projectId}:${branchName}:${path.posix.normalize(resolved.repoRelativePath)}`;
|
|
1073
1325
|
}
|
|
1074
|
-
function readWorkerTextFile(branchPath, filePath, offset, limit) {
|
|
1075
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1326
|
+
function readWorkerTextFile(branchPath, filePath, offset, limit, builtInPaths) {
|
|
1327
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1076
1328
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1077
1329
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1078
1330
|
}
|
|
@@ -1086,8 +1338,8 @@ function readWorkerTextFile(branchPath, filePath, offset, limit) {
|
|
|
1086
1338
|
...formatted
|
|
1087
1339
|
};
|
|
1088
1340
|
}
|
|
1089
|
-
function writeWorkerTextFile(branchPath, filePath, content) {
|
|
1090
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1341
|
+
function writeWorkerTextFile(branchPath, filePath, content, builtInPaths) {
|
|
1342
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1091
1343
|
fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
|
|
1092
1344
|
fs.writeFileSync(resolved.absolutePath, content, "utf8");
|
|
1093
1345
|
return {
|
|
@@ -1096,8 +1348,8 @@ function writeWorkerTextFile(branchPath, filePath, content) {
|
|
|
1096
1348
|
gitBlobHash: getGitBlobHashForContent(content)
|
|
1097
1349
|
};
|
|
1098
1350
|
}
|
|
1099
|
-
function editWorkerTextFile(branchPath, filePath, edits) {
|
|
1100
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1351
|
+
function editWorkerTextFile(branchPath, filePath, edits, builtInPaths) {
|
|
1352
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1101
1353
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
1102
1354
|
throw new Error("edit requires at least one replacement");
|
|
1103
1355
|
}
|
|
@@ -1144,46 +1396,57 @@ function editWorkerTextFile(branchPath, filePath, edits) {
|
|
|
1144
1396
|
};
|
|
1145
1397
|
}
|
|
1146
1398
|
async function executeReadFileOperation(input) {
|
|
1147
|
-
const
|
|
1399
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1148
1400
|
filePath: input.message.filePath,
|
|
1149
1401
|
baseUrl: input.baseUrl,
|
|
1150
1402
|
token: input.token,
|
|
1151
1403
|
projectId: input.projectId,
|
|
1152
1404
|
branchName: input.message.branchName,
|
|
1153
1405
|
sessionId: input.message.sessionId,
|
|
1154
|
-
|
|
1406
|
+
activePlanId: input.message.activePlanId,
|
|
1407
|
+
artifactRoot: input.artifactRoot,
|
|
1408
|
+
planRoot: input.planRoot,
|
|
1409
|
+
access: "read"
|
|
1155
1410
|
});
|
|
1156
|
-
if (artifact) {
|
|
1157
|
-
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1158
|
-
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1159
|
-
}
|
|
1160
|
-
const buffer = fs.readFileSync(artifact.absolutePath);
|
|
1161
|
-
const formatted = formatLineNumberedContent(buffer.toString("utf8"), input.message.offset, input.message.limit);
|
|
1162
|
-
return {
|
|
1163
|
-
type: "read",
|
|
1164
|
-
kind: "text",
|
|
1165
|
-
file: artifact.virtualPath,
|
|
1166
|
-
gitBlobHash: getGitBlobHashForContent(buffer),
|
|
1167
|
-
...formatted
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
1411
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1171
|
-
return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit);
|
|
1412
|
+
return readWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.offset, input.message.limit, builtInPaths);
|
|
1172
1413
|
}
|
|
1173
1414
|
async function executeWriteFileOperation(input) {
|
|
1174
|
-
|
|
1415
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1416
|
+
filePath: input.message.filePath,
|
|
1417
|
+
baseUrl: input.baseUrl,
|
|
1418
|
+
token: input.token,
|
|
1419
|
+
projectId: input.projectId,
|
|
1420
|
+
branchName: input.message.branchName,
|
|
1421
|
+
sessionId: input.message.sessionId,
|
|
1422
|
+
activePlanId: input.message.activePlanId,
|
|
1423
|
+
artifactRoot: input.artifactRoot,
|
|
1424
|
+
planRoot: input.planRoot,
|
|
1425
|
+
access: "write"
|
|
1426
|
+
});
|
|
1175
1427
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1176
|
-
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
|
|
1428
|
+
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1177
1429
|
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
|
|
1178
|
-
return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content);
|
|
1430
|
+
return writeWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.content, builtInPaths);
|
|
1179
1431
|
});
|
|
1180
1432
|
}
|
|
1181
1433
|
async function executeEditFileOperation(input) {
|
|
1182
|
-
|
|
1434
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1435
|
+
filePath: input.message.filePath,
|
|
1436
|
+
baseUrl: input.baseUrl,
|
|
1437
|
+
token: input.token,
|
|
1438
|
+
projectId: input.projectId,
|
|
1439
|
+
branchName: input.message.branchName,
|
|
1440
|
+
sessionId: input.message.sessionId,
|
|
1441
|
+
activePlanId: input.message.activePlanId,
|
|
1442
|
+
artifactRoot: input.artifactRoot,
|
|
1443
|
+
planRoot: input.planRoot,
|
|
1444
|
+
access: "write"
|
|
1445
|
+
});
|
|
1183
1446
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1184
|
-
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath);
|
|
1447
|
+
const resolved = resolveWorkerFilePath(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1185
1448
|
return withFileMutationQueue(mutationQueueKey(input.projectId, input.message.branchName, resolved), async () => {
|
|
1186
|
-
return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits);
|
|
1449
|
+
return editWorkerTextFile(workspace.branchPath, input.message.filePath, input.message.edits, builtInPaths);
|
|
1187
1450
|
});
|
|
1188
1451
|
}
|
|
1189
1452
|
const IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", ".cache", "coverage", "target"]);
|
|
@@ -1204,16 +1467,20 @@ function normalizeFindPattern(pattern) {
|
|
|
1204
1467
|
function walkWorkerEntries(branchPath, start) {
|
|
1205
1468
|
const entries = [];
|
|
1206
1469
|
const visitedDirectories = /* @__PURE__ */ new Set();
|
|
1470
|
+
const realVirtualRoot = start.scope === "virtual" ? fs.realpathSync(start.virtualRootPath) : null;
|
|
1207
1471
|
const visit = (absolutePath, isRoot) => {
|
|
1208
1472
|
let stat;
|
|
1209
1473
|
try {
|
|
1210
1474
|
stat = fs.statSync(absolutePath);
|
|
1475
|
+
if (realVirtualRoot) {
|
|
1476
|
+
assertInsideRoot(realVirtualRoot, fs.realpathSync(absolutePath), `${start.displayPath} path`);
|
|
1477
|
+
}
|
|
1211
1478
|
} catch (error) {
|
|
1212
1479
|
if (isRoot) throw error;
|
|
1213
1480
|
return;
|
|
1214
1481
|
}
|
|
1215
|
-
const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : path.resolve(absolutePath);
|
|
1216
1482
|
const hostRelativePath = path.relative(start.absolutePath, absolutePath).split(path.sep).join(path.posix.sep);
|
|
1483
|
+
const displayPath = start.scope === "project" ? toProjectDisplayPath(branchPath, absolutePath) : start.scope === "virtual" ? hostRelativePath ? `${start.displayPath}/${hostRelativePath}` : start.displayPath : path.resolve(absolutePath);
|
|
1217
1484
|
const matchPath = start.scope === "project" ? displayPath : hostRelativePath || path.basename(start.absolutePath);
|
|
1218
1485
|
entries.push({ absolutePath, displayPath, matchPath, stat });
|
|
1219
1486
|
if (!stat.isDirectory()) return;
|
|
@@ -1244,8 +1511,8 @@ function walkWorkerEntries(branchPath, start) {
|
|
|
1244
1511
|
function isProbablyText(bytes) {
|
|
1245
1512
|
return !bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0);
|
|
1246
1513
|
}
|
|
1247
|
-
function grepWorkerFiles(branchPath, input) {
|
|
1248
|
-
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1514
|
+
function grepWorkerFiles(branchPath, input, builtInPaths) {
|
|
1515
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
1249
1516
|
if (!fs.existsSync(start.absolutePath)) {
|
|
1250
1517
|
throw new Error(`Path not found: ${start.displayPath}`);
|
|
1251
1518
|
}
|
|
@@ -1291,17 +1558,34 @@ function grepWorkerFiles(branchPath, input) {
|
|
|
1291
1558
|
};
|
|
1292
1559
|
}
|
|
1293
1560
|
async function executeGrepOperation(input) {
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1561
|
+
const toolPath = input.message.path ?? ".";
|
|
1562
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1563
|
+
filePath: toolPath,
|
|
1564
|
+
baseUrl: input.baseUrl,
|
|
1565
|
+
token: input.token,
|
|
1566
|
+
projectId: input.projectId,
|
|
1567
|
+
branchName: input.message.branchName,
|
|
1568
|
+
sessionId: input.message.sessionId,
|
|
1569
|
+
activePlanId: input.message.activePlanId,
|
|
1570
|
+
artifactRoot: input.artifactRoot,
|
|
1571
|
+
planRoot: input.planRoot,
|
|
1572
|
+
access: "read"
|
|
1301
1573
|
});
|
|
1574
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1575
|
+
return grepWorkerFiles(
|
|
1576
|
+
workspace.branchPath,
|
|
1577
|
+
{
|
|
1578
|
+
pattern: input.message.pattern,
|
|
1579
|
+
path: input.message.path,
|
|
1580
|
+
glob: input.message.glob,
|
|
1581
|
+
caseSensitive: input.message.caseSensitive,
|
|
1582
|
+
limit: input.message.limit
|
|
1583
|
+
},
|
|
1584
|
+
builtInPaths
|
|
1585
|
+
);
|
|
1302
1586
|
}
|
|
1303
|
-
function findWorkerFiles(branchPath, input) {
|
|
1304
|
-
const start = resolveWorkerFilePath(branchPath, input.path ?? ".");
|
|
1587
|
+
function findWorkerFiles(branchPath, input, builtInPaths) {
|
|
1588
|
+
const start = resolveWorkerFilePath(branchPath, input.path ?? ".", builtInPaths);
|
|
1305
1589
|
if (!fs.existsSync(start.absolutePath) || !fs.statSync(start.absolutePath).isDirectory()) {
|
|
1306
1590
|
throw new Error(`Directory not found: ${start.displayPath}`);
|
|
1307
1591
|
}
|
|
@@ -1335,16 +1619,33 @@ function findWorkerFiles(branchPath, input) {
|
|
|
1335
1619
|
};
|
|
1336
1620
|
}
|
|
1337
1621
|
async function executeFindOperation(input) {
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1622
|
+
const toolPath = input.message.path ?? ".";
|
|
1623
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1624
|
+
filePath: toolPath,
|
|
1625
|
+
baseUrl: input.baseUrl,
|
|
1626
|
+
token: input.token,
|
|
1627
|
+
projectId: input.projectId,
|
|
1628
|
+
branchName: input.message.branchName,
|
|
1629
|
+
sessionId: input.message.sessionId,
|
|
1630
|
+
activePlanId: input.message.activePlanId,
|
|
1631
|
+
artifactRoot: input.artifactRoot,
|
|
1632
|
+
planRoot: input.planRoot,
|
|
1633
|
+
access: "read"
|
|
1344
1634
|
});
|
|
1635
|
+
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1636
|
+
return findWorkerFiles(
|
|
1637
|
+
workspace.branchPath,
|
|
1638
|
+
{
|
|
1639
|
+
pattern: input.message.pattern,
|
|
1640
|
+
path: input.message.path,
|
|
1641
|
+
entryType: input.message.entryType,
|
|
1642
|
+
limit: input.message.limit
|
|
1643
|
+
},
|
|
1644
|
+
builtInPaths
|
|
1645
|
+
);
|
|
1345
1646
|
}
|
|
1346
|
-
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
|
|
1347
|
-
const resolved = resolveWorkerFilePath(branchPath, inputPath);
|
|
1647
|
+
function listWorkerDirectory(branchPath, inputPath = ".", inputLimit, builtInPaths) {
|
|
1648
|
+
const resolved = resolveWorkerFilePath(branchPath, inputPath, builtInPaths);
|
|
1348
1649
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isDirectory()) {
|
|
1349
1650
|
throw new Error(`Directory not found: ${resolved.displayPath}`);
|
|
1350
1651
|
}
|
|
@@ -1362,11 +1663,24 @@ function listWorkerDirectory(branchPath, inputPath = ".", inputLimit) {
|
|
|
1362
1663
|
};
|
|
1363
1664
|
}
|
|
1364
1665
|
async function executeLsOperation(input) {
|
|
1666
|
+
const toolPath = input.message.path ?? ".";
|
|
1667
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1668
|
+
filePath: toolPath,
|
|
1669
|
+
baseUrl: input.baseUrl,
|
|
1670
|
+
token: input.token,
|
|
1671
|
+
projectId: input.projectId,
|
|
1672
|
+
branchName: input.message.branchName,
|
|
1673
|
+
sessionId: input.message.sessionId,
|
|
1674
|
+
activePlanId: input.message.activePlanId,
|
|
1675
|
+
artifactRoot: input.artifactRoot,
|
|
1676
|
+
planRoot: input.planRoot,
|
|
1677
|
+
access: "read"
|
|
1678
|
+
});
|
|
1365
1679
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1366
|
-
return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit);
|
|
1680
|
+
return listWorkerDirectory(workspace.branchPath, input.message.path, input.message.limit, builtInPaths);
|
|
1367
1681
|
}
|
|
1368
|
-
function readWorkerImageFile(branchPath, filePath) {
|
|
1369
|
-
const resolved = resolveWorkerFilePath(branchPath, filePath);
|
|
1682
|
+
function readWorkerImageFile(branchPath, filePath, builtInPaths) {
|
|
1683
|
+
const resolved = resolveWorkerFilePath(branchPath, filePath, builtInPaths);
|
|
1370
1684
|
if (!fs.existsSync(resolved.absolutePath) || !fs.statSync(resolved.absolutePath).isFile()) {
|
|
1371
1685
|
throw new Error(`File not found: ${resolved.displayPath}`);
|
|
1372
1686
|
}
|
|
@@ -1388,38 +1702,20 @@ function readWorkerImageFile(branchPath, filePath) {
|
|
|
1388
1702
|
};
|
|
1389
1703
|
}
|
|
1390
1704
|
async function executeViewFileBytesOperation(input) {
|
|
1391
|
-
const
|
|
1705
|
+
const builtInPaths = await prepareBuiltInToolPaths({
|
|
1392
1706
|
filePath: input.message.filePath,
|
|
1393
1707
|
baseUrl: input.baseUrl,
|
|
1394
1708
|
token: input.token,
|
|
1395
1709
|
projectId: input.projectId,
|
|
1396
1710
|
branchName: input.message.branchName,
|
|
1397
1711
|
sessionId: input.message.sessionId,
|
|
1398
|
-
|
|
1712
|
+
activePlanId: input.message.activePlanId,
|
|
1713
|
+
artifactRoot: input.artifactRoot,
|
|
1714
|
+
planRoot: input.planRoot,
|
|
1715
|
+
access: "read"
|
|
1399
1716
|
});
|
|
1400
|
-
if (artifact) {
|
|
1401
|
-
if (!fs.existsSync(artifact.absolutePath) || !fs.statSync(artifact.absolutePath).isFile()) {
|
|
1402
|
-
throw new Error(`File not found: ${artifact.virtualPath}`);
|
|
1403
|
-
}
|
|
1404
|
-
const extension = path.extname(artifact.absolutePath).toLowerCase();
|
|
1405
|
-
const mediaType = extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".png" ? "image/png" : null;
|
|
1406
|
-
if (!mediaType) {
|
|
1407
|
-
throw new Error("read supports PNG and JPEG image inputs only");
|
|
1408
|
-
}
|
|
1409
|
-
const bytes = fs.readFileSync(artifact.absolutePath);
|
|
1410
|
-
const dimensions = readImageDimensions(bytes, mediaType);
|
|
1411
|
-
return {
|
|
1412
|
-
type: "view_file_bytes",
|
|
1413
|
-
filePath: artifact.virtualPath,
|
|
1414
|
-
mediaType,
|
|
1415
|
-
base64: bytes.toString("base64"),
|
|
1416
|
-
fileSizeBytes: bytes.length,
|
|
1417
|
-
width: dimensions.width,
|
|
1418
|
-
height: dimensions.height
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
1717
|
const workspace = ensureOperationBranch({ ...input, branchName: input.message.branchName });
|
|
1422
|
-
return readWorkerImageFile(workspace.branchPath, input.message.filePath);
|
|
1718
|
+
return readWorkerImageFile(workspace.branchPath, input.message.filePath, builtInPaths);
|
|
1423
1719
|
}
|
|
1424
1720
|
async function executeOperation(input) {
|
|
1425
1721
|
switch (input.message.type) {
|
|
@@ -2058,6 +2354,7 @@ async function startWorker(options) {
|
|
|
2058
2354
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
2059
2355
|
fs.mkdirSync(planRoot, { recursive: true });
|
|
2060
2356
|
const manifestByProjectId = /* @__PURE__ */ new Map();
|
|
2357
|
+
const pendingManifestCheckouts = /* @__PURE__ */ new Map();
|
|
2061
2358
|
const workspaceSyncSingleFlight = new WorkspaceSyncSingleFlight();
|
|
2062
2359
|
let workspaceRemoteUrl = null;
|
|
2063
2360
|
let activeWorkspaceIncidentId = null;
|
|
@@ -2071,6 +2368,7 @@ async function startWorker(options) {
|
|
|
2071
2368
|
let reloadAfterClose = false;
|
|
2072
2369
|
const workspaceSyncInput = (trigger, overrides = {}) => {
|
|
2073
2370
|
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2371
|
+
const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
2074
2372
|
return {
|
|
2075
2373
|
workerLabel: label,
|
|
2076
2374
|
remoteUrl: workspaceRemoteUrl,
|
|
@@ -2078,7 +2376,7 @@ async function startWorker(options) {
|
|
|
2078
2376
|
projectsRoot,
|
|
2079
2377
|
plansRoot: planRoot,
|
|
2080
2378
|
shadowRoot: workspaceShadowRoot,
|
|
2081
|
-
projects
|
|
2379
|
+
projects,
|
|
2082
2380
|
trigger,
|
|
2083
2381
|
...overrides
|
|
2084
2382
|
};
|
|
@@ -2086,37 +2384,89 @@ async function startWorker(options) {
|
|
|
2086
2384
|
const sendWorkspaceSyncResult = (requestId, result) => {
|
|
2087
2385
|
sendWorkerMessage(ws, { type: "workspace_sync_result", requestId, result });
|
|
2088
2386
|
};
|
|
2089
|
-
const
|
|
2387
|
+
const manifestCheckoutKey = (projectId, branchName) => `${projectId}\0${branchName}`;
|
|
2388
|
+
const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
|
|
2389
|
+
(manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
|
|
2390
|
+
);
|
|
2391
|
+
const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
|
|
2090
2392
|
const created = [];
|
|
2091
|
-
for (const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
projectRoot: projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId),
|
|
2098
|
-
branchName,
|
|
2099
|
-
githubRemoteUrl,
|
|
2100
|
-
githubAuth: gitAuthForRemote(githubRemoteUrl, manifest.repoAuthHeader),
|
|
2101
|
-
githubAuthHeader: manifest.repoAuthHeader,
|
|
2102
|
-
defaultBranch: manifest.defaultBranch || "main",
|
|
2103
|
-
reconcileExistingOrigin: Boolean(manifest.repoHttpUrl)
|
|
2104
|
-
});
|
|
2105
|
-
if (!existed) created.push({ projectId: manifest.projectId, branchName });
|
|
2393
|
+
for (const target of targets) {
|
|
2394
|
+
const manifest = manifestByProjectId.get(target.projectId);
|
|
2395
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2396
|
+
if (!manifest || !manifest.branches.includes(target.branchName)) {
|
|
2397
|
+
pendingManifestCheckouts.delete(key);
|
|
2398
|
+
continue;
|
|
2106
2399
|
}
|
|
2400
|
+
const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
|
|
2401
|
+
const branchPath = path.join(projectRoot, target.branchName);
|
|
2402
|
+
const existed = hasNormalVisibleGitDir(branchPath);
|
|
2403
|
+
const remote = visibleCheckoutRemoteFor({
|
|
2404
|
+
baseUrl,
|
|
2405
|
+
token,
|
|
2406
|
+
projectId: manifest.projectId,
|
|
2407
|
+
branchName: target.branchName,
|
|
2408
|
+
manifest
|
|
2409
|
+
});
|
|
2410
|
+
ensureVisibleGitCheckout({
|
|
2411
|
+
projectRoot,
|
|
2412
|
+
branchName: target.branchName,
|
|
2413
|
+
remoteUrl: remote.remoteUrl,
|
|
2414
|
+
remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
|
|
2415
|
+
persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
|
|
2416
|
+
defaultBranch: manifest.defaultBranch || "main",
|
|
2417
|
+
reconcileExistingOrigin: remote.reconcileExistingOrigin,
|
|
2418
|
+
requireRemoteBranch: remote.requireRemoteBranch,
|
|
2419
|
+
requiredBaseCommit: remote.requiredBaseCommit,
|
|
2420
|
+
allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
|
|
2421
|
+
clearPersistentAuth: !remote.persistAuth
|
|
2422
|
+
});
|
|
2423
|
+
if (!existed) created.push(target);
|
|
2107
2424
|
}
|
|
2108
2425
|
return created;
|
|
2109
2426
|
};
|
|
2110
2427
|
const runRequestedWorkspaceSync = async (requestId, trigger, overrides = {}) => {
|
|
2111
2428
|
workspaceSyncRequestsInFlight += 1;
|
|
2112
2429
|
try {
|
|
2113
|
-
|
|
2114
|
-
const result = await workspaceSyncSingleFlight.
|
|
2115
|
-
|
|
2430
|
+
let pendingTargets = [];
|
|
2431
|
+
const result = await workspaceSyncSingleFlight.runPrepared(() => {
|
|
2432
|
+
const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
|
|
2433
|
+
const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
2434
|
+
const allowedCheckoutKeys = new Set(
|
|
2435
|
+
syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
|
|
2436
|
+
);
|
|
2437
|
+
const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
|
|
2438
|
+
(target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
|
|
2439
|
+
);
|
|
2440
|
+
let checkoutTargets = [];
|
|
2441
|
+
if (shouldEnsureCheckouts) {
|
|
2442
|
+
checkoutTargets = workspaceSyncCheckoutTargets({
|
|
2443
|
+
projects: syncProjects,
|
|
2444
|
+
pendingTargets: scopedPendingTargets,
|
|
2445
|
+
includeAllManifestCheckouts: trigger.type === "connect",
|
|
2446
|
+
...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
|
|
2450
|
+
const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
|
|
2451
|
+
strictCanonicalRevalidation: true
|
|
2452
|
+
});
|
|
2453
|
+
const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
|
|
2454
|
+
for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
|
|
2455
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2456
|
+
if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
|
|
2457
|
+
}
|
|
2458
|
+
const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
|
|
2459
|
+
return workspaceSyncInput(trigger, {
|
|
2116
2460
|
...overrides,
|
|
2117
2461
|
...newVisibleCheckouts.length > 0 ? { newVisibleCheckouts } : {}
|
|
2118
|
-
})
|
|
2119
|
-
);
|
|
2462
|
+
});
|
|
2463
|
+
});
|
|
2464
|
+
if (result.outcome === "no_change" || result.outcome === "published" || result.outcome === "updated" || result.outcome === "conflict_reset") {
|
|
2465
|
+
for (const target of pendingTargets) {
|
|
2466
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2467
|
+
if (pendingManifestCheckouts.get(key) === target) pendingManifestCheckouts.delete(key);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2120
2470
|
previousPeriodicFingerprint = null;
|
|
2121
2471
|
sendWorkspaceSyncResult(requestId, result);
|
|
2122
2472
|
return result;
|
|
@@ -2179,7 +2529,8 @@ async function startWorker(options) {
|
|
|
2179
2529
|
version: getWorkerVersion(),
|
|
2180
2530
|
r5dctlVersion: readInstalledCliVersion("r5dctl"),
|
|
2181
2531
|
capabilities: {
|
|
2182
|
-
updateClis: true
|
|
2532
|
+
updateClis: true,
|
|
2533
|
+
canonicalResolverCheckout: true
|
|
2183
2534
|
},
|
|
2184
2535
|
projectRoot: projectsRoot,
|
|
2185
2536
|
artifactRoot,
|
|
@@ -2209,9 +2560,26 @@ async function startWorker(options) {
|
|
|
2209
2560
|
configureGitHubAuth(message.githubCredential);
|
|
2210
2561
|
visibleGitIdentity = message.gitIdentity;
|
|
2211
2562
|
workspaceRemoteUrl = message.workspaceRemoteUrl;
|
|
2563
|
+
const previousCheckoutKeys = new Set(
|
|
2564
|
+
allManifestCheckouts().map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2565
|
+
);
|
|
2212
2566
|
const migratedProjectIds = migrateLegacyProjectRoots(projectsRoot, message.projects);
|
|
2213
2567
|
manifestByProjectId.clear();
|
|
2214
2568
|
for (const project of message.projects) manifestByProjectId.set(project.projectId, project);
|
|
2569
|
+
const currentCheckouts = allManifestCheckouts();
|
|
2570
|
+
const currentCheckoutKeys = new Set(
|
|
2571
|
+
currentCheckouts.map(({ projectId, branchName }) => manifestCheckoutKey(projectId, branchName))
|
|
2572
|
+
);
|
|
2573
|
+
for (const key of pendingManifestCheckouts.keys()) {
|
|
2574
|
+
if (!currentCheckoutKeys.has(key)) pendingManifestCheckouts.delete(key);
|
|
2575
|
+
}
|
|
2576
|
+
for (const checkout of currentCheckouts) {
|
|
2577
|
+
const key = manifestCheckoutKey(checkout.projectId, checkout.branchName);
|
|
2578
|
+
if (previousCheckoutKeys.has(key)) continue;
|
|
2579
|
+
const branchPath = path.join(projectRootFor(projectsRoot, checkout.projectId, manifestByProjectId), checkout.branchName);
|
|
2580
|
+
if (!hasNormalVisibleGitDir(branchPath)) pendingManifestCheckouts.set(key, checkout);
|
|
2581
|
+
}
|
|
2582
|
+
previousPeriodicFingerprint = null;
|
|
2215
2583
|
process.stdout.write(
|
|
2216
2584
|
`[r5d-worker] workspace manifest: ${message.projects.length} projects${migratedProjectIds.length > 0 ? `; migrated ${migratedProjectIds.length} project checkout root(s)` : ""}
|
|
2217
2585
|
`
|
|
@@ -2222,8 +2590,23 @@ async function startWorker(options) {
|
|
|
2222
2590
|
if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
|
|
2223
2591
|
periodicWorkspaceScanInFlight = true;
|
|
2224
2592
|
void (async () => {
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2593
|
+
const genericCheckoutKeys = new Set(
|
|
2594
|
+
workspaceProjectsForSync([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
|
|
2595
|
+
(project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
|
|
2596
|
+
)
|
|
2597
|
+
);
|
|
2598
|
+
const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
|
|
2599
|
+
(target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
|
|
2600
|
+
);
|
|
2601
|
+
if (hasPendingGenericCheckout) {
|
|
2602
|
+
previousPeriodicFingerprint = null;
|
|
2603
|
+
await runRequestedWorkspaceSync(crypto.randomUUID(), {
|
|
2604
|
+
type: "periodic",
|
|
2605
|
+
detail: "workspace manifest additions"
|
|
2606
|
+
});
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
const fingerprint = await workspaceSyncSingleFlight.fingerprintPrepared(() => workspaceSyncInput({ type: "periodic" }));
|
|
2227
2610
|
if (fingerprint === emptyFingerprint) {
|
|
2228
2611
|
previousPeriodicFingerprint = null;
|
|
2229
2612
|
return;
|
|
@@ -2490,6 +2873,7 @@ async function startWorker(options) {
|
|
|
2490
2873
|
projectRoot,
|
|
2491
2874
|
syncRoot,
|
|
2492
2875
|
artifactRoot,
|
|
2876
|
+
planRoot,
|
|
2493
2877
|
manifest
|
|
2494
2878
|
});
|
|
2495
2879
|
ws.send(
|
|
@@ -2631,5 +3015,8 @@ export {
|
|
|
2631
3015
|
resolveHostShell,
|
|
2632
3016
|
resolveWorkerFilePath,
|
|
2633
3017
|
syncSessionArtifacts,
|
|
3018
|
+
visibleCheckoutGitTestHarness,
|
|
3019
|
+
visibleCheckoutRemoteFor,
|
|
3020
|
+
workspaceSyncCheckoutTargets,
|
|
2634
3021
|
writeWorkerTextFile
|
|
2635
3022
|
};
|