@serviceme/devtools-core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ __export(src_exports, {
59
59
  EnvironmentInspector: () => EnvironmentInspector,
60
60
  FsIdentityFileBackend: () => FsIdentityFileBackend,
61
61
  FsToolboxFileBackend: () => FsToolboxFileBackend,
62
+ GIT_PROXY_PATH_SUFFIX: () => GIT_PROXY_PATH_SUFFIX,
62
63
  GitClient: () => GitClient,
63
64
  GitError: () => GitError,
64
65
  GitHubAuthProvider: () => GitHubAuthProvider,
@@ -125,6 +126,7 @@ __export(src_exports, {
125
126
  bootstrapPhase5Placeholders: () => bootstrapPhase5Placeholders,
126
127
  buildDefaultReposFile: () => buildDefaultReposFile,
127
128
  buildGitHubLocalEmail: () => buildGitHubLocalEmail,
129
+ buildGitProxyBase: () => buildGitProxyBase,
128
130
  buildSignedHeaders: () => buildSignedHeaders,
129
131
  copilotDoctor: () => copilotDoctor,
130
132
  copilotPrompt: () => copilotPrompt,
@@ -652,6 +654,50 @@ var AccessControl = class {
652
654
  }
653
655
  };
654
656
 
657
+ // src/logger.ts
658
+ var noopLogger = {
659
+ debug() {
660
+ },
661
+ info() {
662
+ },
663
+ warn() {
664
+ },
665
+ error() {
666
+ }
667
+ };
668
+ function formatArgs(args) {
669
+ return args.map((arg) => {
670
+ if (typeof arg === "string") {
671
+ return arg;
672
+ }
673
+ try {
674
+ return JSON.stringify(arg);
675
+ } catch {
676
+ return String(arg);
677
+ }
678
+ }).join(" ");
679
+ }
680
+ function createConsoleLogger(prefix = "serviceme") {
681
+ return {
682
+ debug(message, ...args) {
683
+ process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
684
+ `);
685
+ },
686
+ info(message, ...args) {
687
+ process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
688
+ `);
689
+ },
690
+ warn(message, ...args) {
691
+ process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}
692
+ `);
693
+ },
694
+ error(message, ...args) {
695
+ process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
696
+ `);
697
+ }
698
+ };
699
+ }
700
+
655
701
  // src/auth/AuthStateManager.ts
656
702
  var import_node_events = require("events");
