replicas-engine 0.1.477 → 0.1.479
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/src/index.js +849 -833
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { Hono as Hono2 } from "hono";
|
|
6
|
-
import { existsSync as
|
|
6
|
+
import { existsSync as existsSync10 } from "fs";
|
|
7
7
|
|
|
8
8
|
// ../shared/src/type-guards.ts
|
|
9
9
|
function isRecord(value) {
|
|
@@ -582,7 +582,7 @@ var WORKSPACE_SIZES = ["small", "large"];
|
|
|
582
582
|
var INVALID_WORKSPACE_SIZE_ERROR = `Invalid size: must be one of ${WORKSPACE_SIZES.join(", ")}`;
|
|
583
583
|
|
|
584
584
|
// ../shared/src/e2b.ts
|
|
585
|
-
var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-22-
|
|
585
|
+
var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-22-v4";
|
|
586
586
|
|
|
587
587
|
// ../shared/src/runtime-env.ts
|
|
588
588
|
function shellQuotePosix(value) {
|
|
@@ -644,10 +644,30 @@ function parsePosixEnvFile(content) {
|
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
// ../shared/src/git.ts
|
|
647
|
-
function
|
|
647
|
+
function parseGitRemote(remoteUrl) {
|
|
648
|
+
const normalized = remoteUrl.trim().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
649
|
+
try {
|
|
650
|
+
const parsed = new URL(normalized);
|
|
651
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "ssh:") return null;
|
|
652
|
+
const path7 = parsed.pathname.replace(/^\/+/, "");
|
|
653
|
+
const host2 = parsed.protocol === "ssh:" ? parsed.hostname : parsed.host;
|
|
654
|
+
if (!host2 || !path7) return null;
|
|
655
|
+
return {
|
|
656
|
+
host: host2.toLowerCase(),
|
|
657
|
+
path: path7,
|
|
658
|
+
protocol: parsed.protocol === "http:" ? "http" : parsed.protocol === "https:" ? "https" : "ssh"
|
|
659
|
+
};
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
const match = normalized.match(/^(?:[^@/\s]+@)?([^:/\s]+):(?:(?:\d+)\/)?(.+)$/);
|
|
663
|
+
if (!match) return null;
|
|
664
|
+
const [, host, path6] = match;
|
|
665
|
+
return host && path6 ? { host: host.toLowerCase(), path: path6, protocol: "ssh" } : null;
|
|
666
|
+
}
|
|
667
|
+
function gitIdentityConfigCommands(identity, scope = "global") {
|
|
648
668
|
return [
|
|
649
|
-
["config",
|
|
650
|
-
["config",
|
|
669
|
+
["config", `--${scope}`, "user.name", identity.name],
|
|
670
|
+
["config", `--${scope}`, "user.email", identity.email]
|
|
651
671
|
];
|
|
652
672
|
}
|
|
653
673
|
|
|
@@ -676,18 +696,6 @@ function isGitHubUrl(url) {
|
|
|
676
696
|
if (!url) return false;
|
|
677
697
|
return GITHUB_HOST_RE.test(url);
|
|
678
698
|
}
|
|
679
|
-
function stripCredentialsFromUrl(url) {
|
|
680
|
-
try {
|
|
681
|
-
const parsed = new URL(url);
|
|
682
|
-
if (parsed.username || parsed.password) {
|
|
683
|
-
parsed.username = "";
|
|
684
|
-
parsed.password = "";
|
|
685
|
-
return parsed.toString();
|
|
686
|
-
}
|
|
687
|
-
} catch {
|
|
688
|
-
}
|
|
689
|
-
return url;
|
|
690
|
-
}
|
|
691
699
|
|
|
692
700
|
// ../shared/src/urls.ts
|
|
693
701
|
function decodePathSegments(segments) {
|
|
@@ -699,21 +707,6 @@ function decodePathSegments(segments) {
|
|
|
699
707
|
}
|
|
700
708
|
});
|
|
701
709
|
}
|
|
702
|
-
function normalizeRepositoryUrl(url, options = {}) {
|
|
703
|
-
if (!url) return null;
|
|
704
|
-
const stripped = stripCredentialsFromUrl(url).trim().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
705
|
-
if (!stripped) return null;
|
|
706
|
-
try {
|
|
707
|
-
const parsed = new URL(stripped);
|
|
708
|
-
parsed.hash = "";
|
|
709
|
-
parsed.search = "";
|
|
710
|
-
parsed.pathname = parsed.pathname.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
711
|
-
const normalized = `${parsed.origin}${parsed.pathname}`;
|
|
712
|
-
return options.lowercase ? normalized.toLowerCase() : normalized;
|
|
713
|
-
} catch {
|
|
714
|
-
return options.lowercase ? stripped.toLowerCase() : stripped;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
710
|
|
|
718
711
|
// ../shared/src/slash-commands.ts
|
|
719
712
|
function normalizeSlashCommandName(name) {
|
|
@@ -4992,502 +4985,98 @@ function removeCredentialFileLines(filePath, shouldRemove) {
|
|
|
4992
4985
|
});
|
|
4993
4986
|
}
|
|
4994
4987
|
|
|
4995
|
-
// src/
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
4988
|
+
// src/git/service.ts
|
|
4989
|
+
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
4990
|
+
import { spawn } from "child_process";
|
|
4991
|
+
import { join as join5 } from "path";
|
|
4992
|
+
|
|
4993
|
+
// src/utils/state.ts
|
|
4994
|
+
import { readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
|
|
4995
|
+
import { existsSync } from "fs";
|
|
4996
|
+
import { join as join3 } from "path";
|
|
4997
|
+
import { homedir as homedir3 } from "os";
|
|
4998
|
+
|
|
4999
|
+
// src/utils/type-guards.ts
|
|
5000
|
+
function isRecord4(value) {
|
|
5001
|
+
return typeof value === "object" && value !== null;
|
|
5005
5002
|
}
|
|
5006
5003
|
|
|
5007
|
-
// src/
|
|
5008
|
-
var
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
}
|
|
5012
|
-
async doRefresh(_config) {
|
|
5013
|
-
console.log("[GitHubTokenManager] Refreshing GitHub token...");
|
|
5014
|
-
const response = await monolithRequest("/v1/engine/github/refresh-token");
|
|
5015
|
-
if (!response.ok) {
|
|
5016
|
-
if (response.status === 403) {
|
|
5017
|
-
await this.clearGitHubCredentials();
|
|
5018
|
-
}
|
|
5019
|
-
const errorText = await response.text();
|
|
5020
|
-
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
5021
|
-
}
|
|
5022
|
-
const data = await response.json();
|
|
5023
|
-
const ghToken = data.userToken?.token ?? data.token;
|
|
5024
|
-
const ghUsername = data.userToken?.username ?? "x-access-token";
|
|
5025
|
-
await this.updateGitCredentials(ghToken);
|
|
5026
|
-
await this.updateGhHostsFile(ghToken, ghUsername);
|
|
5027
|
-
if (data.gitIdentity) {
|
|
5028
|
-
await updateGitIdentity(data.gitIdentity, "GitHubTokenManager");
|
|
5029
|
-
}
|
|
5030
|
-
if (data.userToken) {
|
|
5031
|
-
console.log(`[GitHubTokenManager] Token refreshed with user token for PR attribution, user token expires at ${data.userToken.expiresAt}`);
|
|
5032
|
-
} else {
|
|
5033
|
-
console.log(`[GitHubTokenManager] Token refreshed successfully, expires at ${data.expiresAt}`);
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
async updateGitCredentials(token) {
|
|
5037
|
-
const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5038
|
-
try {
|
|
5039
|
-
await upsertCredentialFileLines(credentialsPath, ["github.com"], [
|
|
5040
|
-
`https://x-access-token:${token}@github.com`
|
|
5041
|
-
]);
|
|
5042
|
-
console.log(`[GitHubTokenManager] Updated ${credentialsPath}`);
|
|
5043
|
-
} catch (error) {
|
|
5044
|
-
console.error("[GitHubTokenManager] Failed to update git credentials:", error);
|
|
5045
|
-
}
|
|
5046
|
-
}
|
|
5047
|
-
async updateGhHostsFile(token, username) {
|
|
5048
|
-
const hostsPath = path.join(ENGINE_ENV.HOME_DIR, ".config", "gh", "hosts.yml");
|
|
5049
|
-
const content = `github.com:
|
|
5050
|
-
oauth_token: ${JSON.stringify(token)}
|
|
5051
|
-
user: ${JSON.stringify(username)}
|
|
5052
|
-
git_protocol: https
|
|
5053
|
-
`;
|
|
5054
|
-
try {
|
|
5055
|
-
await writeSecureCredentialFile(hostsPath, content, { ensureParentDir: true });
|
|
5056
|
-
console.log(`[GitHubTokenManager] Updated ${hostsPath}`);
|
|
5057
|
-
} catch (error) {
|
|
5058
|
-
console.error("[GitHubTokenManager] Failed to update gh hosts file:", error);
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
async clearGitHubCredentials() {
|
|
5062
|
-
const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5063
|
-
await removeCredentialFileLines(
|
|
5064
|
-
credentialsPath,
|
|
5065
|
-
(line) => line.endsWith("@github.com")
|
|
5066
|
-
);
|
|
5067
|
-
}
|
|
5004
|
+
// src/utils/state.ts
|
|
5005
|
+
var STATE_DIR = join3(homedir3(), ".replicas");
|
|
5006
|
+
var STATE_FILE = join3(STATE_DIR, "engine-state.json");
|
|
5007
|
+
var DEFAULT_STATE = {
|
|
5008
|
+
repos: {}
|
|
5068
5009
|
};
|
|
5069
|
-
var
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5010
|
+
var stateWriteLock = new AsyncLock();
|
|
5011
|
+
async function updateEngineState(updater) {
|
|
5012
|
+
await stateWriteLock.run(async () => {
|
|
5013
|
+
await mkdir2(STATE_DIR, { recursive: true });
|
|
5014
|
+
const currentState = await loadEngineState();
|
|
5015
|
+
const nextState = updater(currentState);
|
|
5016
|
+
await atomicWriteFile(STATE_FILE, JSON.stringify(nextState, null, 2));
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
function isEngineRepoDiff(value) {
|
|
5020
|
+
return isRecord4(value) && typeof value.added === "number" && typeof value.removed === "number";
|
|
5021
|
+
}
|
|
5022
|
+
function coerceRepoState(value) {
|
|
5023
|
+
if (!isRecord4(value)) {
|
|
5024
|
+
return null;
|
|
5076
5025
|
}
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
if (!data.token) {
|
|
5087
|
-
await this.clearGitLabCredentials(data.hosts);
|
|
5088
|
-
console.log(`[GitLabTokenManager] No GitLab token to install: ${data.reason}`);
|
|
5089
|
-
return;
|
|
5090
|
-
}
|
|
5091
|
-
const hosts = data.hosts.length > 0 ? data.hosts : ["gitlab.com"];
|
|
5092
|
-
const credentialsPath = path2.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5093
|
-
await upsertCredentialFileLines(
|
|
5094
|
-
credentialsPath,
|
|
5095
|
-
hosts,
|
|
5096
|
-
hosts.map((host) => `https://oauth2:${data.token}@${host}`)
|
|
5097
|
-
);
|
|
5098
|
-
console.log(`[GitLabTokenManager] Updated ${credentialsPath} for ${hosts.join(", ")}`);
|
|
5099
|
-
if (data.gitIdentity) {
|
|
5100
|
-
await updateGitIdentity(data.gitIdentity, "GitLabTokenManager");
|
|
5101
|
-
}
|
|
5026
|
+
if (typeof value.name !== "string") return null;
|
|
5027
|
+
if (typeof value.path !== "string") return null;
|
|
5028
|
+
if (typeof value.defaultBranch !== "string") return null;
|
|
5029
|
+
if (typeof value.currentBranch !== "string") return null;
|
|
5030
|
+
if (!Array.isArray(value.prUrls)) return null;
|
|
5031
|
+
const prUrls = [];
|
|
5032
|
+
for (const entry of value.prUrls) {
|
|
5033
|
+
if (typeof entry !== "string") return null;
|
|
5034
|
+
if (!prUrls.includes(entry)) prUrls.push(entry);
|
|
5102
5035
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5036
|
+
if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
|
|
5037
|
+
if (typeof value.startHooksCompleted !== "boolean") return null;
|
|
5038
|
+
return {
|
|
5039
|
+
name: value.name,
|
|
5040
|
+
path: value.path,
|
|
5041
|
+
defaultBranch: value.defaultBranch,
|
|
5042
|
+
currentBranch: value.currentBranch,
|
|
5043
|
+
prUrls,
|
|
5044
|
+
gitDiff: value.gitDiff,
|
|
5045
|
+
startHooksCompleted: value.startHooksCompleted
|
|
5046
|
+
};
|
|
5047
|
+
}
|
|
5048
|
+
function coerceEngineState(value) {
|
|
5049
|
+
if (!isRecord4(value)) {
|
|
5050
|
+
return {};
|
|
5109
5051
|
}
|
|
5110
|
-
};
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
// src/managers/auth-env-transition.ts
|
|
5118
|
-
function applyAuthEnvTransition(params) {
|
|
5119
|
-
const newOwned = new Set(params.authKeysByMethod[params.newMethod]);
|
|
5120
|
-
const prevOwned = new Set(params.authKeysByMethod[params.prevMethod]);
|
|
5121
|
-
for (const key of params.authKeys) {
|
|
5122
|
-
const value = params.newEnvVars[key];
|
|
5123
|
-
if (value !== void 0) {
|
|
5124
|
-
for (const env of params.envs) {
|
|
5125
|
-
env[key] = value;
|
|
5126
|
-
}
|
|
5127
|
-
} else if (prevOwned.has(key) && !newOwned.has(key)) {
|
|
5128
|
-
for (const env of params.envs) {
|
|
5129
|
-
delete env[key];
|
|
5052
|
+
const partial = {};
|
|
5053
|
+
if (isRecord4(value.repos)) {
|
|
5054
|
+
const repos = {};
|
|
5055
|
+
for (const [repoName, repoState] of Object.entries(value.repos)) {
|
|
5056
|
+
const coerced = coerceRepoState(repoState);
|
|
5057
|
+
if (coerced) {
|
|
5058
|
+
repos[repoName] = coerced;
|
|
5130
5059
|
}
|
|
5131
5060
|
}
|
|
5061
|
+
partial.repos = repos;
|
|
5132
5062
|
}
|
|
5063
|
+
return partial;
|
|
5133
5064
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
super("ClaudeTokenManager");
|
|
5139
|
-
}
|
|
5140
|
-
getSkipReason() {
|
|
5141
|
-
const method = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
|
|
5142
|
-
if (method === "api_key" || method === "bedrock" || method === "foundry") {
|
|
5143
|
-
return `auth method is ${method}`;
|
|
5144
|
-
}
|
|
5145
|
-
return null;
|
|
5146
|
-
}
|
|
5147
|
-
async doRefresh(_config) {
|
|
5148
|
-
await this.refreshWithRequest();
|
|
5149
|
-
}
|
|
5150
|
-
async refreshWithRequest(request) {
|
|
5151
|
-
console.log("[ClaudeTokenManager] Refreshing Claude credentials...");
|
|
5152
|
-
const response = await monolithRequest("/v1/engine/claude/refresh-credentials", {
|
|
5153
|
-
body: request
|
|
5154
|
-
});
|
|
5155
|
-
if (!response.ok) {
|
|
5156
|
-
const errorText = await response.text();
|
|
5157
|
-
throw new Error(`Credentials refresh failed: ${response.status} ${errorText}`);
|
|
5065
|
+
async function loadEngineState() {
|
|
5066
|
+
try {
|
|
5067
|
+
if (!existsSync(STATE_FILE)) {
|
|
5068
|
+
return { ...DEFAULT_STATE };
|
|
5158
5069
|
}
|
|
5159
|
-
const
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
await this.refreshWithRequest(failedMethod && failedMethod !== "none" ? {
|
|
5170
|
-
failedMethod,
|
|
5171
|
-
failureReason
|
|
5172
|
-
} : void 0);
|
|
5173
|
-
if (ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD === "oauth") {
|
|
5174
|
-
this.start().catch((error) => {
|
|
5175
|
-
console.error("[ClaudeTokenManager] Failed to restart OAuth refresh service after fallback:", error);
|
|
5176
|
-
});
|
|
5177
|
-
}
|
|
5178
|
-
return true;
|
|
5179
|
-
} catch (error) {
|
|
5180
|
-
console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
|
|
5181
|
-
return false;
|
|
5182
|
-
}
|
|
5183
|
-
}
|
|
5184
|
-
async applyCredentialsResponse(response) {
|
|
5185
|
-
if (response.type === "oauth") {
|
|
5186
|
-
await this.writeOauthCredentialsFile({
|
|
5187
|
-
accessToken: response.accessToken,
|
|
5188
|
-
refreshToken: response.refreshToken,
|
|
5189
|
-
expiresAt: response.expiresAt,
|
|
5190
|
-
scopes: response.scopes,
|
|
5191
|
-
subscriptionType: response.subscriptionType
|
|
5192
|
-
});
|
|
5193
|
-
} else {
|
|
5194
|
-
await this.removeOauthCredentialsFile();
|
|
5195
|
-
}
|
|
5196
|
-
const envVars = claudeAuthEnvFromResponse(response);
|
|
5197
|
-
applyAuthEnvTransition({
|
|
5198
|
-
prevMethod: ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD ?? "none",
|
|
5199
|
-
newMethod: envVars.REPLICAS_CLAUDE_AUTH_METHOD ?? "none",
|
|
5200
|
-
authKeys: CLAUDE_AUTH_ENV_KEYS,
|
|
5201
|
-
authKeysByMethod: CLAUDE_AUTH_ENV_KEYS_BY_METHOD,
|
|
5202
|
-
newEnvVars: envVars,
|
|
5203
|
-
envs: [ENGINE_ENV, process.env]
|
|
5204
|
-
});
|
|
5205
|
-
}
|
|
5206
|
-
async writeOauthCredentialsFile(credentials) {
|
|
5207
|
-
const credentialsPath = path3.join(ENGINE_ENV.HOME_DIR, ".claude", ".credentials.json");
|
|
5208
|
-
const claudeCliConfig = {
|
|
5209
|
-
claudeAiOauth: {
|
|
5210
|
-
accessToken: credentials.accessToken,
|
|
5211
|
-
refreshToken: credentials.refreshToken,
|
|
5212
|
-
expiresAt: new Date(credentials.expiresAt).getTime(),
|
|
5213
|
-
scopes: credentials.scopes,
|
|
5214
|
-
subscriptionType: credentials.subscriptionType
|
|
5215
|
-
}
|
|
5216
|
-
};
|
|
5217
|
-
try {
|
|
5218
|
-
await writeSecureCredentialFile(
|
|
5219
|
-
credentialsPath,
|
|
5220
|
-
JSON.stringify(claudeCliConfig, null, 2),
|
|
5221
|
-
{ ensureParentDir: true }
|
|
5222
|
-
);
|
|
5223
|
-
console.log(`[ClaudeTokenManager] Updated ${credentialsPath}`);
|
|
5224
|
-
} catch (error) {
|
|
5225
|
-
console.error("[ClaudeTokenManager] Failed to update credentials file:", error);
|
|
5226
|
-
}
|
|
5227
|
-
}
|
|
5228
|
-
async removeOauthCredentialsFile() {
|
|
5229
|
-
const credentialsPath = path3.join(ENGINE_ENV.HOME_DIR, ".claude", ".credentials.json");
|
|
5230
|
-
try {
|
|
5231
|
-
await fs.unlink(credentialsPath);
|
|
5232
|
-
} catch {
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
};
|
|
5236
|
-
var claudeTokenManager = new ClaudeTokenManager();
|
|
5237
|
-
|
|
5238
|
-
// src/managers/codex-token-manager.ts
|
|
5239
|
-
import { promises as fs2 } from "fs";
|
|
5240
|
-
import path4 from "path";
|
|
5241
|
-
var CodexTokenManager = class extends BaseRefreshManager {
|
|
5242
|
-
constructor() {
|
|
5243
|
-
super("CodexTokenManager");
|
|
5244
|
-
}
|
|
5245
|
-
getSkipReason() {
|
|
5246
|
-
if (ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "api_key" || ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "foundry") {
|
|
5247
|
-
return `auth method is ${ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD}`;
|
|
5248
|
-
}
|
|
5249
|
-
if (!ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD && ENGINE_ENV.OPENAI_API_KEY) {
|
|
5250
|
-
return "OPENAI_API_KEY is set";
|
|
5251
|
-
}
|
|
5252
|
-
return null;
|
|
5253
|
-
}
|
|
5254
|
-
async doRefresh(_config) {
|
|
5255
|
-
await this.refreshWithRequest();
|
|
5256
|
-
}
|
|
5257
|
-
async refreshWithRequest(request) {
|
|
5258
|
-
console.log("[CodexTokenManager] Refreshing Codex credentials...");
|
|
5259
|
-
const response = await monolithRequest("/v1/engine/codex/refresh-credentials", {
|
|
5260
|
-
body: request
|
|
5261
|
-
});
|
|
5262
|
-
if (!response.ok) {
|
|
5263
|
-
const errorText = await response.text();
|
|
5264
|
-
throw new Error(`Credentials refresh failed: ${response.status} ${errorText}`);
|
|
5265
|
-
}
|
|
5266
|
-
const data = await response.json();
|
|
5267
|
-
await this.applyCredentialsResponse(data);
|
|
5268
|
-
console.log(`[CodexTokenManager] Credentials refreshed (method=${data.type})`);
|
|
5269
|
-
}
|
|
5270
|
-
async fetchFreshCredentials(failureReason) {
|
|
5271
|
-
const config = this.getRuntimeConfig();
|
|
5272
|
-
if (!config) return false;
|
|
5273
|
-
try {
|
|
5274
|
-
console.log("[CodexTokenManager] Fetching fresh credentials from monolith after auth failure...");
|
|
5275
|
-
const failedMethod = ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD;
|
|
5276
|
-
await this.refreshWithRequest(failedMethod === "oauth" || failedMethod === "api_key" || failedMethod === "foundry" ? {
|
|
5277
|
-
failedMethod,
|
|
5278
|
-
failureReason
|
|
5279
|
-
} : void 0);
|
|
5280
|
-
if (ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "oauth") {
|
|
5281
|
-
this.start().catch((error) => {
|
|
5282
|
-
console.error("[CodexTokenManager] Failed to restart OAuth refresh service after fallback:", error);
|
|
5283
|
-
});
|
|
5284
|
-
}
|
|
5285
|
-
return true;
|
|
5286
|
-
} catch (error) {
|
|
5287
|
-
console.error("[CodexTokenManager] Failed to fetch fresh credentials:", error);
|
|
5288
|
-
return false;
|
|
5289
|
-
}
|
|
5290
|
-
}
|
|
5291
|
-
async applyCredentialsResponse(response) {
|
|
5292
|
-
if (response.type === "oauth") {
|
|
5293
|
-
await this.writeOauthCredentialsFile(response);
|
|
5294
|
-
} else {
|
|
5295
|
-
await this.removeOauthCredentialsFile();
|
|
5296
|
-
}
|
|
5297
|
-
const envVars = codexAuthEnvFromResponse(response);
|
|
5298
|
-
applyAuthEnvTransition({
|
|
5299
|
-
prevMethod: ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD ?? "none",
|
|
5300
|
-
newMethod: envVars.REPLICAS_CODEX_AUTH_METHOD ?? "none",
|
|
5301
|
-
authKeys: CODEX_AUTH_ENV_KEYS,
|
|
5302
|
-
authKeysByMethod: CODEX_AUTH_ENV_KEYS_BY_METHOD,
|
|
5303
|
-
newEnvVars: envVars,
|
|
5304
|
-
envs: [ENGINE_ENV, process.env]
|
|
5305
|
-
});
|
|
5306
|
-
}
|
|
5307
|
-
async writeOauthCredentialsFile(credentials) {
|
|
5308
|
-
const authPath = path4.join(ENGINE_ENV.HOME_DIR, ".codex", "auth.json");
|
|
5309
|
-
const codexAuthConfig = {
|
|
5310
|
-
OPENAI_API_KEY: null,
|
|
5311
|
-
tokens: {
|
|
5312
|
-
id_token: credentials.idToken,
|
|
5313
|
-
access_token: credentials.accessToken,
|
|
5314
|
-
refresh_token: credentials.refreshToken,
|
|
5315
|
-
account_id: credentials.accountId
|
|
5316
|
-
},
|
|
5317
|
-
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
5318
|
-
};
|
|
5319
|
-
try {
|
|
5320
|
-
await writeSecureCredentialFile(
|
|
5321
|
-
authPath,
|
|
5322
|
-
JSON.stringify(codexAuthConfig, null, 2),
|
|
5323
|
-
{ ensureParentDir: true }
|
|
5324
|
-
);
|
|
5325
|
-
console.log(`[CodexTokenManager] Updated ${authPath}`);
|
|
5326
|
-
} catch (error) {
|
|
5327
|
-
console.error("[CodexTokenManager] Failed to update credentials file:", error);
|
|
5328
|
-
}
|
|
5329
|
-
}
|
|
5330
|
-
async removeOauthCredentialsFile() {
|
|
5331
|
-
const authPath = path4.join(ENGINE_ENV.HOME_DIR, ".codex", "auth.json");
|
|
5332
|
-
try {
|
|
5333
|
-
await fs2.unlink(authPath);
|
|
5334
|
-
} catch {
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
};
|
|
5338
|
-
var codexTokenManager = new CodexTokenManager();
|
|
5339
|
-
|
|
5340
|
-
// src/managers/infisical-token-manager.ts
|
|
5341
|
-
import { rm } from "fs/promises";
|
|
5342
|
-
import path5 from "path";
|
|
5343
|
-
var InfisicalTokenManager = class extends BaseRefreshManager {
|
|
5344
|
-
constructor(request = monolithRequest, paths = {
|
|
5345
|
-
homeDir: ENGINE_ENV.HOME_DIR,
|
|
5346
|
-
workspaceRoot: ENGINE_ENV.WORKSPACE_ROOT
|
|
5347
|
-
}, applyCredentials = applyInfisicalCredentials) {
|
|
5348
|
-
super("InfisicalTokenManager", 5 * 60 * 1e3);
|
|
5349
|
-
this.request = request;
|
|
5350
|
-
this.paths = paths;
|
|
5351
|
-
this.applyCredentials = applyCredentials;
|
|
5352
|
-
}
|
|
5353
|
-
request;
|
|
5354
|
-
paths;
|
|
5355
|
-
applyCredentials;
|
|
5356
|
-
nextRefreshDelayMs = 5 * 60 * 1e3;
|
|
5357
|
-
async doRefresh(_config) {
|
|
5358
|
-
const response = await this.request("/v1/engine/infisical/refresh-token");
|
|
5359
|
-
if (!response.ok) throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
|
|
5360
|
-
const data = await response.json();
|
|
5361
|
-
await this.applyCredentials(data, this.paths);
|
|
5362
|
-
const ttlMs = data.configured ? Date.parse(data.expiresAt) - Date.now() : Number.NaN;
|
|
5363
|
-
this.nextRefreshDelayMs = Number.isFinite(ttlMs) ? Math.max(1e3, Math.min(5 * 60 * 1e3, Math.floor(ttlMs * 0.8))) : 5 * 60 * 1e3;
|
|
5364
|
-
}
|
|
5365
|
-
getNextRefreshDelayMs() {
|
|
5366
|
-
return this.nextRefreshDelayMs;
|
|
5367
|
-
}
|
|
5368
|
-
};
|
|
5369
|
-
async function applyInfisicalCredentials(data, paths) {
|
|
5370
|
-
const credentialPath = path5.join(paths.homeDir, ".replicas", "infisical-env.sh");
|
|
5371
|
-
const configPath = path5.join(paths.workspaceRoot, ".infisical.json");
|
|
5372
|
-
if (!data.configured) {
|
|
5373
|
-
delete process.env.INFISICAL_TOKEN;
|
|
5374
|
-
delete process.env.INFISICAL_DOMAIN;
|
|
5375
|
-
await Promise.all([rm(credentialPath, { force: true }), rm(configPath, { force: true })]);
|
|
5376
|
-
return;
|
|
5377
|
-
}
|
|
5378
|
-
process.env.INFISICAL_TOKEN = data.token;
|
|
5379
|
-
process.env.INFISICAL_DOMAIN = data.siteUrl;
|
|
5380
|
-
await Promise.all([
|
|
5381
|
-
writeSecureCredentialFile(credentialPath, [
|
|
5382
|
-
`export INFISICAL_TOKEN=${shellQuotePosix(data.token)}`,
|
|
5383
|
-
`export INFISICAL_DOMAIN=${shellQuotePosix(data.siteUrl)}`,
|
|
5384
|
-
"export INFISICAL_DISABLE_UPDATE_CHECK=true",
|
|
5385
|
-
""
|
|
5386
|
-
].join("\n"), { ensureParentDir: true }),
|
|
5387
|
-
writeSecureCredentialFile(configPath, `${JSON.stringify({
|
|
5388
|
-
workspaceId: data.projectId,
|
|
5389
|
-
defaultEnvironment: data.environment,
|
|
5390
|
-
gitBranchToEnvironmentMapping: null,
|
|
5391
|
-
domain: data.siteUrl
|
|
5392
|
-
}, null, 2)}
|
|
5393
|
-
`, { ensureParentDir: true })
|
|
5394
|
-
]);
|
|
5395
|
-
}
|
|
5396
|
-
var infisicalTokenManager = new InfisicalTokenManager();
|
|
5397
|
-
|
|
5398
|
-
// src/git/service.ts
|
|
5399
|
-
import { readdir, readFile as readFile3, stat } from "fs/promises";
|
|
5400
|
-
import { existsSync as existsSync2 } from "fs";
|
|
5401
|
-
import { spawn } from "child_process";
|
|
5402
|
-
import { join as join5 } from "path";
|
|
5403
|
-
|
|
5404
|
-
// src/utils/state.ts
|
|
5405
|
-
import { readFile as readFile2, mkdir as mkdir2 } from "fs/promises";
|
|
5406
|
-
import { existsSync } from "fs";
|
|
5407
|
-
import { join as join3 } from "path";
|
|
5408
|
-
import { homedir as homedir3 } from "os";
|
|
5409
|
-
|
|
5410
|
-
// src/utils/type-guards.ts
|
|
5411
|
-
function isRecord4(value) {
|
|
5412
|
-
return typeof value === "object" && value !== null;
|
|
5413
|
-
}
|
|
5414
|
-
|
|
5415
|
-
// src/utils/state.ts
|
|
5416
|
-
var STATE_DIR = join3(homedir3(), ".replicas");
|
|
5417
|
-
var STATE_FILE = join3(STATE_DIR, "engine-state.json");
|
|
5418
|
-
var DEFAULT_STATE = {
|
|
5419
|
-
repos: {}
|
|
5420
|
-
};
|
|
5421
|
-
var stateWriteLock = new AsyncLock();
|
|
5422
|
-
async function updateEngineState(updater) {
|
|
5423
|
-
await stateWriteLock.run(async () => {
|
|
5424
|
-
await mkdir2(STATE_DIR, { recursive: true });
|
|
5425
|
-
const currentState = await loadEngineState();
|
|
5426
|
-
const nextState = updater(currentState);
|
|
5427
|
-
await atomicWriteFile(STATE_FILE, JSON.stringify(nextState, null, 2));
|
|
5428
|
-
});
|
|
5429
|
-
}
|
|
5430
|
-
function isEngineRepoDiff(value) {
|
|
5431
|
-
return isRecord4(value) && typeof value.added === "number" && typeof value.removed === "number";
|
|
5432
|
-
}
|
|
5433
|
-
function coerceRepoState(value) {
|
|
5434
|
-
if (!isRecord4(value)) {
|
|
5435
|
-
return null;
|
|
5436
|
-
}
|
|
5437
|
-
if (typeof value.name !== "string") return null;
|
|
5438
|
-
if (typeof value.path !== "string") return null;
|
|
5439
|
-
if (typeof value.defaultBranch !== "string") return null;
|
|
5440
|
-
if (typeof value.currentBranch !== "string") return null;
|
|
5441
|
-
if (!Array.isArray(value.prUrls)) return null;
|
|
5442
|
-
const prUrls = [];
|
|
5443
|
-
for (const entry of value.prUrls) {
|
|
5444
|
-
if (typeof entry !== "string") return null;
|
|
5445
|
-
if (!prUrls.includes(entry)) prUrls.push(entry);
|
|
5446
|
-
}
|
|
5447
|
-
if (!(value.gitDiff === null || isEngineRepoDiff(value.gitDiff))) return null;
|
|
5448
|
-
if (typeof value.startHooksCompleted !== "boolean") return null;
|
|
5449
|
-
return {
|
|
5450
|
-
name: value.name,
|
|
5451
|
-
path: value.path,
|
|
5452
|
-
defaultBranch: value.defaultBranch,
|
|
5453
|
-
currentBranch: value.currentBranch,
|
|
5454
|
-
prUrls,
|
|
5455
|
-
gitDiff: value.gitDiff,
|
|
5456
|
-
startHooksCompleted: value.startHooksCompleted
|
|
5457
|
-
};
|
|
5458
|
-
}
|
|
5459
|
-
function coerceEngineState(value) {
|
|
5460
|
-
if (!isRecord4(value)) {
|
|
5461
|
-
return {};
|
|
5462
|
-
}
|
|
5463
|
-
const partial = {};
|
|
5464
|
-
if (isRecord4(value.repos)) {
|
|
5465
|
-
const repos = {};
|
|
5466
|
-
for (const [repoName, repoState] of Object.entries(value.repos)) {
|
|
5467
|
-
const coerced = coerceRepoState(repoState);
|
|
5468
|
-
if (coerced) {
|
|
5469
|
-
repos[repoName] = coerced;
|
|
5470
|
-
}
|
|
5471
|
-
}
|
|
5472
|
-
partial.repos = repos;
|
|
5473
|
-
}
|
|
5474
|
-
return partial;
|
|
5475
|
-
}
|
|
5476
|
-
async function loadEngineState() {
|
|
5477
|
-
try {
|
|
5478
|
-
if (!existsSync(STATE_FILE)) {
|
|
5479
|
-
return { ...DEFAULT_STATE };
|
|
5480
|
-
}
|
|
5481
|
-
const content = await readFile2(STATE_FILE, "utf-8");
|
|
5482
|
-
const state = coerceEngineState(JSON.parse(content));
|
|
5483
|
-
return {
|
|
5484
|
-
...DEFAULT_STATE,
|
|
5485
|
-
...state,
|
|
5486
|
-
repos: state.repos ?? {}
|
|
5487
|
-
};
|
|
5488
|
-
} catch (error) {
|
|
5489
|
-
console.error("[EngineState] Failed to load state, using defaults:", error);
|
|
5490
|
-
return { ...DEFAULT_STATE };
|
|
5070
|
+
const content = await readFile2(STATE_FILE, "utf-8");
|
|
5071
|
+
const state = coerceEngineState(JSON.parse(content));
|
|
5072
|
+
return {
|
|
5073
|
+
...DEFAULT_STATE,
|
|
5074
|
+
...state,
|
|
5075
|
+
repos: state.repos ?? {}
|
|
5076
|
+
};
|
|
5077
|
+
} catch (error) {
|
|
5078
|
+
console.error("[EngineState] Failed to load state, using defaults:", error);
|
|
5079
|
+
return { ...DEFAULT_STATE };
|
|
5491
5080
|
}
|
|
5492
5081
|
}
|
|
5493
5082
|
async function loadRepoState(repoName) {
|
|
@@ -5563,8 +5152,7 @@ function appendUniqueUrl(urls, url) {
|
|
|
5563
5152
|
var GitService = class {
|
|
5564
5153
|
defaultBranchCache = /* @__PURE__ */ new Map();
|
|
5565
5154
|
cachedPrByRepo = /* @__PURE__ */ new Map();
|
|
5566
|
-
//
|
|
5567
|
-
// the worst case is PR/MR lookup stays skipped until the next engine restart.
|
|
5155
|
+
// An 'unknown' result usually means the origin isn't wired up yet during provisioning, so caching it would permanently mask a remote added moments later.
|
|
5568
5156
|
originInfoCache = /* @__PURE__ */ new Map();
|
|
5569
5157
|
// Broadcaster + UI /repos requests fan in here every ~2s; one shared run.
|
|
5570
5158
|
listReposInFlight = new InFlightMap();
|
|
@@ -5716,361 +5304,752 @@ var GitService = class {
|
|
|
5716
5304
|
console.warn(`[GitService] getGitDiffStats failed for ${repoPath} (defaultBranch=${defaultBranch}):`, error instanceof Error ? error.message : error);
|
|
5717
5305
|
return null;
|
|
5718
5306
|
}
|
|
5719
|
-
});
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
async listUntrackedPaths(repoPath) {
|
|
5310
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
5311
|
+
cwd: repoPath,
|
|
5312
|
+
encoding: "utf-8",
|
|
5313
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5314
|
+
});
|
|
5315
|
+
return stdout.split("\0").filter(Boolean);
|
|
5316
|
+
}
|
|
5317
|
+
async getUntrackedDiff(repoPath) {
|
|
5318
|
+
const paths = await this.listUntrackedPaths(repoPath);
|
|
5319
|
+
if (paths.length === 0) return "";
|
|
5320
|
+
try {
|
|
5321
|
+
await execFileAsync("git", ["add", "--intent-to-add", "--", ...paths], {
|
|
5322
|
+
cwd: repoPath,
|
|
5323
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5324
|
+
});
|
|
5325
|
+
return await this.readUntrackedDiff(repoPath, paths);
|
|
5326
|
+
} finally {
|
|
5327
|
+
try {
|
|
5328
|
+
await execFileAsync("git", ["reset", "--", ...paths], {
|
|
5329
|
+
cwd: repoPath,
|
|
5330
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5331
|
+
});
|
|
5332
|
+
} catch {
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
readUntrackedDiff(repoPath, paths) {
|
|
5337
|
+
return new Promise((resolve4) => {
|
|
5338
|
+
const child = spawn("git", ["diff", "--", ...paths], {
|
|
5339
|
+
cwd: repoPath,
|
|
5340
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5341
|
+
});
|
|
5342
|
+
const chunks = [];
|
|
5343
|
+
let total = 0;
|
|
5344
|
+
let truncated = false;
|
|
5345
|
+
child.stdout.on("data", (chunk) => {
|
|
5346
|
+
if (truncated) return;
|
|
5347
|
+
total += chunk.length;
|
|
5348
|
+
if (total > FULL_DIFF_MAX_BUFFER) {
|
|
5349
|
+
truncated = true;
|
|
5350
|
+
chunks.push(chunk.subarray(0, Math.max(0, FULL_DIFF_MAX_BUFFER - (total - chunk.length))));
|
|
5351
|
+
child.kill("SIGTERM");
|
|
5352
|
+
return;
|
|
5353
|
+
}
|
|
5354
|
+
chunks.push(chunk);
|
|
5355
|
+
});
|
|
5356
|
+
child.stderr.on("data", () => {
|
|
5357
|
+
});
|
|
5358
|
+
child.on("error", () => resolve4(""));
|
|
5359
|
+
child.on("close", (code) => {
|
|
5360
|
+
if (code === 0 || code === 1 || truncated) {
|
|
5361
|
+
resolve4(Buffer.concat(chunks).toString("utf-8"));
|
|
5362
|
+
} else {
|
|
5363
|
+
resolve4("");
|
|
5364
|
+
}
|
|
5365
|
+
});
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5368
|
+
async countUntrackedAddedLines(repoPath) {
|
|
5369
|
+
try {
|
|
5370
|
+
const paths = await this.listUntrackedPaths(repoPath);
|
|
5371
|
+
let total = 0;
|
|
5372
|
+
for (const path6 of paths) {
|
|
5373
|
+
try {
|
|
5374
|
+
const contents = await readFile3(join5(repoPath, path6));
|
|
5375
|
+
if (contents.length === 0 || contents.includes(0)) {
|
|
5376
|
+
continue;
|
|
5377
|
+
}
|
|
5378
|
+
let lines = 0;
|
|
5379
|
+
for (const byte of contents) {
|
|
5380
|
+
if (byte === 10) lines += 1;
|
|
5381
|
+
}
|
|
5382
|
+
total += lines + (contents[contents.length - 1] === 10 ? 0 : 1);
|
|
5383
|
+
} catch {
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
return total;
|
|
5387
|
+
} catch (error) {
|
|
5388
|
+
console.error("Error counting untracked added lines:", error);
|
|
5389
|
+
return 0;
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
getFullGitDiff(repoPath, defaultBranch) {
|
|
5393
|
+
return this.fullDiffInFlight.run(repoPath, async () => {
|
|
5394
|
+
try {
|
|
5395
|
+
const diffBase = await resolveDiffBase(repoPath, defaultBranch);
|
|
5396
|
+
const { stdout: trackedDiff } = await execFileAsync("git", ["diff", diffBase, "-M", "-C"], {
|
|
5397
|
+
cwd: repoPath,
|
|
5398
|
+
encoding: "utf-8",
|
|
5399
|
+
maxBuffer: FULL_DIFF_MAX_BUFFER
|
|
5400
|
+
});
|
|
5401
|
+
const untrackedDiff = await this.getUntrackedAsDiff(repoPath);
|
|
5402
|
+
return trackedDiff + untrackedDiff;
|
|
5403
|
+
} catch {
|
|
5404
|
+
return null;
|
|
5405
|
+
}
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
async getUntrackedAsDiff(repoPath) {
|
|
5409
|
+
try {
|
|
5410
|
+
return await this.getUntrackedDiff(repoPath);
|
|
5411
|
+
} catch (error) {
|
|
5412
|
+
console.error("Error building untracked diff:", error);
|
|
5413
|
+
return "";
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
async getPullRequestUrl(repoName, repoPath, currentBranchArg, persistedRepoStateArg) {
|
|
5417
|
+
try {
|
|
5418
|
+
const currentBranch = currentBranchArg ?? await getCurrentBranch(repoPath);
|
|
5419
|
+
if (!currentBranch) {
|
|
5420
|
+
return { status: "not_found" };
|
|
5421
|
+
}
|
|
5422
|
+
const cachedPr = this.cachedPrByRepo.get(repoName);
|
|
5423
|
+
if (cachedPr && cachedPr.currentBranch === currentBranch) {
|
|
5424
|
+
return { status: "found", url: cachedPr.prUrl };
|
|
5425
|
+
}
|
|
5426
|
+
const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
|
|
5427
|
+
this.cachedPrByRepo.delete(repoName);
|
|
5428
|
+
const result = await this.lookupPrOnRemote(repoName, repoPath, currentBranch);
|
|
5429
|
+
if (result.status === "found") {
|
|
5430
|
+
this.cachedPrByRepo.set(repoName, { prUrl: result.url, currentBranch });
|
|
5431
|
+
if (persistedRepoState && !persistedRepoState.prUrls.includes(result.url)) {
|
|
5432
|
+
await saveRepoState(
|
|
5433
|
+
repoName,
|
|
5434
|
+
{ prUrls: appendUniqueUrl(persistedRepoState.prUrls, result.url) },
|
|
5435
|
+
persistedRepoState
|
|
5436
|
+
);
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5439
|
+
return result;
|
|
5440
|
+
} catch (error) {
|
|
5441
|
+
console.error("Error checking for pull request:", error);
|
|
5442
|
+
return { status: "error" };
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
async lookupPrOnRemote(repoName, repoPath, branch) {
|
|
5446
|
+
const origin = await this.getOriginInfo(repoPath);
|
|
5447
|
+
if (origin.provider === "unknown") {
|
|
5448
|
+
return { status: "not_found" };
|
|
5449
|
+
}
|
|
5450
|
+
try {
|
|
5451
|
+
const { stdout } = await execFileAsync("git", ["ls-remote", "--heads", "origin", branch], {
|
|
5452
|
+
cwd: repoPath,
|
|
5453
|
+
encoding: "utf-8",
|
|
5454
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5455
|
+
});
|
|
5456
|
+
if (!stdout.trim()) {
|
|
5457
|
+
return { status: "not_found" };
|
|
5458
|
+
}
|
|
5459
|
+
} catch {
|
|
5460
|
+
return { status: "error" };
|
|
5461
|
+
}
|
|
5462
|
+
if (origin.provider === "gitlab") {
|
|
5463
|
+
return this.lookupGitLabMrOnRemote(repoName, origin.gitLab, branch);
|
|
5464
|
+
}
|
|
5465
|
+
try {
|
|
5466
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
|
|
5467
|
+
cwd: repoPath,
|
|
5468
|
+
encoding: "utf-8",
|
|
5469
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5470
|
+
});
|
|
5471
|
+
const prInfo = stdout.trim();
|
|
5472
|
+
return prInfo ? { status: "found", url: prInfo } : { status: "not_found" };
|
|
5473
|
+
} catch (error) {
|
|
5474
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5475
|
+
console.warn(`[GitService] gh pr view ${branch} failed for ${repoName}: ${message}`);
|
|
5476
|
+
return { status: "error" };
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
async lookupGitLabMrOnRemote(repoName, remote, branch) {
|
|
5480
|
+
const token = await this.getGitLabAccessToken(remote.host);
|
|
5481
|
+
if (!token) {
|
|
5482
|
+
return { status: "not_found" };
|
|
5483
|
+
}
|
|
5484
|
+
const url = `${remote.instanceUrl}/api/v4/projects/${encodeURIComponent(remote.projectPath)}/merge_requests?source_branch=${encodeURIComponent(branch)}&order_by=updated_at`;
|
|
5485
|
+
try {
|
|
5486
|
+
const response = await fetch(url, {
|
|
5487
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
5488
|
+
signal: AbortSignal.timeout(1e4)
|
|
5489
|
+
});
|
|
5490
|
+
if (!response.ok) {
|
|
5491
|
+
console.warn(`[GitService] GitLab MR lookup failed for ${repoName}: ${response.status}`);
|
|
5492
|
+
return response.status === 404 ? { status: "not_found" } : { status: "error" };
|
|
5493
|
+
}
|
|
5494
|
+
const data = await response.json();
|
|
5495
|
+
if (!Array.isArray(data)) return { status: "not_found" };
|
|
5496
|
+
const branchMatches = data.filter((item) => isRecord4(item) && item.source_branch === branch);
|
|
5497
|
+
const mergeRequest = branchMatches.find((item) => item.state === "opened") ?? branchMatches[0] ?? data[0];
|
|
5498
|
+
if (!isRecord4(mergeRequest)) return { status: "not_found" };
|
|
5499
|
+
const webUrl = mergeRequest.web_url;
|
|
5500
|
+
if (typeof webUrl === "string" && webUrl.length > 0) return { status: "found", url: webUrl };
|
|
5501
|
+
const iid = mergeRequest.iid;
|
|
5502
|
+
return typeof iid === "number" ? { status: "found", url: `${remote.instanceUrl}/${remote.projectPath}/-/merge_requests/${iid}` } : { status: "not_found" };
|
|
5503
|
+
} catch (error) {
|
|
5504
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5505
|
+
console.warn(`[GitService] GitLab MR lookup failed for ${repoName}: ${message}`);
|
|
5506
|
+
return { status: "error" };
|
|
5507
|
+
}
|
|
5720
5508
|
}
|
|
5721
|
-
async
|
|
5722
|
-
const
|
|
5723
|
-
|
|
5724
|
-
encoding: "utf-8",
|
|
5725
|
-
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5726
|
-
});
|
|
5727
|
-
return stdout.split("\0").filter(Boolean);
|
|
5509
|
+
async resolveCodeHostProvider(repoPath) {
|
|
5510
|
+
const origin = await this.getOriginInfo(repoPath);
|
|
5511
|
+
return origin.provider === "unknown" ? void 0 : origin.provider;
|
|
5728
5512
|
}
|
|
5729
|
-
async
|
|
5730
|
-
|
|
5731
|
-
|
|
5513
|
+
async getRepositoryOriginHost(repoPath, refresh = false) {
|
|
5514
|
+
if (refresh) this.originInfoCache.delete(repoPath);
|
|
5515
|
+
const origin = await this.getOriginInfo(repoPath);
|
|
5516
|
+
if (origin.provider === "github") return "github.com";
|
|
5517
|
+
if (origin.provider === "gitlab") return origin.gitLab.host;
|
|
5518
|
+
return null;
|
|
5519
|
+
}
|
|
5520
|
+
async getOriginInfo(repoPath) {
|
|
5521
|
+
const cached = this.originInfoCache.get(repoPath);
|
|
5522
|
+
if (cached !== void 0) {
|
|
5523
|
+
return cached;
|
|
5524
|
+
}
|
|
5525
|
+
let info = { provider: "unknown" };
|
|
5732
5526
|
try {
|
|
5733
|
-
await execFileAsync("git", ["
|
|
5527
|
+
const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], {
|
|
5734
5528
|
cwd: repoPath,
|
|
5529
|
+
encoding: "utf-8",
|
|
5735
5530
|
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5736
5531
|
});
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
});
|
|
5744
|
-
} catch {
|
|
5532
|
+
const originUrl = stdout.trim();
|
|
5533
|
+
if (isGitHubUrl(originUrl)) {
|
|
5534
|
+
info = { provider: "github" };
|
|
5535
|
+
} else {
|
|
5536
|
+
const gitLabRemote = this.parseGitLabRemote(originUrl);
|
|
5537
|
+
if (gitLabRemote) info = { provider: "gitlab", gitLab: gitLabRemote };
|
|
5745
5538
|
}
|
|
5539
|
+
} catch {
|
|
5540
|
+
info = { provider: "unknown" };
|
|
5746
5541
|
}
|
|
5542
|
+
if (info.provider !== "unknown") this.originInfoCache.set(repoPath, info);
|
|
5543
|
+
return info;
|
|
5747
5544
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
child.stdout.on("data", (chunk) => {
|
|
5758
|
-
if (truncated) return;
|
|
5759
|
-
total += chunk.length;
|
|
5760
|
-
if (total > FULL_DIFF_MAX_BUFFER) {
|
|
5761
|
-
truncated = true;
|
|
5762
|
-
chunks.push(chunk.subarray(0, Math.max(0, FULL_DIFF_MAX_BUFFER - (total - chunk.length))));
|
|
5763
|
-
child.kill("SIGTERM");
|
|
5764
|
-
return;
|
|
5765
|
-
}
|
|
5766
|
-
chunks.push(chunk);
|
|
5767
|
-
});
|
|
5768
|
-
child.stderr.on("data", () => {
|
|
5769
|
-
});
|
|
5770
|
-
child.on("error", () => resolve4(""));
|
|
5771
|
-
child.on("close", (code) => {
|
|
5772
|
-
if (code === 0 || code === 1 || truncated) {
|
|
5773
|
-
resolve4(Buffer.concat(chunks).toString("utf-8"));
|
|
5774
|
-
} else {
|
|
5775
|
-
resolve4("");
|
|
5776
|
-
}
|
|
5777
|
-
});
|
|
5778
|
-
});
|
|
5545
|
+
parseGitLabRemote(remoteUrl) {
|
|
5546
|
+
const parsed = parseGitRemote(remoteUrl);
|
|
5547
|
+
if (!parsed) return null;
|
|
5548
|
+
const projectPath = decodePathSegments(parsed.path.split("/").filter(Boolean)).join("/");
|
|
5549
|
+
return {
|
|
5550
|
+
host: parsed.host,
|
|
5551
|
+
instanceUrl: `${parsed.protocol === "http" ? "http" : "https"}://${parsed.host}`,
|
|
5552
|
+
projectPath
|
|
5553
|
+
};
|
|
5779
5554
|
}
|
|
5780
|
-
async
|
|
5555
|
+
async getGitLabAccessToken(host) {
|
|
5781
5556
|
try {
|
|
5782
|
-
const
|
|
5783
|
-
|
|
5784
|
-
|
|
5557
|
+
const credentials = await readFile3(join5(ENGINE_ENV.HOME_DIR, ".git-credentials"), "utf-8");
|
|
5558
|
+
for (const line of credentials.split("\n")) {
|
|
5559
|
+
const trimmed = line.trim();
|
|
5560
|
+
if (!trimmed) continue;
|
|
5785
5561
|
try {
|
|
5786
|
-
const
|
|
5787
|
-
if (
|
|
5788
|
-
|
|
5789
|
-
}
|
|
5790
|
-
let lines = 0;
|
|
5791
|
-
for (const byte of contents) {
|
|
5792
|
-
if (byte === 10) lines += 1;
|
|
5562
|
+
const parsed = new URL(trimmed);
|
|
5563
|
+
if (parsed.protocol === "https:" && parsed.host.toLowerCase() === host && parsed.password) {
|
|
5564
|
+
return decodeURIComponent(parsed.password);
|
|
5793
5565
|
}
|
|
5794
|
-
total += lines + (contents[contents.length - 1] === 10 ? 0 : 1);
|
|
5795
5566
|
} catch {
|
|
5796
5567
|
}
|
|
5797
5568
|
}
|
|
5798
|
-
|
|
5569
|
+
} catch {
|
|
5570
|
+
}
|
|
5571
|
+
return null;
|
|
5572
|
+
}
|
|
5573
|
+
async resolveDefaultBranch(repoPath) {
|
|
5574
|
+
const cached = this.defaultBranchCache.get(repoPath);
|
|
5575
|
+
if (cached) {
|
|
5576
|
+
return cached;
|
|
5577
|
+
}
|
|
5578
|
+
const fromSymbolicRef = await this.resolveDefaultBranchFromSymbolicRef(repoPath);
|
|
5579
|
+
if (fromSymbolicRef) {
|
|
5580
|
+
this.defaultBranchCache.set(repoPath, fromSymbolicRef);
|
|
5581
|
+
return fromSymbolicRef;
|
|
5582
|
+
}
|
|
5583
|
+
return "main";
|
|
5584
|
+
}
|
|
5585
|
+
async resolveDefaultBranchFromSymbolicRef(repoPath) {
|
|
5586
|
+
try {
|
|
5587
|
+
const output = await runGitCommand(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoPath);
|
|
5588
|
+
const match = output.match(/^origin\/(.+)$/);
|
|
5589
|
+
return match ? match[1] : null;
|
|
5590
|
+
} catch {
|
|
5591
|
+
return null;
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observedBranches, includeDiffs = false) {
|
|
5595
|
+
const prResult = await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState);
|
|
5596
|
+
let prUrls = persistedState?.prUrls ?? [];
|
|
5597
|
+
if (prResult.status === "found") {
|
|
5598
|
+
prUrls = appendUniqueUrl(prUrls, prResult.url);
|
|
5599
|
+
}
|
|
5600
|
+
if (observedBranches) {
|
|
5601
|
+
for (const branch of observedBranches) {
|
|
5602
|
+
if (branch === currentBranch) continue;
|
|
5603
|
+
const branchResult = await this.lookupPrOnRemote(repo.name, repo.path, branch);
|
|
5604
|
+
if (branchResult.status === "found") {
|
|
5605
|
+
prUrls = appendUniqueUrl(prUrls, branchResult.url);
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
const gitDiff = await this.getGitDiffStats(repo.path, repo.defaultBranch);
|
|
5610
|
+
const fullDiff = includeDiffs && gitDiff ? await this.getFullGitDiff(repo.path, repo.defaultBranch) : null;
|
|
5611
|
+
const state = {
|
|
5612
|
+
name: repo.name,
|
|
5613
|
+
path: repo.path,
|
|
5614
|
+
defaultBranch: repo.defaultBranch,
|
|
5615
|
+
currentBranch,
|
|
5616
|
+
prUrls,
|
|
5617
|
+
gitDiff: includeDiffs && gitDiff ? { ...gitDiff, ...fullDiff === null ? {} : { fullDiff } } : gitDiff,
|
|
5618
|
+
startHooksCompleted,
|
|
5619
|
+
provider: await this.resolveCodeHostProvider(repo.path)
|
|
5620
|
+
};
|
|
5621
|
+
await saveRepoState(repo.name, state, state);
|
|
5622
|
+
return state;
|
|
5623
|
+
}
|
|
5624
|
+
async safeStat(path6) {
|
|
5625
|
+
try {
|
|
5626
|
+
return await stat(path6);
|
|
5627
|
+
} catch {
|
|
5628
|
+
return null;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
};
|
|
5632
|
+
var gitService = new GitService();
|
|
5633
|
+
|
|
5634
|
+
// src/utils/git-identity.ts
|
|
5635
|
+
async function updateGitIdentity(identity, tag, hosts) {
|
|
5636
|
+
if (!hosts) {
|
|
5637
|
+
try {
|
|
5638
|
+
const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity);
|
|
5639
|
+
await execFileAsync("git", nameArgs);
|
|
5640
|
+
await execFileAsync("git", emailArgs);
|
|
5641
|
+
console.log(`[${tag}] Updated global git identity to ${identity.name} <${identity.email}>`);
|
|
5799
5642
|
} catch (error) {
|
|
5800
|
-
console.error(
|
|
5801
|
-
return 0;
|
|
5643
|
+
console.error(`[${tag}] Failed to update global git identity:`, error);
|
|
5802
5644
|
}
|
|
5645
|
+
return;
|
|
5803
5646
|
}
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
}
|
|
5816
|
-
|
|
5647
|
+
const normalizedHosts = hosts.map((host) => host.toLowerCase());
|
|
5648
|
+
const repositories = await gitService.listRepositories();
|
|
5649
|
+
await Promise.all(repositories.map(async (repository) => {
|
|
5650
|
+
try {
|
|
5651
|
+
const host = await gitService.getRepositoryOriginHost(repository.path, true);
|
|
5652
|
+
if (!host || !normalizedHosts.includes(host)) return;
|
|
5653
|
+
const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity, "local");
|
|
5654
|
+
await execFileAsync("git", nameArgs, { cwd: repository.path });
|
|
5655
|
+
await execFileAsync("git", emailArgs, { cwd: repository.path });
|
|
5656
|
+
console.log(`[${tag}] Updated git identity for ${repository.path} to ${identity.name} <${identity.email}>`);
|
|
5657
|
+
} catch (error) {
|
|
5658
|
+
console.error(`[${tag}] Failed to update git identity for ${repository.path}:`, error);
|
|
5659
|
+
}
|
|
5660
|
+
}));
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
// src/managers/github-token-manager.ts
|
|
5664
|
+
var GitHubTokenManager = class extends BaseRefreshManager {
|
|
5665
|
+
constructor() {
|
|
5666
|
+
super("GitHubTokenManager");
|
|
5667
|
+
}
|
|
5668
|
+
async doRefresh(_config) {
|
|
5669
|
+
console.log("[GitHubTokenManager] Refreshing GitHub token...");
|
|
5670
|
+
const response = await monolithRequest("/v1/engine/github/refresh-token");
|
|
5671
|
+
if (!response.ok) {
|
|
5672
|
+
if (response.status === 403) {
|
|
5673
|
+
await this.clearGitHubCredentials();
|
|
5817
5674
|
}
|
|
5818
|
-
|
|
5675
|
+
const errorText = await response.text();
|
|
5676
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
5677
|
+
}
|
|
5678
|
+
const data = await response.json();
|
|
5679
|
+
const ghToken = data.userToken?.token ?? data.token;
|
|
5680
|
+
const ghUsername = data.userToken?.username ?? "x-access-token";
|
|
5681
|
+
await this.updateGitCredentials(ghToken);
|
|
5682
|
+
await this.updateGhHostsFile(ghToken, ghUsername);
|
|
5683
|
+
if (data.gitIdentity) {
|
|
5684
|
+
await updateGitIdentity(data.gitIdentity, "GitHubTokenManager", ["github.com"]);
|
|
5685
|
+
}
|
|
5686
|
+
if (data.userToken) {
|
|
5687
|
+
console.log(`[GitHubTokenManager] Token refreshed with user token for PR attribution, user token expires at ${data.userToken.expiresAt}`);
|
|
5688
|
+
} else {
|
|
5689
|
+
console.log(`[GitHubTokenManager] Token refreshed successfully, expires at ${data.expiresAt}`);
|
|
5690
|
+
}
|
|
5819
5691
|
}
|
|
5820
|
-
async
|
|
5692
|
+
async updateGitCredentials(token) {
|
|
5693
|
+
const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5694
|
+
try {
|
|
5695
|
+
await upsertCredentialFileLines(credentialsPath, ["github.com"], [
|
|
5696
|
+
`https://x-access-token:${token}@github.com`
|
|
5697
|
+
]);
|
|
5698
|
+
console.log(`[GitHubTokenManager] Updated ${credentialsPath}`);
|
|
5699
|
+
} catch (error) {
|
|
5700
|
+
console.error("[GitHubTokenManager] Failed to update git credentials:", error);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5703
|
+
async updateGhHostsFile(token, username) {
|
|
5704
|
+
const hostsPath = path.join(ENGINE_ENV.HOME_DIR, ".config", "gh", "hosts.yml");
|
|
5705
|
+
const content = `github.com:
|
|
5706
|
+
oauth_token: ${JSON.stringify(token)}
|
|
5707
|
+
user: ${JSON.stringify(username)}
|
|
5708
|
+
git_protocol: https
|
|
5709
|
+
`;
|
|
5821
5710
|
try {
|
|
5822
|
-
|
|
5711
|
+
await writeSecureCredentialFile(hostsPath, content, { ensureParentDir: true });
|
|
5712
|
+
console.log(`[GitHubTokenManager] Updated ${hostsPath}`);
|
|
5823
5713
|
} catch (error) {
|
|
5824
|
-
console.error("
|
|
5825
|
-
return "";
|
|
5714
|
+
console.error("[GitHubTokenManager] Failed to update gh hosts file:", error);
|
|
5826
5715
|
}
|
|
5827
5716
|
}
|
|
5828
|
-
async
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
}
|
|
5717
|
+
async clearGitHubCredentials() {
|
|
5718
|
+
const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5719
|
+
await removeCredentialFileLines(
|
|
5720
|
+
credentialsPath,
|
|
5721
|
+
(line) => line.endsWith("@github.com")
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5724
|
+
};
|
|
5725
|
+
var githubTokenManager = new GitHubTokenManager();
|
|
5726
|
+
|
|
5727
|
+
// src/managers/gitlab-token-manager.ts
|
|
5728
|
+
import path2 from "path";
|
|
5729
|
+
var GitLabTokenManager = class extends BaseRefreshManager {
|
|
5730
|
+
constructor() {
|
|
5731
|
+
super("GitLabTokenManager");
|
|
5732
|
+
}
|
|
5733
|
+
async doRefresh(_config) {
|
|
5734
|
+
const response = await monolithRequest("/v1/engine/gitlab/refresh-token");
|
|
5735
|
+
if (!response.ok) {
|
|
5736
|
+
if (response.status === 403) {
|
|
5737
|
+
await this.clearGitLabCredentials();
|
|
5850
5738
|
}
|
|
5851
|
-
|
|
5852
|
-
} catch (error) {
|
|
5853
|
-
console.error("Error checking for pull request:", error);
|
|
5854
|
-
return { status: "error" };
|
|
5739
|
+
throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
|
|
5855
5740
|
}
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
return
|
|
5741
|
+
const data = await response.json();
|
|
5742
|
+
if (!data.token) {
|
|
5743
|
+
await this.clearGitLabCredentials(data.hosts);
|
|
5744
|
+
console.log(`[GitLabTokenManager] No GitLab token to install: ${data.reason}`);
|
|
5745
|
+
return;
|
|
5861
5746
|
}
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
})
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
return { status: "error" };
|
|
5747
|
+
const hosts = data.hosts.length > 0 ? data.hosts : ["gitlab.com"];
|
|
5748
|
+
const credentialsPath = path2.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5749
|
+
await upsertCredentialFileLines(
|
|
5750
|
+
credentialsPath,
|
|
5751
|
+
hosts,
|
|
5752
|
+
hosts.map((host) => `https://oauth2:${data.token}@${host}`)
|
|
5753
|
+
);
|
|
5754
|
+
console.log(`[GitLabTokenManager] Updated ${credentialsPath} for ${hosts.join(", ")}`);
|
|
5755
|
+
if (data.gitIdentity) {
|
|
5756
|
+
await updateGitIdentity(data.gitIdentity, "GitLabTokenManager", hosts);
|
|
5873
5757
|
}
|
|
5874
|
-
|
|
5875
|
-
|
|
5758
|
+
}
|
|
5759
|
+
async clearGitLabCredentials(hosts = []) {
|
|
5760
|
+
const credentialsPath = path2.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
|
|
5761
|
+
await removeCredentialFileLines(
|
|
5762
|
+
credentialsPath,
|
|
5763
|
+
(line) => line.startsWith("https://oauth2:") && (hosts.length === 0 || hosts.some((host) => line.endsWith(`@${host}`)))
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5766
|
+
};
|
|
5767
|
+
var gitlabTokenManager = new GitLabTokenManager();
|
|
5768
|
+
|
|
5769
|
+
// src/managers/claude-token-manager.ts
|
|
5770
|
+
import { promises as fs } from "fs";
|
|
5771
|
+
import path3 from "path";
|
|
5772
|
+
|
|
5773
|
+
// src/managers/auth-env-transition.ts
|
|
5774
|
+
function applyAuthEnvTransition(params) {
|
|
5775
|
+
const newOwned = new Set(params.authKeysByMethod[params.newMethod]);
|
|
5776
|
+
const prevOwned = new Set(params.authKeysByMethod[params.prevMethod]);
|
|
5777
|
+
for (const key of params.authKeys) {
|
|
5778
|
+
const value = params.newEnvVars[key];
|
|
5779
|
+
if (value !== void 0) {
|
|
5780
|
+
for (const env of params.envs) {
|
|
5781
|
+
env[key] = value;
|
|
5782
|
+
}
|
|
5783
|
+
} else if (prevOwned.has(key) && !newOwned.has(key)) {
|
|
5784
|
+
for (const env of params.envs) {
|
|
5785
|
+
delete env[key];
|
|
5786
|
+
}
|
|
5876
5787
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
return
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
// src/managers/claude-token-manager.ts
|
|
5792
|
+
var ClaudeTokenManager = class extends BaseRefreshManager {
|
|
5793
|
+
constructor() {
|
|
5794
|
+
super("ClaudeTokenManager");
|
|
5795
|
+
}
|
|
5796
|
+
getSkipReason() {
|
|
5797
|
+
const method = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
|
|
5798
|
+
if (method === "api_key" || method === "bedrock" || method === "foundry") {
|
|
5799
|
+
return `auth method is ${method}`;
|
|
5889
5800
|
}
|
|
5801
|
+
return null;
|
|
5890
5802
|
}
|
|
5891
|
-
async
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5803
|
+
async doRefresh(_config) {
|
|
5804
|
+
await this.refreshWithRequest();
|
|
5805
|
+
}
|
|
5806
|
+
async refreshWithRequest(request) {
|
|
5807
|
+
console.log("[ClaudeTokenManager] Refreshing Claude credentials...");
|
|
5808
|
+
const response = await monolithRequest("/v1/engine/claude/refresh-credentials", {
|
|
5809
|
+
body: request
|
|
5810
|
+
});
|
|
5811
|
+
if (!response.ok) {
|
|
5812
|
+
const errorText = await response.text();
|
|
5813
|
+
throw new Error(`Credentials refresh failed: ${response.status} ${errorText}`);
|
|
5895
5814
|
}
|
|
5896
|
-
const
|
|
5815
|
+
const data = await response.json();
|
|
5816
|
+
await this.applyCredentialsResponse(data);
|
|
5817
|
+
console.log(`[ClaudeTokenManager] Credentials refreshed (method=${data.type})`);
|
|
5818
|
+
}
|
|
5819
|
+
async fetchFreshCredentials(failureReason) {
|
|
5820
|
+
const config = this.getRuntimeConfig();
|
|
5821
|
+
if (!config) return false;
|
|
5897
5822
|
try {
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5823
|
+
console.log("[ClaudeTokenManager] Fetching fresh credentials from monolith after auth failure...");
|
|
5824
|
+
const failedMethod = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
|
|
5825
|
+
await this.refreshWithRequest(failedMethod && failedMethod !== "none" ? {
|
|
5826
|
+
failedMethod,
|
|
5827
|
+
failureReason
|
|
5828
|
+
} : void 0);
|
|
5829
|
+
if (ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD === "oauth") {
|
|
5830
|
+
this.start().catch((error) => {
|
|
5831
|
+
console.error("[ClaudeTokenManager] Failed to restart OAuth refresh service after fallback:", error);
|
|
5832
|
+
});
|
|
5905
5833
|
}
|
|
5906
|
-
|
|
5907
|
-
if (!Array.isArray(data)) return { status: "not_found" };
|
|
5908
|
-
const branchMatches = data.filter((item) => isRecord4(item) && item.source_branch === branch);
|
|
5909
|
-
const mergeRequest = branchMatches.find((item) => item.state === "opened") ?? branchMatches[0] ?? data[0];
|
|
5910
|
-
if (!isRecord4(mergeRequest)) return { status: "not_found" };
|
|
5911
|
-
const webUrl = mergeRequest.web_url;
|
|
5912
|
-
if (typeof webUrl === "string" && webUrl.length > 0) return { status: "found", url: webUrl };
|
|
5913
|
-
const iid = mergeRequest.iid;
|
|
5914
|
-
return typeof iid === "number" ? { status: "found", url: `${remote.instanceUrl}/${remote.projectPath}/-/merge_requests/${iid}` } : { status: "not_found" };
|
|
5834
|
+
return true;
|
|
5915
5835
|
} catch (error) {
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
return { status: "error" };
|
|
5836
|
+
console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
|
|
5837
|
+
return false;
|
|
5919
5838
|
}
|
|
5920
5839
|
}
|
|
5921
|
-
async
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
}
|
|
5930
|
-
let info = { provider: "unknown" };
|
|
5931
|
-
try {
|
|
5932
|
-
const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], {
|
|
5933
|
-
cwd: repoPath,
|
|
5934
|
-
encoding: "utf-8",
|
|
5935
|
-
maxBuffer: SUBPROCESS_MAX_BUFFER
|
|
5840
|
+
async applyCredentialsResponse(response) {
|
|
5841
|
+
if (response.type === "oauth") {
|
|
5842
|
+
await this.writeOauthCredentialsFile({
|
|
5843
|
+
accessToken: response.accessToken,
|
|
5844
|
+
refreshToken: response.refreshToken,
|
|
5845
|
+
expiresAt: response.expiresAt,
|
|
5846
|
+
scopes: response.scopes,
|
|
5847
|
+
subscriptionType: response.subscriptionType
|
|
5936
5848
|
});
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5849
|
+
} else {
|
|
5850
|
+
await this.removeOauthCredentialsFile();
|
|
5851
|
+
}
|
|
5852
|
+
const envVars = claudeAuthEnvFromResponse(response);
|
|
5853
|
+
applyAuthEnvTransition({
|
|
5854
|
+
prevMethod: ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD ?? "none",
|
|
5855
|
+
newMethod: envVars.REPLICAS_CLAUDE_AUTH_METHOD ?? "none",
|
|
5856
|
+
authKeys: CLAUDE_AUTH_ENV_KEYS,
|
|
5857
|
+
authKeysByMethod: CLAUDE_AUTH_ENV_KEYS_BY_METHOD,
|
|
5858
|
+
newEnvVars: envVars,
|
|
5859
|
+
envs: [ENGINE_ENV, process.env]
|
|
5860
|
+
});
|
|
5861
|
+
}
|
|
5862
|
+
async writeOauthCredentialsFile(credentials) {
|
|
5863
|
+
const credentialsPath = path3.join(ENGINE_ENV.HOME_DIR, ".claude", ".credentials.json");
|
|
5864
|
+
const claudeCliConfig = {
|
|
5865
|
+
claudeAiOauth: {
|
|
5866
|
+
accessToken: credentials.accessToken,
|
|
5867
|
+
refreshToken: credentials.refreshToken,
|
|
5868
|
+
expiresAt: new Date(credentials.expiresAt).getTime(),
|
|
5869
|
+
scopes: credentials.scopes,
|
|
5870
|
+
subscriptionType: credentials.subscriptionType
|
|
5943
5871
|
}
|
|
5944
|
-
}
|
|
5945
|
-
|
|
5872
|
+
};
|
|
5873
|
+
try {
|
|
5874
|
+
await writeSecureCredentialFile(
|
|
5875
|
+
credentialsPath,
|
|
5876
|
+
JSON.stringify(claudeCliConfig, null, 2),
|
|
5877
|
+
{ ensureParentDir: true }
|
|
5878
|
+
);
|
|
5879
|
+
console.log(`[ClaudeTokenManager] Updated ${credentialsPath}`);
|
|
5880
|
+
} catch (error) {
|
|
5881
|
+
console.error("[ClaudeTokenManager] Failed to update credentials file:", error);
|
|
5946
5882
|
}
|
|
5947
|
-
this.originInfoCache.set(repoPath, info);
|
|
5948
|
-
return info;
|
|
5949
5883
|
}
|
|
5950
|
-
|
|
5951
|
-
const
|
|
5884
|
+
async removeOauthCredentialsFile() {
|
|
5885
|
+
const credentialsPath = path3.join(ENGINE_ENV.HOME_DIR, ".claude", ".credentials.json");
|
|
5952
5886
|
try {
|
|
5953
|
-
|
|
5954
|
-
if (parsed.protocol === "ssh:") {
|
|
5955
|
-
const projectPath = decodePathSegments(parsed.pathname.split("/").filter(Boolean)).join("/");
|
|
5956
|
-
if (!parsed.hostname || !projectPath) return null;
|
|
5957
|
-
const host = parsed.hostname.toLowerCase();
|
|
5958
|
-
return {
|
|
5959
|
-
host,
|
|
5960
|
-
instanceUrl: `https://${host}`,
|
|
5961
|
-
projectPath
|
|
5962
|
-
};
|
|
5963
|
-
}
|
|
5887
|
+
await fs.unlink(credentialsPath);
|
|
5964
5888
|
} catch {
|
|
5965
5889
|
}
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5890
|
+
}
|
|
5891
|
+
};
|
|
5892
|
+
var claudeTokenManager = new ClaudeTokenManager();
|
|
5893
|
+
|
|
5894
|
+
// src/managers/codex-token-manager.ts
|
|
5895
|
+
import { promises as fs2 } from "fs";
|
|
5896
|
+
import path4 from "path";
|
|
5897
|
+
var CodexTokenManager = class extends BaseRefreshManager {
|
|
5898
|
+
constructor() {
|
|
5899
|
+
super("CodexTokenManager");
|
|
5900
|
+
}
|
|
5901
|
+
getSkipReason() {
|
|
5902
|
+
if (ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "api_key" || ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "foundry") {
|
|
5903
|
+
return `auth method is ${ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD}`;
|
|
5976
5904
|
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
try {
|
|
5980
|
-
const parsed = new URL(normalized);
|
|
5981
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
|
5982
|
-
const projectPath = decodePathSegments(parsed.pathname.split("/").filter(Boolean)).join("/");
|
|
5983
|
-
if (!projectPath) return null;
|
|
5984
|
-
return {
|
|
5985
|
-
host: parsed.host.toLowerCase(),
|
|
5986
|
-
instanceUrl: parsed.origin,
|
|
5987
|
-
projectPath
|
|
5988
|
-
};
|
|
5989
|
-
} catch {
|
|
5905
|
+
if (!ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD && ENGINE_ENV.OPENAI_API_KEY) {
|
|
5906
|
+
return "OPENAI_API_KEY is set";
|
|
5990
5907
|
}
|
|
5991
5908
|
return null;
|
|
5992
5909
|
}
|
|
5993
|
-
async
|
|
5910
|
+
async doRefresh(_config) {
|
|
5911
|
+
await this.refreshWithRequest();
|
|
5912
|
+
}
|
|
5913
|
+
async refreshWithRequest(request) {
|
|
5914
|
+
console.log("[CodexTokenManager] Refreshing Codex credentials...");
|
|
5915
|
+
const response = await monolithRequest("/v1/engine/codex/refresh-credentials", {
|
|
5916
|
+
body: request
|
|
5917
|
+
});
|
|
5918
|
+
if (!response.ok) {
|
|
5919
|
+
const errorText = await response.text();
|
|
5920
|
+
throw new Error(`Credentials refresh failed: ${response.status} ${errorText}`);
|
|
5921
|
+
}
|
|
5922
|
+
const data = await response.json();
|
|
5923
|
+
await this.applyCredentialsResponse(data);
|
|
5924
|
+
console.log(`[CodexTokenManager] Credentials refreshed (method=${data.type})`);
|
|
5925
|
+
}
|
|
5926
|
+
async fetchFreshCredentials(failureReason) {
|
|
5927
|
+
const config = this.getRuntimeConfig();
|
|
5928
|
+
if (!config) return false;
|
|
5994
5929
|
try {
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
}
|
|
6005
|
-
}
|
|
5930
|
+
console.log("[CodexTokenManager] Fetching fresh credentials from monolith after auth failure...");
|
|
5931
|
+
const failedMethod = ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD;
|
|
5932
|
+
await this.refreshWithRequest(failedMethod === "oauth" || failedMethod === "api_key" || failedMethod === "foundry" ? {
|
|
5933
|
+
failedMethod,
|
|
5934
|
+
failureReason
|
|
5935
|
+
} : void 0);
|
|
5936
|
+
if (ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD === "oauth") {
|
|
5937
|
+
this.start().catch((error) => {
|
|
5938
|
+
console.error("[CodexTokenManager] Failed to restart OAuth refresh service after fallback:", error);
|
|
5939
|
+
});
|
|
6006
5940
|
}
|
|
6007
|
-
|
|
5941
|
+
return true;
|
|
5942
|
+
} catch (error) {
|
|
5943
|
+
console.error("[CodexTokenManager] Failed to fetch fresh credentials:", error);
|
|
5944
|
+
return false;
|
|
6008
5945
|
}
|
|
6009
|
-
return null;
|
|
6010
5946
|
}
|
|
6011
|
-
async
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
5947
|
+
async applyCredentialsResponse(response) {
|
|
5948
|
+
if (response.type === "oauth") {
|
|
5949
|
+
await this.writeOauthCredentialsFile(response);
|
|
5950
|
+
} else {
|
|
5951
|
+
await this.removeOauthCredentialsFile();
|
|
6015
5952
|
}
|
|
6016
|
-
const
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
5953
|
+
const envVars = codexAuthEnvFromResponse(response);
|
|
5954
|
+
applyAuthEnvTransition({
|
|
5955
|
+
prevMethod: ENGINE_ENV.REPLICAS_CODEX_AUTH_METHOD ?? "none",
|
|
5956
|
+
newMethod: envVars.REPLICAS_CODEX_AUTH_METHOD ?? "none",
|
|
5957
|
+
authKeys: CODEX_AUTH_ENV_KEYS,
|
|
5958
|
+
authKeysByMethod: CODEX_AUTH_ENV_KEYS_BY_METHOD,
|
|
5959
|
+
newEnvVars: envVars,
|
|
5960
|
+
envs: [ENGINE_ENV, process.env]
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5963
|
+
async writeOauthCredentialsFile(credentials) {
|
|
5964
|
+
const authPath = path4.join(ENGINE_ENV.HOME_DIR, ".codex", "auth.json");
|
|
5965
|
+
const codexAuthConfig = {
|
|
5966
|
+
OPENAI_API_KEY: null,
|
|
5967
|
+
tokens: {
|
|
5968
|
+
id_token: credentials.idToken,
|
|
5969
|
+
access_token: credentials.accessToken,
|
|
5970
|
+
refresh_token: credentials.refreshToken,
|
|
5971
|
+
account_id: credentials.accountId
|
|
5972
|
+
},
|
|
5973
|
+
last_refresh: (/* @__PURE__ */ new Date()).toISOString()
|
|
5974
|
+
};
|
|
5975
|
+
try {
|
|
5976
|
+
await writeSecureCredentialFile(
|
|
5977
|
+
authPath,
|
|
5978
|
+
JSON.stringify(codexAuthConfig, null, 2),
|
|
5979
|
+
{ ensureParentDir: true }
|
|
5980
|
+
);
|
|
5981
|
+
console.log(`[CodexTokenManager] Updated ${authPath}`);
|
|
5982
|
+
} catch (error) {
|
|
5983
|
+
console.error("[CodexTokenManager] Failed to update credentials file:", error);
|
|
6020
5984
|
}
|
|
6021
|
-
return "main";
|
|
6022
5985
|
}
|
|
6023
|
-
async
|
|
5986
|
+
async removeOauthCredentialsFile() {
|
|
5987
|
+
const authPath = path4.join(ENGINE_ENV.HOME_DIR, ".codex", "auth.json");
|
|
6024
5988
|
try {
|
|
6025
|
-
|
|
6026
|
-
const match = output.match(/^origin\/(.+)$/);
|
|
6027
|
-
return match ? match[1] : null;
|
|
5989
|
+
await fs2.unlink(authPath);
|
|
6028
5990
|
} catch {
|
|
6029
|
-
return null;
|
|
6030
5991
|
}
|
|
6031
5992
|
}
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
const gitDiff = await this.getGitDiffStats(repo.path, repo.defaultBranch);
|
|
6048
|
-
const fullDiff = includeDiffs && gitDiff ? await this.getFullGitDiff(repo.path, repo.defaultBranch) : null;
|
|
6049
|
-
const state = {
|
|
6050
|
-
name: repo.name,
|
|
6051
|
-
path: repo.path,
|
|
6052
|
-
defaultBranch: repo.defaultBranch,
|
|
6053
|
-
currentBranch,
|
|
6054
|
-
prUrls,
|
|
6055
|
-
gitDiff: includeDiffs && gitDiff ? { ...gitDiff, ...fullDiff === null ? {} : { fullDiff } } : gitDiff,
|
|
6056
|
-
startHooksCompleted,
|
|
6057
|
-
provider: await this.resolveCodeHostProvider(repo.path)
|
|
6058
|
-
};
|
|
6059
|
-
await saveRepoState(repo.name, state, state);
|
|
6060
|
-
return state;
|
|
5993
|
+
};
|
|
5994
|
+
var codexTokenManager = new CodexTokenManager();
|
|
5995
|
+
|
|
5996
|
+
// src/managers/infisical-token-manager.ts
|
|
5997
|
+
import { rm } from "fs/promises";
|
|
5998
|
+
import path5 from "path";
|
|
5999
|
+
var InfisicalTokenManager = class extends BaseRefreshManager {
|
|
6000
|
+
constructor(request = monolithRequest, paths = {
|
|
6001
|
+
homeDir: ENGINE_ENV.HOME_DIR,
|
|
6002
|
+
workspaceRoot: ENGINE_ENV.WORKSPACE_ROOT
|
|
6003
|
+
}, applyCredentials = applyInfisicalCredentials) {
|
|
6004
|
+
super("InfisicalTokenManager", 5 * 60 * 1e3);
|
|
6005
|
+
this.request = request;
|
|
6006
|
+
this.paths = paths;
|
|
6007
|
+
this.applyCredentials = applyCredentials;
|
|
6061
6008
|
}
|
|
6062
|
-
|
|
6063
|
-
|
|
6009
|
+
request;
|
|
6010
|
+
paths;
|
|
6011
|
+
applyCredentials;
|
|
6012
|
+
nextRefreshDelayMs = 5 * 60 * 1e3;
|
|
6013
|
+
async doRefresh(_config) {
|
|
6014
|
+
const response = await this.request("/v1/engine/infisical/refresh-token");
|
|
6015
|
+
if (!response.ok) throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
|
|
6016
|
+
const data = await response.json();
|
|
6017
|
+
await this.applyCredentials(data, this.paths);
|
|
6018
|
+
const ttlMs = data.configured ? Date.parse(data.expiresAt) - Date.now() : Number.NaN;
|
|
6019
|
+
this.nextRefreshDelayMs = Number.isFinite(ttlMs) ? Math.max(1e3, Math.min(5 * 60 * 1e3, Math.floor(ttlMs * 0.8))) : 5 * 60 * 1e3;
|
|
6064
6020
|
}
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
return await stat(path6);
|
|
6068
|
-
} catch {
|
|
6069
|
-
return null;
|
|
6070
|
-
}
|
|
6021
|
+
getNextRefreshDelayMs() {
|
|
6022
|
+
return this.nextRefreshDelayMs;
|
|
6071
6023
|
}
|
|
6072
6024
|
};
|
|
6073
|
-
|
|
6025
|
+
async function applyInfisicalCredentials(data, paths) {
|
|
6026
|
+
const credentialPath = path5.join(paths.homeDir, ".replicas", "infisical-env.sh");
|
|
6027
|
+
const configPath = path5.join(paths.workspaceRoot, ".infisical.json");
|
|
6028
|
+
if (!data.configured) {
|
|
6029
|
+
delete process.env.INFISICAL_TOKEN;
|
|
6030
|
+
delete process.env.INFISICAL_DOMAIN;
|
|
6031
|
+
await Promise.all([rm(credentialPath, { force: true }), rm(configPath, { force: true })]);
|
|
6032
|
+
return;
|
|
6033
|
+
}
|
|
6034
|
+
process.env.INFISICAL_TOKEN = data.token;
|
|
6035
|
+
process.env.INFISICAL_DOMAIN = data.siteUrl;
|
|
6036
|
+
await Promise.all([
|
|
6037
|
+
writeSecureCredentialFile(credentialPath, [
|
|
6038
|
+
`export INFISICAL_TOKEN=${shellQuotePosix(data.token)}`,
|
|
6039
|
+
`export INFISICAL_DOMAIN=${shellQuotePosix(data.siteUrl)}`,
|
|
6040
|
+
"export INFISICAL_DISABLE_UPDATE_CHECK=true",
|
|
6041
|
+
""
|
|
6042
|
+
].join("\n"), { ensureParentDir: true }),
|
|
6043
|
+
writeSecureCredentialFile(configPath, `${JSON.stringify({
|
|
6044
|
+
workspaceId: data.projectId,
|
|
6045
|
+
defaultEnvironment: data.environment,
|
|
6046
|
+
gitBranchToEnvironmentMapping: null,
|
|
6047
|
+
domain: data.siteUrl
|
|
6048
|
+
}, null, 2)}
|
|
6049
|
+
`, { ensureParentDir: true })
|
|
6050
|
+
]);
|
|
6051
|
+
}
|
|
6052
|
+
var infisicalTokenManager = new InfisicalTokenManager();
|
|
6074
6053
|
|
|
6075
6054
|
// src/utils/logger.ts
|
|
6076
6055
|
import { mkdir as mkdir3, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -6195,14 +6174,14 @@ var engineLogger = new EngineLogger();
|
|
|
6195
6174
|
|
|
6196
6175
|
// src/services/replicas-config-service.ts
|
|
6197
6176
|
import { readFile as readFile6, appendFile, writeFile as writeFile4, mkdir as mkdir6 } from "fs/promises";
|
|
6198
|
-
import { existsSync as
|
|
6177
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6199
6178
|
import { join as join9 } from "path";
|
|
6200
6179
|
import { homedir as homedir7 } from "os";
|
|
6201
6180
|
import { spawn as spawn2 } from "child_process";
|
|
6202
6181
|
|
|
6203
6182
|
// src/services/environment-details-service.ts
|
|
6204
6183
|
import { mkdir as mkdir4, readFile as readFile4 } from "fs/promises";
|
|
6205
|
-
import { existsSync as
|
|
6184
|
+
import { existsSync as existsSync2 } from "fs";
|
|
6206
6185
|
import { homedir as homedir5 } from "os";
|
|
6207
6186
|
import { join as join7 } from "path";
|
|
6208
6187
|
var REPLICAS_DIR = join7(homedir5(), ".replicas");
|
|
@@ -6213,7 +6192,7 @@ var OPENCODE_AUTH_PATH = join7(homedir5(), ".local", "share", "opencode", "auth.
|
|
|
6213
6192
|
var GH_HOSTS_PATH = join7(homedir5(), ".config", "gh", "hosts.yml");
|
|
6214
6193
|
var GIT_CREDENTIALS_PATH = join7(homedir5(), ".git-credentials");
|
|
6215
6194
|
function detectClaudeAuthMethod() {
|
|
6216
|
-
if (
|
|
6195
|
+
if (existsSync2(CLAUDE_CREDENTIALS_PATH)) {
|
|
6217
6196
|
return "oauth";
|
|
6218
6197
|
}
|
|
6219
6198
|
if (ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD === "foundry") {
|
|
@@ -6228,7 +6207,7 @@ function detectClaudeAuthMethod() {
|
|
|
6228
6207
|
return "none";
|
|
6229
6208
|
}
|
|
6230
6209
|
function detectCodexAuthMethod() {
|
|
6231
|
-
if (
|
|
6210
|
+
if (existsSync2(CODEX_AUTH_PATH)) {
|
|
6232
6211
|
return "oauth";
|
|
6233
6212
|
}
|
|
6234
6213
|
if (ENGINE_ENV.OPENAI_API_KEY) {
|
|
@@ -6241,7 +6220,7 @@ function detectCursorAuthMethod() {
|
|
|
6241
6220
|
return ENGINE_ENV.CURSOR_API_KEY ? "api_key" : "none";
|
|
6242
6221
|
}
|
|
6243
6222
|
function detectOpencodeAuthMethod() {
|
|
6244
|
-
return
|
|
6223
|
+
return existsSync2(OPENCODE_AUTH_PATH) || ENGINE_ENV.OPENROUTER_API_KEY ? "api_key" : "none";
|
|
6245
6224
|
}
|
|
6246
6225
|
function detectPiAuthMethod() {
|
|
6247
6226
|
return ENGINE_ENV.OPENROUTER_API_KEY ? "api_key" : "none";
|
|
@@ -6309,7 +6288,7 @@ function createDefaultDetails() {
|
|
|
6309
6288
|
}
|
|
6310
6289
|
async function readDetails() {
|
|
6311
6290
|
try {
|
|
6312
|
-
if (!
|
|
6291
|
+
if (!existsSync2(DETAILS_FILE)) {
|
|
6313
6292
|
return createDefaultDetails();
|
|
6314
6293
|
}
|
|
6315
6294
|
const raw = await readFile4(DETAILS_FILE, "utf-8");
|
|
@@ -6335,13 +6314,14 @@ var EnvironmentDetailsService = class {
|
|
|
6335
6314
|
details.engineVersion = E2B_TEMPLATE_NAME;
|
|
6336
6315
|
details.supportsMidTurnSteering = true;
|
|
6337
6316
|
details.supportsGoalUpdates = true;
|
|
6317
|
+
details.supportsQueuedMessageEditing = true;
|
|
6338
6318
|
details.claudeAuthMethod = detectClaudeAuthMethod();
|
|
6339
6319
|
details.codexAuthMethod = detectCodexAuthMethod();
|
|
6340
6320
|
details.cursorAuthMethod = detectCursorAuthMethod();
|
|
6341
6321
|
details.opencodeAuthMethod = detectOpencodeAuthMethod();
|
|
6342
6322
|
details.piAuthMethod = detectPiAuthMethod();
|
|
6343
6323
|
details.gitIdentityConfigured = gitIdentityConfigured;
|
|
6344
|
-
const ghConfigured =
|
|
6324
|
+
const ghConfigured = existsSync2(GH_HOSTS_PATH);
|
|
6345
6325
|
details.githubAccessConfigured = ghConfigured;
|
|
6346
6326
|
details.githubCredentialsConfigured = ghConfigured;
|
|
6347
6327
|
details.gitlabAccessConfigured = gitlabAccessConfigured;
|
|
@@ -6540,7 +6520,7 @@ If your task depends on setup being complete, check the log file before proceedi
|
|
|
6540
6520
|
async function readReplicasConfigFromDir(dirPath) {
|
|
6541
6521
|
for (const filename of REPLICAS_CONFIG_FILENAMES) {
|
|
6542
6522
|
const configPath = join9(dirPath, filename);
|
|
6543
|
-
if (!
|
|
6523
|
+
if (!existsSync3(configPath)) {
|
|
6544
6524
|
continue;
|
|
6545
6525
|
}
|
|
6546
6526
|
const data = await readFile6(configPath, "utf-8");
|
|
@@ -6975,14 +6955,14 @@ var eventService = new EventService();
|
|
|
6975
6955
|
|
|
6976
6956
|
// src/services/preview-service.ts
|
|
6977
6957
|
import { mkdir as mkdir8, readFile as readFile7 } from "fs/promises";
|
|
6978
|
-
import { existsSync as
|
|
6958
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6979
6959
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6980
6960
|
import { homedir as homedir9 } from "os";
|
|
6981
6961
|
import { dirname as dirname2, join as join11 } from "path";
|
|
6982
6962
|
var PREVIEW_PORTS_FILE = join11(homedir9(), ".replicas", "preview-ports.json");
|
|
6983
6963
|
async function readPreviewsFile() {
|
|
6984
6964
|
try {
|
|
6985
|
-
if (!
|
|
6965
|
+
if (!existsSync4(PREVIEW_PORTS_FILE)) {
|
|
6986
6966
|
return { previews: [] };
|
|
6987
6967
|
}
|
|
6988
6968
|
const raw = await readFile7(PREVIEW_PORTS_FILE, "utf-8");
|
|
@@ -7089,7 +7069,7 @@ async function registerDesktopPreview() {
|
|
|
7089
7069
|
}
|
|
7090
7070
|
|
|
7091
7071
|
// src/services/chat/chat-service.ts
|
|
7092
|
-
import { existsSync as
|
|
7072
|
+
import { existsSync as existsSync7 } from "fs";
|
|
7093
7073
|
import { appendFile as appendFile3, copyFile, mkdir as mkdir14, readFile as readFile14, rename as rename2, rm as rm2 } from "fs/promises";
|
|
7094
7074
|
import { homedir as homedir15 } from "os";
|
|
7095
7075
|
import { join as join22 } from "path";
|
|
@@ -7730,6 +7710,12 @@ var MessageQueueService = class {
|
|
|
7730
7710
|
this.queue = [];
|
|
7731
7711
|
return true;
|
|
7732
7712
|
}
|
|
7713
|
+
updateQueuedMessage(messageId, message) {
|
|
7714
|
+
const queuedMessage = this.queue.find((item) => item.id === messageId);
|
|
7715
|
+
if (!queuedMessage) return false;
|
|
7716
|
+
queuedMessage.message = message;
|
|
7717
|
+
return true;
|
|
7718
|
+
}
|
|
7733
7719
|
takeFromQueue(messageId) {
|
|
7734
7720
|
const index = this.queue.findIndex((m) => m.id === messageId);
|
|
7735
7721
|
if (index === -1) return null;
|
|
@@ -7860,6 +7846,9 @@ var CodingAgentManager = class {
|
|
|
7860
7846
|
clearQueue() {
|
|
7861
7847
|
return this.messageQueue.clearQueue();
|
|
7862
7848
|
}
|
|
7849
|
+
updateQueuedMessage(messageId, message) {
|
|
7850
|
+
return this.messageQueue.updateQueuedMessage(messageId, message);
|
|
7851
|
+
}
|
|
7863
7852
|
reorderQueue(messageId, newPosition) {
|
|
7864
7853
|
return this.messageQueue.reorderQueue(messageId, newPosition);
|
|
7865
7854
|
}
|
|
@@ -9811,7 +9800,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
|
|
|
9811
9800
|
var MIN_CODEX_CLI_VERSION = "0.144.6";
|
|
9812
9801
|
var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
|
|
9813
9802
|
var codexCliVersionEnsured = null;
|
|
9814
|
-
var ENGINE_PACKAGE_VERSION = "0.1.
|
|
9803
|
+
var ENGINE_PACKAGE_VERSION = "0.1.479";
|
|
9815
9804
|
var INITIALIZE_METHOD = "initialize";
|
|
9816
9805
|
var INITIALIZED_NOTIFICATION = "initialized";
|
|
9817
9806
|
var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
|
|
@@ -10096,7 +10085,7 @@ var CodexQuotaStatusTracker = class {
|
|
|
10096
10085
|
};
|
|
10097
10086
|
|
|
10098
10087
|
// src/managers/codex-asp/mappers.ts
|
|
10099
|
-
import { existsSync as
|
|
10088
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
10100
10089
|
var localImageCache = /* @__PURE__ */ new Map();
|
|
10101
10090
|
var DEFAULT_MODEL = DEFAULT_CODEX_MODEL;
|
|
10102
10091
|
var THREAD_START_METHOD = "thread/start";
|
|
@@ -10193,7 +10182,7 @@ function transcriptItemsForTurn(turn) {
|
|
|
10193
10182
|
function userImageForLocalPath(path6) {
|
|
10194
10183
|
const cached = localImageCache.get(path6);
|
|
10195
10184
|
if (cached) return cached;
|
|
10196
|
-
if (!
|
|
10185
|
+
if (!existsSync5(path6)) return null;
|
|
10197
10186
|
const image = {
|
|
10198
10187
|
type: "image",
|
|
10199
10188
|
mediaType: inferMediaType(path6),
|
|
@@ -12344,7 +12333,7 @@ ${request.message}` : request.message;
|
|
|
12344
12333
|
};
|
|
12345
12334
|
|
|
12346
12335
|
// src/managers/opencode-manager.ts
|
|
12347
|
-
import { existsSync as
|
|
12336
|
+
import { existsSync as existsSync6 } from "fs";
|
|
12348
12337
|
import { mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
|
|
12349
12338
|
import { delimiter, dirname as dirname6, join as join18 } from "path";
|
|
12350
12339
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -12392,7 +12381,7 @@ var opencodeAuthSchema = z4.record(z4.string(), z4.object({
|
|
|
12392
12381
|
key: z4.string().optional()
|
|
12393
12382
|
}));
|
|
12394
12383
|
async function hasOpenCodeGoCredentials() {
|
|
12395
|
-
if (!
|
|
12384
|
+
if (!existsSync6(OPENCODE_AUTH_PATH2)) return false;
|
|
12396
12385
|
try {
|
|
12397
12386
|
const auth = opencodeAuthSchema.safeParse(JSON.parse(await readFile11(OPENCODE_AUTH_PATH2, "utf8")));
|
|
12398
12387
|
return auth.success && auth.data[OPENCODE_GO_PROVIDER]?.type === "api" && Boolean(auth.data[OPENCODE_GO_PROVIDER]?.key);
|
|
@@ -13836,6 +13825,9 @@ var RelayManager = class {
|
|
|
13836
13825
|
clearQueue() {
|
|
13837
13826
|
return this.inner.clearQueue();
|
|
13838
13827
|
}
|
|
13828
|
+
updateQueuedMessage(messageId, message) {
|
|
13829
|
+
return this.inner.updateQueuedMessage(messageId, message);
|
|
13830
|
+
}
|
|
13839
13831
|
reorderQueue(messageId, newPosition) {
|
|
13840
13832
|
return this.inner.reorderQueue(messageId, newPosition);
|
|
13841
13833
|
}
|
|
@@ -14183,10 +14175,10 @@ function isChatMessageSender(value) {
|
|
|
14183
14175
|
return typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
|
|
14184
14176
|
}
|
|
14185
14177
|
function isCodexAvailable() {
|
|
14186
|
-
return
|
|
14178
|
+
return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
|
|
14187
14179
|
}
|
|
14188
14180
|
function isOpencodeAvailable() {
|
|
14189
|
-
return
|
|
14181
|
+
return existsSync7(OPENCODE_AUTH_PATH3) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
|
|
14190
14182
|
}
|
|
14191
14183
|
function isPiAvailable() {
|
|
14192
14184
|
return Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
|
|
@@ -14569,6 +14561,14 @@ var ChatService = class {
|
|
|
14569
14561
|
queue: chat.provider.getQueue()
|
|
14570
14562
|
};
|
|
14571
14563
|
}
|
|
14564
|
+
updateQueuedMessage(chatId, messageId, message) {
|
|
14565
|
+
const chat = this.requireChat(chatId);
|
|
14566
|
+
const success = chat.provider.updateQueuedMessage(messageId, message);
|
|
14567
|
+
return {
|
|
14568
|
+
success,
|
|
14569
|
+
queue: chat.provider.getQueue()
|
|
14570
|
+
};
|
|
14571
|
+
}
|
|
14572
14572
|
reorderQueue(chatId, messageId, position) {
|
|
14573
14573
|
const chat = this.requireChat(chatId);
|
|
14574
14574
|
const success = chat.provider.reorderQueue(messageId, position);
|
|
@@ -15332,7 +15332,7 @@ import { join as join26, resolve as resolve3 } from "path";
|
|
|
15332
15332
|
// src/services/warm-hooks-service.ts
|
|
15333
15333
|
import { spawn as spawn4 } from "child_process";
|
|
15334
15334
|
import { readFile as readFile17 } from "fs/promises";
|
|
15335
|
-
import { existsSync as
|
|
15335
|
+
import { existsSync as existsSync8 } from "fs";
|
|
15336
15336
|
import { join as join25 } from "path";
|
|
15337
15337
|
|
|
15338
15338
|
// src/services/warm-hook-logs-service.ts
|
|
@@ -15454,7 +15454,7 @@ var warmHookLogsService = new WarmHookLogsService();
|
|
|
15454
15454
|
async function readRepoWarmHook(repoPath) {
|
|
15455
15455
|
for (const filename of REPLICAS_CONFIG_FILENAMES) {
|
|
15456
15456
|
const configPath = join25(repoPath, filename);
|
|
15457
|
-
if (!
|
|
15457
|
+
if (!existsSync8(configPath)) {
|
|
15458
15458
|
continue;
|
|
15459
15459
|
}
|
|
15460
15460
|
try {
|
|
@@ -15715,7 +15715,7 @@ ${combinedScript}` : combinedScript;
|
|
|
15715
15715
|
|
|
15716
15716
|
// src/services/terminal-service.ts
|
|
15717
15717
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
15718
|
-
import { existsSync as
|
|
15718
|
+
import { existsSync as existsSync9 } from "fs";
|
|
15719
15719
|
import { spawn as spawn5 } from "node-pty";
|
|
15720
15720
|
var MAX_REPLAY_CHARS = 1024 * 1024;
|
|
15721
15721
|
var MAX_TERMINAL_SESSIONS = 8;
|
|
@@ -15734,7 +15734,7 @@ var TerminalService = class {
|
|
|
15734
15734
|
});
|
|
15735
15735
|
}
|
|
15736
15736
|
const id = randomUUID6();
|
|
15737
|
-
const shell = process.env.SHELL &&
|
|
15737
|
+
const shell = process.env.SHELL && existsSync9(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
|
|
15738
15738
|
const pty = spawn5(shell, ["-l"], {
|
|
15739
15739
|
name: "xterm-256color",
|
|
15740
15740
|
cols,
|
|
@@ -16164,6 +16164,22 @@ function createV1Routes(deps) {
|
|
|
16164
16164
|
return c.json(jsonError("Failed to remove from queue", error instanceof Error ? error.message : "Unknown error"), 500);
|
|
16165
16165
|
}
|
|
16166
16166
|
});
|
|
16167
|
+
app2.patch("/chats/:chatId/queue/:messageId", async (c) => {
|
|
16168
|
+
try {
|
|
16169
|
+
const body = await c.req.json();
|
|
16170
|
+
const message = body.message?.trim();
|
|
16171
|
+
if (!message) {
|
|
16172
|
+
return c.json(jsonError("Invalid request", "message is required"), 400);
|
|
16173
|
+
}
|
|
16174
|
+
const result = deps.chatService.updateQueuedMessage(c.req.param("chatId"), c.req.param("messageId"), message);
|
|
16175
|
+
return c.json(result);
|
|
16176
|
+
} catch (error) {
|
|
16177
|
+
if (error instanceof ChatNotFoundError) {
|
|
16178
|
+
return c.json(jsonError("Chat not found", error.message), 404);
|
|
16179
|
+
}
|
|
16180
|
+
return c.json(jsonError("Failed to update queued message", error instanceof Error ? error.message : "Unknown error"), 500);
|
|
16181
|
+
}
|
|
16182
|
+
});
|
|
16167
16183
|
app2.post("/chats/:chatId/queue/:messageId/steer", async (c) => {
|
|
16168
16184
|
try {
|
|
16169
16185
|
const result = await deps.chatService.steerFromQueue(c.req.param("chatId"), c.req.param("messageId"));
|
|
@@ -16809,7 +16825,7 @@ async function timeStartupStep(name, fn) {
|
|
|
16809
16825
|
async function waitForInitializationGate() {
|
|
16810
16826
|
if (!ENGINE_ENV.REPLICAS_ENGINE_DEFER_INITIALIZATION) return;
|
|
16811
16827
|
const deadline = Date.now() + ENGINE_INIT_GATE_TIMEOUT_MS;
|
|
16812
|
-
while (!
|
|
16828
|
+
while (!existsSync10(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
|
|
16813
16829
|
if (Date.now() >= deadline) {
|
|
16814
16830
|
throw new Error("Timed out waiting for deferred engine initialization gate");
|
|
16815
16831
|
}
|