@serviceme/devtools-core 0.2.1 → 0.3.1

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
@@ -893,9 +893,7 @@ var GitHubAuthProvider = class {
893
893
  continue;
894
894
  }
895
895
  if (!resp.ok) {
896
- throw new Error(
897
- `GitHub device-code request failed: HTTP ${resp.status}`
898
- );
896
+ throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
899
897
  }
900
898
  const data = await resp.json();
901
899
  if (!data.device_code || !data.user_code || !data.verification_uri) {
@@ -946,17 +944,11 @@ var GitHubAuthProvider = class {
946
944
  if (!isTransientNetworkError(error)) {
947
945
  throw error;
948
946
  }
949
- pollIntervalMs = Math.min(
950
- Math.round(pollIntervalMs * 1.5),
951
- this.cfg.maxPollIntervalMs
952
- );
947
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
953
948
  continue;
954
949
  }
955
950
  if (!resp.ok) {
956
- pollIntervalMs = Math.min(
957
- Math.round(pollIntervalMs * 1.5),
958
- this.cfg.maxPollIntervalMs
959
- );
951
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
960
952
  continue;
961
953
  }
962
954
  const data = await resp.json();
@@ -982,9 +974,7 @@ var GitHubAuthProvider = class {
982
974
  if (data.error === "slow_down") {
983
975
  consecutiveSlowDown++;
984
976
  pollIntervalMs = Math.min(
985
- Math.round(
986
- pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)
987
- ),
977
+ Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
988
978
  this.cfg.maxPollIntervalMs
989
979
  );
990
980
  continue;
@@ -1020,9 +1010,7 @@ var GitHubAuthProvider = class {
1020
1010
  }
1021
1011
  const data = await resp.json();
1022
1012
  if (!data.access_token) {
1023
- throw new Error(
1024
- `GitHub refresh failed: ${data.error ?? "no_access_token"}`
1025
- );
1013
+ throw new Error(`GitHub refresh failed: ${data.error ?? "no_access_token"}`);
1026
1014
  }
1027
1015
  const rawUser = await this.fetchGitHubUser(data.access_token);
1028
1016
  const email = await this.resolveEmail(data.access_token, rawUser);
@@ -1092,16 +1080,13 @@ var GitHubAuthProvider = class {
1092
1080
  */
1093
1081
  async resolveEmail(token, user) {
1094
1082
  if (user.email) return user.email;
1095
- const emailsResp = await this.cfg.fetchImpl(
1096
- "https://api.github.com/user/emails",
1097
- {
1098
- headers: {
1099
- Accept: "application/vnd.github+json",
1100
- Authorization: `Bearer ${token}`,
1101
- "User-Agent": "serviceme-core"
1102
- }
1083
+ const emailsResp = await this.cfg.fetchImpl("https://api.github.com/user/emails", {
1084
+ headers: {
1085
+ Accept: "application/vnd.github+json",
1086
+ Authorization: `Bearer ${token}`,
1087
+ "User-Agent": "serviceme-core"
1103
1088
  }
1104
- );
1089
+ });
1105
1090
  if (!emailsResp.ok) {
1106
1091
  return null;
1107
1092
  }
@@ -2562,54 +2547,82 @@ var GitClient = class {
2562
2547
  * Rewrite an upstream URL to its proxy form.
2563
2548
  *
2564
2549
  * Examples:
2565
- * rewriteRemoteUrl("medalsoftchina-ms-skills",
2550
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ",
2566
2551
  * "https://github.com/medalsoftchina/ms-skills.git")
2567
- * → "http://localhost:3000/git-proxy/medalsoftchina-ms-skills"
2552
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL21lZGFsc29mdGNoaW5hL21zLXNraWxscy5naXQ"
2568
2553
  *
2569
- * rewriteRemoteUrl("my-team-internal",
2554
+ * rewriteRemoteUrl("repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0",
2570
2555
  * "git@github.com:medalsoftchina/ms-skills.git")
2571
- * → "http://localhost:3000/git-proxy/my-team-internal"
2556
+ * → "http://localhost:3000/git-proxy/repo-aHR0cHM6Ly9naXRodWIuY29tL3RlYW0vaW50ZXJuYWwuZ2l0"
2572
2557
  *
2573
2558
  * The original URL's host/owner is intentionally DROPPED — the proxy
2574
2559
  * knows the upstream from the per-repo config, not from the URL. This
2575
- * means callers can't accidentally route a user-repo URL through the
2576
- * default-repo proxy slot.
2560
+ * means callers can route both default and user repos through the same
2561
+ * dynamic proxy-id scheme.
2577
2562
  */
2578
- rewriteRemoteUrl(_repoId, _originalUrl) {
2563
+ rewriteRemoteUrl(_repoId, originalUrl, useProxy = true) {
2564
+ if (!useProxy) {
2565
+ return originalUrl;
2566
+ }
2579
2567
  return `${this.serverProxyBase}/${_repoId}`;
2580
2568
  }
2581
2569
  /**
2582
2570
  * `git clone <proxyUrl> <localPath>` — initialize a new local repo
2583
2571
  * from the proxy. Returns when the clone succeeds; throws on failure.
2572
+ *
2573
+ * When `branch` is provided, the clone is restricted to that branch
2574
+ * via `git clone -b <branch> -- <proxyUrl> <localPath>` so the local
2575
+ * checkout lands on the configured `repo.branch` instead of the
2576
+ * upstream default branch. The parameter is optional and backward
2577
+ * compatible — omitting it keeps the original behaviour.
2584
2578
  */
2585
- async clone(repoId, originalUrl, localPath) {
2586
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2587
- const args = ["clone", "--", proxyUrl, localPath];
2579
+ async clone(repoId, originalUrl, localPath, branch, useProxy = true) {
2580
+ const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl, useProxy);
2581
+ const args = branch ? ["clone", "-b", branch, "--", proxyUrl, localPath] : ["clone", "--", proxyUrl, localPath];
2588
2582
  await this.runOrThrow(args, { cwd: process.cwd() });
2589
2583
  }
2590
2584
  /**
2591
2585
  * `git fetch <remote> <branch>` + return the resulting commit SHA.
2592
2586
  * Operates on an already-cloned repo at `localPath`.
2587
+ *
2588
+ * When `branch` is provided, the pull target is forced to that branch:
2589
+ * if the local working tree is on a different branch, `git checkout
2590
+ * <branch>` is issued first (git auto-tracks `origin/<branch>`), so
2591
+ * the sync leaves the repo parked on the configured `repo.branch`.
2592
+ * When `branch` is omitted the legacy behaviour is preserved — the
2593
+ * current local branch is fast-forwarded. The parameter is optional
2594
+ * and backward compatible.
2593
2595
  */
2594
- async pull(repoId, localPath) {
2596
+ async pull(repoId, localPath, branch, useProxy = true, originalUrl) {
2595
2597
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2596
2598
  if (!remoteUrl) {
2597
2599
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2598
2600
  }
2599
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2601
+ const targetUrl = useProxy ? remoteUrl : originalUrl ?? remoteUrl;
2602
+ const proxyUrl = this.rewriteRemoteUrl(repoId, targetUrl, useProxy);
2600
2603
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2601
2604
  const before = await this.safeRevParse(localPath);
2602
2605
  await this.runOrThrow(["fetch", "origin"], { cwd: localPath });
2603
2606
  const after = await this.safeRevParse(localPath);
2604
- const branch = await this.currentBranch(localPath);
2605
- if (!branch) {
2607
+ let targetBranch = branch;
2608
+ if (!targetBranch) {
2609
+ targetBranch = await this.currentBranch(localPath);
2610
+ } else {
2611
+ const current = await this.currentBranch(localPath);
2612
+ if (current !== targetBranch) {
2613
+ await this.runOrThrow(["checkout", targetBranch], { cwd: localPath });
2614
+ }
2615
+ }
2616
+ if (!targetBranch) {
2606
2617
  throw new Error(`could not determine current branch at ${localPath}`);
2607
2618
  }
2608
- await this.runOrThrow(["merge", "--ff-only", `origin/${branch}`], { cwd: localPath });
2619
+ await this.runOrThrow(["merge", "--ff-only", `origin/${targetBranch}`], {
2620
+ cwd: localPath
2621
+ });
2609
2622
  return {
2610
2623
  updated: before !== after,
2611
2624
  commitSha: after,
2612
- branch
2625
+ branch: targetBranch
2613
2626
  };
2614
2627
  }
2615
2628
  /**
@@ -2617,12 +2630,12 @@ var GitClient = class {
2617
2630
  * for committing locally first. The server's GitProxy injects the
2618
2631
  * PAT on the way upstream.
2619
2632
  */
2620
- async push(repoId, localPath, branch) {
2633
+ async push(repoId, localPath, branch, useProxy = true) {
2621
2634
  const remoteUrl = await this.getRemoteUrl(localPath, "origin");
2622
2635
  if (!remoteUrl) {
2623
2636
  throw new Error(`no 'origin' remote configured at ${localPath}`);
2624
2637
  }
2625
- const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl);
2638
+ const proxyUrl = this.rewriteRemoteUrl(repoId, remoteUrl, useProxy);
2626
2639
  await this.runOrThrow(["remote", "set-url", "origin", proxyUrl], { cwd: localPath });
2627
2640
  const result = await this.spawner.spawn(
2628
2641
  ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`],
@@ -2642,8 +2655,8 @@ var GitClient = class {
2642
2655
  * `git ls-remote <proxyUrl>` — list advertised branches without
2643
2656
  * cloning. Used by addUserRepo to detect the default branch.
2644
2657
  */
2645
- async lsRemote(repoId, originalUrl) {
2646
- const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl);
2658
+ async lsRemote(repoId, originalUrl, useProxy = true) {
2659
+ const proxyUrl = this.rewriteRemoteUrl(repoId, originalUrl, useProxy);
2647
2660
  const result = await this.spawner.spawn(["ls-remote", "--heads", "--", proxyUrl], {
2648
2661
  cwd: process.cwd()
2649
2662
  });
@@ -3409,16 +3422,36 @@ function isUserRepo(repo) {
3409
3422
 
3410
3423
  // src/repo-manager/index.ts
3411
3424
  function idFromUrl(url) {
3412
- const trimmed = url.trim().replace(/\.git$/, "");
3413
- const last = trimmed.split("/").pop() ?? "";
3414
- const safe = last.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
3425
+ const trimmed = url.trim().replace(/\/+$/, "").replace(/\.git$/, "");
3426
+ const sanitize = (value) => value.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3427
+ let owner = "";
3428
+ let repo = "";
3429
+ if (/^https?:\/\//i.test(trimmed)) {
3430
+ const parsed = new URL(trimmed);
3431
+ const segments = parsed.pathname.split("/").filter(Boolean);
3432
+ repo = segments.at(-1) ?? "";
3433
+ owner = segments.length >= 2 ? segments.at(-2) ?? "" : "";
3434
+ } else if (trimmed.startsWith("git@")) {
3435
+ const colon = trimmed.indexOf(":");
3436
+ const repoPath = colon >= 0 ? trimmed.slice(colon + 1) : "";
3437
+ const segments = repoPath.split("/").filter(Boolean);
3438
+ repo = segments.at(-1) ?? "";
3439
+ owner = segments.length >= 2 ? segments.at(-2) ?? "" : "";
3440
+ } else {
3441
+ repo = trimmed.split("/").pop() ?? "";
3442
+ }
3443
+ const safeRepo = sanitize(repo);
3444
+ const safeOwner = sanitize(owner);
3445
+ const safe = safeOwner ? `${safeOwner}-${safeRepo}` : safeRepo;
3415
3446
  if (!SAFE_REPO_ID_PATTERN.test(safe)) {
3416
- throw new InvalidRepoUrlError(
3417
- `cannot derive a valid repo id from url: ${url}`
3418
- );
3447
+ throw new InvalidRepoUrlError(`cannot derive a valid repo id from url: ${url}`);
3419
3448
  }
3420
3449
  return safe;
3421
3450
  }
3451
+ function proxyRepoId(url) {
3452
+ const encoded = Buffer.from(url.trim(), "utf8").toString("base64url");
3453
+ return `repo-${encoded}`;
3454
+ }
3422
3455
  var RepoManagerError = class extends Error {
3423
3456
  constructor(message) {
3424
3457
  super(message);
@@ -3447,7 +3480,7 @@ var RepoManager = class {
3447
3480
  * but do NOT abort the loop — the UI surfaces per-repo state and the
3448
3481
  * user can retry.
3449
3482
  *
3450
- * Per spec §3.1: 4 default repos; the loop honors `enabled=false` and
3483
+ * Per spec §3.1: iterate all default repos; the loop honors `enabled=false` and
3451
3484
  * disables (rather than removes) repos the user has turned off.
3452
3485
  */
3453
3486
  async ensureDefaults() {
@@ -3464,7 +3497,7 @@ var RepoManager = class {
3464
3497
  try {
3465
3498
  if (!this.skipClone) {
3466
3499
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3467
- await this.git.clone(repo.id, repo.url, localPath);
3500
+ await this.git.clone(repo.id, repo.url, localPath, repo.branch, true);
3468
3501
  }
3469
3502
  await this.store.updateRepo(repo.id, {
3470
3503
  lastSyncStatus: "ok",
@@ -3493,6 +3526,8 @@ var RepoManager = class {
3493
3526
  async pullOne(repoId) {
3494
3527
  const repo = this.store.get(repoId);
3495
3528
  if (!repo) throw new RepoNotInStoreError(repoId);
3529
+ const useProxy = repo.useProxy ?? true;
3530
+ const proxyId = useProxy ? proxyRepoId(repo.url) : repo.id;
3496
3531
  const localPath = getRepoDir(repoId);
3497
3532
  const exists = await this.pathExists(localPath);
3498
3533
  if (exists && !await this.isValidGitRepo(localPath)) {
@@ -3501,7 +3536,7 @@ var RepoManager = class {
3501
3536
  if (!exists || !await this.pathExists(localPath)) {
3502
3537
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3503
3538
  if (!this.skipClone) {
3504
- await this.git.clone(repoId, repo.url, localPath);
3539
+ await this.git.clone(proxyId, repo.url, localPath, repo.branch, useProxy);
3505
3540
  }
3506
3541
  const result = {
3507
3542
  updated: true,
@@ -3515,7 +3550,7 @@ var RepoManager = class {
3515
3550
  return result;
3516
3551
  }
3517
3552
  try {
3518
- const result = await this.git.pull(repoId, localPath);
3553
+ const result = await this.git.pull(proxyId, localPath, repo.branch, useProxy, repo.url);
3519
3554
  await this.store.updateRepo(repoId, {
3520
3555
  lastSyncStatus: "ok",
3521
3556
  lastSyncAt: this.now(),
@@ -3564,15 +3599,15 @@ var RepoManager = class {
3564
3599
  async addUserRepo(input) {
3565
3600
  const url = input.url.trim();
3566
3601
  if (!/^https?:\/\//.test(url) && !url.startsWith("git@")) {
3567
- throw new InvalidRepoUrlError(
3568
- `expected https:// or git@ URL, got: ${url}`
3569
- );
3602
+ throw new InvalidRepoUrlError(`expected https:// or git@ URL, got: ${url}`);
3570
3603
  }
3571
3604
  const id = idFromUrl(url);
3605
+ const useProxy = input.useProxy ?? true;
3606
+ const userProxyId = useProxy ? proxyRepoId(url) : id;
3572
3607
  assertSafeRepoId(id);
3573
3608
  let branch = input.branch?.trim();
3574
3609
  if (!branch) {
3575
- const branches = await this.git.lsRemote(id, url);
3610
+ const branches = await this.git.lsRemote(userProxyId, url, useProxy);
3576
3611
  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"));
3577
3612
  if (!head) {
3578
3613
  throw new InvalidRepoUrlError(
@@ -3590,6 +3625,7 @@ var RepoManager = class {
3590
3625
  url,
3591
3626
  branch,
3592
3627
  enabled: true,
3628
+ useProxy,
3593
3629
  writeEnabled: false,
3594
3630
  source: "user",
3595
3631
  addedAt: this.now()
@@ -3599,7 +3635,7 @@ var RepoManager = class {
3599
3635
  if (!this.skipClone) {
3600
3636
  const localPath = getRepoDir(id);
3601
3637
  await fs8.mkdir(path11.dirname(localPath), { recursive: true });
3602
- await this.git.clone(id, url, localPath);
3638
+ await this.git.clone(userProxyId, url, localPath, branch, useProxy);
3603
3639
  cloned = true;
3604
3640
  }
3605
3641
  return { repo, branch, cloned };
@@ -3672,10 +3708,11 @@ var FIXED_ADDED_AT = "2026-06-29T00:00:00.000Z";
3672
3708
  var DEFAULT_REPO_SEEDS = [
3673
3709
  {
3674
3710
  id: "medalsoftchina-ms-skills",
3675
- name: "MS DevTools Skills (\u7CBE\u9009)",
3711
+ name: "SERVICEME (\u7CBE\u9009)",
3676
3712
  url: "https://github.com/medalsoftchina/ms-skills.git",
3677
3713
  branch: "v2",
3678
3714
  enabled: true,
3715
+ useProxy: true,
3679
3716
  writeEnabled: true,
3680
3717
  source: "default",
3681
3718
  description: "Medalsoft \u7CBE\u9009 skill/agent \u4ED3\u5E93\uFF0C\u53EF\u8BFB\u53EF\u5199"
@@ -3686,6 +3723,7 @@ var DEFAULT_REPO_SEEDS = [
3686
3723
  url: "https://github.com/anthropics/skills.git",
3687
3724
  branch: "main",
3688
3725
  enabled: true,
3726
+ useProxy: true,
3689
3727
  writeEnabled: false,
3690
3728
  source: "default",
3691
3729
  description: "Anthropic \u5B98\u65B9 skills \u793A\u4F8B"
@@ -3696,16 +3734,29 @@ var DEFAULT_REPO_SEEDS = [
3696
3734
  url: "https://github.com/github/awesome-copilot.git",
3697
3735
  branch: "main",
3698
3736
  enabled: true,
3737
+ useProxy: true,
3699
3738
  writeEnabled: false,
3700
3739
  source: "default",
3701
3740
  description: "GitHub Copilot \u793E\u533A\u7CBE\u9009 prompts/agents"
3702
3741
  },
3742
+ {
3743
+ id: "mattpocock-skills",
3744
+ name: "Matt Pocock Skills",
3745
+ url: "https://github.com/mattpocock/skills.git",
3746
+ branch: "main",
3747
+ enabled: true,
3748
+ useProxy: true,
3749
+ writeEnabled: false,
3750
+ source: "default",
3751
+ description: "Matt Pocock \u793E\u533A skills \u4ED3\u5E93"
3752
+ },
3703
3753
  {
3704
3754
  id: "composiohq-awesome-claude-skills",
3705
3755
  name: "Composio Awesome Claude Skills",
3706
3756
  url: "https://github.com/ComposioHQ/awesome-claude-skills.git",
3707
3757
  branch: "main",
3708
3758
  enabled: true,
3759
+ useProxy: true,
3709
3760
  writeEnabled: false,
3710
3761
  source: "default",
3711
3762
  description: "Composio \u7EF4\u62A4\u7684 Claude skills \u96C6\u5408"
@@ -3782,6 +3833,7 @@ var baseRepoFields = {
3782
3833
  url: z.string().url(),
3783
3834
  branch: z.string().min(1).max(200),
3784
3835
  enabled: z.boolean(),
3836
+ useProxy: z.boolean().optional().default(true),
3785
3837
  writeEnabled: z.boolean(),
3786
3838
  addedAt: isoTimestampSchema,
3787
3839
  lastSyncAt: isoTimestampSchema.optional(),