657
703
  var AuthStateManager = class {
@@ -814,6 +860,7 @@ var AuthCore = class {
814
860
  this.state = opts.stateManager ?? new AuthStateManager();
815
861
  this.tokenStore = opts.tokenStore;
816
862
  this.accessControl = opts.accessControl;
863
+ this.logger = opts.logger ?? noopLogger;
817
864
  }
818
865
  /** Snapshot of every account, the active provider, and the last error. */
819
866
  status() {
@@ -835,16 +882,28 @@ var AuthCore = class {
835
882
  */
836
883
  async login(provider, ui, shouldContinue) {
837
884
  const providerImpl = this.registry.get(provider);
885
+ this.logger.info("[AuthCore] Starting device-flow login", { provider });
838
886
  try {
839
887
  const initial = await providerImpl.requestDeviceFlow();
840
888
  await ui(initial);
889
+ if (!initial.deviceCode) {
890
+ this.logger.error("[AuthCore] Provider did not return deviceCode", provider);
891
+ throw new Error("Auth provider did not return deviceCode for device flow completion");
892
+ }
841
893
  const session = await providerImpl.completeDeviceFlow(
842
- initial.userCode ? initial.deviceCode ?? "" : "",
843
- shouldContinue
894
+ initial.deviceCode,
895
+ shouldContinue,
896
+ initial.pollIntervalMs
844
897
  );
845
- return await this.persistSession(providerImpl, session);
898
+ const result = await this.persistSession(providerImpl, session);
899
+ this.logger.info("[AuthCore] Device-flow login completed", { provider });
900
+ return result;
846
901
  } catch (err) {
847
- this.state.recordError(err instanceof Error ? err.message : String(err));
902
+ const message = err instanceof Error ? err.message : String(err);
903
+ this.logger.error("[AuthCore] Device-flow login failed", message, {
904
+ provider
905
+ });
906
+ this.state.recordError(message);
848
907
  throw err;
849
908
  }
850
909
  }
@@ -869,7 +928,10 @@ var AuthCore = class {
869
928
  if (!provider) return null;
870
929
  const account = this.state.getActiveAccount();
871
930
  if (!account) return null;
872
- const envelope = await this.tokenStore.get({ provider, accountId: account.id });
931
+ const envelope = await this.tokenStore.get({
932
+ provider,
933
+ accountId: account.id
934
+ });
873
935
  if (!envelope) return null;
874
936
  return { provider, account, token: envelope.token };
875
937
  }
@@ -879,9 +941,15 @@ var AuthCore = class {
879
941
  * every account.
880
942
  */
881
943
  async logout(provider) {
944
+ this.logger.info("[AuthCore] Logout requested", {
945
+ provider: provider ?? "all"
946
+ });
882
947
  if (!provider) {
883
948
  for (const account of this.state.listAccounts()) {
884
- await this.tokenStore.delete({ provider: account.provider, accountId: account.id });
949
+ await this.tokenStore.delete({
950
+ provider: account.provider,
951
+ accountId: account.id
952
+ });
885
953
  this.state.removeAccount(account.provider, account.id);
886
954
  }
887
955
  this.state.clearAll();
@@ -956,6 +1024,10 @@ var AuthCore = class {
956
1024
  avatarUrl: meta.avatarUrl,
957
1025
  expiresAt: meta.expiresAt ?? expiresAt
958
1026
  };
1027
+ this.logger.debug("[AuthCore] Persisting session", {
1028
+ provider: meta.provider,
1029
+ accountId: meta.id
1030
+ });
959
1031
  await this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {
960
1032
  expiresAt
961
1033
  });
@@ -1058,11 +1130,13 @@ var GitHubAuthProvider = class {
1058
1130
  userUrl: config.userUrl ?? DEFAULT_USER_URL,
1059
1131
  scope: config.scope ?? DEFAULT_SCOPE,
1060
1132
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
1061
- maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
1133
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 6e4,
1062
1134
  maxWaitMs: config.maxWaitMs,
1063
1135
  fetchImpl: config.fetchImpl ?? fetch,
1064
1136
  deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
1065
1137
  };
1138
+ this.logger = config.logger ?? noopLogger;
1139
+ this.sleepImpl = config.sleepImpl ?? sleep;
1066
1140
  }
1067
1141
  async requestDeviceFlow(opts) {
1068
1142
  const body = JSON.stringify({
@@ -1071,6 +1145,10 @@ var GitHubAuthProvider = class {
1071
1145
  });
1072
1146
  let lastNetworkError;
1073
1147
  for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
1148
+ this.logger.debug("[GitHubAuthProvider] Requesting device code", {
1149
+ attempt,
1150
+ maxAttempts: DEVICE_CODE_MAX_ATTEMPTS
1151
+ });
1074
1152
  let resp;
1075
1153
  try {
1076
1154
  resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
@@ -1084,45 +1162,99 @@ var GitHubAuthProvider = class {
1084
1162
  });
1085
1163
  } catch (error) {
1086
1164
  if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
1165
+ this.logger.error(
1166
+ "[GitHubAuthProvider] Device-code request failed (non-retryable)",
1167
+ error instanceof Error ? error.message : String(error),
1168
+ { attempt }
1169
+ );
1087
1170
  throw error;
1088
1171
  }
1172
+ const delayMs = this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1);
1173
+ this.logger.warn(
1174
+ "[GitHubAuthProvider] Transient network error requesting device code, retrying",
1175
+ {
1176
+ attempt,
1177
+ delayMs,
1178
+ error: error instanceof Error ? error.message : String(error)
1179
+ }
1180
+ );
1089
1181
  lastNetworkError = error;
1090
- await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
1182
+ await this.sleepImpl(delayMs);
1091
1183
  continue;
1092
1184
  }
1093
1185
  if (!resp.ok) {
1186
+ this.logger.error(
1187
+ "[GitHubAuthProvider] Device-code request rejected by GitHub",
1188
+ `HTTP ${resp.status}`
1189
+ );
1094
1190
  throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
1095
1191
  }
1096
1192
  const data = await resp.json();
1097
1193
  if (!data.device_code || !data.user_code || !data.verification_uri) {
1194
+ this.logger.error(
1195
+ "[GitHubAuthProvider] Device-code response missing required fields",
1196
+ JSON.stringify(Object.keys(data))
1197
+ );
1098
1198
  throw new Error("GitHub device-code response missing required fields");
1099
1199
  }
1200
+ this.logger.info("[GitHubAuthProvider] Device code obtained", {
1201
+ userCode: data.user_code,
1202
+ verificationUri: data.verification_uri,
1203
+ expiresInSec: data.expires_in,
1204
+ pollIntervalSec: data.interval
1205
+ });
1100
1206
  return {
1101
1207
  provider: this.providerId,
1208
+ deviceCode: data.device_code,
1102
1209
  userCode: data.user_code,
1103
1210
  verificationUrl: data.verification_uri,
1104
1211
  expiresAt: Date.now() + data.expires_in * 1e3,
1212
+ pollIntervalMs: data.interval * 1e3,
1105
1213
  message: `Open ${data.verification_uri} and enter ${data.user_code}`
1106
1214
  };
1107
1215
  }
1108
1216
  throw lastNetworkError ?? new Error("GitHub device-code request failed");
1109
1217
  }
1110
- async completeDeviceFlow(deviceCode, shouldContinue) {
1218
+ async completeDeviceFlow(deviceCode, shouldContinue, initialPollIntervalMs) {
1111
1219
  const start = Date.now();
1112
- let pollIntervalMs = this.cfg.minPollIntervalMs;
1220
+ let pollIntervalMs = Math.max(this.cfg.minPollIntervalMs, initialPollIntervalMs ?? 0);
1113
1221
  let consecutiveSlowDown = 0;
1222
+ let pollCount = 0;
1223
+ this.logger.info("[GitHubAuthProvider] Starting device-flow polling", {
1224
+ initialPollIntervalMs: pollIntervalMs,
1225
+ maxWaitMs: this.cfg.maxWaitMs
1226
+ });
1114
1227
  while (true) {
1115
1228
  if (shouldContinue && !shouldContinue()) {
1229
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
1230
+ elapsedMs: Date.now() - start,
1231
+ pollCount
1232
+ });
1116
1233
  throw new Error("GitHub device flow cancelled by caller");
1117
1234
  }
1118
1235
  const elapsed = Date.now() - start;
1119
1236
  if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
1237
+ this.logger.warn("[GitHubAuthProvider] Device flow exceeded max wait time", {
1238
+ elapsedMs: elapsed,
1239
+ maxWaitMs: this.cfg.maxWaitMs,
1240
+ pollCount
1241
+ });
1120
1242
  throw new Error("GitHub device flow exceeded max wait time");
1121
1243
  }
1122
- await sleep(pollIntervalMs);
1244
+ await this.sleepImpl(pollIntervalMs);
1123
1245
  if (shouldContinue && !shouldContinue()) {
1246
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
1247
+ elapsedMs: Date.now() - start,
1248
+ pollCount
1249
+ });
1124
1250
  throw new Error("GitHub device flow cancelled by caller");
