@xrxs-ai/dsh-xrxs-plugins-store 0.2.0 → 0.2.1

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.
@@ -1,6 +1,6 @@
1
1
  import type { ApiClient } from './api-client.ts';
2
2
  import { ApiError } from './api-client.ts';
3
- import type { HostInfo } from './host-info.ts';
3
+ import type { HostInfo, HostKind } from './host-info.ts';
4
4
  /**
5
5
  * dsh CLI 发现 + 安装执行链路(设计文档 §3.1 / §3.2 / §3.4):
6
6
  * - 执行通道按优先级:PATH/常见位置里的 dsh shim(**先探测再用**)→ RunAsNode 兜底 → 降级「复制命令」;
@@ -48,8 +48,9 @@ export interface DshCliResolution {
48
48
  /** CLI 输出里的「本机不可用」信号 → 中文原因;不是这类信号返回 null。 */
49
49
  export declare function describeUnavailableCliOutput(output: string): string | null;
50
50
  /** 探测 + 解析可用通道(异步:会真实执行 shim 的 --version,只在安装/卸载路径调用)。
51
+ * `kind` 决定 RunAsNode 兜底通道优先哪个桌面应用(见 DESKTOP_APP_NAMES_BY_KIND);
51
52
  * `appRoots` 是测试注入点(覆盖桌面应用候选根目录),生产调用不传。 */
52
- export declare function resolveDshCli(env?: NodeJS.ProcessEnv, execPath?: string, appRoots?: readonly string[]): Promise<DshCliResolution>;
53
+ export declare function resolveDshCli(env?: NodeJS.ProcessEnv, execPath?: string, appRoots?: readonly string[], kind?: HostKind): Promise<DshCliResolution>;
53
54
  /** 只要通道(兼容旧调用形态)。 */
54
55
  export declare function discoverDshCli(env?: NodeJS.ProcessEnv, execPath?: string): Promise<DshCli | null>;
55
56
  /** 「两个通道都不可用」的中文说明(讲清哪一层不可用,不回显 CLI 英文原文)。 */
@@ -61,7 +62,9 @@ export type KnownInstallErrorKind = 'minimum-release-age' | 'version-incompatibl
61
62
  /** 市场记录的版本在 registry 上不存在(ERR_PNPM_NO_MATCHING_VERSION)。 */
62
63
  | 'version-not-published'
63
64
  /** 整个包在 registry 上不存在(404)。 */
