@morehao/dsh-skills-console 0.2.3 → 0.2.4

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
@@ -41,6 +41,8 @@ DSH(DeepSeek Harness)的**技能控制台**:装、更新、删除、接入
41
41
 
42
42
  ## 安装
43
43
 
44
+ 插件依赖里自带 `skills@1.7.4`,**不需要用户预装 Node / npm / npx**:CLI 由宿主自带的 Node 启动,装插件时随依赖一起落盘。安装技能仍需要**本机有 git**(CLI 会 `git clone` 仓库);首次装插件需要联网拉取依赖。
45
+
44
46
  在 Harness 里装本地 link:
45
47
 
46
48
  ```bash
@@ -55,7 +57,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
55
57
  name: '@morehao/dsh-skills-console'
56
58
  ```
57
59
 
58
- 客户端 half 只注册 `platform: web`,面板出现在 DSH Web 的设置页 / 侧栏。
60
+ 客户端 half 只注册 `platform: web`,面板出现在 DSH Web 的设置页 / 侧栏。本地 `link:` 开发时,先在仓库跑一次 `npm install`(装上 `skills`),否则 CLI 依赖层不会命中。
59
61
 
60
62
  ## 快速上手
61
63
 
@@ -66,7 +68,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
66
68
 
67
69
  ## 可靠性
68
70
 
69
- - **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json` 的增删改只经 `skills` CLI,插件不直接写;探测 / 更新检查会把仓库临时 clone 到 `os.tmpdir()`(只读、用完即删),结果绝不落进 canonical 或 lock。
71
+ - **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json`(设了 `XDG_STATE_HOME` 时为 `$XDG_STATE_HOME/skills/.skill-lock.json`)的增删改只经 `skills` CLI,插件不直接写;探测 / 更新检查会把仓库临时 clone 到 `os.tmpdir()`(只读、用完即删),结果绝不落进 canonical 或 lock。
70
72
  - **只动自己的东西**:只处理本插件创建的 `managed` 链接,以及已记账的 `managed-external`;`real` 目录和 `foreign-link` 永不改写,删除前体检直接中止。
71
73
  - **启停不改正文**:`mode` 只增删 DSH 侧链接,避免本地修改被下一次 `add` 覆盖。
72
74
  - **可回滚**:删除进回收站(7 天);更新前快照到 `backups/`,失败自动还原(接口另提供手动回滚)。
@@ -78,7 +80,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
78
80
  | 项 | 路径 | 归属 |
79
81
  | --- | --- | --- |
80
82
  | canonical 真身 | `~/.agents/skills` | `skills` CLI 独占 |
81
- | CLI lock | `~/.agents/.skill-lock.json` | `skills` CLI 独占(本插件只读) |
83
+ | CLI lock | `~/.agents/.skill-lock.json`(设了 `XDG_STATE_HOME` 时为 `$XDG_STATE_HOME/skills/.skill-lock.json`) | `skills` CLI 独占(本插件只读) |
82
84
  | DSH 全局根 | `~/.dsh/skills` | 本插件写软链 |
83
85
  | 插件状态 | `$DSH_HOME/skills-console/state.json` | 本插件(`DSH_SKILLS_CONSOLE_HOME` 可覆盖) |
84
86
  | 备份 / 回收站 | `$DSH_HOME/skills-console/{backups,trash}` | 本插件 |
@@ -90,11 +92,12 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
90
92
  | --- | --- | --- |
91
93
  | `DSH_SKILLS_CONSOLE_HOME` | `$DSH_HOME/skills-console` | 状态、备份、回收站与事件日志的根 |
92
94
  | `DSH_SKILLS_CONSOLE_CLI` | 自动定位 | 覆盖 `skills` CLI(测试桩也走这里) |
