@morehao/dsh-skills-console 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -29,7 +29,7 @@ DSH(DeepSeek Harness)的**技能控制台**:装、更新、删除、接入
29
29
  | 接入 | 软链四态判定(`managed` / `managed-external` / `foreign-link` / `real`),**绝不覆盖**别人创建的东西;只列本机已安装目录,已删的 Agent 自动从列表消失 |
30
30
  | 启停 | `mode: on/off` 只摘或重建 DSH 侧链接,不动 `SKILL.md`,其他 Agent 的链接也不受影响 |
31
31
  | 作用范围 | 全局 `~/.dsh/skills`,或仅指定项目;项目可直接勾选 DSH 已登记的工作区,也可手动选目录 |
32
- | 订阅 | 关注 `owner/repo`(可指定 ref / 子目录),只记快照、不装技能;装好后再逐个安装 |
32
+ | 订阅 | 关注 `owner/repo`(可指定 ref / 子目录;ref 缺省时跟随仓库默认分支,`master` 仓库也能用),只记快照、不装技能;上游读不到快照时直接报错,不会留下空订阅 |
33
33
 
34
34
  ## 界面
35
35
 
@@ -57,18 +57,16 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
57
57
 
58
58
  客户端 half 只注册 `platform: web`,面板出现在 DSH Web 的设置页 / 侧栏。
59
59
 
60
- > 本机可能还装着 `@michengai/dsh-skills-manager`(显示名「技能管理」)。两者互不干扰(路由前缀 `/api/morehao/skills-console/*`、状态目录 `$DSH_HOME/skills-console/` 都不同),但会各出一个设置分区;只想留一个时把另一个 `plugin_manager set_plugin enabled:false` 即可。
61
-
62
60
  ## 快速上手
63
61
 
64
- 1. **添加技能**:点右上角「添加技能」,填 `owner/repo`(需要的话再列出技能名),确认即可。安装完成后会在列表里出现。
62
+ 1. **添加技能**:点右上角「添加技能」,填 `owner/repo` 或 GitHub URL,点「检测技能」;仓库里有多个技能时勾选要装的(支持多选,默认全选,列表带每个技能的说明),只有一个会默认勾上。检测不到时可以直接手填技能名,留空则装仓库内全部。
65
63
  2. **接到别的 Agent**:点行内「接入设置」,勾选目标 Agent;需要限定项目时改选「仅以下项目」并勾工作区。之后新装技能默认接哪些 Agent,用它旁边的「自动接入」设定。
66
64
  3. **检查更新**:点列表头上的刷新图标;有更新的行会亮起「更新」,也可以一次「全部更新」。本地改过的技能会先要求确认。
67
65
  4. **临时停用**:关掉行内开关。DSH 看不到它,但文件和其他 Agent 的接入都还在;再打开就恢复。
68
66
 
69
- ## 设计约束
67
+ ## 可靠性
70
68
 
71
- - **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json` 的增删改只经 `skills` CLI,插件不直接写、不克隆仓库、不代理来源。
69
+ - **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json` 的增删改只经 `skills` CLI,插件不直接写;探测 / 更新检查会把仓库临时 clone 到 `os.tmpdir()`(只读、用完即删),结果绝不落进 canonical 或 lock。
72
70
  - **只动自己的东西**:只处理本插件创建的 `managed` 链接,以及已记账的 `managed-external`;`real` 目录和 `foreign-link` 永不改写,删除前体检直接中止。
73
71
  - **启停不改正文**:`mode` 只增删 DSH 侧链接,避免本地修改被下一次 `add` 覆盖。
74
72
  - **可回滚**:删除进回收站(7 天);更新前快照到 `backups/`,失败自动还原(接口另提供手动回滚)。
@@ -86,45 +84,26 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
86
84
  | 备份 / 回收站 | `$DSH_HOME/skills-console/{backups,trash}` | 本插件 |
