@serviceme/devtools-core 0.3.1 → 0.3.3

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.mjs CHANGED
@@ -454,6 +454,50 @@ var AccessControl = class {
454
454
  }
455
455
  };
456
456
 
457
+ // src/logger.ts
458
+ var noopLogger = {
459
+ debug() {
460
+ },
461
+ info() {
462
+ },
463
+ warn() {
464
+ },
465
+ error() {
466
+ }
467
+ };
468
+ function formatArgs(args) {
469
+ return args.map((arg) => {
470
+ if (typeof arg === "string") {
471
+ return arg;
472
+ }
473
+ try {
474
+ return JSON.stringify(arg);
475
+ } catch {
476
+ return String(arg);
477
+ }
478
+ }).join(" ");
479
+ }
480
+ function createConsoleLogger(prefix = "serviceme") {
481
+ return {
482
+ debug(message, ...args) {
483
+ process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
484
+ `);
485
+ },
486
+ info(message, ...args) {
487
+ process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
488
+ `);
489
+ },
490
+ warn(message, ...args) {
491
+ process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}
492
+ `);
493
+ },
494
+ error(message, ...args) {
495
+ process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
496
+ `);
497
+ }
498
+ };
499
+ }
500
+
457
501
  // src/auth/AuthStateManager.ts
458
502
  import { EventEmitter } from "events";
459
503
  var AuthStateManager = class {
@@ -616,6 +660,7 @@ var AuthCore = class {
616
660
  this.state = opts.stateManager ?? new AuthStateManager();
617
661
  this.tokenStore = opts.tokenStore;
618
662
  this.accessControl = opts.accessControl;
663
+ this.logger = opts.logger ?? noopLogger;
619
664
  }
620
665
  /** Snapshot of every account, the active provider, and the last error. */
621
666
  status() {
@@ -637,16 +682,28 @@ var AuthCore = class {
637
682
  */
638
683
  async login(provider, ui, shouldContinue) {
639
684
  const providerImpl = this.registry.get(provider);
685
+ this.logger.info("[AuthCore] Starting device-flow login", { provider });
640
686
  try {
641
687
  const initial = await providerImpl.requestDeviceFlow();
642
688
  await ui(initial);
689
+ if (!initial.deviceCode) {
690
+ this.logger.error("[AuthCore] Provider did not return deviceCode", provider);
691
+ throw new Error("Auth provider did not return deviceCode for device flow completion");
692
+ }
643
693
  const session = await providerImpl.completeDeviceFlow(
644
- initial.userCode ? initial.deviceCode ?? "" : "",
645
- shouldContinue
694
+ initial.deviceCode,
695
+ shouldContinue,
696
+ initial.pollIntervalMs
646
697
  );
647
- return await this.persistSession(providerImpl, session);
698
+ const result = await this.persistSession(providerImpl, session);
699
+ this.logger.info("[AuthCore] Device-flow login completed", { provider });
700
+ return result;
648
701
  } catch (err) {
649
- this.state.recordError(err instanceof Error ? err.message : String(err));
702
+ const message = err instanceof Error ? err.message : String(err);
703
+ this.logger.error("[AuthCore] Device-flow login failed", message, {
704
+ provider
705
+ });
706
+ this.state.recordError(message);
650
707
  throw err;
651
708
  }
652
709
  }
@@ -671,7 +728,10 @@ var AuthCore = class {
671
728
  if (!provider) return null;
672
729
  const account = this.state.getActiveAccount();
673
730
  if (!account) return null;
674
- const envelope = await this.tokenStore.get({ provider, accountId: account.id });
731
+ const envelope = await this.tokenStore.get({
732
+ provider,
733
+ accountId: account.id
734
+ });
675
735
  if (!envelope) return null;
676
736
  return { provider, account, token: envelope.token };
677
737
  }
@@ -681,9 +741,15 @@ var AuthCore = class {
681
741
  * every account.
682
742
  */
683
743
  async logout(provider) {
744
+ this.logger.info("[AuthCore] Logout requested", {
745
+ provider: provider ?? "all"
746
+ });
684
747
  if (!provider) {
685
748
  for (const account of this.state.listAccounts()) {
686
- await this.tokenStore.delete({ provider: account.provider, accountId: account.id });
749
+ await this.tokenStore.delete({
750
+ provider: account.provider,
751
+ accountId: account.id
752
+ });
687
753
  this.state.removeAccount(account.provider, account.id);
688
754
  }
689
755
  this.state.clearAll();
@@ -758,6 +824,10 @@ var AuthCore = class {
758
824
  avatarUrl: meta.avatarUrl,
759
825
  expiresAt: meta.expiresAt ?? expiresAt
760
826
  };
827
+ this.logger.debug("[AuthCore] Persisting session", {
828
+ provider: meta.provider,
829
+ accountId: meta.id
830
+ });
761
831
  await this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {
762
832
  expiresAt
763
833
  });
@@ -860,11 +930,13 @@ var GitHubAuthProvider = class {
860
930
  userUrl: config.userUrl ?? DEFAULT_USER_URL,
861
931
  scope: config.scope ?? DEFAULT_SCOPE,
862
932
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
863
- maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
933
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 6e4,
864
934
  maxWaitMs: config.maxWaitMs,
865
935
  fetchImpl: config.fetchImpl ?? fetch,
866
936
  deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
867
937
  };
938
+ this.logger = config.logger ?? noopLogger;
939
+ this.sleepImpl = config.sleepImpl ?? sleep;
868
940
  }
869
941
  async requestDeviceFlow(opts) {
870
942
  const body = JSON.stringify({
@@ -873,6 +945,10 @@ var GitHubAuthProvider = class {
873
945
  });
874
946
  let lastNetworkError;
875
947
  for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
948
+ this.logger.debug("[GitHubAuthProvider] Requesting device code", {
949
+ attempt,
950
+ maxAttempts: DEVICE_CODE_MAX_ATTEMPTS
951
+ });
876
952
  let resp;
877
953
  try {
878
954
  resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
@@ -886,45 +962,99 @@ var GitHubAuthProvider = class {
886
962
  });
887
963
  } catch (error) {
888
964
  if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
965
+ this.logger.error(
966
+ "[GitHubAuthProvider] Device-code request failed (non-retryable)",
967
+ error instanceof Error ? error.message : String(error),
968
+ { attempt }
969
+ );
889
970
  throw error;
890
971
  }
972
+ const delayMs = this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1);
973
+ this.logger.warn(
974
+ "[GitHubAuthProvider] Transient network error requesting device code, retrying",
975
+ {
976
+ attempt,
977
+ delayMs,
978
+ error: error instanceof Error ? error.message : String(error)
979
+ }
980
+ );
891
981
  lastNetworkError = error;
892
- await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
982
+ await this.sleepImpl(delayMs);
893
983
  continue;
894
984
  }
895
985
  if (!resp.ok) {
986
+ this.logger.error(
987
+ "[GitHubAuthProvider] Device-code request rejected by GitHub",
988
+ `HTTP ${resp.status}`
989
+ );
896
990
  throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
897
991
  }
898
992
  const data = await resp.json();
899
993
  if (!data.device_code || !data.user_code || !data.verification_uri) {
994
+ this.logger.error(
995
+ "[GitHubAuthProvider] Device-code response missing required fields",
996
+ JSON.stringify(Object.keys(data))
997
+ );
900
998
  throw new Error("GitHub device-code response missing required fields");
901
999
  }
1000
+ this.logger.info("[GitHubAuthProvider] Device code obtained", {
1001
+ userCode: data.user_code,
1002
+ verificationUri: data.verification_uri,
1003
+ expiresInSec: data.expires_in,
1004
+ pollIntervalSec: data.interval
1005
+ });
902
1006
  return {
903
1007
  provider: this.providerId,
1008
+ deviceCode: data.device_code,
904
1009
  userCode: data.user_code,
905
1010
  verificationUrl: data.verification_uri,
906
1011
  expiresAt: Date.now() + data.expires_in * 1e3,
1012
+ pollIntervalMs: data.interval * 1e3,
907
1013
  message: `Open ${data.verification_uri} and enter ${data.user_code}`
908
1014
  };
909
1015
  }
910
1016
  throw lastNetworkError ?? new Error("GitHub device-code request failed");
911
1017
  }
912
- async completeDeviceFlow(deviceCode, shouldContinue) {
1018
+ async completeDeviceFlow(deviceCode, shouldContinue, initialPollIntervalMs) {
913
1019
  const start = Date.now();
914
- let pollIntervalMs = this.cfg.minPollIntervalMs;
1020
+ let pollIntervalMs = Math.max(this.cfg.minPollIntervalMs, initialPollIntervalMs ?? 0);
915
1021
  let consecutiveSlowDown = 0;
1022
+ let pollCount = 0;
1023
+ this.logger.info("[GitHubAuthProvider] Starting device-flow polling", {
1024
+ initialPollIntervalMs: pollIntervalMs,
1025
+ maxWaitMs: this.cfg.maxWaitMs
1026
+ });
916
1027
  while (true) {
917
1028
  if (shouldContinue && !shouldContinue()) {
1029
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
1030
+ elapsedMs: Date.now() - start,
1031
+ pollCount
1032
+ });
918
1033
  throw new Error("GitHub device flow cancelled by caller");
919
1034
  }
920
1035
  const elapsed = Date.now() - start;
921
1036
  if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
1037
+ this.logger.warn("[GitHubAuthProvider] Device flow exceeded max wait time", {
1038
+ elapsedMs: elapsed,
1039
+ maxWaitMs: this.cfg.maxWaitMs,
1040
+ pollCount
1041
+ });
922
1042
  throw new Error("GitHub device flow exceeded max wait time");
923
1043
  }
924
- await sleep(pollIntervalMs);
1044
+ await this.sleepImpl(pollIntervalMs);
925
1045
  if (shouldContinue && !shouldContinue()) {
1046
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
1047
+ elapsedMs: Date.now() - start,
1048
+ pollCount
1049
+ });
926
1050
  throw new Error("GitHub device flow cancelled by caller");
927
1051
  }
1052
+ pollCount++;
1053
+ this.logger.debug("[GitHubAuthProvider] Polling for authorization", {
1054
+ pollCount,
1055
+ elapsedMs: Date.now() - start,
1056
+ pollIntervalMs
1057
+ });
928
1058
  let resp;
929
1059
  try {
930
1060
  resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
@@ -942,19 +1072,59 @@ var GitHubAuthProvider = class {
942
1072
  });
