@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/CHANGELOG.md +11 -0
- package/CHANGELOG.zh-CN.md +11 -0
- package/README.md +10 -2
- package/README.zh-CN.md +10 -2
- package/assets/icon.svg +29 -0
- package/lib/client.js +14 -6
- package/lib/core.js +335 -44
- package/lib/index.js +59 -47
- package/lib/repository-updates.js +5 -5
- package/package.json +4 -3
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
|
-
|
|
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
|
-
|
|
49
|
-
const ranks = new Map(
|
|
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
|
-
|
|
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
|
|
592
|
-
const paths =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
674
|
+
`${JSON.stringify({ version: ROOTS_CONFIG_VERSION, paths, customSources }, null, 2)}
|
|
602
675
|
`
|
|
603
676
|
);
|
|
604
|
-
config.paths =
|
|
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 (
|
|
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
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
|
2742
|
+
await renameWithRetry(path, backup);
|
|
2460
2743
|
backups.push({ path, backup });
|
|
2461
2744
|
}
|
|
2462
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|