dsh-plugin 1.4.9 → 1.4.11

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.
Files changed (33) hide show
  1. package/client/client.js +355 -365
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +45 -19
  4. package/lib/services/install/npm-check.js +4 -5
  5. package/lib/services/install/task-queue.js +51 -13
  6. package/lib/services/probe.js +20 -7
  7. package/lib/services/profile/profile.js +1 -1
  8. package/lib/types/http/routes.d.ts +3 -0
  9. package/lib/types/services/install/install-types.d.ts +2 -0
  10. package/lib/types/services/install/npm-check.d.ts +1 -1
  11. package/lib/types/services/install/task-queue.d.ts +7 -0
  12. package/lib/types/services/probe.d.ts +2 -2
  13. package/lib/types/services/profile/profile.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/client/components/PluginHubSection.tsx +1 -1
  16. package/src/client/components/catalog/PluginCard.tsx +2 -2
  17. package/src/client/components/modals/NotificationsModal.tsx +13 -9
  18. package/src/client/components/modals/modals.tsx +10 -6
  19. package/src/client/data/catalog.ts +1 -1
  20. package/src/client/hooks/useCatalog.ts +2 -2
  21. package/src/client/locales.ts +7 -0
  22. package/src/client/logic/constants.ts +1 -1
  23. package/src/client/logic/failures.ts +76 -95
  24. package/src/client/logic/urls.ts +5 -2
  25. package/src/client/styles/InstalledView.module.css +1 -1
  26. package/src/client/styles/Modal.module.css +2 -2
  27. package/src/client/types.ts +1 -1
  28. package/src/server/http/routes.ts +41 -19
  29. package/src/server/services/install/install-types.ts +2 -0
  30. package/src/server/services/install/npm-check.ts +4 -5
  31. package/src/server/services/install/task-queue.ts +49 -12
  32. package/src/server/services/probe.ts +20 -6
  33. package/src/server/services/profile/profile.ts +1 -1
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { spawn, spawnSync } from 'node:child_process'
11
11
  import { resolvePackageEntry } from './package-entry.ts'
12
- import { readFileSync, statSync } from 'node:fs'
12
+ import { existsSync, readFileSync, statSync } from 'node:fs'
13
13
  import { dirname, join, resolve } from 'node:path'
14
14
  import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
15
15
  import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
@@ -129,20 +129,55 @@ function pushLine(task: InstallTask, line: string): void {
129
129
  }
130
130
  }
131
131
 