943
1073
  } catch (error) {
944
1074
  if (!isTransientNetworkError(error)) {
1075
+ this.logger.error(
1076
+ "[GitHubAuthProvider] Non-retryable error while polling token endpoint",
1077
+ error instanceof Error ? error.message : String(error),
1078
+ { pollCount }
1079
+ );
945
1080
  throw error;
946
1081
  }
947
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1082
+ const nextPollIntervalMs = Math.min(
1083
+ Math.round(pollIntervalMs * 1.5),
1084
+ this.cfg.maxPollIntervalMs
1085
+ );
1086
+ this.logger.warn(
1087
+ "[GitHubAuthProvider] Transient network error while polling, backing off",
1088
+ {
1089
+ pollCount,
1090
+ error: error instanceof Error ? error.message : String(error),
1091
+ previousPollIntervalMs: pollIntervalMs,
1092
+ nextPollIntervalMs
1093
+ }
1094
+ );
1095
+ pollIntervalMs = nextPollIntervalMs;
948
1096
  continue;
949
1097
  }
950
1098
  if (!resp.ok) {
951
- pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1099
+ const nextPollIntervalMs = Math.min(
1100
+ Math.round(pollIntervalMs * 1.5),
1101
+ this.cfg.maxPollIntervalMs
1102
+ );
1103
+ this.logger.warn(
1104
+ "[GitHubAuthProvider] Token endpoint returned non-OK status, backing off",
1105
+ {
1106
+ pollCount,
1107
+ status: resp.status,
1108
+ previousPollIntervalMs: pollIntervalMs,
1109
+ nextPollIntervalMs
1110
+ }
1111
+ );
1112
+ pollIntervalMs = nextPollIntervalMs;
952
1113
  continue;
953
1114
  }
954
1115
  const data = await resp.json();
955
1116
  if (data.access_token) {
1117
+ this.logger.info("[GitHubAuthProvider] Authorization granted, fetching user profile", {
1118
+ pollCount,
1119
+ elapsedMs: Date.now() - start
1120
+ });
956
1121
  const rawUser = await this.fetchGitHubUser(data.access_token);
957
1122
  const email = await this.resolveEmail(data.access_token, rawUser);
1123
+ this.logger.info("[GitHubAuthProvider] Device-flow login completed", {
1124
+ pollCount,
1125
+ elapsedMs: Date.now() - start,
1126
+ login: rawUser.login
1127
+ });
958
1128
  return {
959
1129
  token: data.access_token,
960
1130
  refreshToken: data.refresh_token,
@@ -969,22 +1139,42 @@ var GitHubAuthProvider = class {
969
1139
  };
970
1140
  }
971
1141
  if (data.error === "authorization_pending") {
1142
+ this.logger.debug("[GitHubAuthProvider] Authorization still pending", {
1143
+ pollCount,
1144
+ elapsedMs: Date.now() - start
1145
+ });
972
1146
  continue;
973
1147
  }
974
1148
  if (data.error === "slow_down") {
975
1149
  consecutiveSlowDown++;
976
- pollIntervalMs = Math.min(
977
- Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
978
- this.cfg.maxPollIntervalMs
979
- );
1150
+ const nextPollIntervalMs = Math.min(pollIntervalMs + 5e3, this.cfg.maxPollIntervalMs);
1151
+ this.logger.warn("[GitHubAuthProvider] GitHub requested slower polling (slow_down)", {
1152
+ pollCount,
1153
+ consecutiveSlowDown,
1154
+ previousPollIntervalMs: pollIntervalMs,
1155
+ nextPollIntervalMs
1156
+ });
1157
+ pollIntervalMs = nextPollIntervalMs;
980
1158
  continue;
981
1159
  }
982
1160
  if (data.error === "access_denied") {
1161
+ this.logger.warn("[GitHubAuthProvider] User denied authorization", {
1162
+ pollCount
1163
+ });
983
1164
  throw new Error("User denied authorization");
984
1165
  }
985
1166
  if (data.error === "expired_token") {
1167
+ this.logger.warn(
1168
+ "[GitHubAuthProvider] Device code expired before authorization completed",
1169
+ { pollCount, elapsedMs: Date.now() - start }
1170
+ );
986
1171
  throw new Error("GitHub device code expired \u2014 restart the flow");
987
1172
  }
1173
+ this.logger.error(
1174
+ "[GitHubAuthProvider] Unexpected device-flow error from token endpoint",
1175
+ data.error ?? "unknown",
1176
+ { pollCount }
1177
+ );
988
1178
  throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
989
1179
  }
990
1180
  }
@@ -1671,6 +1861,7 @@ var DRAFTS_SUBDIR = "drafts";
1671
1861
  var SKILL_DRAFTS_SUBDIR = "skills";
1672
1862
  var AGENT_DRAFTS_SUBDIR = "agents";
1673
1863
  var REPOS_CONFIG_FILENAME = "repos.json";
1864
+ var SERVER_PROXY_GLOBAL_FILENAME = "server-proxy.json";
1674
1865
  var SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
1675
1866
  var SERVICEME_HOME_ENV = "SERVICEME_HOME";
1676
1867
  var activeOverrides = {};