1125
1251
  }
1252
+ pollCount++;
1253
+ this.logger.debug("[GitHubAuthProvider] Polling for authorization", {
1254
+ pollCount,
1255
+ elapsedMs: Date.now() - start,
1256
+ pollIntervalMs
1257
+ });
1126
1258
  let resp;
1127
1259
  try {
1128
1260
  resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
@@ -1140,19 +1272,59 @@ var GitHubAuthProvider = class {
1140
1272
  });
1141
1273
  } catch (error) {
1142
1274
  if (!isTransientNetworkError(error)) {
1275
+ this.logger.error(
1276
+ "[GitHubAuthProvider] Non-retryable error while polling token endpoint",
1277
+ error instanceof Error ? error.message : String(error),
1278
+ { pollCount }
1279
+ );
1143
1280
  throw error;
1144
1281
  }
1145
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1282
+ const nextPollIntervalMs = Math.min(
1283
+ Math.round(pollIntervalMs * 1.5),
1284
+ this.cfg.maxPollIntervalMs
1285
+ );
1286
+ this.logger.warn(
1287
+ "[GitHubAuthProvider] Transient network error while polling, backing off",
1288
+ {
1289
+ pollCount,
1290
+ error: error instanceof Error ? error.message : String(error),
1291
+ previousPollIntervalMs: pollIntervalMs,
1292
+ nextPollIntervalMs
1293
+ }
1294
+ );
1295
+ pollIntervalMs = nextPollIntervalMs;
1146
1296
  continue;
1147
1297
  }
