@noob-stupid/dsh-plugin-console 0.5.103 → 0.5.105
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.
|
@@ -474,7 +474,7 @@ function settleSelfUpdateGuard(session, { targetVersion = undefined, deps = {} }
|
|
|
474
474
|
* · 校验没过时把那段失败文案同时推进 `errors` 与 `note`(面板/接口都看得见);
|
|
475
475
|
* · `installedVersion` 在"已自愈"时改报**盘上真实那一版**(否则会留一个 null 让人以为目录还是空的)。
|
|
476
476
|
*/
|
|
477
|
-
function finishSelfUpdate({ profileDir, packageName, latest, spec, installed, method, command, errors, lockVersion, lockUpdated, lockNote, registryRange, selfUpdate = null, copy = null, tarball = null, lockPatch = null }) {
|
|
477
|
+
function finishSelfUpdate({ profileDir, packageName, latest, spec, installed, method, command, errors, lockVersion, lockUpdated, lockNote, registryRange, selfUpdate = null, copy = null, tarball = null, lockPatch = null, modeNote = null }) {
|
|
478
478
|
const CONSOLE_PACKAGE = packageName
|
|
479
479
|
const isRegistryRange = registryRange
|
|
480
480
|
const label = (e) => (e instanceof Error ? e.message : String(e))
|
|
@@ -514,6 +514,8 @@ function finishSelfUpdate({ profileDir, packageName, latest, spec, installed, me
|
|
|
514
514
|
else if (lockPatch.changed === true && lockPatch.verified === null) noteParts.push(`pnpm-lock.yaml 已补齐 ${latest} 的版本/完整性,但 pnpm 的只读校验**无法确认**(${lockPatch.verifyNote ?? '原因未知'})`)
|
|
515
515
|
else if (lockPatch.reason !== null && lockPatch.reason !== undefined) noteParts.push(`pnpm-lock.yaml 未补齐:${lockPatch.reason}`)
|
|
516
516
|
}
|
|
517
|
+
// ★ 2026-11-04 加法:本轮到底走的是哪条腿(manual-overlay = 不走包管理器)、lock 同步到什么程度,一句说清
|
|
518
|
+
if (typeof modeNote === 'string' && modeNote !== '') noteParts.push(modeNote)
|
|
517
519
|
|
|
518
520
|
// ★ 加法:自更新校验的结论(只有"本插件自己 + 更新前就有完整副本"时 selfUpdate 才非空)
|
|
519
521
|
const verified = selfUpdate === null || selfUpdate === undefined ? true : selfUpdate.verified === true
|
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
// L1 · domain —— selfupdate-copy.js
|
|
1
|
+
// L1 · domain —— selfupdate-copy.js(自更新的**本体**:下载 tarball → 只覆盖文件、不清空目录 → 补 lock)
|
|
2
2
|
//
|
|
3
3
|
// ── 现场(真机 2026-11-04 实测采样,用户提供的原始时间线)─────────────────────────────────
|
|
4
|
-
// 0.5.
|
|
5
|
-
// pnpm 一启动就**先清空目标目录**再重算整棵依赖树;这一步在 profile
|
|
6
|
-
// (`
|
|
7
|
-
// · 撞 90s/
|
|
4
|
+
// 0.5.103 的自更新是「包管理器优先」:`pnpm update` → `pnpm add` → 才轮到本模块的手铺(见 selfupdate.js 顶部)。
|
|
5
|
+
// pnpm 一启动就**先清空目标目录**再重算整棵依赖树;这一步在 profile 里要跑 3~9 分钟
|
|
6
|
+
// (`18:32:31 pkg=False files=1 bak=1 pnpm=1` → `18:43:48` 仍是空壳),于是:
|
|
7
|
+
// · 撞 90s/300s 超时被杀 → 目录停在"空了"的状态,跑了一整夜都没轮到第三步;
|
|
8
8
|
// · 就算第三步轮到了,既有 curl 通道也是"先 rmSync 整目录再 copyTree"(见 install.js#curlManualInstall),
|
|
9
9
|
// 宿主正读着这些文件时更容易被 Windows 拒绝。
|
|
10
|
-
// ⇒
|
|
10
|
+
// ⇒ 用户拍板「自更新彻底不走 pnpm,改回下载 tarball + 覆盖解包」:本模块的三个函数从此是**主路径**,
|
|
11
|
+
// 不再有任何包管理器排在它们前面(补 lock 也一样,见 ③)。
|
|
11
12
|
//
|
|
12
|
-
// ──
|
|
13
|
-
// ① prepareSelfUpdateTarball
|
|
14
|
-
//
|
|
15
|
-
// ② overlaySelfUpdateTarball:用备好的 tarball **只覆盖文件、绝不清空目录**(copyTree 的语义,
|
|
13
|
+
// ── 本模块只做三件事 ─────────────────────────────────────────────────────────
|
|
14
|
+
// ① prepareSelfUpdateTarball:registry 元数据 → `dist.tarball` → curl 下载到临时目录(**唯一的取包动作**);
|
|
15
|
+
// ② overlaySelfUpdateTarball:用下载的 tarball **只覆盖文件、绝不清空目录**(copyTree 的语义,
|
|
16
16
|
// 见 infra/fsx.js —— 它只 copyFileSync,不删任何东西),随后**读回** package.json 的版本与期望值比对;
|
|
17
17
|
// ③ patchSelfUpdateLock:把 `pnpm-lock.yaml` 里该包的版本/完整性更新到目标版本(**唯一**写入口仍是
|
|
18
|
-
// domain/lock-importer.js#writeLockRegistryVersion,本模块不另写一套 lock
|
|
19
|
-
//
|
|
20
|
-
// 并如实上报(宁可报"没补上",也绝不留下 pnpm 会拒绝的 lock)。
|
|
18
|
+
// domain/lock-importer.js#writeLockRegistryVersion,本模块不另写一套 lock 解析;它自带"写后读回逐字核对"),
|
|
19
|
+
// 之后**默认不跑 pnpm**(见 patchSelfUpdateLock 里 verify 那一段);只读校验只在调用方显式注入时执行。
|
|
21
20
|
//
|
|
22
21
|
// ── 与既有实现的关系(复用,不另造一套)──────────────────────────────────────────
|
|
23
|
-
// · 盒子校验 `verifyPackageBox`(install-box.js)、覆盖复制 `copyTree`(fsx.js
|
|
24
|
-
// lock
|
|
22
|
+
// · 盒子校验 `verifyPackageBox`(install-box.js)、覆盖复制 `copyTree`(fsx.js)、临时目录删除核实
|
|
23
|
+
// `removeDirVerified`(fsx.js)、lock 写入(lock-importer.js)、失败兜底与自愈(self-update-guard.js)都是既有件;
|
|
25
24
|
// · 本模块**不判断"更新成功与否"**:那由 selfupdate.js 的回读核实 + 自更新守卫说了算。
|
|
26
25
|
//
|
|
27
26
|
// ── 边界(别越线)──────────────────────────────────────────────────────────────
|
|
28
27
|
// ① 只对**自更新**生效:由 selfupdate.js 在 `/self-update` 那条路里调用;装别的插件的通道一个字节都不经过这里。
|
|
29
|
-
// ② 拿不到 tarball / 没有该版本的完整性摘要 ⇒ 什么都不做(返回 reason
|
|
28
|
+
// ② 拿不到 tarball / 没有该版本的完整性摘要 ⇒ 什么都不做(返回 reason),调用方回落到既有 curl 通道。
|
|
30
29
|
// ③ 覆盖失败(EPERM/EBUSY 等)⇒ 如实返回 reason,**不重试、不删目录、不谎报**。
|
|
31
30
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
32
31
|
import { join } from 'node:path'
|
|
33
32
|
import { tmpdir } from 'node:os'
|
|
34
|
-
import {
|
|
33
|
+
import { writeLockRegistryVersion } from './lock-importer.js'
|
|
35
34
|
import { verifyPackageBox } from './install-box.js'
|
|
36
35
|
import { execFileAsync } from '../infra/exec.js'
|
|
37
36
|
import { copyTree, removeDirVerified } from '../infra/fsx.js'
|
|
@@ -224,7 +223,7 @@ async function prepareSelfUpdateTarball({ packageName, version, registries = [],
|
|
|
224
223
|
* (copyTree 一个都不删,这个数是给"没清空目录"作证的)。
|
|
225
224
|
*/
|
|
226
225
|
async function overlaySelfUpdateTarball({ prepared = null, profileDir, packageName, expectedVersion = null, deps = {} } = {}) {
|
|
227
|
-
const out = { ok: false, version: null, files: 0, preserved: 0, boxNote: null, reason: null }
|
|
226
|
+
const out = { ok: false, version: null, files: 0, preserved: 0, boxNote: null, links: null, reason: null }
|
|
228
227
|
const label = (error) => (error instanceof Error ? error.message : String(error))
|
|
229
228
|
if (prepared === null || prepared === undefined || prepared.ok !== true || typeof prepared.tgzPath !== 'string') {
|
|
230
229
|
out.reason = `没有可用的预下载 tarball${prepared?.reason ? `(${prepared.reason})` : ''}`
|
|
@@ -245,6 +244,9 @@ async function overlaySelfUpdateTarball({ prepared = null, profileDir, packageNa
|
|
|
245
244
|
const existing = filesUnder(target)
|
|
246
245
|
let preserved = 0
|
|
247
246
|
for (const rel of existing) if (!tarballFiles.has(rel)) preserved += 1
|
|
247
|
+
// ★ 覆盖之前先**断开硬链接**(2026-11-04 加法,见 breakHardLinksUnder:pnpm 的 node_modules 文件是指向
|
|
248
|
+
// store 的硬链接,copyFileSync 会顺着链接把 store 里那一版的内容改掉 —— 波及别的 profile)
|
|
249
|
+
out.links = breakHardLinksUnder(target, tarballFiles)
|
|
248
250
|
// ★ 只覆盖:copyTree 只 copyFileSync(不删任何东西),目录缺就建目录 —— 见 infra/fsx.js
|
|
249
251
|
copyTree(pkgDir, target)
|
|
250
252
|
try {
|
|
@@ -271,6 +273,34 @@ async function overlaySelfUpdateTarball({ prepared = null, profileDir, packageNa
|
|
|
271
273
|
}
|
|
272
274
|
}
|
|
273
275
|
|
|
276
|
+
/**
|
|
277
|
+
* 覆盖之前**断开硬链接**(2026-11-04 加法,用户红线「不能影响别的功能」)。
|
|
278
|
+
* 现场事实:pnpm 把 store 里的文件**硬链接**进 `node_modules`,而 `copyFileSync` 是"打开目标文件、截断、写入"
|
|
279
|
+
* —— 它顺着硬链接**写回 store**:把 store 里 0.5.103 那份的内容换成 0.5.101 的(本机实测确认:nlink=2 时
|
|
280
|
+
* copyFileSync 会让另一份的内容一起变)。store 被改脏会波及**别的 profile**(同一 store 里那一版从此是坏的)。
|
|
281
|
+
* 所以只对"本来就在、且 nlink > 1"的目标文件做 **delete-then-copy**(删一个文件 ≠ 清空目录:
|
|
282
|
+
* 它本来就要被覆盖;tarball 里没有的文件一个都不动)。
|
|
283
|
+
* 删不掉(宿主正锁着该文件)⇒ 退回原地覆盖 = 与上一版行为逐字一致,**不新增失败模式**。
|
|
284
|
+
* 返回 `{ broken, failed }`:断了几个硬链接、几个没断开(如实上报,供调用方展示)。
|
|
285
|
+
*/
|
|
286
|
+
function breakHardLinksUnder(root, rels) {
|
|
287
|
+
const out = { broken: 0, failed: 0 }
|
|
288
|
+
for (const rel of rels) {
|
|
289
|
+
const full = join(String(root), ...String(rel).split('/'))
|
|
290
|
+
// 目标里还没有这个文件(首次铺入 / 新版新增)⇒ 不需要断链,更不是失败
|
|
291
|
+
if (existsSync(full) !== true) continue
|
|
292
|
+
try {
|
|
293
|
+
const info = statSync(full)
|
|
294
|
+
if (info.isFile() !== true || info.nlink <= 1) continue
|
|
295
|
+
rmSync(full, { force: true })
|
|
296
|
+
out.broken += 1
|
|
297
|
+
} catch {
|
|
298
|
+
out.failed += 1
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out
|
|
302
|
+
}
|
|
303
|
+
|
|
274
304
|
/** 读一份包目录里清单的版本(读不到返回 null)。 */
|
|
275
305
|
function readVersionOf(dir) {
|
|
276
306
|
try {
|
|
@@ -285,11 +315,14 @@ function readVersionOf(dir) {
|
|
|
285
315
|
* 复用 lockfile-health.js 的既有解析器)传进来,两处各写一份必然分叉。 */
|
|
286
316
|
|
|
287
317
|
/**
|
|
288
|
-
* ③
|
|
289
|
-
* 写入走 domain/lock-importer.js#writeLockRegistryVersion(唯一写入口,内部再复用 importer
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
318
|
+
* ③ 落地成功之后**必须**把 lock 补到目标版本(不补就重演"下次 pnpm 操作把版本还原"的老毛病)。
|
|
319
|
+
* 写入走 domain/lock-importer.js#writeLockRegistryVersion(唯一写入口,内部再复用 importer 规划器,
|
|
320
|
+
* 并且**写后读回逐字核对**)。
|
|
321
|
+
* ★ 2026-11-04 改动:pnpm 的只读解析校验(`pnpm install --lockfile-only --dry-run --frozen-lockfile`)
|
|
322
|
+
* 改成**显式注入才跑**(`deps.verifyLock`)—— 自更新这条路从此不启动任何 pnpm。不注入时 verified=null
|
|
323
|
+
* 并如实写明"未做解析层校验"(**绝不**把它当作通过)。注入时逐字沿用既有逻辑:复核**明确不通过**
|
|
324
|
+
* (verified === false)⇒ 把 lock 还原成改动前的字节,并按未补上如实上报:宁可回到"会被还原"的老状态
|
|
325
|
+
* + 一条可复制的命令,也绝不留下 pnpm 会拒绝的 lock。
|
|
293
326
|
* 返回 `{ attempted, changed, unchanged, version, integrityWritten, renamed, verified, verifyNote, rolledBack, reason, note }`。
|
|
294
327
|
*/
|
|
295
328
|
async function patchSelfUpdateLock({ profileDir, packageName, targetVersion, spec = null, integrity = null, section = 'dependencies', deps = {} } = {}) {
|
|
@@ -340,7 +373,17 @@ async function patchSelfUpdateLock({ profileDir, packageName, targetVersion, spe
|
|
|
340
373
|
out.reason = written?.reason ?? 'lock 未写入(原因未给出)'
|
|
341
374
|
return out
|
|
342
375
|
}
|
|
343
|
-
|
|
376
|
+
// ★ 2026-11-04 改动(用户拍板「自更新彻底不走 pnpm」):pnpm 的**只读解析校验**也改成"显式注入才跑"。
|
|
377
|
+
// 理由与自更新同一条:`pnpm install --lockfile-only --dry-run` 在这台机器上同样要几分钟(必被超时杀),
|
|
378
|
+
// 而自更新不能为了复核再启动一次包管理器。不注入 ⇒ `verified=null`(**无法确认**,不是"通过"),
|
|
379
|
+
// 并如实说明"未做解析层校验";写入器(writeLockRegistryVersion)本身已经做了**写后读回逐字核对**。
|
|
380
|
+
// 注入(既有测试 / 未来调用方)⇒ 逐字沿用原逻辑,含"明确不通过就把 lock 还原成改动前的字节"。
|
|
381
|
+
const verify = typeof deps.verifyLock === 'function' ? deps.verifyLock : null
|
|
382
|
+
if (verify === null) {
|
|
383
|
+
out.verified = null
|
|
384
|
+
out.verifyNote = '本轮不启动 pnpm(自更新不再走包管理器):lock 已定点写入并读回核对,但未做 pnpm 的解析层只读校验'
|
|
385
|
+
return out
|
|
386
|
+
}
|
|
344
387
|
try {
|
|
345
388
|
const verdict = await verify({ profileDir, registry: deps.registry ?? null, execOpts: { timeout: LOCK_VERIFY_TIMEOUT_MS }, ...(deps.verifyArgs ?? {}) })
|
|
346
389
|
out.verified = verdict?.verified ?? null
|
|
@@ -380,6 +423,7 @@ export {
|
|
|
380
423
|
LOCK_VERIFY_TIMEOUT_MS,
|
|
381
424
|
META_TIMEOUT_MS,
|
|
382
425
|
WORK_PREFIX,
|
|
426
|
+
breakHardLinksUnder,
|
|
383
427
|
disposeSelfUpdateTarball,
|
|
384
428
|
downloadToFile,
|
|
385
429
|
encodePackageName,
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
// L1 · domain —— selfupdate.js
|
|
1
|
+
// L1 · domain —— selfupdate.js(控制台自更新:**下载 tarball + 只覆盖解包**;自更新通路彻底不调用 pnpm)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// 现场(2026-11-04 真机采样,8 秒粒度;用户原话「全面修复这个,不行就把这个插件更新改为之前的版本」):
|
|
4
|
+
// 18:31:26 pkg=True ver=0.5.103 files=165 bak=0
|
|
5
|
+
// 18:32:31 pkg=False files=1 bak=1 pnpm=1 ← 备份刚建好;pnpm 一启动就把目标目录清空了
|
|
6
|
+
// 18:32:39 pkg=False files=1 bak=0 ← 备份 8 秒后被收尾段删掉(它以为更新成功了)
|
|
7
|
+
// 18:37:30 / 18:38:11 / 18:41:25 pnpm 两轮,每轮 3~9 分钟,都被超时杀
|
|
8
|
+
// 18:43:48 仍是空壳(1 文件) ← 排在 pnpm 后面的"手铺兜底"一次都没轮到
|
|
9
|
+
// ⇒ 只要 pnpm 参与自更新,它就**先清空目标目录**;本机 pnpm 必超时被杀 ⇒ 目录一直空着。
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// 现在的顺序(**自更新通路不再调用任何包管理器**;装别的插件的 /install 通道一个字节都没动):
|
|
12
|
+
// ① registry 元数据取目标版本 → curl 下载该版本 tarball
|
|
13
|
+
// ② **只覆盖、不清空**地解到 node_modules/<pkg>(复用 selfupdate-copy.js#overlaySelfUpdateTarball:
|
|
14
|
+
// infra/fsx.js#copyTree 只 copyFileSync,一个文件都不删)
|
|
15
|
+
// ③ 读回 package.json 核实版本 === 目标版本(不符/读不出 ⇒ 如实失败,才回落到既有 curl 通道)
|
|
16
|
+
// ④ 补 pnpm-lock.yaml(lock-importer.js#writeLockRegistryVersion:定点写 + 写后读回核对)
|
|
17
|
+
// ⑤ 既有守卫收尾(self-update-guard.js:更新前备份、校验不过就自愈、绝对路径 manualCommand)
|
|
18
|
+
// lock 这一层只做"定点写入 + 读回核对",**故意不跑** pnpm 的只读解析校验(`--lockfile-only --dry-run`
|
|
19
|
+
// 在这台机器上同样要几分钟):结果里 lockPatch.verified 恒为 null,note 明说"未做解析层校验"。
|
|
13
20
|
import { readFileSync } from 'node:fs'
|
|
14
21
|
import { join } from 'node:path'
|
|
15
22
|
import { linkSpecFor, linkSpecIsIntact, materializePackageForLink, probeRegistryPackage, sameSourceSpec, suggestedPinAction } from './dep-source.js'
|
|
16
23
|
import { canWriteImporterEntry, lockEntryKeyMatches, verifyLockReadOnly, writeLockImporterEntry } from './lock-importer.js'
|
|
17
24
|
import { readManifestDeps } from './lockfile-health.js'
|
|
25
|
+
// ↓ pnpmInstall / runPnpmWithFallback **不是**自更新在用(自更新通路一次都不调用它们):
|
|
26
|
+
// 它们是 /install 通道(domain/install-channels.js / install-job.js)的包管理器入口,函数本身原样保留。
|
|
27
|
+
// 本文件只在 selfUpdateToLatestLocked 的签名里留着 `runPnpm` / `pnpmAdd` 两个**观测位**(默认绑真实现),
|
|
28
|
+
// 正文不调用 —— 测试据此注入计数桩、断言"自更新全程 0 次 pnpm"(变异:把那两步加回去 ⇒ 计数立刻 > 0)。
|
|
18
29
|
import { pnpmInstall } from './install.js'
|
|
19
30
|
// 同一个 profile + 包名 的安装互斥(2026-11-04 加法,见 domain/install-lock.js 顶部现场):
|
|
20
31
|
// 「插件卡片的更新」与「市场页那颗自更新 pill」动的是**同一个** node_modules/<控制台> 目录 —— 必须串行。
|
|
@@ -23,11 +34,11 @@ import { withInstallLockOr } from './install-lock.js'
|
|
|
23
34
|
// begin/note/settle 是同一套判据与同一份 manualCommand;finishSelfUpdate 是本文件收尾段**原样搬出**的(腾行数用)。
|
|
24
35
|
import { beginSelfUpdateGuard, finishSelfUpdate, noteGuardStep, settleSelfUpdateGuard } from './self-update-guard.js'
|
|
25
36
|
// 只读读数(spec 判定 / 已装版本 / lock 里钉住的版本)2026-11-04 搬进 selfupdate-read.js:
|
|
26
|
-
// 本文件贴着 600
|
|
37
|
+
// 本文件贴着 600 行架构棘轮,而每轮都要在自更新路径上腾出空间(上一轮:手铺 + 补 lock;本轮:去掉 pnpm 两步)。
|
|
27
38
|
// 名字与语义一个字未改;下面的 export 语句照旧把它们**原样 re-export**(外部调用方与测试不用改)。
|
|
28
39
|
import { isRegistryRange, lockVersion, profileSpec, readInstalledVersion } from './selfupdate-read.js'
|
|
29
|
-
//
|
|
30
|
-
//
|
|
40
|
+
// 手铺本体(2026-11-04 加法,见 selfupdate-copy.js 顶部现场):下载 tarball → **只覆盖文件、不清空目录**地
|
|
41
|
+
// 铺进去 → 再把 lock 的版本/完整性补上。本轮起它就是自更新的**主路径**(不再有"pnpm 失败才轮到它")。
|
|
31
42
|
import { disposeSelfUpdateTarball, overlaySelfUpdateTarball, patchSelfUpdateLock, prepareSelfUpdateTarball } from './selfupdate-copy.js'
|
|
32
43
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
33
44
|
import { runPnpmWithFallback } from '../infra/exec.js'
|
|
@@ -40,11 +51,11 @@ function selfUpdateCommand(latest, profileName = '<你的profile>') {
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
/**
|
|
43
|
-
* 把控制台升到 `latest`
|
|
44
|
-
* 依赖以参数注入(便于单测替换):`runPnpm
|
|
45
|
-
* `curlManualInstall
|
|
54
|
+
* 把控制台升到 `latest` 并**把落地的版本写进 lock**。
|
|
55
|
+
* 依赖以参数注入(便于单测替换):`runPnpm` / `pnpmAdd`(**自更新不再调用**,只作观测位)、
|
|
56
|
+
* `curlManualInstall`(tarball 覆盖失败时的既有兜底通道,由路由传入)。
|
|
46
57
|
*
|
|
47
|
-
* ★ 2026-11-04
|
|
58
|
+
* ★ 2026-11-04 加法(见 domain/install-lock.js 顶部现场):整段操作过**同一个包互斥闸** ——
|
|
48
59
|
* 这条路和插件卡片的「更新」(`/install` → domain/install-channels.js)动的是**同一个**
|
|
49
60
|
* `node_modules/@noob-stupid/dsh-plugin-console`,真机事故就是两边同时装这个包。
|
|
50
61
|
* 拿不到锁 ⇒ 如实返回"正在安装中"(ok=false + reason),**不装、不碰目录**(结构同 finishSelfUpdate)。
|
|
@@ -65,13 +76,14 @@ async function selfUpdateToLatest(args = {}) {
|
|
|
65
76
|
/**
|
|
66
77
|
* 上面那个互斥外壳里的正文。
|
|
67
78
|
*
|
|
68
|
-
* ★ 2026-11-04
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
79
|
+
* ★ 2026-11-04 改法(用户拍板:「自更新彻底不走 pnpm」):
|
|
80
|
+
* 采样证明**只要 pnpm 参与自更新,它就先清空目标目录**,而本机 pnpm 要 3~9 分钟、必被超时杀
|
|
81
|
+
* ⇒ 目录一直空着,排在后面的手铺兜底永远轮不到。所以这条路起**不再调用 pnpm update / pnpm add**:
|
|
82
|
+
* ① 下载目标版本 tarball → ② 只覆盖、不清空地解到 node_modules/<pkg> → ③ 读回核实版本 === 目标版本
|
|
83
|
+
* → ④ 补 pnpm-lock.yaml(定点写 + 写后读回核对)→ ⑤ 守卫收尾(备份 / 自愈 / 绝对路径 manualCommand)。
|
|
84
|
+
* `runPnpm` / `pnpmAdd` 两个注入位**故意留着**(正文一次都不调用):它们是"自更新有没有偷偷跑起 pnpm"
|
|
85
|
+
* 的观测点 —— 测试注入计数桩断言 0 次;变异验证(把那两步加回去)会让那条断言立刻变红。
|
|
86
|
+
* `copyDeps` / `selfUpdateDeps` 只是单测注入口(默认全是既有实现),路由侧一个字都不用改。
|
|
75
87
|
*/
|
|
76
88
|
async function selfUpdateToLatestLocked({ profileDir, latest, registries, curlManualInstall, runPnpm = runPnpmWithFallback, pnpmAdd = pnpmInstall, profileName = null, execOpts = {}, selfUpdateDeps = {}, copyDeps = {} }) {
|
|
77
89
|
const command = selfUpdateCommand(latest, profileName ?? '<你的profile>')
|
|
@@ -82,81 +94,74 @@ async function selfUpdateToLatestLocked({ profileDir, latest, registries, curlMa
|
|
|
82
94
|
// 自更新兜底(规矩:只对本插件生效 / 更新前就有完整副本 / 真的装在 node_modules 里,三条闸都在 begin 里)
|
|
83
95
|
const guard = beginSelfUpdateGuard({ packageName: CONSOLE_PACKAGE, profileDir, targetVersion: latest, deps: selfUpdateDeps })
|
|
84
96
|
|
|
85
|
-
//
|
|
97
|
+
// ① 目标版本的 tarball(自更新**唯一**的取包动作:元数据 → dist.tarball → curl 下载)
|
|
86
98
|
const prepared = await prepareSelfUpdateTarball({ packageName: CONSOLE_PACKAGE, version: latest, registries, deps: copyDeps })
|
|
87
99
|
|
|
88
|
-
// ① spec 是版本范围 → pnpm update(spec 不变,lock 提到范围内最新)
|
|
89
|
-
if (isRegistryRange(spec)) {
|
|
90
|
-
try {
|
|
91
|
-
await runPnpm(['update', CONSOLE_PACKAGE], { execOpts: { cwd: profileDir, timeout: 300000, ...execOpts } })
|
|
92
|
-
method = 'pnpm-update'
|
|
93
|
-
} catch (error) {
|
|
94
|
-
errors.push(`pnpm update 失败:${label(error)}`)
|
|
95
|
-
}
|
|
96
|
-
noteGuardStep(guard, 'pnpm-update') // 这一步结束就校验一次(只记录,自愈留到收尾统一做)
|
|
97
|
-
}
|
|
98
|
-
// ② 还没到 latest(超出 range / git·file 来源)→ pnpm add <pkg>@<latest>(spec 与 lock 一起改写)
|
|
99
|
-
if (readInstalledVersion(profileDir, CONSOLE_PACKAGE) !== latest) {
|
|
100
|
-
try {
|
|
101
|
-
await pnpmAdd(profileDir, `${CONSOLE_PACKAGE}@${latest}`, registries?.[0])
|
|
102
|
-
method = method === null ? 'pnpm-add' : `${method}+pnpm-add`
|
|
103
|
-
} catch (error) {
|
|
104
|
-
errors.push(`pnpm add 失败:${label(error)}`)
|
|
105
|
-
}
|
|
106
|
-
noteGuardStep(guard, 'pnpm-add')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
100
|
let installed = readInstalledVersion(profileDir, CONSOLE_PACKAGE)
|
|
110
101
|
let lock = lockVersion(profileDir, CONSOLE_PACKAGE)
|
|
111
102
|
let lockUpdated = installed === latest && lock === latest
|
|
112
103
|
let lockNote = null
|
|
113
104
|
let copy = null
|
|
114
105
|
let lockPatch = null
|
|
106
|
+
let modeNote = null
|
|
115
107
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
// ② **只覆盖、不清空目录**地解到 node_modules/<pkg>(copyTree 只 copyFileSync:宿主正读着的文件也能覆盖,
|
|
109
|
+
// 而且覆盖失败时目录不会先变空)。目录里已经是目标版本且没有 tarball ⇒ 什么都不做。
|
|
110
|
+
if (installed !== latest || prepared.ok === true) {
|
|
119
111
|
const overlay = await overlaySelfUpdateTarball({ prepared, profileDir, packageName: CONSOLE_PACKAGE, expectedVersion: latest, deps: copyDeps })
|
|
112
|
+
noteGuardStep(guard, 'manual-overlay') // 覆盖之后立刻校验一次(记录器只读盘,自愈留到收尾统一做)
|
|
120
113
|
if (overlay.ok === true) {
|
|
121
|
-
method =
|
|
114
|
+
method = 'manual-overlay'
|
|
122
115
|
installed = overlay.version ?? readInstalledVersion(profileDir, CONSOLE_PACKAGE)
|
|
123
|
-
copy = { channel: 'overlay', files: overlay.files, preserved: overlay.preserved, version: overlay.version ?? null, boxNote: overlay.boxNote ?? null, noClear: true, reason: null }
|
|
116
|
+
copy = { channel: 'overlay', files: overlay.files, preserved: overlay.preserved, version: overlay.version ?? null, boxNote: overlay.boxNote ?? null, links: overlay.links ?? null, noClear: true, reason: null }
|
|
124
117
|
} else {
|
|
125
|
-
//
|
|
118
|
+
// ②b 没 tarball / 覆盖失败 → **既有** curl 通道(先删再铺)。它同样不碰 pnpm,行为与 0.5.102 逐字一致。
|
|
126
119
|
try {
|
|
127
120
|
const info = await curlManualInstall(profileDir, CONSOLE_PACKAGE, registries, null, latest)
|
|
128
|
-
method =
|
|
121
|
+
method = 'manual-copy'
|
|
129
122
|
installed = info?.version ?? readInstalledVersion(profileDir, CONSOLE_PACKAGE)
|
|
130
|
-
copy = { channel: 'curl', files: null, preserved: null, version: info?.version ?? null, boxNote: info?.boxNote ?? null, noClear: false, reason: overlay.reason ?? null }
|
|
123
|
+
copy = { channel: 'curl', files: null, preserved: null, version: info?.version ?? null, boxNote: info?.boxNote ?? null, links: null, noClear: false, reason: overlay.reason ?? null }
|
|
131
124
|
} catch (error) {
|
|
132
125
|
errors.push(`手动铺文件失败:${label(error)}`)
|
|
133
|
-
copy = { channel: null, files: null, preserved: null, version: null, boxNote: null, noClear: false, reason: overlay.reason ?? null }
|
|
126
|
+
copy = { channel: null, files: null, preserved: null, version: null, boxNote: null, links: null, noClear: false, reason: overlay.reason ?? null }
|
|
134
127
|
}
|
|
128
|
+
noteGuardStep(guard, 'manual-copy')
|
|
135
129
|
}
|
|
130
|
+
} else {
|
|
131
|
+
// 目录里本来就是目标版本(且 tarball 没备成)⇒ 如实说明"没改动任何文件",绝不假装更新过
|
|
132
|
+
method = 'already-current'
|
|
133
|
+
modeNote = `目录里的版本已是 ${latest},本轮没有改动任何文件`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
lock = lockVersion(profileDir, CONSOLE_PACKAGE)
|
|
137
|
+
lockUpdated = installed === latest && lock === latest
|
|
138
|
+
// ③ 文件已落地但 lock 还钉在旧版本 ⇒ **必须补 lock**,否则下一次 pnpm 操作就把版本还原
|
|
139
|
+
if (installed === latest && !lockUpdated) {
|
|
140
|
+
lockPatch = await patchSelfUpdateLock({
|
|
141
|
+
profileDir, packageName: CONSOLE_PACKAGE, targetVersion: latest, spec,
|
|
142
|
+
integrity: prepared.ok === true ? prepared.integrity : null,
|
|
143
|
+
section: manifestSectionOf(profileDir, CONSOLE_PACKAGE), deps: copyDeps,
|
|
144
|
+
})
|
|
136
145
|
lock = lockVersion(profileDir, CONSOLE_PACKAGE)
|
|
137
146
|
lockUpdated = installed === latest && lock === latest
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!lockUpdated) {
|
|
151
|
-
lockNote = `此更新未写入 pnpm-lock.yaml(lock 里仍是 ${lock ?? '未知'}):之后任何 pnpm 操作(开关插件、dsh plugin add/remove)都会把它还原成 lock 里的版本。要真正落地请执行:${command}`
|
|
152
|
-
if (lockPatch !== null && lockPatch.reason !== null) lockNote = `${lockNote}(补 lock 未成功:${lockPatch.reason})`
|
|
153
|
-
}
|
|
147
|
+
if (lockPatch.reason !== null) errors.push(`补 pnpm-lock.yaml 未成功:${lockPatch.reason}`)
|
|
148
|
+
}
|
|
149
|
+
if (!lockUpdated) {
|
|
150
|
+
lockNote = `此更新未写入 pnpm-lock.yaml(lock 里仍是 ${lock ?? '未知'}):之后任何 pnpm 操作(开关插件、dsh plugin add/remove)都会把它还原成 lock 里的版本。要真正落地请执行:${command}`
|
|
151
|
+
if (lockPatch !== null && lockPatch.reason !== null) lockNote = `${lockNote}(补 lock 未成功:${lockPatch.reason})`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ④ 如实说明本轮走的是哪条腿(**绝不静默**):manual-* 一律"没走包管理器",并交代 lock 的边界
|
|
155
|
+
if (copy !== null) {
|
|
156
|
+
modeNote = method === null
|
|
157
|
+
? `本轮既没覆盖成功、也没走通既有兜底通道(全程**没有调用包管理器 pnpm**);lock 已尽力同步${lockUpdated === true ? '' : '但最终没同步(详见 lockNote)'}`
|
|
158
|
+
: `本轮走的是 ${method}(下载 tarball 后${copy.channel === 'overlay' ? '只覆盖文件、不清空目录' : '先清后铺'}),全程**没有调用包管理器 pnpm**;lock 已尽力同步${lockUpdated === true ? '' : '但最终没同步(详见 lockNote)'}——若 lock 最终没同步,下一次包管理器操作可能把版本还原`
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
//
|
|
161
|
+
// 临时工作目录(下载的 tarball / 解包目录)收尾即删;删不掉要如实记下来(本机 %TEMP% 下 rmSync 会静默落空)
|
|
157
162
|
const cleanup = disposeSelfUpdateTarball(prepared)
|
|
158
163
|
|
|
159
|
-
//
|
|
164
|
+
// 收尾:全部动作走完才统一校验(仍不过才自愈),然后交给**原样搬出**的收尾段组装返回结构
|
|
160
165
|
const selfUpdate = settleSelfUpdateGuard(guard, { targetVersion: latest })
|
|
161
166
|
const tarball = {
|
|
162
167
|
ok: prepared.ok === true,
|
|
@@ -168,7 +173,7 @@ async function selfUpdateToLatestLocked({ profileDir, latest, registries, curlMa
|
|
|
168
173
|
ms: prepared.ms ?? 0,
|
|
169
174
|
cleanup: { removed: cleanup.removed === true, note: cleanup.note ?? null },
|
|
170
175
|
}
|
|
171
|
-
return finishSelfUpdate({ profileDir, packageName: CONSOLE_PACKAGE, latest, spec, installed, method, command, errors, lockVersion: lock, lockUpdated, lockNote, registryRange: isRegistryRange, selfUpdate, copy, tarball, lockPatch })
|
|
176
|
+
return finishSelfUpdate({ profileDir, packageName: CONSOLE_PACKAGE, latest, spec, installed, method, command, errors, lockVersion: lock, lockUpdated, lockNote, registryRange: isRegistryRange, selfUpdate, copy, tarball, lockPatch, modeNote })
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.105",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|