@serviceme/devtools-core 0.2.1 → 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
@@ -1086,9 +1086,7 @@ var GitHubAuthProvider = class {
1086
1086
  continue;
1087
1087
  }
1088
1088
  if (!resp.ok) {
1089
- throw new Error(
1090
- `GitHub device-code request failed: HTTP ${resp.status}`
1091
- );
1089
+ throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
1092
1090
  }
1093
1091
  const data = await resp.json();
1094
1092
  if (!data.device_code || !data.user_code || !data.verification_uri) {
@@ -1139,17 +1137,11 @@ var GitHubAuthProvider = class {
1139
1137
  if (!isTransientNetworkError(error)) {
1140
1138
  throw error;
1141
1139
  }
1142
- pollIntervalMs = Math.min(
1143
- Math.round(pollIntervalMs * 1.5),
1144
- this.cfg.maxPollIntervalMs
1145
- );
1140
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1146
1141
  continue;
1147
1142
  }
1148
1143
  if (!resp.ok) {
1149
- pollIntervalMs = Math.min(
1150
- Math.round(pollIntervalMs * 1.5),
1151
- this.cfg.maxPollIntervalMs
1152
- );
1144
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
1153
1145
  continue;
1154
1146
  }
1155
1147
  const data = await resp.json();
@@ -1175,9 +1167,7 @@ var GitHubAuthProvider = class {
1175
1167
  if (data.error === "slow_down") {
1176
1168
  consecutiveSlowDown++;
1177
1169
  pollIntervalMs = Math.min(
1178
- Math.round(
1179
- pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)
1180
- ),
1170
+ Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
1181
1171
  this.cfg.maxPollIntervalMs
1182
1172
  );
1183
1173
  continue;
@@ -1213,9 +1203,7 @@ var GitHubAuthProvider = class {
1213
1203
  }
1214
1204
  const data = await resp.json();
1215
1205
  if (!data.access_token) {
1216
- throw new Error(
1217
- `GitHub refresh failed: ${data.error ?? "no_access_token"}`
1218
- );
1206
+ throw new Error(`GitHub refresh failed: ${data.error ?? "no_access_token"}`);
1219
1207
  }
1220
1208
  const rawUser = await this.fetchGitHubUser(data.access_token);
1221
1209
  const email = await this.resolveEmail(data.access_token, rawUser);
@@ -1285,16 +1273,13 @@ var GitHubAuthProvider = class {
1285
1273
  */
1286
1274
  async resolveEmail(token, user) {
1287
1275
  if (user.email) return user.email;
1288
- const emailsResp = await this.cfg.fetchImpl(
1289
- "https://api.github.com/user/emails",
1290
- {
1291
- headers: {
1292
- Accept: "application/vnd.github+json",
1293
- Authorization: `Bearer ${token}`,
1294
- "User-Agent": "serviceme-core"
1295
- }
1276
+ const emailsResp = await this.cfg.fetchImpl("https://api.github.com/user/emails", {
1277
+ headers: {
1278
+ Accept: "application/vnd.github+json",
1279
+ Authorization: `Bearer ${token}`,
1280
+ "User-Agent": "serviceme-core"
1296
1281
  }
1297
- );
1282
+ });
1298
1283
  if (!emailsResp.ok) {
1299
1284
  return null;
1300
1285
  }
@@ -2745,54 +2730,82 @@ var GitClient = class {
2745
2730
  * Rewrite an upstream URL to its proxy form.
2746
2731
  *
2747
2732
  * Examples:
2748
- * rewriteRemoteUrl("medalsoftchina-ms-skills",
2733
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ",
2749
2734
  * "https://github.com/medalsoftchina/ms-skills.git")
2750
- * → "http://localhost:3000/git-proxy/medalsoftchina-ms-skills"
2735
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ"
2751
2736
  *
2752
- * rewriteRemoteUrl("my-team-internal",
2737
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0",
2753
2738
  * "git@github.com:medalsoftchina/ms-skills.git")
2754
- * → "http://localhost:3000/git-proxy/my-team-internal"
2739
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0"
2755
2740
  *
2756
2741
  * The original URL's host/owner is intentionally DROPPED — the proxy
2757
2742
  * knows the upstream from the per-repo config, not from the URL. This
2758
- * means callers can't accidentally route a user-repo URL through the
2759
- * default-repo proxy slot.
2743
+ * means callers can route both default and user repos through the same
2744
+ * dynamic proxy-id scheme.
2760
2745
  */
2761
- rewriteRemoteUrl(_repoId, _originalUrl) {
2746
+ rewriteRemoteUrl(_repoId, originalUrl, useProxy = true) {
2747
+ if (!useProxy) {
2748
+ return originalUrl;
2749
+ }
2762
2750
  return `${this.serverProxyBase}/${_repoId}`;
2763
2751
  }
2764
2752
  /**
2765
2753
  * `git clone <proxyUrl> <localPath>` — initialize a new local repo
2766
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.
2767
2761
  */
2768
- async clone(repoId, originalUrl, localPath) {
2769
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2770
- 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];
2771
2765
  await this.runOrThrow(args, { cwd: process.cwd() });
2772
2766
  }
2773
2767
  /**
2774
2768
  * `git fetch <remote> <branch>` + return the resulting commit SHA.
2775
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.
2776
2778
  */
2777
- async pull(repoId, localPath) {
2779
+ async pull(repoId, localPath, branch, useProxy = true, originalUrl) {
2778
2780
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2779
2781
  if (!remoteUrl) {
2780
2782
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2781
2783
  }
2782
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2784
+ const targetUrl = useProxy ? remoteUrl : originalUrl ?? remoteUrl;
2785
+ const proxyUrl = this.rewriteRemoteUrl(repoId, targetUrl, useProxy);
2783
2786
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2784
2787
  const before = await this.safeRevParse(localPath);
2785
2788
  await this.runOrThrow(["fetch", "origin"], { cwd: localPath });
2786
2789
  const after = await this.safeRevParse(localPath);
2787
- const branch = await this.currentBranch(localPath);
2788
- 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) {
2789
2800
  throw new Error(`could not determine current branch at ${localPath}`);
2790
2801
  }
2791
- await this.runOrThrow(["merge", "--ff-only", `origin/${branch}`], { cwd: localPath });
2802
+ await this.runOrThrow(["merge", "--ff-only", `origin/${targetBranch}`], {
2803
+ cwd: localPath
2804
+ });
2792
2805
  return {
2793
2806
  updated: before !== after,
2794
2807
  commitSha: after,
2795
- branch
2808
+ branch: targetBranch
2796
2809
  };
2797
2810
  }
2798
2811
  /**
@@ -2800,12 +2813,12 @@ var GitClient = class {
2800
2813
  * for committing locally first. The server's GitProxy injects the
2801
2814
  * PAT on the way upstream.
2802
2815
  */
2803
- async push(repoId, localPath, branch) {
2816
+ async push(repoId, localPath, branch, useProxy = true) {
2804
2817
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2805
2818
  if (!remoteUrl) {
2806
2819
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2807
2820
  }
2808
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2821
+ const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl, useProxy);
2809
2822
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2810
2823
  const result = await this.spawner.spawn(
2811
2824
  ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`],
@@ -2825,8 +2838,8 @@ var GitClient = class {
2825
2838
  * `git ls-remote <proxyUrl>` — list advertised branches without
2826
2839
  * cloning. Used by addUserRepo to detect the default branch.
2827
2840
  */
2828
- async lsRemote(repoId, originalUrl) {
2829
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2841
+ async lsRemote(repoId, originalUrl, useProxy = true) {
2842
+ const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl, useProxy);
2830
2843
  const result = await this.spawner.spawn(["ls-remote", "--heads", "--", proxyUrl], {
2831
2844
  cwd: process.cwd()
2832
2845
  });
@@ -3585,16 +3598,36 @@ function isUserRepo(repo) {
3585
3598
 
3586
3599
  // src/repo-manager/index.ts
3587
3600
  function idFromUrl(url) {
3588
- const trimmed = url.trim().replace(/\.git$/, "");
3589
- const last = trimmed.split("/").pop() ?? "";
3590
- 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;
3591
3622
  if (!SAFE_REPO_ID_PATTERN.test(safe)) {
3592
- throw new InvalidRepoUrlError(
3593
- `cannot derive a valid repo id from url: ${url}`
3594
- );
3623
+ throw new InvalidRepoUrlError(`cannot derive a valid repo id from url: ${url}`);
3595
3624
  }
3596
3625
  return safe;
3597
3626
  }
3627
+ function proxyRepoId(url) {
3628
+ const encoded = Buffer.from(url.trim(), "utf8").toString("base64url");
3629
+ return `repo-${encoded}`;
3630
+ }
3598
3631
  var RepoManagerError = class extends Error {
3599
3632
  constructor(message) {
3600
3633
  super(message);
@@ -3623,7 +3656,7 @@ var RepoManager = class {
3623
3656
  * but do NOT abort the loop — the UI surfaces per-repo state and the
3624
3657
  * user can retry.
3625
3658
  *
3626
- * 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
3627
3660
  * disables (rather than removes) repos the user has turned off.
3628
3661
  */
3629
3662
  async ensureDefaults() {
@@ -3640,7 +3673,7 @@ var RepoManager = class {
3640
3673
  try {
3641
3674
  if (!this.skipClone) {
3642
3675
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3643
- await this.git.clone(repo.id, repo.url, localPath);
3676
+ await this.git.clone(repo.id, repo.url, localPath, repo.branch, true);
3644
3677
  }
3645
3678
  await this.store.updateRepo(repo.id, {
3646
3679
  lastSyncStatus: "ok",
@@ -3669,6 +3702,8 @@ var RepoManager = class {
3669
3702
  async pullOne(repoId) {
3670
3703
  const repo = this.store.get(repoId);
3671
3704
  if (!repo) throw new RepoNotInStoreError(repoId);
3705
+ const useProxy = repo.useProxy ?? true;
3706
+ const proxyId = useProxy ? proxyRepoId(repo.url) : repo.id;
3672
3707
  const localPath = getRepoDir(repoId);
3673
3708
  const exists = await this.pathExists(localPath);
3674
3709
  if (exists && !await this.isValidGitRepo(localPath)) {
@@ -3677,7 +3712,7 @@ var RepoManager = class {
3677
3712
  if (!exists || !await this.pathExists(localPath)) {
3678
3713
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3679
3714
  if (!this.skipClone) {
3680
- await this.git.clone(repoId, repo.url, localPath);
3715
+ await this.git.clone(proxyId, repo.url, localPath, repo.branch, useProxy);
3681
3716
  }
3682
3717
  const result = {
3683
3718
  updated: true,
@@ -3691,7 +3726,7 @@ var RepoManager = class {
3691
3726
  return result;
3692
3727
  }
3693
3728
  try {
3694
- const result = await this.git.pull(repoId, localPath);
3729
+ const result = await this.git.pull(proxyId, localPath, repo.branch, useProxy, repo.url);
3695
3730
  await this.store.updateRepo(repoId, {
3696
3731
  lastSyncStatus: "ok",
3697
3732
  lastSyncAt: this.now(),
@@ -3740,15 +3775,15 @@ var RepoManager = class {
3740
3775
  async addUserRepo(input) {
3741
3776
  const url = input.url.trim();
3742
3777
  if (!/^https?:\/\//.test(url) && !url.startsWith("git@")) {
3743
- throw new InvalidRepoUrlError(
3744
- `expected https:// or git@ URL, got: ${url}`
3745
- );
3778
+ throw new InvalidRepoUrlError(`expected https:// or git@ URL, got: ${url}`);
3746
3779
  }
3747
3780
  const id = idFromUrl(url);
3781
+ const useProxy = input.useProxy ?? true;
3782
+ const userProxyId = useProxy ? proxyRepoId(url) : id;
3748
3783
  assertSafeRepoId(id);
3749
3784
  let branch = input.branch?.trim();
3750
3785
  if (!branch) {
3751
- const branches = await this.git.lsRemote(id, url);
3786
+ const branches = await this.git.lsRemote(userProxyId, url, useProxy);
3752
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"));
3753
3788
  if (!head) {
3754
3789
  throw new InvalidRepoUrlError(
@@ -3766,6 +3801,7 @@ var RepoManager = class {
3766
3801
  url,
3767
3802
  branch,
3768
3803
  enabled: true,
3804
+ useProxy,
3769
3805
  writeEnabled: false,
3770
3806
  source: "user",
3771
3807
  addedAt: this.now()
@@ -3775,7 +3811,7 @@ var RepoManager = class {
3775
3811
  if (!this.skipClone) {
3776
3812
  const localPath = getRepoDir(id);
3777
3813
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3778
- await this.git.clone(id, url, localPath);
3814
+ await this.git.clone(userProxyId, url, localPath, branch, useProxy);
3779
3815
  cloned = true;
3780
3816
  }
3781
3817
  return { repo, branch, cloned };
@@ -3848,10 +3884,11 @@ var FIXED_ADDED_AT = "2026-06-29T00:00:00.000Z";
3848
3884
  var DEFAULT_REPO_SEEDS = [
3849
3885
  {
3850
3886
  id: "medalsoftchina-ms-skills",
3851
- name: "MS DevTools Skills (\u7CBE\u9009)",
3887
+ name: "SERVICEME (\u7CBE\u9009)",
3852
3888
  url: "https://github.com/medalsoftchina/ms-skills.git",
3853
3889
  branch: "v2",
3854
3890
  enabled: true,
3891
+ useProxy: true,
3855
3892
  writeEnabled: true,
3856
3893
  source: "default",
3857
3894
  description: "Medalsoft \u7CBE\u9009 skill/agent \u4ED3\u5E93\uFF0C\u53EF\u8BFB\u53EF\u5199"
@@ -3862,6 +3899,7 @@ var DEFAULT_REPO_SEEDS = [
3862
3899
  url: "https://github.com/anthropics/skills.git",
3863
3900
  branch: "main",
3864
3901
  enabled: true,
3902
+ useProxy: true,
3865
3903
  writeEnabled: false,
3866
3904
  source: "default",
3867
3905
  description: "Anthropic \u5B98\u65B9 skills \u793A\u4F8B"
@@ -3872,16 +3910,29 @@ var DEFAULT_REPO_SEEDS = [
3872
3910
  url: "https://github.com/github/awesome-copilot.git",
3873
3911
  branch: "main",
3874
3912
  enabled: true,
3913
+ useProxy: true,
3875
3914
  writeEnabled: false,
3876
3915
  source: "default",
3877
3916
  description: "GitHub Copilot \u793E\u533A\u7CBE\u9009 prompts/agents"
3878
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
+ },
3879
3929
  {
3880
3930
  id: "composiohq-awesome-claude-skills",
3881
3931
  name: "Composio Awesome Claude Skills",
3882
3932
  url: "https://github.com/ComposioHQ/awesome-claude-skills.git",
3883
3933
  branch: "main",
3884
3934
  enabled: true,
3935
+ useProxy: true,
3885
3936
  writeEnabled: false,
3886
3937
  source: "default",
3887
3938
  description: "Composio \u7EF4\u62A4\u7684 Claude skills \u96C6\u5408"
@@ -3958,6 +4009,7 @@ var baseRepoFields = {
3958
4009
  url: import_zod.z.string().url(),
3959
4010
  branch: import_zod.z.string().min(1).max(200),
3960
4011
  enabled: import_zod.z.boolean(),
4012
+ useProxy: import_zod.z.boolean().optional().default(true),
3961
4013
  writeEnabled: import_zod.z.boolean(),
3962
4014
  addedAt: isoTimestampSchema,
3963
4015
  lastSyncAt: isoTimestampSchema.optional(),