dsh-plugin 2.2.3 → 2.2.5
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/client/client.js +382 -322
- package/client/client.js.map +1 -1
- package/lib/services/install/task-queue.js +10 -9
- package/lib/types/services/install/task-queue.d.ts +2 -1
- package/package.json +1 -1
- package/src/client/components/catalog/CatalogList.tsx +4 -1
- package/src/client/components/catalog/PluginCard.tsx +10 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +5 -2
- package/src/client/components/views/InstalledView.tsx +5 -1
- package/src/client/components/views/MarketView.tsx +1 -0
- package/src/client/hooks/useCatalog.ts +35 -11
- package/src/client/locales.ts +6 -0
- package/src/client/logic/format.ts +10 -0
- package/src/client/logic/installed.ts +4 -0
- package/src/client/logic/release-age.ts +26 -5
- package/src/server/services/install/task-queue.ts +10 -10
|
@@ -18,7 +18,7 @@ import { backfillOperationLog, createOperationLogTap, hasSubstantiveOutput, oper
|
|
|
18
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
19
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
20
20
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
21
|
-
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
|
|
21
|
+
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory, validPackageName } from '../profile/profile.js';
|
|
22
22
|
import { appendLog } from '../log.js';
|
|
23
23
|
import { commandOnPath, gitMissingDetail } from './preflight.js';
|
|
24
24
|
import { isTransportFailure, registryHost } from './registry-fallback.js';
|
|
@@ -200,14 +200,17 @@ export function desktopCliBootstrap(entry) {
|
|
|
200
200
|
}
|
|
201
201
|
/**
|
|
202
202
|
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
203
|
-
*
|
|
203
|
+
* 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
|
|
204
204
|
*/
|
|
205
205
|
export function npmInstallTargetOf(target, opts) {
|
|
206
206
|
if (opts.global || githubRepoOf(target) !== null)
|
|
207
207
|
return target;
|
|
208
208
|
if (opts.pinned !== undefined && opts.pinned !== '')
|
|
209
209
|
return opts.pinned;
|
|
210
|
-
|
|
210
|
+
// 裸包名才补 @latest:pnpm 对已存在的精确 spec 是幂等的,不显式要求就会留用旧版本
|
|
211
|
+
if (!(opts.update || opts.add) || !validPackageName(target))
|
|
212
|
+
return target;
|
|
213
|
+
return `${target}@latest`;
|
|
211
214
|
}
|
|
212
215
|
function stopChild(child) {
|
|
213
216
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
@@ -372,15 +375,13 @@ function spawnMutation(options) {
|
|
|
372
375
|
&& process.versions.electron !== undefined) {
|
|
373
376
|
pushLine(task, '[desktop-cli-fallback] running inside Electron but no desktop CLI bootstrap was found — falling back to dsh on PATH (a desktop-managed profile will be rejected)');
|
|
374
377
|
}
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// update 动作实际执行 `add <target>@latest`(pnpm 对已存在依赖带显式版本即原位覆盖升级),
|
|
379
|
-
// CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
|
|
378
|
+
// 安装/更新 npm 包都显式 @latest:pnpm add <pkg>(无版本)对已存在的精确 spec 是幂等的,
|
|
379
|
+
// 会原样留用旧版本(更新不生效,普通安装也可能装回不支持当前宿主的旧包)。
|
|
380
|
+
// update 动作同样执行 `add <target>@latest`,CLI 动词透传 add,不与官方 update 动词分叉。
|
|
380
381
|
const isUpdate = action === 'update' || updateNpm === true;
|
|
381
382
|
const cliAction = action === 'update' ? 'add' : action;
|
|
382
383
|
// 钉住版本优先于 @latest:提示的是「已过门禁」的版本,装 latest 会被 pnpm 拒绝
|
|
383
|
-
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec });
|
|
384
|
+
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec, add: cliAction === 'add' });
|
|
384
385
|
const args = isGlobalNpm
|
|
385
386
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
386
387
|
: [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget];
|
|
@@ -40,12 +40,13 @@ export declare function cancelTask(id: number): boolean;
|
|
|
40
40
|
export declare function desktopCliBootstrap(entry: string): string | null;
|
|
41
41
|
/**
|
|
42
42
|
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
43
|
-
*
|
|
43
|
+
* 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
|
|
44
44
|
*/
|
|
45
45
|
export declare function npmInstallTargetOf(target: string, opts: {
|
|
46
46
|
update: boolean;
|
|
47
47
|
global: boolean;
|
|
48
48
|
pinned?: string;
|
|
49
|
+
add?: boolean;
|
|
49
50
|
}): string;
|
|
50
51
|
/**
|
|
51
52
|
* 插件变更入队并返回其任务。任务严格串行:队列 worker 只在上一个结束后才启动下一个。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
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",
|
|
@@ -13,7 +13,7 @@ import { siteUrl } from '../../logic/urls.ts'
|
|
|
13
13
|
import { useIncrementalList } from '../../hooks/useIncrementalList.ts'
|
|
14
14
|
import { PluginCard } from './PluginCard.tsx'
|
|
15
15
|
|
|
16
|
-
export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, forceReload, category, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
|
|
16
|
+
export function CatalogList({ plugins, failed, visible, total, t, langPath, reload, forceReload, category, copied, installedName, installedVersion, hasUpdate, updatePendingUntil, langKey, onInstall, onUninstall, hasProxy, onOpenDiagnostics }: {
|
|
17
17
|
plugins: HubPlugin[] | null
|
|
18
18
|
failed: boolean
|
|
19
19
|
visible: HubPlugin[]
|
|
@@ -28,6 +28,8 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
|
|
|
28
28
|
installedName: (p: HubPlugin) => string | null
|
|
29
29
|
installedVersion: (p: HubPlugin) => string | null
|
|
30
30
|
hasUpdate: (p: HubPlugin) => boolean
|
|
31
|
+
/** 更新被供应链门禁暂缓时的可重试时刻(毫秒);无则 null */
|
|
32
|
+
updatePendingUntil: (p: HubPlugin) => number | null
|
|
31
33
|
langKey: LocaleId
|
|
32
34
|
onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
|
|
33
35
|
onUninstall: (p: HubPlugin) => void
|
|
@@ -72,6 +74,7 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
|
|
|
72
74
|
installedName,
|
|
73
75
|
installedVersion,
|
|
74
76
|
hasUpdate,
|
|
77
|
+
updatePendingUntil,
|
|
75
78
|
t,
|
|
76
79
|
langKey,
|
|
77
80
|
langPath,
|
|
@@ -11,16 +11,18 @@ import styles from '../../styles/List.module.css'
|
|
|
11
11
|
import type { HubPlugin, LocaleId, Translate } from '../../types.ts'
|
|
12
12
|
import { CATEGORY_LABELS, categoryLabel } from '../../logic/constants.ts'
|
|
13
13
|
import { pluginDetailUrl } from '../../logic/urls.ts'
|
|
14
|
-
import { fmtStars, relTime } from '../../logic/format.ts'
|
|
14
|
+
import { fmtStars, clockTime, relTime } from '../../logic/format.ts'
|
|
15
15
|
|
|
16
|
-
export function PluginCard({ plugin: p, copied, installedName, installedVersion, hasUpdate, t, langKey, langPath, onInstall, onUninstall }: {
|
|
16
|
+
export function PluginCard({ plugin: p, copied, installedName, installedVersion, hasUpdate, updatePendingUntil, t, langKey, langPath, onInstall, onUninstall }: {
|
|
17
17
|
plugin: HubPlugin
|
|
18
18
|
copied: string | null
|
|
19
19
|
installedName: (p: HubPlugin) => string | null
|
|
20
20
|
/** 已安装版本号(安装时从目录信号记录;无 release 的仓库为空,无版本号可展示) */
|
|
21
21
|
installedVersion: (p: HubPlugin) => string | null
|
|
22
|
-
/**
|
|
22
|
+
/** 已安装且目录有更新(有版本比版本、无版本比仓库更新时间);门禁复核未就绪时为 false */
|
|
23
23
|
hasUpdate: (p: HubPlugin) => boolean
|
|
24
|
+
/** 更新被供应链门禁暂缓时的可重试时刻(毫秒);无则 null */
|
|
25
|
+
updatePendingUntil: (p: HubPlugin) => number | null
|
|
24
26
|
t: Translate
|
|
25
27
|
langKey: LocaleId
|
|
26
28
|
langPath: string
|
|
@@ -33,6 +35,8 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
|
|
|
33
35
|
const pkg = installedName(p)
|
|
34
36
|
const isInstalled = pkg !== null
|
|
35
37
|
const update = hasUpdate(p)
|
|
38
|
+
// 门禁暂缓:更新确实存在但现在装不上,给确切时刻而不是让徽标静默消失
|
|
39
|
+
const pendingUntil = isInstalled && !update ? updatePendingUntil(p) : null
|
|
36
40
|
// 版本号展示:已安装 → 当前已装版本(最能反映用户状态);未安装 → 目录最新版本
|
|
37
41
|
const versionShown = isInstalled ? installedVersion(p) : p.version
|
|
38
42
|
|
|
@@ -44,6 +48,9 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
|
|
|
44
48
|
versionShown ? h('span', { className: styles.versionBadge, title: t('version') }, versionShown) : null,
|
|
45
49
|
// 已安装且有更新:醒目提示(有 release 比版本号、无 release 比仓库最近更新时间)
|
|
46
50
|
update ? h('span', { className: styles.updateBadge, title: t('updateAvailableHint') }, t('updateAvailable')) : null,
|
|
51
|
+
pendingUntil !== null
|
|
52
|
+
? h('span', { className: styles.updateBadge, title: t('updatePendingHint') }, t('updatePending', { time: clockTime(pendingUntil) }))
|
|
53
|
+
: null,
|
|
47
54
|
p.category ? h('span', { className: styles.categoryBadge }, categoryLabel(CATEGORY_LABELS, p.category, langKey)) : null,
|
|
48
55
|
p.compatibility?.status === 'verified'
|
|
49
56
|
? h('span', { className: styles.verified }, t('verified'))
|
|
@@ -14,7 +14,7 @@ import type { LocaleId, Translate } from '../../types.ts'
|
|
|
14
14
|
import type { InstalledItem } from '../../logic/installed.ts'
|
|
15
15
|
import { CATEGORY_LABELS, categoryLabel } from '../../logic/constants.ts'
|
|
16
16
|
import { pluginDetailUrl } from '../../logic/urls.ts'
|
|
17
|
-
import { fmtStars } from '../../logic/format.ts'
|
|
17
|
+
import { clockTime, fmtStars } from '../../logic/format.ts'
|
|
18
18
|
import { CloseIcon, CopyIcon, FolderIcon, LinkIcon } from '../ui/icons.tsx'
|
|
19
19
|
|
|
20
20
|
/** ISO 安装时间 → 本地可读;非法值返回 null(行内不显示)。 */
|
|
@@ -69,7 +69,10 @@ export function InstalledDetailModal({ item, t, lang, langPath, onClose, onUpdat
|
|
|
69
69
|
item.installedVersion && item.catalogVersion
|
|
70
70
|
? t('versionUpHint', { from: item.installedVersion, to: item.catalogVersion })
|
|
71
71
|
: t('updateAvailableHint'))
|
|
72
|
-
: null
|
|
72
|
+
: item.pendingUntil != null
|
|
73
|
+
// 门禁暂缓:给确切可重试时刻,而不是让「有更新」静默消失
|
|
74
|
+
? h('div', { className: styles.detailUpdateHint }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
75
|
+
: null,
|
|
73
76
|
// 详情信息网格:分类 / 状态 / 版本 / 收录入口 / 包名 / 统计 / 安装路径 / 安装时间 / 最后更新
|
|
74
77
|
h('div', { className: styles.detailGrid },
|
|
75
78
|
p?.category
|
|
@@ -17,6 +17,7 @@ import styles from '../../styles/InstalledView.module.css'
|
|
|
17
17
|
import type { LocaleId, Translate } from '../../types.ts'
|
|
18
18
|
import type { InstalledItem } from '../../logic/installed.ts'
|
|
19
19
|
import { CATEGORY_LABELS, categoryLabel } from '../../logic/constants.ts'
|
|
20
|
+
import { clockTime } from '../../logic/format.ts'
|
|
20
21
|
import { SortArrowIcon } from '../ui/icons.tsx'
|
|
21
22
|
|
|
22
23
|
const INSTALLED_SORTS = ['sortName', 'sortInstalledAt', 'sortStars', 'sortForks'] as const
|
|
@@ -108,7 +109,10 @@ function InstalledRow({ item, t, langKey, canReveal, revealLabel, pendingRestart
|
|
|
108
109
|
: null,
|
|
109
110
|
item.hasUpdate
|
|
110
111
|
? h('span', { className: styles.updateBadge, title: t('updateAvailableHint') }, t('updateAvailable'))
|
|
111
|
-
: null
|
|
112
|
+
: item.pendingUntil != null
|
|
113
|
+
// 门禁暂缓:更新存在但还装不上,给确切时刻,避免看起来「已是最新」
|
|
114
|
+
? h('span', { className: styles.updateBadge, title: t('updatePendingHint') }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
115
|
+
: null,
|
|
112
116
|
),
|
|
113
117
|
// 来源标签列(行尾、操作按钮前,右对齐固定):目录插件 →「插件市场」(品牌色),
|
|
114
118
|
// 命令行安装 →「手动安装」(琥珀色);两词 4 字等宽,所有条目标签同列对齐
|
|
@@ -59,6 +59,7 @@ export function MarketView({ catalog, t, langPath, langKey, copied, resultText,
|
|
|
59
59
|
installedName: catalog.installedName,
|
|
60
60
|
installedVersion: catalog.installedVersion,
|
|
61
61
|
hasUpdate: catalog.hasUpdate,
|
|
62
|
+
updatePendingUntil: catalog.updatePendingUntil,
|
|
62
63
|
langKey,
|
|
63
64
|
onInstall,
|
|
64
65
|
onUninstall,
|
|
@@ -16,7 +16,7 @@ import type { SortKey } from '../logic/constants.ts'
|
|
|
16
16
|
import { fetchCatalog, fetchStats } from '../data/catalog.ts'
|
|
17
17
|
import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
|
|
18
18
|
import { fetchInstalled, fetchNpmReleaseAge, type NpmReleaseInfo } from '../data/host.ts'
|
|
19
|
-
import { isReleaseTooFresh,
|
|
19
|
+
import { isReleaseTooFresh, pickHubUpdate, updateDecision } from '../logic/release-age.ts'
|
|
20
20
|
import {
|
|
21
21
|
hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf, npmPackageOf,
|
|
22
22
|
} from '../logic/installed.ts'
|
|
@@ -248,16 +248,24 @@ export function useCatalog(lang: LocaleId) {
|
|
|
248
248
|
* - npm 上最新版并不高于已装版本 —— 更新走的是 `pkg@latest`,此时无论目录标到哪个 tag,
|
|
249
249
|
* 都不可能装上(作者可能只打了 GitHub release 没发 npm),提示只会变成永远点不掉的循环。
|
|
250
250
|
*/
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
if (!releaseAgeReady) return
|
|
251
|
+
const updateDecisions = useMemo(() => {
|
|
252
|
+
const map = new Map<string, { offered: boolean; pendingUntil: number | null }>()
|
|
253
|
+
if (!releaseAgeReady) return map
|
|
254
254
|
const now = Date.now()
|
|
255
255
|
for (const pkg of releaseAgeCandidates) {
|
|
256
|
-
|
|
256
|
+
map.set(pkg, updateDecision(releaseAge.releases[pkg] ?? null, installedByPackage.get(pkg) ?? null, now))
|
|
257
257
|
}
|
|
258
|
-
return
|
|
258
|
+
return map
|
|
259
259
|
}, [releaseAgeReady, releaseAgeCandidates, releaseAge.releases, installedByPackage])
|
|
260
260
|
|
|
261
|
+
const updateBlocked = useMemo(() => {
|
|
262
|
+
const blocked = new Set<string>()
|
|
263
|
+
for (const [pkg, decision] of updateDecisions) {
|
|
264
|
+
if (!decision.offered) blocked.add(pkg)
|
|
265
|
+
}
|
|
266
|
+
return blocked
|
|
267
|
+
}, [updateDecisions])
|
|
268
|
+
|
|
261
269
|
/**
|
|
262
270
|
* 是否有更新:仅对已安装插件有意义。
|
|
263
271
|
* 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
|
|
@@ -267,18 +275,33 @@ export function useCatalog(lang: LocaleId) {
|
|
|
267
275
|
const hasUpdate = (p: HubPlugin): boolean => {
|
|
268
276
|
if (!hasUpdateOf(p, installed, versions)) return false
|
|
269
277
|
const pkg = npmPackageOf(p, versions)
|
|
270
|
-
|
|
278
|
+
if (pkg === null) return true
|
|
279
|
+
// 复核没回来时先不亮:否则会出现「先亮有更新、几秒后又撤掉」
|
|
280
|
+
return releaseAgeReady && !updateBlocked.has(pkg)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 更新被门禁暂缓时的可重试时刻(毫秒);未安装/无更新/非门禁原因 → null。 */
|
|
284
|
+
const updatePendingUntil = (p: HubPlugin): number | null => {
|
|
285
|
+
if (!hasUpdateOf(p, installed, versions)) return null
|
|
286
|
+
const pkg = npmPackageOf(p, versions)
|
|
287
|
+
if (pkg === null) return null
|
|
288
|
+
return updateDecisions.get(pkg)?.pendingUntil ?? null
|
|
271
289
|
}
|
|
272
290
|
|
|
273
|
-
/** 已安装项统一列表(目录元数据 +
|
|
291
|
+
/** 已安装项统一列表(目录元数据 + 运行时信息合并):校验未就绪/被拦下的去掉更新标记、
|
|
292
|
+
* 门禁暂缓的带上可重试时刻,驱动「已安装」tab。 */
|
|
274
293
|
const installedItems = useMemo<InstalledItem[]>(() => {
|
|
275
|
-
if (updateBlocked.size === 0) return rawInstalledItems
|
|
276
294
|
return rawInstalledItems.map((item) => {
|
|
277
295
|
if (!item.hasUpdate || !item.plugin) return item
|
|
278
296
|
const pkg = npmPackageOf(item.plugin, versions)
|
|
279
|
-
|
|
297
|
+
if (pkg === null) return item
|
|
298
|
+
if (!releaseAgeReady) return { ...item, hasUpdate: false }
|
|
299
|
+
const decision = updateDecisions.get(pkg)
|
|
300
|
+
return decision === undefined || decision.offered
|
|
301
|
+
? item
|
|
302
|
+
: { ...item, hasUpdate: false, pendingUntil: decision.pendingUntil }
|
|
280
303
|
})
|
|
281
|
-
}, [rawInstalledItems,
|
|
304
|
+
}, [rawInstalledItems, releaseAgeReady, updateDecisions, versions])
|
|
282
305
|
|
|
283
306
|
/**
|
|
284
307
|
* Hub 自身更新状态:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
|
|
@@ -353,6 +376,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
353
376
|
installedName,
|
|
354
377
|
installedVersion,
|
|
355
378
|
hasUpdate,
|
|
379
|
+
updatePendingUntil,
|
|
356
380
|
hubHasUpdate,
|
|
357
381
|
hubUpdateState,
|
|
358
382
|
hubUpdateTarget,
|
package/src/client/locales.ts
CHANGED
|
@@ -482,6 +482,10 @@ export const zh = {
|
|
|
482
482
|
update: '可更新',
|
|
483
483
|
updateAvailable: '有更新',
|
|
484
484
|
updateAvailableHint: '检测到更新:有版本号的仓库比对版本号,无版本号的仓库比对仓库最近更新时间',
|
|
485
|
+
// 门禁暂缓:新版本发布未满 24 小时,此时安装会被 pnpm 静默回退(或直接拒绝),
|
|
486
|
+
// 给出确切可重试时刻,而不是让徽标静默消失、让用户以为已经是最新
|
|
487
|
+
updatePending: '暂缓到 {time} 后可更新',
|
|
488
|
+
updatePendingHint: '新版本发布未满 24 小时,pnpm 供应链门禁会拦下;到时间后再更新即可',
|
|
485
489
|
hubUpdateHint: '检测到 DSH Plugin Hub 新版本,点击查看更新内容并更新',
|
|
486
490
|
hubUpdateTitle: 'DSH Plugin Hub 有新版本',
|
|
487
491
|
hubUpdateDesc: 'v{version} 已发布,点击下方按钮即可更新,完成后需重启宿主生效。',
|
|
@@ -977,6 +981,8 @@ export const en = {
|
|
|
977
981
|
update: 'Update',
|
|
978
982
|
updateAvailable: 'Update available',
|
|
979
983
|
updateAvailableHint: 'An update was detected: repos with a release are compared by tag, repos without one by last push time',
|
|
984
|
+
updatePending: 'Update available after {time}',
|
|
985
|
+
updatePendingHint: 'The new release is less than 24 hours old, so the pnpm supply-chain hold rejects it until then',
|
|
980
986
|
hubUpdateHint: 'A new DSH Plugin Hub version is available — click to view the changelog and update',
|
|
981
987
|
hubUpdateTitle: 'A new DSH Plugin Hub version is available',
|
|
982
988
|
hubUpdateDesc: 'v{version} has been released — click below to update, then restart the host to apply it.',
|
|
@@ -24,3 +24,13 @@ export function relTime(iso: string | undefined, t: Translate): string {
|
|
|
24
24
|
if (days < 365) return t('monthsAgo', { months: Math.floor(days / 30) })
|
|
25
25
|
return t('yearsAgo', { years: Math.floor(days / 365) })
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/** 具体时刻:当天只给「时:分」,跨天再补「月/日」,用于「暂缓到何时可更新」。 */
|
|
29
|
+
export function clockTime(ms: number): string {
|
|
30
|
+
const at = new Date(ms)
|
|
31
|
+
const now = new Date()
|
|
32
|
+
const hh = String(at.getHours()).padStart(2, '0')
|
|
33
|
+
const mm = String(at.getMinutes()).padStart(2, '0')
|
|
34
|
+
const sameDay = at.getFullYear() === now.getFullYear() && at.getMonth() === now.getMonth() && at.getDate() === now.getDate()
|
|
35
|
+
return sameDay ? `${hh}:${mm}` : `${at.getMonth() + 1}/${at.getDate()} ${hh}:${mm}`
|
|
36
|
+
}
|
|
@@ -42,6 +42,8 @@ export interface InstalledItem {
|
|
|
42
42
|
catalogVersion: string | null
|
|
43
43
|
/** 是否有可用更新(有版本比版本,无版本比仓库更新时间) */
|
|
44
44
|
hasUpdate: boolean
|
|
45
|
+
/** 更新被供应链门禁暂缓时的可重试时刻(毫秒);无更新/非门禁原因 → null */
|
|
46
|
+
pendingUntil: number | null
|
|
45
47
|
/** 是否已加载进运行中 loader(官方 ctx.loader 对账);false = 已安装但未挂载(装完未重启) */
|
|
46
48
|
loaded: boolean
|
|
47
49
|
/** 是否真正的 dsh 插件(包内声明 dsh 配置 / 在 profile bundles 清单):
|
|
@@ -230,6 +232,7 @@ export function installedItemsOf(
|
|
|
230
232
|
installedAt: rec?.installedAt ?? null,
|
|
231
233
|
catalogVersion: p.version ?? null,
|
|
232
234
|
hasUpdate: hasUpdateOf(p, installed, versions),
|
|
235
|
+
pendingUntil: null,
|
|
233
236
|
loaded: loadedNames?.includes(name) ?? false,
|
|
234
237
|
dshCapable: dshCapableNames?.includes(name) ?? false,
|
|
235
238
|
})
|
|
@@ -254,6 +257,7 @@ export function installedItemsOf(
|
|
|
254
257
|
catalogVersion: null,
|
|
255
258
|
// 目录外没有目录信号可比,无法判断是否有更新
|
|
256
259
|
hasUpdate: false,
|
|
260
|
+
pendingUntil: null,
|
|
257
261
|
loaded: loadedNames?.includes(name) ?? false,
|
|
258
262
|
dshCapable: dshCapableNames?.includes(name) ?? false,
|
|
259
263
|
})
|
|
@@ -61,23 +61,44 @@ export function compareVersions(a: string | null | undefined, b: string | null |
|
|
|
61
61
|
return 0
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** 「可更新」判定结果:是否提示,以及被供应链门禁拦下时的到期时刻。 */
|
|
65
|
+
export interface UpdateDecision {
|
|
66
|
+
offered: boolean
|
|
67
|
+
/** 发布未满门槛 → 可重试时刻(毫秒);非门禁原因(npm 上没有更高版本)→ null */
|
|
68
|
+
pendingUntil: number | null
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
/**
|
|
65
72
|
* 是否值得提示「可更新」:npm 上最新可安装版本确实比已装版本新,且已过供应链门禁。
|
|
66
73
|
*
|
|
67
74
|
* 更新命令执行的是 `pkg@latest`,所以「目录里标的版本」不是最终能装上的版本:
|
|
68
75
|
* 作者可能只打了 GitHub release 没发 npm —— 此时提示会变成永远点不掉的循环。
|
|
69
|
-
* 任一信息缺失(查不到发布信息 / 没有已装版本记录)返回
|
|
76
|
+
* 任一信息缺失(查不到发布信息 / 没有已装版本记录)返回 offered,交回调用方原有判定:
|
|
70
77
|
* 宁可提示,也不因为一次查询失败把真实更新吃掉。
|
|
71
78
|
*/
|
|
79
|
+
export function updateDecision(
|
|
80
|
+
release: { version: string; publishedAt: string } | null,
|
|
81
|
+
installedVersion: string | null,
|
|
82
|
+
now: number,
|
|
83
|
+
): UpdateDecision {
|
|
84
|
+
if (release === null) return { offered: true, pendingUntil: null }
|
|
85
|
+
if (isReleaseTooFresh(release.publishedAt, now)) {
|
|
86
|
+
const ms = Date.parse(release.publishedAt)
|
|
87
|
+
return { offered: false, pendingUntil: Number.isNaN(ms) ? null : ms + MIN_RELEASE_AGE_MS }
|
|
88
|
+
}
|
|
89
|
+
if (installedVersion !== null && compareVersions(release.version, installedVersion) <= 0) {
|
|
90
|
+
return { offered: false, pendingUntil: null }
|
|
91
|
+
}
|
|
92
|
+
return { offered: true, pendingUntil: null }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 是否提示「可更新」(只要布尔结果的场景)。 */
|
|
72
96
|
export function isUpdateOffered(
|
|
73
97
|
release: { version: string; publishedAt: string } | null,
|
|
74
98
|
installedVersion: string | null,
|
|
75
99
|
now: number,
|
|
76
100
|
): boolean {
|
|
77
|
-
|
|
78
|
-
if (isReleaseTooFresh(release.publishedAt, now)) return false
|
|
79
|
-
if (installedVersion !== null && compareVersions(release.version, installedVersion) <= 0) return false
|
|
80
|
-
return true
|
|
101
|
+
return updateDecision(release, installedVersion, now).offered
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
/**
|
|
@@ -20,7 +20,7 @@ import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
|
20
20
|
import type { LoaderHandle } from '../loader.ts'
|
|
21
21
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.ts'
|
|
22
22
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.ts'
|
|
23
|
-
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.ts'
|
|
23
|
+
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory, validPackageName } from '../profile/profile.ts'
|
|
24
24
|
import { appendLog } from '../log.ts'
|
|
25
25
|
import { commandOnPath, gitMissingDetail } from './preflight.ts'
|
|
26
26
|
import { isTransportFailure, registryHost } from './registry-fallback.ts'
|
|
@@ -218,12 +218,14 @@ export function desktopCliBootstrap(entry: string): string | null {
|
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
221
|
-
*
|
|
221
|
+
* 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
|
|
222
222
|
*/
|
|
223
|
-
export function npmInstallTargetOf(target: string, opts: { update: boolean; global: boolean; pinned?: string }): string {
|
|
223
|
+
export function npmInstallTargetOf(target: string, opts: { update: boolean; global: boolean; pinned?: string; add?: boolean }): string {
|
|
224
224
|
if (opts.global || githubRepoOf(target) !== null) return target
|
|
225
225
|
if (opts.pinned !== undefined && opts.pinned !== '') return opts.pinned
|
|
226
|
-
|
|
226
|
+
// 裸包名才补 @latest:pnpm 对已存在的精确 spec 是幂等的,不显式要求就会留用旧版本
|
|
227
|
+
if (!(opts.update || opts.add) || !validPackageName(target)) return target
|
|
228
|
+
return `${target}@latest`
|
|
227
229
|
}
|
|
228
230
|
|
|
229
231
|
function stopChild(child: ReturnType<typeof spawn>): void {
|
|
@@ -432,15 +434,13 @@ function spawnMutation(options: {
|
|
|
432
434
|
&& (process.versions as Record<string, string | undefined>).electron !== undefined) {
|
|
433
435
|
pushLine(task, '[desktop-cli-fallback] running inside Electron but no desktop CLI bootstrap was found — falling back to dsh on PATH (a desktop-managed profile will be rejected)')
|
|
434
436
|
}
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// update 动作实际执行 `add <target>@latest`(pnpm 对已存在依赖带显式版本即原位覆盖升级),
|
|
439
|
-
// CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
|
|
437
|
+
// 安装/更新 npm 包都显式 @latest:pnpm add <pkg>(无版本)对已存在的精确 spec 是幂等的,
|
|
438
|
+
// 会原样留用旧版本(更新不生效,普通安装也可能装回不支持当前宿主的旧包)。
|
|
439
|
+
// update 动作同样执行 `add <target>@latest`,CLI 动词透传 add,不与官方 update 动词分叉。
|
|
440
440
|
const isUpdate = action === 'update' || updateNpm === true
|
|
441
441
|
const cliAction = action === 'update' ? 'add' : action
|
|
442
442
|
// 钉住版本优先于 @latest:提示的是「已过门禁」的版本,装 latest 会被 pnpm 拒绝
|
|
443
|
-
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec })
|
|
443
|
+
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec, add: cliAction === 'add' })
|
|
444
444
|
const args = isGlobalNpm
|
|
445
445
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
446
446
|
: [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget]
|