@michengai/dsh-skills-manager 1.2.4 → 1.2.6

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
@@ -900,6 +900,49 @@ function parseBoolValue(raw) {
900
900
  if (v === "false" || v === "no" || v === "off" || v === "0") return false;
901
901
  return void 0;
902
902
  }
903
+ function linkedWriteError(path) {
904
+ return {
905
+ ok: false,
906
+ error: `\u76EE\u5F55\u94FE\u63A5\u4E0D\u80FD\u5220\u9664\u6216\u8986\u76D6: ${path}`,
907
+ code: "error.skill.linked",
908
+ params: { path }
909
+ };
910
+ }
911
+ async function readlinkOrNull(path) {
912
+ try {
913
+ return await fs.readlink(path);
914
+ } catch {
915
+ return null;
916
+ }
917
+ }
918
+ async function isDirectoryLink(path) {
919
+ const st = await lstatOrNull(path);
920
+ if (!st) return false;
921
+ if (st.isSymbolicLink()) {
922
+ try {
923
+ return (await fs.stat(path)).isDirectory();
924
+ } catch {
925
+ return false;
926
+ }
927
+ }
928
+ if (process.platform !== "win32" || !st.isDirectory()) return false;
929
+ return await readlinkOrNull(path) !== null;
930
+ }
931
+ async function linkedDirectoryBundle(directory, name) {
932
+ const bundlePath = entryPath(directory, name);
933
+ if (bundlePath === null || !await isDirectoryLink(bundlePath)) return null;
934
+ const docPath = join(bundlePath, "SKILL.md");
935
+ const docStat = await lstatOrNull(docPath);
936
+ if (!docStat?.isFile() || docStat.isSymbolicLink()) return null;
937
+ return {
938
+ kind: "bundle",
939
+ docPath,
940
+ entryPath: bundlePath,
941
+ realDocPath: await fs.realpath(docPath),
942
+ realEntryPath: await fs.realpath(bundlePath),
943
+ linked: true
944
+ };
945
+ }
903
946
  async function resolveEntry(root, name) {
904
947
  const definition = rootDefinition(root);
905
948
  if (definition && (!definition.mutable || definition.key === "agents")) {
@@ -917,6 +960,8 @@ async function resolveEntry(root, name) {
917
960
  if (!rootStat || !rootStat.isDirectory() || rootStat.isSymbolicLink())
918
961
  return null;
919
962
  const rootReal = await resolvedPath(rootPath);
963
+ const linked = await linkedDirectoryBundle(directory, name);
964
+ if (linked) return linked;
920
965
  const bundleStat = await lstatOrNull(bundlePath);
921
966
  if (bundleStat && bundleStat.isDirectory() && !bundleStat.isSymbolicLink()) {
922
967
  const bundleDoc = join(bundlePath, "SKILL.md");
@@ -1032,7 +1077,18 @@ async function scanEntries(root, options = {}) {
1032
1077
  const rootReal = await resolvedPath(directory);
1033
1078
  for (const it of items) {
1034
1079
  try {
1035
- if (it.isSymbolicLink()) continue;
1080
+ if (it.isSymbolicLink() || process.platform === "win32" && it.isDirectory()) {
1081
+ const linked = await linkedDirectoryBundle(directory, it.name);
1082
+ if (linked) {
1083
+ const doc = options.metadataOnly ? { map: {}, hasFrontmatter: false } : parseSkillDoc(await fs.readFile(linked.realDocPath, "utf8"));
1084
+ byName.set(it.name, {
1085
+ ...entryOf(it.name, "bundle", linked.docPath, doc),
1086
+ ...linked
1087
+ });
1088
+ continue;
1089
+ }
1090
+ if (it.isSymbolicLink()) continue;
1091
+ }
1036
1092
  if (it.isDirectory() && entryPath(directory, it.name) !== null) {
1037
1093
  const docPath = join(directory, it.name, "SKILL.md");
1038
1094
  const st = await lstatOrNull(docPath);
@@ -1769,6 +1825,9 @@ async function deleteSkill(root, name, log, options = {}) {
1769
1825
  if ((await lstatOrNull(target.path))?.isSymbolicLink())
1770
1826
  return { ok: false, error: `\u6280\u80FD\u76EE\u5F55\u4E0D\u5B89\u5168\uFF0C\u62D2\u7EDD\u5199\u5165: ${target.path}`, code: "error.root.unsafe", params: { path: target.path } };
1771
1827
  }
1828
+ } else {
1829
+ const bundle = entryPath(definition.path, name);
1830
+ if (bundle && await isDirectoryLink(bundle)) return linkedWriteError(bundle);
1772
1831
  }
1773
1832
  const id = options.repositoryTrash?.id || options.catalogTrash?.id || `${Date.now()}-${randomUUID()}`;
1774
1833
  if (!/^\d+-[a-f0-9-]{36}$/.test(id)) throw codedError("\u56DE\u6536\u7AD9\u6761\u76EE\u7F16\u53F7\u975E\u6CD5", "error.trash.invalid", { id });
@@ -2136,6 +2195,10 @@ async function copyToTemporary(source, target, isDir) {
2136
2195
  }
2137
2196
  }
2138
2197
  async function replaceWithCopy(source, dest, isDir, existing = []) {
2198
+ for (const path of [dest, ...existing]) {
2199
+ if (await isDirectoryLink(path))
2200
+ throw codedError(`\u76EE\u5F55\u94FE\u63A5\u4E0D\u80FD\u5220\u9664\u6216\u8986\u76D6: ${path}`, "error.skill.linked", { path });
2201
+ }
2139
2202
  const stage = await copyToTemporary(source, dest, isDir);
2140
2203
  const backups = [];
2141
2204
  try {
@@ -2279,6 +2342,16 @@ async function importSkill(source, log, options = {}) {
2279
2342
  });
2280
2343
  continue;
2281
2344
  }
2345
+ const linkPath = join(targetRoot, c.kebab);
2346
+ if (await isDirectoryLink(linkPath)) {
2347
+ failed.push({
2348
+ source: c.source,
2349
+ error: `\u76EE\u5F55\u94FE\u63A5\u4E0D\u80FD\u5220\u9664\u6216\u8986\u76D6: ${linkPath}`,
2350
+ code: "error.skill.linked",
2351
+ params: { path: linkPath }
2352
+ });
2353
+ continue;
2354
+ }
2282
2355
  const dest = c.isDir ? join(targetRoot, c.kebab) : join(targetRoot, `${c.kebab}.md`);
2283
2356
  const paths = [
2284
2357
  join(targetRoot, c.kebab),
@@ -2646,7 +2719,7 @@ async function skillDetail(key, name, options = {}) {
2646
2719
  frontmatter: summary.hasFrontmatter ? Object.fromEntries(Object.entries(doc.map)) : null,
2647
2720
  diagnostics: summary.diagnostics,
2648
2721
  loadable: summary.loadable,
2649
- sourceReadOnly: !root.mutable
2722
+ sourceReadOnly: !root.mutable || entry.linked === true
2650
2723
  };
2651
2724
  }
2652
2725
  async function listProviderCandidates(options = {}) {
@@ -2673,7 +2746,7 @@ async function listProviderCandidates(options = {}) {
2673
2746
  const project = root.scope === "project";
2674
2747
  const policyOnly = root.mutable && root.key !== "agents";
2675
2748
  const overlayRank = group.length > 1 ? Math.min(...group.map((item) => Math.min(item.root.rank, item.entry.providerRank ?? item.root.rank))) - 1 : void 0;
2676
- const needsOverlay = !policyOnly || group.length > 1 || policy.override !== void 0 || !entry.invocationPolicyValid || policyResult.writable === false;
2749
+ const needsOverlay = !policyOnly || group.length > 1 || policy.override !== void 0 || !entry.invocationPolicyValid || policyResult.writable === false || !policy.sourceEnabled;
2677
2750
  if (!needsOverlay) continue;
2678
2751
  candidates.push({
2679
2752
  name: canonicalSkillName({ root, entry, policy }),
@@ -2700,8 +2773,8 @@ async function listProviderCandidates(options = {}) {
2700
2773
  metadata: {
2701
2774
  dshSkillsManager: {
2702
2775
  root: root.key,
2703
- readOnly: !root.mutable,
2704
- sourceReadOnly: !root.mutable,
2776
+ readOnly: !root.mutable || entry.linked === true,
2777
+ sourceReadOnly: !root.mutable || entry.linked === true,
2705
2778
  policyOnly
2706
2779
  }
2707
2780
  }
@@ -2904,7 +2977,8 @@ async function state(options = {}) {
2904
2977
  effectiveUserInvocable: policy.userInvocable,
2905
2978
  diagnostics: e.diagnostics,
2906
2979
  path: e.docPath,
2907
- categoryId: policyResult.state.categoryMembers[root.key]?.[e.name] || null
2980
+ categoryId: policyResult.state.categoryMembers[root.key]?.[e.name] || null,
2981
+ linked: e.linked === true
2908
2982
  });
2909
2983
  all.push({
2910
2984
  root,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "DSH Skills Manager — 在 DeepSeek Harness 中统一加载并安全管理本机 Agent Skills · Load and safely manage local Agent Skills in DSH",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {