@michengai/dsh-skills-manager 1.2.19 → 1.2.21

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/lib/core.js CHANGED
@@ -39,14 +39,27 @@ const EXTERNAL_SOURCE_ORDER = [
39
39
  "lingma",
40
40
  "antigravity"
41
41
  ];
42
+ const CUSTOM_SOURCE_PREFIX = "custom:";
43
+ const CUSTOM_SOURCE_ID_RE = /^custom:[a-z0-9]+(?:-[a-z0-9]+)*$/;
44
+ const MAX_CUSTOM_SOURCES = 32;
45
+ const MAX_CUSTOM_LABEL_LENGTH = 48;
46
+ const MAX_SOURCE_SCAN_DEPTH = 2;
47
+ const MAX_SOURCE_SCAN_CANDIDATES = 40;
48
+ function isCustomSourceKey(key) {
49
+ return typeof key === "string" && key.startsWith(CUSTOM_SOURCE_PREFIX);
50
+ }
42
51
  function rankSources(sources, startRank) {
43
52
  const order = (root) => EXTERNAL_SOURCE_ORDER.indexOf(root.key);
44
53
  const external = sources.filter((root) => !root.native);
45
- for (const root of external) {
54
+ const custom = external.filter((root) => isCustomSourceKey(root.key));
55
+ const preset = external.filter((root) => !isCustomSourceKey(root.key));
56
+ for (const root of preset) {
46
57
  if (order(root) < 0) throw new Error(`\u6280\u80FD\u6765\u6E90\u672A\u767B\u8BB0\u5230 EXTERNAL_SOURCE_ORDER: ${root.key}`);
47
58
  }
48
- external.sort((a, b) => order(a) - order(b));
49
- const ranks = new Map(external.map((root, index) => [root.key, startRank + index * 10]));
59
+ preset.sort((a, b) => order(a) - order(b));
60
+ const ranks = new Map(preset.map((root, index) => [root.key, startRank + index * 10]));
61
+ const customBase = startRank + preset.length * 10;
62
+ custom.forEach((root, index) => ranks.set(root.key, customBase + index * 10));
50
63
  return sources.map((root) => ({ ...root, rank: root.native ? root.rank : ranks.get(root.key) })).sort((a, b) => a.rank - b.rank);
51
64
  }
52
65
  const PROJECT_SOURCES = rankSources([
@@ -341,6 +354,18 @@ function userRoots() {
341
354
  root.defaultPath = root.path;
342
355
  root.path = override;
343
356
  }
357
+ for (const custom of loadRootPathConfig().customSources) {
358
+ roots.push({
359
+ key: custom.id,
360
+ path: custom.path,
361
+ label: custom.label,
362
+ ...custom.group ? { group: custom.group } : {},
363
+ mutable: false,
364
+ toggleable: true,
365
+ native: false,
366
+ custom: true
367
+ });
368
+ }
344
369
  return rankSources(roots, 500);
345
370
  }
346
371
  function projectIdentity(path) {
@@ -536,6 +561,7 @@ function rootPathError(code, message, params = {}) {
536
561
  function corruptRootPathConfig(file, reason) {
537
562
  return {
538
563
  paths: {},
564
+ customSources: [],
539
565
  warnings: [
540
566
  {
541
567
  code: "warning.rootPath.corrupt",
@@ -545,6 +571,49 @@ function corruptRootPathConfig(file, reason) {
545
571
  ]
546
572
  };
547
573
  }
574
+ function customSourceSlug(label) {
575
+ const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);
576
+ return slug || "source";
577
+ }
578
+ function normalizeCustomLabel(value) {
579
+ const label = typeof value === "string" ? value.trim() : "";
580
+ return label && label.length <= MAX_CUSTOM_LABEL_LENGTH ? label : "";
581
+ }
582
+ function parseCustomSources(value, warnings) {
583
+ if (value === void 0) return [];
584
+ if (!Array.isArray(value)) {
585
+ warnings.push({
586
+ code: "warning.rootPath.invalid",
587
+ params: { path: "customSources" },
588
+ error: "\u81EA\u5B9A\u4E49\u6765\u6E90\u914D\u7F6E\u5FC5\u987B\u662F\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: customSources"
589
+ });
590
+ return [];
591
+ }
592
+ const accepted = [];
593
+ const ids = /* @__PURE__ */ new Set();
594
+ const paths = /* @__PURE__ */ new Set();
595
+ for (const entry of value.slice(0, MAX_CUSTOM_SOURCES)) {
596
+ const record = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
597
+ const id = record && typeof record.id === "string" ? record.id.trim() : "";
598
+ const label = record ? normalizeCustomLabel(record.label) : "";
599
+ const requested = record && typeof record.path === "string" ? record.path.trim() : "";
600
+ const group = record && typeof record.group === "string" ? normalizeCustomLabel(record.group) : "";
601
+ const identity = requested && isAbsolute(requested) ? pathIdentity(requested) : "";
602
+ if (!record || !CUSTOM_SOURCE_ID_RE.test(id) || !label || !requested || !isAbsolute(requested) || requested.length > MAX_ROOT_PATH_LENGTH || ids.has(id) || paths.has(identity)) {
603
+ const key = id || (record && typeof record.id === "string" ? record.id : "customSources");
604
+ warnings.push({
605
+ code: "warning.rootPath.invalid",
606
+ params: { path: key },
607
+ error: `\u81EA\u5B9A\u4E49\u6765\u6E90\u914D\u7F6E\u9879\u65E0\u6548\uFF0C\u5DF2\u5FFD\u7565: ${key}`
608
+ });
609
+ continue;
610
+ }
611
+ ids.add(id);
612
+ paths.add(identity);
613
+ accepted.push({ id, label, path: resolve(requested), ...group ? { group } : {} });
614
+ }
615
+ return accepted;
616
+ }
548
617
  function readRootPathConfigFile() {
549
618
  const file = rootPathConfigPath();
550
619
  let raw;
@@ -552,7 +621,7 @@ function readRootPathConfigFile() {
552
621
  raw = readFileSync(file, "utf8");
553
622
  } catch (caught) {
554
623
  const error = caught;
555
- if (error && error.code === "ENOENT") return { paths: {}, warnings: [] };
624
+ if (error && error.code === "ENOENT") return { paths: {}, customSources: [], warnings: [] };
556
625
  return corruptRootPathConfig(file, "\u6280\u80FD\u76EE\u5F55\u914D\u7F6E\u4E0D\u53EF\u8BFB");
557
626
  }
558
627
  let document;
@@ -578,7 +647,11 @@ function readRootPathConfigFile() {
578
647
  }
579
648
  accepted[key] = resolve(requested);
580
649
  }
581
- return { paths: accepted, warnings };
650
+ const customSources = parseCustomSources(
651
+ document && typeof document === "object" && !Array.isArray(document) ? document.customSources : void 0,
652
+ warnings
653
+ );
654
+ return { paths: accepted, customSources, warnings };
582
655
  }
583
656
  function loadRootPathConfig() {
584
657
  if (!rootPathConfig) rootPathConfig = readRootPathConfigFile();
@@ -588,25 +661,97 @@ function reloadRootPathOverrides() {
588
661
  rootPathConfig = null;
589
662
  return loadRootPathConfig();
590
663
  }
591
- async function writeRootPathOverrides(config, key, value) {
592
- const paths = { ...config.paths };
593
- if (value === null) delete paths[key];
594
- else paths[key] = value;
595
- const nextPaths = Object.fromEntries(
596
- Object.entries(paths).sort(([left], [right]) => left.localeCompare(right))
664
+ async function writeRootPathConfig(config, next) {
665
+ const paths = Object.fromEntries(
666
+ Object.entries(next.paths ?? config.paths).sort(
667
+ ([left], [right]) => left.localeCompare(right)
668
+ )
597
669
  );
670
+ const customSources = next.customSources ?? config.customSources;
598
671
  await fs.mkdir(managerHomePath(), { recursive: true });
599
672
  await writeFileAtomically(
600
673
  rootPathConfigPath(),
601
- `${JSON.stringify({ version: ROOTS_CONFIG_VERSION, paths: nextPaths }, null, 2)}
674
+ `${JSON.stringify({ version: ROOTS_CONFIG_VERSION, paths, customSources }, null, 2)}
602
675
  `
603
676
  );
604
- config.paths = nextPaths;
677
+ config.paths = paths;
678
+ config.customSources = customSources;
605
679
  config.warnings = [];
606
680
  }
681
+ async function writeRootPathOverrides(config, key, value) {
682
+ const paths = { ...config.paths };
683
+ if (value === null) delete paths[key];
684
+ else paths[key] = value;
685
+ await writeRootPathConfig(config, { paths });
686
+ }
687
+ async function validateSourceRootPath(requested, options = {}) {
688
+ if (!isAbsolute(requested))
689
+ return rootPathError(
690
+ "error.rootPath.absolute",
691
+ `\u6280\u80FD\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84: ${requested}`,
692
+ { path: requested }
693
+ );
694
+ if (requested.length > MAX_ROOT_PATH_LENGTH)
695
+ return rootPathError(
696
+ "error.rootPath.tooLong",
697
+ `\u6280\u80FD\u76EE\u5F55\u8DEF\u5F84\u8D85\u8FC7 ${MAX_ROOT_PATH_LENGTH} \u4E2A\u5B57\u7B26: ${requested}`,
698
+ { path: requested }
699
+ );
700
+ let info;
701
+ try {
702
+ info = await fs.stat(requested);
703
+ } catch {
704
+ return rootPathError(
705
+ "error.rootPath.notFound",
706
+ `\u6280\u80FD\u76EE\u5F55\u4E0D\u5B58\u5728: ${requested}`,
707
+ { path: requested }
708
+ );
709
+ }
710
+ if (!info.isDirectory())
711
+ return rootPathError(
712
+ "error.rootPath.notDirectory",
713
+ `\u6280\u80FD\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\u5939: ${requested}`,
714
+ { path: requested }
715
+ );
716
+ const candidate = await comparisonPath(requested);
717
+ const managerHome = await comparisonPath(managerHomePath());
718
+ if (isSameOrDescendant(managerHome, candidate) || isSameOrDescendant(candidate, managerHome))
719
+ return rootPathError(
720
+ "error.rootPath.reserved",
721
+ `\u6280\u80FD\u76EE\u5F55\u4E0D\u80FD\u4E0E\u6280\u80FD\u7BA1\u7406\u5668\u7684\u72B6\u6001\u76EE\u5F55\u91CD\u53E0: ${requested}`,
722
+ { path: requested }
723
+ );
724
+ for (const other of userRoots()) {
725
+ if (other.key === options.excludeKey) continue;
726
+ const otherPath = await comparisonPath(other.path);
727
+ if (isSameOrDescendant(candidate, otherPath) || isSameOrDescendant(otherPath, candidate))
728
+ return rootPathError(
729
+ "error.rootPath.overlap",
730
+ `\u6280\u80FD\u76EE\u5F55\u4E0D\u80FD\u4E0E\u5176\u5B83\u6280\u80FD\u6765\u6E90\u91CD\u53E0: ${other.path}`,
731
+ { path: requested }
732
+ );
733
+ }
734
+ return { ok: true, path: resolve(requested) };
735
+ }
607
736
  async function setRootPath(key, value) {
608
737
  const rootKey = typeof key === "string" ? key.trim() : "";
609
738
  const current = userRoots().find((root) => root.key === rootKey);
739
+ if (rootKey && isCustomSourceKey(rootKey)) {
740
+ if (!current)
741
+ return rootPathError(
742
+ "error.rootPath.unsupported",
743
+ `\u8BE5\u6280\u80FD\u6765\u6E90\u4E0D\u652F\u6301\u4FEE\u6539\u76EE\u5F55: ${rootKey}`,
744
+ { root: rootKey }
745
+ );
746
+ if (value === null || value === void 0 || typeof value === "string" && value.trim() === "") {
747
+ const removed = await removeCustomSource(rootKey);
748
+ if (removed.ok === false) return removed;
749
+ return { ok: true, root: { key: rootKey, path: current.path } };
750
+ }
751
+ const updated = await updateCustomSource(rootKey, { path: value });
752
+ if (updated.ok === false) return updated;
753
+ return { ok: true, root: { key: rootKey, path: updated.source.path } };
754
+ }
610
755
  if (!rootKey || !current || !OVERRIDABLE_ROOT_KEYS.has(rootKey))
611
756
  return rootPathError(
612
757
  "error.rootPath.unsupported",
@@ -626,10 +771,154 @@ async function setRootPath(key, value) {
626
771
  { path: String(value) }
627
772
  );
628
773
  const requested = value.trim();
629
- if (!isAbsolute(requested))
774
+ if (pathIdentity(requested) === pathIdentity(defaultPath)) {
775
+ await writeRootPathOverrides(config, rootKey, null);
776
+ return { ok: true, root: { key: rootKey, path: defaultPath } };
777
+ }
778
+ const checked = await validateSourceRootPath(requested, { excludeKey: rootKey });
779
+ if (checked.ok === false) return checked;
780
+ await writeRootPathOverrides(config, rootKey, checked.path);
781
+ return { ok: true, root: { key: rootKey, path: checked.path, defaultPath } };
782
+ }
783
+ function nextCustomSourceId(label, taken) {
784
+ const base = CUSTOM_SOURCE_PREFIX + customSourceSlug(label);
785
+ if (!taken.has(base)) return base;
786
+ for (let index = 2; index <= MAX_CUSTOM_SOURCES + 1; index++) {
787
+ const candidate = `${base}-${index}`;
788
+ if (!taken.has(candidate)) return candidate;
789
+ }
790
+ return "";
791
+ }
792
+ async function addCustomSources(input) {
793
+ const config = loadRootPathConfig();
794
+ const list = Array.isArray(input) ? input : [input];
795
+ if (!list.length)
796
+ return rootPathError("error.customSource.empty", "\u6CA1\u6709\u8981\u6DFB\u52A0\u7684\u6280\u80FD\u6765\u6E90", {});
797
+ const taken = new Set(userRoots().map((root) => root.key));
798
+ const next = [...config.customSources];
799
+ const added = [];
800
+ for (const entry of list) {
801
+ if (next.length >= MAX_CUSTOM_SOURCES)
802
+ return rootPathError(
803
+ "error.customSource.limit",
804
+ `\u81EA\u5B9A\u4E49\u6765\u6E90\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650 ${MAX_CUSTOM_SOURCES} \u4E2A`,
805
+ { limit: MAX_CUSTOM_SOURCES }
806
+ );
807
+ const record = entry && typeof entry === "object" ? entry : null;
808
+ const requested = record && typeof record.path === "string" ? record.path.trim() : "";
809
+ if (requested && isAbsolute(requested) && next.some((item) => pathIdentity(item.path) === pathIdentity(requested)))
810
+ continue;
811
+ const checked = await validateSourceRootPath(requested);
812
+ if (checked.ok === false) return checked;
813
+ const label = record && normalizeCustomLabel(record.label) || basename(checked.path);
814
+ if (!label)
815
+ return rootPathError(
816
+ "error.customSource.label",
817
+ `\u81EA\u5B9A\u4E49\u6765\u6E90\u5FC5\u987B\u6709\u663E\u793A\u540D: ${checked.path}`,
818
+ { path: checked.path }
819
+ );
820
+ if (next.some((item) => pathIdentity(item.path) === pathIdentity(checked.path)))
821
+ continue;
822
+ const id = nextCustomSourceId(label, taken);
823
+ if (!id)
824
+ return rootPathError(
825
+ "error.customSource.limit",
826
+ `\u81EA\u5B9A\u4E49\u6765\u6E90\u6570\u91CF\u5DF2\u8FBE\u4E0A\u9650 ${MAX_CUSTOM_SOURCES} \u4E2A`,
827
+ { limit: MAX_CUSTOM_SOURCES }
828
+ );
829
+ taken.add(id);
830
+ const group = record ? normalizeCustomLabel(record.group) : "";
831
+ const source = {
832
+ id,
833
+ label,
834
+ path: checked.path,
835
+ ...group ? { group } : {}
836
+ };
837
+ next.push(source);
838
+ added.push(source);
839
+ }
840
+ if (added.length) await writeRootPathConfig(config, { customSources: next });
841
+ return { ok: true, added };
842
+ }
843
+ async function updateCustomSource(key, patch) {
844
+ const id = typeof key === "string" ? key.trim() : "";
845
+ const config = loadRootPathConfig();
846
+ const index = config.customSources.findIndex((item) => item.id === id);
847
+ if (!CUSTOM_SOURCE_ID_RE.test(id) || index < 0)
848
+ return rootPathError(
849
+ "error.customSource.unknown",
850
+ `\u672A\u767B\u8BB0\u7684\u81EA\u5B9A\u4E49\u6765\u6E90: ${id || "(\u7A7A)"}`,
851
+ { root: id }
852
+ );
853
+ const record = patch && typeof patch === "object" ? patch : {};
854
+ const current = config.customSources[index];
855
+ const label = record.label === void 0 ? current.label : normalizeCustomLabel(record.label);
856
+ if (!label)
857
+ return rootPathError(
858
+ "error.customSource.label",
859
+ `\u81EA\u5B9A\u4E49\u6765\u6E90\u5FC5\u987B\u6709\u663E\u793A\u540D: ${String(record.label)}`,
860
+ { path: String(record.label) }
861
+ );
862
+ let path = current.path;
863
+ if (record.path !== void 0) {
864
+ const requested = typeof record.path === "string" ? record.path.trim() : "";
865
+ const checked = await validateSourceRootPath(requested, { excludeKey: id });
866
+ if (checked.ok === false) return checked;
867
+ path = checked.path;
868
+ }
869
+ const group = record.group === void 0 ? current.group : normalizeCustomLabel(record.group) || void 0;
870
+ const source = { id, label, path, ...group ? { group } : {} };
871
+ const next = config.customSources.map(
872
+ (item, position) => position === index ? source : item
873
+ );
874
+ await writeRootPathConfig(config, { customSources: next });
875
+ return { ok: true, source };
876
+ }
877
+ async function removeCustomSource(key) {
878
+ const id = typeof key === "string" ? key.trim() : "";
879
+ const config = loadRootPathConfig();
880
+ if (!config.customSources.some((item) => item.id === id))
881
+ return rootPathError(
882
+ "error.customSource.unknown",
883
+ `\u672A\u767B\u8BB0\u7684\u81EA\u5B9A\u4E49\u6765\u6E90: ${id || "(\u7A7A)"}`,
884
+ { root: id }
885
+ );
886
+ await writeRootPathConfig(config, {
887
+ customSources: config.customSources.filter((item) => item.id !== id)
888
+ });
889
+ return { ok: true, removed: id };
890
+ }
891
+ async function collectSkillDirectories(root) {
892
+ const found = [];
893
+ async function walk(directory, depth, segments) {
894
+ if (found.length >= MAX_SOURCE_SCAN_CANDIDATES) return;
895
+ const skillsDir = join(directory, "skills");
896
+ if ((await lstatOrNull(skillsDir))?.isDirectory())
897
+ found.push({ path: skillsDir, depth, segments });
898
+ if (depth >= MAX_SOURCE_SCAN_DEPTH) return;
899
+ let entries;
900
+ try {
901
+ entries = await fs.readdir(directory, { withFileTypes: true });
902
+ } catch {
903
+ return;
904
+ }
905
+ for (const entry of entries) {
906
+ if (found.length >= MAX_SOURCE_SCAN_CANDIDATES) return;
907
+ if (!entry.isDirectory()) continue;
908
+ if (entry.name.startsWith(".") || entry.name === "skills" || entry.name === "node_modules")
909
+ continue;
910
+ await walk(join(directory, entry.name), depth + 1, segments.concat(entry.name));
911
+ }
912
+ }
913
+ await walk(root, 0, []);
914
+ return found;
915
+ }
916
+ async function scanCustomSourceCandidates(inputPath) {
917
+ const requested = typeof inputPath === "string" ? inputPath.trim() : "";
918
+ if (!requested || !isAbsolute(requested))
630
919
  return rootPathError(
631
920
  "error.rootPath.absolute",
632
- `\u6280\u80FD\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84: ${requested}`,
921
+ `\u6280\u80FD\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84: ${requested || "(\u7A7A)"}`,
633
922
  { path: requested }
634
923
  );
635
924
  if (requested.length > MAX_ROOT_PATH_LENGTH)
@@ -638,10 +927,6 @@ async function setRootPath(key, value) {
638
927
  `\u6280\u80FD\u76EE\u5F55\u8DEF\u5F84\u8D85\u8FC7 ${MAX_ROOT_PATH_LENGTH} \u4E2A\u5B57\u7B26: ${requested}`,
639
928
  { path: requested }
640
929
  );
641
- if (pathIdentity(requested) === pathIdentity(defaultPath)) {
642
- await writeRootPathOverrides(config, rootKey, null);
643
- return { ok: true, root: { key: rootKey, path: defaultPath } };
644
- }
645
930
  let info;
646
931
  try {
647
932
  info = await fs.stat(requested);
@@ -658,26 +943,24 @@ async function setRootPath(key, value) {
658
943
  `\u6280\u80FD\u76EE\u5F55\u4E0D\u662F\u6587\u4EF6\u5939: ${requested}`,
659
944
  { path: requested }
660
945
  );
661
- const candidate = await comparisonPath(requested);
662
- const managerHome = await comparisonPath(managerHomePath());
663
- if (isSameOrDescendant(managerHome, candidate) || isSameOrDescendant(candidate, managerHome))
664
- return rootPathError(
665
- "error.rootPath.reserved",
666
- `\u6280\u80FD\u76EE\u5F55\u4E0D\u80FD\u4E0E\u6280\u80FD\u7BA1\u7406\u5668\u7684\u72B6\u6001\u76EE\u5F55\u91CD\u53E0: ${requested}`,
667
- { path: requested }
668
- );
669
- for (const other of userRoots()) {
670
- if (other.key === rootKey) continue;
671
- const otherPath = await comparisonPath(other.path);
672
- if (isSameOrDescendant(candidate, otherPath) || isSameOrDescendant(otherPath, candidate))
673
- return rootPathError(
674
- "error.rootPath.overlap",
675
- `\u6280\u80FD\u76EE\u5F55\u4E0D\u80FD\u4E0E\u5176\u5B83\u6280\u80FD\u6765\u6E90\u91CD\u53E0: ${other.path}`,
676
- { path: requested }
677
- );
946
+ const root = resolve(requested);
947
+ const product = basename(root) || root;
948
+ const registered = new Set(userRoots().map((item) => pathIdentity(item.path)));
949
+ const candidates = [];
950
+ for (const item of await collectSkillDirectories(root)) {
951
+ const skills = await discoverReadonlyEntries(item.path);
952
+ candidates.push({
953
+ path: item.path,
954
+ label: item.depth === 0 ? product : [product].concat(item.segments).join(" / "),
955
+ group: product,
956
+ depth: item.depth,
957
+ skillCount: skills.entries.length,
958
+ // 与已有来源重叠的候选仍然列出来,但界面要禁止登记,否则会绕过停用策略。
959
+ conflict: await overlapsUserSkillRoot(item.path),
960
+ registered: registered.has(pathIdentity(item.path))
961
+ });
678
962
  }
679
- await writeRootPathOverrides(config, rootKey, resolve(requested));
680
- return { ok: true, root: { key: rootKey, path: resolve(requested), defaultPath } };
963
+ return { ok: true, root, candidates };
681
964
  }
682
965
  async function browseDirectories(inputPath) {
683
966
  const requested = String(inputPath == null ? "" : inputPath).trim();
@@ -2456,17 +2739,17 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
2456
2739
  try {
2457
2740
  for (const path of existing) {
2458
2741
  const backup = temporaryPath(path, "backup");
2459
- await fs.rename(path, backup);
2742
+ await renameWithRetry(path, backup);
2460
2743
  backups.push({ path, backup });
2461
2744
  }
2462
- await fs.rename(stage, dest);
2745
+ await renameWithRetry(stage, dest);
2463
2746
  } catch (caught) {
2464
2747
  const error = caught;
2465
2748
  await fs.rm(stage, { recursive: true, force: true }).catch(() => void 0);
2466
2749
  const rollbackFailures = [];
2467
2750
  for (const item of backups.reverse()) {
2468
2751
  try {
2469
- await fs.rename(item.backup, item.path);
2752
+ await renameWithRetry(item.backup, item.path);
2470
2753
  } catch (caught2) {
2471
2754
  const rollbackError = caught2;
2472
2755
  rollbackFailures.push(item.backup);
@@ -2848,7 +3131,7 @@ async function prepareUploadedSource(sessionRoot, input) {
2848
3131
  const batchRoot = join(sessionRoot, "batch");
2849
3132
  const wrappedRoot = join(batchRoot, skillName);
2850
3133
  await fs.mkdir(batchRoot, { recursive: true });
2851
- await fs.rename(contentRoot, wrappedRoot);
3134
+ await renameWithRetry(contentRoot, wrappedRoot);
2852
3135
  return batchRoot;
2853
3136
  }
2854
3137
  async function importUploadedSkill(input, log, options = {}) {
@@ -2919,7 +3202,7 @@ description: ${yamlString(description)}
2919
3202
  ${body}
2920
3203
  `;
2921
3204
  await fs.writeFile(join(stage, "SKILL.md"), content, "utf8");
2922
- await fs.rename(stage, target);
3205
+ await renameWithRetry(stage, target);
2923
3206
  } catch (caught) {
2924
3207
  const error = caught;
2925
3208
  await fs.rm(stage, { recursive: true, force: true }).catch(() => void 0);
@@ -3211,7 +3494,7 @@ async function state(options = {}) {
3211
3494
  params: { path: root.path },
3212
3495
  error: `\u6280\u80FD\u626B\u63CF\u8FBE\u5230\u904D\u5386\u4E0A\u9650\uFF0C\u90E8\u5206\u6280\u80FD\u672A\u663E\u793A: ${root.path}`
3213
3496
  });
3214
- if (root.defaultPath && !exists)
3497
+ if ((root.defaultPath || root.custom) && !exists)
3215
3498
  result.warnings.push({
3216
3499
  code: "warning.rootPath.missing",
3217
3500
  params: { path: root.path },
@@ -3261,6 +3544,8 @@ async function state(options = {}) {
3261
3544
  native: root.native,
3262
3545
  rank: root.rank,
3263
3546
  scope: root.scope || "user",
3547
+ ...root.custom ? { custom: true } : {},
3548
+ ...root.group ? { group: root.group } : {},
3264
3549
  ...root.projectRoot ? {
3265
3550
  projectRoot: root.projectRoot,
3266
3551
  projectName: root.projectName,
@@ -3310,7 +3595,9 @@ async function state(options = {}) {
3310
3595
  return result;
3311
3596
  }
3312
3597
  export {
3598
+ CUSTOM_SOURCE_PREFIX,
3313
3599
  KEBAB_RE,
3600
+ addCustomSources,
3314
3601
  assignSkillCategory,
3315
3602
  browseDirectories,
3316
3603
  checkedRepositoryRootDefinition,
@@ -3326,6 +3613,7 @@ export {
3326
3613
  importSkill,
3327
3614
  importUploadedSkill,
3328
3615
  indexRegisteredWorkspaces,
3616
+ isCustomSourceKey,
3329
3617
  listProviderCandidates,
3330
3618
  listTrash,
3331
3619
  listWorkspaces,
@@ -3340,6 +3628,7 @@ export {
3340
3628
  projectRoots,
3341
3629
  readManagerState,
3342
3630
  reloadRootPathOverrides,
3631
+ removeCustomSource,
3343
3632
  renameCategory,
3344
3633
  renameWithRetry,
3345
3634
  repositoryDestinations,
@@ -3349,6 +3638,7 @@ export {
3349
3638
  resolveWorkspaceTarget,
3350
3639
  restoreTrash,
3351
3640
  rootPathConfigPath,
3641
+ scanCustomSourceCandidates,
3352
3642
  scanEntries,
3353
3643
  setCategoryEnabled,
3354
3644
  setOnlineUpdate,
@@ -3361,6 +3651,7 @@ export {
3361
3651
  toKebab,
3362
3652
  trashRootPath,
3363
3653
  unquote,
3654
+ updateCustomSource,
3364
3655
  userRoots,
3365
3656
  workspaceSkills
3366
3657
  };