dsh-plugin 2.2.6 → 2.3.0

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,23 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * profile 内 YAML 配置损坏的识别:pnpm 会在 profile 目录读 pnpm-workspace.yaml(构建设置)
7
+ * 与 pnpm-lock.yaml(锁文件),任一含重复键或缩进错误时,解析阶段就直接退出,报错形如
8
+ * `[ERROR] duplicated mapping key (10:3)`。此时依赖解析、registry 请求都还没开始,
9
+ * 与插件、网络、源都无关 —— 只能由用户修本机文件,因此单独打 ASCII 标记,
10
+ * 前端据此给处理步骤,且不引导向插件仓库提 Issue。
11
+ */
12
+
13
+ /** 本机 YAML 解析失败的判据:重复的映射键、映射/序列缩进错误(js-yaml 原文)。 */
14
+ const YAML_PARSE_ERROR_RE = /duplicated mapping key|bad indentation of a (?:mapping|sequence) entry/i
15
+
16
+ /**
17
+ * 输出里出现本机 YAML 解析失败 → 返回 ASCII 标记行;否则 null。
18
+ * 纯 ASCII + 不提插件:前端按 `[profile-yaml]` 归类,Windows GBK 解码下同样能命中。
19
+ */
20
+ export function profileYamlMarker(output: string): string | null {
21
+ if (!YAML_PARSE_ERROR_RE.test(output)) return null
22
+ return '[profile-yaml] pnpm could not parse a YAML file in the profile directory (duplicated mapping key or broken indentation) - fix or delete pnpm-workspace.yaml / pnpm-lock.yaml in the profile directory and retry; local environment issue, not a problem with the plugin'
23
+ }
@@ -26,6 +26,7 @@ import { commandOnPath, gitMissingDetail } from './preflight.ts'
26
26
  import { isTransportFailure, registryHost } from './registry-fallback.ts'
27
27
  import { dshTooOldMarker } from './dsh-compat.ts'
28
28
  import { OutputDecoder } from './decode-output.ts'
29
+ import { profileYamlMarker } from './profile-yaml.ts'
29
30
  import { runtimeExecPath } from '../runtime-exec.ts'
30
31
 
31
32
  /** 内存任务注册表,按任务 id 索引。 */