132
- function cliInvocation(): Invocation {
132
+ function cliInvocation(profile: string): Invocation {
133
133
  const entry = process.argv[1]
134
- if (entry !== undefined && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
135
- const absoluteEntry = resolve(entry)
136
- return {
137
- file: process.execPath,
138
- prefixArgs: [...process.execArgv, absoluteEntry],
139
- cwd: dirname(absoluteEntry),
140
- useShell: false,
134
+ if (entry !== undefined) {
135
+ // 打包桌面端(Electron):宿主入口在 app.asar 内,机器上根本没有 `dsh` 可执行文件
136
+ //(Windows 那条 dsh.cmd shim 只存在于宿主进程自己的 PATH 里),直接回退 PATH 必然
137
+ // 报 [dsh-missing](见 issue #71)。这里改用应用自己的 CLI 引导脚本调起同一份打包
138
+ // CLI:Electron 可执行文件 + --expose-internals + desktop-cli.js,与内置终端、
139
+ // 官方插件页安装完全同一条路径 —— desktop profile 由应用独占管理的授权也在其中。
140
+ const bootstrap = desktopCliBootstrap(entry)
141
+ if (bootstrap !== null) {
142
+ return {
143
+ file: process.execPath,
144
+ prefixArgs: ['--expose-internals', bootstrap],
145
+ // 工作目录用 profile 目录(真实存在的目录):asar 内的路径不能作为 spawn 的 cwd
146
+ cwd: profileDirectory(profile),
147
+ useShell: false,
148
+ // Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
149
+ env: { ELECTRON_RUN_AS_NODE: '1' },
150
+ }
151
+ }
152
+ if (/[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
153
+ const absoluteEntry = resolve(entry)
154
+ return {
155
+ file: process.execPath,
156
+ prefixArgs: [...process.execArgv, absoluteEntry],
157
+ cwd: dirname(absoluteEntry),
158
+ useShell: false,
159
+ }
141
160
  }
142
161
  }
143
162
  return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
144
163
  }
145
164
 
165
+ /**
166
+ * 打包桌面端宿主的 CLI 引导脚本路径(`<asar 根>/lib/desktop-cli.js`,即应用内
167
+ * pnpm 服务的 dshBootstrapPath)。宿主入口落在 app.asar 内时按 asar 根反推候选路径,
168
+ * 命中存在的那个才采用;布局不认识(官方改版)时返回 null,调用方回退原逻辑。
169
+ * 导出供单测覆盖路径推导。
170
+ */
171
+ export function desktopCliBootstrap(entry: string): string | null {
172
+ const marker = /[\\/]app\.asar(?=[\\/]|$)/i.exec(entry)
173
+ if (marker === null) return null
174
+ const root = entry.slice(0, marker.index + marker[0].length)
175
+ for (const candidate of [join(root, 'lib', 'desktop-cli.js'), join(root, 'dsh', 'lib', 'desktop-cli.js')]) {
176
+ if (existsSync(candidate)) return candidate
177
+ }
178
+ return null
179
+ }
180
+
146
181
  /**
147
182
  * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
148
183
  * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
@@ -335,7 +370,7 @@ function spawnMutation(options: {
335
370
  const isGlobalNpm = (globalNpm ?? []).length > 0
336
371
  const invocation = isGlobalNpm
337
372
  ? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
338
- : cliInvocation()
373
+ : cliInvocation(profile)
339
374
  // 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
340
375
  // (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
341
376
  // 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
@@ -383,7 +418,9 @@ function spawnMutation(options: {
383
418
  }
384
419
  child = spawn(invocation.file, attemptArgs, {
385
420
  cwd: invocation.cwd,
386
- env,
421
+ // invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
422
+ // 没有它时保持 env 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
423
+ env: invocation.env === undefined ? env : { ...(env ?? process.env), ...invocation.env },
387
424
  shell: invocation.useShell,
388
425
  // detached 使子进程成为独立进程组组长:取消时能整组清理(见 stopChild),
389
426
  // 且宿主进程崩溃后安装任务不会被连带误杀
@@ -485,7 +522,7 @@ function spawnMutation(options: {
485
522
 
486
523
  return (async () => {
487
524
  let result = await spawnOnce(args)
488
- // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`,npm/cli#9911):
525
+ // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`):
489
526
  // 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
490
527
  // 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
491
528
  // 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
@@ -38,7 +38,7 @@ const probeFail = (): ProbeResult => ({ ok: false, ms: null, status: null })
38
38
  * 代理受限或吊销服务不可达时整个握手直接失败(CRYPT_E_NO_REVOCATION_CHECK),
39
39
  * 表现为 `curl: (35) schannel: next InitializeSecurityContext failed` —— 而 Node/npm/
40
40
  * 浏览器各有自己的 TLS 实现,照常可用,用户只会看到「插件市场打不开」。
41
- * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,握手不再被它拦死;
41
+ * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,避免它拦死握手;
42
42
  * 其它平台后端(SecureTransport / OpenSSL)没有这个强制行为,不加参数。
43
43
  * 运行时读 process.platform(而非模块级常量),便于测试覆盖两条分支。
44
44
  */
@@ -51,6 +51,9 @@ export function curlRevokeFlagUnsupported(code: number | null, stderr: string):
51
51
  return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr)
52
52
  }
53
53
 
54
+ /** 彻底关闭证书吊销检查的兜底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
55
+ const NO_REVOKE_ARG = '--ssl-no-revoke'
56
+
54
57
  /** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
55
58
  export function tlsRevocationBlocked(stderr: string): boolean {
56
59
  return /CRYPT_E_NO_REVOCATION_CHECK|CRYPT_E_REVOCATION_OFFLINE|0x80092012/i.test(stderr)
@@ -96,7 +99,7 @@ function macSystemProxy(): string | null {
96
99
  * `http=host:port;https=host:port`(按协议分别配置)。
97
100
  * 字符类必须同时排除 `/` 与 `=`:只排除 `=` 时,带 scheme 的值会让「主机名」吞掉 `//`,
98
101
  * 拼出 `http:////127.0.0.1:10793` 这种立刻失败的非法地址 —— 代理本来是通的,却把诊断、
99
- * 目录拉取、npm/git 预检全判成「不可达」(dsh-plugin-hub#66)。
102
+ * 目录拉取、npm/git 预检全判成「不可达」。
100
103
  */
101
104
  export function parseWinProxyServer(stdout: string): string | null {
102
105
  const m = /([^:\s=/]+):(\d+)/.exec(stdout)
@@ -159,12 +162,23 @@ function runCurl(args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Pro
159
162
  }
160
163
 
161
164
  /** 带 TLS 兜底的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
162
- * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败。 */
165
+ * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
166
+ * 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
167
+ * 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
168
+ * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达(见 issue #71)。
169
+ * 吊销检查只影响本次探测自身的信任判断,不改变系统 TLS 配置。 */
163
170
  async function curlWithTlsFallback(args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Promise<CurlRun> {
164
171
  const tlsArgs = curlTlsArgs()
165
- const first = await runCurl([...tlsArgs, ...args], env, timeoutMs)
166
- if (tlsArgs.length === 0 || !curlRevokeFlagUnsupported(first.code, first.stderr)) return first
167
- return runCurl(args, env, timeoutMs)
172
+ let run = await runCurl([...tlsArgs, ...args], env, timeoutMs)
173
+ if (tlsArgs.length > 0 && curlRevokeFlagUnsupported(run.code, run.stderr)) {
174
+ run = await runCurl(args, env, timeoutMs)
175
+ }
176
+ if (tlsArgs.length > 0 && tlsRevocationBlocked(run.stderr)) {
177
+ const retry = await runCurl([NO_REVOKE_ARG, ...args], env, timeoutMs)
178
+ // 只在兜底本身跑起来(非「不认识该参数」)时采用其结果,否则保留原始失败线索
179
+ if (retry.code !== null && !curlRevokeFlagUnsupported(retry.code, retry.stderr)) run = retry
180
+ }
181
+ return run
168
182
  }
169
183
 
170
184
  /**
@@ -22,7 +22,7 @@ export function readProfileArg(fallback = 'web'): string {
22
22
  * - Electron 桌面宿主:不传 `--profile`,而是把 profile 目录作为位置参数传入
23
23
  * (`… <dshRoot> <home>\profiles\desktop …`)。
24
24
  * 桌面端只认 `--profile` 会永远落回 fallback,把路由/日志/安装全挂到错误的 profile 上
25
- * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」,dsh-plugin-hub#65)。
25
+ * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」)。
26
26
  * 位置参数按「父目录名为 profiles」精确识别 profile 目录,避免误取其它位置参数。
27
27
  */
28
28
  export function profileFromArgv(argv: readonly string[], fallback = 'web'): string {