replicas-engine 0.1.477 → 0.1.478

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.
Files changed (2) hide show
  1. package/dist/src/index.js +730 -751
  2. 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 existsSync11 } from "fs";
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-v2";
585
+ var E2B_TEMPLATE_NAME = "replicas-sandbox-2026-07-22-v3";
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 gitIdentityConfigCommands(identity) {
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", "--global", "user.name", identity.name],
650
- ["config", "--global", "user.email", identity.email]
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/utils/git-identity.ts
4996
- async function updateGitIdentity(identity, tag) {
4997
- try {
4998
- const [nameArgs, emailArgs] = gitIdentityConfigCommands(identity);
4999
- await execFileAsync("git", nameArgs);
5000
- await execFileAsync("git", emailArgs);
5001
- console.log(`[${tag}] Updated git identity to ${identity.name} <${identity.email}>`);
5002
- } catch (error) {
5003
- console.error(`[${tag}] Failed to update git identity:`, error);
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/managers/github-token-manager.ts
5008
- var GitHubTokenManager = class extends BaseRefreshManager {
5009
- constructor() {
5010
- super("GitHubTokenManager");
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 githubTokenManager = new GitHubTokenManager();
5070
-
5071
- // src/managers/gitlab-token-manager.ts
5072
- import path2 from "path";
5073
- var GitLabTokenManager = class extends BaseRefreshManager {
5074
- constructor() {
5075
- super("GitLabTokenManager");
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
- async doRefresh(_config) {
5078
- const response = await monolithRequest("/v1/engine/gitlab/refresh-token");
5079
- if (!response.ok) {
5080
- if (response.status === 403) {
5081
- await this.clearGitLabCredentials();
5082
- }
5083
- throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
5084
- }
5085
- const data = await response.json();
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
- async clearGitLabCredentials(hosts = []) {
5104
- const credentialsPath = path2.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
5105
- await removeCredentialFileLines(
5106
- credentialsPath,
5107
- (line) => line.startsWith("https://oauth2:") && (hosts.length === 0 || hosts.some((host) => line.endsWith(`@${host}`)))
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
- var gitlabTokenManager = new GitLabTokenManager();
5112
-
5113
- // src/managers/claude-token-manager.ts
5114
- import { promises as fs } from "fs";
5115
- import path3 from "path";
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
- // src/managers/claude-token-manager.ts
5136
- var ClaudeTokenManager = class extends BaseRefreshManager {
5137
- constructor() {
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 data = await response.json();
5160
- await this.applyCredentialsResponse(data);
5161
- console.log(`[ClaudeTokenManager] Credentials refreshed (method=${data.type})`);
5162
- }
5163
- async fetchFreshCredentials(failureReason) {
5164
- const config = this.getRuntimeConfig();
5165
- if (!config) return false;
5166
- try {
5167
- console.log("[ClaudeTokenManager] Fetching fresh credentials from monolith after auth failure...");
5168
- const failedMethod = ENGINE_ENV.REPLICAS_CLAUDE_AUTH_METHOD;
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
- // No invalidation on purpose `git remote set-url` mid-session is rare and
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();
@@ -5817,260 +5405,651 @@ var GitService = class {
5817
5405
  }
5818
5406
  });
5819
5407
  }
5820
- async getUntrackedAsDiff(repoPath) {
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
+ }
5508
+ }
5509
+ async resolveCodeHostProvider(repoPath) {
5510
+ const origin = await this.getOriginInfo(repoPath);
5511
+ return origin.provider === "unknown" ? void 0 : origin.provider;
5512
+ }
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" };
5526
+ try {
5527
+ const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], {
5528
+ cwd: repoPath,
5529
+ encoding: "utf-8",
5530
+ maxBuffer: SUBPROCESS_MAX_BUFFER
5531
+ });
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 };
5538
+ }
5539
+ } catch {
5540
+ info = { provider: "unknown" };
5541
+ }
5542
+ if (info.provider !== "unknown") this.originInfoCache.set(repoPath, info);
5543
+ return info;
5544
+ }
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
+ };
5554
+ }
5555
+ async getGitLabAccessToken(host) {
5556
+ try {
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;
5561
+ try {
5562
+ const parsed = new URL(trimmed);
5563
+ if (parsed.protocol === "https:" && parsed.host.toLowerCase() === host && parsed.password) {
5564
+ return decodeURIComponent(parsed.password);
5565
+ }
5566
+ } catch {
5567
+ }
5568
+ }
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}>`);
5642
+ } catch (error) {
5643
+ console.error(`[${tag}] Failed to update global git identity:`, error);
5644
+ }
5645
+ return;
5646
+ }
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();
5674
+ }
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
+ }
5691
+ }
5692
+ async updateGitCredentials(token) {
5693
+ const credentialsPath = path.join(ENGINE_ENV.HOME_DIR, ".git-credentials");
5821
5694
  try {
5822
- return await this.getUntrackedDiff(repoPath);
5695
+ await upsertCredentialFileLines(credentialsPath, ["github.com"], [
5696
+ `https://x-access-token:${token}@github.com`
5697
+ ]);
5698
+ console.log(`[GitHubTokenManager] Updated ${credentialsPath}`);
5823
5699
  } catch (error) {
5824
- console.error("Error building untracked diff:", error);
5825
- return "";
5700
+ console.error("[GitHubTokenManager] Failed to update git credentials:", error);
5826
5701
  }