@@ -811,6 +812,18 @@ function annotateNpmTooLow(result: InstallResult, options: {
811
812
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
812
813
  }
813
814
 
815
+ /** 安装失败且 profile 的 YAML 配置解析不了(重复键/缩进错误)→ 注入 [profile-yaml] 标记。
816
+ * 判据见 profile-yaml.ts:这类失败发生在依赖解析之前,与插件、源、网络都无关。 */
817
+ function annotateProfileYaml(result: InstallResult, options: {
818
+ task?: InstallTask
819
+ }): InstallResult {
820
+ if (result.exitCode === 0 && !result.timedOut) return result
821
+ const line = profileYamlMarker(`${result.stderr}\n${result.stdout}`)
822
+ if (line === null) return result
823
+ if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
824
+ return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
825
+ }
826
+
814
827
  /** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
815
828
  * 异步是因为要联网;只在失败路径上发生。 */
816
829
  async function annotateRegistryMissing(result: InstallResult, options: {
@@ -970,6 +983,9 @@ export async function runPluginMutation(options: {
970
983
  }
971
984
  }
972
985
  }
986
+ // profile 里的 YAML 配置(pnpm-workspace.yaml / pnpm-lock.yaml)解析失败 → 追加 [profile-yaml]
987
+ // 标记,前端给「修本机文件」的步骤,不引导向插件仓库提 Issue
988
+ result = annotateProfileYaml(result, options)
973
989
  // 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
974
990
  result = await annotateRegistryMissing(result, options)
975
991
  // 插件声明的最低 DSH 版本高于当前宿主 → 追加 [dsh-too-old] 标记,前端提示先升级 DSH
@@ -287,13 +287,29 @@ export async function finalUrl(url: string, proxy: string, timeoutMs: number): P
287
287
  */
288
288
  export function fetchViaCurl(
289
289
  url: string, proxy: string, timeoutMs: number,
290
- ): Promise<{ ok: boolean; body: string; reason: ProbeFailReason | null }> {
290
+ ): Promise<{ ok: boolean; body: string; reason: ProbeFailReason | null; detail: string | null }> {
291
291
  // 同样用 -sS:正文之外还要 stderr 里的 TLS 失败线索(被 stderr 收集,不外泄给用户)。
292
- // --compressed:目录 JSON 约 3.6MB,源站支持 gzip,压缩后弱网下也能在超时内拉完。
293
- const args = ['-sS', '--compressed', '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url]
294
- return curlWithTlsFallback(args, proxyEnv(proxy), timeoutMs).then((r) => ({
295
- ok: r.code === 0,
296
- body: r.stdout,
297
- reason: r.code !== 0 && tlsRevocationBlocked(r.stderr) ? 'tlsRevocation' : null,
298
- }))
292
+ // 编码不用 --compressed 的自动协商,而是明确只接受 gzip:协商会让服务端按本机 curl 声明的
293
+ // 能力回 br / zstd,解不开时 curl 非零退出且正文为空,目录拉取直接失败;而 gzip 的解码器
294
+ // 每个 curl 都有,对 JSON 的压缩率也够。
295
+ const gzip = ['-H', 'Accept-Encoding: gzip', '--compressed']
296
+ const attempt = (extra: string[]): string[] => [
297
+ '-sS', ...gzip, ...extra, '--max-time', String(Math.max(1, Math.round(timeoutMs / 1000))), url,
298
+ ]
299
+ return curlWithTlsFallback(attempt([]), proxyEnv(proxy), timeoutMs).then(async (first) => {
300
+ // 首轮失败(连接被重置 / 编码解不开 / HTTP/2 流异常)时退到 HTTP/1.1 再试一次:
301
+ // Windows 自带 curl 与大响应 + HTTP/2 的组合偶发流级失败,换协议版本代价最小;
302
+ // 只在真失败时才多一次请求。
303
+ const r = first.code === 0
304
+ ? first
305
+ : await curlWithTlsFallback(attempt(['--http1.1']), proxyEnv(proxy), timeoutMs)
306
+ const failed = r.code !== 0 || r.stdout === ''
307
+ return {
308
+ ok: !failed,
309
+ body: r.stdout,
310
+ reason: failed && tlsRevocationBlocked(r.stderr) ? 'tlsRevocation' : null,
311
+ // 只留退出码与 stderr 末行:正文不进错误详情,且要够短才能在界面上完整显示
312
+ detail: failed ? `curl exit ${r.code ?? 'null'}: ${r.stderr.trim().split('\n').slice(-1)[0] ?? ''}`.slice(0, 300) : null,
313
+ }
314
+ })
299
315
  }
@@ -157,16 +157,23 @@ function quoteYamlKey(key: string): string {
157
157
  * 向 profile 的 pnpm-workspace.yaml 追加一条 `allowBuilds` 记录:与已有条目合并,
158
158
  * 其余 yaml 保持不变;重写时丢弃损坏的占位条目(pnpm 安装失败的 bug 会写入
159
159
  * 字面量 `set this to true or false`)。
160
+ *
161
+ * 任何形态的既有 `allowBuilds` 都会被**整体替换**成规范化块:块写法、行内数组
162
+ * `allowBuilds: ["a@1"]`、以及末尾无换行的 `allowBuilds:` 都算命中。否则会在文件里
163
+ * 再多写一个同名键 —— pnpm 解析时直接报 `duplicated mapping key`,该 profile 里
164
+ * 所有插件都装不上(#562)。
160
165
  */
161
166
  export function addAllowBuildsKey(profile: string, key: string): void {
162
167
  const file = join(profileDirectory(profile), 'pnpm-workspace.yaml')
163
168
  let yaml = ''
164
169
  try { yaml = readFileSync(file, 'utf8') } catch { /* 文件不存在,下方创建 */ }
165
170
  const eol = /\r\n/.test(yaml) ? '\r\n' : '\n'
166
- const blockRe = /allowBuilds:[ \t]*\r?\n((?:[ \t]+[^\r\n]*\r?\n?)*)/g
171
+ // header 行(可选行内值)+ 其后任意缩进条目行;行内写法没有后续缩进行也能匹配
172
+ const blockRe = /^[ \t]*allowBuilds:[^\r\n]*(?:(?:\r?\n)[ \t]{2,}[^\r\n]*)*/gm
167
173
  const map = new Map<string, string>()
168
174
  for (const match of Array.from(yaml.matchAll(blockRe))) {
169
- for (const line of match[1].split(/\r?\n/)) {
175
+ const [header, ...rest] = match[0].split(/\r?\n/)
176
+ for (const line of rest) {
170
177
  const m = /^[ \t]+(\S.*?)\s*:\s*(true|false)?\s*$/.exec(line)
171
178
  if (m === null || m[1] === '') continue
172
179
  let k = m[1]
@@ -175,16 +182,20 @@ export function addAllowBuildsKey(profile: string, key: string): void {
175
182
  }
176
183
  map.set(k, m[2] ?? 'true')
177
184
  }
185
+ // 行内数组写法:allowBuilds: ["a@1.0.0", 'b@2'] —— 保留已有条目,别丢
186
+ const inline = /^[ \t]*allowBuilds:\s*\[(.*)\]\s*$/.exec(header)
187
+ if (inline !== null) {
188
+ for (const raw of inline[1].split(',')) {
189
+ const k = raw.trim().replace(/^['"]|['"]$/g, '')
190
+ if (k !== '') map.set(k, 'true')
191
+ }
192
+ }
178
193
  }
179
194
  map.set(key, 'true')
180
195
  const block = Array.from(map).map(([k, v]) => ` ${quoteYamlKey(k)}: ${v}`).join(eol)
181
196
  const blockText = `allowBuilds:${eol}${block}${eol}`
182
- let next: string
183
- if (/allowBuilds:[ \t]*\r?\n/.test(yaml)) {
184
- let seen = 0
185
- next = yaml.replace(blockRe, () => (seen++ === 0 ? blockText : ''))
186
- } else {
187
- next = `${yaml.replace(/\r?\n?$/, eol)}${blockText}`
188
- }
197
+ let seen = 0
198
+ let next = yaml.replace(blockRe, () => (seen++ === 0 ? blockText : ''))
199
+ if (seen === 0) next = `${yaml.replace(/\r?\n?$/, eol)}${blockText}`
189
200
  writeFileSync(file, next)
190
201
  }