dsh-plugin 2.2.6 → 2.2.7

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,22 @@
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
+ /** 本机 YAML 解析失败的判据:重复的映射键、映射/序列缩进错误(js-yaml 原文)。 */
13
+ const YAML_PARSE_ERROR_RE = /duplicated mapping key|bad indentation of a (?:mapping|sequence) entry/i;
14
+ /**
15
+ * 输出里出现本机 YAML 解析失败 → 返回 ASCII 标记行;否则 null。
16
+ * 纯 ASCII + 不提插件:前端按 `[profile-yaml]` 归类,Windows GBK 解码下同样能命中。
17
+ */
18
+ export function profileYamlMarker(output) {
19
+ if (!YAML_PARSE_ERROR_RE.test(output))
20
+ return null;
21
+ 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';
22
+ }
@@ -24,6 +24,7 @@ import { commandOnPath, gitMissingDetail } from './preflight.js';
24
24
  import { isTransportFailure, registryHost } from './registry-fallback.js';
25
25
  import { dshTooOldMarker } from './dsh-compat.js';
26
26
  import { OutputDecoder } from './decode-output.js';
27
+ import { profileYamlMarker } from './profile-yaml.js';
27
28
  import { runtimeExecPath } from '../runtime-exec.js';
28
29
  /** 内存任务注册表,按任务 id 索引。 */
29
30
  const tasks = new Map();
@@ -778,6 +779,18 @@ function annotateNpmTooLow(result, options) {
778
779
  pushLine(options.task, line);
779
780
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
780
781
  }
782
+ /** 安装失败且 profile 的 YAML 配置解析不了(重复键/缩进错误)→ 注入 [profile-yaml] 标记。
783
+ * 判据见 profile-yaml.ts:这类失败发生在依赖解析之前,与插件、源、网络都无关。 */
784
+ function annotateProfileYaml(result, options) {
785
+ if (result.exitCode === 0 && !result.timedOut)
786
+ return result;
787
+ const line = profileYamlMarker(`${result.stderr}\n${result.stdout}`);
788
+ if (line === null)
789
+ return result;
790
+ if (options.task && options.task.status !== 'cancelled')
791
+ pushLine(options.task, line);
792
+ return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
793
+ }
781
794
  /** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
782
795
  * 异步是因为要联网;只在失败路径上发生。 */
783
796
  async function annotateRegistryMissing(result, options) {
@@ -922,6 +935,9 @@ export async function runPluginMutation(options) {
922
935
  }
923
936
  }
924
937
  }
938
+ // profile 里的 YAML 配置(pnpm-workspace.yaml / pnpm-lock.yaml)解析失败 → 追加 [profile-yaml]
939
+ // 标记,前端给「修本机文件」的步骤,不引导向插件仓库提 Issue
940
+ result = annotateProfileYaml(result, options);
925
941
  // 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
926
942
  result = await annotateRegistryMissing(result, options);
927
943
  // 插件声明的最低 DSH 版本高于当前宿主 → 追加 [dsh-too-old] 标记,前端提示先升级 DSH
