dsh-skill-hub 0.3.12 → 0.3.13

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/CONTRIBUTING.md CHANGED
@@ -39,7 +39,7 @@ src/client/locales/ dictionaries by view (common/skills/market/sources/detai
39
39
  ```bash
40
40
  npm install
41
41
  npm run typecheck # tsc --noEmit
42
- npm test # vitest (217 tests across 13 suites)
42
+ npm test # vitest (233 tests across 14 suites)
43
43
  npm run build # tsc declarations + tsdown bundles (lib/index.js + lib/client.js)
44
44
  ```
45
45
 
@@ -68,10 +68,12 @@ npm run build # tsc declarations + tsdown bundles (lib/index.js + lib/clie
68
68
  ```bash
69
69
  # after a change:
70
70
  npm run build
71
- cp lib/index.js lib/client.js ~/.dsh/profiles/web/node_modules/dsh-skill-hub/lib/
72
71
  # restart the dsh web process, then verify Settings → 技能 and Settings → 插件 → Skill Hub
73
72
  ```
74
73
 
74
+ When the web profile installs this repo as a link (`"dsh-skill-hub": "link:/path/to/repo"`, the
75
+ usual local-dev setup), `lib/` is picked up on the next `dsh web` restart — no copy step needed.
76
+
75
77
  ## Issues
76
78
 
77
79
  - **Bugs**: include the dsh version, Node version, the plugin version, and the exact steps.
package/README.md CHANGED
@@ -24,7 +24,7 @@ dsh plugin --profile web add dsh-skill-hub
24
24
  # restart dsh web → Settings → 技能 → Market → scan → import
25
25
  ```
26
26
 
27
- Requires `Node ^22.19 || >=24` + dsh web (`0.1.5-rc.2`, `0.1.x` forward compatible).
27
+ Requires `Node ^22.19 || >=24` + dsh web (`0.1.6-alpha.1`, `0.1.x` forward compatible).
28
28
 
29
29
  ## Features
30
30
 
@@ -115,6 +115,7 @@ npm run build # tsc + tsdown → lib/index.js + lib/client.js
115
115
 
116
116
  - `duplicate loader entry id: skill-hub` — remove the duplicate install (keep one `dsh plugin add` method).
117
117
  - Skill missing — check the diagnostics section (frontmatter / name mismatch / short description).
118
+ - Empty source group — its skills were deleted, or their disabled records were lost (sidecar restored/hand-edited). Startup reconciles `.disabled` files on disk; source groups with no visible member are no longer rendered.
118
119
  - Dots missing in `/` menu — dsh internals changed; catalog still works.
119
120
 
120
121
  ## Community
package/README.zh.md CHANGED
@@ -24,7 +24,7 @@ dsh plugin --profile web add dsh-skill-hub
24
24
  # 重启 dsh web → 设置 → 技能 → 市场 → 扫描 → 导入
25
25
  ```
26
26
 
27
- 要求 `Node ^22.19 || >=24` + dsh web(`0.1.5-rc.2`,兼容后续 `0.1.x`)。
27
+ 要求 `Node ^22.19 || >=24` + dsh web(`0.1.6-alpha.1`,兼容后续 `0.1.x`)。
28
28
 
29
29
  ## 功能
30
30
 
@@ -115,6 +115,7 @@ npm run build # tsc + tsdown → lib/index.js + lib/client.js
115
115
 
116
116
  - `duplicate loader entry id: skill-hub` — 删掉重复安装(只留一种 `dsh plugin add`)。
117
117
  - 技能不出现 — 看诊断区(缺 frontmatter / 名称不一致 / 描述过短)。
118
+ - 来源组空壳 — 组内技能已被删除、或禁用记录丢失(状态文件被恢复/手改)。启动时自动对账磁盘上的 `.disabled` 文件补记录;无可见成员的来源组不再渲染。
118
119
  - `/` 菜单圆点消失 — dsh 内部触发源变更,目录功能不受影响。
119
120
 
120
121
  ## 社区
package/lib/client.js CHANGED
@@ -959,6 +959,27 @@ window.__ModuleLoader__.load({
959
959
  });
960
960
  }
