dsh-plugin 2.0.5 → 2.0.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.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 安装前置探测:命令是否存在、安装目标是否需要 git。
3
+ *
4
+ * 命令缺失时各平台报错形态不同(Windows 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
5
+ * 统一在 spawn 前用 which/where 探测,缺失则打 ASCII 标记,前端归类不受编码影响。
6
+ */
7
+ import { spawnSync } from 'node:child_process';
8
+ /** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
9
+ export const GIT_MISSING_DETAIL = '[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)';
10
+ /** 命令存在性探测:which/where 找 PATH 里的命令。
11
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
12
+ export function commandOnPath(cmd, env) {
13
+ try {
14
+ const probe = process.platform === 'win32' ? 'where' : 'which';
15
+ const res = spawnSync(probe, [cmd], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
16
+ return res.status === 0 && (res.stdout ?? '').trim().length > 0;
17
+ }
18
+ catch {
19
+ return true;
20
+ }
21
+ }
22
+ /** 该安装目标是否需要 git:github: / git+ / git@ 与 owner/repo 简写都要先 clone;
23
+ * release 直链(https://…releases/download/…tgz)由 pnpm 直接下载,不需要 git。
24
+ * owner/repo 段首必须是字母数字:`./x`、`../x` 这类相对路径是本地目录,不能误判。 */
25
+ export function needsGit(target) {
26
+ const v = target.trim();
27
+ if (/^(github:|git\+|git@)/i.test(v))
28
+ return true;
29
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9._-]+$/.test(v);
30
+ }
31
+ /** git 源安装前置检查:需要 git 但本机 PATH 里没有 → 返回 [git-missing] 文本;否则 null。 */
32
+ export function gitMissingDetail(target, env) {
33
+ if (!needsGit(target))
34
+ return null;
35
+ return commandOnPath('git', env) ? null : GIT_MISSING_DETAIL;
36
+ }
@@ -7,7 +7,7 @@
7
7
  * 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
8
8
  * 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
9
9
  */
10
- import { spawn, spawnSync } from 'node:child_process';
10
+ import { spawn } from 'node:child_process';
11
11
  import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.js';
12
12
  import { existsSync, readFileSync, statSync } from 'node:fs';
13
13
  import { dirname, join, resolve } from 'node:path';
@@ -20,6 +20,7 @@ import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '.
20
20
  import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
21
21
  import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
22
22
  import { appendLog } from '../log.js';
23
+ import { commandOnPath, gitMissingDetail } from './preflight.js';
23
24
  /** 内存任务注册表,按任务 id 索引。 */
24
25
  const tasks = new Map();
25
26
  let nextTaskId = 1;
@@ -194,22 +195,6 @@ export function desktopCliBootstrap(entry) {
194
195
  }
195
196
  return null;
196
197
  }