@@ -156,6 +156,11 @@ function quoteYamlKey(key) {
156
156
  * 向 profile 的 pnpm-workspace.yaml 追加一条 `allowBuilds` 记录:与已有条目合并,
157
157
  * 其余 yaml 保持不变;重写时丢弃损坏的占位条目(pnpm 安装失败的 bug 会写入
158
158
  * 字面量 `set this to true or false`)。
159
+ *
160
+ * 任何形态的既有 `allowBuilds` 都会被**整体替换**成规范化块:块写法、行内数组
161
+ * `allowBuilds: ["a@1"]`、以及末尾无换行的 `allowBuilds:` 都算命中。否则会在文件里
162
+ * 再多写一个同名键 —— pnpm 解析时直接报 `duplicated mapping key`,该 profile 里
163
+ * 所有插件都装不上(#562)。
159
164
  */
160
165
  export function addAllowBuildsKey(profile, key) {
161
166
  const file = join(profileDirectory(profile), 'pnpm-workspace.yaml');
@@ -165,10 +170,12 @@ export function addAllowBuildsKey(profile, key) {
165
170
  }
166
171
  catch { /* 文件不存在,下方创建 */ }
167
172
  const eol = /\r\n/.test(yaml) ? '\r\n' : '\n';
168
- const blockRe = /allowBuilds:[ \t]*\r?\n((?:[ \t]+[^\r\n]*\r?\n?)*)/g;
173
+ // header 行(可选行内值)+ 其后任意缩进条目行;行内写法没有后续缩进行也能匹配
174
+ const blockRe = /^[ \t]*allowBuilds:[^\r\n]*(?:(?:\r?\n)[ \t]{2,}[^\r\n]*)*/gm;
169
175
  const map = new Map();
170
176
  for (const match of Array.from(yaml.matchAll(blockRe))) {
171
- for (const line of match[1].split(/\r?\n/)) {
177
+ const [header, ...rest] = match[0].split(/\r?\n/);
178
+ for (const line of rest) {
172
179
  const m = /^[ \t]+(\S.*?)\s*:\s*(true|false)?\s*$/.exec(line);
173
180
  if (m === null || m[1] === '')
174
181
  continue;
@@ -178,17 +185,22 @@ export function addAllowBuildsKey(profile, key) {
178
185
  }
179
186
  map.set(k, m[2] ?? 'true');
180
187
  }
188
+ // 行内数组写法:allowBuilds: ["a@1.0.0", 'b@2'] —— 保留已有条目,别丢
189
+ const inline = /^[ \t]*allowBuilds:\s*\[(.*)\]\s*$/.exec(header);
190
+ if (inline !== null) {
191
+ for (const raw of inline[1].split(',')) {
192
+ const k = raw.trim().replace(/^['"]|['"]$/g, '');
193
+ if (k !== '')
194
+ map.set(k, 'true');
195
+ }
196
+ }
181
197
  }
182
198
  map.set(key, 'true');
183
199
  const block = Array.from(map).map(([k, v]) => ` ${quoteYamlKey(k)}: ${v}`).join(eol);
184
200
  const blockText = `allowBuilds:${eol}${block}${eol}`;
185
- let next;
186
- if (/allowBuilds:[ \t]*\r?\n/.test(yaml)) {
187
- let seen = 0;
188
- next = yaml.replace(blockRe, () => (seen++ === 0 ? blockText : ''));
189
- }
190
- else {
201
+ let seen = 0;
202
+ let next = yaml.replace(blockRe, () => (seen++ === 0 ? blockText : ''));
203
+ if (seen === 0)
191
204
  next = `${yaml.replace(/\r?\n?$/, eol)}${blockText}`;
192
- }
193
205
  writeFileSync(file, next);
194
206
  }
@@ -0,0 +1,16 @@
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 解析失败 → 返回 ASCII 标记行;否则 null。
14
+ * 纯 ASCII + 不提插件:前端按 `[profile-yaml]` 归类,Windows GBK 解码下同样能命中。
15
+ */
16
+ export declare function profileYamlMarker(output: string): string | null;
@@ -36,5 +36,10 @@ export declare function parseAllowBuildsKey(output: string): string | null;
36
36
  * 向 profile 的 pnpm-workspace.yaml 追加一条 `allowBuilds` 记录:与已有条目合并,
37
37
  * 其余 yaml 保持不变;重写时丢弃损坏的占位条目(pnpm 安装失败的 bug 会写入
38
38
  * 字面量 `set this to true or false`)。
39
+ *
40
+ * 任何形态的既有 `allowBuilds` 都会被**整体替换**成规范化块:块写法、行内数组
41
+ * `allowBuilds: ["a@1"]`、以及末尾无换行的 `allowBuilds:` 都算命中。否则会在文件里
42
+ * 再多写一个同名键 —— pnpm 解析时直接报 `duplicated mapping key`,该 profile 里
43
+ * 所有插件都装不上(#562)。
39
44
  */
40
45
  export declare function addAllowBuildsKey(profile: string, key: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
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",
@@ -327,6 +327,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
327
327
  // 发现补丁没被用上就中止安装,任何插件都装不进来 —— 不引导提 Issue
328
328
  return h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
329
329
  }
330
+ if (kind === 'profileYamlCorrupted') {
331
+ // profile 里的 YAML 配置解析不了(pnpm-workspace.yaml / pnpm-lock.yaml 有重复键、
332
+ // 缩进坏了):pnpm 在依赖解析之前就退出,该 profile 内的安装都会失败 ——
333
+ // 给修复/重建指引,不引导提 Issue
334
+ return h('div', { className: styles.failPrepareHint }, t('failProfileYamlHint'))
335
+ }
330
336
  if (kind === 'profileState') {
331
337
  // profile 的 pnpm 元数据与本机不一致(lockfile 过期 / packageManager 版本不符 /
332
338
  // 补丁未生效):pnpm 在真正装插件之前就中止,该 profile 内的安装都会失败 ——
@@ -462,6 +462,10 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
462
462
  // profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm 发现
463
463
  // 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue
464
464
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
465
+ : failureKind === 'profileYamlCorrupted'
466
+ // profile 里的 YAML 配置解析不了(pnpm-workspace.yaml / pnpm-lock.yaml 有重复键、缩进坏了):
467
+ // pnpm 在依赖解析之前就退出,该 profile 内的安装都会失败 —— 给修复/重建指引,不引导提 Issue
468
+ ? h('div', { className: styles.failPrepareHint }, t('failProfileYamlHint'))
465
469
  : failureKind === 'profileState'
466
470
  // profile 的 pnpm 元数据与本机不一致(lockfile 过期 / packageManager 版本不符 /
467
471
  // 补丁未生效):pnpm 在真正装插件之前就中止,该 profile 内的安装都会失败 ——
@@ -353,6 +353,7 @@ export const zh = {
353
353
  failPnpmStoreHint: '安装器调用 pnpm 时报「store 位置不匹配」(ERR_PNPM_UNEXPECTED_STORE):你的 profile 目录里旧插件依赖是用另一个大版本的 pnpm 生成的,当前 pnpm 出于安全不认旧目录,导致任何插件装进该 profile 都会失败。请删除该 profile 目录下的 node_modules 与 pnpm-lock.yaml(可先到「系统日志」或 ~/.dsh/profiles/<profile>/hub.log 查看完整报错),再重新安装。这不是插件本身的问题。',
354
354
  failPnpmUnusedPatchHint: '安装器调用 pnpm 时报「有些补丁没有被使用」(ERR_PNPM_UNUSED_PATCH):你的 profile 里留着一条指向旧版本 dsh-plugin 的补丁声明(例如 dsh-plugin@1.4.2),而本次安装解析到的版本已经不是它,pnpm 出于安全直接中止整次安装,因此任何插件都装不进来。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)的 pnpm-workspace.yaml(pnpm 11 及以上)或 package.json 的 pnpm.patchedDependencies(pnpm 10)里删除该 dsh-plugin 补丁条目后重试。',
355
355
  failProfileStateHint: '安装器调用 pnpm 时报 profile 的元数据与当前环境对不上(如 ERR_PNPM_OUTDATED_LOCKFILE / ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND / ERR_PNPM_BAD_PM_VERSION / ERR_PNPM_PATCH_NOT_APPLIED):profile 里的 pnpm-lock.yaml、package.json 或补丁声明与当前 pnpm 版本不一致,pnpm 在真正装插件之前就中止了,所以这个 profile 里装任何插件都会失败。这不是插件本身的问题——请在 profile 目录(~/.dsh/profiles/<profile>)里删除 pnpm-lock.yaml 后重试(pnpm 会按当前依赖重新生成);若报的是 packageManager 版本不符,把 package.json 里的 packageManager 字段改成当前 pnpm 的大版本或直接删掉该字段;仍不行请重建该 profile 的依赖。',
356
+ failProfileYamlHint: '安装器调用 pnpm 时报「YAML 配置损坏」(duplicated mapping key):profile 目录里的 pnpm-workspace.yaml 或 pnpm-lock.yaml 出现了重复的键(多见于手工编辑、合并冲突残留,或第三方工具重复写入同一条目)。pnpm 在解析阶段就直接退出,依赖根本没开始装,所以这个 profile 里装任何插件都会失败。这不是插件本身的问题——请打开 ~/.dsh/profiles/<profile>/ 检查这两个文件,删掉重复的那一行或那一段;不确定该改哪一处时,可以先删掉 pnpm-lock.yaml(必要时连 node_modules 一起)让 pnpm 重新生成,然后重试。',
356
357
  failNodeEngineHint: '这个插件(或它的某个依赖)声明了更高的 Node / pnpm 版本要求(ERR_PNPM_UNSUPPORTED_ENGINE / EBADENGINE),当前版本不满足,pnpm 在安装阶段直接拒绝。这不是插件本身的问题——请把 Node 升级到要求的版本(nvm / fnm 切换,或安装新版 Node.js),确认 node -v 与 pnpm -v 都满足要求后重试。',
357
358
  // 404 依赖:点名 registry 上不存在的那个包(取不到包名时跳过这行),再给删除条目的指引
358
359
  failPnpmMissingDepTarget: '{pkg} 在 npm registry 上不存在(404)',
@@ -389,6 +390,8 @@ export const zh = {
389
390
  failReason_pnpmPolicy: '版本未满最小发布年龄,或缺完整性校验 / 来源不可信。',
390
391
  failKind_pnpmUnusedPatch: 'profile 残留旧补丁声明',
391
392
  failReason_pnpmUnusedPatch: 'profile 里留着指向旧版本 dsh-plugin 的补丁,pnpm 出于安全中止了安装。',
393
+ failKind_profileYamlCorrupted: 'profile 的 YAML 配置损坏',
394
+ failReason_profileYamlCorrupted: 'profile 目录里的 pnpm-workspace.yaml 或 pnpm-lock.yaml 有重复的键,pnpm 在解析阶段就退出了。',
392
395
  failKind_profileState: 'profile 的 pnpm 元数据与本机不一致',
393
396
  failReason_profileState: 'profile 的 lockfile / package.json / packageManager 版本与当前 pnpm 对不上,安装没开始就被中止。',
394
397
  failKind_nodeEngine: '本机 Node / pnpm 版本不满足要求',
@@ -852,6 +855,7 @@ export const en = {
852
855
  failPnpmStoreHint: 'pnpm reported an unexpected store location (ERR_PNPM_UNEXPECTED_STORE): the profile directory was previously set up with a different major version of pnpm, and the current pnpm refuses to reuse it — no plugin can be installed into that profile. Delete node_modules and pnpm-lock.yaml under the profile directory (see System Logs or ~/.dsh/profiles/<profile>/hub.log for the full output), then reinstall. This is not a problem with the plugin itself.',
853
856
  failPnpmUnusedPatchHint: 'pnpm reported that some patches were not used (ERR_PNPM_UNUSED_PATCH): your profile still holds a patch entry for an older dsh-plugin version (for example dsh-plugin@1.4.2), while this install resolved a different version, so pnpm aborts the whole install for safety — no plugin can be installed into that profile. This is not a problem with the plugin itself. Remove that dsh-plugin patch entry from pnpm-workspace.yaml (pnpm 11+) or from pnpm.patchedDependencies in package.json (pnpm 10) inside the profile directory (~/.dsh/profiles/<profile>), then retry.',
854
857
  failProfileStateHint: 'pnpm reported that the profile metadata does not match the current environment (e.g. ERR_PNPM_OUTDATED_LOCKFILE / ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND / ERR_PNPM_BAD_PM_VERSION / ERR_PNPM_PATCH_NOT_APPLIED): pnpm-lock.yaml, package.json or the patch declarations inside the profile disagree with the pnpm version in use, so pnpm aborts before installing anything — every plugin install into this profile will fail. This is not a problem with the plugin itself. Delete pnpm-lock.yaml inside the profile directory (~/.dsh/profiles/<profile>) and retry (pnpm regenerates it from the declared dependencies); if the error is about packageManager, set that field in package.json to the current pnpm major or remove it; if it still fails, rebuild the profile dependencies.',
858
+ failProfileYamlHint: 'pnpm reported a broken YAML config (duplicated mapping key): a YAML file inside the profile directory — pnpm-workspace.yaml or pnpm-lock.yaml — contains a duplicate key, usually after a manual edit, a leftover merge conflict, or a tool writing the same entry twice. pnpm stops at parse time and nothing is installed, so every plugin install into this profile fails the same way. This is not a problem with the plugin itself. Open ~/.dsh/profiles/<profile>/ and remove the duplicated line or block; if you are not sure which file to fix, delete pnpm-lock.yaml (and node_modules if needed) so pnpm regenerates them, then retry.',
855
859
  failNodeEngineHint: 'This plugin (or one of its dependencies) declares a higher Node / pnpm version (ERR_PNPM_UNSUPPORTED_ENGINE / EBADENGINE), which the current one does not satisfy, so pnpm refuses at install time. This is not a problem with the plugin itself. Upgrade Node to the required version (switch with nvm / fnm, or install a newer Node.js), confirm node -v and pnpm -v both satisfy it, then retry.',
856
860
  // 缺失依赖:点名 404 的包(解析不出时跳过该行),再给删除指引
857
861
  failPnpmMissingDepTarget: '{pkg} was not found on the npm registry (404)',
@@ -888,6 +892,8 @@ export const en = {
888
892
  failReason_pnpmPolicy: 'The version is younger than the minimum release age, or an integrity / trust check failed.',
889
893
  failKind_pnpmUnusedPatch: 'stale patch entry in the profile',
890
894
  failReason_pnpmUnusedPatch: 'The profile keeps a patch for an older dsh-plugin version, so pnpm aborted for safety.',
895
+ failKind_profileYamlCorrupted: 'profile YAML config is corrupted',
896
+ failReason_profileYamlCorrupted: 'A YAML file in the profile directory has a duplicate key, so pnpm aborts at parse time.',
891
897
  failKind_profileState: 'profile pnpm metadata does not match this machine',
892
898
  failReason_profileState: 'The profile lockfile / package.json / packageManager version disagrees with the current pnpm, so the install aborts before starting.',
893
899
  failKind_nodeEngine: 'Node / pnpm on this machine is below the required version',
@@ -148,7 +148,7 @@ export function removeNotification(id: number): NotificationRecord[] {
148
148
  return next
149
149
  }
150
150
 
151
- export type FailureKind = 'npmTooOld' | 'nodeEngine' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmModulesMismatch' | 'profileState' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'registryAuth' | 'profileExclusive' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'buildToolchainMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'tlsTrust' | 'network' | 'hostCliSpawn' | 'hubState' | 'profileBundleMissing' | 'timeout' | 'profileLock' | 'dshTooOld' | 'localEnv' | 'hub'
151
+ export type FailureKind = 'npmTooOld' | 'nodeEngine' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmModulesMismatch' | 'profileYamlCorrupted' | 'profileState' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'registryAuth' | 'profileExclusive' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'buildToolchainMissing' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'hostIncompatible' | 'tlsTrust' | 'network' | 'hostCliSpawn' | 'hubState' | 'profileBundleMissing' | 'timeout' | 'profileLock' | 'dshTooOld' | 'localEnv' | 'hub'
152
152
 
153
153
  /** 失败动作口径:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
154
154
  export type FailureAction = 'install' | 'update' | 'uninstall'
@@ -254,6 +254,10 @@ export function classifyFailure(message: string): FailureKind {
254
254
  if (missingTarballIntegrity(message) || mismatchedTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge|untrusted origin/i.test(message)) return 'pnpmPolicy'
255
255
  // profile 残留旧版本 patch 声明,pnpm 中止安装。必须在兜底之前
256
256
  if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
257
+ // profile 里的 YAML 配置解析失败(pnpm-workspace.yaml / pnpm-lock.yaml 有重复键、缩进坏了):
258
+ // pnpm 在依赖解析之前就退出,装任何插件都会同样失败,属本机 profile 配置问题。
259
+ // 必须在 profileState / pluginPrepare 之前 —— 日志尾部常带 `Command failed: … pnpm.mjs add …`
260
+ if (/\[profile-yaml\]|duplicated mapping key|bad indentation of a (?:mapping|sequence) entry/i.test(message)) return 'profileYamlCorrupted'
257
261
  // profile 的 pnpm 元数据与本机不一致(lockfile 过期 / 找不到可安装的 manifest / packageManager
258
262
  // 版本不符 / 补丁未生效 / workspace 依赖缺失):都是 profile 自身状态,这个 profile 里装任何
259
263
  // 插件都会同样失败。必须在 pluginPrepare 之前 —— 这类错误常被 pnpm 包一层 Command failed
@@ -356,7 +360,7 @@ const PEER_WARN_RE = /missing peer|issues with peer dependencies|peer dependenci
356
360
  /** 安装器自己打的诊断标记行:它们本身就是「为什么没有日志 / 卡在哪一步」的证据,
357
361
  * 不含 CORE_LINE_RE 的关键词,必须无条件保留 —— 否则上报正文会退回成只剩宿主那句摘要,
358
362
  * 我们又拿不到任何可排查的信息(#382/#393 就是这样)。 */
359
- const MARKER_LINE_RE = /^\[(?:pnpm log|no-output|desktop-cli-fallback|dsh-too-old|host-spawn|npm-too-low|git-missing|npm-missing|pnpm-missing|dsh-missing|network|packaging|registry-missing|registry-mirror-lag|registry-fallback|allow build|stale pipe|install-channel|internal|exit\b|timed out)/i
363
+ const MARKER_LINE_RE = /^\[(?:pnpm log|no-output|desktop-cli-fallback|dsh-too-old|host-spawn|npm-too-low|git-missing|npm-missing|pnpm-missing|dsh-missing|network|packaging|profile-yaml|registry-missing|registry-mirror-lag|registry-fallback|allow build|stale pipe|install-channel|internal|exit\b|timed out)/i
360
364
  /** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
361
365
  * 固定模板与 URL 编码开销约 1~2K,核心错误(以 ASCII 日志为主)可安全带到 ~5K;
362
366
  * 仍超长时 pluginIssueUrl 会逐档缩小核心预算,最终 URL 不会超限。 */
@@ -48,6 +48,7 @@ function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
48
48
  case 'npmTooOld': return `npm too old for this ${word}`
49
49
  case 'nodeEngine': return `Node / pnpm on the user machine is below the version this ${word} requires`
50
50
  case 'profileState': return `the user profile's pnpm metadata does not match the local pnpm for this ${word}`
51
+ case 'profileYamlCorrupted': return 'the user profile has a corrupted YAML config (duplicated mapping key)'
51
52
  case 'dshMissing': return 'dsh command not found on the user machine'
52
53
  case 'pnpmMissing': return 'pnpm command not found on the user machine'
53
54
  case 'pnpmStore': return 'pnpm store version mismatch on the user machine'
@@ -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
@@ -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
  }