961
961
  /**
962
+ * Match origin collections against the currently visible enabled skills and
963
+ * disabled records, dropping collections with no visible member. Without
964
+ * this, a stale origin (skill deleted on disk, or a `.disabled` file whose
965
+ * sidecar record was lost) renders a group header with zero rows — an empty
966
+ * shell the sources tab otherwise never shows (project/personal groups
967
+ * already disappear when they have nothing to display).
968
+ */
969
+ function visibleCollections(collections, visibleSkills, disabledRecords, normalized, sourceFilter, origins) {
970
+ const visible = [];
971
+ for (const collection of collections) {
972
+ const skills = visibleSkills.filter((skill) => collection.skillNames.includes(skill.name));
973
+ const disabledMembers = disabledRecords.filter((record) => collection.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter));
974
+ if (skills.length > 0 || disabledMembers.length > 0) visible.push({
975
+ collection,
976
+ skills,
977
+ disabledMembers
978
+ });
979
+ }
980
+ return visible;
981
+ }
982
+ /**
962
983
  * Sort a skill list in place-safe copy order: name ascending, added
963
984
  * descending (newest first, unknown addedAt last), or uses descending
964
985
  * (most-called first). Unknown values always trail.
@@ -3833,10 +3854,12 @@ window.__ModuleLoader__.load({
3833
3854
  const [topOverKey, setTopOverKey] = (0, react.useState)(null);
3834
3855
  /** 重复技能名集合:整表只建一次,行内用 has 取代逐行线性 includes。 */
3835
3856
  const duplicateNames = (0, react.useMemo)(() => new Set(catalog?.duplicateNames ?? []), [catalog]);
3836
- const projectSkillsAll = filterBySource(sorted, sourceFilter, origins).filter((skill) => isProjectSource(skill.source));
3857
+ const sourceFiltered = filterBySource(sorted, sourceFilter, origins);
3858
+ const projectSkillsAll = sourceFiltered.filter((skill) => isProjectSource(skill.source));
3837
3859
  const hasProject = projectSkillsAll.length > 0;
3838
- const collections = groupsState?.collections ?? [];
3839
- const uncategorized = filterBySource(sorted, sourceFilter, origins).filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3860
+ const visible = visibleCollections(groupsState?.collections ?? [], sourceFiltered, catalog?.disabled ?? [], normalized, sourceFilter, origins);
3861
+ const collections = visible.map((entry) => entry.collection);
3862
+ const uncategorized = sourceFiltered.filter((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
3840
3863
  const personalDisabled = (catalog?.disabled ?? []).filter((record) => origins[record.name] === void 0).filter((record) => normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)).filter((record) => sourceFilter === "all" || sourceFilter === "private");
3841
3864
  const allPersonalNames = [...uncategorized.map((s) => s.name), ...personalDisabled.map((r) => r.name)];
3842
3865
  const hasPersonal = allPersonalNames.length > 0;
@@ -3895,7 +3918,7 @@ window.__ModuleLoader__.load({
3895
3918
  openDetail: hub.openDetail,
3896
3919
  requestDeleteSkill: hub.requestDeleteSkill
3897
3920
  };
3898
- if (skillView === "flat") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: filterBySource(sorted, sourceFilter, origins).map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
3921
+ if (skillView === "flat") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: sourceFiltered.map((skill) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillRow, {
3899
3922
  skill,
3900
3923
  ...rowProps
3901
3924
  }, skill.name)) });
@@ -3933,10 +3956,9 @@ window.__ModuleLoader__.load({
3933
3956
  }, "project");