1148
1298
  if (!resp.ok) {
1149
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1299
+ const nextPollIntervalMs = Math.min(
1300
+ Math.round(pollIntervalMs * 1.5),
1301
+ this.cfg.maxPollIntervalMs
1302
+ );
1303
+ this.logger.warn(
1304
+ "[GitHubAuthProvider] Token endpoint returned non-OK status, backing off",
1305
+ {
1306
+ pollCount,
1307
+ status: resp.status,
1308
+ previousPollIntervalMs: pollIntervalMs,
1309
+ nextPollIntervalMs
1310
+ }
1311
+ );
1312
+ pollIntervalMs = nextPollIntervalMs;
1150
1313
  continue;
1151
1314
  }
1152
1315
  const data = await resp.json();
1153
1316
  if (data.access_token) {
1317
+ this.logger.info("[GitHubAuthProvider] Authorization granted, fetching user profile", {
1318
+ pollCount,
1319
+ elapsedMs: Date.now() - start
1320
+ });
1154
1321
  const rawUser = await this.fetchGitHubUser(data.access_token);
1155
1322
  const email = await this.resolveEmail(data.access_token, rawUser);
1323
+ this.logger.info("[GitHubAuthProvider] Device-flow login completed", {
1324
+ pollCount,
1325
+ elapsedMs: Date.now() - start,
1326
+ login: rawUser.login
1327
+ });
1156
1328
  return {
1157
1329
  token: data.access_token,
1158
1330
  refreshToken: data.refresh_token,
@@ -1167,22 +1339,42 @@ var GitHubAuthProvider = class {
1167
1339
  };
1168
1340
  }
1169
1341
  if (data.error === "authorization_pending") {
1342
+ this.logger.debug("[GitHubAuthProvider] Authorization still pending", {
1343
+ pollCount,
1344
+ elapsedMs: Date.now() - start
1345
+ });
1170
1346
  continue;
1171
1347
  }
1172
1348
  if (data.error === "slow_down") {
1173
1349
  consecutiveSlowDown++;
1174
- pollIntervalMs = Math.min(
1175
- Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
1176
- this.cfg.maxPollIntervalMs
1177
- );
1350
+ const nextPollIntervalMs = Math.min(pollIntervalMs + 5e3, this.cfg.maxPollIntervalMs);
1351
+ this.logger.warn("[GitHubAuthProvider] GitHub requested slower polling (slow_down)", {
1352
+ pollCount,
1353
+ consecutiveSlowDown,
1354
+ previousPollIntervalMs: pollIntervalMs,
1355
+ nextPollIntervalMs
1356
+ });
1357
+ pollIntervalMs = nextPollIntervalMs;
1178
1358
  continue;
1179
1359
  }
1180
1360
  if (data.error === "access_denied") {
1361
+ this.logger.warn("[GitHubAuthProvider] User denied authorization", {
1362
+ pollCount
1363
+ });
1181
1364
  throw new Error("User denied authorization");
1182
1365
  }
1183
1366
  if (data.error === "expired_token") {
1367
+ this.logger.warn(
1368
+ "[GitHubAuthProvider] Device code expired before authorization completed",
1369
+ { pollCount, elapsedMs: Date.now() - start }
1370
+ );
1184
1371
  throw new Error("GitHub device code expired \u2014 restart the flow");
1185
1372
  }
1373
+ this.logger.error(
1374
+ "[GitHubAuthProvider] Unexpected device-flow error from token endpoint",
1375
+ data.error ?? "unknown",
1376
+ { pollCount }
1377
+ );
1186
1378
  throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
1187
1379
  }
1188
1380
  }