@@ -1771,6 +1962,9 @@ function getMigrationFailuresPath() {
1771
1962
  function getKnownWorkspacesPath() {
1772
1963
  return path2.join(getServicemeHome(), KNOWN_WORKSPACES_FILENAME);
1773
1964
  }
1965
+ function getServerProxyGlobalPath() {
1966
+ return path2.join(getServicemeHome(), SERVER_PROXY_GLOBAL_FILENAME);
1967
+ }
1774
1968
  var CREDENTIALS_CONFIG_FILENAME = "credentials.json";
1775
1969
  var DEVICE_JSON_FILENAME = "device.json";
1776
1970
  var TOOLBOX_JSON_FILENAME = "toolbox.json";
@@ -1862,10 +2056,20 @@ function isRecord(value) {
1862
2056
  function isNodeError(value) {
1863
2057
  return value instanceof Error && typeof value.code === "string";
1864
2058
  }
2059
+ var LOCK_PID_FILE = "pid";
2060
+ function isProcessAlive(pid) {
2061
+ try {
2062
+ process.kill(pid, 0);
2063
+ return true;
2064
+ } catch {
2065
+ return false;
2066
+ }
2067
+ }
1865
2068
  var FileLock = class {
1866
2069
  constructor(filePath, timeoutMs, retryMs) {
1867
2070
  this.acquired = false;
1868
2071
  this.dirPath = `${filePath}.lock`;
2072
+ this.pidFilePath = path3.join(this.dirPath, LOCK_PID_FILE);
1869
2073
  this.timeoutMs = timeoutMs;
1870
2074
  this.retryMs = retryMs;
1871
2075
  }
@@ -1874,12 +2078,18 @@ var FileLock = class {
1874
2078
  while (true) {
1875
2079
  try {
1876
2080
  await fsp.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });
2081
+ await fsp.writeFile(this.pidFilePath, String(process.pid), "utf8").catch(() => void 0);
1877
2082
  this.acquired = true;
1878
2083
  return;
1879
2084
  } catch (err) {
1880
2085
  if (!isNodeError(err) || err.code !== "EEXIST") {
1881
2086
  throw err;
1882
2087
  }
2088
+ const stale = await this.isStaleLock();
2089
+ if (stale) {
2090
+ await fsp.rm(this.dirPath, { recursive: true, force: true });
2091
+ continue;
2092
+ }
1883
2093
  if (Date.now() - start >= this.timeoutMs) {
1884
2094
  throw new Error(`IdentityStore lock acquisition timed out for ${this.dirPath}`);
1885
2095
  }
@@ -1887,6 +2097,16 @@ var FileLock = class {
1887
2097
  }
1888
2098
  }
1889
2099
  }
2100
+ async isStaleLock() {
2101
+ try {
2102
+ const pidStr = await fsp.readFile(this.pidFilePath, "utf8");
2103
+ const pid = Number.parseInt(pidStr.trim(), 10);
2104
+ if (!Number.isFinite(pid) || pid <= 0) return true;
2105
+ return !isProcessAlive(pid);
2106
+ } catch {
2107
+ return true;
2108
+ }
2109
+ }
1890
2110
  async release() {
1891
2111
  if (!this.acquired) return;
1892
2112
  this.acquired = false;
@@ -2315,6 +2535,14 @@ var TOOL_CHECK_TIMEOUT_MS = {
2315
2535
  pnpm: 15e3,
2316
2536
  nrm: 15e3
2317
2537
  };
2538
+ var POSIX_LOGIN_SHELL_FALLBACK_TOOLS = /* @__PURE__ */ new Set([
2539
+ "npm",
2540
+ "pnpm",
2541
+ "nrm",
2542
+ "node",
2543
+ "dotnet",
2544
+ "rtk"
2545
+ ]);
2318
2546
  var ERROR_CODE_NOT_FOUND = 127;
2319
2547
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
2320
2548
  var EnvironmentInspector = class {
@@ -2358,6 +2586,23 @@ var EnvironmentInspector = class {
2358
2586
  timeoutMs: this.getToolTimeout(toolName)
2359
2587
  });
2360
2588
  return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
2589
+ } catch {
2590
+ if (this.shouldUsePosixLoginShellFallback(toolName)) {
2591
+ return this.getToolPathFromLoginShell(toolName);
2592
+ }
2593
+ return void 0;
2594
+ }
2595
+ }
2596
+ async getToolPathFromLoginShell(toolName) {
2597
+ try {
2598
+ const result = await this.runCommandFn("/bin/bash", {
2599
+ args: [
2600
+ "-lc",
2601
+ 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; command -v ' + toolName
2602
+ ],
2603
+ timeoutMs: this.getToolTimeout(toolName)
2604
+ });
2605
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
2361
2606
  } catch {
2362
2607
  return void 0;
2363
2608
  }
@@ -2382,12 +2627,33 @@ var EnvironmentInspector = class {
2382
2627
  }
2383
2628
  }
2384
2629
  async getToolVersion(toolName) {
2385
- const invocation = await this.getVersionInvocation(toolName);
2386
- const result = await this.runCommandFn(invocation.command, {
2387
- args: invocation.args,
2388
- timeoutMs: this.getToolTimeout(toolName)
2389
- });
2390
- return this.parseVersion(toolName, result.stdout || result.stderr);
2630
+ try {
2631
+ const invocation = await this.getVersionInvocation(toolName);
2632
+ const result = await this.runCommandFn(invocation.command, {
2633
+ args: invocation.args,
2634
+ timeoutMs: this.getToolTimeout(toolName)
2635
+ });
2636
+ return this.parseVersion(toolName, result.stdout || result.stderr);
2637
+ } catch (error) {
2638
+ if (!this.shouldUsePosixLoginShellFallback(toolName)) {
2639
+ throw error;
2640
+ }
2641
+ const fallbackResult = await this.runCommandFn("/bin/bash", {
2642
+ args: [
2643
+ "-lc",
2644
+ `export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; ${toolName} --version`
2645
+ ],
2646
+ timeoutMs: this.getToolTimeout(toolName)
2647
+ });
2648
+ const fallbackOutput = (fallbackResult.stdout || fallbackResult.stderr).trim();
2649
+ if (!fallbackOutput) {
2650
+ throw error;
2651
+ }
2652
+ return this.parseVersion(toolName, fallbackOutput);
2653
+ }
2654
+ }
2655
+ shouldUsePosixLoginShellFallback(toolName) {
2656
+ return this.platform !== "win32" && POSIX_LOGIN_SHELL_FALLBACK_TOOLS.has(toolName);
2391
2657
  }
2392
2658
  async checkNvm() {
2393
2659
  if (this.platform === "win32") {
@@ -2437,7 +2703,7 @@ var EnvironmentInspector = class {
2437
2703
  };
2438
2704
  }
2439
2705
  try {
2440
- const result = await this.runCommandFn("dotnet", {
2706
+ const result = await this.runCommandFn(dotnetPath, {
2441
2707
  args: ["nuget", "list", "source"],
2442
2708
  timeoutMs: this.getToolTimeout("nuget")
2443
2709
  });
@@ -2973,54 +3239,85 @@ function detectIndent(text) {
2973
3239
  return 2;
2974
3240
  }
2975
3241
 
2976
- // src/logger.ts
2977
- var noopLogger = {
2978
- debug() {
2979
- },
2980
- info() {
2981
- },
2982
- warn() {
2983
- },
2984
- error() {
2985
- }
2986
- };
2987
- function formatArgs(args) {
2988
- return args.map((arg) => {
2989
- if (typeof arg === "string") {
2990
- return arg;
2991
- }
2992
- try {
2993
- return JSON.stringify(arg);
2994
- } catch {
2995
- return String(arg);
2996
- }
2997
- }).join(" ");
2998
- }
2999
- function createConsoleLogger(prefix = "serviceme") {
3000
- return {
3001
- debug(message, ...args) {
3002
- process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
3003
- `);
3004
- },
3005
- info(message, ...args) {
3006
- process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
3007
- `);
3008
- },
3009
- warn(message, ...args) {
3010
- process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}
3011
- `);
3012
- },
3013
- error(message, ...args) {
3014
- process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
3015
- `);
3242
+ // src/paths/serverProxyGlobal.ts
3243
+ import { randomUUID as randomUUID2 } from "crypto";
3244
+ import * as fs6 from "fs/promises";
3245
+ import { open as open2 } from "fs/promises";
3246
+ import * as path9 from "path";
3247
+ async function readServerProxyGlobal() {
3248
+ const filePath = getServerProxyGlobalPath();
3249
+ try {
3250
+ const raw = await fs6.readFile(filePath, "utf8");
3251
+ const parsed = JSON.parse(raw);
3252
+ if (!isServerProxyGlobalState(parsed)) {
3253
+ throw new Error(
3254
+ `Invalid ${SERVER_PROXY_GLOBAL_FILENAME}: expected {enabled: boolean, lastServerUrl?: string, updatedAt: string}, got ${JSON.stringify(parsed).slice(0, 80)}`
3255
+ );
3016
3256
  }
3257
+ return parsed;
3258
+ } catch (err) {
3259
+ if (isENOENT(err)) return null;
3260
+ throw err;
3261
+ }
3262
+ }
3263
+ async function writeServerProxyGlobal(patch) {
3264
+ const filePath = getServerProxyGlobalPath();
3265
+ const dirPath = path9.dirname(filePath);
3266
+ await fs6.mkdir(dirPath, { recursive: true });
3267
+ const current = await readServerProxyGlobal() ?? {
3268
+ enabled: false,
3269
+ allowOverride: false,
3270
+ lastServerUrl: void 0,
3271
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
3017
3272
  };
3273
+ const next = {
3274
+ enabled: patch.enabled !== void 0 ? patch.enabled : current.enabled,
3275
+ allowOverride: patch.allowOverride !== void 0 ? patch.allowOverride : current.allowOverride,
3276
+ lastServerUrl: patch.lastServerUrl === void 0 ? current.lastServerUrl : patch.lastServerUrl === null ? void 0 : patch.lastServerUrl,
3277
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3278
+ };
3279
+ const tmpPath = `${filePath}.tmp-${randomUUID2()}`;
3280
+ const fh = await open2(tmpPath, "w");
3281
+ try {
3282
+ await fh.writeFile(JSON.stringify(next, null, " "), "utf8");
3283
+ await fh.sync();
3284
+ } finally {
3285
+ await fh.close();
3286
+ }
3287
+ await fs6.rename(tmpPath, filePath);
3288
+ return next;
3289
+ }
3290
+ async function migrateLegacyServerProxyEnabled(readLegacy, clearLegacy) {
3291
+ const legacyEnabled = readLegacy();
3292
+ if (legacyEnabled !== true) return null;
3293
+ const existing = await readServerProxyGlobal();
3294
+ if (existing?.enabled === true) {
3295
+ await clearLegacy();
3296
+ return null;
3297
+ }
3298
+ const next = await writeServerProxyGlobal({ enabled: true });
3299
+ await clearLegacy();
3300
+ return next;
3301
+ }
3302
+ function isServerProxyGlobalState(v) {
3303
+ if (!v || typeof v !== "object") return false;
3304
+ const obj = v;
3305
+ if (typeof obj.enabled !== "boolean") return false;
3306
+ if (typeof obj.allowOverride !== "boolean") return false;
3307
+ if (typeof obj.updatedAt !== "string") return false;
3308
+ if (obj.lastServerUrl !== void 0 && obj.lastServerUrl !== null && typeof obj.lastServerUrl !== "string") {
3309
+ return false;
3310
+ }
3311
+ return true;
3312
+ }
3313
+ function isENOENT(err) {
3314
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
3018
3315
  }
3019
3316
 
3020
3317
  // src/phase5/bootstrap.ts
3021
- import { randomUUID as randomUUID2 } from "crypto";
3022
- import * as fs6 from "fs/promises";
3023
- import * as path9 from "path";
3318
+ import { randomUUID as randomUUID3 } from "crypto";
3319
+ import * as fs7 from "fs/promises";
3320
+ import * as path10 from "path";
3024
3321
  function getPhase5FileSpecs() {
3025
3322
  return [
3026
3323
  {
@@ -3051,7 +3348,7 @@ function getPhase5FileSpecs() {
3051
3348
  path: getMachineIdPath(),
3052
3349
  // Random uuid, written as a bare string. Subsequent
3053
3350
  // activations see the file and skip re-randomizing.
3054
- defaultContent: randomUUID2()
3351
+ defaultContent: randomUUID3()
3055
3352
  },
3056
3353
  {
3057
3354
  path: getProfilesJsonPath(),
@@ -3065,19 +3362,19 @@ async function bootstrapPhase5Placeholders() {
3065
3362
  const result = { created: [], skipped: [], failed: [] };
3066
3363
  const home = getServicemeHome();
3067
3364
  try {
3068
- await fs6.mkdir(home, { recursive: true });
3365
+ await fs7.mkdir(home, { recursive: true });
3069
3366
  } catch (err) {
3070
3367
  result.failed.push({ path: home, reason: err.message });
3071
3368
  return result;
3072
3369
  }
3073
3370
  for (const spec of getPhase5FileSpecs()) {
3074
3371
  try {
3075
- await fs6.access(spec.path);
3372
+ await fs7.access(spec.path);
3076
3373
  result.skipped.push(spec.path);
3077
3374
  } catch {
3078
3375
  try {
3079
- await fs6.mkdir(path9.dirname(spec.path), { recursive: true });
3080
- await fs6.writeFile(spec.path, spec.defaultContent, "utf8");
3376
+ await fs7.mkdir(path10.dirname(spec.path), { recursive: true });
3377
+ await fs7.writeFile(spec.path, spec.defaultContent, "utf8");
3081
3378
  result.created.push(spec.path);
3082
3379
  } catch (writeErr) {
3083
3380
  result.failed.push({ path: spec.path, reason: writeErr.message });
@@ -3088,16 +3385,16 @@ async function bootstrapPhase5Placeholders() {
3088
3385
  }
3089
3386
 
3090
3387
  // src/project/projectTools.ts
3091
- import * as fs7 from "fs/promises";
3092
- import * as path10 from "path";
3388
+ import * as fs8 from "fs/promises";
3389
+ import * as path11 from "path";
3093
3390
  import {
3094
3391
  createServicemeError as createServicemeError7
3095
3392
  } from "@serviceme/devtools-protocol";
3096
3393
 
3097
3394
  // src/utils/fileUtils.ts
3098
3395
  import { constants, createWriteStream } from "fs";
3099
- import { access as access5, copyFile, lstat, mkdir as mkdir4, readdir as readdir3, rename as rename3, rm as rm3 } from "fs/promises";
3100
- import { dirname as dirname6, join as join7 } from "path";
3396
+ import { access as access5, copyFile, lstat, mkdir as mkdir5, readdir as readdir3, rename as rename4, rm as rm3 } from "fs/promises";
3397
+ import { dirname as dirname7, join as join8 } from "path";
3101
3398
  import yauzl from "yauzl";
3102
3399
  var unzipFile = (zipPath, dest) => {
3103
3400
  return new Promise((resolve3, reject) => {
@@ -3107,12 +3404,12 @@ var unzipFile = (zipPath, dest) => {
3107
3404
  zipfile.readEntry();
3108
3405
  zipfile.on("entry", (entry) => {
3109
3406
  if (/\/$/.test(entry.fileName)) {
3110
- void mkdir4(join7(dest, entry.fileName), { recursive: true }).then(() => {
3407
+ void mkdir5(join8(dest, entry.fileName), { recursive: true }).then(() => {
3111
3408
  zipfile.readEntry();
3112
3409
  }).catch(reject);
3113
3410
  } else {
3114
- const outputPath = join7(dest, entry.fileName);
3115
- void mkdir4(dirname6(outputPath), { recursive: true }).then(() => {
3411
+ const outputPath = join8(dest, entry.fileName);
3412
+ void mkdir5(dirname7(outputPath), { recursive: true }).then(() => {
3116
3413
  zipfile.openReadStream(
3117
3414
  entry,
3118
3415
  (streamError, readStream) => {
@@ -3157,10 +3454,10 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
3157
3454
  }
3158
3455
  await rm3(destPath, { recursive: true, force: true });
3159
3456
  }
3160
- await mkdir4(destPath, { recursive: true });
3457
+ await mkdir5(destPath, { recursive: true });
3161
3458
  const children = await readdir3(sourcePath);
3162
3459
  for (const child of children) {
3163
- await mergeEntry(join7(sourcePath, child), join7(destPath, child), overwrite);
3460
+ await mergeEntry(join8(sourcePath, child), join8(destPath, child), overwrite);
3164
3461
  }
3165
3462
  await rm3(sourcePath, { recursive: true, force: true });
3166
3463
  return;
@@ -3173,18 +3470,18 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
3173
3470
  await rm3(destPath, { recursive: true, force: true });
3174
3471
  }
3175
3472
  try {
3176
- await rename3(sourcePath, destPath);
3473
+ await rename4(sourcePath, destPath);
3177
3474
  } catch {
3178
3475
  await copyFile(sourcePath, destPath);
3179
3476
  await rm3(sourcePath, { recursive: true, force: true });
3180
3477
  }
3181
3478
  };
3182
3479
  var moveFiles = async (sourceDir, destDir, overwrite = false) => {
3183
- await mkdir4(destDir, { recursive: true });
3480
+ await mkdir5(destDir, { recursive: true });
3184
3481
  const files = await readdir3(sourceDir);
3185
3482
  for (const file of files) {
3186
- const sourceFile = join7(sourceDir, file);
3187
- const destFile = join7(destDir, file);
3483
+ const sourceFile = join8(sourceDir, file);
3484
+ const destFile = join8(destDir, file);
3188
3485
  if (!overwrite) {
3189
3486
  try {
3190
3487
  await access5(destFile, constants.F_OK);
@@ -3201,10 +3498,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
3201
3498
  var ProjectTools = class {
3202
3499
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
3203
3500
  await unzipFile(zipPath, tempExtractDir);
3204
- let sourceDir = path10.join(tempExtractDir, input.extractedDirName);
3501
+ let sourceDir = path11.join(tempExtractDir, input.extractedDirName);
3205
3502
  let actualDirName = input.extractedDirName;
3206
3503
  if (!await this.pathExists(sourceDir)) {
3207
- const entries = await fs7.readdir(tempExtractDir, { withFileTypes: true });
3504
+ const entries = await fs8.readdir(tempExtractDir, { withFileTypes: true });
3208
3505
  const directories = entries.filter(
3209
3506
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
3210
3507
  );
@@ -3216,7 +3513,7 @@ var ProjectTools = class {
3216
3513
  );
3217
3514
  if (selectedDirectory) {
3218
3515
  actualDirName = selectedDirectory;
3219
- sourceDir = path10.join(tempExtractDir, actualDirName);
3516
+ sourceDir = path11.join(tempExtractDir, actualDirName);
3220
3517
  } else if (directories.length === 0) {
3221
3518
  throw new Error(
3222
3519
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -3266,7 +3563,7 @@ var ProjectTools = class {
3266
3563
  } else {
3267
3564
  for (const scriptPath of scripts) {
3268
3565
  try {
3269
- await fs7.chmod(scriptPath, 493);
3566
+ await fs8.chmod(scriptPath, 493);
3270
3567
  updatedCount += 1;
3271
3568
  } catch {
3272
3569
  }
@@ -3314,7 +3611,7 @@ var ProjectTools = class {
3314
3611
  };
3315
3612
  }
3316
3613
  async ensurePresetManifest(workspacePath, preset) {
3317
- const presetManifestPath = path10.join(
3614
+ const presetManifestPath = path11.join(
3318
3615
  workspacePath,
3319
3616
  ".ms-scaffold",
3320
3617
  "presets",
@@ -3323,11 +3620,11 @@ var ProjectTools = class {
3323
3620
  if (await this.pathExists(presetManifestPath)) {
3324
3621
  return;
3325
3622
  }
3326
- const projectModePath = path10.join(workspacePath, ".ms-scaffold", "project-mode.json");
3623
+ const projectModePath = path11.join(workspacePath, ".ms-scaffold", "project-mode.json");
3327
3624
  if (!await this.pathExists(projectModePath)) {
3328
3625
  return;
3329
3626
  }
3330
- const projectModeRaw = await fs7.readFile(projectModePath, "utf8");
3627
+ const projectModeRaw = await fs8.readFile(projectModePath, "utf8");
3331
3628
  const projectMode = JSON.parse(projectModeRaw);
3332
3629
  const synthesizedPreset = {
3333
3630
  preset,
@@ -3339,8 +3636,8 @@ var ProjectTools = class {
3339
3636
  mergeManagedFiles: [],
3340
3637
  userOwnedPaths: []
3341
3638
  };
3342
- await fs7.mkdir(path10.dirname(presetManifestPath), { recursive: true });
3343
- await fs7.writeFile(
3639
+ await fs8.mkdir(path11.dirname(presetManifestPath), { recursive: true });
3640
+ await fs8.writeFile(
3344
3641
  presetManifestPath,
3345
3642
  `${JSON.stringify(synthesizedPreset, null, 2)}
3346
3643
  `,
@@ -3351,12 +3648,12 @@ var ProjectTools = class {
3351
3648
  const results = [];
3352
3649
  let entries;
3353
3650
  try {
3354
- entries = await fs7.readdir(dir, { withFileTypes: true });
3651
+ entries = await fs8.readdir(dir, { withFileTypes: true });
3355
3652
  } catch {
3356
3653
  return results;
3357
3654
  }
3358
3655
  for (const entry of entries) {
3359
- const fullPath = path10.join(dir, entry.name);
3656
+ const fullPath = path11.join(dir, entry.name);
3360
3657
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
3361
3658
  results.push(...await this.findScripts(fullPath, extensions));
3362
3659
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -3367,7 +3664,7 @@ var ProjectTools = class {
3367
3664
  }
3368
3665
  async pathExists(targetPath) {
3369
3666
  try {
3370
- await fs7.access(targetPath);
3667
+ await fs8.access(targetPath);
3371
3668
  return true;
3372
3669
  } catch {
3373
3670
  return false;
@@ -3384,7 +3681,7 @@ var ProjectTools = class {
3384
3681
  const matches = [];
3385
3682
  for (const directoryName of directoryNames) {
3386
3683
  if (await this.directoryMatchesProjectPattern(
3387
- path10.join(tempExtractDir, directoryName),
3684
+ path11.join(tempExtractDir, directoryName),
3388
3685
  projectFilePattern
3389
3686
  )) {
3390
3687
  matches.push(directoryName);
@@ -3396,7 +3693,7 @@ var ProjectTools = class {
3396
3693
  return null;
3397
3694
  }
3398
3695
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
3399
- const entries = await fs7.readdir(directoryPath);
3696
+ const entries = await fs8.readdir(directoryPath);
3400
3697
  if (projectFilePattern.includes("*")) {
3401
3698
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
3402
3699
  return entries.some((entry) => regex.test(entry));
@@ -3409,8 +3706,8 @@ function createProjectTools() {
3409
3706
  }
3410
3707
 
3411
3708
  // src/repo-manager/index.ts
3412
- import * as fs8 from "fs/promises";
3413
- import * as path11 from "path";
3709
+ import * as fs9 from "fs/promises";
3710
+ import * as path12 from "path";
3414
3711
 
3415
3712
  // src/repos/types.ts
3416
3713
  function isDefaultRepo(repo) {
@@ -3492,11 +3789,11 @@ var RepoManager = class {
3492
3789
  const exists = await this.pathExists(localPath);
3493
3790
  if (exists) {
3494
3791
  if (await this.isValidGitRepo(localPath)) continue;
3495
- await fs8.rm(localPath, { recursive: true, force: true });
3792
+ await fs9.rm(localPath, { recursive: true, force: true });
3496
3793
  }
3497
3794
  try {
3498
3795
  if (!this.skipClone) {
3499
- await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3796
+ await fs9.mkdir(path12.dirname(localPath), { recursive: true });
3500
3797
  await this.git.clone(repo.id, repo.url, localPath, repo.branch, true);
3501
3798
  }
3502
3799
  await this.store.updateRepo(repo.id, {
@@ -3531,10 +3828,10 @@ var RepoManager = class {
3531
3828
  const localPath = getRepoDir(repoId);
3532
3829
  const exists = await this.pathExists(localPath);
3533
3830
  if (exists && !await this.isValidGitRepo(localPath)) {
3534
- await fs8.rm(localPath, { recursive: true, force: true });
3831
+ await fs9.rm(localPath, { recursive: true, force: true });
3535
3832
  }
3536
3833
  if (!exists || !await this.pathExists(localPath)) {
3537
- await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3834
+ await fs9.mkdir(path12.dirname(localPath), { recursive: true });
3538
3835
  if (!this.skipClone) {
3539
3836
  await this.git.clone(proxyId, repo.url, localPath, repo.branch, useProxy);
3540
3837
  }
@@ -3634,7 +3931,7 @@ var RepoManager = class {
3634
3931
  let cloned = false;
3635
3932
  if (!this.skipClone) {
3636
3933
  const localPath = getRepoDir(id);
3637
- await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3934
+ await fs9.mkdir(path12.dirname(localPath), { recursive: true });
3638
3935
  await this.git.clone(userProxyId, url, localPath, branch, useProxy);
3639
3936
  cloned = true;
3640
3937
  }
@@ -3659,7 +3956,7 @@ var RepoManager = class {
3659
3956
  await this.store.removeUserRepo(repoId);
3660
3957
  const localPath = getRepoDir(repoId);
3661
3958
  try {
3662
- await fs8.rm(localPath, { recursive: true, force: true });
3959
+ await fs9.rm(localPath, { recursive: true, force: true });
3663
3960
  } catch (err) {
3664
3961
  if (err.code !== "ENOENT") throw err;
3665
3962
  }
@@ -3682,7 +3979,7 @@ var RepoManager = class {
3682
3979
  }
3683
3980
  /** Force-create the SERVICEME home directory tree (idempotent). */
3684
3981
  async ensureHome() {
3685
- await fs8.mkdir(getServicemeHome(), { recursive: true });
3982
+ await fs9.mkdir(getServicemeHome(), { recursive: true });
3686
3983
  }
3687
3984
  /**
3688
3985
  * Returns `true` when `p` contains a `.git` entry — i.e. it is an
@@ -3690,11 +3987,11 @@ var RepoManager = class {
3690
3987
  * (e.g. from an interrupted clone) return `false`.
3691
3988
  */
3692
3989
  async isValidGitRepo(p) {
3693
- return this.pathExists(path11.join(p, ".git"));
3990
+ return this.pathExists(path12.join(p, ".git"));
3694
3991
  }
3695
3992
  async pathExists(p) {
3696
3993
  try {
3697
- await fs8.stat(p);
3994
+ await fs9.stat(p);
3698
3995
  return true;
3699
3996
  } catch {
3700
3997
  return false;
@@ -3819,8 +4116,8 @@ function resolveDefaultRepoId(existing, existingIds) {
3819
4116
  }
3820
4117
 
3821
4118
  // src/repos/loader.ts
3822
- import * as fs9 from "fs/promises";
3823
- import * as path12 from "path";
4119
+ import * as fs10 from "fs/promises";
4120
+ import * as path13 from "path";
3824
4121
  import { z } from "zod";
3825
4122
  var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
3826
4123
  var repoIdSchema = z.string().min(1).max(64).regex(SAFE_REPO_ID_PATTERN, "repo id must match /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/");
@@ -3903,7 +4200,7 @@ var ReposLoader = class {
3903
4200
  this.configPath = options.configPath;
3904
4201
  this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
3905
4202
  this.randomSuffix = options.randomSuffix ?? defaultRandomSuffix;
3906
- this.fileSystem = options.fileSystem ?? fs9;
4203
+ this.fileSystem = options.fileSystem ?? fs10;
3907
4204
  }
3908
4205
  /** Absolute path of the file this loader reads/writes. */
3909
4206
  getConfigPath() {
@@ -3956,7 +4253,7 @@ var ReposLoader = class {
3956
4253
  */
3957
4254
  async save(config) {
3958
4255
  const validated = reposFileSchema.parse(config);
3959
- const dir = path12.dirname(this.configPath);
4256
+ const dir = path13.dirname(this.configPath);
3960
4257
  await this.fileSystem.mkdir(dir, { recursive: true });
3961
4258
  const serialized = `${JSON.stringify(validated, null, 2)}
3962
4259
  `;
@@ -3965,7 +4262,7 @@ var ReposLoader = class {
3965
4262
  try {
3966
4263
  await this.fileSystem.rename(tempPath, this.configPath);
3967
4264
  } catch (error) {
3968
- const unlink2 = this.fileSystem.unlink ?? fs9.unlink;
4265
+ const unlink2 = this.fileSystem.unlink ?? fs10.unlink;
3969
4266
  await unlink2(tempPath).catch(() => void 0);
3970
4267
  throw error;
3971
4268
  }
@@ -4025,7 +4322,7 @@ function narrowRepoConfig(repo) {
4025
4322
 
4026
4323
  // src/repos/store.ts
4027
4324
  import { EventEmitter as EventEmitter2 } from "events";
4028
- import * as fs10 from "fs";
4325
+ import * as fs11 from "fs";
4029
4326
  var ReposStore = class {
4030
4327
  constructor(options = {}) {
4031
4328
  this.config = null;
@@ -4034,7 +4331,7 @@ var ReposStore = class {
4034
4331
  this.reloadTimer = null;
4035
4332
  this.lastLoadResult = null;
4036
4333
  this.loader = options.loader ?? new ReposLoader({ configPath: "" });
4037
- this.fileSystem = options.fileSystem ?? { watch: fs10.watch };
4334
+ this.fileSystem = options.fileSystem ?? { watch: fs11.watch };
4038
4335
  this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
4039
4336
  this.debounceMs = options.debounceMs ?? 50;
4040
4337
  this.createFsWatcher = options.createFsWatcher ?? ((p, cb) => this.defaultCreateFsWatcher(p, cb));
@@ -4316,17 +4613,17 @@ async function bootstrapDefaults(store) {
4316
4613
  }
4317
4614
 
4318
4615
  // src/scheduled-tasks/daemon/DaemonLogger.ts
4319
- import * as fs11 from "fs";
4320
- import * as path13 from "path";
4616
+ import * as fs12 from "fs";
4617
+ import * as path14 from "path";
4321
4618
  var CONFIG_DIR = ".serviceme";
4322
4619
  var LOG_FILE = "scheduler.log";
4323
4620
  var MAX_LOG_SIZE = 1024 * 1024;
4324
4621
  var DaemonLogger = class {
4325
4622
  constructor(workspacePath, options = {}) {
4326
- this.logPath = options.logPath ?? path13.join(workspacePath, CONFIG_DIR, LOG_FILE);
4327
- const dir = path13.dirname(this.logPath);
4328
- if (!fs11.existsSync(dir)) {
4329
- fs11.mkdirSync(dir, { recursive: true });
4623
+ this.logPath = options.logPath ?? path14.join(workspacePath, CONFIG_DIR, LOG_FILE);
4624
+ const dir = path14.dirname(this.logPath);
4625
+ if (!fs12.existsSync(dir)) {
4626
+ fs12.mkdirSync(dir, { recursive: true });
4330
4627
  }
4331
4628
  }
4332
4629
  getLogPath() {
@@ -4337,16 +4634,16 @@ var DaemonLogger = class {
4337
4634
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
4338
4635
  `;
4339
4636
  this.rotateIfNeeded();
4340
- fs11.appendFileSync(this.logPath, line, "utf-8");
4637
+ fs12.appendFileSync(this.logPath, line, "utf-8");
4341
4638
  }
4342
4639
  rotateIfNeeded() {
4343
4640
  try {
4344
- const stats = fs11.statSync(this.logPath);
4641
+ const stats = fs12.statSync(this.logPath);
4345
4642
  if (stats.size > MAX_LOG_SIZE) {
4346
- const content = fs11.readFileSync(this.logPath, "utf-8");
4643
+ const content = fs12.readFileSync(this.logPath, "utf-8");
4347
4644
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
4348
4645
  if (halfIdx > 0) {
4349
- fs11.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
4646
+ fs12.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
4350
4647
  }
4351
4648
  }
4352
4649
  } catch {
@@ -4355,39 +4652,39 @@ var DaemonLogger = class {
4355
4652
  };
4356
4653
 
4357
4654
  // src/scheduled-tasks/daemon/PidManager.ts
4358
- import * as fs12 from "fs";
4359
- import * as path14 from "path";
4655
+ import * as fs13 from "fs";
4656
+ import * as path15 from "path";
4360
4657
  var CONFIG_DIR2 = ".serviceme";
4361
4658
  var PID_FILE = "scheduler.pid";
4362
4659
  var PidManager = class {
4363
4660
  constructor(workspacePath, options = {}) {
4364
- this.pidPath = options.pidPath ?? path14.join(workspacePath, CONFIG_DIR2, PID_FILE);
4661
+ this.pidPath = options.pidPath ?? path15.join(workspacePath, CONFIG_DIR2, PID_FILE);
4365
4662
  }
4366
4663
  getPidPath() {
4367
4664
  return this.pidPath;
4368
4665
  }
4369
4666
  writePid(pid) {
4370
- const dir = path14.dirname(this.pidPath);
4371
- if (!fs12.existsSync(dir)) {
4372
- fs12.mkdirSync(dir, { recursive: true });
4667
+ const dir = path15.dirname(this.pidPath);
4668
+ if (!fs13.existsSync(dir)) {
4669
+ fs13.mkdirSync(dir, { recursive: true });
4373
4670
  }
4374
- fs12.writeFileSync(this.pidPath, String(pid), "utf-8");
4671
+ fs13.writeFileSync(this.pidPath, String(pid), "utf-8");
4375
4672
  }
4376
4673
  readPid() {
4377
4674
  let stat5;
4378
4675
  try {
4379
- stat5 = fs12.statSync(this.pidPath);
4676
+ stat5 = fs13.statSync(this.pidPath);
4380
4677
  } catch {
4381
4678
  return null;
4382
4679
  }
4383
4680
  if (!stat5.isFile()) return null;
4384
- const raw = fs12.readFileSync(this.pidPath, "utf-8").trim();
4681
+ const raw = fs13.readFileSync(this.pidPath, "utf-8").trim();
4385
4682
  const pid = Number.parseInt(raw, 10);
4386
4683
  return Number.isNaN(pid) ? null : pid;
4387
4684
  }
4388
4685
  removePid() {
4389
- if (fs12.existsSync(this.pidPath)) {
4390
- fs12.unlinkSync(this.pidPath);
4686
+ if (fs13.existsSync(this.pidPath)) {
4687
+ fs13.unlinkSync(this.pidPath);
4391
4688
  }
4392
4689
  }
4393
4690
  isProcessRunning(pid) {
@@ -4408,13 +4705,13 @@ var PidManager = class {
4408
4705
  };
4409
4706
 
4410
4707
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
4411
- import * as fs17 from "fs";
4708
+ import * as fs18 from "fs";
4412
4709
  import * as os5 from "os";
4413
- import * as path18 from "path";
4710
+ import * as path19 from "path";
4414
4711
 
4415
4712
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
4416
4713
  import { spawn as spawn3 } from "child_process";
4417
- import * as fs13 from "fs";
4714
+ import * as fs14 from "fs";
4418
4715
 
4419
4716
  // src/scheduled-tasks/executors/timeout.ts
4420
4717
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -4449,7 +4746,7 @@ function redactArgs(args) {
4449
4746
  function writeDiagnostic(message) {
4450
4747
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
4451
4748
  if (logPath) {
4452
- fs13.appendFileSync(logPath, message);
4749
+ fs14.appendFileSync(logPath, message);
4453
4750
  return;
4454
4751
  }
4455
4752
  process.stderr.write(message);
@@ -4655,15 +4952,15 @@ ${body}`.trim()
4655
4952
 
4656
4953
  // src/scheduled-tasks/executors/ShellExecutor.ts
4657
4954
  import { spawn as spawn4 } from "child_process";
4658
- import * as fs14 from "fs";
4659
- import * as path15 from "path";
4955
+ import * as fs15 from "fs";
4956
+ import * as path16 from "path";
4660
4957
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
4661
4958
  var DEFAULT_TIMEOUT_MS4 = 6e4;
4662
4959
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
4663
4960
  function resolveShellExecution(script, options = {}) {
4664
4961
  const platform3 = options.platform ?? process.platform;
4665
4962
  const env = options.env ?? process.env;
4666
- const fileExists = options.fileExists ?? fs14.existsSync;
4963
+ const fileExists = options.fileExists ?? fs15.existsSync;
4667
4964
  if (platform3 === "win32") {
4668
4965
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
4669
4966
  if (posixShell) {
@@ -4708,10 +5005,10 @@ function findWindowsPosixShell(env, fileExists) {
4708
5005
  if (fileExists(candidate)) return candidate;
4709
5006
  }
4710
5007
  const pathValue = env.Path ?? env.PATH ?? "";
4711
- for (const dir of pathValue.split(path15.win32.delimiter)) {
5008
+ for (const dir of pathValue.split(path16.win32.delimiter)) {
4712
5009
  if (!dir) continue;
4713
5010
  for (const executable of POSIX_SHELL_CANDIDATES) {
4714
- const candidate = path15.win32.join(dir, executable);
5011
+ const candidate = path16.win32.join(dir, executable);
4715
5012
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
4716
5013
  return candidate;
4717
5014
  }
@@ -4720,13 +5017,13 @@ function findWindowsPosixShell(env, fileExists) {
4720
5017
  return null;
4721
5018
  }
4722
5019
  function isWindowsWslLauncher(candidate) {
4723
- const normalized = path15.win32.normalize(candidate).toLowerCase();
5020
+ const normalized = path16.win32.normalize(candidate).toLowerCase();
4724
5021
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
4725
5022
  }
4726
5023
  function writeDiagnostic2(message) {
4727
5024
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
4728
5025
  if (logPath) {
4729
- fs14.appendFileSync(logPath, message);
5026
+ fs15.appendFileSync(logPath, message);
4730
5027
  return;
4731
5028
  }
4732
5029
  process.stderr.write(message);
@@ -4868,10 +5165,10 @@ function getExecutor(taskType) {
4868
5165
  }
4869
5166
 
4870
5167
  // src/scheduled-tasks/TaskConfigManager.ts
4871
- import { randomUUID as randomUUID3 } from "crypto";
4872
- import * as fs15 from "fs";
5168
+ import { randomUUID as randomUUID4 } from "crypto";
5169
+ import * as fs16 from "fs";
4873
5170
  import * as os4 from "os";
4874
- import * as path16 from "path";
5171
+ import * as path17 from "path";
4875
5172
  import {
4876
5173
  createServicemeError as createServicemeError8,
4877
5174
  isScheduledTasksConfig,
@@ -4901,7 +5198,7 @@ function v1ContainerShape(value) {
4901
5198
  }
4902
5199
  function defaultWorkspaceContext() {
4903
5200
  const home = os4.homedir() || "/";
4904
- return { path: home, name: path16.basename(home) || home };
5201
+ return { path: home, name: path17.basename(home) || home };
4905
5202
  }
4906
5203
  function requireNonEmptyString(payload, field, taskType) {
4907
5204
  if (!isRecord2(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
@@ -4936,10 +5233,10 @@ var TaskConfigManager = class {
4936
5233
  return this.configPath;
4937
5234
  }
4938
5235
  readConfig() {
4939
- if (!fs15.existsSync(this.configPath)) {
5236
+ if (!fs16.existsSync(this.configPath)) {
4940
5237
  return emptyConfig();
4941
5238
  }
4942
- const raw = fs15.readFileSync(this.configPath, "utf-8");
5239
+ const raw = fs16.readFileSync(this.configPath, "utf-8");
4943
5240
  let parsed;
4944
5241
  try {
4945
5242
  parsed = JSON.parse(raw);
@@ -4992,7 +5289,7 @@ var TaskConfigManager = class {
4992
5289
  const target = this.migrationFailuresPath ?? getMigrationFailuresPath();
4993
5290
  const prior = (() => {
4994
5291
  try {
4995
- return JSON.parse(fs15.readFileSync(target, "utf-8"));
5292
+ return JSON.parse(fs16.readFileSync(target, "utf-8"));
4996
5293
  } catch {
4997
5294
  return [];
4998
5295
  }
@@ -5005,8 +5302,8 @@ var TaskConfigManager = class {
5005
5302
  snippet: raw.slice(0, 500),
5006
5303
  recordedAt: (/* @__PURE__ */ new Date()).toISOString()
5007
5304
  });
5008
- fs15.mkdirSync(path16.dirname(target), { recursive: true });
5009
- fs15.writeFileSync(target, JSON.stringify(failures, null, " "), "utf-8");
5305
+ fs16.mkdirSync(path17.dirname(target), { recursive: true });
5306
+ fs16.writeFileSync(target, JSON.stringify(failures, null, " "), "utf-8");
5010
5307
  } catch (writeError) {
5011
5308
  this.warn(
5012
5309
  `TaskConfigManager: also failed to write migration-failures log: ${String(writeError)}`
@@ -5014,13 +5311,13 @@ var TaskConfigManager = class {
5014
5311
  }
5015
5312
  }
5016
5313
  writeConfig(config) {
5017
- const dir = path16.dirname(this.configPath);
5018
- if (!fs15.existsSync(dir)) {
5019
- fs15.mkdirSync(dir, { recursive: true });
5314
+ const dir = path17.dirname(this.configPath);
5315
+ if (!fs16.existsSync(dir)) {
5316
+ fs16.mkdirSync(dir, { recursive: true });
5020
5317
  }
5021
5318
  const tmp = `${this.configPath}.tmp`;
5022
- fs15.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
5023
- fs15.renameSync(tmp, this.configPath);
5319
+ fs16.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
5320
+ fs16.renameSync(tmp, this.configPath);
5024
5321
  }
5025
5322
  listTasks() {
5026
5323
  return this.readConfig().tasks;
@@ -5042,7 +5339,7 @@ var TaskConfigManager = class {
5042
5339
  const config = this.readConfig();
5043
5340
  const now = (/* @__PURE__ */ new Date()).toISOString();
5044
5341
  const task = {
5045
- id: randomUUID3(),
5342
+ id: randomUUID4(),
5046
5343
  name: input.name,
5047
5344
  description: input.description,
5048
5345
  enabled: input.enabled ?? true,
@@ -5297,9 +5594,9 @@ var TaskExecutionEngine = class {
5297
5594
  };
5298
5595
 
5299
5596
  // src/scheduled-tasks/TaskLogManager.ts
5300
- import { randomUUID as randomUUID4 } from "crypto";
5301
- import * as fs16 from "fs";
5302
- import * as path17 from "path";
5597
+ import { randomUUID as randomUUID5 } from "crypto";
5598
+ import * as fs17 from "fs";
5599
+ import * as path18 from "path";
5303
5600
  var MAX_LOGS = 200;
5304
5601
  function emptyLogFile() {
5305
5602
  return { logs: [] };
@@ -5328,11 +5625,11 @@ var TaskLogManager = class {
5328
5625
  return this.logPath;
5329
5626
  }
5330
5627
  readLogFile() {
5331
- if (!fs16.existsSync(this.logPath)) {
5628
+ if (!fs17.existsSync(this.logPath)) {
5332
5629
  return emptyLogFile();
5333
5630
  }
5334
5631
  try {
5335
- const raw = fs16.readFileSync(this.logPath, "utf-8");
5632
+ const raw = fs17.readFileSync(this.logPath, "utf-8");
5336
5633
  const parsed = JSON.parse(raw);
5337
5634
  const file = validateAndRepairLogFile(parsed);
5338
5635
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -5348,26 +5645,26 @@ var TaskLogManager = class {
5348
5645
  }
5349
5646
  backupCorruptedFile() {
5350
5647
  try {
5351
- if (fs16.existsSync(this.logPath)) {
5648
+ if (fs17.existsSync(this.logPath)) {
5352
5649
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
5353
- fs16.copyFileSync(this.logPath, backupPath);
5650
+ fs17.copyFileSync(this.logPath, backupPath);
5354
5651
  }
5355
5652
  } catch {
5356
5653
  }
5357
5654
  }
5358
5655
  writeLogFile(file) {
5359
- const dir = path17.dirname(this.logPath);
5360
- if (!fs16.existsSync(dir)) {
5361
- fs16.mkdirSync(dir, { recursive: true });
5656
+ const dir = path18.dirname(this.logPath);
5657
+ if (!fs17.existsSync(dir)) {
5658
+ fs17.mkdirSync(dir, { recursive: true });
5362
5659
  }
5363
5660
  const tmp = `${this.logPath}.tmp`;
5364
- fs16.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
5365
- fs16.renameSync(tmp, this.logPath);
5661
+ fs17.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
5662
+ fs17.renameSync(tmp, this.logPath);
5366
5663
  }
5367
5664
  appendLog(input) {
5368
5665
  const file = this.readLogFile();
5369
5666
  const log = {
5370
- id: randomUUID4(),
5667
+ id: randomUUID5(),
5371
5668
  taskId: input.taskId,
5372
5669
  taskName: input.taskName,
5373
5670
  startedAt: input.startedAt,
@@ -5422,12 +5719,12 @@ var SchedulerDaemon = class {
5422
5719
  this.lastRun = /* @__PURE__ */ new Map();
5423
5720
  this.taskRunning = /* @__PURE__ */ new Set();
5424
5721
  this.workspacePath = workspacePath;
5425
- const configDir = path18.join(workspacePath, ".serviceme");
5722
+ const configDir = path19.join(workspacePath, ".serviceme");
5426
5723
  this.configManager = new TaskConfigManager({
5427
- configPath: path18.join(configDir, "scheduled-tasks.json")
5724
+ configPath: path19.join(configDir, "scheduled-tasks.json")
5428
5725
  });
5429
5726
  this.logManager = new TaskLogManager({
5430
- logPath: path18.join(configDir, "scheduled-tasks-log.json")
5727
+ logPath: path19.join(configDir, "scheduled-tasks-log.json")
5431
5728
  });
5432
5729
  this.pidManager = new PidManager(workspacePath);
5433
5730
  this.logger = new DaemonLogger(workspacePath);
@@ -5479,8 +5776,8 @@ var SchedulerDaemon = class {
5479
5776
  const configPath = this.configManager.getConfigPath();
5480
5777
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
5481
5778
  try {
5482
- if (fs17.existsSync(dir)) {
5483
- this.watcher = fs17.watch(dir, (_eventType, filename) => {
5779
+ if (fs18.existsSync(dir)) {
5780
+ this.watcher = fs18.watch(dir, (_eventType, filename) => {
5484
5781
  if (filename === "scheduled-tasks.json") {
5485
5782
  this.logger.log("info", "Config file changed, reconciling...");
5486
5783
  }
@@ -5633,9 +5930,9 @@ function matchCronField(field, value) {
5633
5930
  }
5634
5931
 
5635
5932
  // src/scheduled-tasks/daemon/SchedulerDaemonV2.ts
5636
- import * as fs18 from "fs";
5933
+ import * as fs19 from "fs";
5637
5934
  import * as os6 from "os";
5638
- import * as path19 from "path";
5935
+ import * as path20 from "path";
5639
5936
  var TICK_INTERVAL2 = 1e3;
5640
5937
  var MIN_SCHEDULE_INTERVAL2 = 1e3;
5641
5938
  var SCHEDULER_LOG_FILENAME2 = "scheduler.log";
@@ -5655,7 +5952,7 @@ var SchedulerDaemonV2 = class {
5655
5952
  this.logManager = options.logManager ?? new TaskLogManager();
5656
5953
  this.pidManager = options.pidManager ?? new PidManager("", { pidPath: getSchedulerPidPath() });
5657
5954
  this.logger = options.logger ?? new DaemonLogger(os6.homedir(), {
5658
- logPath: path19.join(path19.dirname(this.pidManager.getPidPath()), SCHEDULER_LOG_FILENAME2)
5955
+ logPath: path20.join(path20.dirname(this.pidManager.getPidPath()), SCHEDULER_LOG_FILENAME2)
5659
5956
  });
5660
5957
  this.getExecutor = options.getExecutor ?? getExecutor;
5661
5958
  this.tryAcquireLock = options.tryAcquireLock ?? (() => true);
@@ -5713,7 +6010,7 @@ var SchedulerDaemonV2 = class {
5713
6010
  const now = Date.now();
5714
6011
  for (const task of config.tasks) {
5715
6012
  if (!task.enabled) continue;
5716
- if (!fs18.existsSync(task.workspace.path)) {
6013
+ if (!fs19.existsSync(task.workspace.path)) {
5717
6014
  this.disableTaskForMissingWorkspace(task, config);
5718
6015
  continue;
5719
6016
  }
@@ -5870,21 +6167,21 @@ function matchCronField2(field, value) {
5870
6167
  }
5871
6168
 
5872
6169
  // src/scheduled-tasks/migration/MigrateToGlobal.ts
5873
- import * as fs19 from "fs";
5874
- import * as path20 from "path";
6170
+ import * as fs20 from "fs";
6171
+ import * as path21 from "path";
5875
6172
  import { isScheduledTasksConfigV1 as isScheduledTasksConfigV12, migrateV1ToV2 as migrateV1ToV22 } from "@serviceme/devtools-protocol";
5876
6173
  var WORKSPACE_DIR = ".serviceme";
5877
6174
  var V1_FILENAME = "scheduled-tasks.json";
5878
6175
  function defaultProbe(workspacePath) {
5879
6176
  return {
5880
6177
  path: workspacePath,
5881
- name: path20.basename(workspacePath) || workspacePath
6178
+ name: path21.basename(workspacePath) || workspacePath
5882
6179
  };
5883
6180
  }
5884
6181
  function readV1Config(v1Path) {
5885
6182
  let raw;
5886
6183
  try {
5887
- raw = fs19.readFileSync(v1Path, "utf-8");
6184
+ raw = fs20.readFileSync(v1Path, "utf-8");
5888
6185
  } catch (err) {
5889
6186
  return {
5890
6187
  ok: false,
@@ -5907,27 +6204,27 @@ function readV1Config(v1Path) {
5907
6204
  }
5908
6205
  function safeDelete(filePath) {
5909
6206
  try {
5910
- fs19.unlinkSync(filePath);
6207
+ fs20.unlinkSync(filePath);
5911
6208
  } catch {
5912
6209
  }
5913
6210
  }
5914
6211
  function ensureDir(filePath) {
5915
- const dir = path20.dirname(filePath);
5916
- if (!fs19.existsSync(dir)) {
5917
- fs19.mkdirSync(dir, { recursive: true });
6212
+ const dir = path21.dirname(filePath);
6213
+ if (!fs20.existsSync(dir)) {
6214
+ fs20.mkdirSync(dir, { recursive: true });
5918
6215
  }
5919
6216
  }
5920
6217
  function readJsonFile(filePath) {
5921
- if (!fs19.existsSync(filePath)) return null;
6218
+ if (!fs20.existsSync(filePath)) return null;
5922
6219
  try {
5923
- return JSON.parse(fs19.readFileSync(filePath, "utf-8"));
6220
+ return JSON.parse(fs20.readFileSync(filePath, "utf-8"));
5924
6221
  } catch {
5925
6222
  return null;
5926
6223
  }
5927
6224
  }
5928
6225
  function writeJsonFile(filePath, data) {
5929
6226
  ensureDir(filePath);
5930
- fs19.writeFileSync(filePath, JSON.stringify(data, null, " "), "utf-8");
6227
+ fs20.writeFileSync(filePath, JSON.stringify(data, null, " "), "utf-8");
5931
6228
  }
5932
6229
  function disambiguateName(task, existingNames, workspaceName) {
5933
6230
  if (!existingNames.has(task.name)) {
@@ -5957,8 +6254,8 @@ async function migrateToGlobal(options) {
5957
6254
  const conflicts = [];
5958
6255
  const issues = [];
5959
6256
  for (const workspacePath of options.workspacePaths) {
5960
- const v1Path = path20.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
5961
- if (!fs19.existsSync(v1Path)) continue;
6257
+ const v1Path = path21.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
6258
+ if (!fs20.existsSync(v1Path)) continue;
5962
6259
  const v1 = readV1Config(v1Path);
5963
6260
  if (!v1.ok) {
5964
6261
  failures.push({
@@ -6000,8 +6297,8 @@ async function migrateToGlobal(options) {
6000
6297
  if (migrated > 0) {
6001
6298
  ensureDir(globalConfigPath);
6002
6299
  const tmp = `${globalConfigPath}.tmp`;
6003
- fs19.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6004
- fs19.renameSync(tmp, globalConfigPath);
6300
+ fs20.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6301
+ fs20.renameSync(tmp, globalConfigPath);
6005
6302
  }
6006
6303
  if (failures.length > priorFailures.length) {
6007
6304
  writeJsonFile(migrationFailuresPath, failures);
@@ -6018,8 +6315,8 @@ async function migrateToGlobal(options) {
6018
6315
 
6019
6316
  // src/scheduled-tasks/workspace-probe/WorkspaceProbe.ts
6020
6317
  import { spawn as spawn5 } from "child_process";
6021
- import * as fs20 from "fs";
6022
- import * as path21 from "path";
6318
+ import * as fs21 from "fs";
6319
+ import * as path22 from "path";
6023
6320
  var DEFAULT_TIMEOUT_MS5 = 2e3;
6024
6321
  var GitTimeoutError = class extends Error {
6025
6322
  constructor() {
@@ -6082,8 +6379,8 @@ var WorkspaceProbe = class {
6082
6379
  }
6083
6380
  }
6084
6381
  async probe(workspacePath) {
6085
- const name = path21.basename(workspacePath) || workspacePath;
6086
- if (!workspacePath || !fs20.existsSync(workspacePath)) {
6382
+ const name = path22.basename(workspacePath) || workspacePath;
6383
+ if (!workspacePath || !fs21.existsSync(workspacePath)) {
6087
6384
  return {
6088
6385
  workspace: { path: workspacePath, name },
6089
6386
  error: "path-not-found"
@@ -6235,8 +6532,8 @@ var SkillReconciler = class {
6235
6532
  };
6236
6533
 
6237
6534
  // src/skills/SkillStore.ts
6238
- import * as fs21 from "fs/promises";
6239
- import * as path22 from "path";
6535
+ import * as fs22 from "fs/promises";
6536
+ import * as path23 from "path";
6240
6537
  var USER_SKILL_MARKER_FILE = ".serviceme-skill.json";
6241
6538
  var LEGACY_USER_SKILL_MARKER_FILE = ".ms-devtools-skill.json";
6242
6539
  var WORKSPACE_SKILLS_ROOT_RELATIVE = ".github/skills";
@@ -6252,7 +6549,7 @@ var SkillStore = class {
6252
6549
  constructor(options) {
6253
6550
  this.workspacePath = options.workspacePath;
6254
6551
  this.userSkillsRoot = options.userSkillsRoot;
6255
- this.fileSystem = options.fileSystem ?? fs21;
6552
+ this.fileSystem = options.fileSystem ?? fs22;
6256
6553
  }
6257
6554
  normalizeRemoteSkillId(remoteId) {
6258
6555
  if (remoteId.startsWith("official/")) {
@@ -6271,10 +6568,10 @@ var SkillStore = class {
6271
6568
  return WORKSPACE_SKILLS_MARKER_RELATIVE;
6272
6569
  }
6273
6570
  getUserSkillPath(skillId) {
6274
- return path22.join(this.userSkillsRoot, skillId);
6571
+ return path23.join(this.userSkillsRoot, skillId);
6275
6572
  }
6276
6573
  async listWorkspaceSkillIds() {
6277
- const skillsRootPath = path22.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE);
6574
+ const skillsRootPath = path23.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE);
6278
6575
  try {
6279
6576
  const entries = await this.fileSystem.readdir(skillsRootPath, {
6280
6577
  withFileTypes: true
@@ -6298,7 +6595,7 @@ var SkillStore = class {
6298
6595
  const targetDir = this.getUserSkillPath(skillId);
6299
6596
  await this.fileSystem.mkdir(targetDir, { recursive: true });
6300
6597
  await this.fileSystem.writeFile(
6301
- path22.join(targetDir, USER_SKILL_MARKER_FILE),
6598
+ path23.join(targetDir, USER_SKILL_MARKER_FILE),
6302
6599
  JSON.stringify({ skillId, installedBy: "serviceme" }, null, 2),
6303
6600
  "utf-8"
6304
6601
  );
@@ -6307,7 +6604,7 @@ var SkillStore = class {
6307
6604
  await this.migrateLegacyUserSkillMarker(skillId);
6308
6605
  try {
6309
6606
  const marker = await this.fileSystem.readFile(
6310
- path22.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
6607
+ path23.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
6311
6608
  "utf-8"
6312
6609
  );
6313
6610
  const parsed = JSON.parse(marker);
@@ -6324,8 +6621,8 @@ var SkillStore = class {
6324
6621
  */
6325
6622
  async migrateLegacyUserSkillMarker(skillId) {
6326
6623
  const targetDir = this.getUserSkillPath(skillId);
6327
- const newPath = path22.join(targetDir, USER_SKILL_MARKER_FILE);
6328
- const legacyPath = path22.join(targetDir, LEGACY_USER_SKILL_MARKER_FILE);
6624
+ const newPath = path23.join(targetDir, USER_SKILL_MARKER_FILE);
6625
+ const legacyPath = path23.join(targetDir, LEGACY_USER_SKILL_MARKER_FILE);
6329
6626
  try {
6330
6627
  await this.fileSystem.readFile(newPath, "utf-8");
6331
6628
  return;
@@ -6338,12 +6635,12 @@ var SkillStore = class {
6338
6635
  }
6339
6636
  }
6340
6637
  async writeSkillFiles(skillId, scope, files) {
6341
- const root = scope === "workspace" ? path22.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE) : this.userSkillsRoot;
6342
- const targetDir = path22.join(root, skillId);
6638
+ const root = scope === "workspace" ? path23.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE) : this.userSkillsRoot;
6639
+ const targetDir = path23.join(root, skillId);
6343
6640
  await this.fileSystem.mkdir(targetDir, { recursive: true });
6344
6641
  for (const file of files) {
6345
- const filePath = path22.join(targetDir, file.path);
6346
- await this.fileSystem.mkdir(path22.dirname(filePath), { recursive: true });
6642
+ const filePath = path23.join(targetDir, file.path);
6643
+ await this.fileSystem.mkdir(path23.dirname(filePath), { recursive: true });
6347
6644
  await this.fileSystem.writeFile(filePath, file.content, "utf-8");
6348
6645
  if (file.executable) {
6349
6646
  try {
@@ -6356,8 +6653,8 @@ var SkillStore = class {
6356
6653
  };
6357
6654
 
6358
6655
  // src/submit/index.ts
6359
- import * as fs22 from "fs/promises";
6360
- import * as path23 from "path";
6656
+ import * as fs23 from "fs/promises";
6657
+ import * as path24 from "path";
6361
6658
 
6362
6659
  // src/submit/types.ts
6363
6660
  var SubmitError = class extends Error {
@@ -6407,14 +6704,14 @@ var SubmitClient = class {
6407
6704
  throw new SubmitError(v.reason ?? "unknown", v.detail ?? "validation denied");
6408
6705
  }
6409
6706
  const localRepoPath = getRepoDir(repoId);
6410
- const targetDir = path23.join(localRepoPath, "skills", skillName);
6411
- await fs22.mkdir(targetDir, { recursive: true });
6707
+ const targetDir = path24.join(localRepoPath, "skills", skillName);
6708
+ await fs23.mkdir(targetDir, { recursive: true });
6412
6709
  for (const f of files) {
6413
- const full = path23.join(targetDir, f.path);
6414
- await fs22.mkdir(path23.dirname(full), { recursive: true });
6710
+ const full = path24.join(targetDir, f.path);
6711
+ await fs23.mkdir(path24.dirname(full), { recursive: true });
6415
6712
  const tmp = `${full}.${process.pid}.${Date.now()}.tmp`;
6416
- await fs22.writeFile(tmp, f.content, "utf8");
6417
- await fs22.rename(tmp, full);
6713
+ await fs23.writeFile(tmp, f.content, "utf8");
6714
+ await fs23.rename(tmp, full);
6418
6715
  }
6419
6716
  const commitMessage = `feat(skills): add ${skillName}`;
6420
6717
  const { commitSha } = await this.git.commit(localRepoPath, commitMessage);
@@ -6483,7 +6780,7 @@ function touchLastUsedAt(tools, id, when = /* @__PURE__ */ new Date()) {
6483
6780
 
6484
6781
  // src/toolbox/ToolboxStore.ts
6485
6782
  import * as fsp2 from "fs/promises";
6486
- import * as path24 from "path";
6783
+ import * as path25 from "path";
6487
6784
  import { setTimeout as delay2 } from "timers/promises";
6488
6785
 
6489
6786
  // src/toolbox/types.ts
@@ -6518,11 +6815,11 @@ var LOCK_DIR_MODE2 = 448;
6518
6815
  var DEFAULT_LOCK_TIMEOUT_MS2 = 5e3;
6519
6816
  var DEFAULT_LOCK_RETRY_MS2 = 25;
6520
6817
  var TMP_SUFFIX2 = ".tmp";
6521
- var WORKSPACE_TOOLBOX_RELATIVE_PATH = path24.join(".github", ".serviceme-toolbox.json");
6818
+ var WORKSPACE_TOOLBOX_RELATIVE_PATH = path25.join(".github", ".serviceme-toolbox.json");
6522
6819
  var LEGACY_WORKSPACE_TOOLBOX_FILENAME = ".ms-devtools-toolbox.json";
6523
6820
  async function migrateLegacyWorkspaceToolboxFile(filePath) {
6524
6821
  if (!filePath) return;
6525
- const legacyPath = path24.join(path24.dirname(filePath), LEGACY_WORKSPACE_TOOLBOX_FILENAME);
6822
+ const legacyPath = path25.join(path25.dirname(filePath), LEGACY_WORKSPACE_TOOLBOX_FILENAME);
6526
6823
  if (legacyPath === filePath) return;
6527
6824
  try {
6528
6825
  await fsp2.access(filePath);
@@ -6567,7 +6864,7 @@ var FsToolboxFileBackend = class {
6567
6864
  }
6568
6865
  }
6569
6866
  async write(filePath, payload) {
6570
- await fsp2.mkdir(path24.dirname(filePath), { recursive: true });
6867
+ await fsp2.mkdir(path25.dirname(filePath), { recursive: true });
6571
6868
  const tmpPath = `${filePath}${TMP_SUFFIX2}`;
6572
6869
  const bytes = Buffer.from(JSON.stringify(payload, null, " "), "utf8");
6573
6870
  await fsp2.rm(tmpPath, { force: true });
@@ -6599,10 +6896,19 @@ function coercePersistedToolbox(parsed) {
6599
6896
  function isNodeError2(value) {
6600
6897
  return value instanceof Error && typeof value.code === "string";
6601
6898
  }
6899
+ function isProcessAlive2(pid) {
6900
+ try {
6901
+ process.kill(pid, 0);
6902
+ return true;
6903
+ } catch {
6904
+ return false;
6905
+ }
6906
+ }
6602
6907
  var ToolboxFileLock = class {
6603
6908
  constructor(filePath, timeoutMs, retryMs) {
6604
6909
  this.acquired = false;
6605
6910
  this.dirPath = `${filePath}.lock`;
6911
+ this.pidFilePath = path25.join(this.dirPath, "pid");
6606
6912
  this.timeoutMs = timeoutMs;
6607
6913
  this.retryMs = retryMs;
6608
6914
  }
@@ -6611,10 +6917,16 @@ var ToolboxFileLock = class {
6611
6917
  while (true) {
6612
6918
  try {
6613
6919
  await fsp2.mkdir(this.dirPath, { mode: LOCK_DIR_MODE2 });
6920
+ await fsp2.writeFile(this.pidFilePath, String(process.pid), "utf8").catch(() => void 0);
6614
6921
  this.acquired = true;
6615
6922
  return;
6616
6923
  } catch (err) {
6617
6924
  if (!isNodeError2(err) || err.code !== "EEXIST") throw err;
6925
+ const stale = await this.isStaleLock();
6926
+ if (stale) {
6927
+ await fsp2.rm(this.dirPath, { recursive: true, force: true });
6928
+ continue;
6929
+ }
6618
6930
  if (Date.now() - start >= this.timeoutMs) {
6619
6931
  throw new Error(`ToolboxStore lock acquisition timed out for ${this.dirPath}`);
6620
6932
  }
@@ -6622,6 +6934,16 @@ var ToolboxFileLock = class {
6622
6934
  }
6623
6935
  }
6624
6936
  }
6937
+ async isStaleLock() {
6938
+ try {
6939
+ const pidStr = await fsp2.readFile(this.pidFilePath, "utf8");
6940
+ const pid = Number.parseInt(pidStr.trim(), 10);
6941
+ if (!Number.isFinite(pid) || pid <= 0) return true;
6942
+ return !isProcessAlive2(pid);
6943
+ } catch {
6944
+ return true;
6945
+ }
6946
+ }
6625
6947
  async release() {
6626
6948
  if (!this.acquired) return;
6627
6949
  this.acquired = false;
@@ -6630,7 +6952,7 @@ var ToolboxFileLock = class {
6630
6952
  };
6631
6953
  function defaultWorkspacePath() {
6632
6954
  if (process.env.SERVICEME_NO_WORKSPACE_TOOLBOX === "1") return null;
6633
- return path24.join(process.cwd(), WORKSPACE_TOOLBOX_RELATIVE_PATH);
6955
+ return path25.join(process.cwd(), WORKSPACE_TOOLBOX_RELATIVE_PATH);
6634
6956
  }
6635
6957
  var ToolboxStore = class {
6636
6958
  constructor(opts = {}) {
@@ -6937,6 +7259,7 @@ export {
6937
7259
  SCHEDULER_LOCK_FILENAME,
6938
7260
  SCHEDULER_LOG_FILENAME,
6939
7261
  SCHEDULER_PID_FILENAME,
7262
+ SERVER_PROXY_GLOBAL_FILENAME,
6940
7263
  SERVICEME_DIR_NAME,
6941
7264
  SERVICEME_HOME_ENV,
6942
7265
  SKILL_DRAFTS_SUBDIR,
@@ -7004,6 +7327,7 @@ export {
7004
7327
  getSchedulerLockPath,
7005
7328
  getSchedulerLogPath,
7006
7329
  getSchedulerPidPath,
7330
+ getServerProxyGlobalPath,
7007
7331
  getServicemeHome,
7008
7332
  getSkillDraftsDir,
7009
7333
  getToolboxJsonPath,
@@ -7014,6 +7338,7 @@ export {
7014
7338
  isUserRepo,
7015
7339
  matchesCron,
7016
7340
  mergeWithDefaults,
7341
+ migrateLegacyServerProxyEnabled,
7017
7342
  migrateToGlobal,
7018
7343
  moveFiles,
7019
7344
  narrowRepoConfig,
@@ -7021,6 +7346,7 @@ export {
7021
7346
  parseAgentToolPermissions,
7022
7347
  parseIntervalMs,
7023
7348
  randomInstallationId,
7349
+ readServerProxyGlobal,
7024
7350
  reindexOrder,
7025
7351
  reposFileSchema,
7026
7352
  resetUserHomeOverrides,
@@ -7035,6 +7361,7 @@ export {
7035
7361
  unzipFile,
7036
7362
  userRepoSchema as userRepoConfigSchema,
7037
7363
  validateReposFile,
7038
- validateTaskPayload
7364
+ validateTaskPayload,
7365
+ writeServerProxyGlobal
7039
7366
  };
7040
7367
  //# sourceMappingURL=index.mjs.map