@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.
- package/lib/host/installer.d.ts +6 -3
- package/lib/host/installer.js +62 -11
- package/package.json +1 -1
package/lib/host/installer.d.ts
CHANGED
|
@@ -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;
|
package/lib/host/installer.js
CHANGED
|
@@ -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
|
-
/**
|
|
20
|
-
|
|
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
|
|
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)},卸载降级为可复制命令`);
|