@serviceme/devtools-core 0.2.0 → 0.3.0

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
@@ -75,7 +75,6 @@ __export(src_exports, {
75
75
  MIGRATION_FAILURES_FILENAME: () => MIGRATION_FAILURES_FILENAME,
76
76
  MicrosoftAuthProvider: () => MicrosoftAuthProvider,
77
77
  MicrosoftProviderNotHostedError: () => MicrosoftProviderNotHostedError,
78
- MigrateToGlobal: () => MigrateToGlobal,
79
78
  NodeGitSpawner: () => NodeGitSpawner,
80
79
  PROFILES_JSON_FILENAME: () => PROFILES_JSON_FILENAME,
81
80
  PidManager: () => PidManager,
@@ -174,6 +173,7 @@ __export(src_exports, {
174
173
  isUserRepo: () => isUserRepo,
175
174
  matchesCron: () => matchesCron,
176
175
  mergeWithDefaults: () => mergeWithDefaults,
176
+ migrateToGlobal: () => migrateToGlobal,
177
177
  moveFiles: () => moveFiles,
178
178
  narrowRepoConfig: () => narrowRepoConfig,
179
179
  noopLogger: () => noopLogger,
@@ -1035,6 +1035,11 @@ var DEFAULT_DEVICE_CODE_URL = "https://github.com/login/device/code";
1035
1035
  var DEFAULT_TOKEN_URL = "https://github.com/login/oauth/access_token";
1036
1036
  var DEFAULT_USER_URL = "https://api.github.com/user";
1037
1037
  var DEFAULT_SCOPE = "read:user user:email";
1038
+ var DEVICE_CODE_MAX_ATTEMPTS = 3;
1039
+ var DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS = 500;
1040
+ function isTransientNetworkError(error) {
1041
+ return error instanceof TypeError;
1042
+ }
1038
1043
  var GitHubAuthProvider = class {
1039
1044
  constructor(config) {
1040
1045
  this.providerId = "github";
@@ -1050,7 +1055,8 @@ var GitHubAuthProvider = class {
1050
1055
  minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
1051
1056
  maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
1052
1057
  maxWaitMs: config.maxWaitMs,
1053
- fetchImpl: config.fetchImpl ?? fetch
1058
+ fetchImpl: config.fetchImpl ?? fetch,
1059
+ deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
1054
1060
  };
1055
1061
  }
1056
1062
  async requestDeviceFlow(opts) {
@@ -1058,29 +1064,43 @@ var GitHubAuthProvider = class {
1058
1064
  client_id: this.cfg.clientId,
1059
1065
  scope: opts?.scope ?? this.cfg.scope
1060
1066
  });
1061
- const resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
1062
- method: "POST",
1063
- headers: {
1064
- Accept: "application/json",
1065
- "Content-Type": "application/json",
1066
- "User-Agent": "serviceme-core"
1067
- },
1068
- body
1069
- });
1070
- if (!resp.ok) {
1071
- throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
1072
- }
1073
- const data = await resp.json();
1074
- if (!data.device_code || !data.user_code || !data.verification_uri) {
1075
- throw new Error("GitHub device-code response missing required fields");
1067
+ let lastNetworkError;
1068
+ for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
1069
+ let resp;
1070
+ try {
1071
+ resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
1072
+ method: "POST",
1073
+ headers: {
1074
+ Accept: "application/json",
1075
+ "Content-Type": "application/json",
1076
+ "User-Agent": "serviceme-core"
1077
+ },
1078
+ body
1079
+ });
1080
+ } catch (error) {
1081
+ if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
1082
+ throw error;
1083
+ }
1084
+ lastNetworkError = error;
1085
+ await sleep(this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1));
1086
+ continue;
1087
+ }
1088
+ if (!resp.ok) {
1089
+ throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
1090
+ }
1091
+ const data = await resp.json();
1092
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
1093
+ throw new Error("GitHub device-code response missing required fields");
1094
+ }
1095
+ return {
1096
+ provider: this.providerId,
1097
+ userCode: data.user_code,
1098
+ verificationUrl: data.verification_uri,
1099
+ expiresAt: Date.now() + data.expires_in * 1e3,
1100
+ message: `Open ${data.verification_uri} and enter ${data.user_code}`
1101
+ };
1076
1102
  }