5827
5702
  }
5828
- async getPullRequestUrl(repoName, repoPath, currentBranchArg, persistedRepoStateArg) {
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
+ `;
5829
5710
  try {
5830
- const currentBranch = currentBranchArg ?? await getCurrentBranch(repoPath);
5831
- if (!currentBranch) {
5832
- return { status: "not_found" };
5833
- }
5834
- const cachedPr = this.cachedPrByRepo.get(repoName);
5835
- if (cachedPr && cachedPr.currentBranch === currentBranch) {
5836
- return { status: "found", url: cachedPr.prUrl };
5837
- }
5838
- const persistedRepoState = persistedRepoStateArg ?? await loadRepoState(repoName);
5839
- this.cachedPrByRepo.delete(repoName);
5840
- const result = await this.lookupPrOnRemote(repoName, repoPath, currentBranch);
5841
- if (result.status === "found") {
5842
- this.cachedPrByRepo.set(repoName, { prUrl: result.url, currentBranch });
5843
- if (persistedRepoState && !persistedRepoState.prUrls.includes(result.url)) {
5844
- await saveRepoState(
5845
- repoName,
5846
- { prUrls: appendUniqueUrl(persistedRepoState.prUrls, result.url) },
5847
- persistedRepoState
5848
- );
5849
- }
5850
- }
5851
- return result;
5711
+ await writeSecureCredentialFile(hostsPath, content, { ensureParentDir: true });
5712
+ console.log(`[GitHubTokenManager] Updated ${hostsPath}`);
5852
5713
  } catch (error) {
5853
- console.error("Error checking for pull request:", error);
5854
- return { status: "error" };
5714
+ console.error("[GitHubTokenManager] Failed to update gh hosts file:", error);
5855
5715
  }
5856
5716
  }
5857
- async lookupPrOnRemote(repoName, repoPath, branch) {
5858
- const origin = await this.getOriginInfo(repoPath);
5859
- if (origin.provider === "unknown") {
5860
- return { status: "not_found" };
5861
- }
5862
- try {
5863
- const { stdout } = await execFileAsync("git", ["ls-remote", "--heads", "origin", branch], {
5864
- cwd: repoPath,
5865
- encoding: "utf-8",
5866
- maxBuffer: SUBPROCESS_MAX_BUFFER
5867
- });
5868
- if (!stdout.trim()) {
5869
- return { status: "not_found" };
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();
5870
5738
  }
5871
- } catch {
5872
- return { status: "error" };
5739
+ throw new Error(`Token refresh failed: ${response.status} ${await response.text()}`);
5873
5740
  }
5874
- if (origin.provider === "gitlab") {
5875
- return this.lookupGitLabMrOnRemote(repoName, origin.gitLab, branch);
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;
5876
5746
  }
5877
- try {
5878
- const { stdout } = await execFileAsync("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"], {
5879
- cwd: repoPath,
5880
- encoding: "utf-8",
5881
- maxBuffer: SUBPROCESS_MAX_BUFFER
5882
- });
5883
- const prInfo = stdout.trim();
5884
- return prInfo ? { status: "found", url: prInfo } : { status: "not_found" };
5885
- } catch (error) {
5886
- const message = error instanceof Error ? error.message : String(error);
5887
- console.warn(`[GitService] gh pr view ${branch} failed for ${repoName}: ${message}`);
5888
- 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);
5889
5757
  }
5890
5758
  }
5891
- async lookupGitLabMrOnRemote(repoName, remote, branch) {
5892
- const token = await this.getGitLabAccessToken(remote.host);
5893
- if (!token) {
5894
- return { status: "not_found" };
5895
- }
5896
- const url = `${remote.instanceUrl}/api/v4/projects/${encodeURIComponent(remote.projectPath)}/merge_requests?source_branch=${encodeURIComponent(branch)}&order_by=updated_at`;
5897
- try {
5898
- const response = await fetch(url, {
5899
- headers: { Authorization: `Bearer ${token}` },
5900
- signal: AbortSignal.timeout(1e4)
5901
- });
5902
- if (!response.ok) {
5903
- console.warn(`[GitService] GitLab MR lookup failed for ${repoName}: ${response.status}`);
5904
- return response.status === 404 ? { status: "not_found" } : { status: "error" };
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];
5905
5786
  }
5906
- const data = await response.json();
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" };
5915
- } catch (error) {
5916
- const message = error instanceof Error ? error.message : String(error);
5917
- console.warn(`[GitService] GitLab MR lookup failed for ${repoName}: ${message}`);
5918
- return { status: "error" };
5919
5787
  }
5920
5788
  }
5921
- async resolveCodeHostProvider(repoPath) {
5922
- const origin = await this.getOriginInfo(repoPath);
5923
- return origin.provider === "unknown" ? void 0 : origin.provider;
5789
+ }
5790
+
5791
+ // src/managers/claude-token-manager.ts
5792
+ var ClaudeTokenManager = class extends BaseRefreshManager {
5793
+ constructor() {
5794
+ super("ClaudeTokenManager");
5924
5795
  }
5925
- async getOriginInfo(repoPath) {
5926
- const cached = this.originInfoCache.get(repoPath);
5927
- if (cached !== void 0) {
5928
- return cached;
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}`;
5929
5800
  }