64
- | 'package-not-found';
65
+ | 'package-not-found'
66
+ /** 依赖的安装脚本被 pnpm 10+ 默认拦截(ERR_PNPM_IGNORED_BUILDS),需用户批准构建。 */
67
+ | 'build-scripts-ignored';
65
68
  export interface KnownInstallError {
66
69
  kind: KnownInstallErrorKind;
67
70
  hint: string;
@@ -16,8 +16,22 @@ const CLI_UNAVAILABLE_PATTERNS = [
16
16
  /Node\.js runtime not found/i,
17
17
  /Please run DeepSeek Harness Desktop/i,
18
18
  ];
19
- /** 桌面应用的候选名(macOS 为 <名>.app;Windows/Linux 为安装目录名)。 */
20
- const DESKTOP_APP_NAMES = ['DSH Desktop', 'DeepSeek Harness Desktop'];
19
+ /** 各宿主对应的桌面应用名(按优先序)。RunAsNode 兜底通道从应用内置的 desktop-cli.js 跑,
20
+ * 而 CLI 的 peer 校验按**其所在应用**内置的 dsh 版本来 —— 必须优先用与当前宿主同源的应用,
21
+ * 否则会出现「插件装在薪灵(~/.xsh)、却借 DSH Desktop 的旧运行时校验」的错位拒绝
22
+ * (实测:薪灵 0.2.1-rc.2 的机器上按 DSH Desktop 0.2.0-rc.2 拒装)。 */
23
+ const DESKTOP_APP_NAMES_BY_KIND = {
24
+ cinlyn: ['薪灵'],
25
+ dsh: ['DSH Desktop', 'DeepSeek Harness Desktop'],
26
+ };
27
+ /** 全部应用候选:当前 kind 的在前,其余 kind 的追加在后仅作兜底(跨源应用的 dsh 版本可能错位,见上)。 */
28
+ function desktopAppNames(kind) {
29
+ const primary = DESKTOP_APP_NAMES_BY_KIND[kind];
30
+ const rest = Object.values(DESKTOP_APP_NAMES_BY_KIND)
31
+ .flat()
32
+ .filter((name) => !primary.includes(name));
33
+ return [...primary, ...rest];
34
+ }
21
35
  function shimNames(platform) {
22
36
  return platform === 'win32' ? ['dsh.cmd', 'dsh.exe', 'dsh'] : ['dsh'];
23
37
  }
@@ -107,7 +121,7 @@ function desktopDirCandidate(appRoot, appName, platform) {
107
121
  return isRunnableFile(executable, platform) ? { executable, cliJs } : null;
108
122
  }
109
123
  /** RunAsNode 候选 2:按 OS 常见安装位置兜底。 */
110
- function runAsNodeFromKnownLocations(platform, env, home, overrideRoots) {
124
+ function runAsNodeFromKnownLocations(platform, env, home, appNames, overrideRoots) {
111
125
  const roots = overrideRoots ??
112
126
  (platform === 'darwin'
113
127
  ? ['/Applications', join(home, 'Applications')]
@@ -115,12 +129,12 @@ function runAsNodeFromKnownLocations(platform, env, home, overrideRoots) {
115
129
  ? [join(env.LOCALAPPDATA ?? join(home, 'AppData', 'Local'), 'Programs')]
116
130
  : ['/opt', join(home, '.local', 'share')]);
117
131
  for (const root of roots) {
118
- for (const appName of DESKTOP_APP_NAMES) {
132
+ for (const appName of appNames) {
119
133
  const candidate = platform === 'darwin'
120
134
  ? macAppCandidate(join(root, `${appName}.app`), appName, platform)
121
135
  : desktopDirCandidate(join(root, appName), appName, platform);
122
136
  if (candidate !== null)
123
- return candidate;
137
+ return { ...candidate, appName };
124
138
  }
125
139
  }
126
140
  return null;
@@ -129,11 +143,16 @@ function runAsNodeFromKnownLocations(platform, env, home, overrideRoots) {
129
143
  * 运行时优先 `DSH_NODE`(应用注入给子进程的自家 node;本机实测应用主进程环境里没有该变量,
130
144
  * 所以实际走「应用可执行文件 + ELECTRON_RUN_AS_NODE=1」),两者都要求落地校验。
131
145
  * 全部失败 → null,由调用方降级为「复制命令」。 */
132
- function findRunAsNode(platform, env, home, execPath, notes, appRoots) {
146
+ function findRunAsNode(platform, env, home, execPath, notes, kind, appRoots) {
133
147
  const candidate = runAsNodeFromCurrentProcess(execPath, platform) ??
134
- runAsNodeFromKnownLocations(platform, env, home, appRoots);
148
+ runAsNodeFromKnownLocations(platform, env, home, desktopAppNames(kind), appRoots);
135
149
  if (candidate === null)
136
150
  return null;
151
+ // 跨宿主兜底:命中的不是当前 kind 对应的应用,peer 校验会按那个应用的 dsh 版本来,提前写进日志
152
+ const primary = DESKTOP_APP_NAMES_BY_KIND[kind];
153
+ if (candidate.appName !== undefined && !primary.includes(candidate.appName)) {
154
+ notes.push(`本机没找到与当前宿主(${kind === 'cinlyn' ? '薪灵' : 'DSH Desktop'})同源的桌面应用,安装命令借用了「${candidate.appName}」内的 dsh CLI —— 它内置的 dsh 版本可能与当前运行时不一致,peer 校验可能因此误拒`);
155
+ }
137
156
  const dshNode = env.DSH_NODE?.trim();
138
157
  if (dshNode !== undefined && dshNode !== '') {
139
158
  if (isRunnableFile(dshNode, platform)) {
@@ -171,8 +190,9 @@ export function describeUnavailableCliOutput(output) {
171
190
  return null;
172
191
  }
173
192
  /** 探测 + 解析可用通道(异步:会真实执行 shim 的 --version,只在安装/卸载路径调用)。
193
+ * `kind` 决定 RunAsNode 兜底通道优先哪个桌面应用(见 DESKTOP_APP_NAMES_BY_KIND);
174
194
  * `appRoots` 是测试注入点(覆盖桌面应用候选根目录),生产调用不传。 */
175
- export async function resolveDshCli(env = process.env, execPath = process.execPath, appRoots) {
195
+ export async function resolveDshCli(env = process.env, execPath = process.execPath, appRoots, kind = 'dsh') {
176
196
  const platform = process.platform;
177
197
  const home = env.HOME ?? env.USERPROFILE ?? homedir();
178
198
  const rejected = [];
@@ -187,7 +207,7 @@ export async function resolveDshCli(env = process.env, execPath = process.execPa
187
207
  return { cli: { kind: 'shim', path: shimPath }, rejected, notes };
188
208
  rejected.push({ kind: 'shim', path: shimPath, reason: probe.reason });
189
209
  }
190
- const runAsNode = findRunAsNode(platform, env, home, execPath, notes, appRoots);
210
+ const runAsNode = findRunAsNode(platform, env, home, execPath, notes, kind, appRoots);
191
211
  return { cli: runAsNode, rejected, notes };
192
212
  }
193
213
  /** 只要通道(兼容旧调用形态)。 */
@@ -255,6 +275,21 @@ export function buildPluginRemoveCommand(cli, packageName, host, withProfile = t
255
275
  }
256
276
  /** pnpm 的 NO_MATCHING_VERSION 文案里带可用最新版本:`The latest release of <name> is "0.4.11".` */
257
277
  const LATEST_RELEASE_PATTERN = /The latest release of\s+([^\s]+)\s+is\s+"?([0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+-]*)"?/i;
278
+ /** 从 pnpm 的「Ignored build scripts: @scope/pkg@1.2.3, other@4.5.6」行提取包名(剥掉版本号)。 */
279
+ function parseIgnoredBuildPackages(output) {
280
+ const match = /Ignored build scripts:\s*(.+)/.exec(output);
281
+ if (match === null)
282
+ return [];
283
+ return (match[1] ?? '')
284
+ .split(',')
285
+ .map((item) => item.trim())
286
+ .filter((item) => item !== '')
287
+ .map((item) => {
288
+ const at = item.lastIndexOf('@');
289
+ // scoped 名(@scope/pkg@1.2.3)与非 scoped(pkg@1.2.3)都剥最后一段 @版本;无版本原样保留
290
+ return at > 0 ? item.slice(0, at) : item;
291
+ });
292
+ }
258
293
  /** 从 CLI 输出里解析 registry 上实际存在的最新版本;解析不到返回 null。 */
259
294
  export function parseLatestPublishedVersion(output) {
260
295
  const matched = LATEST_RELEASE_PATTERN.exec(output);
@@ -279,6 +314,22 @@ export function parseKnownInstallError(stderr, packageName, version, profile) {
279
314
  ].join('\n'),
280
315
  };
281
316
  }
317
+ // pnpm 10+ 默认不执行依赖的安装脚本(供应链防护):插件依赖带 postinstall 的包会触发。
318
+ // 这不是包坏了,是「需要用户显式批准」—— 给出批准入口,不替用户做供应链决定。
319
+ if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts/i.test(stderr)) {
320
+ const packages = parseIgnoredBuildPackages(stderr);
321
+ return {
322
+ kind: 'build-scripts-ignored',
323
+ hint: [
324
+ 'pnpm 10+ 默认不执行依赖包的安装脚本(供应链防护),该插件依赖的下列包需要构建批准:',
325
+ packages.length > 0 ? ` ${packages.join(' , ')}` : '',
326
+ '请在 DSH 终端执行:cd <你的 profile 目录> && pnpm approve-builds,勾选上述包后重新安装。',
327
+ '批准 = 允许这些包在本机运行安装脚本,请自行确认后再批(一次批准长期有效)。',
328
+ ]
329
+ .filter((line) => line !== '')
330
+ .join('\n'),
331
+ };
332
+ }
282
333
  if (/ERR_PNPM_NO_MATCHING_VERSION|No matching version found/i.test(stderr)) {
283
334
  const latest = parseLatestPublishedVersion(stderr);
284
335
  const recorded = version !== undefined && version !== '' ? version : '未知';
@@ -419,7 +470,7 @@ export async function installPlugin(request, deps) {
419
470
  const resolved = request.sourceType === 'npm'
420
471
  ? { spec: request.version !== '' ? `${request.name}@${request.version}` : request.name, version: request.version }
421
472
  : await downloadTgzSpec(request, deps);
422
- const resolution = await resolveDshCli(deps.env ?? process.env, deps.execPath ?? process.execPath, deps.appRoots);
473
+ const resolution = await resolveDshCli(deps.env ?? process.env, deps.execPath ?? process.execPath, deps.appRoots, host.kind);
423
474
  logCliResolution(deps.log, resolution);
424
475
  if (resolution.cli === null) {
425
476
  // 两个通道都不可用才降级;降级原因进日志(契约 §1.3:降级返回 200 + 可复制命令)
@@ -447,7 +498,7 @@ export async function installPlugin(request, deps) {
447
498
  export async function uninstallPlugin(packageName, deps) {
448
499
  assertRemovablePackageName(packageName); // 宽松守卫(见函数注释:卸载不套 npm 严格正则,避免挡到 legacy 包)
449
500
  const host = deps.getHostInfo();
450
- const resolution = await resolveDshCli(deps.env ?? process.env, deps.execPath ?? process.execPath, deps.appRoots);
501
+ const resolution = await resolveDshCli(deps.env ?? process.env, deps.execPath ?? process.execPath, deps.appRoots, host.kind);
451
502
  logCliResolution(deps.log, resolution);
452
503
  if (resolution.cli === null) {
453
504
  deps.log.warn?.(`xrxs-plugins-store: ${describeUnavailableCli(resolution)},卸载降级为可复制命令`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrxs-ai/dsh-xrxs-plugins-store",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "DSH 私有插件市场插件(静态持久 bundle,双 face)",
5
5
  "type": "module",
6
6
  "license": "MIT",