1077
- return {
1078
- provider: this.providerId,
1079
- userCode: data.user_code,
1080
- verificationUrl: data.verification_uri,
1081
- expiresAt: Date.now() + data.expires_in * 1e3,
1082
- message: `Open ${data.verification_uri} and enter ${data.user_code}`
1083
- };
1103
+ throw lastNetworkError ?? new Error("GitHub device-code request failed");
1084
1104
  }
1085
1105
  async completeDeviceFlow(deviceCode, shouldContinue) {
1086
1106
  const start = Date.now();
@@ -1098,19 +1118,28 @@ var GitHubAuthProvider = class {
1098
1118
  if (shouldContinue && !shouldContinue()) {
1099
1119
  throw new Error("GitHub device flow cancelled by caller");
1100
1120
  }
1101
- const resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
1102
- method: "POST",
1103
- headers: {
1104
- Accept: "application/json",
1105
- "Content-Type": "application/json",
1106
- "User-Agent": "serviceme-core"
1107
- },
1108
- body: JSON.stringify({
1109
- client_id: this.cfg.clientId,
1110
- device_code: deviceCode,
1111
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1112
- })
1113
- });
1121
+ let resp;
1122
+ try {
1123
+ resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
1124
+ method: "POST",
1125
+ headers: {
1126
+ Accept: "application/json",
1127
+ "Content-Type": "application/json",
1128
+ "User-Agent": "serviceme-core"
1129
+ },
1130
+ body: JSON.stringify({
1131
+ client_id: this.cfg.clientId,
1132
+ device_code: deviceCode,
1133
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
1134
+ })
1135
+ });
1136
+ } catch (error) {
1137
+ if (!isTransientNetworkError(error)) {
1138
+ throw error;
1139
+ }
1140
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1141
+ continue;
1142
+ }
1114
1143
  if (!resp.ok) {
1115
1144
  pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1116
1145
  continue;
@@ -2284,14 +2313,7 @@ function extractFrontmatter(raw) {
2284
2313
  }
2285
2314
  return { data, body };
2286
2315
  }
2287
- var SKIP_DIRS = /* @__PURE__ */ new Set([
2288
- ".git",
2289
- "node_modules",
2290
- ".vscode",
2291
- "dist",
2292
- "build",
2293
- "out"
2294
- ]);
2316
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".vscode", "dist", "build", "out"]);
2295
2317
  var SKIP_TOP_LEVEL_SCAN_DIRS = /* @__PURE__ */ new Set([...SKIP_DIRS, "plugins"]);
2296
2318
 
2297
2319
  // src/drafts/index.ts