5930
- let info = { provider: "unknown" };
5801
+ return null;
5802
+ }
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}`);
5814
+ }
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;
5931
5822
  try {
5932
- const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], {
5933
- cwd: repoPath,
5934
- encoding: "utf-8",
5935
- maxBuffer: SUBPROCESS_MAX_BUFFER
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
+ });
5833
+ }
5834
+ return true;
5835
+ } catch (error) {
5836
+ console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
5837
+ return false;
5838
+ }
5839
+ }
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
- const originUrl = stdout.trim();
5938
- if (isGitHubUrl(originUrl)) {
5939
- info = { provider: "github" };
5940
- } else {
5941
- const gitLabRemote = this.parseGitLabRemote(originUrl);
5942
- if (gitLabRemote) info = { provider: "gitlab", gitLab: gitLabRemote };
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
- } catch {
5945
- info = { provider: "unknown" };
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
- parseGitLabRemote(remoteUrl) {
5951
- const sshUrl = remoteUrl.trim().replace(/\/+$/, "").replace(/\.git$/, "");
5884
+ async removeOauthCredentialsFile() {
5885
+ const credentialsPath = path3.join(ENGINE_ENV.HOME_DIR, ".claude", ".credentials.json");
5952
5886
  try {
5953
- const parsed = new URL(sshUrl);
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
- const scpLike = sshUrl.match(/^git@([^:/]+):(?:(?:\d+)\/)?(.+)$/);
5967
- if (scpLike) {
5968
- const [, host, projectPath] = scpLike;
5969
- if (!host || !projectPath) return null;
5970
- const decodedProjectPath = decodePathSegments(projectPath.split("/").filter(Boolean)).join("/");
5971
- return {
5972
- host: host.toLowerCase(),
5973
- instanceUrl: `https://${host}`,
5974
- projectPath: decodedProjectPath
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
- const normalized = normalizeRepositoryUrl(remoteUrl);
5978
- if (!normalized) return null;
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 getGitLabAccessToken(host) {
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
- const credentials = await readFile3(join5(ENGINE_ENV.HOME_DIR, ".git-credentials"), "utf-8");
5996
- for (const line of credentials.split("\n")) {
5997
- const trimmed = line.trim();
5998
- if (!trimmed) continue;
5999
- try {
6000
- const parsed = new URL(trimmed);
6001
- if (parsed.protocol === "https:" && parsed.host.toLowerCase() === host && parsed.password) {
6002
- return decodeURIComponent(parsed.password);
6003
- }
6004
- } catch {
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
- } catch {
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 resolveDefaultBranch(repoPath) {
6012
- const cached = this.defaultBranchCache.get(repoPath);
6013
- if (cached) {
6014
- return cached;
5947
+ async applyCredentialsResponse(response) {
5948
+ if (response.type === "oauth") {
5949
+ await this.writeOauthCredentialsFile(response);
5950
+ } else {
5951
+ await this.removeOauthCredentialsFile();
6015
5952
  }
6016
- const fromSymbolicRef = await this.resolveDefaultBranchFromSymbolicRef(repoPath);
6017
- if (fromSymbolicRef) {
6018
- this.defaultBranchCache.set(repoPath, fromSymbolicRef);
6019
- return fromSymbolicRef;
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 resolveDefaultBranchFromSymbolicRef(repoPath) {
5986
+ async removeOauthCredentialsFile() {
5987
+ const authPath = path4.join(ENGINE_ENV.HOME_DIR, ".codex", "auth.json");
6024
5988
  try {
6025
- const output = await runGitCommand(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoPath);
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
- async refreshRepoMetadata(repo, currentBranch, startHooksCompleted, persistedState, observedBranches, includeDiffs = false) {
6033
- const prResult = await this.getPullRequestUrl(repo.name, repo.path, currentBranch, persistedState);
6034
- let prUrls = persistedState?.prUrls ?? [];
6035
- if (prResult.status === "found") {
6036
- prUrls = appendUniqueUrl(prUrls, prResult.url);
6037
- }
6038
- if (observedBranches) {
6039
- for (const branch of observedBranches) {
6040
- if (branch === currentBranch) continue;
6041
- const branchResult = await this.lookupPrOnRemote(repo.name, repo.path, branch);
6042
- if (branchResult.status === "found") {
6043
- prUrls = appendUniqueUrl(prUrls, branchResult.url);
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
- pathExists(path6) {
6063
- return existsSync2(path6);
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
- async safeStat(path6) {
6066
- try {
6067
- return await stat(path6);
6068
- } catch {
6069
- return null;
6070
- }
6021
+ getNextRefreshDelayMs() {
6022
+ return this.nextRefreshDelayMs;
6071
6023
  }
6072
6024
  };
6073
- var gitService = new GitService();
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 existsSync4 } from "fs";
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 existsSync3 } from "fs";
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 (existsSync3(CLAUDE_CREDENTIALS_PATH)) {
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 (existsSync3(CODEX_AUTH_PATH)) {
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 existsSync3(OPENCODE_AUTH_PATH) || ENGINE_ENV.OPENROUTER_API_KEY ? "api_key" : "none";
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 (!existsSync3(DETAILS_FILE)) {
6291
+ if (!existsSync2(DETAILS_FILE)) {
6313
6292
  return createDefaultDetails();
6314
6293
  }
6315
6294
  const raw = await readFile4(DETAILS_FILE, "utf-8");
@@ -6341,7 +6320,7 @@ var EnvironmentDetailsService = class {
6341
6320
  details.opencodeAuthMethod = detectOpencodeAuthMethod();
6342
6321
  details.piAuthMethod = detectPiAuthMethod();
6343
6322
  details.gitIdentityConfigured = gitIdentityConfigured;
6344
- const ghConfigured = existsSync3(GH_HOSTS_PATH);
6323
+ const ghConfigured = existsSync2(GH_HOSTS_PATH);
6345
6324
  details.githubAccessConfigured = ghConfigured;
6346
6325
  details.githubCredentialsConfigured = ghConfigured;
6347
6326
  details.gitlabAccessConfigured = gitlabAccessConfigured;
@@ -6540,7 +6519,7 @@ If your task depends on setup being complete, check the log file before proceedi
6540
6519
  async function readReplicasConfigFromDir(dirPath) {
6541
6520
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
6542
6521
  const configPath = join9(dirPath, filename);
6543
- if (!existsSync4(configPath)) {
6522
+ if (!existsSync3(configPath)) {
6544
6523
  continue;
6545
6524
  }
6546
6525
  const data = await readFile6(configPath, "utf-8");
@@ -6975,14 +6954,14 @@ var eventService = new EventService();
6975
6954
 
6976
6955
  // src/services/preview-service.ts
6977
6956
  import { mkdir as mkdir8, readFile as readFile7 } from "fs/promises";
6978
- import { existsSync as existsSync5 } from "fs";
6957
+ import { existsSync as existsSync4 } from "fs";
6979
6958
  import { randomUUID as randomUUID2 } from "crypto";
6980
6959
  import { homedir as homedir9 } from "os";
6981
6960
  import { dirname as dirname2, join as join11 } from "path";
6982
6961
  var PREVIEW_PORTS_FILE = join11(homedir9(), ".replicas", "preview-ports.json");
6983
6962
  async function readPreviewsFile() {
6984
6963
  try {
6985
- if (!existsSync5(PREVIEW_PORTS_FILE)) {
6964
+ if (!existsSync4(PREVIEW_PORTS_FILE)) {
6986
6965
  return { previews: [] };
6987
6966
  }
6988
6967
  const raw = await readFile7(PREVIEW_PORTS_FILE, "utf-8");
@@ -7089,7 +7068,7 @@ async function registerDesktopPreview() {
7089
7068
  }
7090
7069
 
7091
7070
  // src/services/chat/chat-service.ts
7092
- import { existsSync as existsSync8 } from "fs";
7071
+ import { existsSync as existsSync7 } from "fs";
7093
7072
  import { appendFile as appendFile3, copyFile, mkdir as mkdir14, readFile as readFile14, rename as rename2, rm as rm2 } from "fs/promises";
7094
7073
  import { homedir as homedir15 } from "os";
7095
7074
  import { join as join22 } from "path";
@@ -9811,7 +9790,7 @@ var DEFAULT_CODEX_ARGS = ["app-server", "--listen", "stdio://"];
9811
9790
  var MIN_CODEX_CLI_VERSION = "0.144.6";
9812
9791
  var CODEX_UPGRADE_TIMEOUT_MS = 12e4;
9813
9792
  var codexCliVersionEnsured = null;
9814
- var ENGINE_PACKAGE_VERSION = "0.1.477";
9793
+ var ENGINE_PACKAGE_VERSION = "0.1.478";
9815
9794
  var INITIALIZE_METHOD = "initialize";
9816
9795
  var INITIALIZED_NOTIFICATION = "initialized";
9817
9796
  var ACCOUNT_LOGIN_START_METHOD = "account/login/start";
@@ -10096,7 +10075,7 @@ var CodexQuotaStatusTracker = class {
10096
10075
  };
10097
10076
 
10098
10077
  // src/managers/codex-asp/mappers.ts
10099
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
10078
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
10100
10079
  var localImageCache = /* @__PURE__ */ new Map();
10101
10080
  var DEFAULT_MODEL = DEFAULT_CODEX_MODEL;
10102
10081
  var THREAD_START_METHOD = "thread/start";
@@ -10193,7 +10172,7 @@ function transcriptItemsForTurn(turn) {
10193
10172
  function userImageForLocalPath(path6) {
10194
10173
  const cached = localImageCache.get(path6);
10195
10174
  if (cached) return cached;
10196
- if (!existsSync6(path6)) return null;
10175
+ if (!existsSync5(path6)) return null;
10197
10176
  const image = {
10198
10177
  type: "image",
10199
10178
  mediaType: inferMediaType(path6),
@@ -12344,7 +12323,7 @@ ${request.message}` : request.message;
12344
12323
  };
12345
12324
 
12346
12325
  // src/managers/opencode-manager.ts
12347
- import { existsSync as existsSync7 } from "fs";
12326
+ import { existsSync as existsSync6 } from "fs";
12348
12327
  import { mkdir as mkdir12, readFile as readFile11 } from "fs/promises";
12349
12328
  import { delimiter, dirname as dirname6, join as join18 } from "path";
12350
12329
  import { randomBytes as randomBytes2 } from "crypto";
@@ -12392,7 +12371,7 @@ var opencodeAuthSchema = z4.record(z4.string(), z4.object({
12392
12371
  key: z4.string().optional()
12393
12372
  }));
12394
12373
  async function hasOpenCodeGoCredentials() {
12395
- if (!existsSync7(OPENCODE_AUTH_PATH2)) return false;
12374
+ if (!existsSync6(OPENCODE_AUTH_PATH2)) return false;
12396
12375
  try {
12397
12376
  const auth = opencodeAuthSchema.safeParse(JSON.parse(await readFile11(OPENCODE_AUTH_PATH2, "utf8")));
12398
12377
  return auth.success && auth.data[OPENCODE_GO_PROVIDER]?.type === "api" && Boolean(auth.data[OPENCODE_GO_PROVIDER]?.key);
@@ -14183,10 +14162,10 @@ function isChatMessageSender(value) {
14183
14162
  return typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
14184
14163
  }
14185
14164
  function isCodexAvailable() {
14186
- return existsSync8(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
14165
+ return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
14187
14166
  }
14188
14167
  function isOpencodeAvailable() {
14189
- return existsSync8(OPENCODE_AUTH_PATH3) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
14168
+ return existsSync7(OPENCODE_AUTH_PATH3) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
14190
14169
  }
14191
14170
  function isPiAvailable() {
14192
14171
  return Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
@@ -15332,7 +15311,7 @@ import { join as join26, resolve as resolve3 } from "path";
15332
15311
  // src/services/warm-hooks-service.ts
15333
15312
  import { spawn as spawn4 } from "child_process";
15334
15313
  import { readFile as readFile17 } from "fs/promises";
15335
- import { existsSync as existsSync9 } from "fs";
15314
+ import { existsSync as existsSync8 } from "fs";
15336
15315
  import { join as join25 } from "path";
15337
15316
 
15338
15317
  // src/services/warm-hook-logs-service.ts
@@ -15454,7 +15433,7 @@ var warmHookLogsService = new WarmHookLogsService();
15454
15433
  async function readRepoWarmHook(repoPath) {
15455
15434
  for (const filename of REPLICAS_CONFIG_FILENAMES) {
15456
15435
  const configPath = join25(repoPath, filename);
15457
- if (!existsSync9(configPath)) {
15436
+ if (!existsSync8(configPath)) {
15458
15437
  continue;
15459
15438
  }
15460
15439
  try {
@@ -15715,7 +15694,7 @@ ${combinedScript}` : combinedScript;
15715
15694
 
15716
15695
  // src/services/terminal-service.ts
15717
15696
  import { randomUUID as randomUUID6 } from "crypto";
15718
- import { existsSync as existsSync10 } from "fs";
15697
+ import { existsSync as existsSync9 } from "fs";
15719
15698
  import { spawn as spawn5 } from "node-pty";
15720
15699
  var MAX_REPLAY_CHARS = 1024 * 1024;
15721
15700
  var MAX_TERMINAL_SESSIONS = 8;
@@ -15734,7 +15713,7 @@ var TerminalService = class {
15734
15713
  });
15735
15714
  }
15736
15715
  const id = randomUUID6();
15737
- const shell = process.env.SHELL && existsSync10(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
15716
+ const shell = process.env.SHELL && existsSync9(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
15738
15717
  const pty = spawn5(shell, ["-l"], {
15739
15718
  name: "xterm-256color",
15740
15719
  cols,
@@ -16809,7 +16788,7 @@ async function timeStartupStep(name, fn) {
16809
16788
  async function waitForInitializationGate() {
16810
16789
  if (!ENGINE_ENV.REPLICAS_ENGINE_DEFER_INITIALIZATION) return;
16811
16790
  const deadline = Date.now() + ENGINE_INIT_GATE_TIMEOUT_MS;
16812
- while (!existsSync11(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
16791
+ while (!existsSync10(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
16813
16792
  if (Date.now() >= deadline) {
16814
16793
  throw new Error("Timed out waiting for deferred engine initialization gate");
16815
16794
  }