dsh-plugin 1.1.1 → 1.2.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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +4772 -1659
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +451 -28
- package/lib/index.js +5 -1
- package/lib/services/{install-types.js → install/install-types.js} +8 -2
- package/lib/services/install/install.js +4 -0
- package/lib/services/install/npm-check.js +55 -0
- package/lib/services/{npm-resolve.js → install/npm-resolve.js} +11 -5
- package/lib/services/{preflight.js → install/preflight.js} +23 -7
- package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
- package/lib/services/loader.js +60 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
- package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
- package/lib/services/{profile.js → profile/profile.js} +59 -4
- package/lib/services/{progress.js → profile/progress.js} +4 -0
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +5 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
- package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -5
- package/lib/types/services/install/npm-check.d.ts +12 -0
- package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
- package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
- package/lib/types/services/loader.d.ts +14 -4
- package/lib/types/services/log.d.ts +49 -0
- package/lib/types/services/probe.d.ts +30 -0
- package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +9 -3
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
- package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +346 -121
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -5
- package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
- package/src/client/components/modals/LogsModal.tsx +301 -0
- package/src/client/components/modals/LogsPathModal.tsx +161 -0
- package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +51 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -6
- package/src/client/components/ui/Toggle.tsx +28 -0
- package/src/client/components/ui/icons.tsx +343 -0
- package/src/client/components/views/CustomInstallView.tsx +250 -0
- package/src/client/components/views/DiagnosticsView.tsx +172 -0
- package/src/client/components/views/InstalledView.tsx +328 -0
- package/src/client/components/views/LogsView.tsx +150 -0
- package/src/client/components/views/MarketView.tsx +66 -0
- package/src/client/components/views/SettingsView.tsx +273 -0
- package/src/client/data/catalog.ts +38 -0
- package/src/client/data/host.ts +107 -0
- package/src/client/data/hub.ts +41 -0
- package/src/client/hooks/useCatalog.ts +77 -117
- package/src/client/hooks/useIncrementalList.ts +46 -0
- package/src/client/hooks/useLanguage.ts +40 -0
- package/src/client/hooks/useSettings.ts +90 -0
- package/src/client/hooks/useTaskQueue.ts +168 -47
- package/src/client/index.tsx +4 -0
- package/src/client/locales.ts +365 -39
- package/src/client/logic/constants.ts +87 -0
- package/src/client/{lib → logic}/failures.ts +99 -74
- package/src/client/{lib → logic}/format.ts +4 -0
- package/src/client/logic/install-command.ts +56 -0
- package/src/client/logic/installed.ts +207 -0
- package/src/client/logic/normalize.ts +52 -0
- package/src/client/{lib → logic}/renderMarkdown.ts +4 -0
- package/src/client/logic/urls.ts +125 -0
- package/src/client/styles/CustomInstallView.module.css +191 -0
- package/src/client/styles/DiagnosticsView.module.css +231 -0
- package/src/client/styles/Dropdown.module.css +46 -41
- package/src/client/styles/Header.module.css +105 -133
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +11 -1
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +357 -71
- package/src/client/styles/SectionTabs.module.css +142 -0
- package/src/client/styles/SettingsView.module.css +232 -0
- package/src/client/styles/Toggle.module.css +60 -0
- package/src/client/styles/tokens.module.css +6 -1
- package/src/client/types.ts +28 -4
- package/src/server/http/routes.ts +426 -28
- package/src/server/index.ts +5 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
- package/src/server/services/{install.ts → install/install.ts} +9 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
- package/src/server/services/loader.ts +59 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
- package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
- package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -104
- package/src/client/components/icons.tsx +0 -125
- package/src/client/lib/catalog.ts +0 -272
- package/src/client/lib/env.ts +0 -16
- /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
- /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* Install runner 的统一出口:执行真实插件变更(spawn 官方 dsh CLI 子进程)。
|
|
3
7
|
* 实现按职责拆分在同目录子模块,本文件只做公共 API 的 re-export,保持外部
|
|
4
|
-
* import 面稳定(routes.ts / index.ts 仍从 `services/install.ts` 引入)。
|
|
8
|
+
* import 面稳定(routes.ts / index.ts 仍从 `services/install/install.ts` 引入)。
|
|
5
9
|
*
|
|
6
10
|
* 模块划分:
|
|
7
11
|
* - install-types.ts 公共类型与常量
|
|
@@ -11,8 +15,8 @@
|
|
|
11
15
|
* - task-queue.ts 任务注册表 + FIFO 队列 worker + 子进程生命周期
|
|
12
16
|
*/
|
|
13
17
|
export type { InstallResult, InstallTask, Invocation, PendingRestart, QueueItem } from './install-types.ts'
|
|
14
|
-
export type { LoaderHandle } from '
|
|
15
|
-
export { addPendingRestart, clearPendingRestart, listPendingRestarts } from '
|
|
16
|
-
export { dumpLoaderEntries, removeLoadedEntry } from '
|
|
17
|
-
export { addAllowBuildsKey, githubTarget, parseAllowBuildsKey, readProfileArg, validPackageName } from '
|
|
18
|
+
export type { LoaderHandle } from '../loader.ts'
|
|
19
|
+
export { addPendingRestart, clearPendingRestart, listPendingRestarts } from '../profile/pending-restart.ts'
|
|
20
|
+
export { dumpLoaderEntries, removeLoadedEntry } from '../loader.ts'
|
|
21
|
+
export { addAllowBuildsKey, githubRepoOf, githubTarget, globalNpmPackagesOf, installTargetOf, parseAllowBuildsKey, readProfileArg, validPackageName } from '../profile/profile.ts'
|
|
18
22
|
export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.ts'
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
|
|
7
|
+
*
|
|
8
|
+
* 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时
|
|
9
|
+
* 有已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`(build-ideal-tree.js
|
|
10
|
+
* 的 #loadPeerSet),官方标记 Cannot Reproduce(npm/cli#8261、#9787),npm 11.6.0 起不再复现。
|
|
11
|
+
* 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,若按插件打包问题引导提 Issue 就误导了用户。
|
|
12
|
+
*
|
|
13
|
+
* 这里的 `npmTooLowMarker` 是纯函数(输出文本 + 环境 → 标记行或 null),不触碰任务/队列,
|
|
14
|
+
* 由调用方(task-queue.ts)负责把标记追加到任务输出;spawn 读 npm 版本是唯一副作用,独立可测。
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
17
|
+
|
|
18
|
+
/** npm arborist 内部崩溃特征:`Cannot read properties of null (reading 'edgesOut')`。 */
|
|
19
|
+
export const NPM_CRASH_EDGES_OUT_RE = /Cannot read properties of null \(reading 'edgesOut'\)|edgesOut/i
|
|
20
|
+
/** 该缺陷在 npm 11.6.0 起不再复现;低于此版本遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
|
|
21
|
+
export const NPM_MIN_VERSION = [11, 6, 0]
|
|
22
|
+
|
|
23
|
+
/** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */
|
|
24
|
+
export function npmVersionOf(env: NodeJS.ProcessEnv | undefined): [number, number, number] | null {
|
|
25
|
+
try {
|
|
26
|
+
const res = spawnSync('npm', ['-v'], { env, encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
27
|
+
if (res.error !== undefined || res.status !== 0) return null
|
|
28
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(res.stdout ?? '')
|
|
29
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function belowMin(v: [number, number, number]): boolean {
|
|
36
|
+
for (let i = 0; i < NPM_MIN_VERSION.length; i++) {
|
|
37
|
+
if (v[i] < NPM_MIN_VERSION[i]) return true
|
|
38
|
+
if (v[i] > NPM_MIN_VERSION[i]) return false
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 安装失败输出含 npm arborist edgesOut 崩溃特征,且本机 npm 版本低于阈值时,返回
|
|
45
|
+
* `[npm-too-low] npm@X.Y.Z …` 标记行;否则返回 null。前端据此展示「本机 npm 版本过低」
|
|
46
|
+
* 的准确原因,避免该报错被误判成插件打包/分发问题。
|
|
47
|
+
*/
|
|
48
|
+
export function npmTooLowMarker(output: string, env?: NodeJS.ProcessEnv): string | null {
|
|
49
|
+
if (!NPM_CRASH_EDGES_OUT_RE.test(output)) return null
|
|
50
|
+
const v = npmVersionOf(env)
|
|
51
|
+
if (v === null || !belowMin(v)) return null
|
|
52
|
+
return `[npm-too-low] npm@${v.join('.')} — npm arborist crashed while resolving peer deps ('edgesOut'); local npm is below ${NPM_MIN_VERSION.join('.')}; upgrade npm (npm install -g npm@latest) and retry`
|
|
53
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* npm 包反查:git 分发不完整(缺构建产物/子模块)导致安装失败时,用 npm
|
|
3
7
|
* registry 的搜索接口按 repository 地址反查该仓库对应的官方 npm 包。
|
|
4
8
|
*
|
|
5
9
|
* 这是通用机制,不针对任何具体插件:只要作者发布了 repository 指向该
|
|
6
10
|
* GitHub 仓库的 npm 包(scope 名与 GitHub 用户名是否一致无所谓),就能命中,
|
|
7
|
-
*
|
|
11
|
+
* 从而把 git 分发不完整的插件切换到 npm 通道安装。命中结果带内存缓存,
|
|
8
12
|
* 避免同一会话内重复查询 registry。
|
|
9
13
|
*/
|
|
10
14
|
import { get } from 'node:https'
|
|
@@ -19,20 +23,22 @@ const REQUEST_TIMEOUT_MS = 8000
|
|
|
19
23
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
20
24
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
21
25
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
26
|
+
* @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
|
|
22
27
|
*/
|
|
23
|
-
export function resolveNpmPackage(repo: string): Promise<string | null> {
|
|
28
|
+
export function resolveNpmPackage(repo: string, registry = ''): Promise<string | null> {
|
|
24
29
|
const key = repo.toLowerCase()
|
|
25
30
|
if (cache.has(key)) return Promise.resolve(cache.get(key) ?? null)
|
|
26
|
-
const name = searchRepo(repo)
|
|
31
|
+
const name = searchRepo(repo, registry)
|
|
27
32
|
name.then((found) => cache.set(key, found)).catch(() => cache.set(key, null))
|
|
28
33
|
return name
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/** 用 npm search 接口按 repository 地址反查,并做铁证校验(包元数据必须指向该仓库)。 */
|
|
32
|
-
function searchRepo(repo: string): Promise<string | null> {
|
|
37
|
+
function searchRepo(repo: string, registry: string): Promise<string | null> {
|
|
33
38
|
const [owner, name] = repo.split('/')
|
|
34
39
|
if (owner === undefined || name === undefined || name === '') return Promise.resolve(null)
|
|
35
|
-
const
|
|
40
|
+
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '')
|
|
41
|
+
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size=10`
|
|
36
42
|
return new Promise((resolve) => {
|
|
37
43
|
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
38
44
|
if (res.statusCode !== 200) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
+
* 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否
|
|
3
7
|
* 真的包含 package.json 声明的入口文件(main / exports["."].default)。
|
|
4
8
|
* git 分发常不提交构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时
|
|
5
9
|
* ERR_MODULE_NOT_FOUND 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
|
|
@@ -13,9 +17,14 @@ import { tmpdir } from 'node:os'
|
|
|
13
17
|
import { join } from 'node:path'
|
|
14
18
|
import { pipeline } from 'node:stream/promises'
|
|
15
19
|
import { promisify } from 'node:util'
|
|
20
|
+
import { githubRepoOf } from '../profile/profile.ts'
|
|
16
21
|
|
|
17
22
|
const execFileAsync = promisify(execFile)
|
|
18
23
|
|
|
24
|
+
/** 单次网络操作超时(ms):git ls-remote / codeload 下载。网络不可达时不能无限挂起,
|
|
25
|
+
* 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
|
|
26
|
+
const PREFLIGHT_TIMEOUT_MS = 15_000
|
|
27
|
+
|
|
19
28
|
/** 预检结果:ok=false 表示分发改入口文件缺失,missing 为该文件在包内的相对路径。 */
|
|
20
29
|
export interface PreflightResult {
|
|
21
30
|
ok: boolean
|
|
@@ -23,12 +32,15 @@ export interface PreflightResult {
|
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
export async function preflightTarget(target: string): Promise<PreflightResult> {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
const [
|
|
35
|
+
const source = githubRepoOf(target)
|
|
36
|
+
if (source === null) return { ok: true, missing: null }
|
|
37
|
+
const [owner, repo] = source.split('/')
|
|
29
38
|
let commit = ''
|
|
30
39
|
try {
|
|
31
|
-
const { stdout } = await execFileAsync('git', ['ls-remote', `https://github.com/${owner}/${repo}.git`, 'HEAD']
|
|
40
|
+
const { stdout } = await execFileAsync('git', ['ls-remote', `https://github.com/${owner}/${repo}.git`, 'HEAD'], {
|
|
41
|
+
timeout: PREFLIGHT_TIMEOUT_MS,
|
|
42
|
+
maxBuffer: 1024 * 1024,
|
|
43
|
+
})
|
|
32
44
|
commit = stdout.split(/\s+/)[0] ?? ''
|
|
33
45
|
} catch {
|
|
34
46
|
return { ok: true, missing: null }
|
|
@@ -58,13 +70,15 @@ export async function preflightTarget(target: string): Promise<PreflightResult>
|
|
|
58
70
|
|
|
59
71
|
function download(url: string, dest: string): Promise<void> {
|
|
60
72
|
return new Promise((resolve, reject) => {
|
|
61
|
-
get(url, (res) => {
|
|
73
|
+
const req = get(url, { timeout: PREFLIGHT_TIMEOUT_MS }, (res) => {
|
|
62
74
|
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303) {
|
|
63
75
|
res.resume()
|
|
64
76
|
if (res.headers.location) {
|
|
65
77
|
void download(res.headers.location, dest).then(resolve, reject)
|
|
66
78
|
return
|
|
67
79
|
}
|
|
80
|
+
reject(new Error('download failed: redirect without location'))
|
|
81
|
+
return
|
|
68
82
|
}
|
|
69
83
|
if (res.statusCode !== 200) {
|
|
70
84
|
res.resume()
|
|
@@ -72,7 +86,10 @@ function download(url: string, dest: string): Promise<void> {
|
|
|
72
86
|
return
|
|
73
87
|
}
|
|
74
88
|
void pipeline(res, createWriteStream(dest)).then(resolve, reject)
|
|
75
|
-
})
|
|
89
|
+
})
|
|
90
|
+
// 网络不可达时 connect/响应可能无限挂起:超时销毁,避免预检卡死(放行交给装后校验)
|
|
91
|
+
req.on('timeout', () => req.destroy(new Error(`download timeout after ${PREFLIGHT_TIMEOUT_MS}ms`)))
|
|
92
|
+
req.on('error', reject)
|
|
76
93
|
})
|
|
77
94
|
}
|
|
78
95
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* 后台安装/卸载任务队列:内存任务注册表 + FIFO 串行 worker + 子进程生命周期管理。
|
|
3
7
|
* 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
|
|
4
8
|
* 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
|
|
@@ -8,11 +12,13 @@ import { readFileSync, statSync } from 'node:fs'
|
|
|
8
12
|
import { dirname, join, resolve } from 'node:path'
|
|
9
13
|
import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
|
|
10
14
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
15
|
+
import { npmTooLowMarker } from './npm-check.ts'
|
|
16
|
+
import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
17
|
+
import type { LoaderHandle } from '../loader.ts'
|
|
18
|
+
import { removeLoadedEntry } from '../loader.ts'
|
|
19
|
+
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.ts'
|
|
20
|
+
import { addAllowBuildsKey, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.ts'
|
|
21
|
+
import { appendLog } from '../log.ts'
|
|
16
22
|
|
|
17
23
|
/** In-memory task registry, keyed by task id. */
|
|
18
24
|
const tasks = new Map<number, InstallTask>()
|
|
@@ -163,6 +169,8 @@ export function startPluginMutation(options: {
|
|
|
163
169
|
target: string
|
|
164
170
|
timeoutMs?: number
|
|
165
171
|
env?: NodeJS.ProcessEnv
|
|
172
|
+
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
173
|
+
globalNpm?: string[]
|
|
166
174
|
/** 待重启行的展示目标(owner/repo):卸载时用于把 npm 包名映射回仓库名 */
|
|
167
175
|
displayTarget?: string
|
|
168
176
|
/** 运行中 loader:卸载成功后主动移除条目、立即生效(缺失时卸载仍需重启清理) */
|
|
@@ -170,7 +178,7 @@ export function startPluginMutation(options: {
|
|
|
170
178
|
/** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
|
|
171
179
|
attempts?: string[]
|
|
172
180
|
}): InstallTask {
|
|
173
|
-
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
|
|
181
|
+
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
|
|
174
182
|
nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
|
|
175
183
|
tasks.set(task.id, task)
|
|
176
184
|
if (tasks.size > MAX_TASKS) {
|
|
@@ -183,6 +191,15 @@ export function startPluginMutation(options: {
|
|
|
183
191
|
})
|
|
184
192
|
}
|
|
185
193
|
queue.push({ task, options: { ...options } })
|
|
194
|
+
// 系统日志:记录任务入队(安装/卸载开始)
|
|
195
|
+
const category = options.action === 'add' ? 'install' : 'uninstall'
|
|
196
|
+
appendLog(options.profile, {
|
|
197
|
+
at: Date.now(),
|
|
198
|
+
level: 'info',
|
|
199
|
+
category,
|
|
200
|
+
event: `${category}.start`,
|
|
201
|
+
message: `${options.action === 'add' ? '开始安装' : '开始卸载'} ${options.displayTarget ?? options.target}`,
|
|
202
|
+
})
|
|
186
203
|
pumpQueue()
|
|
187
204
|
return task
|
|
188
205
|
}
|
|
@@ -196,6 +213,34 @@ function pumpQueue(): void {
|
|
|
196
213
|
task.status = 'running'
|
|
197
214
|
void runPluginMutation({ ...options, task }).finally(() => {
|
|
198
215
|
runningChildren.delete(task.id)
|
|
216
|
+
// 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载日志
|
|
217
|
+
const show = task.displayTarget ?? task.target
|
|
218
|
+
const category = task.action === 'add' ? 'install' : 'uninstall'
|
|
219
|
+
if (task.status === 'done') {
|
|
220
|
+
appendLog(options.profile, {
|
|
221
|
+
at: Date.now(),
|
|
222
|
+
level: 'success',
|
|
223
|
+
category,
|
|
224
|
+
event: `${category}.done`,
|
|
225
|
+
message: `${task.action === 'add' ? '安装成功' : '卸载成功'} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
226
|
+
})
|
|
227
|
+
} else if (task.status === 'failed') {
|
|
228
|
+
appendLog(options.profile, {
|
|
229
|
+
at: Date.now(),
|
|
230
|
+
level: 'error',
|
|
231
|
+
category,
|
|
232
|
+
event: `${category}.fail`,
|
|
233
|
+
message: `${task.action === 'add' ? '安装失败' : '卸载失败'} ${show}`,
|
|
234
|
+
})
|
|
235
|
+
} else if (task.status === 'cancelled') {
|
|
236
|
+
appendLog(options.profile, {
|
|
237
|
+
at: Date.now(),
|
|
238
|
+
level: 'warn',
|
|
239
|
+
category,
|
|
240
|
+
event: 'task.cancel',
|
|
241
|
+
message: `已取消 ${show}`,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
199
244
|
// 已终态的任务移出队列:既防 queue 数组无限增长(内存泄漏),也让下一轮 pump 干净
|
|
200
245
|
const idx = queue.findIndex((it) => it.task === task)
|
|
201
246
|
if (idx >= 0) queue.splice(idx, 1)
|
|
@@ -219,24 +264,37 @@ function spawnMutation(options: {
|
|
|
219
264
|
displayTarget?: string
|
|
220
265
|
/** 运行中 loader:卸载成功后主动移除条目、立即生效(缺失时卸载仍需重启清理) */
|
|
221
266
|
uninstallLoader?: LoaderHandle
|
|
267
|
+
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
268
|
+
globalNpm?: string[]
|
|
222
269
|
}): Promise<InstallResult> {
|
|
223
|
-
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader } = options
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm } = options
|
|
271
|
+
// 全局 npm 安装:直接 spawn 本机 npm(`npm install -g <pkgs>`),不是 dsh plugin 命令
|
|
272
|
+
const isGlobalNpm = (globalNpm ?? []).length > 0
|
|
273
|
+
const invocation = isGlobalNpm
|
|
274
|
+
? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
275
|
+
: cliInvocation()
|
|
276
|
+
const args = isGlobalNpm
|
|
277
|
+
? ['install', '-g', ...(globalNpm ?? [])]
|
|
278
|
+
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action, target]
|
|
279
|
+
// 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
|
|
280
|
+
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
281
|
+
const executed = isGlobalNpm
|
|
282
|
+
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
283
|
+
: `dsh plugin --profile ${profile} ${action} ${target}`
|
|
229
284
|
if (task && !task.attempts.includes(executed)) {
|
|
230
285
|
task.attempts.push(executed)
|
|
231
286
|
}
|
|
232
|
-
|
|
287
|
+
// 单次 spawn 并捕获结果:流式输出打日志 + 超时/取消兜底,close 后 resolve。
|
|
288
|
+
// 不做任何终态后处理(exitCode/status/needsRestart 由下方统一收尾),
|
|
289
|
+
// 这样全局 npm 首次失败后可以干净地换参数重试一次。
|
|
290
|
+
const spawnOnce = (attemptArgs: string[]): Promise<InstallResult> => new Promise((resolveOnce) => {
|
|
233
291
|
let stdout = ''
|
|
234
292
|
let stderr = ''
|
|
235
293
|
let timedOut = false
|
|
236
294
|
let settled = false
|
|
237
295
|
let child: ReturnType<typeof spawn>
|
|
238
296
|
try {
|
|
239
|
-
child = spawn(invocation.file,
|
|
297
|
+
child = spawn(invocation.file, attemptArgs, {
|
|
240
298
|
cwd: invocation.cwd,
|
|
241
299
|
env,
|
|
242
300
|
shell: invocation.useShell,
|
|
@@ -251,7 +309,7 @@ function spawnMutation(options: {
|
|
|
251
309
|
task.exitCode = null
|
|
252
310
|
pushLine(task, `[error] ${error instanceof Error ? error.message : String(error)}`)
|
|
253
311
|
}
|
|
254
|
-
|
|
312
|
+
resolveOnce({
|
|
255
313
|
exitCode: null,
|
|
256
314
|
timedOut: false,
|
|
257
315
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -268,9 +326,8 @@ function spawnMutation(options: {
|
|
|
268
326
|
let exitWatch: ReturnType<typeof setTimeout> | undefined
|
|
269
327
|
let forceSettle: ReturnType<typeof setTimeout> | undefined
|
|
270
328
|
|
|
271
|
-
// 唯一收尾入口:close / error / 兜底超时
|
|
272
|
-
|
|
273
|
-
const settle = (result: InstallResult): void => {
|
|
329
|
+
// 唯一收尾入口:close / error / 兜底超时 三者只放行一次,确保任务持续推进
|
|
330
|
+
const settleOnce = (result: InstallResult): void => {
|
|
274
331
|
if (settled) return
|
|
275
332
|
settled = true
|
|
276
333
|
if (timer !== undefined) clearTimeout(timer)
|
|
@@ -278,7 +335,7 @@ function spawnMutation(options: {
|
|
|
278
335
|
if (exitWatch !== undefined) clearTimeout(exitWatch)
|
|
279
336
|
if (forceSettle !== undefined) clearTimeout(forceSettle)
|
|
280
337
|
if (task) runningChildren.delete(task.id)
|
|
281
|
-
|
|
338
|
+
resolveOnce(result)
|
|
282
339
|
}
|
|
283
340
|
|
|
284
341
|
timer = setTimeout(() => {
|
|
@@ -294,7 +351,7 @@ function spawnMutation(options: {
|
|
|
294
351
|
if (kind === 'stdout') stdout = (stdout + text).slice(-CAPTURE_LIMIT_BYTES)
|
|
295
352
|
else stderr = (stderr + text).slice(-CAPTURE_LIMIT_BYTES)
|
|
296
353
|
if (task) {
|
|
297
|
-
// 原生透传 pnpm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
|
|
354
|
+
// 原生透传 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
|
|
298
355
|
for (const line of text.split(/\r?\n|\r/)) {
|
|
299
356
|
if (line.trim() !== '') pushLine(task, line.trimEnd())
|
|
300
357
|
}
|
|
@@ -305,9 +362,8 @@ function spawnMutation(options: {
|
|
|
305
362
|
|
|
306
363
|
child.once('exit', () => {
|
|
307
364
|
// dsh 内部会 fork pnpm:pnpm 退出后仍攥着 stdout/stderr 管道时,
|
|
308
|
-
// close
|
|
309
|
-
//
|
|
310
|
-
// 保证任务必然进入终态、后续排队任务自动启动。
|
|
365
|
+
// close 事件不会到来。exit 后给 close 一段宽限,未按时触发则整组清理强制结束,
|
|
366
|
+
// 确保任务进入终态、后续排队任务自动启动。
|
|
311
367
|
exitWatch = setTimeout(() => {
|
|
312
368
|
if (settled) return
|
|
313
369
|
if (task && task.status !== 'cancelled') pushLine(task, '[stale pipe] force closing child')
|
|
@@ -321,7 +377,7 @@ function spawnMutation(options: {
|
|
|
321
377
|
task.timedOut = true
|
|
322
378
|
pushLine(task, '[timed out]')
|
|
323
379
|
}
|
|
324
|
-
|
|
380
|
+
settleOnce({ exitCode: null, timedOut: true, error: 'child failed to close', stdout, stderr })
|
|
325
381
|
}, 5_000)
|
|
326
382
|
}, 3_000)
|
|
327
383
|
})
|
|
@@ -332,43 +388,62 @@ function spawnMutation(options: {
|
|
|
332
388
|
task.exitCode = null
|
|
333
389
|
pushLine(task, `[error] ${error.message}`)
|
|
334
390
|
}
|
|
335
|
-
|
|
391
|
+
settleOnce({ exitCode: null, timedOut: false, error: error.message, stdout, stderr })
|
|
336
392
|
})
|
|
337
393
|
|
|
338
|
-
child.once('close',
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
394
|
+
child.once('close', (code) => {
|
|
395
|
+
settleOnce({ exitCode: code, timedOut, error: null, stdout, stderr })
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
return (async () => {
|
|
400
|
+
let result = await spawnOnce(args)
|
|
401
|
+
// npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`,npm/cli#9911):
|
|
402
|
+
// 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
|
|
403
|
+
// 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
|
|
404
|
+
// 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
|
|
405
|
+
// 跳过 peer 自动解析即可绕过该 bug(peer 通常由同装的 @deepseek-ai/dsh 依赖树天然提供)。
|
|
406
|
+
// 二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
|
|
407
|
+
if (isGlobalNpm && result.exitCode !== 0 && !result.timedOut && task?.status !== 'cancelled') {
|
|
408
|
+
const retryArgs = ['install', '-g', '--legacy-peer-deps', ...(globalNpm ?? [])]
|
|
409
|
+
const retryExecuted = `npm install -g --legacy-peer-deps ${(globalNpm ?? []).join(' ')}`
|
|
410
|
+
if (task && !task.attempts.includes(retryExecuted)) task.attempts.push(retryExecuted)
|
|
411
|
+
if (task) pushLine(task, '[npm] peer-set resolution crashed npm (known npm bug) — retrying with --legacy-peer-deps')
|
|
412
|
+
result = await spawnOnce(retryArgs)
|
|
413
|
+
}
|
|
414
|
+
// 统一终态后处理:exitCode/status/needsRestart 以最后一次尝试为准
|
|
415
|
+
if (task) {
|
|
416
|
+
task.exitCode = result.exitCode
|
|
417
|
+
task.timedOut = result.timedOut
|
|
418
|
+
if (task.status !== 'cancelled') {
|
|
419
|
+
task.status = result.timedOut || result.exitCode !== 0 ? 'failed' : 'done'
|
|
420
|
+
if (task.status === 'done') {
|
|
421
|
+
task.progress = 100
|
|
422
|
+
if (isGlobalNpm) {
|
|
423
|
+
// 全局 npm 安装:装的是本机 CLI 工具(如 dsh-tui),不进任何 profile,无需宿主重启
|
|
424
|
+
task.needsRestart = false
|
|
425
|
+
} else if (action === 'remove' && uninstallLoader) {
|
|
426
|
+
// 卸载成功 → 主动从运行中 loader 停用该包条目;
|
|
427
|
+
// 仅当宿主从未加载过它(磁盘已干净)才判「无需重启」;
|
|
428
|
+
// 只要曾在 loader 中存活(停用过 live entry)就仍要求重启——
|
|
429
|
+
// 带 UI 的插件(侧边栏面板等宿主启动时渲染的槽位)disable 后不会
|
|
430
|
+
// 主动消失,重启才能立即摘除面板
|
|
431
|
+
const removed = await removeLoadedEntry(uninstallLoader, target)
|
|
432
|
+
task.needsRestart = !removed
|
|
433
|
+
if (removed) clearPendingRestart(displayTarget ?? target)
|
|
434
|
+
else addPendingRestart(displayTarget ?? target, 'uninstall')
|
|
435
|
+
} else {
|
|
436
|
+
// 安装成功(或卸载但无 loader 引用)→ 登记待重启:
|
|
437
|
+
// 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
|
|
438
|
+
task.needsRestart = true
|
|
439
|
+
addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall')
|
|
365
440
|
}
|
|
366
441
|
}
|
|
367
|
-
|
|
368
|
-
settle({ exitCode: code, timedOut, error: null, stdout, stderr })
|
|
442
|
+
pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]`)
|
|
369
443
|
}
|
|
370
|
-
}
|
|
371
|
-
|
|
444
|
+
}
|
|
445
|
+
return result
|
|
446
|
+
})()
|
|
372
447
|
}
|
|
373
448
|
|
|
374
449
|
/**
|
|
@@ -385,8 +460,11 @@ export function verifyInstalledEntry(profile: string, target: string): { name: s
|
|
|
385
460
|
return { name: null, missing: null }
|
|
386
461
|
}
|
|
387
462
|
const deps = (profilePkg?.dependencies ?? {}) as Record<string, string>
|
|
388
|
-
// target 自身是依赖名(npm 包),或通过
|
|
389
|
-
const
|
|
463
|
+
// target 自身是依赖名(npm 包),或通过 Git specifier 反查依赖名
|
|
464
|
+
const targetRepo = githubRepoOf(target)?.toLowerCase()
|
|
465
|
+
const name = targetRepo !== undefined
|
|
466
|
+
? Object.keys(deps).find((k) => githubRepoOf(deps[k])?.toLowerCase() === targetRepo)
|
|
467
|
+
: deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
|
|
390
468
|
if (!name) return { name: null, missing: null }
|
|
391
469
|
const pkgDir = join(profileDirectory(profile), 'node_modules', name)
|
|
392
470
|
let entry: string | null = null
|
|
@@ -421,6 +499,21 @@ function ignoredBuildKeys(output: string): string[] {
|
|
|
421
499
|
.filter((s) => s !== '')
|
|
422
500
|
}
|
|
423
501
|
|
|
502
|
+
/** npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 把 [npm-too-low] 标记追加到任务输出,
|
|
503
|
+
* 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
|
|
504
|
+
* 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
|
|
505
|
+
function annotateNpmTooLow(result: InstallResult, options: {
|
|
506
|
+
action: 'add' | 'remove'
|
|
507
|
+
task?: InstallTask
|
|
508
|
+
env?: NodeJS.ProcessEnv
|
|
509
|
+
}): InstallResult {
|
|
510
|
+
if (options.action !== 'add' || result.exitCode === 0 || result.timedOut) return result
|
|
511
|
+
const line = npmTooLowMarker(`${result.stderr}\n${result.stdout}`, options.env)
|
|
512
|
+
if (line === null) return result
|
|
513
|
+
if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
|
|
514
|
+
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
|
|
515
|
+
}
|
|
516
|
+
|
|
424
517
|
/**
|
|
425
518
|
* Run a plugin mutation with one recovery path: when `add` fails because
|
|
426
519
|
* pnpm's allowBuilds gate blocks a build script — either the git-hosted
|
|
@@ -441,9 +534,13 @@ export async function runPluginMutation(options: {
|
|
|
441
534
|
displayTarget?: string
|
|
442
535
|
/** 运行中 loader:卸载成功后主动移除条目、立即生效(缺失时卸载仍需重启清理) */
|
|
443
536
|
uninstallLoader?: LoaderHandle
|
|
537
|
+
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
538
|
+
globalNpm?: string[]
|
|
444
539
|
}): Promise<InstallResult> {
|
|
445
|
-
|
|
446
|
-
|
|
540
|
+
let result = await spawnMutation(options)
|
|
541
|
+
// allowBuilds 放行 / 装后入口校验都是 profile 专属(pnpm 装进 profile 的场景);
|
|
542
|
+
// 全局 npm 安装直接走 npm,无 pnpm allowBuilds 拦截、也不进 profile,跳过这两类恢复
|
|
543
|
+
if (options.action === 'add' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
447
544
|
const output = `${result.stderr}\n${result.stdout}`
|
|
448
545
|
// git 源:pnpm 打印的 key@url 提示
|
|
449
546
|
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
|
|
@@ -457,15 +554,16 @@ export async function runPluginMutation(options: {
|
|
|
457
554
|
pushLine(options.task, `[allow build] ${key}`)
|
|
458
555
|
}
|
|
459
556
|
addAllowBuildsKey(options.profile, key)
|
|
460
|
-
|
|
557
|
+
result = await spawnMutation(options)
|
|
461
558
|
}
|
|
462
559
|
}
|
|
463
560
|
// 依赖原生模块/构建脚本被 pnpm v11 拦截(ERR_PNPM_IGNORED_BUILDS):git 与 npm 源都会出现。
|
|
464
561
|
// allowBuilds 的 key 是输出里列出的 name@version(如 node-pty@1.1.0),逐个放行后重试一次;
|
|
465
562
|
// 输出解析不到时回退用目标名(插件自身构建脚本被拦的 npm 场景)。
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
563
|
+
// 用重试后的最终结果判断:git 源放行 prepare 后可能又在依赖层撞上原生构建拦截。
|
|
564
|
+
if (result.exitCode !== 0 && !result.timedOut && `${result.stderr}\n${result.stdout}`.includes('ERR_PNPM_IGNORED_BUILDS')) {
|
|
565
|
+
const keys = ignoredBuildKeys(`${result.stderr}\n${result.stdout}`)
|
|
566
|
+
if (keys.length === 0 && githubRepoOf(options.target) === null) keys.push(options.target)
|
|
469
567
|
if (keys.length > 0) {
|
|
470
568
|
if (options.task) {
|
|
471
569
|
options.task.status = 'running'
|
|
@@ -473,13 +571,14 @@ export async function runPluginMutation(options: {
|
|
|
473
571
|
pushLine(options.task, `[allow build] ${keys.join(', ')}`)
|
|
474
572
|
}
|
|
475
573
|
for (const key of keys) addAllowBuildsKey(options.profile, key)
|
|
476
|
-
|
|
574
|
+
result = await spawnMutation(options)
|
|
477
575
|
}
|
|
478
576
|
}
|
|
479
577
|
}
|
|
480
578
|
// pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
|
|
481
|
-
//
|
|
482
|
-
|
|
579
|
+
// 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
|
|
580
|
+
// 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
|
|
581
|
+
if (options.action === 'add' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
483
582
|
const verified = verifyInstalledEntry(options.profile, options.target)
|
|
484
583
|
if (verified.name !== null && verified.missing !== null) {
|
|
485
584
|
clearPendingRestart(options.displayTarget ?? options.target)
|
|
@@ -493,5 +592,6 @@ export async function runPluginMutation(options: {
|
|
|
493
592
|
return { exitCode: 1, timedOut: false, error: `entry file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` }
|
|
494
593
|
}
|
|
495
594
|
}
|
|
496
|
-
|
|
595
|
+
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
596
|
+
return annotateNpmTooLow(result, options)
|
|
497
597
|
}
|