87
85
  | 事件日志 | `$DSH_HOME/skills-console/dsh-skills-console.log` | 本插件(JSONL,1MB 滚动) |
88
86
 
89
- 环境变量:`DSH_SKILLS_CONSOLE_HOME`、`DSH_SKILLS_CONSOLE_CLI`、`DSH_SKILLS_CONSOLE_NPX_SPEC`、`DSH_SKILLS_CONSOLE_TIMEOUT_MS`、`GITHUB_TOKEN` / `GH_TOKEN`。
90
-
91
- ## 开发
92
-
93
- ```bash
94
- npm install # 只装构建期依赖(esbuild);运行时依赖仍为零
95
- npm run build:client # 从 client/ 生成根 client.js(生成物要提交,不要手改)
96
- npm run dev:client # 监听 client/ 自动重建(配合 link: 接入,改完刷新即可)
97
- npm test # node --test,80 个离线用例,临时 HOME / DSH_HOME,不碰真实目录
98
- npm run check # 语法自检 + 生成物一致性 + 命名/导出接线 + 打包白名单 + Release 说明投影冒烟
99
- npm run sync:agents -- <skills/dist/cli.mjs> # 重新导出 agent 目录表(默认写回 lib/agent-table.json)
100
- ```
101
-
102
- Web Client 的源码在 `client/`(`host` / `copy` / `styles` / `api` / `icons` / `hooks` / `actions` / `views/*` / `section` / `surface` / `app`),仓根 `client.js` 是 `npm run build:client` 的产物;只改产物不改源码,`npm run check` 会直接红。
103
-
104
- 目录结构、测试策略与调试手段见 [docs/development.md](docs/development.md);在本仓工作的 AI agent 请先读 [AGENTS.md](AGENTS.md)。
87
+ ### 环境变量
105
88
 
106
- ## 发布
107
-
108
- 打注记 tag `vX.Y.Z` → GitHub Actions(OIDC / Trusted Publishing)自动发布到 npm → 用 tag 注记正文建 GitHub Release,不需要任何 npm token。
109
-
110
- ```bash
111
- # 升版本走分支 + PR;合并后在 main 最新提交上打注记 tag(正文就是 Release body)
112
- git tag -a v0.2.0 -m "release: v0.2.0
113
- <变更说明>"
114
- git push origin v0.2.0
115
- ```
89
+ | 变量 | 默认 | 作用 |
90
+ | --- | --- | --- |
91
+ | `DSH_SKILLS_CONSOLE_HOME` | `$DSH_HOME/skills-console` | 状态、备份、回收站与事件日志的根 |
92
+ | `DSH_SKILLS_CONSOLE_CLI` | 自动定位 | 覆盖 `skills` CLI(测试桩也走这里) |
93
+ | `DSH_SKILLS_CONSOLE_NPX_SPEC` | `skills` | 需要 `npx` 兜底时用的包名 / 版本 |
94
+ | `DSH_SKILLS_CONSOLE_TIMEOUT_MS` | `120000` | 单次 CLI 调用超时 |
95
+ | `DSH_SKILLS_CONSOLE_GIT` | `auto` | 设为 `off` 关闭 git 通道,强制只走 GitHub REST |
96
+ | `DSH_SKILLS_CONSOLE_GIT_TIMEOUT_MS` | `60000` | git 单次操作(`ls-remote` / `clone`)超时 |
97
+ | `GITHUB_TOKEN` / `GH_TOKEN` | 无 | 只读环境变量;带上后 REST 配额从匿名 60 次/小时提到 5000 次/小时 |
116
98
 
117
- 完整流程、一次性建包配置与排障清单见 [docs/publishing.md](docs/publishing.md)。
99
+ 关于 GitHub 配额:**匿名 REST 是 60 次/小时,按出口 IP 共享**(公司、校园网出口常被同网段的人一起打满),打满后探测与更新会收到 403。本插件默认让探测、更新检查与 git 兜底走系统 `git`(`ls-remote` + `clone --filter=blob:none --no-checkout`),不占 REST 配额、也不需要 token;git 不可用时才回退 REST,所以原本能用的环境不会因为这条路径变坏。限流且 git 也不可用时,错误文案会给出恢复时间,并提示设置 `GITHUB_TOKEN`。
118
100
 
