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.
Files changed (2) hide show
  1. package/dist/src/index.js +849 -833
  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-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 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();
@@ -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 listUntrackedPaths(repoPath) {
5722
- const { stdout } = await execFileAsync("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
5723
- cwd: repoPath,
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 getUntrackedDiff(repoPath) {
5730
- const paths = await this.listUntrackedPaths(repoPath);
5731
- if (paths.length === 0) return "";
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", ["add", "--intent-to-add", "--", ...paths], {
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
- return await this.readUntrackedDiff(repoPath, paths);
5738
- } finally {
5739
- try {
5740
- await execFileAsync("git", ["reset", "--", ...paths], {
5741
- cwd: repoPath,
5742
- maxBuffer: SUBPROCESS_MAX_BUFFER
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
- readUntrackedDiff(repoPath, paths) {
5749
- return new Promise((resolve4) => {
5750
- const child = spawn("git", ["diff", "--", ...paths], {
5751
- cwd: repoPath,
5752
- stdio: ["ignore", "pipe", "pipe"]
5753
- });
5754
- const chunks = [];
5755
- let total = 0;
5756
- let truncated = false;
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 countUntrackedAddedLines(repoPath) {
5555
+ async getGitLabAccessToken(host) {
5781
5556
  try {
5782
- const paths = await this.listUntrackedPaths(repoPath);
5783
- let total = 0;
5784
- for (const path6 of paths) {
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 contents = await readFile3(join5(repoPath, path6));
5787
- if (contents.length === 0 || contents.includes(0)) {
5788
- continue;
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
- return total;
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("Error counting untracked added lines:", error);
5801
- return 0;
5643
+ console.error(`[${tag}] Failed to update global git identity:`, error);
5802
5644
  }
5645
+ return;
5803
5646
  }
5804
- getFullGitDiff(repoPath, defaultBranch) {
5805
- return this.fullDiffInFlight.run(repoPath, async () => {
5806
- try {
5807
- const diffBase = await resolveDiffBase(repoPath, defaultBranch);
5808
- const { stdout: trackedDiff } = await execFileAsync("git", ["diff", diffBase, "-M", "-C"], {
5809
- cwd: repoPath,
5810
- encoding: "utf-8",
5811
- maxBuffer: FULL_DIFF_MAX_BUFFER
5812
- });
5813
- const untrackedDiff = await this.getUntrackedAsDiff(repoPath);
5814
- return trackedDiff + untrackedDiff;
5815
- } catch {
5816
- return null;
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 getUntrackedAsDiff(repoPath) {
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
- return await this.getUntrackedDiff(repoPath);
5711
+ await writeSecureCredentialFile(hostsPath, content, { ensureParentDir: true });
5712
+ console.log(`[GitHubTokenManager] Updated ${hostsPath}`);
5823
5713
  } catch (error) {
5824
- console.error("Error building untracked diff:", error);
5825
- return "";
5714
+ console.error("[GitHubTokenManager] Failed to update gh hosts file:", error);
5826
5715
  }
5827
5716
  }
5828
- async getPullRequestUrl(repoName, repoPath, currentBranchArg, persistedRepoStateArg) {
5829
- 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
- }
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
- return result;
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
- async lookupPrOnRemote(repoName, repoPath, branch) {
5858
- const origin = await this.getOriginInfo(repoPath);
5859
- if (origin.provider === "unknown") {
5860
- return { status: "not_found" };
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
- 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" };
5870
- }
5871
- } catch {
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
- if (origin.provider === "gitlab") {
5875
- return this.lookupGitLabMrOnRemote(repoName, origin.gitLab, branch);
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
- 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" };
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 lookupGitLabMrOnRemote(repoName, remote, branch) {
5892
- const token = await this.getGitLabAccessToken(remote.host);
5893
- if (!token) {
5894
- return { status: "not_found" };
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 url = `${remote.instanceUrl}/api/v4/projects/${encodeURIComponent(remote.projectPath)}/merge_requests?source_branch=${encodeURIComponent(branch)}&order_by=updated_at`;
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
- 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" };
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
- 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" };
5834
+ return true;
5915
5835
  } 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" };
5836
+ console.error("[ClaudeTokenManager] Failed to fetch fresh credentials:", error);
5837
+ return false;
5919
5838
  }
5920
5839
  }
5921
- async resolveCodeHostProvider(repoPath) {
5922
- const origin = await this.getOriginInfo(repoPath);
5923
- return origin.provider === "unknown" ? void 0 : origin.provider;
5924
- }
5925
- async getOriginInfo(repoPath) {
5926
- const cached = this.originInfoCache.get(repoPath);
5927
- if (cached !== void 0) {
5928
- return cached;
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
- 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");
@@ -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 = existsSync3(GH_HOSTS_PATH);
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 (!existsSync4(configPath)) {
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 existsSync5 } from "fs";
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 (!existsSync5(PREVIEW_PORTS_FILE)) {
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 existsSync8 } from "fs";
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.477";
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 existsSync6, readFileSync as readFileSync3 } from "fs";
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 (!existsSync6(path6)) return null;
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 existsSync7 } from "fs";
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 (!existsSync7(OPENCODE_AUTH_PATH2)) return false;
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 existsSync8(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
14178
+ return existsSync7(CODEX_AUTH_PATH2) || Boolean(ENGINE_ENV.OPENAI_API_KEY);
14187
14179
  }
14188
14180
  function isOpencodeAvailable() {
14189
- return existsSync8(OPENCODE_AUTH_PATH3) || Boolean(ENGINE_ENV.OPENROUTER_API_KEY);
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 existsSync9 } from "fs";
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 (!existsSync9(configPath)) {
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 existsSync10 } from "fs";
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 && existsSync10(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
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 (!existsSync11(SANDBOX_PATHS.ENGINE_INIT_GATE)) {
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
  }