93
- | `DSH_SKILLS_CONSOLE_NPX_SPEC` | `skills` | 需要 `npx` 兜底时用的包名 / 版本 |
95
+ | `DSH_SKILLS_CONSOLE_NPX_SPEC` | `skills` | 仅 legacy `npx` 兜底用;桌面端没有 npx,正常应显示 `dependency` |
94
96
  | `DSH_SKILLS_CONSOLE_TIMEOUT_MS` | `120000` | 单次 CLI 调用超时 |
95
97
  | `DSH_SKILLS_CONSOLE_GIT` | `auto` | 设为 `off` 关闭 git 通道,强制只走 GitHub REST |
96
98
  | `DSH_SKILLS_CONSOLE_GIT_TIMEOUT_MS` | `60000` | git 单次操作(`ls-remote` / `clone`)超时 |
97
99
  | `GITHUB_TOKEN` / `GH_TOKEN` | 无 | 只读环境变量;带上后 REST 配额从匿名 60 次/小时提到 5000 次/小时 |
100
+ | `XDG_STATE_HOME` | 未设置 | 设置后 CLI lock 移到 `$XDG_STATE_HOME/skills/.skill-lock.json`,插件跟随读取 |
98
101
 
99
102
  关于 GitHub 配额:**匿名 REST 是 60 次/小时,按出口 IP 共享**(公司、校园网出口常被同网段的人一起打满),打满后探测与更新会收到 403。本插件默认让探测、更新检查与 git 兜底走系统 `git`(`ls-remote` + `clone --filter=blob:none --no-checkout`),不占 REST 配额、也不需要 token;git 不可用时才回退 REST,所以原本能用的环境不会因为这条路径变坏。限流且 git 也不可用时,错误文案会给出恢复时间,并提示设置 `GITHUB_TOKEN`。
100
103
 
package/lib/core/cli.js CHANGED
@@ -40,10 +40,15 @@ function findInPath(command, envPath) {
40
40
  }
41
41
 
42
42
  /**
43
- * CLI 定位四层(设计 §8.2):显式覆盖 → 插件/宿主自带依赖 → PATH → npx。
43
+ * CLI 定位顺序:显式覆盖 → 本插件依赖 → 宿主 profile → PATH → npx(legacy)。
44
+ * 插件依赖排在 profile 前面:版本随插件锁死,不被 PATH 或别的插件带的旧版抢占。
44
45
  * 每一层都记来源,诊断页要展示"这次到底跑的哪一个"。
45
46
  */
