@ricsam/r5d-worker 0.0.46 → 0.0.48
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/README.md +2 -0
- package/dist/cjs/main.cjs +258 -33
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/port-forward-client.cjs +148 -0
- package/dist/cjs/workspace-sync.cjs +619 -57
- package/dist/mjs/main.mjs +257 -34
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/port-forward-client.mjs +114 -0
- package/dist/mjs/workspace-sync.mjs +616 -56
- package/dist/types/main.d.ts +40 -3
- package/dist/types/port-forward-client.d.ts +8 -0
- package/dist/types/workspace-sync.d.ts +30 -3
- package/package.json +1 -1
package/dist/mjs/main.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { installCliUpdate, readInstalledCliVersion } from "./cli-update.mjs";
|
|
|
8
8
|
import { configureVisibleGitIdentity } from "./git-identity.mjs";
|
|
9
9
|
import { hasWorkerHeartbeatTimedOut, WORKER_HEARTBEAT_INTERVAL_MS } from "./heartbeat.mjs";
|
|
10
10
|
import { terminateProcessTree } from "./process-tree.mjs";
|
|
11
|
+
import { openWorkerPortForwardRelay } from "./port-forward-client.mjs";
|
|
11
12
|
import {
|
|
12
13
|
isRetryableWorkerServerStatus,
|
|
13
14
|
superviseWorkerRuntime,
|
|
@@ -21,7 +22,8 @@ import {
|
|
|
21
22
|
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
22
23
|
WorkspaceSyncSingleFlight,
|
|
23
24
|
calculateWorkspaceDiffFingerprint,
|
|
24
|
-
synchronizeWorkspace
|
|
25
|
+
synchronizeWorkspace,
|
|
26
|
+
workspaceProjectsForSync
|
|
25
27
|
} from "./workspace-sync.mjs";
|
|
26
28
|
class WorkerServerUnavailableError extends Error {
|
|
27
29
|
name = "WorkerServerUnavailableError";
|
|
@@ -578,6 +580,14 @@ function validatePlanId(planId) {
|
|
|
578
580
|
throw new Error(`Invalid plan id from server: ${planId}`);
|
|
579
581
|
}
|
|
580
582
|
}
|
|
583
|
+
const NON_RECURSIVE_GIT_CONFIG_ARGS = [
|
|
584
|
+
"-c",
|
|
585
|
+
"submodule.recurse=false",
|
|
586
|
+
"-c",
|
|
587
|
+
"fetch.recurseSubmodules=false",
|
|
588
|
+
"-c",
|
|
589
|
+
"push.recurseSubmodules=false"
|
|
590
|
+
];
|
|
581
591
|
function gitExtraHeaderConfigKey(extraHeaderUrl) {
|
|
582
592
|
return `http.${normalizeBaseUrl(extraHeaderUrl)}/.extraHeader`;
|
|
583
593
|
}
|
|
@@ -585,10 +595,23 @@ function gitAuthArgs(auth) {
|
|
|
585
595
|
if (!auth) {
|
|
586
596
|
return [];
|
|
587
597
|
}
|
|
588
|
-
|
|
598
|
+
const scopedKey = gitExtraHeaderConfigKey(auth.extraHeaderUrl);
|
|
599
|
+
return ["-c", "http.extraHeader=", "-c", `${scopedKey}=`, "-c", `${scopedKey}=${auth.header}`];
|
|
600
|
+
}
|
|
601
|
+
const visibleCheckoutGitTestHarness = {
|
|
602
|
+
gitAuthArgs,
|
|
603
|
+
commandArgs: (args, auth) => ["git", ...NON_RECURSIVE_GIT_CONFIG_ARGS, ...gitAuthArgs(auth), ...args],
|
|
604
|
+
cloneArgs: (remoteUrl, branchPath) => ["clone", "--no-recurse-submodules", "--origin", "origin", remoteUrl, branchPath],
|
|
605
|
+
fetchArgs: (...args) => ["fetch", "--no-recurse-submodules", ...args]
|
|
606
|
+
};
|
|
607
|
+
function visibleCheckoutCloneArgs(remoteUrl, branchPath) {
|
|
608
|
+
return visibleCheckoutGitTestHarness.cloneArgs(remoteUrl, branchPath);
|
|
609
|
+
}
|
|
610
|
+
function visibleCheckoutFetchArgs(...args) {
|
|
611
|
+
return visibleCheckoutGitTestHarness.fetchArgs(...args);
|
|
589
612
|
}
|
|
590
613
|
function runGit(args, options = {}) {
|
|
591
|
-
const command =
|
|
614
|
+
const command = visibleCheckoutGitTestHarness.commandArgs(args, options.auth);
|
|
592
615
|
const result = Bun.spawnSync(command, {
|
|
593
616
|
cwd: options.cwd,
|
|
594
617
|
stdout: "pipe",
|
|
@@ -732,8 +755,28 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
|
732
755
|
function fallbackInternalRemoteUrl(baseUrl, projectId) {
|
|
733
756
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
734
757
|
}
|
|
735
|
-
function
|
|
736
|
-
|
|
758
|
+
function visibleCheckoutRemoteFor(input) {
|
|
759
|
+
const canonicalCheckout = input.manifest?.canonicalCheckouts?.find((checkout) => checkout.branchName === input.branchName);
|
|
760
|
+
const isCanonicalManifestBranch = Boolean(canonicalCheckout);
|
|
761
|
+
const usesCanonicalRemote = isCanonicalManifestBranch || !input.manifest?.repoHttpUrl;
|
|
762
|
+
if (usesCanonicalRemote) {
|
|
763
|
+
return {
|
|
764
|
+
remoteUrl: fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
765
|
+
authHeader: `Authorization: Bearer ${input.token}`,
|
|
766
|
+
persistAuth: false,
|
|
767
|
+
reconcileExistingOrigin: isCanonicalManifestBranch,
|
|
768
|
+
requireRemoteBranch: isCanonicalManifestBranch,
|
|
769
|
+
requiredBaseCommit: canonicalCheckout?.scaffoldCommitHash ?? null
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return {
|
|
773
|
+
remoteUrl: input.manifest?.repoHttpUrl ?? fallbackInternalRemoteUrl(input.baseUrl, input.projectId),
|
|
774
|
+
authHeader: input.manifest?.repoAuthHeader ?? null,
|
|
775
|
+
persistAuth: true,
|
|
776
|
+
reconcileExistingOrigin: Boolean(input.manifest?.repoHttpUrl),
|
|
777
|
+
requireRemoteBranch: false,
|
|
778
|
+
requiredBaseCommit: null
|
|
779
|
+
};
|
|
737
780
|
}
|
|
738
781
|
function gitExtraHeaderUrlForRemote(remoteUrl) {
|
|
739
782
|
try {
|
|
@@ -762,6 +805,10 @@ function ensureOriginRemote(branchPath, remoteUrl) {
|
|
|
762
805
|
runGit(["remote", "add", "origin", remoteUrl], { cwd: branchPath });
|
|
763
806
|
}
|
|
764
807
|
}
|
|
808
|
+
function originRemoteUrl(branchPath) {
|
|
809
|
+
if (!tryGit(["remote", "get-url", "origin"], { cwd: branchPath })) return null;
|
|
810
|
+
return runGit(["remote", "get-url", "origin"], { cwd: branchPath });
|
|
811
|
+
}
|
|
765
812
|
function shellQuote(value) {
|
|
766
813
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
767
814
|
}
|
|
@@ -830,6 +877,18 @@ function configureVisibleGitHubAuth(branchPath, remoteUrl, authHeader) {
|
|
|
830
877
|
}
|
|
831
878
|
runGit(["config", `${gitExtraHeaderConfigKey(gitExtraHeaderUrlForRemote(remoteUrl))}`, authHeader], { cwd: branchPath });
|
|
832
879
|
}
|
|
880
|
+
function clearVisibleGitAuthentication(branchPath) {
|
|
881
|
+
let extraHeaderKeys = [];
|
|
882
|
+
try {
|
|
883
|
+
extraHeaderKeys = runGit(["config", "--local", "--name-only", "--get-regexp", "^http\\..*extraheader$"], { cwd: branchPath }).split(/\r?\n/).filter(Boolean);
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
886
|
+
for (const key of extraHeaderKeys) {
|
|
887
|
+
tryGit(["config", "--local", "--unset-all", key], { cwd: branchPath });
|
|
888
|
+
}
|
|
889
|
+
runGit(["config", "--local", "--replace-all", "credential.helper", ""], { cwd: branchPath });
|
|
890
|
+
tryGit(["config", "--local", "--unset-all", "credential.useHttpPath"], { cwd: branchPath });
|
|
891
|
+
}
|
|
833
892
|
function dockerConfigPath() {
|
|
834
893
|
return path.join(os.homedir(), ".docker", "config.json");
|
|
835
894
|
}
|
|
@@ -918,18 +977,58 @@ function checkoutVisibleBranch(input) {
|
|
|
918
977
|
});
|
|
919
978
|
const clean = !hasWorktreeChanges(input.branchPath);
|
|
920
979
|
if (remoteBranchExists && clean) {
|
|
921
|
-
runGit(["checkout", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
980
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
981
|
+
cwd: input.branchPath
|
|
982
|
+
});
|
|
922
983
|
return;
|
|
923
984
|
}
|
|
924
985
|
if (branchExists) {
|
|
925
|
-
runGit(["checkout", input.branchName], { cwd: input.branchPath });
|
|
986
|
+
runGit(["checkout", "--no-recurse-submodules", input.branchName], { cwd: input.branchPath });
|
|
926
987
|
return;
|
|
927
988
|
}
|
|
928
989
|
if (defaultRemoteExists) {
|
|
929
|
-
runGit(["checkout", "-B", input.branchName, `origin/${input.defaultBranch}`], {
|
|
990
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.defaultBranch}`], {
|
|
991
|
+
cwd: input.branchPath
|
|
992
|
+
});
|
|
930
993
|
return;
|
|
931
994
|
}
|
|
932
|
-
runGit(["checkout", "-B", input.branchName], { cwd: input.branchPath });
|
|
995
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName], { cwd: input.branchPath });
|
|
996
|
+
}
|
|
997
|
+
function migrateExistingCheckoutToRequiredRemote(input) {
|
|
998
|
+
if (originRemoteUrl(input.branchPath) === input.remoteUrl) return;
|
|
999
|
+
if (hasWorktreeChanges(input.branchPath)) {
|
|
1000
|
+
throw new Error(`Cannot migrate dirty checkout for ${input.branchName} to its canonical remote`);
|
|
1001
|
+
}
|
|
1002
|
+
runGit(
|
|
1003
|
+
visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
|
|
1004
|
+
{
|
|
1005
|
+
cwd: input.branchPath,
|
|
1006
|
+
auth: input.remoteAuth
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
const canonicalRemoteRef = `refs/remotes/origin/${input.branchName}`;
|
|
1010
|
+
if (!tryGit(["show-ref", "--verify", "--quiet", canonicalRemoteRef], { cwd: input.branchPath })) {
|
|
1011
|
+
throw new Error(`Canonical branch ${input.branchName} is unavailable during checkout migration`);
|
|
1012
|
+
}
|
|
1013
|
+
if (input.requiredBaseCommit && (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, canonicalRemoteRef], { cwd: input.branchPath }))) {
|
|
1014
|
+
throw new Error(`Canonical branch ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (tryGit(["rev-parse", "--verify", "HEAD"], { cwd: input.branchPath })) {
|
|
1017
|
+
const previousHead = runGit(["rev-parse", "HEAD"], { cwd: input.branchPath });
|
|
1018
|
+
runGit(["update-ref", `refs/r5d/pre-canonical-checkout/${previousHead}`, previousHead], { cwd: input.branchPath });
|
|
1019
|
+
}
|
|
1020
|
+
ensureOriginRemote(input.branchPath, input.remoteUrl);
|
|
1021
|
+
runGit(["checkout", "--no-recurse-submodules", "-B", input.branchName, `origin/${input.branchName}`], {
|
|
1022
|
+
cwd: input.branchPath
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function assertCheckoutDerivedFromRequiredBase(input) {
|
|
1026
|
+
const remoteRevision = `refs/remotes/origin/${input.branchName}`;
|
|
1027
|
+
for (const revision of [remoteRevision, "HEAD"]) {
|
|
1028
|
+
if (!tryGit(["cat-file", "-e", `${input.requiredBaseCommit}^{commit}`], { cwd: input.branchPath }) || !tryGit(["merge-base", "--is-ancestor", input.requiredBaseCommit, revision], { cwd: input.branchPath })) {
|
|
1029
|
+
throw new Error(`Canonical checkout ${input.branchName} is not derived from required scaffold ${input.requiredBaseCommit}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
933
1032
|
}
|
|
934
1033
|
function ensureVisibleGitCheckout(input) {
|
|
935
1034
|
validateBranchName(input.branchName);
|
|
@@ -939,40 +1038,114 @@ function ensureVisibleGitCheckout(input) {
|
|
|
939
1038
|
if (createdCheckout) {
|
|
940
1039
|
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
941
1040
|
fs.mkdirSync(path.dirname(branchPath), { recursive: true });
|
|
942
|
-
|
|
1041
|
+
const cloned = tryGit(visibleCheckoutCloneArgs(input.remoteUrl, branchPath), {
|
|
1042
|
+
auth: input.remoteAuth
|
|
1043
|
+
});
|
|
1044
|
+
if (!cloned && input.requireRemoteBranch) {
|
|
1045
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1046
|
+
throw new Error(`Failed to clone canonical checkout for branch ${input.branchName}`);
|
|
1047
|
+
}
|
|
1048
|
+
if (!cloned) {
|
|
943
1049
|
fs.mkdirSync(branchPath, { recursive: true });
|
|
944
1050
|
runGit(["init"], { cwd: branchPath });
|
|
945
1051
|
}
|
|
946
|
-
ensureOriginRemote(branchPath, input.
|
|
947
|
-
|
|
1052
|
+
ensureOriginRemote(branchPath, input.remoteUrl);
|
|
1053
|
+
if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
|
|
1054
|
+
else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
|
|
948
1055
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
949
|
-
tryGit(
|
|
1056
|
+
tryGit(visibleCheckoutFetchArgs("origin", "--prune"), { cwd: branchPath, auth: input.remoteAuth });
|
|
1057
|
+
if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
|
|
1058
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1059
|
+
throw new Error(`Canonical branch ${input.branchName} is unavailable after clone`);
|
|
1060
|
+
}
|
|
950
1061
|
checkoutVisibleBranch({ branchPath, branchName: input.branchName, defaultBranch: input.defaultBranch });
|
|
1062
|
+
if (input.requiredBaseCommit) {
|
|
1063
|
+
try {
|
|
1064
|
+
assertCheckoutDerivedFromRequiredBase({
|
|
1065
|
+
branchPath,
|
|
1066
|
+
branchName: input.branchName,
|
|
1067
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1068
|
+
});
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
fs.rmSync(branchPath, { recursive: true, force: true });
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
951
1074
|
return branchPath;
|
|
952
1075
|
}
|
|
1076
|
+
if (input.requireRemoteBranch) {
|
|
1077
|
+
if (input.allowRequiredRemoteMigration === false && originRemoteUrl(branchPath) !== input.remoteUrl) {
|
|
1078
|
+
throw new Error(`Canonical checkout ${input.branchName} no longer uses its required remote`);
|
|
1079
|
+
}
|
|
1080
|
+
migrateExistingCheckoutToRequiredRemote({
|
|
1081
|
+
branchPath,
|
|
1082
|
+
branchName: input.branchName,
|
|
1083
|
+
remoteUrl: input.remoteUrl,
|
|
1084
|
+
remoteAuth: input.remoteAuth,
|
|
1085
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1086
|
+
});
|
|
1087
|
+
runGit(
|
|
1088
|
+
visibleCheckoutFetchArgs("--no-tags", input.remoteUrl, `+refs/heads/${input.branchName}:refs/remotes/origin/${input.branchName}`),
|
|
1089
|
+
{
|
|
1090
|
+
cwd: branchPath,
|
|
1091
|
+
auth: input.remoteAuth
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
953
1095
|
if (input.reconcileExistingOrigin) {
|
|
954
|
-
ensureOriginRemote(branchPath, input.
|
|
1096
|
+
ensureOriginRemote(branchPath, input.remoteUrl);
|
|
955
1097
|
}
|
|
956
|
-
|
|
1098
|
+
if (input.requireRemoteBranch && !tryGit(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${input.branchName}`], { cwd: branchPath })) {
|
|
1099
|
+
throw new Error(`Existing checkout for ${input.branchName} does not contain its canonical remote branch`);
|
|
1100
|
+
}
|
|
1101
|
+
if (input.requiredBaseCommit) {
|
|
1102
|
+
assertCheckoutDerivedFromRequiredBase({
|
|
1103
|
+
branchPath,
|
|
1104
|
+
branchName: input.branchName,
|
|
1105
|
+
requiredBaseCommit: input.requiredBaseCommit
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
if (input.clearPersistentAuth) clearVisibleGitAuthentication(branchPath);
|
|
1109
|
+
else configureVisibleGitHubAuth(branchPath, input.remoteUrl, input.persistentAuthHeader);
|
|
957
1110
|
configureVisibleGitIdentity(branchPath, visibleGitIdentity, runGit);
|
|
958
1111
|
return branchPath;
|
|
959
1112
|
}
|
|
960
1113
|
function ensureBranchWorkspace(input) {
|
|
961
1114
|
const defaultBranch = input.manifest?.defaultBranch || "main";
|
|
962
|
-
const
|
|
963
|
-
const githubAuth = gitAuthForRemote(githubRemoteUrl, input.manifest?.repoAuthHeader);
|
|
1115
|
+
const remote = visibleCheckoutRemoteFor(input);
|
|
964
1116
|
return {
|
|
965
1117
|
branchPath: ensureVisibleGitCheckout({
|
|
966
1118
|
projectRoot: input.projectRoot,
|
|
967
1119
|
branchName: input.branchName,
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1120
|
+
remoteUrl: remote.remoteUrl,
|
|
1121
|
+
remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
|
|
1122
|
+
persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
|
|
971
1123
|
defaultBranch,
|
|
972
|
-
reconcileExistingOrigin:
|
|
1124
|
+
reconcileExistingOrigin: remote.reconcileExistingOrigin,
|
|
1125
|
+
requireRemoteBranch: remote.requireRemoteBranch,
|
|
1126
|
+
requiredBaseCommit: remote.requiredBaseCommit,
|
|
1127
|
+
clearPersistentAuth: !remote.persistAuth
|
|
973
1128
|
})
|
|
974
1129
|
};
|
|
975
1130
|
}
|
|
1131
|
+
function workspaceSyncCheckoutTargets(input) {
|
|
1132
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1133
|
+
const add = (target) => {
|
|
1134
|
+
selected.set(`${target.projectId}\0${target.branchName}`, target);
|
|
1135
|
+
};
|
|
1136
|
+
if (input.canonicalCheckoutOnly) {
|
|
1137
|
+
add(input.canonicalCheckoutOnly);
|
|
1138
|
+
} else if (input.includeAllManifestCheckouts) {
|
|
1139
|
+
for (const project of input.projects) {
|
|
1140
|
+
for (const branchName of project.branches) add({ projectId: project.projectId, branchName });
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
for (const target of input.pendingTargets) add(target);
|
|
1144
|
+
}
|
|
1145
|
+
return [...selected.values()].sort(
|
|
1146
|
+
(left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
976
1149
|
function resolveCommandCwd(branchPath, cwd) {
|
|
977
1150
|
if (!cwd) {
|
|
978
1151
|
return branchPath;
|
|
@@ -2196,6 +2369,7 @@ async function startWorker(options) {
|
|
|
2196
2369
|
let reloadAfterClose = false;
|
|
2197
2370
|
const workspaceSyncInput = (trigger, overrides = {}) => {
|
|
2198
2371
|
if (!workspaceRemoteUrl) throw new Error("Worker workspace manifest has not been received");
|
|
2372
|
+
const projects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
2199
2373
|
return {
|
|
2200
2374
|
workerLabel: label,
|
|
2201
2375
|
remoteUrl: workspaceRemoteUrl,
|
|
@@ -2203,7 +2377,7 @@ async function startWorker(options) {
|
|
|
2203
2377
|
projectsRoot,
|
|
2204
2378
|
plansRoot: planRoot,
|
|
2205
2379
|
shadowRoot: workspaceShadowRoot,
|
|
2206
|
-
projects
|
|
2380
|
+
projects,
|
|
2207
2381
|
trigger,
|
|
2208
2382
|
...overrides
|
|
2209
2383
|
};
|
|
@@ -2215,7 +2389,7 @@ async function startWorker(options) {
|
|
|
2215
2389
|
const allManifestCheckouts = () => [...manifestByProjectId.values()].flatMap(
|
|
2216
2390
|
(manifest) => manifest.branches.map((branchName) => ({ projectId: manifest.projectId, branchName }))
|
|
2217
2391
|
);
|
|
2218
|
-
const ensureVisibleWorkspaceCheckouts = (targets) => {
|
|
2392
|
+
const ensureVisibleWorkspaceCheckouts = (targets, options2 = {}) => {
|
|
2219
2393
|
const created = [];
|
|
2220
2394
|
for (const target of targets) {
|
|
2221
2395
|
const manifest = manifestByProjectId.get(target.projectId);
|
|
@@ -2227,15 +2401,25 @@ async function startWorker(options) {
|
|
|
2227
2401
|
const projectRoot = projectRootFor(projectsRoot, manifest.projectId, manifestByProjectId);
|
|
2228
2402
|
const branchPath = path.join(projectRoot, target.branchName);
|
|
2229
2403
|
const existed = hasNormalVisibleGitDir(branchPath);
|
|
2230
|
-
const
|
|
2404
|
+
const remote = visibleCheckoutRemoteFor({
|
|
2405
|
+
baseUrl,
|
|
2406
|
+
token,
|
|
2407
|
+
projectId: manifest.projectId,
|
|
2408
|
+
branchName: target.branchName,
|
|
2409
|
+
manifest
|
|
2410
|
+
});
|
|
2231
2411
|
ensureVisibleGitCheckout({
|
|
2232
2412
|
projectRoot,
|
|
2233
2413
|
branchName: target.branchName,
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2414
|
+
remoteUrl: remote.remoteUrl,
|
|
2415
|
+
remoteAuth: gitAuthForRemote(remote.remoteUrl, remote.authHeader),
|
|
2416
|
+
persistentAuthHeader: remote.persistAuth ? remote.authHeader : null,
|
|
2237
2417
|
defaultBranch: manifest.defaultBranch || "main",
|
|
2238
|
-
reconcileExistingOrigin:
|
|
2418
|
+
reconcileExistingOrigin: remote.reconcileExistingOrigin,
|
|
2419
|
+
requireRemoteBranch: remote.requireRemoteBranch,
|
|
2420
|
+
requiredBaseCommit: remote.requiredBaseCommit,
|
|
2421
|
+
allowRequiredRemoteMigration: !(options2.strictCanonicalRevalidation && remote.requireRemoteBranch),
|
|
2422
|
+
clearPersistentAuth: !remote.persistAuth
|
|
2239
2423
|
});
|
|
2240
2424
|
if (!existed) created.push(target);
|
|
2241
2425
|
}
|
|
@@ -2247,15 +2431,30 @@ async function startWorker(options) {
|
|
|
2247
2431
|
let pendingTargets = [];
|
|
2248
2432
|
const result = await workspaceSyncSingleFlight.runPrepared(() => {
|
|
2249
2433
|
const shouldEnsureCheckouts = !overrides.skipVisibleMirror && !overrides.resetToCanonical;
|
|
2434
|
+
const syncProjects = workspaceProjectsForSync([...manifestByProjectId.values()], trigger);
|
|
2435
|
+
const allowedCheckoutKeys = new Set(
|
|
2436
|
+
syncProjects.flatMap((project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName)))
|
|
2437
|
+
);
|
|
2438
|
+
const scopedPendingTargets = [...pendingManifestCheckouts.values()].filter(
|
|
2439
|
+
(target) => allowedCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
|
|
2440
|
+
);
|
|
2250
2441
|
let checkoutTargets = [];
|
|
2251
2442
|
if (shouldEnsureCheckouts) {
|
|
2252
|
-
checkoutTargets =
|
|
2443
|
+
checkoutTargets = workspaceSyncCheckoutTargets({
|
|
2444
|
+
projects: syncProjects,
|
|
2445
|
+
pendingTargets: scopedPendingTargets,
|
|
2446
|
+
includeAllManifestCheckouts: trigger.type === "connect",
|
|
2447
|
+
...trigger.canonicalCheckoutOnly && trigger.projectId && trigger.branchName ? { canonicalCheckoutOnly: { projectId: trigger.projectId, branchName: trigger.branchName } } : {}
|
|
2448
|
+
});
|
|
2253
2449
|
}
|
|
2254
|
-
pendingTargets = shouldEnsureCheckouts ?
|
|
2255
|
-
const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets
|
|
2450
|
+
pendingTargets = shouldEnsureCheckouts ? scopedPendingTargets : [];
|
|
2451
|
+
const createdCheckouts = ensureVisibleWorkspaceCheckouts(checkoutTargets, {
|
|
2452
|
+
strictCanonicalRevalidation: true
|
|
2453
|
+
});
|
|
2256
2454
|
const newVisibleCheckoutByKey = /* @__PURE__ */ new Map();
|
|
2257
2455
|
for (const target of [...overrides.newVisibleCheckouts ?? [], ...createdCheckouts, ...pendingTargets]) {
|
|
2258
|
-
|
|
2456
|
+
const key = manifestCheckoutKey(target.projectId, target.branchName);
|
|
2457
|
+
if (allowedCheckoutKeys.has(key)) newVisibleCheckoutByKey.set(key, target);
|
|
2259
2458
|
}
|
|
2260
2459
|
const newVisibleCheckouts = [...newVisibleCheckoutByKey.values()];
|
|
2261
2460
|
return workspaceSyncInput(trigger, {
|
|
@@ -2331,7 +2530,9 @@ async function startWorker(options) {
|
|
|
2331
2530
|
version: getWorkerVersion(),
|
|
2332
2531
|
r5dctlVersion: readInstalledCliVersion("r5dctl"),
|
|
2333
2532
|
capabilities: {
|
|
2334
|
-
updateClis: true
|
|
2533
|
+
updateClis: true,
|
|
2534
|
+
canonicalResolverCheckout: true,
|
|
2535
|
+
browserPortForwarding: true
|
|
2335
2536
|
},
|
|
2336
2537
|
projectRoot: projectsRoot,
|
|
2337
2538
|
artifactRoot,
|
|
@@ -2391,7 +2592,15 @@ async function startWorker(options) {
|
|
|
2391
2592
|
if (!workspaceRemoteUrl || activeWorkspaceIncidentId || cliUpdateInProgress || periodicWorkspaceScanInFlight) return;
|
|
2392
2593
|
periodicWorkspaceScanInFlight = true;
|
|
2393
2594
|
void (async () => {
|
|
2394
|
-
|
|
2595
|
+
const genericCheckoutKeys = new Set(
|
|
2596
|
+
workspaceProjectsForSync([...manifestByProjectId.values()], { type: "periodic" }).flatMap(
|
|
2597
|
+
(project) => project.branches.map((branchName) => manifestCheckoutKey(project.projectId, branchName))
|
|
2598
|
+
)
|
|
2599
|
+
);
|
|
2600
|
+
const hasPendingGenericCheckout = [...pendingManifestCheckouts.values()].some(
|
|
2601
|
+
(target) => genericCheckoutKeys.has(manifestCheckoutKey(target.projectId, target.branchName))
|
|
2602
|
+
);
|
|
2603
|
+
if (hasPendingGenericCheckout) {
|
|
2395
2604
|
previousPeriodicFingerprint = null;
|
|
2396
2605
|
await runRequestedWorkspaceSync(crypto.randomUUID(), {
|
|
2397
2606
|
type: "periodic",
|
|
@@ -2476,6 +2685,17 @@ async function startWorker(options) {
|
|
|
2476
2685
|
pendingProcessTerminals.delete(message.runId);
|
|
2477
2686
|
return;
|
|
2478
2687
|
}
|
|
2688
|
+
if (message.type === "port_forward_connect") {
|
|
2689
|
+
openWorkerPortForwardRelay({
|
|
2690
|
+
baseUrl,
|
|
2691
|
+
token,
|
|
2692
|
+
label,
|
|
2693
|
+
forwardId: message.forwardId,
|
|
2694
|
+
relayConnectionId: message.relayConnectionId,
|
|
2695
|
+
workerPort: message.workerPort
|
|
2696
|
+
});
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2479
2699
|
if (message.type === "update_clis") {
|
|
2480
2700
|
if (cliUpdateInProgress) {
|
|
2481
2701
|
sendWorkerMessage(ws, {
|
|
@@ -2808,5 +3028,8 @@ export {
|
|
|
2808
3028
|
resolveHostShell,
|
|
2809
3029
|
resolveWorkerFilePath,
|
|
2810
3030
|
syncSessionArtifacts,
|
|
3031
|
+
visibleCheckoutGitTestHarness,
|
|
3032
|
+
visibleCheckoutRemoteFor,
|
|
3033
|
+
workspaceSyncCheckoutTargets,
|
|
2811
3034
|
writeWorkerTextFile
|
|
2812
3035
|
};
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import WebSocket, {} from "ws";
|
|
3
|
+
const RELAY_FRAME_BYTES = 64 * 1024;
|
|
4
|
+
const RELAY_PAUSE_BYTES = 1024 * 1024;
|
|
5
|
+
const RELAY_RESUME_BYTES = 256 * 1024;
|
|
6
|
+
function relayUrl(baseUrl, label, forwardId, relayConnectionId) {
|
|
7
|
+
const url = new URL("/worker/port-forward/ws", baseUrl);
|
|
8
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
9
|
+
url.searchParams.set("label", label);
|
|
10
|
+
url.searchParams.set("forward", forwardId);
|
|
11
|
+
url.searchParams.set("connection", relayConnectionId);
|
|
12
|
+
return url.toString();
|
|
13
|
+
}
|
|
14
|
+
function rawDataBuffer(data) {
|
|
15
|
+
if (Buffer.isBuffer(data)) return data;
|
|
16
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
17
|
+
return Buffer.from(data);
|
|
18
|
+
}
|
|
19
|
+
function openWorkerPortForwardRelay(options) {
|
|
20
|
+
const socket = net.createConnection({ host: "127.0.0.1", port: options.workerPort, allowHalfOpen: true });
|
|
21
|
+
socket.pause();
|
|
22
|
+
socket.setNoDelay(true);
|
|
23
|
+
const relay = new WebSocket(relayUrl(options.baseUrl, options.label, options.forwardId, options.relayConnectionId), {
|
|
24
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
25
|
+
perMessageDeflate: false
|
|
26
|
+
});
|
|
27
|
+
let relayReady = false;
|
|
28
|
+
let targetReady = false;
|
|
29
|
+
const pendingChunks = [];
|
|
30
|
+
let pendingBytes = 0;
|
|
31
|
+
let closing = false;
|
|
32
|
+
let resumeTimer;
|
|
33
|
+
const maybeResume = () => {
|
|
34
|
+
if (relayReady && targetReady && !resumeTimer) socket.resume();
|
|
35
|
+
};
|
|
36
|
+
const cleanup = () => {
|
|
37
|
+
if (closing) return;
|
|
38
|
+
closing = true;
|
|
39
|
+
if (resumeTimer) clearInterval(resumeTimer);
|
|
40
|
+
socket.destroy();
|
|
41
|
+
if (relay.readyState === WebSocket.OPEN || relay.readyState === WebSocket.CONNECTING) relay.close();
|
|
42
|
+
};
|
|
43
|
+
const sendError = (message) => {
|
|
44
|
+
if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "error", error: message }));
|
|
45
|
+
cleanup();
|
|
46
|
+
};
|
|
47
|
+
const waitForRelayDrain = () => {
|
|
48
|
+
if (resumeTimer || relay.bufferedAmount <= RELAY_PAUSE_BYTES) return;
|
|
49
|
+
socket.pause();
|
|
50
|
+
resumeTimer = setInterval(() => {
|
|
51
|
+
if (relay.readyState !== WebSocket.OPEN) return cleanup();
|
|
52
|
+
if (relay.bufferedAmount > RELAY_RESUME_BYTES) return;
|
|
53
|
+
clearInterval(resumeTimer);
|
|
54
|
+
resumeTimer = void 0;
|
|
55
|
+
maybeResume();
|
|
56
|
+
}, 10);
|
|
57
|
+
};
|
|
58
|
+
const sendChunk = (chunk) => {
|
|
59
|
+
for (let offset = 0; offset < chunk.byteLength; offset += RELAY_FRAME_BYTES) {
|
|
60
|
+
relay.send(chunk.subarray(offset, Math.min(chunk.byteLength, offset + RELAY_FRAME_BYTES)), { binary: true });
|
|
61
|
+
}
|
|
62
|
+
waitForRelayDrain();
|
|
63
|
+
};
|
|
64
|
+
socket.once("connect", () => {
|
|
65
|
+
targetReady = true;
|
|
66
|
+
maybeResume();
|
|
67
|
+
});
|
|
68
|
+
socket.on("data", (chunk) => {
|
|
69
|
+
if (!relayReady || relay.readyState !== WebSocket.OPEN) {
|
|
70
|
+
pendingBytes += chunk.byteLength;
|
|
71
|
+
if (pendingBytes > RELAY_PAUSE_BYTES) return cleanup();
|
|
72
|
+
pendingChunks.push(Buffer.from(chunk));
|
|
73
|
+
socket.pause();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
sendChunk(chunk);
|
|
77
|
+
});
|
|
78
|
+
socket.on("end", () => {
|
|
79
|
+
if (relay.readyState === WebSocket.OPEN) relay.send(JSON.stringify({ type: "end" }));
|
|
80
|
+
});
|
|
81
|
+
socket.on("error", (error) => sendError(`Could not connect to 127.0.0.1:${options.workerPort}: ${error.message}`));
|
|
82
|
+
socket.on("close", cleanup);
|
|
83
|
+
relay.on("message", (data, isBinary) => {
|
|
84
|
+
if (!isBinary) {
|
|
85
|
+
let control;
|
|
86
|
+
try {
|
|
87
|
+
control = JSON.parse(rawDataBuffer(data).toString("utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return cleanup();
|
|
90
|
+
}
|
|
91
|
+
if (control.type === "ready") {
|
|
92
|
+
relayReady = true;
|
|
93
|
+
for (const chunk of pendingChunks.splice(0)) sendChunk(chunk);
|
|
94
|
+
pendingBytes = 0;
|
|
95
|
+
maybeResume();
|
|
96
|
+
} else if (control.type === "end") {
|
|
97
|
+
socket.end();
|
|
98
|
+
} else if (control.type === "error") {
|
|
99
|
+
socket.destroy(new Error(control.error || "Port-forward relay failed."));
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!socket.write(rawDataBuffer(data))) {
|
|
104
|
+
relay.pause();
|
|
105
|
+
socket.once("drain", () => relay.resume());
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
relay.once("error", cleanup);
|
|
109
|
+
relay.once("close", cleanup);
|
|
110
|
+
socket.pause();
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
openWorkerPortForwardRelay
|
|
114
|
+
};
|