@@ -2057,10 +2249,20 @@ function isRecord(value) {
2057
2249
  function isNodeError(value) {
2058
2250
  return value instanceof Error && typeof value.code === "string";
2059
2251
  }
2252
+ var LOCK_PID_FILE = "pid";
2253
+ function isProcessAlive(pid) {
2254
+ try {
2255
+ process.kill(pid, 0);
2256
+ return true;
2257
+ } catch {
2258
+ return false;
2259
+ }
2260
+ }
2060
2261
  var FileLock = class {
2061
2262
  constructor(filePath, timeoutMs, retryMs) {
2062
2263
  this.acquired = false;
2063
2264
  this.dirPath = `${filePath}.lock`;
2265
+ this.pidFilePath = path3.join(this.dirPath, LOCK_PID_FILE);
2064
2266
  this.timeoutMs = timeoutMs;
2065
2267
  this.retryMs = retryMs;
2066
2268
  }
@@ -2069,12 +2271,18 @@ var FileLock = class {
2069
2271
  while (true) {
2070
2272
  try {
2071
2273
  await fsp.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });
2274
+ await fsp.writeFile(this.pidFilePath, String(process.pid), "utf8").catch(() => void 0);
2072
2275
  this.acquired = true;
2073
2276
  return;
2074
2277
  } catch (err) {
2075
2278
  if (!isNodeError(err) || err.code !== "EEXIST") {
2076
2279
  throw err;
2077
2280
  }
2281
+ const stale = await this.isStaleLock();
2282
+ if (stale) {
2283
+ await fsp.rm(this.dirPath, { recursive: true, force: true });
2284
+ continue;
2285
+ }
2078
2286
  if (Date.now() - start >= this.timeoutMs) {
2079
2287
  throw new Error(`IdentityStore lock acquisition timed out for ${this.dirPath}`);
2080
2288
  }
@@ -2082,6 +2290,16 @@ var FileLock = class {
2082
2290
  }
2083
2291
  }
2084
2292
  }
2293
+ async isStaleLock() {
2294
+ try {
2295
+ const pidStr = await fsp.readFile(this.pidFilePath, "utf8");
2296
+ const pid = Number.parseInt(pidStr.trim(), 10);
2297
+ if (!Number.isFinite(pid) || pid <= 0) return true;
2298
+ return !isProcessAlive(pid);
2299
+ } catch {
2300
+ return true;
2301
+ }
2302
+ }
2085
2303
  async release() {
2086
2304
  if (!this.acquired) return;
2087
2305
  this.acquired = false;
@@ -2507,12 +2725,43 @@ var TOOL_CHECK_TIMEOUT_MS = {
2507
2725
  pnpm: 15e3,
2508
2726
  nrm: 15e3
2509
2727
  };
2728
+ var POSIX_LOGIN_SHELL_FALLBACK_TOOLS = /* @__PURE__ */ new Set([
2729
+ "npm",
2730
+ "pnpm",
2731
+ "nrm",
2732
+ "node",
2733
+ "dotnet",
2734
+ "rtk"
2735
+ ]);
2510
2736
  var ERROR_CODE_NOT_FOUND = 127;
2511
2737
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
2738
+ var NVM_FALLBACK_DIRS = [
2739
+ "/opt/homebrew/opt/nvm",
2740
+ "/usr/local/opt/nvm",
2741
+ "$HOME/.nvm"
2742
+ ];
2743
+ function resolveUserLoginShell(platform3, envShell) {
2744
+ const shell = envShell?.trim();
2745
+ if (shell && shell.length > 0) {
2746
+ return shell;
2747
+ }
2748
+ return platform3 === "darwin" ? "/bin/zsh" : "/bin/bash";
2749
+ }
2750
+ function buildNvmSourcingSnippet() {
2751
+ const fallbackList = NVM_FALLBACK_DIRS.map((dir) => `"${dir}"`).join(" ");
2752
+ return `for d in $NVM_DIR ${fallbackList}; do [ -n "$d" ] && [ -s "$d/nvm.sh" ] && export NVM_DIR="$d" && . "$d/nvm.sh" && break; done`;
2753
+ }
2754
+ function posixLoginShellArgs(platform3, envShell, command) {
2755
+ return {
2756
+ command: resolveUserLoginShell(platform3, envShell),
2757
+ args: ["-lc", `${buildNvmSourcingSnippet()}; ${command}`]
2758
+ };
2759
+ }
2512
2760
  var EnvironmentInspector = class {
2513
2761
  constructor(options = {}) {
2514
2762
  this.runCommandFn = options.runCommand ?? runCommand;
2515
2763
  this.platform = options.platform ?? process.platform;
2764
+ this.shell = options.shell ?? process.env.SHELL;
2516
2765
  }
2517
2766
  async checkEnvironment() {
2518
2767
  const results = await Promise.all(
@@ -2550,6 +2799,21 @@ var EnvironmentInspector = class {
2550
2799
  timeoutMs: this.getToolTimeout(toolName)
2551
2800
  });
2552
2801
  return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
2802
+ } catch {
2803
+ if (this.shouldUsePosixLoginShellFallback(toolName)) {
2804
+ return this.getToolPathFromLoginShell(toolName);
2805
+ }
2806
+ return void 0;
2807
+ }
2808
+ }
2809
+ async getToolPathFromLoginShell(toolName) {
2810
+ try {
2811
+ const spawnArgs = posixLoginShellArgs(this.platform, this.shell, `command -v ${toolName}`);
2812
+ const result = await this.runCommandFn(spawnArgs.command, {
2813
+ args: spawnArgs.args,
2814
+ timeoutMs: this.getToolTimeout(toolName)
2815
+ });
2816
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
2553
2817
  } catch {
2554
2818
  return void 0;
2555
2819
  }
@@ -2574,12 +2838,31 @@ var EnvironmentInspector = class {
2574
2838
  }
2575
2839
  }
2576
2840
  async getToolVersion(toolName) {
2577
- const invocation = await this.getVersionInvocation(toolName);
2578
- const result = await this.runCommandFn(invocation.command, {
2579
- args: invocation.args,
2580
- timeoutMs: this.getToolTimeout(toolName)
2581
- });
2582
- return this.parseVersion(toolName, result.stdout || result.stderr);
2841
+ try {
2842
+ const invocation = await this.getVersionInvocation(toolName);
2843
+ const result = await this.runCommandFn(invocation.command, {
2844
+ args: invocation.args,
2845
+ timeoutMs: this.getToolTimeout(toolName)
2846
+ });
2847
+ return this.parseVersion(toolName, result.stdout || result.stderr);
2848
+ } catch (error) {
2849
+ if (!this.shouldUsePosixLoginShellFallback(toolName)) {
2850
+ throw error;
2851
+ }
2852
+ const fallbackArgs = posixLoginShellArgs(this.platform, this.shell, `${toolName} --version`);
2853
+ const fallbackResult = await this.runCommandFn(fallbackArgs.command, {
2854
+ args: fallbackArgs.args,
2855
+ timeoutMs: this.getToolTimeout(toolName)
2856
+ });
2857
+ const fallbackOutput = (fallbackResult.stdout || fallbackResult.stderr).trim();
2858
+ if (!fallbackOutput) {
2859
+ throw error;
2860
+ }
2861
+ return this.parseVersion(toolName, fallbackOutput);
2862
+ }
2863
+ }
2864
+ shouldUsePosixLoginShellFallback(toolName) {
2865
+ return this.platform !== "win32" && POSIX_LOGIN_SHELL_FALLBACK_TOOLS.has(toolName);
2583
2866
  }
2584
2867
  async checkNvm() {
2585
2868
  if (this.platform === "win32") {
@@ -2601,11 +2884,9 @@ var EnvironmentInspector = class {
2601
2884
  }
2602
2885
  }
2603
2886
  try {
2604
- const result = await this.runCommandFn("/bin/bash", {
2605
- args: [
2606
- "-lc",
2607
- 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version'
2608
- ],
2887
+ const nvmArgs = posixLoginShellArgs(this.platform, this.shell, "nvm --version");
2888
+ const result = await this.runCommandFn(nvmArgs.command, {
2889
+ args: nvmArgs.args,
2609
2890
  timeoutMs: this.getToolTimeout("nvm")
2610
2891
  });
2611
2892
  return {
@@ -2629,7 +2910,7 @@ var EnvironmentInspector = class {
2629
2910
  };
2630
2911
  }
2631
2912
  try {
2632
- const result = await this.runCommandFn("dotnet", {
2913
+ const result = await this.runCommandFn(dotnetPath, {
2633
2914
  args: ["nuget", "list", "source"],
2634
2915
  timeoutMs: this.getToolTimeout("nuget")
2635
2916
  });
@@ -2792,7 +3073,9 @@ var GitClient = class {
2792
3073
  }
2793
3074
  const targetUrl = useProxy ? remoteUrl : originalUrl ?? remoteUrl;
2794
3075
  const proxyUrl = this.rewriteRemoteUrl(repoId, targetUrl, useProxy);
2795
- await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
3076
+ await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], {
3077
+ cwd: localPath
3078
+ });
2796
3079
  const before = await this.safeRevParse(localPath);
2797
3080
  await this.runOrThrow(["fetch", "origin"], { cwd: localPath });
2798
3081
  const after = await this.safeRevParse(localPath);
@@ -2828,7 +3111,9 @@ var GitClient = class {
2828
3111
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2829
3112
  }
2830
3113
  const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl, useProxy);
2831
- await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
3114
+ await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], {
3115
+ cwd: localPath
3116
+ });
2832
3117
  const result = await this.spawner.spawn(
2833
3118
  ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`],
2834
3119
  { cwd: localPath }
@@ -2882,7 +3167,9 @@ var GitClient = class {
2882
3167
  return { commitSha: sha };
2883
3168
  }
2884
3169
  async runOrThrow(args, opts) {
2885
- const result = await this.spawner.spawn(args, { cwd: opts.cwd ?? process.cwd() });
3170
+ const result = await this.spawner.spawn(args, {
3171
+ cwd: opts.cwd ?? process.cwd()
3172
+ });
2886
3173
  if (result.code !== 0) {
2887
3174
  throw new GitError(args, result);
2888
3175
  }
@@ -2897,7 +3184,9 @@ var GitClient = class {
2897
3184
  return url.length > 0 ? url : void 0;
2898
3185
  }
2899
3186
  async safeRevParse(localPath) {
2900
- const result = await this.spawner.spawn(["rev-parse", "HEAD"], { cwd: localPath });
3187
+ const result = await this.spawner.spawn(["rev-parse", "HEAD"], {
3188
+ cwd: localPath
3189
+ });
2901
3190
  if (result.code !== 0) return "";
2902
3191
  return result.stdout.trim();
2903
3192
  }
@@ -2944,7 +3233,11 @@ var StubGitSpawner = class {
2944
3233
  this.calls.push({ args, cwd: opts.cwd });
2945
3234
  const next = this.script.shift();
2946
3235
  if (!next) {
2947
- return { stdout: "", stderr: `stub: no scripted response for ${args.join(" ")}`, code: 1 };
3236
+ return {
3237
+ stdout: "",
3238
+ stderr: `stub: no scripted response for ${args.join(" ")}`,
3239
+ code: 1
3240
+ };
2948
3241
  }
2949
3242
  return next;
2950
3243
  }
@@ -2956,6 +3249,10 @@ function toFileUrl(absolutePath) {
2956
3249
  }
2957
3250
  return `file://${normalized}`;
2958
3251
  }
3252
+ var GIT_PROXY_PATH_SUFFIX = "/git-proxy";
3253
+ function buildGitProxyBase(serverBaseUrl) {
3254
+ return `${serverBaseUrl.replace(/\/+$/, "")}${GIT_PROXY_PATH_SUFFIX}`;
3255
+ }
2959
3256
 
2960
3257
  // src/image/imageTools.ts
2961
3258
  var fs5 = __toESM(require("fs/promises"));
@@ -3160,50 +3457,6 @@ function detectIndent(text) {
3160
3457
  return 2;
3161
3458
  }
3162
3459
 
3163
- // src/logger.ts
3164
- var noopLogger = {
3165
- debug() {
3166
- },
3167
- info() {
3168
- },
3169
- warn() {
3170
- },
3171
- error() {
3172
- }
3173
- };
3174
- function formatArgs(args) {
3175
- return args.map((arg) => {
3176
- if (typeof arg === "string") {
3177
- return arg;
3178
- }
3179
- try {
3180
- return JSON.stringify(arg);
3181
- } catch {
3182
- return String(arg);
3183
- }
3184
- }).join(" ");
3185
- }
3186
- function createConsoleLogger(prefix = "serviceme") {
3187
- return {
3188
- debug(message, ...args) {
3189
- process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
3190
- `);
3191
- },
3192
- info(message, ...args) {
3193
- process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
3194
- `);
3195
- },
3196
- warn(message, ...args) {
3197
- process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}
3198
- `);
3199
- },
3200
- error(message, ...args) {
3201
- process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
3202
- `);
3203
- }
3204
- };
3205
- }
3206
-
3207
3460
  // src/paths/serverProxyGlobal.ts
3208
3461
  var import_node_crypto4 = require("crypto");
3209
3462
  var fs6 = __toESM(require("fs/promises"));
@@ -6853,10 +7106,19 @@ function coercePersistedToolbox(parsed) {
6853
7106
  function isNodeError2(value) {
6854
7107
  return value instanceof Error && typeof value.code === "string";
6855
7108
  }
7109
+ function isProcessAlive2(pid) {
7110
+ try {
7111
+ process.kill(pid, 0);
7112
+ return true;
7113
+ } catch {
7114
+ return false;
7115
+ }
7116
+ }
6856
7117
  var ToolboxFileLock = class {
6857
7118
  constructor(filePath, timeoutMs, retryMs) {
6858
7119
  this.acquired = false;
6859
7120
  this.dirPath = `${filePath}.lock`;
7121
+ this.pidFilePath = path25.join(this.dirPath, "pid");
6860
7122
  this.timeoutMs = timeoutMs;
6861
7123
  this.retryMs = retryMs;
6862
7124
  }
@@ -6865,10 +7127,16 @@ var ToolboxFileLock = class {
6865
7127
  while (true) {
6866
7128
  try {
6867
7129
  await fsp2.mkdir(this.dirPath, { mode: LOCK_DIR_MODE2 });
7130
+ await fsp2.writeFile(this.pidFilePath, String(process.pid), "utf8").catch(() => void 0);
6868
7131
  this.acquired = true;
6869
7132
  return;
6870
7133
  } catch (err) {
6871
7134
  if (!isNodeError2(err) || err.code !== "EEXIST") throw err;
7135
+ const stale = await this.isStaleLock();
7136
+ if (stale) {
7137
+ await fsp2.rm(this.dirPath, { recursive: true, force: true });
7138
+ continue;
7139
+ }
6872
7140
  if (Date.now() - start >= this.timeoutMs) {
6873
7141
  throw new Error(`ToolboxStore lock acquisition timed out for ${this.dirPath}`);
6874
7142
  }
@@ -6876,6 +7144,16 @@ var ToolboxFileLock = class {
6876
7144
  }
6877
7145
  }
6878
7146
  }
7147
+ async isStaleLock() {
7148
+ try {
7149
+ const pidStr = await fsp2.readFile(this.pidFilePath, "utf8");
7150
+ const pid = Number.parseInt(pidStr.trim(), 10);
7151
+ if (!Number.isFinite(pid) || pid <= 0) return true;
7152
+ return !isProcessAlive2(pid);
7153
+ } catch {
7154
+ return true;
7155
+ }
7156
+ }
6879
7157
  async release() {
6880
7158
  if (!this.acquired) return;
6881
7159
  this.acquired = false;
@@ -7155,6 +7433,7 @@ var ToolboxCore = class {
7155
7433
  EnvironmentInspector,
7156
7434
  FsIdentityFileBackend,
7157
7435
  FsToolboxFileBackend,
7436
+ GIT_PROXY_PATH_SUFFIX,
7158
7437
  GitClient,
7159
7438
  GitError,
7160
7439
  GitHubAuthProvider,
@@ -7221,6 +7500,7 @@ var ToolboxCore = class {
7221
7500
  bootstrapPhase5Placeholders,
7222
7501
  buildDefaultReposFile,
7223
7502
  buildGitHubLocalEmail,
7503
+ buildGitProxyBase,
7224
7504
  buildSignedHeaders,
7225
7505
  copilotDoctor,
7226
7506
  copilotPrompt,