dsh-plugin 2.3.1 → 2.3.3
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 +15 -21
- package/README.md +15 -21
- package/client/client.js +412 -382
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +4 -4
- package/lib/http/diagnostics.js +4 -4
- package/lib/http/install.js +21 -10
- package/lib/http/npm-release-age.js +9 -3
- package/lib/http/shared.js +1 -1
- package/lib/services/host-version.js +2 -2
- package/lib/services/install/dsh-compat.js +1 -1
- package/lib/services/install/npm-resolve.js +1 -1
- package/lib/services/install/operation-log.js +3 -3
- package/lib/services/install/preflight.js +1 -1
- package/lib/services/install/registry-fallback.js +3 -3
- package/lib/services/install/release-target.js +2 -2
- package/lib/services/install/task-queue.js +35 -20
- package/lib/services/loader.js +8 -8
- package/lib/services/npm-release-age.js +46 -9
- package/lib/services/probe.js +12 -12
- package/lib/services/profile/installed-versions.js +2 -2
- package/lib/services/profile/profile.js +2 -2
- package/lib/types/services/host-version.d.ts +1 -1
- package/lib/types/services/install/install-types.d.ts +5 -5
- package/lib/types/services/install/operation-log.d.ts +1 -1
- package/lib/types/services/install/preflight.d.ts +1 -1
- package/lib/types/services/install/registry-fallback.d.ts +1 -1
- package/lib/types/services/install/release-target.d.ts +1 -1
- package/lib/types/services/install/task-queue.d.ts +10 -7
- package/lib/types/services/loader.d.ts +5 -5
- package/lib/types/services/npm-release-age.d.ts +14 -2
- package/lib/types/services/probe.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +59 -28
- package/src/client/components/catalog/CatalogList.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +3 -3
- package/src/client/components/modals/AboutModal.tsx +2 -2
- package/src/client/components/modals/ConfirmDialog.tsx +2 -2
- package/src/client/components/modals/HubUpdateModal.tsx +3 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
- package/src/client/components/modals/LogsPathModal.tsx +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +2 -2
- package/src/client/components/modals/modals.tsx +13 -11
- package/src/client/components/views/CustomInstallView.tsx +2 -2
- package/src/client/components/views/InstalledView.tsx +2 -2
- package/src/client/components/views/SettingsView.tsx +3 -3
- package/src/client/data/catalog.ts +1 -1
- package/src/client/data/host.ts +3 -1
- package/src/client/data/hub.ts +3 -3
- package/src/client/hooks/useCatalog.ts +14 -14
- package/src/client/hooks/useSettings.ts +2 -2
- package/src/client/hooks/useTaskQueue.ts +31 -24
- package/src/client/locales.ts +21 -21
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/ensure-plugin-css.ts +2 -2
- package/src/client/logic/failures.ts +9 -9
- package/src/client/logic/install-command.ts +5 -3
- package/src/client/logic/installed.ts +1 -1
- package/src/client/logic/release-age.ts +10 -10
- package/src/client/logic/urls.ts +3 -3
- package/src/client/styles/Header.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/styles/SectionTabs.module.css +1 -1
- package/src/client/styles/tokens.module.css +1 -1
- package/src/client/types.ts +4 -4
- package/src/server/http/catalog.ts +4 -4
- package/src/server/http/diagnostics.ts +4 -4
- package/src/server/http/install.ts +20 -10
- package/src/server/http/npm-release-age.ts +9 -3
- package/src/server/http/shared.ts +1 -1
- package/src/server/services/host-version.ts +2 -2
- package/src/server/services/install/dsh-compat.ts +1 -1
- package/src/server/services/install/install-types.ts +5 -5
- package/src/server/services/install/npm-resolve.ts +1 -1
- package/src/server/services/install/operation-log.ts +3 -3
- package/src/server/services/install/preflight.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +3 -3
- package/src/server/services/install/release-target.ts +2 -2
- package/src/server/services/install/task-queue.ts +40 -26
- package/src/server/services/loader.ts +10 -10
- package/src/server/services/npm-release-age.ts +50 -10
- package/src/server/services/probe.ts +12 -12
- package/src/server/services/profile/installed-versions.ts +2 -2
- package/src/server/services/profile/profile.ts +2 -2
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 故发布太新的版本不该提示更新。Hub
|
|
6
|
+
* 发布冷却期的时间判定(纯函数):pnpm 会不提示地回退发布还不满 24 小时的版本,
|
|
7
|
+
* 故发布太新的版本不该提示更新。Hub 自身更新与插件更新共用同一规则。
|
|
8
8
|
*/
|
|
9
9
|
import type { HubUpdateInfo, HubUpdateState, HubUpdateVersion } from '../types.ts'
|
|
10
10
|
|
|
11
|
-
/** pnpm
|
|
11
|
+
/** pnpm 发布冷却期:dsh profile 默认 minimumReleaseAge=1440(分钟);+30 分钟缓冲抵消网络/CDN 边缘时间偏差。 */
|
|
12
12
|
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/** 发布时间是否仍在冷却期内(太新 → 暂缓提示);无值/解析不出返回 false(不拦真实更新)。 */
|
|
15
15
|
export function isReleaseTooFresh(publishedAt: string | null | undefined, now: number): boolean {
|
|
16
16
|
if (typeof publishedAt !== 'string' || publishedAt === '') return false
|
|
17
17
|
const ms = Date.parse(publishedAt)
|
|
@@ -61,20 +61,20 @@ export function compareVersions(a: string | null | undefined, b: string | null |
|
|
|
61
61
|
return 0
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/**
|
|
64
|
+
/** 「可更新」判定结果:是否提示,以及还没过冷却期时的可重试时刻。 */
|
|
65
65
|
export interface UpdateDecision {
|
|
66
66
|
offered: boolean
|
|
67
|
-
/**
|
|
67
|
+
/** 发布还不满冷却期 → 可重试时刻(毫秒);非冷却期原因(npm 上没有更高版本)→ null */
|
|
68
68
|
pendingUntil: number | null
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* 是否值得提示「可更新」:npm
|
|
72
|
+
* 是否值得提示「可更新」:npm 上最新可安装版本确实比已装版本新,且已过发布冷却期。
|
|
73
73
|
*
|
|
74
74
|
* 更新命令执行的是 `pkg@latest`,所以「目录里标的版本」不是最终能装上的版本:
|
|
75
75
|
* 作者可能只打了 GitHub release 没发 npm —— 此时提示会变成永远点不掉的循环。
|
|
76
76
|
* 任一信息缺失(查不到发布信息 / 没有已装版本记录)返回 offered,交回调用方原有判定:
|
|
77
|
-
*
|
|
77
|
+
* 宁可提示,也不因为一次查询失败漏掉真实更新。
|
|
78
78
|
*/
|
|
79
79
|
export function updateDecision(
|
|
80
80
|
release: { version: string; publishedAt: string } | null,
|
|
@@ -104,8 +104,8 @@ export function isUpdateOffered(
|
|
|
104
104
|
/**
|
|
105
105
|
* 选出本次真正该提示的更新版本。
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
* 提示被整体跳过。这里把最新版与 recentVersions
|
|
107
|
+
* 只看最新版会漏更新:连续发版时间隔小于发布冷却期时,最新版永远「太新」,
|
|
108
|
+
* 提示被整体跳过。这里把最新版与 recentVersions 一起当候选,取「高于当前版本且已过冷却期」
|
|
109
109
|
* 的最高版本;一个都没有但仍存在更高版本 → pending(暂缓,不谎称已是最新)。
|
|
110
110
|
*/
|
|
111
111
|
export function pickHubUpdate(info: HubUpdateInfo, currentVersion: string, now: number): { state: HubUpdateState; target: HubUpdateVersion | null } {
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -228,7 +228,7 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
228
228
|
* 错误摘要,供插件中心维护者分诊 —— 不再把环境问题/插件中心自身问题误投给插件作者。
|
|
229
229
|
*/
|
|
230
230
|
export type HubIssueVariant =
|
|
231
|
-
/**
|
|
231
|
+
/** 缺省:安装器主动把失败送到插件中心分诊(未分类时的默认去向) */
|
|
232
232
|
| 'triage'
|
|
233
233
|
/** 从判定由插件中心自己产出的分支(插件打包问题 / allowBuilds 拦截)的次要入口提交:用户怀疑是误报 */
|
|
234
234
|
| 'misjudge'
|
|
@@ -249,7 +249,7 @@ export function hubIssueUrl(
|
|
|
249
249
|
const zhWord = actionWordZh(action)
|
|
250
250
|
const repo = pluginRepo && pluginRepo.trim() !== '' ? pluginRepo.trim() : 'unknown'
|
|
251
251
|
const title = variant === 'misjudge'
|
|
252
|
-
//
|
|
252
|
+
// 标题就点明「可能是误判」,维护者一眼能区分它和未分类时的默认去向
|
|
253
253
|
? `[dsh-plugin-hub] possible misjudgment: ${repo}`
|
|
254
254
|
: `[dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
|
|
255
255
|
const code = coreErrorCode(message)
|
|
@@ -264,7 +264,7 @@ export function hubIssueUrl(
|
|
|
264
264
|
...(variant === 'misjudge'
|
|
265
265
|
// 这些分支的判据由插件中心自己产出、或掺了我们的动作(自动放行 allowBuilds)——
|
|
266
266
|
// 一旦我们自己误报,用户就被送去作者仓库、我们永远收不到反馈。
|
|
267
|
-
//
|
|
267
|
+
// 正文写清它来自哪条分支、判据是谁给的,维护者才不会把它当成未分类时的默认去向。
|
|
268
268
|
? [
|
|
269
269
|
'- Filed from: a branch that normally routes to the plugin repo — filed here because that verdict may be our own misjudgment.',
|
|
270
270
|
'- Source of that verdict: produced by dsh-plugin-hub\'s own check or its auto-allow retry, not by plain pnpm output.',
|
|
@@ -876,7 +876,7 @@
|
|
|
876
876
|
|
|
877
877
|
/* —— 安装失败记录 —— */
|
|
878
878
|
|
|
879
|
-
/* 失败记录列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height
|
|
879
|
+
/* 失败记录列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 限制滚动 */
|
|
880
880
|
.failList {
|
|
881
881
|
display: flex;
|
|
882
882
|
flex-direction: column;
|
|
@@ -904,7 +904,7 @@
|
|
|
904
904
|
|
|
905
905
|
/* —— 任务通知中心(成功 + 失败) —— */
|
|
906
906
|
|
|
907
|
-
/* 通知列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height
|
|
907
|
+
/* 通知列表:纵向排列,高度随记录数自适应 —— 记录少时贴合内容,记录多时由窗体 max-height 限制滚动 */
|
|
908
908
|
.noticeList {
|
|
909
909
|
display: flex;
|
|
910
910
|
flex-direction: column;
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
|
|
106
106
|
/* —— 通知中心入口 —— */
|
|
107
107
|
|
|
108
|
-
/* 通知铃铛按钮:位于「设置」tab 后边、靠右对齐(margin-left:auto
|
|
108
|
+
/* 通知铃铛按钮:位于「设置」tab 后边、靠右对齐(margin-left:auto 占满 tab 剩余空间),
|
|
109
109
|
在 38px tab 栏里垂直居中;常态透明底只显铃铛轮廓,hover 浅灰,不抢 tab 视觉层级 */
|
|
110
110
|
.noticeBtn {
|
|
111
111
|
position: relative;
|
package/src/client/types.ts
CHANGED
|
@@ -110,8 +110,8 @@ export interface HubUpdateInfo {
|
|
|
110
110
|
notes?: string | Record<string, string> | null
|
|
111
111
|
/**
|
|
112
112
|
* 最近若干发版(含最新版),按版本号降序无关、由客户端自行排序。
|
|
113
|
-
* 用途:连续快速发版时,最新版往往还在 pnpm 的 24h
|
|
114
|
-
*
|
|
113
|
+
* 用途:连续快速发版时,最新版往往还在 pnpm 的 24h 发布冷却期内;
|
|
114
|
+
* 客户端据此回退到「更早、但已过冷却期」的版本提示更新,而不是整体跳过。
|
|
115
115
|
*/
|
|
116
116
|
recentVersions?: HubUpdateVersion[]
|
|
117
117
|
}
|
|
@@ -126,8 +126,8 @@ export interface HubUpdateVersion {
|
|
|
126
126
|
/**
|
|
127
127
|
* Hub 自我更新状态 —— 供版本弹窗选文案与按钮,四态互斥:
|
|
128
128
|
* - `none` 未取到接口中心信息(拉取失败)→ 只说当前版本,不提示更新
|
|
129
|
-
* - `available`
|
|
130
|
-
* - `pending`
|
|
129
|
+
* - `available` 远端有更新且已过发布冷却期 → 提示更新
|
|
130
|
+
* - `pending` 远端有更新但发布还不满冷却期(见 useCatalog)→ 暂缓提示并说明原因
|
|
131
131
|
* - `latest` 与远端最新版一致 → 确已最新
|
|
132
132
|
* 「有新版但暂缓提示」必须与「确已最新」分开:两者若共用文案,弹窗会在本机版本落后时
|
|
133
133
|
* 谎称「你正在使用 v{远端最新版},已是最新」。
|
|
@@ -42,7 +42,7 @@ function fetchCatalogOnce(target: string, proxy: string): ReturnType<typeof fetc
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
|
|
45
|
-
* maxAgeMs 传 Infinity
|
|
45
|
+
* maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时使用。 */
|
|
46
46
|
function readCatalogCache(file: string, maxAgeMs: number): string | null {
|
|
47
47
|
try {
|
|
48
48
|
const entry = JSON.parse(readFileSync(file, 'utf8')) as CatalogCacheEntry
|
|
@@ -56,7 +56,7 @@ function readCatalogCache(file: string, maxAgeMs: number): string | null {
|
|
|
56
56
|
return null
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/** 写缓存:目录不存在则创建;写失败直接忽略(缓存只是加速,不影响功能)。 */
|
|
60
60
|
function writeCatalogCache(file: string, body: string): void {
|
|
61
61
|
try {
|
|
62
62
|
mkdirSync(dirname(file), { recursive: true })
|
|
@@ -108,7 +108,7 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
|
|
|
108
108
|
handler: async (request, response) => {
|
|
109
109
|
if (!requireMethod(request, response, 'GET')) return
|
|
110
110
|
// 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
|
|
111
|
-
// 转发(curl 子进程注入代理 env),与 npm / git
|
|
111
|
+
// 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理规则,
|
|
112
112
|
// 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
|
|
113
113
|
const url = new URL(request.url ?? '/', 'http://localhost')
|
|
114
114
|
const settings = loadSettings(profile)
|
|
@@ -145,7 +145,7 @@ export function registerCatalogRoutes(webServer: WebServerService, profile: stri
|
|
|
145
145
|
sendJson(response, 200, JSON.parse(stale))
|
|
146
146
|
return
|
|
147
147
|
} catch {
|
|
148
|
-
//
|
|
148
|
+
// 备用缓存也损坏:落到下面的 502
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
// 带上 curl 退出码与 stderr 末行:只回 error 字段时,用户贴出来的响应里
|
|
@@ -14,7 +14,7 @@ import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.ts'
|
|
|
14
14
|
import { appendLog } from '../services/log.ts'
|
|
15
15
|
import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
16
16
|
|
|
17
|
-
/** 本机实际生效的 npm registry
|
|
17
|
+
/** 本机实际生效的 npm registry:诊断与安装同一套规则 —— 设置里留空时读用户 npm 配置
|
|
18
18
|
* (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
|
|
19
19
|
* 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
|
|
20
20
|
function configuredNpmRegistry(profile: string): string {
|
|
@@ -75,7 +75,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
75
75
|
}
|
|
76
76
|
const settings = loadSettings(profile)
|
|
77
77
|
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
78
|
-
// 未配置(空串)时探测本机 npm 配置里实际生效的源 ——
|
|
78
|
+
// 未配置(空串)时探测本机 npm 配置里实际生效的源 —— 与安装同一套规则。
|
|
79
79
|
const registry = settings.npmRegistry.replace(/\/+$/, '') || configuredNpmRegistry(profile)
|
|
80
80
|
// 连通性自检打各通道的轻量资源:npm 源拉包元数据、GitHub 行探小仓库、
|
|
81
81
|
// 目录站按 Range 取 1KB(探市场真实下载的 plugins.zh.json,但不整份下载)。
|
|
@@ -91,7 +91,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
91
91
|
{ key: 'catalog', url: 'https://api.dsh-plugin.org/plugins.zh.json', display: '', cmd: 'curl -r 0-1023 https://api.dsh-plugin.org/plugins.zh.json', range: '0-1023' },
|
|
92
92
|
]
|
|
93
93
|
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
94
|
-
//
|
|
94
|
+
// 验证「代理能不能把请求带出去」。与安装同一套规则:curl 子进程注入该代理 env。
|
|
95
95
|
// 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
|
|
96
96
|
if (settings.proxy !== '') {
|
|
97
97
|
allChecks.push({ key: 'proxy', url: 'https://github.com', display: settings.proxy, cmd: `curl -s -o /dev/null -x ${settings.proxy} https://github.com`, proxy: settings.proxy })
|
|
@@ -114,7 +114,7 @@ export function registerDiagnosticsRoutes(webServer: WebServerService, profile:
|
|
|
114
114
|
const results: Array<{ key: string; ok: boolean }> = []
|
|
115
115
|
for (const c of checks) {
|
|
116
116
|
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display })
|
|
117
|
-
// git 行走真实克隆握手(gitLsRemote),其余走 curl
|
|
117
|
+
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时保护
|
|
118
118
|
const r = c.git
|
|
119
119
|
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
120
120
|
: await probe(c.url, c.proxy, c.range)
|
|
@@ -12,11 +12,12 @@ import { isDshNpmPackageForRepo } from '../services/install/npm-resolve.ts'
|
|
|
12
12
|
import { probeUrl, systemProxy } from '../services/probe.ts'
|
|
13
13
|
import { loadSettings, type HubSettings } from '../services/settings.ts'
|
|
14
14
|
import { registryCandidates, registryHost, userNpmRegistry } from '../services/install/registry-fallback.ts'
|
|
15
|
+
import { lookupReleaseAge } from '../services/npm-release-age.ts'
|
|
15
16
|
import { profileDirectory } from '../services/profile/profile.ts'
|
|
16
17
|
import { readInstalled } from './catalog.ts'
|
|
17
18
|
import { COMMAND_TIMEOUT_MS, readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
18
19
|
|
|
19
|
-
/** 安装/卸载 CLI
|
|
20
|
+
/** 安装/卸载 CLI 的子进程环境:原样传递宿主环境并注入 HTTP(S) 代理,
|
|
20
21
|
* 代理来源与诊断一致(设置代理 → 系统代理 → 宿主 env);registry 由任务队列按候选源逐次注入。 */
|
|
21
22
|
function mutationEnv(settings: HubSettings): NodeJS.ProcessEnv {
|
|
22
23
|
const env: NodeJS.ProcessEnv = { ...process.env, CI: 'true' }
|
|
@@ -66,11 +67,11 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
66
67
|
const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
|
|
67
68
|
? 'en'
|
|
68
69
|
: 'zh'
|
|
69
|
-
//
|
|
70
|
+
// 代理规则与诊断/安装通道一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
70
71
|
const effectiveProxy = settings.proxy !== ''
|
|
71
72
|
? settings.proxy
|
|
72
73
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
73
|
-
// 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm
|
|
74
|
+
// 安装用的 npm 源:设置里显式选择优先;未配置时读本机 npm 配置(与诊断同一套规则)。
|
|
74
75
|
// 只有公共源允许失败后换源;自定义/内网源保持单一候选,避免私有包名泄露给公共源。
|
|
75
76
|
const registries = registryCandidates(
|
|
76
77
|
settings.npmRegistry,
|
|
@@ -78,7 +79,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
78
79
|
settings.registryFallback,
|
|
79
80
|
)
|
|
80
81
|
// 全局 npm 安装(`npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
81
|
-
// 包列表优先取 body.globalNpm
|
|
82
|
+
// 包列表优先取 body.globalNpm,取不到时再从 rawRepo 解析,保证直接调 API 传完整命令也能识别。
|
|
82
83
|
const bodyGlobalNpm = Array.isArray((body as { globalNpm?: unknown }).globalNpm)
|
|
83
84
|
? (body as { globalNpm: unknown[] }).globalNpm.filter((x): x is string => typeof x === 'string' && x !== '')
|
|
84
85
|
: []
|
|
@@ -89,7 +90,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
89
90
|
sendJson(response, 400, { error: 'unsupported install target' })
|
|
90
91
|
return
|
|
91
92
|
}
|
|
92
|
-
//
|
|
93
|
+
// 通道校验与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
|
|
93
94
|
if (source !== 'catalog' && !settings.enableNpmInstall) {
|
|
94
95
|
sendJson(response, 403, { error: 'npm installs are disabled by the security settings' })
|
|
95
96
|
return
|
|
@@ -122,8 +123,8 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
122
123
|
return
|
|
123
124
|
}
|
|
124
125
|
const target = rawRepo
|
|
125
|
-
//
|
|
126
|
-
//
|
|
126
|
+
// 锁定精确版本(Hub 自我更新):最新版可能仍在 pnpm 发布冷却期内,
|
|
127
|
+
// 客户端提示的是「已过冷却期」的版本,服务端必须按该版本安装而不是 @latest。
|
|
127
128
|
const pinVersion = typeof (body as { version?: unknown }).version === 'string'
|
|
128
129
|
? (body as { version: string }).version.trim()
|
|
129
130
|
: ''
|
|
@@ -181,6 +182,15 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
181
182
|
}
|
|
182
183
|
ordered = [chosen, ...registries.filter((r) => r !== chosen)]
|
|
183
184
|
}
|
|
185
|
+
// 新安装锁定「已过发布冷却期」的最高稳定版:pnpm 对发布还不满 24 小时的版本会不提示地回退,
|
|
186
|
+
// 锁定后装到的就是预期版本。更新已有目标时客户端已按冷却期筛选,继续走 @latest。
|
|
187
|
+
let installSpec = pinVersion !== '' ? `${target}@${pinVersion}` : undefined
|
|
188
|
+
if (installSpec === undefined && already === null) {
|
|
189
|
+
// 解析源与安装首选源一致,锁定的版本一定存在于真正会用的那个源;解析失败则不锁定,不阻断安装。
|
|
190
|
+
// 超时 5s:网络刚探测通过,卡住时也不能把安装请求拖到默认的 8s
|
|
191
|
+
const release = await lookupReleaseAge(target, ordered[0] ?? '', effectiveProxy, 5000)
|
|
192
|
+
if (release !== null && release.installable !== null) installSpec = `${target}@${release.installable.version}`
|
|
193
|
+
}
|
|
184
194
|
// 入队前复检:网络探测的 await 期间并发请求可能已通过上方检查,而此处到 startPluginMutation
|
|
185
195
|
// 之间无 await,JS 单线程保证同一目标至多一个任务,杜绝双击/并发双任务。
|
|
186
196
|
if (hasQueuedTarget(target)) {
|
|
@@ -190,7 +200,7 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
190
200
|
// 后台启动 CLI;客户端轮询 /status 获取进度
|
|
191
201
|
const task = startPluginMutation({
|
|
192
202
|
// mode=update(已安装目标的覆盖更新)以 update 动作入队:
|
|
193
|
-
// 日志按 update
|
|
203
|
+
// 日志按 update 类别写入磁盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
|
|
194
204
|
action: mode === 'update' ? 'update' : 'add',
|
|
195
205
|
profile,
|
|
196
206
|
target,
|
|
@@ -198,13 +208,13 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
198
208
|
displayTarget: githubRepoOf(displayRepo) ?? undefined,
|
|
199
209
|
// 覆盖更新:npm 命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
200
210
|
updateNpm: mode === 'update',
|
|
201
|
-
// 运行中 loader
|
|
211
|
+
// 运行中 loader:安装成功后把新插件热加载进 loader,非桌面端 + 纯服务端插件无需重启即生效
|
|
202
212
|
loader,
|
|
203
213
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
204
214
|
env: mutationEnv(settings),
|
|
205
215
|
// 候选源(首选在前):任务内遇到传输层失败时按顺序换源重试
|
|
206
216
|
registries: ordered,
|
|
207
|
-
installSpec
|
|
217
|
+
installSpec,
|
|
208
218
|
// 自定义安装入口渠道:日志/报错据此溯源
|
|
209
219
|
installChannel,
|
|
210
220
|
})
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* npm 包发布时间查询路由:按已安装插件的 npm 包名按需查询最新版本的发布时间,
|
|
7
|
-
* 供客户端判定 pnpm 24h
|
|
7
|
+
* 供客户端判定 pnpm 24h 发布冷却期(发布还不满 24 小时的版本不提示更新)。
|
|
8
8
|
*/
|
|
9
9
|
import { systemProxy } from '../services/probe.ts'
|
|
10
10
|
import { lookupReleaseAge, parseReleaseAgeRequest } from '../services/npm-release-age.ts'
|
|
11
|
+
import { userNpmRegistry } from '../services/install/registry-fallback.ts'
|
|
12
|
+
import { profileDirectory } from '../services/profile/profile.ts'
|
|
11
13
|
import { loadSettings } from '../services/settings.ts'
|
|
12
14
|
import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
13
15
|
|
|
@@ -25,13 +27,17 @@ export function registerNpmReleaseAgeRoutes(webServer: WebServerService, profile
|
|
|
25
27
|
return
|
|
26
28
|
}
|
|
27
29
|
const settings = loadSettings(profile)
|
|
28
|
-
//
|
|
30
|
+
// 代理规则与安装/目录一致:设置里的代理 → 系统代理 → 环境变量 → 直连
|
|
29
31
|
const proxy = settings.proxy !== ''
|
|
30
32
|
? settings.proxy
|
|
31
33
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
32
34
|
// 并发查询:单包各自带超时且失败即 null,整体不因任一包失败而失败
|
|
35
|
+
// 查询源与安装首选源同一套规则(设置 → 本机 npm 配置):安装锁定的版本必须来自真正要用的那个源
|
|
36
|
+
const registry = settings.npmRegistry !== ''
|
|
37
|
+
? settings.npmRegistry
|
|
38
|
+
: userNpmRegistry(profileDirectory(profile))
|
|
33
39
|
const results = await Promise.all(packages.map(async (name) =>
|
|
34
|
-
[name, await lookupReleaseAge(name,
|
|
40
|
+
[name, await lookupReleaseAge(name, registry, proxy)] as const))
|
|
35
41
|
sendJson(response, 200, { packages: Object.fromEntries(results) })
|
|
36
42
|
} catch (error) {
|
|
37
43
|
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
@@ -42,7 +42,7 @@ function findPids() {
|
|
|
42
42
|
|
|
43
43
|
const targets = findPids()
|
|
44
44
|
// 一个监听进程都没找到 = 端口探测本身就失败了(netstat 输出格式 / 本地化差异),
|
|
45
|
-
// 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE
|
|
45
|
+
// 旧宿主仍占着端口。此时再拉一个 dsh web 只会在日志里以 EADDRINUSE 无提示地退出,
|
|
46
46
|
// 用户却看到「重启完成」——不制造这种假象,留一行可诊断的记录后退出。
|
|
47
47
|
if (targets.length === 0) {
|
|
48
48
|
logLine('restart aborted: no listener found on port ' + port)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* 宿主与命令行工具的版本探测:dsh 版本同时用于诊断快照和「插件要求更高 DSH」判定,
|
|
7
|
-
* pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms
|
|
7
|
+
* pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时保护,拿不到一律 null,绝不挂起。
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync } from 'node:fs'
|
|
10
10
|
import { dirname, join, resolve } from 'node:path'
|
|
@@ -39,7 +39,7 @@ export function toolVersion(cmd: string, args: string[]): string | null {
|
|
|
39
39
|
let cachedDshVersion: string | null = null
|
|
40
40
|
|
|
41
41
|
/** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
|
|
42
|
-
* (dsh 不在 PATH
|
|
42
|
+
* (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
|
|
43
43
|
export function hostDshVersion(): string | null {
|
|
44
44
|
if (cachedDshVersion !== null) return cachedDshVersion
|
|
45
45
|
const viaCli = toolVersion('dsh', ['--version'])
|
|
@@ -36,14 +36,14 @@ export interface InstallTask {
|
|
|
36
36
|
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
|
|
37
37
|
timedOut: boolean
|
|
38
38
|
exitCode: number | null
|
|
39
|
-
/** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…`
|
|
39
|
+
/** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出;没有进度行时按阶段行估算 */
|
|
40
40
|
progress: number
|
|
41
41
|
/** 输出行最新在前(消费方展示尾部)。 */
|
|
42
42
|
lines: string[]
|
|
43
|
-
/**
|
|
44
|
-
* 无 loader
|
|
43
|
+
/** 完成后是否仍需宿主重启才生效:仅「热加载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
|
|
44
|
+
* 无 loader」等刷新解决不了的例外场景为 true(弹窗给「立即重启」);其余一律 false。 */
|
|
45
45
|
needsRestart: boolean
|
|
46
|
-
/**
|
|
46
|
+
/** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,仅剩客户端 UI 待页面重新加载,
|
|
47
47
|
* 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
|
|
48
48
|
needsReload?: boolean
|
|
49
49
|
/** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
|
|
@@ -79,7 +79,7 @@ export interface QueueItem {
|
|
|
79
79
|
installChannel?: 'npm'
|
|
80
80
|
/** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
|
|
81
81
|
registries?: string[]
|
|
82
|
-
/** 精确安装 spec(Hub
|
|
82
|
+
/** 精确安装 spec(Hub 自我更新时锁定版本) */
|
|
83
83
|
installSpec?: string
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -83,7 +83,7 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
|
|
86
|
-
* 与其它 GitHub
|
|
86
|
+
* 与其它 GitHub 访问同一套规则走 curl + 代理 env(Node 内置 https 读不到系统代理)。
|
|
87
87
|
* 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
|
|
88
88
|
*/
|
|
89
89
|
async function canonicalRepo(repo: string, proxy: string): Promise<string> {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
|
|
8
8
|
* 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
|
|
9
9
|
* 只允许读 profile 目录下的 pnpm.log(dsh 自己的 operation 日志),
|
|
10
|
-
*
|
|
10
|
+
* 防止插件把任意路径打印进日志造成越权读取;任何异常都降级为 null,
|
|
11
11
|
* 不改变原有失败处理。
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
@@ -53,7 +53,7 @@ export function operationLogPathOf(output: string): string | null {
|
|
|
53
53
|
/**
|
|
54
54
|
* 只允许读 profile 目录下的 pnpm.log(dsh 自己的操作日志):
|
|
55
55
|
* 绝对路径必须落在该目录内,相对路径按该目录解析。
|
|
56
|
-
* 未提供 profileDir
|
|
56
|
+
* 未提供 profileDir 时退回旧规则(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
|
|
57
57
|
*/
|
|
58
58
|
function allowedLogPath(raw: string, profileDir?: string): string | null {
|
|
59
59
|
const path = resolve(profileDir ?? '', raw)
|
|
@@ -96,7 +96,7 @@ export function hasSubstantiveOutput(output: string): boolean {
|
|
|
96
96
|
/**
|
|
97
97
|
* 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
|
|
98
98
|
* 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
|
|
99
|
-
* `.plugin-manager/logs/<operation>/pnpm.log
|
|
99
|
+
* `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
|
|
100
100
|
* 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
|
|
101
101
|
*/
|
|
102
102
|
export function createOperationLogTap(profileDir: string): { stop: () => string } {
|
|
@@ -10,7 +10,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
10
10
|
export const GIT_MISSING_DETAIL = '[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)'
|
|
11
11
|
|
|
12
12
|
/** 命令存在性探测:which/where 找 PATH 里的命令。
|
|
13
|
-
* 探测自身异常(which/where 不可用等)返回 true
|
|
13
|
+
* 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
|
|
14
14
|
export function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean {
|
|
15
15
|
try {
|
|
16
16
|
const probe = process.platform === 'win32' ? 'where' : 'which'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* npm
|
|
3
|
-
*
|
|
2
|
+
* npm 换源:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
|
|
3
|
+
* 自动换源重试;自定义/内网源不换源,避免私有包名泄露给公共源。
|
|
4
4
|
*/
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ export function isPublicRegistry(value: string): boolean {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
|
|
40
|
-
* 只有公共源才追加后备(最多 3
|
|
40
|
+
* 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
|
|
41
41
|
*/
|
|
42
42
|
export function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[] {
|
|
43
43
|
const base = normalizeRegistry(configured) || normalizeRegistry(userRegistry) || PUBLIC_REGISTRIES[0]!
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 预构建 release 直链的仓库身份解析。部分插件(如 loopx)当初以 GitHub release 的 .tgz
|
|
7
7
|
* 安装过,仓库 HEAD 反而是 monorepo 根、走 git 直装会因缺入口文件失败;老安装记录里留的
|
|
8
8
|
* 就是这种 release 直链。这里把「历史安装目标」还原成所属仓库身份,供已装态读回、队列
|
|
9
|
-
*
|
|
9
|
+
* 去重与展示复用(单一事实来源,避免两端规则漂移)。
|
|
10
10
|
*
|
|
11
11
|
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
12
12
|
* 输入是记录/展示数据、绝非 shell 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
|
|
@@ -21,7 +21,7 @@ export interface GithubReleaseTarget {
|
|
|
21
21
|
repo: string
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf
|
|
24
|
+
/** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同一套规则)。 */
|
|
25
25
|
const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i
|
|
26
26
|
|
|
27
27
|
/** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
|