@@ -2708,54 +2730,82 @@ var GitClient = class {
2708
2730
  * Rewrite an upstream URL to its proxy form.
2709
2731
  *
2710
2732
  * Examples:
2711
- * rewriteRemoteUrl("medalsoftchina-ms-skills",
2733
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ",
2712
2734
  * "https://github.com/medalsoftchina/ms-skills.git")
2713
- * → "http://localhost:3000/git-proxy/medalsoftchina-ms-skills"
2735
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ"
2714
2736
  *
2715
- * rewriteRemoteUrl("my-team-internal",
2737
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0",
2716
2738
  * "git@github.com:medalsoftchina/ms-skills.git")
2717
- * → "http://localhost:3000/git-proxy/my-team-internal"
2739
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0"
2718
2740
  *
2719
2741
  * The original URL's host/owner is intentionally DROPPED — the proxy
2720
2742
  * knows the upstream from the per-repo config, not from the URL. This
2721
- * means callers can't accidentally route a user-repo URL through the
2722
- * default-repo proxy slot.
2743
+ * means callers can route both default and user repos through the same
2744
+ * dynamic proxy-id scheme.
2723
2745
  */
2724
- rewriteRemoteUrl(_repoId, _originalUrl) {
2746
+ rewriteRemoteUrl(_repoId, originalUrl, useProxy = true) {
2747
+ if (!useProxy) {
2748
+ return originalUrl;
2749
+ }
2725
2750
  return `${this.serverProxyBase}/${_repoId}`;
2726
2751
  }
2727
2752
  /**
2728
2753
  * `git clone <proxyUrl> <localPath>` — initialize a new local repo
2729
2754
  * from the proxy. Returns when the clone succeeds; throws on failure.
2755
+ *
2756
+ * When `branch` is provided, the clone is restricted to that branch
2757
+ * via `git clone -b <branch> -- <proxyUrl> <localPath>` so the local
2758
+ * checkout lands on the configured `repo.branch` instead of the
2759
+ * upstream default branch. The parameter is optional and backward
2760
+ * compatible — omitting it keeps the original behaviour.
2730
2761
  */
2731
- async clone(repoId, originalUrl, localPath) {
2732
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2733
- const args = ["clone", "--", proxyUrl, localPath];
2762
+ async clone(repoId, originalUrl, localPath, branch, useProxy = true) {
2763
+ const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl, useProxy);
2764
+ const args = branch ? ["clone", "-b", branch, "--", proxyUrl, localPath] : ["clone", "--", proxyUrl, localPath];
2734
2765
  await this.runOrThrow(args, { cwd: process.cwd() });
2735
2766
  }
2736
2767
  /**
2737
2768
  * `git fetch <remote> <branch>` + return the resulting commit SHA.
2738
2769
  * Operates on an already-cloned repo at `localPath`.
2770
+ *
2771
+ * When `branch` is provided, the pull target is forced to that branch:
2772
+ * if the local working tree is on a different branch, `git checkout
2773
+ * <branch>` is issued first (git auto-tracks `origin/<branch>`), so
2774
+ * the sync leaves the repo parked on the configured `repo.branch`.
2775
+ * When `branch` is omitted the legacy behaviour is preserved — the
2776
+ * current local branch is fast-forwarded. The parameter is optional
2777
+ * and backward compatible.
2739
2778
  */
2740
- async pull(repoId, localPath) {
2779
+ async pull(repoId, localPath, branch, useProxy = true, originalUrl) {
2741
2780
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2742
2781
  if (!remoteUrl) {
2743
2782
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2744
2783
  }
2745
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2784
+ const targetUrl = useProxy ? remoteUrl : originalUrl ?? remoteUrl;
2785
+ const proxyUrl = this.rewriteRemoteUrl(repoId, targetUrl, useProxy);
2746
2786
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2747
2787
  const before = await this.safeRevParse(localPath);
2748
2788
  await this.runOrThrow(["fetch", "origin"], { cwd: localPath });
2749
2789
  const after = await this.safeRevParse(localPath);
2750
- const branch = await this.currentBranch(localPath);
2751
- if (!branch) {
2790
+ let targetBranch = branch;
2791
+ if (!targetBranch) {
2792
+ targetBranch = await this.currentBranch(localPath);
2793
+ } else {
2794
+ const current = await this.currentBranch(localPath);
2795
+ if (current !== targetBranch) {
2796
+ await this.runOrThrow(["checkout", targetBranch], { cwd: localPath });
2797
+ }
2798
+ }
2799
+ if (!targetBranch) {
2752
2800
  throw new Error(`could not determine current branch at ${localPath}`);
2753
2801
  }
2754
- await this.runOrThrow(["merge", "--ff-only", `origin/${branch}`], { cwd: localPath });
2802
+ await this.runOrThrow(["merge", "--ff-only", `origin/${targetBranch}`], {
2803
+ cwd: localPath
2804
+ });
2755
2805
  return {
2756
2806
  updated: before !== after,
2757
2807
  commitSha: after,
2758
- branch
2808
+ branch: targetBranch
2759
2809
  };
2760
2810
  }
2761
2811
  /**
@@ -2763,12 +2813,12 @@ var GitClient = class {
2763
2813
  * for committing locally first. The server's GitProxy injects the
2764
2814
  * PAT on the way upstream.
2765
2815
  */
2766
- async push(repoId, localPath, branch) {
2816
+ async push(repoId, localPath, branch, useProxy = true) {
2767
2817
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2768
2818
  if (!remoteUrl) {
2769
2819
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2770
2820
  }
2771
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2821
+ const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl, useProxy);
2772
2822
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2773
2823
  const result = await this.spawner.spawn(
2774
2824
  ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`],
@@ -2788,8 +2838,8 @@ var GitClient = class {
2788
2838
  * `git ls-remote <proxyUrl>` — list advertised branches without
2789
2839
  * cloning. Used by addUserRepo to detect the default branch.
2790
2840
  */
2791
- async lsRemote(repoId, originalUrl) {
2792
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2841
+ async lsRemote(repoId, originalUrl, useProxy = true) {
2842
+ const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl, useProxy);
2793
2843
  const result = await this.spawner.spawn(["ls-remote", "--heads", "--", proxyUrl], {
2794
2844
  cwd: process.cwd()
2795
2845
  });
@@ -3548,14 +3598,36 @@ function isUserRepo(repo) {
3548
3598
 
3549
3599
  // src/repo-manager/index.ts
3550
3600
  function idFromUrl(url) {
3551
- const trimmed = url.trim().replace(/\.git$/, "");
3552
- const last = trimmed.split("/").pop() ?? "";
3553
- const safe = last.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
3601
+ const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
3602
+ const sanitize = (value) => value.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3603
+ let owner = "";
3604
+ let repo = "";
3605
+ if (/^https?:\/\//i.test(trimmed)) {
3606
+ const parsed = new URL(trimmed);
3607
+ const segments = parsed.pathname.split("/").filter(Boolean);
3608
+ repo = segments.at(-1) ?? "";
3609
+ owner = segments.length >= 2 ? segments.at(-2) ?? "" : "";
3610
+ } else if (trimmed.startsWith("git@")) {
3611
+ const colon = trimmed.indexOf(":");
3612
+ const repoPath = colon >= 0 ? trimmed.slice(colon + 1) : "";
3613
+ const segments = repoPath.split("/").filter(Boolean);
3614
+ repo = segments.at(-1) ?? "";
3615
+ owner = segments.length >= 2 ? segments.at(-2) ?? "" : "";
3616
+ } else {
3617
+ repo = trimmed.split("/").pop() ?? "";
3618
+ }
3619
+ const safeRepo = sanitize(repo);
3620
+ const safeOwner = sanitize(owner);
3621
+ const safe = safeOwner ? `${safeOwner}-${safeRepo}` : safeRepo;
3554
3622
  if (!SAFE_REPO_ID_PATTERN.test(safe)) {
3555
3623
  throw new InvalidRepoUrlError(`cannot derive a valid repo id from url: ${url}`);
3556
3624
  }
3557
3625
  return safe;
3558
3626
  }
3627
+ function proxyRepoId(url) {
3628
+ const encoded = Buffer.from(url.trim(), "utf8").toString("base64url");
3629
+ return `repo-${encoded}`;
3630
+ }
3559
3631
  var RepoManagerError = class extends Error {
3560
3632
  constructor(message) {
3561
3633
  super(message);
@@ -3584,7 +3656,7 @@ var RepoManager = class {
3584
3656
  * but do NOT abort the loop — the UI surfaces per-repo state and the
3585
3657
  * user can retry.
3586
3658
  *
3587
- * Per spec §3.1: 4 default repos; the loop honors `enabled=false` and
3659
+ * Per spec §3.1: iterate all default repos; the loop honors `enabled=false` and
3588
3660
  * disables (rather than removes) repos the user has turned off.
3589
3661
  */
3590
3662
  async ensureDefaults() {
@@ -3594,11 +3666,14 @@ var RepoManager = class {
3594
3666
  if (!repo.enabled) continue;
3595
3667
  const localPath = getRepoDir(repo.id);
3596
3668
  const exists = await this.pathExists(localPath);
3597
- if (exists) continue;
3669
+ if (exists) {
3670
+ if (await this.isValidGitRepo(localPath)) continue;
3671
+ await fs8.rm(localPath, { recursive: true, force: true });
3672
+ }
3598
3673
  try {
3599
3674
  if (!this.skipClone) {
3600
3675
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3601
- await this.git.clone(repo.id, repo.url, localPath);
3676
+ await this.git.clone(repo.id, repo.url, localPath, repo.branch, true);
3602
3677
  }
3603
3678
  await this.store.updateRepo(repo.id, {
3604
3679
  lastSyncStatus: "ok",
@@ -3627,12 +3702,17 @@ var RepoManager = class {
3627
3702
  async pullOne(repoId) {
3628
3703
  const repo = this.store.get(repoId);
3629
3704
  if (!repo) throw new RepoNotInStoreError(repoId);
3705
+ const useProxy = repo.useProxy ?? true;
3706
+ const proxyId = useProxy ? proxyRepoId(repo.url) : repo.id;
3630
3707
  const localPath = getRepoDir(repoId);
3631
3708
  const exists = await this.pathExists(localPath);
3632
- if (!exists) {
3709
+ if (exists && !await this.isValidGitRepo(localPath)) {
3710
+ await fs8.rm(localPath, { recursive: true, force: true });
3711
+ }
3712
+ if (!exists || !await this.pathExists(localPath)) {
3633
3713
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3634
3714
  if (!this.skipClone) {
3635
- await this.git.clone(repoId, repo.url, localPath);
3715
+ await this.git.clone(proxyId, repo.url, localPath, repo.branch, useProxy);
3636
3716
  }
3637
3717
  const result = {
3638
3718
  updated: true,
@@ -3646,7 +3726,7 @@ var RepoManager = class {
3646
3726
  return result;
3647
3727
  }
3648
3728
  try {
3649
- const result = await this.git.pull(repoId, localPath);
3729
+ const result = await this.git.pull(proxyId, localPath, repo.branch, useProxy, repo.url);
3650
3730
  await this.store.updateRepo(repoId, {
3651
3731
  lastSyncStatus: "ok",
3652
3732
  lastSyncAt: this.now(),
@@ -3698,10 +3778,12 @@ var RepoManager = class {
3698
3778
  throw new InvalidRepoUrlError(`expected https:// or git@ URL, got: ${url}`);
3699
3779
  }
3700
3780
  const id = idFromUrl(url);
3781
+ const useProxy = input.useProxy ?? true;
3782
+ const userProxyId = useProxy ? proxyRepoId(url) : id;
3701
3783
  assertSafeRepoId(id);
3702
3784
  let branch = input.branch?.trim();
3703
3785
  if (!branch) {
3704
- const branches = await this.git.lsRemote(id, url);
3786
+ const branches = await this.git.lsRemote(userProxyId, url, useProxy);
3705
3787
  const head = branches.find((b) => b.ref === "refs/heads/main") ?? branches.find((b) => b.ref === "refs/heads/master") ?? branches.find((b) => b.ref.endsWith("/v2"));
3706
3788
  if (!head) {
3707
3789
  throw new InvalidRepoUrlError(
@@ -3719,6 +3801,7 @@ var RepoManager = class {
3719
3801
  url,
3720
3802
  branch,
3721
3803
  enabled: true,
3804
+ useProxy,
3722
3805
  writeEnabled: false,
3723
3806
  source: "user",
3724
3807
  addedAt: this.now()
@@ -3728,7 +3811,7 @@ var RepoManager = class {
3728
3811
  if (!this.skipClone) {
3729
3812
  const localPath = getRepoDir(id);
3730
3813
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3731
- await this.git.clone(id, url, localPath);
3814
+ await this.git.clone(userProxyId, url, localPath, branch, useProxy);
3732
3815
  cloned = true;
3733
3816
  }
3734
3817
  return { repo, branch, cloned };
@@ -3777,6 +3860,14 @@ var RepoManager = class {
3777
3860
  async ensureHome() {
3778
3861
  await fs8.mkdir(getServicemeHome(), { recursive: true });
3779
3862
  }
3863
+ /**
3864
+ * Returns `true` when `p` contains a `.git` entry — i.e. it is an
3865
+ * initialised git repository and safe to `pull`. Empty directories
3866
+ * (e.g. from an interrupted clone) return `false`.
3867
+ */
3868
+ async isValidGitRepo(p) {
3869
+ return this.pathExists(path11.join(p, ".git"));
3870
+ }
3780
3871
  async pathExists(p) {
3781
3872
  try {
3782
3873
  await fs8.stat(p);
@@ -3793,10 +3884,11 @@ var FIXED_ADDED_AT = "2026-06-29T00:00:00.000Z";
3793
3884
  var DEFAULT_REPO_SEEDS = [
3794
3885
  {
3795
3886
  id: "medalsoftchina-ms-skills",
3796
- name: "MS DevTools Skills (\u7CBE\u9009)",
3887
+ name: "SERVICEME (\u7CBE\u9009)",
3797
3888
  url: "https://github.com/medalsoftchina/ms-skills.git",
3798
3889
  branch: "v2",
3799
3890
  enabled: true,
3891
+ useProxy: true,
3800
3892
  writeEnabled: true,
3801
3893
  source: "default",
3802
3894
  description: "Medalsoft \u7CBE\u9009 skill/agent \u4ED3\u5E93\uFF0C\u53EF\u8BFB\u53EF\u5199"
@@ -3807,6 +3899,7 @@ var DEFAULT_REPO_SEEDS = [
3807
3899
  url: "https://github.com/anthropics/skills.git",
3808
3900
  branch: "main",
3809
3901
  enabled: true,
3902
+ useProxy: true,
3810
3903
  writeEnabled: false,
3811
3904
  source: "default",
3812
3905
  description: "Anthropic \u5B98\u65B9 skills \u793A\u4F8B"
@@ -3817,16 +3910,29 @@ var DEFAULT_REPO_SEEDS = [
3817
3910
  url: "https://github.com/github/awesome-copilot.git",
3818
3911
  branch: "main",
3819
3912
  enabled: true,
3913
+ useProxy: true,
3820
3914
  writeEnabled: false,
3821
3915
  source: "default",
3822
3916
  description: "GitHub Copilot \u793E\u533A\u7CBE\u9009 prompts/agents"
3823
3917
  },
3918
+ {
3919
+ id: "mattpocock-skills",
3920
+ name: "Matt Pocock Skills",
3921
+ url: "https://github.com/mattpocock/skills.git",
3922
+ branch: "main",
3923
+ enabled: true,
3924
+ useProxy: true,
3925
+ writeEnabled: false,
3926
+ source: "default",
3927
+ description: "Matt Pocock \u793E\u533A skills \u4ED3\u5E93"
3928
+ },
3824
3929
  {
3825
3930
  id: "composiohq-awesome-claude-skills",
3826
3931
  name: "Composio Awesome Claude Skills",
3827
3932
  url: "https://github.com/ComposioHQ/awesome-claude-skills.git",
3828
3933
  branch: "main",
3829
3934
  enabled: true,
3935
+ useProxy: true,
3830
3936
  writeEnabled: false,
3831
3937
  source: "default",
3832
3938
  description: "Composio \u7EF4\u62A4\u7684 Claude skills \u96C6\u5408"
@@ -3903,6 +4009,7 @@ var baseRepoFields = {
3903
4009
  url: import_zod.z.string().url(),
3904
4010
  branch: import_zod.z.string().min(1).max(200),
3905
4011
  enabled: import_zod.z.boolean(),
4012
+ useProxy: import_zod.z.boolean().optional().default(true),
3906
4013
  writeEnabled: import_zod.z.boolean(),
3907
4014
  addedAt: isoTimestampSchema,
3908
4015
  lastSyncAt: isoTimestampSchema.optional(),
@@ -5949,7 +6056,10 @@ function readV1Config(v1Path) {
5949
6056
  try {
5950
6057
  raw = fs19.readFileSync(v1Path, "utf-8");
5951
6058
  } catch (err) {
5952
- return { ok: false, error: `read failed: ${err instanceof Error ? err.message : String(err)}` };
6059
+ return {
6060
+ ok: false,
6061
+ error: `read failed: ${err instanceof Error ? err.message : String(err)}`
6062
+ };
5953
6063
  }
5954
6064
  let parsed;
5955
6065
  try {
@@ -6004,79 +6114,77 @@ function disambiguateName(task, existingNames, workspaceName) {
6004
6114
  existingNames.add(candidate);
6005
6115
  return { ...task, name: candidate };
6006
6116
  }
6007
- var MigrateToGlobal = class {
6008
- /**
6009
- * Run the migration. Idempotent: each v1 file is deleted after processing,
6010
- * so re-running this method on a partially-migrated state is safe (the
6011
- * already-migrated files no longer exist; only the remainder is processed).
6012
- */
6013
- static async run(options) {
6014
- const globalConfigPath = options.globalConfigPath ?? getScheduledTasksConfigPath();
6015
- const migrationFailuresPath = options.migrationFailuresPath ?? getMigrationFailuresPath();
6016
- const probe = options.probe ?? defaultProbe;
6017
- const existing = readJsonFile(globalConfigPath);
6018
- const baseConfig = existing && existing.version === 2 ? existing : { version: 2, tasks: [] };
6019
- const existingNames = new Set(baseConfig.tasks.map((t) => t.name));
6020
- const priorFailures = readJsonFile(migrationFailuresPath) ?? [];
6021
- const failures = [...priorFailures];
6022
- let migrated = 0;
6023
- const conflicts = [];
6024
- const issues = [];
6025
- for (const workspacePath of options.workspacePaths) {
6026
- const v1Path = path20.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
6027
- if (!fs19.existsSync(v1Path)) continue;
6028
- const v1 = readV1Config(v1Path);
6029
- if (!v1.ok) {
6030
- failures.push({
6031
- workspacePath,
6032
- v1Path,
6033
- error: v1.error,
6034
- at: (/* @__PURE__ */ new Date()).toISOString()
6035
- });
6036
- safeDelete(v1Path);
6037
- continue;
6038
- }
6039
- let workspaceRef;
6040
- try {
6041
- workspaceRef = await probe(workspacePath);
6042
- } catch (err) {
6043
- failures.push({
6044
- workspacePath,
6045
- v1Path,
6046
- error: `probe failed: ${err instanceof Error ? err.message : String(err)}`,
6047
- at: (/* @__PURE__ */ new Date()).toISOString()
6048
- });
6049
- safeDelete(v1Path);
6050
- continue;
6051
- }
6052
- const { config, issues: taskIssues } = (0, import_devtools_protocol9.migrateV1ToV2)(v1.config, workspaceRef);
6053
- issues.push(...taskIssues);
6054
- const newTasks = config.tasks.map((t) => {
6055
- if (existingNames.has(t.name)) {
6056
- conflicts.push(t.name);
6057
- return disambiguateName(t, existingNames, workspaceRef.name);
6058
- }
6059
- existingNames.add(t.name);
6060
- return t;
6117
+ async function migrateToGlobal(options) {
6118
+ const globalConfigPath = options.globalConfigPath ?? getScheduledTasksConfigPath();
6119
+ const migrationFailuresPath = options.migrationFailuresPath ?? getMigrationFailuresPath();
6120
+ const probe = options.probe ?? defaultProbe;
6121
+ const existing = readJsonFile(globalConfigPath);
6122
+ const baseConfig = existing && existing.version === 2 ? existing : { version: 2, tasks: [] };
6123
+ const existingNames = new Set(baseConfig.tasks.map((t) => t.name));
6124
+ const priorFailures = readJsonFile(migrationFailuresPath) ?? [];
6125
+ const failures = [...priorFailures];
6126
+ let migrated = 0;
6127
+ const conflicts = [];
6128
+ const issues = [];
6129
+ for (const workspacePath of options.workspacePaths) {
6130
+ const v1Path = path20.join(workspacePath, WORKSPACE_DIR, V1_FILENAME);
6131
+ if (!fs19.existsSync(v1Path)) continue;
6132
+ const v1 = readV1Config(v1Path);
6133
+ if (!v1.ok) {
6134
+ failures.push({
6135
+ workspacePath,
6136
+ v1Path,
6137
+ error: v1.error,
6138
+ at: (/* @__PURE__ */ new Date()).toISOString()
6061
6139
  });
6062
- baseConfig.tasks.push(...newTasks);
6063
- migrated += 1;
6064
6140
  safeDelete(v1Path);
6141
+ continue;
6065
6142
  }
6066
- if (migrated > 0) {
6067
- ensureDir(globalConfigPath);
6068
- const tmp = `${globalConfigPath}.tmp`;
6069
- fs19.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6070
- fs19.renameSync(tmp, globalConfigPath);
6071
- }
6072
- if (failures.length > priorFailures.length) {
6073
- writeJsonFile(migrationFailuresPath, failures);
6074
- } else if (failures.length === 0 && priorFailures.length > 0) {
6075
- safeDelete(migrationFailuresPath);
6143
+ let workspaceRef;
6144
+ try {
6145
+ workspaceRef = await probe(workspacePath);
6146
+ } catch (err) {
6147
+ failures.push({
6148
+ workspacePath,
6149
+ v1Path,
6150
+ error: `probe failed: ${err instanceof Error ? err.message : String(err)}`,
6151
+ at: (/* @__PURE__ */ new Date()).toISOString()
6152
+ });
6153
+ safeDelete(v1Path);
6154
+ continue;
6076
6155
  }
6077
- return { migrated, failed: failures.length - priorFailures.length, conflicts, issues };
6156
+ const { config, issues: taskIssues } = (0, import_devtools_protocol9.migrateV1ToV2)(v1.config, workspaceRef);
6157
+ issues.push(...taskIssues);
6158
+ const newTasks = config.tasks.map((t) => {
6159
+ if (existingNames.has(t.name)) {
6160
+ conflicts.push(t.name);
6161
+ return disambiguateName(t, existingNames, workspaceRef.name);
6162
+ }
6163
+ existingNames.add(t.name);
6164
+ return t;
6165
+ });
6166
+ baseConfig.tasks.push(...newTasks);
6167
+ migrated += 1;
6168
+ safeDelete(v1Path);
6078
6169
  }
6079
- };
6170
+ if (migrated > 0) {
6171
+ ensureDir(globalConfigPath);
6172
+ const tmp = `${globalConfigPath}.tmp`;
6173
+ fs19.writeFileSync(tmp, JSON.stringify(baseConfig, null, " "), "utf-8");
6174
+ fs19.renameSync(tmp, globalConfigPath);
6175
+ }
6176
+ if (failures.length > priorFailures.length) {
6177
+ writeJsonFile(migrationFailuresPath, failures);
6178
+ } else if (failures.length === 0 && priorFailures.length > 0) {
6179
+ safeDelete(migrationFailuresPath);
6180
+ }
6181
+ return {
6182
+ migrated,
6183
+ failed: failures.length - priorFailures.length,
6184
+ conflicts,
6185
+ issues
6186
+ };
6187
+ }
6080
6188
 
6081
6189
  // src/scheduled-tasks/workspace-probe/WorkspaceProbe.ts
6082
6190
  var import_node_child_process5 = require("child_process");
@@ -6979,7 +7087,6 @@ var ToolboxCore = class {
6979
7087
  MIGRATION_FAILURES_FILENAME,
6980
7088
  MicrosoftAuthProvider,
6981
7089
  MicrosoftProviderNotHostedError,
6982
- MigrateToGlobal,
6983
7090
  NodeGitSpawner,
6984
7091
  PROFILES_JSON_FILENAME,
6985
7092
  PidManager,
@@ -7078,6 +7185,7 @@ var ToolboxCore = class {
7078
7185
  isUserRepo,
7079
7186
  matchesCron,
7080
7187
  mergeWithDefaults,
7188
+ migrateToGlobal,
7081
7189
  moveFiles,
7082
7190
  narrowRepoConfig,
7083
7191
  noopLogger,