@gehennawu/dsh-service 0.13.1 → 0.13.2
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.js +60 -3
- package/index.js +168 -11
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -120,6 +120,22 @@ window.__ModuleLoader__.load({
|
|
|
120
120
|
'update.upgradeError': '插件升级失败',
|
|
121
121
|
'update.upgradeErrorDetail': '插件升级失败({detail})',
|
|
122
122
|
'update.upgradeSuccess': '升级成功,服务重启中…',
|
|
123
|
+
'update.guardActiveWork': '有活动中的会话或后台任务,请处理完毕后再升级',
|
|
124
|
+
'update.guardLinkInstall': '插件通过 link: 安装(开发模式),不能从 registry 一键升级;请更新源码仓库后重启',
|
|
125
|
+
'update.guardFileInstall': '插件通过 file: 安装,不能从 registry 一键升级',
|
|
126
|
+
'update.guardNoNewer': 'registry 最新版不高于当前版本,已拒绝可能回退的升级',
|
|
127
|
+
'update.guardNoProfile': '没有找到安装了本插件的 profile,无法定位升级目标',
|
|
128
|
+
'update.guardAmbiguous': '多个 profile 都安装了本插件且无法确定当前加载的副本,已中止',
|
|
129
|
+
'update.guardStale': '命令报告成功但安装版本没有变化(可能被 pnpm 安全等待期拦下),保持当前版本不重启',
|
|
130
|
+
'update.guardUnreadable': '命令成功但无法确认安装后的版本,已中止重启',
|
|
131
|
+
'update.failPnpmMissing': '找不到 pnpm,无法执行升级;请先安装 pnpm 再重试',
|
|
132
|
+
'update.failNetwork': '拉取依赖时网络临时失败,请稍后重试',
|
|
133
|
+
'update.failFetchTimeout': '下载超时(网络较慢或安装包较大),请稍后重试',
|
|
134
|
+
'update.failReleaseAge': '新版本被 pnpm 安全等待期拦截,已自动放行重试仍失败',
|
|
135
|
+
'update.failHoist': 'profile 的 node_modules 由不同版本的 pnpm 创建,已重建重试仍失败',
|
|
136
|
+
'update.failAddingToRoot': 'pnpm 拒绝在 workspace 根目录安装(缺少 -w)',
|
|
137
|
+
'update.failNotWorkspace': 'profile 不是 pnpm workspace 却传入了 -w',
|
|
138
|
+
'update.failIgnoredBuilds': '依赖构建脚本被 pnpm 默认拦截,无法完成升级',
|
|
123
139
|
'restart.title': '服务重启',
|
|
124
140
|
'restart.description': '重启 dsh web 进程。运行中的工作会中断,持久化会话可恢复。也可在对话中输入 /restart。',
|
|
125
141
|
'restart.button': '重启 dsh web',
|
|
@@ -306,6 +322,22 @@ window.__ModuleLoader__.load({
|
|
|
306
322
|
'update.upgradeError': 'Plugin upgrade failed',
|
|
307
323
|
'update.upgradeErrorDetail': 'Plugin upgrade failed ({detail})',
|
|
308
324
|
'update.upgradeSuccess': 'Upgrade successful, restarting…',
|
|
325
|
+
'update.guardActiveWork': 'Active sessions or background tasks are running; resolve them before upgrading',
|
|
326
|
+
'update.guardLinkInstall': 'Installed via link: (development mode) — cannot one-click upgrade from the registry; update the source checkout and restart',
|
|
327
|
+
'update.guardFileInstall': 'Installed via file: — cannot one-click upgrade from the registry',
|
|
328
|
+
'update.guardNoNewer': 'The registry latest is not higher than the current version; refused a possible downgrade',
|
|
329
|
+
'update.guardNoProfile': 'No profile with this plugin installed was found to target the upgrade at',
|
|
330
|
+
'update.guardAmbiguous': 'Several profiles install this plugin and the loaded copy could not be determined; aborted',
|
|
331
|
+
'update.guardStale': 'The command reported success but the installed version did not change (pnpm safety wait likely blocked it); keeping the current version without restarting',
|
|
332
|
+
'update.guardUnreadable': 'The command succeeded but the installed version could not be confirmed; the restart was cancelled',
|
|
333
|
+
'update.failPnpmMissing': 'pnpm was not found, so the upgrade cannot run; install pnpm first and retry',
|
|
334
|
+
'update.failNetwork': 'A transient network failure occurred while fetching dependencies; please retry shortly',
|
|
335
|
+
'update.failFetchTimeout': 'Download timed out (slow network or large package); please retry later',
|
|
336
|
+
'update.failReleaseAge': 'The new release is blocked by pnpm\'s fresh-release safety wait; one automatic bypass retry also failed',
|
|
337
|
+
'update.failHoist': 'This profile\'s node_modules was created by a different pnpm major; the rebuild retry also failed',
|
|
338
|
+
'update.failAddingToRoot': 'pnpm refused to add at the workspace root (missing -w)',
|
|
339
|
+
'update.failNotWorkspace': '-w was passed but the profile is not a pnpm workspace',
|
|
340
|
+
'update.failIgnoredBuilds': 'Dependency build scripts are blocked by pnpm by default, so the upgrade could not finish',
|
|
309
341
|
'restart.title': 'Service restart',
|
|
310
342
|
'restart.description': 'Restart the dsh web process. Active work will be interrupted; persisted sessions can be resumed. You can also type /restart in a conversation.',
|
|
311
343
|
'restart.button': 'Restart dsh web',
|
|
@@ -913,6 +945,25 @@ window.__ModuleLoader__.load({
|
|
|
913
945
|
reader.readAsArrayBuffer(file)
|
|
914
946
|
}
|
|
915
947
|
|
|
948
|
+
// 宿主返回的稳定失败码 → 词典文案;未知错误详情(如 dsh-failed: …)透出原文。
|
|
949
|
+
const UPGRADE_FAILURES = {
|
|
950
|
+
'active-work': 'update.guardActiveWork',
|
|
951
|
+
'link-install': 'update.guardLinkInstall',
|
|
952
|
+
'file-install': 'update.guardFileInstall',
|
|
953
|
+
'no-newer-version': 'update.guardNoNewer',
|
|
954
|
+
'no-profile-found': 'update.guardNoProfile',
|
|
955
|
+
'ambiguous-profile': 'update.guardAmbiguous',
|
|
956
|
+
'upgrade-stale': 'update.guardStale',
|
|
957
|
+
'installed-version-unreadable': 'update.guardUnreadable',
|
|
958
|
+
'pnpm-missing': 'update.failPnpmMissing',
|
|
959
|
+
'transient-network': 'update.failNetwork',
|
|
960
|
+
'fetch-timeout': 'update.failFetchTimeout',
|
|
961
|
+
'release-age-violation': 'update.failReleaseAge',
|
|
962
|
+
'hoist-pattern-diff': 'update.failHoist',
|
|
963
|
+
'adding-to-root': 'update.failAddingToRoot',
|
|
964
|
+
'not-a-workspace': 'update.failNotWorkspace',
|
|
965
|
+
'ignored-builds': 'update.failIgnoredBuilds',
|
|
966
|
+
}
|
|
916
967
|
const upgradePlugin = async () => {
|
|
917
968
|
setUpgradeBusy(true)
|
|
918
969
|
setUpgradeError(null)
|
|
@@ -920,10 +971,16 @@ window.__ModuleLoader__.load({
|
|
|
920
971
|
const versionRes = await ctx.connection.rpc.call('/dsh-service', 'version', {})
|
|
921
972
|
const previousInstanceId = versionRes && versionRes.ok ? versionRes.value.instanceId : undefined
|
|
922
973
|
const res = await ctx.connection.rpc.call('/dsh-service', 'upgrade', {})
|
|
923
|
-
// 宿主错误详情(如 npm-failed: …、npm was not found on PATH)随通用文案透出,便于排查。
|
|
924
974
|
if (!res || res.ok === false) {
|
|
925
|
-
const
|
|
926
|
-
|
|
975
|
+
const code = res && typeof res.error === 'string' ? res.error.trim() : ''
|
|
976
|
+
const mapped = typeof code === 'string' && code.length > 0 ? UPGRADE_FAILURES[code] : undefined
|
|
977
|
+
// 已知失败码走双语词典;其余(如 npm-failed: …、dsh-failed: …)随通用文案透出宿主错误详情。
|
|
978
|
+
if (mapped !== undefined) {
|
|
979
|
+
setUpgradeError(translate(mapped))
|
|
980
|
+
} else {
|
|
981
|
+
setUpgradeError(translate('update.upgradeErrorDetail', { detail: code || 'upgrade failed' }))
|
|
982
|
+
}
|
|
983
|
+
return
|
|
927
984
|
}
|
|
928
985
|
if (typeof previousInstanceId === 'string' && previousInstanceId.length > 0) {
|
|
929
986
|
startRecovery(previousInstanceId).catch(() => {})
|
package/index.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { createHmac, randomBytes, randomUUID } from 'node:crypto'
|
|
4
4
|
import { Buffer } from 'node:buffer'
|
|
5
5
|
import { constants as fsConstants } from 'node:fs'
|
|
6
|
-
import { access, cp, lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from 'node:fs/promises'
|
|
7
7
|
import { homedir } from 'node:os'
|
|
8
|
-
import { basename, join, relative, resolve, sep } from 'node:path'
|
|
8
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
10
11
|
import https from 'node:https'
|
|
11
12
|
|
|
12
13
|
const require = createRequire(import.meta.url)
|
|
@@ -23,6 +24,14 @@ const BACKUP_NAME = /^dsh-backup-\d{8}-\d{6}\.tar\.gz$/
|
|
|
23
24
|
const USAGE_INDEX_VERSION = 4
|
|
24
25
|
const USAGE_INDEX_FILE = 'dsh-service-usage-index.json'
|
|
25
26
|
|
|
27
|
+
// 升级目标白名单:命令与包名全部来自宿主常量,浏览器不传任何输入。
|
|
28
|
+
// TARGET_RE 与 dsh-market 同源:只放行「包名@版本」这一种形状的字符集。
|
|
29
|
+
const TARGET_RE = /^[A-Za-z0-9@:./_#+~^=-]+$/
|
|
30
|
+
const RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
|
|
31
|
+
const FETCH_TIMEOUT_OVERRIDE = '--config.fetchTimeout=600000'
|
|
32
|
+
// 当前正在运行的插件源码目录:定位「本插件由哪个 profile 挂载」时与磁盘副本做 realpath 匹配。
|
|
33
|
+
const loadedPluginDir = dirname(fileURLToPath(import.meta.url))
|
|
34
|
+
|
|
26
35
|
// 读取当前 dsh 版本。DSH 包由宿主安装,不作为插件依赖打包进来。
|
|
27
36
|
let dshVersion = 'unknown'
|
|
28
37
|
let pluginVersion = 'unknown'
|
|
@@ -476,6 +485,7 @@ function toolFailureCode(tool, code, message) {
|
|
|
476
485
|
if (/ABORT|CANCEL/i.test(value) || /aborted|cancelled|canceled/i.test(message)) return undefined
|
|
477
486
|
if (value && value !== 'UNKNOWN' && value !== 'Error') return value
|
|
478
487
|
if (/requires reading\b/i.test(message)) return 'FS_NOT_OBSERVED'
|
|
488
|
+
if (/cannot read[^\n]*as an image/i.test(message)) return 'IMAGE_NOT_SUPPORTED'
|
|
479
489
|
if (/old_string was not found/i.test(message)) return 'OLD_STRING_NOT_FOUND'
|
|
480
490
|
if (/no such file or directory|path[^\n]*not found/i.test(message)) return 'PATH_NOT_FOUND'
|
|
481
491
|
if (/file access denied|permission denied|EACCES/i.test(message)) return 'PERMISSION_DENIED'
|
|
@@ -487,6 +497,7 @@ function toolFailureCode(tool, code, message) {
|
|
|
487
497
|
|
|
488
498
|
function toolFailureMessage(tool, code) {
|
|
489
499
|
if (code === 'FS_NOT_OBSERVED') return `${tool} requires reading <path> first — read the file, then retry`
|
|
500
|
+
if (code === 'IMAGE_NOT_SUPPORTED') return `${tool} failed: the current model does not support image input; switch to an image-capable model`
|
|
490
501
|
if (code === 'OLD_STRING_NOT_FOUND') return `${tool}: old_string was not found in <path>`
|
|
491
502
|
if (code === 'PATH_NOT_FOUND') return `${tool} search failed: <path> not found`
|
|
492
503
|
if (code === 'PERMISSION_DENIED') return `${tool} failed: permission denied for <path>`
|
|
@@ -663,11 +674,19 @@ function modeString(mode) {
|
|
|
663
674
|
}
|
|
664
675
|
|
|
665
676
|
async function runFixedCommand(ctx, argv) {
|
|
677
|
+
const result = await runCommandResult(ctx, argv)
|
|
678
|
+
if (result.exitCode !== 0 || result.signal !== null) {
|
|
679
|
+
throw new Error(`${argv[0]}-failed: ${(result.stderr || '').trim() || result.signal || result.exitCode}`)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 非抛出的命令执行:返回退出码/信号/stdout/stderr,供升级流程做失败分类与一次性恢复。
|
|
684
|
+
async function runCommandResult(ctx, argv) {
|
|
666
685
|
const subprocess = ctx.get('subprocess')
|
|
667
686
|
if (subprocess === undefined) throw new Error('subprocess-unavailable')
|
|
668
687
|
const executable = await subprocess.resolveExecutable(argv[0])
|
|
669
688
|
let spawnArgv = [executable, ...argv.slice(1)]
|
|
670
|
-
// Windows 上白名单命令(如 npm)经 PATHEXT 解析为 .cmd/.bat 脚本;subprocess 服务的
|
|
689
|
+
// Windows 上白名单命令(如 npm/dsh)经 PATHEXT 解析为 .cmd/.bat 脚本;subprocess 服务的
|
|
671
690
|
// spawn 不带 shell,Node 对 .cmd/.bat 一律抛 EINVAL,无法直接执行。固定包一层
|
|
672
691
|
// cmd.exe /d /s /c + 已解析的绝对路径,全部参数仍是宿主白名单常量,无输入拼接。
|
|
673
692
|
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(executable)) {
|
|
@@ -680,22 +699,160 @@ async function runFixedCommand(ctx, argv) {
|
|
|
680
699
|
cwd: process.platform === 'win32' ? (process.env.SystemRoot || 'C:\\Windows') : '/',
|
|
681
700
|
stdio: {
|
|
682
701
|
stdin: 'ignore',
|
|
683
|
-
stdout: { maxBytes:
|
|
684
|
-
stderr: { maxBytes:
|
|
702
|
+
stdout: { maxBytes: 64 * 1024 },
|
|
703
|
+
stderr: { maxBytes: 512 * 1024 },
|
|
685
704
|
},
|
|
686
705
|
graceMs: 5000,
|
|
687
706
|
})
|
|
688
707
|
const outcome = await handle.done
|
|
689
708
|
const stderr = handle.collected.stderr?.readFrom(0).text || ''
|
|
690
|
-
|
|
691
|
-
|
|
709
|
+
const stdout = handle.collected.stdout?.readFrom(0).text || ''
|
|
710
|
+
return { exitCode: outcome.exitCode ?? 1, signal: outcome.signal, stdout, stderr }
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// 定位「安装了本插件的 profile」。只读扫描 DSH_HOME/profiles/*/package.json,浏览器不传
|
|
714
|
+
// 任何路径/名字;多候选时优先匹配当前正在运行的插件源码目录(realpath),避免同秒竞态。
|
|
715
|
+
async function resolveUpgradeProfile(dshHome) {
|
|
716
|
+
const profilesDir = join(dshHome, 'profiles')
|
|
717
|
+
let entries
|
|
718
|
+
try {
|
|
719
|
+
entries = await readdir(profilesDir, { withFileTypes: true })
|
|
720
|
+
} catch (_) {
|
|
721
|
+
throw new Error('no-profile-found')
|
|
722
|
+
}
|
|
723
|
+
const candidates = []
|
|
724
|
+
for (const entry of entries) {
|
|
725
|
+
if (!entry.isDirectory() || entry.name === 'node_modules') continue
|
|
726
|
+
const dir = join(profilesDir, entry.name)
|
|
727
|
+
let manifest
|
|
728
|
+
try {
|
|
729
|
+
manifest = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'))
|
|
730
|
+
} catch (_) {
|
|
731
|
+
continue
|
|
732
|
+
}
|
|
733
|
+
const spec = manifest?.dependencies?.[PLUGIN_PACKAGE]
|
|
734
|
+
if (typeof spec !== 'string') continue
|
|
735
|
+
candidates.push({ name: entry.name, dir, spec })
|
|
736
|
+
}
|
|
737
|
+
if (candidates.length === 0) throw new Error('no-profile-found')
|
|
738
|
+
let loadedReal = null
|
|
739
|
+
try { loadedReal = await realpath(loadedPluginDir) } catch (_) {}
|
|
740
|
+
const matches = []
|
|
741
|
+
if (loadedReal !== null) {
|
|
742
|
+
for (const candidate of candidates) {
|
|
743
|
+
let installedReal = null
|
|
744
|
+
try { installedReal = await realpath(join(candidate.dir, 'node_modules', PLUGIN_PACKAGE)) } catch (_) {}
|
|
745
|
+
if (installedReal === loadedReal) matches.push(candidate)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const selected = matches.length === 1 ? matches[0] : (candidates.length === 1 ? candidates[0] : null)
|
|
749
|
+
if (selected === null) throw new Error('ambiguous-profile')
|
|
750
|
+
let workspace = false
|
|
751
|
+
try { workspace = (await stat(join(selected.dir, 'pnpm-workspace.yaml'))).isFile() } catch (_) {}
|
|
752
|
+
return { name: selected.name, dir: selected.dir, spec: selected.spec, workspace }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function readInstalledPluginVersion(profile) {
|
|
756
|
+
try {
|
|
757
|
+
const manifest = JSON.parse(await readFile(join(profile.dir, 'node_modules', PLUGIN_PACKAGE, 'package.json'), 'utf8'))
|
|
758
|
+
const version = typeof manifest?.version === 'string' ? manifest.version : ''
|
|
759
|
+
return version === '' ? null : version
|
|
760
|
+
} catch (_) {
|
|
761
|
+
return null
|
|
692
762
|
}
|
|
693
763
|
}
|
|
694
764
|
|
|
695
|
-
|
|
696
|
-
|
|
765
|
+
// pnpm 失败分类:dsh plugin 转发 pnpm 时只报「pnpm failed in profile directory」,不报原因;
|
|
766
|
+
// 必须按输出特征识别真实失败(踩坑见 KNOWLEDGE.md「pnpm 失败模式识别与自动恢复」)。
|
|
767
|
+
function classifyUpgradeFailure(output) {
|
|
768
|
+
const text = String(output || '')
|
|
769
|
+
if (text.includes('ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF')) {
|
|
770
|
+
return { code: 'hoist-pattern-diff', recoverable: true, message: 'hoist-pattern-diff' }
|
|
771
|
+
}
|
|
772
|
+
if (text.includes('ERR_PNPM_ADDING_TO_ROOT')) {
|
|
773
|
+
return { code: 'adding-to-root', recoverable: false, message: 'adding-to-root' }
|
|
774
|
+
}
|
|
775
|
+
if (/--workspace-root may only be used inside a workspace/i.test(text)) {
|
|
776
|
+
return { code: 'not-a-workspace', recoverable: false, message: 'not-a-workspace' }
|
|
777
|
+
}
|
|
778
|
+
if (text.includes('ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION') || text.includes('ERR_PNPM_NO_MATURE_MATCHING_VERSION')) {
|
|
779
|
+
return { code: 'release-age-violation', recoverable: true, message: 'release-age-violation' }
|
|
780
|
+
}
|
|
781
|
+
if (text.includes('ERR_PNPM_IGNORED_BUILDS')) {
|
|
782
|
+
return { code: 'ignored-builds', recoverable: false, message: 'ignored-builds' }
|
|
783
|
+
}
|
|
784
|
+
if (/ERR_PNPM_FETCH_5\d\d|ERR_PNPM_META_FETCH_FAIL|FetchError|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|socket hang up|network timeout/i.test(text)) {
|
|
785
|
+
return { code: 'transient-network', recoverable: true, message: 'transient-network' }
|
|
786
|
+
}
|
|
787
|
+
if (/operation was aborted due to timeout|TimeoutError|error \(23\)/i.test(text)) {
|
|
788
|
+
return { code: 'fetch-timeout', recoverable: true, message: 'fetch-timeout' }
|
|
789
|
+
}
|
|
790
|
+
if (text.includes('pnpm not found on PATH')) {
|
|
791
|
+
return { code: 'pnpm-missing', recoverable: false, message: 'pnpm-missing' }
|
|
792
|
+
}
|
|
793
|
+
return null
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 一键升级:走「dsh plugin --profile <p> add <pkg>@<版本>」,落在 DSH 真正读取的 profile
|
|
797
|
+
// node_modules,替代旧的无升级语义的 npm install -g。命令、包名、版本全部宿主白名单常量。
|
|
798
|
+
// 安全教义守卫:活动工作拒用、link/file 拒绝、latest 不高于当前版本拒绝(防降级)一律先于网络。
|
|
799
|
+
async function upgradePlugin(ctx, dshHome) {
|
|
800
|
+
const activity = collectActiveWork(ctx)
|
|
801
|
+
if (activity.hasActive) throw new Error('active-work')
|
|
802
|
+
|
|
803
|
+
const profile = await resolveUpgradeProfile(dshHome)
|
|
804
|
+
if (profile.spec.startsWith('link:')) throw new Error('link-install')
|
|
805
|
+
if (profile.spec.startsWith('file:')) throw new Error('file-install')
|
|
806
|
+
|
|
807
|
+
const published = await fetchPublishedVersions(PLUGIN_PACKAGE)
|
|
808
|
+
const targetVersion = published.latest
|
|
809
|
+
if (parseSemver(targetVersion) !== null && parseSemver(pluginVersion) !== null && compareSemver(targetVersion, pluginVersion) <= 0) {
|
|
810
|
+
throw new Error('no-newer-version')
|
|
811
|
+
}
|
|
812
|
+
const target = `${PLUGIN_PACKAGE}@${targetVersion}`
|
|
813
|
+
if (!TARGET_RE.test(PLUGIN_PACKAGE) || !TARGET_RE.test(target) || !/^[A-Za-z0-9][A-Za-z0-9._+\-]*$/.test(targetVersion)) {
|
|
814
|
+
throw new Error('invalid-upgrade-target')
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const addArgs = profile.workspace ? ['add', '-w'] : ['add']
|
|
818
|
+
const dshArgs = ['dsh', 'plugin', '--profile', profile.name]
|
|
819
|
+
const run = (extra) => runCommandResult(ctx, [...dshArgs, ...extra])
|
|
820
|
+
const ok = (result) => result.exitCode === 0 && result.signal === null
|
|
821
|
+
|
|
822
|
+
// pnpm 中断(signal 非 null)表示进程被终止,不做自动恢复。
|
|
823
|
+
let result = await run([...addArgs, target])
|
|
824
|
+
if (!ok(result) && result.signal === null) {
|
|
825
|
+
const failure = classifyUpgradeFailure(`${result.stderr}\n${result.stdout}`)
|
|
826
|
+
if (failure !== null) {
|
|
827
|
+
if (failure.code === 'hoist-pattern-diff') {
|
|
828
|
+
const rebuild = await run(['install', '--no-frozen-lockfile'])
|
|
829
|
+
if (ok(rebuild)) result = await run([...addArgs, target])
|
|
830
|
+
} else if (failure.code === 'release-age-violation' || failure.code === 'fetch-timeout') {
|
|
831
|
+
const override = failure.code === 'release-age-violation' ? RELEASE_AGE_OVERRIDE : FETCH_TIMEOUT_OVERRIDE
|
|
832
|
+
if (!addArgs.includes(override)) result = await run([addArgs[0], override, ...addArgs.slice(1), target])
|
|
833
|
+
} else if (failure.code === 'transient-network') {
|
|
834
|
+
result = await run([...addArgs, target])
|
|
835
|
+
} else {
|
|
836
|
+
throw new Error(failure.code)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (!ok(result)) {
|
|
842
|
+
const failure = classifyUpgradeFailure(`${result.stderr}\n${result.stdout}`)
|
|
843
|
+
throw new Error(failure !== null ? failure.code : `dsh-failed: ${(result.stderr || '').trim().slice(-400) || result.signal || result.exitCode}`)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// pnpm 干净退出 ≠ 真升级:minimumReleaseAge 会静默保住旧版。重读磁盘安装版本确认变化,只在其进了才重启。
|
|
847
|
+
const installed = await readInstalledPluginVersion(profile)
|
|
848
|
+
if (installed === null) throw new Error('installed-version-unreadable')
|
|
849
|
+
const advanced = parseSemver(installed) !== null && parseSemver(pluginVersion) !== null
|
|
850
|
+
? compareSemver(installed, pluginVersion) > 0
|
|
851
|
+
: installed !== pluginVersion
|
|
852
|
+
if (!advanced) throw new Error('upgrade-stale')
|
|
853
|
+
|
|
697
854
|
scheduleRestart(ctx)
|
|
698
|
-
return {
|
|
855
|
+
return { result: 'upgraded', profile: profile.name, previous: pluginVersion, installed }
|
|
699
856
|
}
|
|
700
857
|
|
|
701
858
|
async function permissionSnapshot(ctx, dshHome, plans) {
|
|
@@ -1088,7 +1245,7 @@ function apply(ctx) {
|
|
|
1088
1245
|
|
|
1089
1246
|
if (endpoint === 'upgrade') {
|
|
1090
1247
|
try {
|
|
1091
|
-
return { ok: true, value: await upgradePlugin(ctx) }
|
|
1248
|
+
return { ok: true, value: await upgradePlugin(ctx, dshHome) }
|
|
1092
1249
|
} catch (error) {
|
|
1093
1250
|
return { ok: false, error: error?.message || String(error) }
|
|
1094
1251
|
}
|