3934
3957
  if (topKey.startsWith("col:")) {
3935
3958
  const colName = topKey.slice(4);
3936
- const collection = collections.find((c) => c.name === colName);
3937
- if (collection === void 0) return null;
3938
- const skills = filterBySource(sorted, sourceFilter, origins).filter((skill) => collection.skillNames.includes(skill.name));
3939
- const disabledMembers = (catalog?.disabled ?? []).filter((record) => collection.skillNames.includes(record.name) && (normalized.length === 0 || record.name.toLocaleLowerCase().includes(normalized) || record.description.toLocaleLowerCase().includes(normalized)) && (sourceFilter === "all" || (origins[record.name] ?? "private") === sourceFilter));
3959
+ const entry = visible.find((item) => item.collection.name === colName);
3960
+ if (entry === void 0) return null;
3961
+ const { collection, skills, disabledMembers } = entry;
3940
3962
  const collapsed = collapsedGroups.has("col:" + collection.name);
3941
3963
  const view = groupSwitchView(collection.skillNames, viewNames);
3942
3964
  const check = sourceCheck[collection.name];
@@ -4407,6 +4429,23 @@ window.__ModuleLoader__.load({
4407
4429
  function VersionChoiceDialog(props) {
4408
4430
  const { choice, busy, onSelect, onCustom, onCancel, onConfirm } = props;
4409
4431
  const effective = choice.custom.trim() !== "" ? choice.custom.trim() : choice.selected;
4432
+ const branchOptions = choice.branches.filter((branch) => !choice.releases.includes(branch));
4433
+ const [refGroup, setRefGroup] = (0, react.useState)(choice.releases.includes(choice.selected) || choice.branches.length === 0 ? "releases" : "branches");
4434
+ (0, react.useEffect)(() => {
4435
+ if (choice.loading) return;
4436
+ if (choice.releases.includes(choice.selected)) setRefGroup("releases");
4437
+ else if (branchOptions.includes(choice.selected)) setRefGroup("branches");
4438
+ }, [choice.loading, choice.repo]);
4439
+ const activeRefs = refGroup === "releases" ? choice.releases : branchOptions;
4440
+ const listedRefs = [...choice.releases, ...branchOptions];
4441
+ const currentUnlistedRef = choice.selected !== "" && !listedRefs.includes(choice.selected) ? choice.selected : void 0;
4442
+ const selectRefs = currentUnlistedRef !== void 0 ? [currentUnlistedRef, ...activeRefs] : activeRefs;
4443
+ const selectValue = selectRefs.includes(choice.selected) ? choice.selected : selectRefs[0] ?? "";
4444
+ const selectGroup = (next) => {
4445
+ setRefGroup(next);
4446
+ const nextRefs = next === "releases" ? choice.releases : branchOptions;
4447
+ if (nextRefs.length > 0 && !nextRefs.includes(choice.selected)) onSelect(nextRefs[0]);
4448
+ };
4410
4449
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogShell, {
4411
4450
  onClose: onCancel,
4412
4451
  children: [
@@ -4422,38 +4461,42 @@ window.__ModuleLoader__.load({
4422
4461
  className: panel_module_css_default.dialogText,
4423
4462
  children: tt("market.scanning")
4424
4463
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4425
- choice.releases.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4426
- className: panel_module_css_default.dialogText,
4427
- style: { marginBottom: 4 },
4428
- children: tt("market.versionReleases")
4429
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4430
- className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4431
- value: choice.releases.includes(choice.selected) ? choice.selected : choice.releases[0],
4432
- onChange: (event) => {
4433
- onSelect(event.target.value);
4434
- onCustom("");
4435
- },
4436
- children: choice.releases.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4437
- value: tag,
4438
- children: tag
4439
- }, tag))
4440
- })] }) : null,
4441
- choice.branches.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4442
- className: panel_module_css_default.dialogText,
4443
- style: { marginBottom: 4 },
4444
- children: tt("market.versionBranches")
4445
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4465
+ className: panel_module_css_default.segmented,
4466
+ role: "group",
4467
+ "aria-label": tt("market.versionTitle"),
4468
+ style: { marginBottom: 8 },
4469
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4470
+ type: "button",
4471
+ className: panel_module_css_default.segBtn + (refGroup === "releases" ? " " + panel_module_css_default.segBtnActive : ""),
4472
+ disabled: choice.releases.length === 0,
4473
+ "aria-pressed": refGroup === "releases",
4474
+ onClick: () => {
4475
+ selectGroup("releases");
4476
+ },
4477
+ children: tt("market.versionReleases")
4478
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4479
+ type: "button",
4480
+ className: panel_module_css_default.segBtn + (refGroup === "branches" ? " " + panel_module_css_default.segBtnActive : ""),
4481
+ disabled: branchOptions.length === 0,
4482
+ "aria-pressed": refGroup === "branches",
4483
+ onClick: () => {
4484
+ selectGroup("branches");
4485
+ },
4486
+ children: tt("market.versionBranches")
4487
+ })]
4488
+ }),
4489
+ selectValue !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4446
4490
  className: panel_module_css_default.select + " " + panel_module_css_default.dialogSelect,
4447
- value: choice.branches.includes(choice.selected) ? choice.selected : choice.branches[0],
4491
+ value: selectValue,
4448
4492
  onChange: (event) => {
4449
4493
  onSelect(event.target.value);
4450
- onCustom("");
4451
4494
  },
4452
- children: choice.branches.map((branch) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4453
- value: branch,
4454
- children: branch
4455
- }, branch))
4456
- })] }) : null,
4495
+ children: selectRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4496
+ value: ref,
4497
+ children: ref
4498
+ }, ref))
4499
+ }) : null,
4457
4500
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4458
4501
  className: panel_module_css_default.dialogText,
4459
4502
  style: { marginBottom: 4 },
@@ -5322,14 +5365,15 @@ window.__ModuleLoader__.load({
5322
5365
  choice: versionDialog,
5323
5366
  busy: versionBusy,
5324
5367
  onSelect: (selected) => {
5325
- setVersionDialog({
5326
- ...versionDialog,
5327
- selected
5368
+ setVersionDialog((previous) => previous === null ? previous : {
5369
+ ...previous,
5370
+ selected,
5371
+ custom: ""
5328
5372
  });
5329
5373
  },
5330
5374
  onCustom: (custom) => {
5331
- setVersionDialog({
5332
- ...versionDialog,
5375
+ setVersionDialog((previous) => previous === null ? previous : {
5376
+ ...previous,
5333
5377
  custom
5334
5378
  });
5335
5379
  },