46
- export function locateCli({ env = process.env, logger = () => {} } = {}) {
47
+ export function locateCli({
48
+ env = process.env,
49
+ logger = () => {},
50
+ resolvePackage = () => require.resolve('skills/package.json'),
51
+ } = {}) {
47
52
  const override = env.DSH_SKILLS_CONSOLE_CLI;
48
53
  if (override && override.trim()) {
49
54
  const parts = override.trim().split(/\s+/);
@@ -51,7 +56,20 @@ export function locateCli({ env = process.env, logger = () => {} } = {}) {
51
56
  return { command: parts[0], prefixArgs: parts.slice(1), source: 'env-override', jsonCapable: true };
52
57
  }
53
58
 
54
- // 宿主 profile 里的 skills 依赖(最常见的实际位置)。
59
+ // 插件自己的 runtime 依赖:装插件时由宿主 pnpm 一起装好,用户不需要 Node/npm/npx。
60
+ try {
61
+ const pkg = resolvePackage();
62
+ const bin = path.join(path.dirname(pkg), 'bin', 'cli.mjs');
63
+ if (fs.existsSync(bin)) {
64
+ return { command: process.execPath, prefixArgs: [bin], source: 'dependency', jsonCapable: true };
65
+ }
66
+ // 包在但入口丢了,说明安装不完整;交给后面的层兜底,但要留痕
67
+ logger('cli.dependency.bin-missing', { pkg, bin });
68
+ } catch {
69
+ // 没装在本插件可见范围内(例如 link: 开发仓库还没 npm install),继续往下走
70
+ }
71
+
72
+ // 宿主 profile 里的 skills 依赖(兼容旧插件 / 其他插件已经带进来的版本)。
55
73
  // 注意:GUI 启动的宿主进程常常拿不到 DSH_PROFILE_DIR,所以再按 DSH_HOME/profiles/* 兜一层探测。
56
74
  const profileDir = env.DSH_PROFILE_DIR;
57
75
  if (profileDir) {
@@ -74,16 +92,6 @@ export function locateCli({ env = process.env, logger = () => {} } = {}) {
74
92
  // 没有 profiles 目录就继续往下走
75
93
  }
76
94
 
77
- try {
78
- const pkg = require.resolve('skills/package.json');
79
- const bin = path.join(path.dirname(pkg), 'bin', 'cli.mjs');
80
- if (fs.existsSync(bin)) {
81
- return { command: process.execPath, prefixArgs: [bin], source: 'dependency', jsonCapable: true };
82
- }
83
- } catch {
84
- // 没有作为依赖装在本插件可见范围内,继续往下走
85
- }
86
-
87
95
  const onPath = findInPath('skills', env.PATH);
88
96
  if (onPath) return { command: onPath, prefixArgs: [], source: 'path', jsonCapable: true };
89
97
 
@@ -105,8 +113,13 @@ export function runCli(location, args, { timeoutMs = 120000, signal, env = proce
105
113
  stdio: ['ignore', 'pipe', 'pipe'],
106
114
  env: {
107
115
  ...env,
116
+ // 桌面宿主本身就是 Electron 的 Node 模式;显式补上,避免调用方传了裁剪过的 env
117
+ ...(process.versions.electron && location.command === process.execPath ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
108
118
  CI: '1',
109
119
  NO_COLOR: '1',
120
+ // CLI 的遥测只看这两个变量;CI=1 不会关闭,只会在上报里打 ci=1 标记
121
+ DO_NOT_TRACK: '1',
122
+ DISABLE_TELEMETRY: '1',
110
123
  TERM: 'dumb',
111
124
  GIT_TERMINAL_PROMPT: '0',
112
125
  GIT_HTTP_LOW_SPEED_LIMIT: '1',
@@ -151,12 +164,12 @@ export function runCli(location, args, { timeoutMs = 120000, signal, env = proce
151
164
  });
152
165
  }
153
166
 
154
- export function createCli({ env = process.env, logger = () => {} } = {}) {
167
+ export function createCli({ env = process.env, logger = () => {}, resolvePackage } = {}) {
155
168
  let cached = null;
156
169
  let capabilities = null;
157
170
 
158
171
  function location() {
159
- if (!cached) cached = locateCli({ env, logger });
172
+ if (!cached) cached = locateCli({ env, logger, resolvePackage });
160
173
  return cached;
161
174
  }
162
175
 
@@ -178,7 +191,16 @@ export function createCli({ env = process.env, logger = () => {} } = {}) {
178
191
  /** 能力位:`--json` 与 `--full-depth` 任一不支持都要在 UI 上禁用写操作(漏装比不可用更危险)。 */
179
192
  async capabilities({ force = false } = {}) {
180
193
  if (capabilities && !force) return capabilities;
181
- const loc = location();
194
+ let loc;
195
+ try {
196
+ loc = location();
197
+ } catch (error) {
198
+ if (!(error instanceof CliUnavailableError)) throw error;
199
+ // 不缓存:用户补好依赖 / PATH 后,下一次 /state 就能恢复,不必重启宿主;
200
+ // 同时清掉旧的成功缓存,避免 force 探测到 missing 后又回落到上一轮的 usable。
201
+ capabilities = null;
202
+ return { version: null, json: false, fullDepth: false, source: 'missing', usable: false };
203
+ }
182
204
  if (!loc.jsonCapable) {
183
205
  capabilities = { version: null, json: false, fullDepth: false, source: loc.source, usable: false };
184
206
  return capabilities;
package/lib/core/paths.js CHANGED
@@ -12,6 +12,10 @@ export function createPaths(env = process.env, overrides = {}) {
12
12
  const dshHome = overrides.dshHome ?? env.DSH_HOME ?? path.join(home, '.dsh');
13
13
  const stateDir =
14
14
  overrides.stateDir ?? env.DSH_SKILLS_CONSOLE_HOME ?? path.join(dshHome, 'skills-console');
15
+ // 与 skills CLI 的 getSkillLockPath 对齐:设了 XDG_STATE_HOME 时 lock 在 XDG 下
16
+ const lockFile = env.XDG_STATE_HOME
17
+ ? path.join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json')
18
+ : path.join(home, '.agents', '.skill-lock.json');
15
19
 
16
20
  return {
17
21
  home,
@@ -20,8 +24,8 @@ export function createPaths(env = process.env, overrides = {}) {
20
24
  stateDir,
21
25
  /** canonical 真身:CLI 唯一的落点(F1) */
22
26
  canonical: path.join(home, '.agents', 'skills'),
23
- /** CLI 全局 lock(F4) */
24
- lockFile: path.join(home, '.agents', '.skill-lock.json'),
27
+ /** CLI 全局 lock(F4);XDG_STATE_HOME 优先,与 CLI 一致 */
28
+ lockFile,
25
29
  /** DSH 原生 user-dsh 根(rank 400) */
26
30
  dshGlobalSkills: path.join(dshHome, 'skills'),
27
31
  stateFile: path.join(stateDir, 'state.json'),
@@ -33,6 +33,27 @@ function nowIso() {
33
33
  return new Date().toISOString();
34
34
  }
35
35
 
36
+ // skills CLI 对任意 GitHub 仓库走 git clone;没装 git 时它把错误打在 stdout(spinner)里
37
+ const GIT_MISSING_RE = /spawn git ENOENT|git: command not found|not recognized as an internal or external command/i;
38
+
39
+ /** 把 CLI 的 add 失败翻译成可执行错误;git 缺失时不要只给一句 spawn ENOENT。 */
40
+ function cliAddFailure(result, action) {
41
+ const detail = {
42
+ code: result.code,
43
+ stderr: (result.stderr || '').slice(-2000),
44
+ stdout: (result.stdout || '').slice(-2000),
45
+ };
46
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
47
+ if (GIT_MISSING_RE.test(output)) {
48
+ return new ServiceError(
49
+ 'error.skills.git-missing',
50
+ '安装/更新技能需要本机有 git(skills CLI 会 clone 仓库);请安装 git 后重试',
51
+ detail,
52
+ );
53
+ }
54
+ return new ServiceError('error.skills.cli-failed', `skills ${action} 失败`, detail);
55
+ }
56
+
36
57
  /** 本机时区的 `HH:mm`。手写而不是 `toLocaleTimeString`:不受 ICU / locale 差异影响。 */
37
58
  function formatClock(ms) {
38
59
  const at = new Date(ms);
@@ -192,6 +213,19 @@ export function createService({ paths, store, registry, cli, logger, env = proce
192
213
  if (store.readOnly) throw new StateReadOnlyError(store.problem);
193
214
  }
194
215
 
216
+ /** 所有会调 CLI 的写操作共用;缺失时给 400,不要把裸 CliUnavailableError 漏成 500。 */
217
+ async function requireUsableCli() {
218
+ const capabilities = await cli.capabilities();
219
+ if (!capabilities.usable) {
220
+ throw new ServiceError(
221
+ 'error.skills.cli-missing',
222
+ 'skills CLI 不可用或不支持 --json/--full-depth,写操作已禁用',
223
+ capabilities,
224
+ );
225
+ }
226
+ return capabilities;
227
+ }
228
+
195
229
  function detectedTargets() {
196
230
  return registry.detected();
197
231
  }
@@ -831,10 +865,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
831
865
 
832
866
  async install({ source, subscriptionId, skills = [], commit, targets, scopeProjects = [] }) {
833
867
  assertWritable();
834
- const capabilities = await cli.capabilities();
835
- if (!capabilities.usable) {
836
- throw new ServiceError('error.skills.cli-missing', 'skills CLI 不可用或不支持 --json/--full-depth,写操作已禁用', capabilities);
837
- }
868
+ await requireUsableCli();
838
869
  // 技能名可留空:有订阅快照就装快照里的条目,否则交给 CLI 的 `--skill '*'`(整仓全部)
839
870
  const requested = (skills || []).filter(Boolean);
840
871
 
@@ -853,12 +884,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
853
884
 
854
885
  const result = await cli.add({ source: sourceArg, skills: planned });
855
886
  if (result.timedOut) throw new ServiceError('error.skills.cli-timeout', 'skills add 超时');
856
- if (result.code !== 0) {
857
- throw new ServiceError('error.skills.cli-failed', 'skills add 失败', {
858
- code: result.code,
859
- stderr: result.stderr.slice(-2000),
860
- });
861
- }
887
+ if (result.code !== 0) throw cliAddFailure(result, 'add');
862
888
  // 留空时实际装了哪些技能以 CLI 的 JSON 输出为准(可能不止一个)
863
889
  const names = planned.length > 0 ? planned : installedNamesFrom(result.stdout);
864
890
  if (names.length === 0) {
@@ -947,6 +973,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
947
973
 
948
974
  async remove({ name }) {
949
975
  assertWritable();
976
+ await requireUsableCli(); // 快照与体检之前就拒绝,避免留下孤儿 trash
950
977
  return enqueue(`remove:${name}`, async () => {
951
978
  const canonicalPath = path.join(paths.canonical, name);
952
979
  if (!fs.existsSync(canonicalPath)) {
@@ -1341,6 +1368,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
1341
1368
 
1342
1369
  async applyUpdate({ name, token, overwrite = false }) {
1343
1370
  assertWritable();
1371
+ await requireUsableCli();
1344
1372
  return enqueue(`apply:${name}`, async () => {
1345
1373
  const plan = await this.planUpdate({ name });
1346
1374
  if (token && token !== plan.token) {
@@ -1371,12 +1399,21 @@ export function createService({ paths, store, registry, cli, logger, env = proce
1371
1399
 
1372
1400
  if (result.code !== 0 || !present || !verified) {
1373
1401
  if (backup) restoreBackup({ backup, toDir: canonicalPath });
1402
+ const failure = result.code !== 0 ? cliAddFailure(result, 'add') : null;
1403
+ const gitMissing = failure?.code === 'error.skills.git-missing';
1374
1404
  logger('updates.apply.failed', { name, code: result.code, verified, present });
1405
+ const suffix = backup ? ';已回滚到上一版' : ';没有可回滚的备份';
1375
1406
  return {
1376
1407
  ok: false,
1377
- code: 'error.skills.update-failed',
1378
- message: backup ? '更新失败,已回滚到上一版' : '更新失败,没有可回滚的备份',
1379
- detail: { cliCode: result.code, verified, present, stderr: result.stderr.slice(-2000) },
1408
+ code: gitMissing ? failure.code : 'error.skills.update-failed',
1409
+ message: gitMissing ? `${failure.message}${suffix}` : `更新失败${suffix}`,
1410
+ detail: {
1411
+ cliCode: result.code,
1412
+ verified,
1413
+ present,
1414
+ stderr: (result.stderr || '').slice(-2000),
1415
+ ...(failure?.detail || {}),
1416
+ },
1380
1417
  backup: backup?.key || null,
1381
1418
  };
1382
1419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-skills-console",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "morehao <morehao@qq.com>",
@@ -68,6 +68,9 @@
68
68
  "dsh-plugin",
69
69
  "skills"
70
70
  ],
71
+ "dependencies": {
72
+ "skills": "1.7.4"
73
+ },
71
74
  "devDependencies": {
72
75
  "esbuild": "0.28.2"
73
76
  }