119
- ## 从 `dsh-skills-manager` 迁移
101
+ > git 通道只在 `os.tmpdir()` 里读一份临时仓库,用完即删 —— 不写 canonical、不写 CLI lock,落盘始终只交给 `skills` CLI。
120
102
 
121
- `v0.2.0` 起包名、插件 id、路由前缀、状态目录与环境变量统一改为 `skills-console`,**不提供兼容读旧值**。保住旧账本(订阅、回收站、链接记录、版本缓存)只需搬一次目录:
122
103
 
123
- ```bash
124
- mv ~/.dsh/skills-manager ~/.dsh/skills-console
125
- ```
104
+ ## 开发与发布
126
105
 
127
- 完整对照、不搬的后果与安装侧改动见 [docs/migration-from-skills-manager.md](docs/migration-from-skills-manager.md)。
106
+ 源码结构、测试策略与调试手段见 [docs/development.md](docs/development.md)(含本地 `link:` 验证与回退步骤);发布流程与排障清单见 [docs/publishing.md](docs/publishing.md)。在本仓工作的 AI agent 请先读 [AGENTS.md](AGENTS.md)。
128
107
 
129
108
  ## 边界(明确不做)
130
109
 
package/client.js CHANGED
@@ -148,11 +148,23 @@ var DICT = {
148
148
  "banner.visible": "模型可见技能与本地账本不一致,差异已列在下方",
149
149
  "form.source": "来源(owner/repo 或 GitHub URL)",
150
150
  "form.sourceHint": "例如 morehao/ai-kit",
151
+ "form.probe": "检测技能",
152
+ "form.probing": "读取中…",
153
+ "form.probingHint": "正在读仓库的文件树,稍等一下…",
154
+ "form.detected": "检测到 {n} 个技能,勾选要安装的",
155
+ "form.skipped": "另有 {n} 个 SKILL.md 缺少 name/description,CLI 无法安装,已跳过",
156
+ "form.selectAll": "全选",
157
+ "form.selectNone": "全不选",
158
+ "form.pickHint": "先填来源并检测;仓库里有多个技能时可以多选,只有一个会默认勾上。",
159
+ "form.noSkills": "仓库里没找到 SKILL.md,可以直接填技能名安装",
160
+ "form.probeFailed": "检测失败,仍可手动填技能名",
151
161
  "form.skills": "技能名(多个用逗号分隔)",
152
162
  "form.ref": "ref(分支 / tag,可空)",
163
+ "form.refHint": "留空则自动跟随仓库默认分支",
153
164
  "form.subpath": "子目录(可空)",
154
165
  "form.hint": "会立即下载并写入 ~/.agents/skills,并按「自动接入」设置接到对应 Agent。",
155
166
  "form.subHint": "只记录你关注这个仓库,不会安装任何技能。",
167
+ "subscription.defaultRef": "默认分支",
156
168
  "confirm.deleteTitle": "删除技能",
157
169
  "confirm.deleteBody": "会从本机移除,断开本插件建立的软链并调 skills CLI 卸载;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
158
170
  "confirm.updateTitle": "本地有改动",
@@ -266,11 +278,23 @@ var DICT = {
266
278
  "banner.visible": "Model-visible skills differ from the local ledger",
267
279
  "form.source": "Source (owner/repo or GitHub URL)",
268
280
  "form.sourceHint": "e.g. morehao/ai-kit",
281
+ "form.probe": "Detect",
282
+ "form.probing": "Reading…",
283
+ "form.probingHint": "Reading the repository tree…",
284
+ "form.detected": "Found {n} skills; pick the ones to install",
285
+ "form.skipped": "{n} SKILL.md file(s) skipped: missing name/description",
286
+ "form.selectAll": "Select all",
287
+ "form.selectNone": "Clear",
288
+ "form.pickHint": "Enter a source and detect first. Pick one or more when the repo has several.",
289
+ "form.noSkills": "No SKILL.md found; type skill names instead",
290
+ "form.probeFailed": "Detection failed; you can still type names",
269
291
  "form.skills": "Skill names (comma separated)",
270
292
  "form.ref": "ref (branch / tag, optional)",
293
+ "form.refHint": "empty follows the repository default branch",
271
294
  "form.subpath": "Subdirectory (optional)",
272
295
  "form.hint": "Downloads into ~/.agents/skills and links agents per the auto-link setting.",
273
296
  "form.subHint": "Only records the repository; installs nothing.",
297
+ "subscription.defaultRef": "default branch",
274
298
  "confirm.deleteTitle": "Delete skill",
275
299
  "confirm.deleteBody": "Removes it locally, disconnects this plugin’s links and calls skills remove. A snapshot stays in trash for 7 days. Your source repo is untouched.",
276
300
  "confirm.updateTitle": "Local changes",
@@ -888,6 +912,10 @@ var CSS = `
888
912
  border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); outline: none; }
889
913
  .dshsc-field input:focus { border-color: var(--dsw-alias-brand-primary);
890
914
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
915
+ /* 来源输入 + 检测按钮同一行;输入框吃掉剩余宽度,按钮不被挤压 */
916
+ .dshsc-inline { display: flex; gap: 6px; align-items: stretch; }
917
+ .dshsc-inline input { flex: 1; min-width: 0; }
918
+ .dshsc-inline .dshsc-btn { flex: none; }
891
919
 
892
920
  .dshsc-check { display: flex; align-items: center; gap: 9px; font-size: 13px; padding: 5px 8px; border-radius: 8px; cursor: pointer; }
893
921
  .dshsc-check:hover { background: var(--dsw-alias-bg-layer-1); }
@@ -905,6 +933,14 @@ var CSS = `
905
933
  .dshsc-check input:disabled { opacity: .55; cursor: not-allowed; }
906
934
  .dshsc-check small { color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
907
935
  .dshsc-check span { white-space: nowrap; }
936
+ /* 添加技能:每个技能一行,名字 + 两行描述 + 路径;描述太长时截断,不把弹窗撑高 */
937
+ .dshsc-skill { align-items: flex-start; }
938
+ .dshsc-skill input[type="checkbox"] { margin-top: 3px; }
939
+ .dshsc-skill-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
940
+ .dshsc-skill-name { font-weight: 600; }
941
+ .dshsc-check .dshsc-skill-desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
942
+ overflow: hidden; white-space: normal; line-height: 1.4; }
943
+ .dshsc-check .dshsc-skill-path { font-size: 11px; opacity: .75; }
908
944
 
909
945
  .dshsc-panel { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1);
910
946
  padding: 4px; max-height: 300px; overflow: auto; display: flex; flex-direction: column; }
@@ -1009,11 +1045,13 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1009
1045
  function requestAddSubscription() {
1010
1046
  setModal({ type: "add-subscription" });
1011
1047
  }
1012
- async function addSkill(form) {
1013
- const names = form.skills.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
1048
+ async function probeSkills(source) {
1049
+ return apiCall("POST", "/probe", { source: String(source || "").trim() });
1050
+ }
1051
+ async function installSkills({ source, skills }) {
1014
1052
  setModal(null);
1015
1053
  await run("install", async () => {
1016
- const result = await apiCall("POST", "/install", { source: form.source.trim(), skills: names });
1054
+ const result = await apiCall("POST", "/install", { source: String(source || "").trim(), skills });
1017
1055
  const warnings = result.warnings?.length || 0;
1018
1056
  setToast({
1019
1057
  kind: warnings ? "err" : "ok",
@@ -1060,7 +1098,8 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1060
1098
  saveLinks,
1061
1099
  requestAddSkill,
1062
1100
  requestAddSubscription,
1063
- addSkill,
1101
+ probeSkills,
1102
+ installSkills,
1064
1103
  addSubscription,
1065
1104
  refreshSubscription,
1066
1105
  deleteSubscription,
@@ -1768,7 +1807,7 @@ function SubscriptionsTab({
1768
1807
  onClick: () => onToggle(subscription.id)
1769
1808
  }, h(CaretIcon, { open })),
1770
1809
  h("span", { className: "dshsc-name" }, subscription.source),
1771
- h("span", { className: "dshsc-tag" }, subscription.ref || "default"),
1810
+ h("span", { className: "dshsc-tag" }, subscription.ref || t("subscription.defaultRef")),
1772
1811
  h(
1773
1812
  "span",
1774
1813
  { className: "dshsc-substat" },
@@ -1848,6 +1887,9 @@ function Overlay({ t, title, children, footer, onCancel }) {
1848
1887
  }
1849
1888
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
1850
1889
  const [form, setForm] = import_react3.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
1890
+ const incomplete = fields.some(
1891
+ (field) => field.required !== false && !String(form[field.key] || "").trim()
1892
+ );
1851
1893
  return h(
1852
1894
  Overlay,
1853
1895
  {
@@ -1859,7 +1901,7 @@ function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
1859
1901
  h("button", {
1860
1902
  key: "s",
1861
1903
  className: "dshsc-btn primary",
1862
- disabled: !form[fields[0].key]?.trim?.() || fields.some((f) => f.key === "skills") && !form.skills?.trim(),
1904
+ disabled: incomplete,
1863
1905
  onClick: () => onSubmit(form)
1864
1906
  }, t("action.save"))
1865
1907
  ]
@@ -2029,18 +2071,153 @@ function AutoLinkModal({ t, settings, agents, onCancel, onSave }) {
2029
2071
  h("p", { className: "dshsc-sub" }, t("autoLink.hint"))
2030
2072
  );
2031
2073
  }
2074
+ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
2075
+ const [source, setSource] = import_react3.default.useState("");
2076
+ const [phase, setPhase] = import_react3.default.useState("idle");
2077
+ const [options, setOptions] = import_react3.default.useState([]);
2078
+ const [selected, setSelected] = import_react3.default.useState(() => /* @__PURE__ */ new Set());
2079
+ const [manual, setManual] = import_react3.default.useState("");
2080
+ const [skipped, setSkipped] = import_react3.default.useState(0);
2081
+ const [error, setError] = import_react3.default.useState(null);
2082
+ const reset = (value) => {
2083
+ setSource(value);
2084
+ setPhase("idle");
2085
+ setOptions([]);
2086
+ setSelected(/* @__PURE__ */ new Set());
2087
+ setSkipped(0);
2088
+ setError(null);
2089
+ };
2090
+ const probe = async () => {
2091
+ const value = source.trim();
2092
+ if (!value || phase === "probing") return;
2093
+ setPhase("probing");
2094
+ setError(null);
2095
+ try {
2096
+ const result = await onProbe(value);
2097
+ const found = Array.isArray(result?.skills) ? result.skills : [];
2098
+ setOptions(found);
2099
+ setSelected(new Set(found.map((item) => item.name)));
2100
+ setSkipped(Math.max(0, Number(result?.skipped) || 0));
2101
+ setPhase(found.length > 0 ? "picked" : "empty");
2102
+ } catch (err) {
2103
+ setError(err?.message || String(err));
2104
+ setPhase("failed");
2105
+ }
2106
+ };
2107
+ const toggle = (name) => setSelected((prev) => {
2108
+ const next = new Set(prev);
2109
+ if (next.has(name)) next.delete(name);
2110
+ else next.add(name);
2111
+ return next;
2112
+ });
2113
+ const names = manual.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
2114
+ const skippedText = skipped > 0 ? t("form.skipped").replace("{n}", String(skipped)) : null;
2115
+ const manualMode = phase === "empty" || phase === "failed";
2116
+ const skills = phase === "picked" ? [...selected] : names;
2117
+ const canSubmit = phase === "picked" ? selected.size > 0 : manualMode ? names.length > 0 : false;
2118
+ const submit = () => {
2119
+ if (!canSubmit) return;
2120
+ onInstall({ source: source.trim(), skills });
2121
+ };
2122
+ return h(
2123
+ Overlay,
2124
+ {
2125
+ t,
2126
+ title: t("action.addSkill"),
2127
+ onCancel,
2128
+ footer: [
2129
+ h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
2130
+ h("button", {
2131
+ key: "s",
2132
+ className: "dshsc-btn primary",
2133
+ disabled: !canSubmit,
2134
+ onClick: submit
2135
+ }, t("action.save"))
2136
+ ]
2137
+ },
2138
+ h("p", { className: "dshsc-sub" }, t("form.pickHint")),
2139
+ h(
2140
+ "div",
2141
+ { className: "dshsc-field" },
2142
+ h("span", null, t("form.source")),
2143
+ h(
2144
+ "div",
2145
+ { className: "dshsc-inline" },
2146
+ h("input", {
2147
+ value: source,
2148
+ placeholder: t("form.sourceHint"),
2149
+ onChange: (event) => reset(event.target.value),
2150
+ onKeyDown: (event) => {
2151
+ if (event.key === "Enter") void probe();
2152
+ }
2153
+ }),
2154
+ h("button", {
2155
+ className: "dshsc-btn",
2156
+ disabled: !source.trim() || phase === "probing",
2157
+ onClick: () => void probe()
2158
+ }, phase === "probing" ? t("form.probing") : t("form.probe"))
2159
+ )
2160
+ ),
2161
+ phase === "picked" ? h(
2162
+ "div",
2163
+ { key: "picked" },
2164
+ h("div", { className: "dshsc-group" }, t("form.detected").replace("{n}", String(options.length))),
2165
+ h(
2166
+ "div",
2167
+ { style: { display: "flex", gap: 6, marginBottom: 6 } },
2168
+ h("button", {
2169
+ className: "dshsc-btn ghost",
2170
+ onClick: () => setSelected(new Set(options.map((item) => item.name)))
2171
+ }, t("form.selectAll")),
2172
+ h("button", {
2173
+ className: "dshsc-btn ghost",
2174
+ onClick: () => setSelected(/* @__PURE__ */ new Set())
2175
+ }, t("form.selectNone"))
2176
+ ),
2177
+ h("div", { className: "dshsc-panel" }, options.map((option) => h(
2178
+ "label",
2179
+ { className: "dshsc-check dshsc-skill", key: option.name },
2180
+ h("input", {
2181
+ type: "checkbox",
2182
+ checked: selected.has(option.name),
2183
+ onChange: () => toggle(option.name)
2184
+ }),
2185
+ h(
2186
+ "span",
2187
+ { className: "dshsc-skill-main" },
2188
+ h("span", { className: "dshsc-skill-name" }, option.name),
2189
+ option.description ? h("small", { className: "dshsc-skill-desc" }, option.description) : null,
2190
+ h("small", { className: "dshsc-skill-path" }, option.skillPath)
2191
+ )
2192
+ ))),
2193
+ skippedText ? h("p", { className: "dshsc-sub" }, skippedText) : null
2194
+ ) : null,
2195
+ phase === "probing" ? h("p", { className: "dshsc-sub" }, t("form.probingHint")) : null,
2196
+ manualMode ? h(
2197
+ "div",
2198
+ { key: "manual" },
2199
+ h("p", { className: "dshsc-sub" }, phase === "empty" ? skippedText || t("form.noSkills") : `${t("form.probeFailed")}${error ? ` · ${error}` : ""}`),
2200
+ h(
2201
+ "label",
2202
+ { className: "dshsc-field" },
2203
+ h("span", null, t("form.skills")),
2204
+ h("input", {
2205
+ value: manual,
2206
+ placeholder: "git-kit, handoff",
2207
+ onChange: (event) => setManual(event.target.value)
2208
+ })
2209
+ )
2210
+ ) : null,
2211
+ h("p", { className: "dshsc-sub" }, t("form.hint"))
2212
+ );
2213
+ }
2032
2214
  function Modals({ t, modal, data, autoLinkAgents, pickDirectory, actions, onClose, onSaveAutoLink }) {
2033
2215
  if (modal.type === "add-skill") {
2034
- return h(FormModal, {
2216
+ return h(AddSkillModal, {
2035
2217
  t,
2036
- title: t("action.addSkill"),
2037
- hint: t("form.hint"),
2038
- fields: [
2039
- { key: "source", label: t("form.source"), placeholder: t("form.sourceHint") },
2040
- { key: "skills", label: t("form.skills"), placeholder: "git-kit, handoff" }
2041
- ],
2042
2218
  onCancel: onClose,
2043
- onSubmit: (form) => void actions.addSkill(form)
2219
+ onProbe: actions.probeSkills,
2220
+ onInstall: actions.installSkills
2044
2221
  });
2045
2222
  }
2046
2223
  if (modal.type === "add-subscription") {
@@ -2050,8 +2227,8 @@ function Modals({ t, modal, data, autoLinkAgents, pickDirectory, actions, onClos
2050
2227
  hint: t("form.subHint"),
2051
2228
  fields: [
2052
2229
  { key: "source", label: t("form.source"), placeholder: t("form.sourceHint") },
2053
- { key: "ref", label: t("form.ref"), placeholder: "main" },
2054
- { key: "subpath", label: t("form.subpath"), placeholder: "skills" }
2230
+ { key: "ref", label: t("form.ref"), placeholder: t("form.refHint"), required: false },
2231
+ { key: "subpath", label: t("form.subpath"), placeholder: "skills", required: false }
2055
2232
  ],
2056
2233
  onCancel: onClose,
2057
2234
  onSubmit: (form) => void actions.addSubscription(form)
@@ -2267,7 +2444,8 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
2267
2444
  autoLinkAgents: autolinkTargets,
2268
2445
  pickDirectory,
2269
2446
  actions: {
2270
- addSkill: actions.addSkill,
2447
+ probeSkills: actions.probeSkills,
2448
+ installSkills: actions.installSkills,
2271
2449
  addSubscription: actions.addSubscription,
2272
2450
  saveLinks: actions.saveLinks
2273
2451
  },
package/lib/core/cli.js CHANGED
@@ -213,10 +213,12 @@ export function createCli({ env = process.env, logger = () => {} } = {}) {
213
213
 
214
214
  /**
215
215
  * 安装:`-a universal` 必带(F17),`--json` 配 `-y`(F11),`--full-depth` 必带(F24)。
216
+ * 技能名留空 = `--skill '*'`(整仓全部);不能留空值的 `--skill`,那会把后面的 `-a` 当成它的参数。
216
217
  * 永不带 `--all` / `-a '*'`。
217
218
  */
218
219
  async add({ source, skills = [], timeoutMs = env.DSH_SKILLS_CONSOLE_TIMEOUT_MS ? Number(env.DSH_SKILLS_CONSOLE_TIMEOUT_MS) : 120000 }) {
219
- const args = ['add', source, '--skill', ...skills, '-a', 'universal', '-g', '-y', '--json', '--full-depth'];
220
+ const names = skills.length > 0 ? skills : ['*'];
221
+ const args = ['add', source, '--skill', ...names, '-a', 'universal', '-g', '-y', '--json', '--full-depth'];
220
222
  const result = await call(args, { timeoutMs });
221
223
  return result;
222
224
  },