197
- /**
198
- * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
199
- * 命令缺失时各平台报错形态不同(Windows shell 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
200
- * 统一在 spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
201
- * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
202
- */
203
- function dshOnPath(env) {
204
- try {
205
- const probe = process.platform === 'win32' ? 'where' : 'which';
206
- const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
207
- return res.status === 0 && (res.stdout ?? '').trim().length > 0;
208
- }
209
- catch {
210
- return true;
211
- }
212
- }
213
198
  function stopChild(child) {
214
199
  if (process.platform === 'win32' && child.pid !== undefined) {
215
200
  const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
@@ -400,7 +385,7 @@ function spawnMutation(options) {
400
385
  // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
401
386
  // 命令缺失时各平台报错形态不同(GBK 乱码 / spawn ENOENT),统一前置拦截并打 ASCII 标记
402
387
  // [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
403
- if (invocation.file === 'dsh' && !dshOnPath(env)) {
388
+ if (invocation.file === 'dsh' && !commandOnPath('dsh', env)) {
404
389
  const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)';
405
390
  if (task) {
406
391
  task.status = 'failed';
@@ -411,6 +396,21 @@ function spawnMutation(options) {
411
396
  resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail });
412
397
  return;
413
398
  }
399
+ // git 源安装:pnpm 需要调用 git 解析仓库;Windows 缺 git 只回 GBK 乱码,前端认不出来。
400
+ // 前置探测并打 ASCII 标记 [git-missing],给出装 git 的指引而不是笼统的安装失败。
401
+ if (!isGlobalNpm && action !== 'remove') {
402
+ const gitMissing = gitMissingDetail(target, env);
403
+ if (gitMissing !== null) {
404
+ if (task) {
405
+ task.status = 'failed';
406
+ task.exitCode = null;
407
+ task.progress = 0;
408
+ pushLine(task, gitMissing);
409
+ }
410
+ resolveOnce({ exitCode: 1, timedOut: false, error: gitMissing, stdout: '', stderr: gitMissing });
411
+ return;
412
+ }
413
+ }
414
414
  child = spawn(invocation.file, attemptArgs, {
415
415
  cwd: invocation.cwd,
416
416
  // invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
@@ -0,0 +1,11 @@
1
+ /** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
2
+ export declare const GIT_MISSING_DETAIL = "[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)";
3
+ /** 命令存在性探测:which/where 找 PATH 里的命令。
4
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
5
+ export declare function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean;
6
+ /** 该安装目标是否需要 git:github: / git+ / git@ 与 owner/repo 简写都要先 clone;
7
+ * release 直链(https://…releases/download/…tgz)由 pnpm 直接下载,不需要 git。
8
+ * owner/repo 段首必须是字母数字:`./x`、`../x` 这类相对路径是本地目录,不能误判。 */
9
+ export declare function needsGit(target: string): boolean;
10
+ /** git 源安装前置检查:需要 git 但本机 PATH 里没有 → 返回 [git-missing] 文本;否则 null。 */
11
+ export declare function gitMissingDetail(target: string, env?: NodeJS.ProcessEnv): string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -48,6 +48,12 @@ export interface InstalledItem {
48
48
  dshCapable: boolean
49
49
  }
50
50
 
51
+ /** 本地路径安装(file:/link:/绝对或相对路径):身份在本机,不能按 npm 包名认领目录条目。 */
52
+ function isLocalSpec(value: string): boolean {
53
+ const v = value.trim()
54
+ return /^(file|link):/i.test(v) || v.startsWith('.') || v.startsWith('/') || /^[A-Za-z]:[\\/]/.test(v)
55
+ }
56
+
51
57
  /** 安装时记录的目录信号(按仓库小写 key 查表)。 */
52
58
  function signalOf(repo: string | null, versions: Record<string, InstalledVersionSignal>): InstalledVersionSignal | null {
53
59
  if (!repo) return null
@@ -66,15 +72,13 @@ export function installedNameOf(
66
72
  for (const [name, spec] of Object.entries(installed)) {
67
73
  if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
68
74
  }
69
- // npm 安装:profile 依赖 key 直接是 npm 包名。包名来源两处——目录数据(npmPackage),
70
- // 或服务端安装时持久化的映射(versions[repo].npmPackage,覆盖组织 scope 与 GitHub
71
- // 用户名不一致、目录未下发包名的场景);命中任一并依赖 key 真实存在即视为已安装。
72
- // 只认 npm 安装的依赖(spec 无法解析出仓库身份):git/本地安装的依赖身份由仓库匹配决定,
73
- // 包名同名不代表同仓库——否则「git 装 A 仓库」会被「npm 包名同名但仓库不同」的目录条目
74
- // 误认领成已安装(假阳性),installedItems 里出现同 key 双条目,排序一变化就渲染出幽灵行。
75
+ // 只认 npm 安装的依赖(spec 无法解析出仓库身份、也不是本地路径):git 安装的依赖身份由仓库
76
+ // 匹配决定,file:/link: 安装的本地插件身份在本机 —— 包名同名不代表同仓库,否则「本地装 A」
77
+ // 会被目录里同名的 B 认领,已安装页显示成 B 的信息。
75
78
  const pkg = ((plugin.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? '').toLowerCase()
76
79
  if (pkg) {
77
80
  for (const [name, spec] of Object.entries(installed)) {
81
+ if (isLocalSpec(spec)) continue
78
82
  if (isRepoLike(repoFromInstallTarget(spec))) continue
79
83
  if (name.toLowerCase() === pkg) return name
80
84
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 安装前置探测:命令是否存在、安装目标是否需要 git。
3
+ *
4
+ * 命令缺失时各平台报错形态不同(Windows 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
5
+ * 统一在 spawn 前用 which/where 探测,缺失则打 ASCII 标记,前端归类不受编码影响。
6
+ */
7
+ import { spawnSync } from 'node:child_process'
8
+
9
+ /** 缺 git 的失败标记:前端按 `[git-missing]` 归类并给「安装 git」指引(乱码免疫)。 */
10
+ export const GIT_MISSING_DETAIL = '[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)'
11
+
12
+ /** 命令存在性探测:which/where 找 PATH 里的命令。
13
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
14
+ export function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean {
15
+ try {
16
+ const probe = process.platform === 'win32' ? 'where' : 'which'
17
+ const res = spawnSync(probe, [cmd], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] })
18
+ return res.status === 0 && (res.stdout ?? '').trim().length > 0
19
+ } catch {
20
+ return true
21
+ }
22
+ }
23
+
24
+ /** 该安装目标是否需要 git:github: / git+ / git@ 与 owner/repo 简写都要先 clone;
25
+ * release 直链(https://…releases/download/…tgz)由 pnpm 直接下载,不需要 git。
26
+ * owner/repo 段首必须是字母数字:`./x`、`../x` 这类相对路径是本地目录,不能误判。 */
27
+ export function needsGit(target: string): boolean {
28
+ const v = target.trim()
29
+ if (/^(github:|git\+|git@)/i.test(v)) return true
30
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9._-]+$/.test(v)
31
+ }
32
+
33
+ /** git 源安装前置检查:需要 git 但本机 PATH 里没有 → 返回 [git-missing] 文本;否则 null。 */
34
+ export function gitMissingDetail(target: string, env?: NodeJS.ProcessEnv): string | null {
35
+ if (!needsGit(target)) return null
36
+ return commandOnPath('git', env) ? null : GIT_MISSING_DETAIL
37
+ }
@@ -22,6 +22,7 @@ import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '.
22
22
  import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.ts'
23
23
  import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.ts'
24
24
  import { appendLog } from '../log.ts'
25
+ import { commandOnPath, gitMissingDetail } from './preflight.ts'
25
26
 
26
27
  /** 内存任务注册表,按任务 id 索引。 */
27
28
  const tasks = new Map<number, InstallTask>()
@@ -212,22 +213,6 @@ export function desktopCliBootstrap(entry: string): string | null {
212
213
  return null
213
214
  }
214
215
 
215
- /**
216
- * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
217
- * 命令缺失时各平台报错形态不同(Windows shell 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
218
- * 统一在 spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
219
- * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
220
- */
221
- function dshOnPath(env?: NodeJS.ProcessEnv): boolean {
222
- try {
223
- const probe = process.platform === 'win32' ? 'where' : 'which'
224
- const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] })
225
- return res.status === 0 && (res.stdout ?? '').trim().length > 0
226
- } catch {
227
- return true
228
- }
229
- }
230
-
231
216
  function stopChild(child: ReturnType<typeof spawn>): void {
232
217
  if (process.platform === 'win32' && child.pid !== undefined) {
233
218
  const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' })
@@ -452,7 +437,7 @@ function spawnMutation(options: {
452
437
  // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
453
438
  // 命令缺失时各平台报错形态不同(GBK 乱码 / spawn ENOENT),统一前置拦截并打 ASCII 标记
454
439
  // [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
455
- if (invocation.file === 'dsh' && !dshOnPath(env)) {
440
+ if (invocation.file === 'dsh' && !commandOnPath('dsh', env)) {
456
441
  const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)'
457
442
  if (task) {
458
443
  task.status = 'failed'
@@ -463,6 +448,21 @@ function spawnMutation(options: {
463
448
  resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail })
464
449
  return
465
450
  }
451
+ // git 源安装:pnpm 需要调用 git 解析仓库;Windows 缺 git 只回 GBK 乱码,前端认不出来。
452
+ // 前置探测并打 ASCII 标记 [git-missing],给出装 git 的指引而不是笼统的安装失败。
453
+ if (!isGlobalNpm && action !== 'remove') {
454
+ const gitMissing = gitMissingDetail(target, env)
455
+ if (gitMissing !== null) {
456
+ if (task) {
457
+ task.status = 'failed'
458
+ task.exitCode = null
459
+ task.progress = 0
460
+ pushLine(task, gitMissing)
461
+ }
462
+ resolveOnce({ exitCode: 1, timedOut: false, error: gitMissing, stdout: '', stderr: gitMissing })
463
+ return
464
+ }
465
+ }
466
466
  child = spawn(invocation.file, attemptArgs, {
467
467
  cwd: invocation.cwd,
468
468
  // invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):