@noob-stupid/dsh-plugin-console 0.5.37 → 0.5.39
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/lib/client.js +30 -0
- package/lib/server/domain/bundle-refs.js +9 -2
- package/lib/server/domain/dep-source.js +61 -2
- package/lib/server/domain/install-cleanup.js +23 -2
- package/lib/server/domain/install-job.js +35 -47
- package/lib/server/domain/install.js +3 -0
- package/lib/server/domain/lock-importer.js +411 -0
- package/lib/server/domain/manifest.js +31 -5
- package/lib/server/domain/patch.js +14 -3
- package/lib/server/domain/persist.js +401 -0
- package/lib/server/domain/plugin-actions.js +42 -3
- package/lib/server/domain/selfupdate.js +210 -12
- package/package.json +1 -1
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
// ③ 回读核实 readInstalledVersion / lockVersion;都不匹配才回落手铺文件,且**必须**带 lockNote 警告。
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
|
-
import { linkSpecFor, linkSpecIsIntact, materializePackageForLink, probeRegistryPackage, suggestedPinAction } from './dep-source.js'
|
|
15
|
+
import { linkSpecFor, linkSpecIsIntact, materializePackageForLink, probeRegistryPackage, sameSourceSpec, suggestedPinAction } from './dep-source.js'
|
|
16
|
+
import { canWriteImporterEntry, lockEntryKeyMatches, verifyLockReadOnly, writeLockImporterEntry } from './lock-importer.js'
|
|
17
|
+
import { readManifestDeps } from './lockfile-health.js'
|
|
16
18
|
import { pnpmInstall } from './install.js'
|
|
17
19
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
18
20
|
import { runPnpmWithFallback } from '../infra/exec.js'
|
|
@@ -72,8 +74,15 @@ function lockVersion(profileDir, name) {
|
|
|
72
74
|
return null
|
|
73
75
|
}
|
|
74
76
|
for (let i = 0; i < lines.length; i += 1) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// 键形态三种都认(0.5.38 改错):`name`、`'name':`(pnpm 对**含 `/`/`@`** 的包名加引号)、
|
|
78
|
+
// `name:`(pnpm 对**未加 scope** 的包名不加引号)。旧判据漏了第三种 ⇒ 未加 scope 的 `link:`
|
|
79
|
+
// 依赖在 importers 里明明有条目却读不出来(真机 desktop profile 的 `dshmarket` 就是:
|
|
80
|
+
// lock 的 35-37 行写着 `dshmarket: / specifier: link:… / version: link:../../plugin-src/dshmarket`,
|
|
81
|
+
// 而 `lockVersion()` 返回 null)——后果是面板永远报"没写进 pnpm-lock.yaml"、pin 永远 partial、
|
|
82
|
+
// 每次对账都白跑一次 `pnpm add`。真值一律从下面这一处读,别在调用方再补一套。
|
|
83
|
+
// 0.5.39:键判据搬进 lock-importer.js#lockEntryKeyMatches —— 定点**写**入也要认同一批键形态,
|
|
84
|
+
// 两处各写一份必然再次分叉(写进去的键、读的时候认不出,就是 0.5.38 那个现场)。
|
|
85
|
+
if (lockEntryKeyMatches(lines[i], name)) {
|
|
77
86
|
for (let k = i + 1; k < Math.min(i + 8, lines.length); k += 1) {
|
|
78
87
|
const m = /^\s*version:\s*(\S+)\s*$/u.exec(lines[k])
|
|
79
88
|
if (m) return m[1]
|
|
@@ -202,14 +211,113 @@ const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u
|
|
|
202
211
|
*/
|
|
203
212
|
const DIST_TAG_RE = /^[A-Za-z][A-Za-z0-9._-]*$/u
|
|
204
213
|
|
|
214
|
+
/**
|
|
215
|
+
* manifest spec 的写入器(0.5.39 加法):**复用 manifest.js 那一个既有写入器**
|
|
216
|
+
* `setProfileDependency`(只改 `dependencies[包名]` 一个键、走同一把写队列),本模块**不新造第二套**。
|
|
217
|
+
* 为什么用动态 import:manifest.js 顶部 import 了本模块的 `reconcileLockfile`(syncLockFor 用),
|
|
218
|
+
* 静态互相 import 会成环;这里只在**真的需要改清单**时才按需取一次(同进程内 import 有缓存,零成本)。
|
|
219
|
+
* 单测可用 `deps.setSpec` 直接替换(离线夹具不需要真写清单时)。
|
|
220
|
+
*/
|
|
221
|
+
async function defaultSetSpec(profileDir, packageName, spec) {
|
|
222
|
+
// 注意:这里**不能**用解构(`const { setProfileDependency } = await import(…)`)—— 架构守卫的自由变量
|
|
223
|
+
// 扫描认不出解构声明(`const {` 形态),会把导入名误判成"没声明/没 import"的自由变量。
|
|
224
|
+
const manifestModule = await import('./manifest.js')
|
|
225
|
+
return manifestModule.setProfileDependency(profileDir, packageName, spec)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 该包在清单里属于哪个依赖分区(复用 lockfile-health.js 的既有解析器,不新写一份)。 */
|
|
229
|
+
function manifestSectionOf(profileDir, name) {
|
|
230
|
+
try {
|
|
231
|
+
const { deps } = readManifestDeps(readFileSync(join(String(profileDir), 'package.json'), 'utf8'))
|
|
232
|
+
return deps.find((d) => d.name === name)?.section ?? 'dependencies'
|
|
233
|
+
} catch {
|
|
234
|
+
return 'dependencies'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 非 registry 来源的 lock 持久化收口(0.5.39 加法,用户点名的修 1):
|
|
240
|
+
* · `items`(**能由 importer 条目完整表达**的来源,目前只有 `link:`)→ **只写 lock importer 条目**
|
|
241
|
+
* (lock-importer.js 是唯一实现;persist.js 走的 reconcile 通路就是这一条),清单形态与目标不等价时
|
|
242
|
+
* 先用既有 `setProfileDependency` 写规范形;写完跑**一次** pnpm **只读**校验确认 lock 仍可解析;
|
|
243
|
+
* · `skipped`(file:/workspace:/portal:/git+/URL…)→ lock 记录不止 importer 一行(pnpm 还要写
|
|
244
|
+
* packages/snapshots 的解析记录),本地无法确定 ⇒ **既不伪造条目、也不用 `pnpm add` 当持久化手段**,
|
|
245
|
+
* 由调用方如实报「未持久化」+ 一键钉住。
|
|
246
|
+
* 返回 `{ writes, verified, verifyNote, errors, manifestWrites, changed }`;`verified` 为 `true|false|null`
|
|
247
|
+
* (null = pnpm 执行方式不可用 ⇒ 无法确认)。
|
|
248
|
+
*/
|
|
249
|
+
async function settleImporterEntries({ profileDir, items = [], deps = {}, registries = [] } = {}) {
|
|
250
|
+
const setSpec = typeof deps.setSpec === 'function' ? deps.setSpec : defaultSetSpec
|
|
251
|
+
const writeImporter = typeof deps.writeImporter === 'function' ? deps.writeImporter : writeLockImporterEntry
|
|
252
|
+
const verify = typeof deps.verifyLock === 'function' ? deps.verifyLock : verifyLockReadOnly
|
|
253
|
+
const writes = []
|
|
254
|
+
const errors = []
|
|
255
|
+
const manifestWrites = []
|
|
256
|
+
for (const item of items) {
|
|
257
|
+
// ① 清单里必须有这个来源的声明:lock 条目不能单独存在(清单没有 = pnpm 会把它当多余条目清掉)
|
|
258
|
+
const before = profileSpec(profileDir, item.name)
|
|
259
|
+
if (typeof before !== 'string' || before.trim() === '') {
|
|
260
|
+
writes.push({ name: item.name, changed: false, skipped: true, reason: '清单 dependencies 里没有这个依赖的声明(lock 条目不能单独存在)—— 未写 lock,请先走「声明」通路' })
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
// ② 形态与目标**不等价**才算真漂移 → 写**规范形**;等价(正/反斜杠、相对↔绝对同一目标)⇒ 零写盘
|
|
264
|
+
if (!sameSourceSpec(profileDir, before, item.spec)) {
|
|
265
|
+
try {
|
|
266
|
+
const wrote = await setSpec(profileDir, item.name, item.spec)
|
|
267
|
+
manifestWrites.push({ name: item.name, changed: wrote?.changed === true, before, after: wrote?.after ?? null })
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const reason = `清单写回失败:${error instanceof Error ? error.message : String(error)}`
|
|
270
|
+
errors.push(reason)
|
|
271
|
+
writes.push({ name: item.name, changed: false, skipped: true, reason })
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
const actual = profileSpec(profileDir, item.name)
|
|
275
|
+
if (!sameSourceSpec(profileDir, actual, item.spec)) {
|
|
276
|
+
const reason = `清单写回未生效(现在仍是 ${actual ?? '(没有这个条目)'})—— 未写 lock`
|
|
277
|
+
errors.push(`${item.name}:${reason}`)
|
|
278
|
+
writes.push({ name: item.name, changed: false, skipped: true, reason })
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ③ 定点写 lock importer 条目(读改写 + 读回核实都在 lock-importer.js 里)
|
|
283
|
+
let written = null
|
|
284
|
+
try {
|
|
285
|
+
written = await writeImporter(profileDir, { name: item.name, spec: item.spec, section: manifestSectionOf(profileDir, item.name), deps })
|
|
286
|
+
} catch (error) {
|
|
287
|
+
written = { changed: false, reason: `lock 写入失败:${error instanceof Error ? error.message : String(error)}` }
|
|
288
|
+
}
|
|
289
|
+
if (written?.changed !== true && written?.unchanged !== true) errors.push(`${item.name}:${written?.reason ?? 'lock 写入未成功'}`)
|
|
290
|
+
writes.push({ name: item.name, ...written, reason: written?.reason ?? null })
|
|
291
|
+
}
|
|
292
|
+
let verified
|
|
293
|
+
let verifyNote = null
|
|
294
|
+
if (writes.some((w) => w.changed === true)) {
|
|
295
|
+
// 写出后**必须**用 pnpm 的只读用法确认 lock 仍可解析;失败或无法确认 ⇒ 调用方报「未持久化」
|
|
296
|
+
let verdict = null
|
|
297
|
+
try {
|
|
298
|
+
verdict = await verify({ profileDir, registry: registries?.[0] ?? null })
|
|
299
|
+
} catch (error) {
|
|
300
|
+
verdict = { verified: null, reason: `只读校验执行失败:${error instanceof Error ? error.message : String(error)}` }
|
|
301
|
+
}
|
|
302
|
+
verified = verdict?.verified ?? null
|
|
303
|
+
verifyNote = verdict?.reason ?? null
|
|
304
|
+
if (verified !== true) errors.push(`pnpm 只读校验${verified === null ? '无法确认' : '未通过'}:${verifyNote ?? '原因未知'}`)
|
|
305
|
+
}
|
|
306
|
+
return { writes, manifestWrites, verified, verifyNote, errors, changed: writes.some((w) => w.changed === true) }
|
|
307
|
+
}
|
|
308
|
+
|
|
205
309
|
/**
|
|
206
310
|
* 通用 lock 对账(**任何**安装通道装完都该调用,支持一次对账多个包):
|
|
207
311
|
* 走非 pnpm 通道(并行 curl / curl tarball / GitHub Release / git clone 装配 / 套装装配)装的包,
|
|
208
312
|
* node_modules 里的版本与 `pnpm-lock.yaml` 记的版本可能不一致——之后任何 pnpm 操作(开关插件改
|
|
209
313
|
* `dsh.profile.bundles`、`dsh plugin add/remove`)都可能按 lock 把它**还原**甚至当外来物处理。这里:
|
|
210
314
|
* ① 全部一致 → 直接返回(不跑 pnpm,零成本);
|
|
211
|
-
* ② 有漂移 →
|
|
212
|
-
*
|
|
315
|
+
* ② 有漂移 → registry 形态(版本号/dist-tag):**一次** `pnpm add <spec1> <spec2> …` 把它们精确写进
|
|
316
|
+
* lock(**行为一个字未改**);非 registry 来源里的 `link:`:**只写 lock importer 条目**
|
|
317
|
+
* (domain/lock-importer.js,唯一实现),再用 pnpm 的**只读**用法确认 lock 可解析 ——
|
|
318
|
+
* `pnpm add` **不再**充当非 registry 来源的持久化手段;
|
|
319
|
+
* ③ 仍对不上(含只读校验没通过/无法确认)→ 返回 lockNote(逐包列出)与可复制命令,由调用方如实展示,
|
|
320
|
+
* 绝不假装成功。
|
|
213
321
|
* `packages` 形如 [{ name, version }];只给 `packageName` 时等价于单包对账(向后兼容)。
|
|
214
322
|
*
|
|
215
323
|
* ★ 缺陷②修复(0.4.0-beta.16 / 0.3.63):写回 spec 前必须确认来源,**绝不能把 release 来源的包
|
|
@@ -223,7 +331,7 @@ const DIST_TAG_RE = /^[A-Za-z][A-Za-z0-9._-]*$/u
|
|
|
223
331
|
* 查无此包(404)→ 物化到 `<DSH_HOME>/plugin-src/<包名>` 并写 `link:<绝对路径>`,同时回 depNote;
|
|
224
332
|
* 连物化都做不到 → **跳过对齐且不碰 package.json**,只记 note。
|
|
225
333
|
*/
|
|
226
|
-
async function reconcileLockfile({ profileDir, packageName = null, packages = null, registries = [], pnpmAdd = pnpmInstall, execOpts = {}, fetchJson = fetchJsonUrl, home = null } = {}) {
|
|
334
|
+
async function reconcileLockfile({ profileDir, packageName = null, packages = null, registries = [], pnpmAdd = pnpmInstall, execOpts = {}, fetchJson = fetchJsonUrl, home = null, deps = {} } = {}) {
|
|
227
335
|
const targets = Array.isArray(packages)
|
|
228
336
|
? packages.filter((p) => p && typeof p.name === 'string' && p.name !== '')
|
|
229
337
|
: (packageName === null ? [] : [{ name: packageName, version: null }])
|
|
@@ -257,8 +365,13 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
257
365
|
const aligned = (r) => {
|
|
258
366
|
if (r.installed === null) return false
|
|
259
367
|
if (typeof r.spec === 'string' && r.spec.startsWith('link:')) {
|
|
368
|
+
// 0.5.39 改错(Fix 2):lock 里那条 link 条目还必须**指向同一个目标**才算对齐。
|
|
369
|
+
// 旧判据只要求它以 `link:` 开头 ⇒ 「清单指 A、lock 指 B」被当成对齐(真实漂移:两处目标不同),
|
|
370
|
+
// 而 pnpm 下一次操作会按 lock 把链接还原到 B。等价判据与清单写入共用同一把尺子
|
|
371
|
+
// (dep-source.js#sameSourceSpec:正/反斜杠、相对↔绝对都算同一目标)。
|
|
260
372
|
return linkSpecIsIntact(profileDir, r.name, r.spec)
|
|
261
373
|
&& typeof r.lock === 'string' && r.lock.trim().startsWith('link:')
|
|
374
|
+
&& sameSourceSpec(profileDir, r.lock, r.spec)
|
|
262
375
|
}
|
|
263
376
|
return r.installed === r.lock || sourcePinned(r.lock)
|
|
264
377
|
}
|
|
@@ -288,8 +401,11 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
288
401
|
if (/^https?:/u.test(spec.trim()) || misrecorded(row)) {
|
|
289
402
|
return linkPlan(row, '该包原先以 tarball URL 记录(pnpm 10 重写 lock 会丢 integrity,实测 ERR_PNPM_MISSING_TARBALL_INTEGRITY)')
|
|
290
403
|
}
|
|
291
|
-
// link:
|
|
404
|
+
// link: 规格:链接**还在**(node_modules 里就是指向清单那个目标的链接)→ 一个字节都不用物化,
|
|
405
|
+
// 按清单形态重放即可(0.5.39:等价即零漂移,别为了写一条 lock 去复制整棵包);
|
|
406
|
+
// 链接已被 release/curl 通道的"先删再铺"打断 → 先把新副本刷进 plugin-src 再重放(既有能力,不变)
|
|
292
407
|
if (spec.trim().startsWith('link:')) {
|
|
408
|
+
if (linkSpecIsIntact(profileDir, name, spec)) return { spec: spec.trim(), reason: null, note: null }
|
|
293
409
|
const dir = materializePackageForLink(profileDir, name, home === null ? {} : { home })
|
|
294
410
|
return { spec: dir === null ? spec.trim() : linkSpecFor(dir), reason: null, note: null }
|
|
295
411
|
}
|
|
@@ -319,14 +435,29 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
319
435
|
const errors = []
|
|
320
436
|
let method = null
|
|
321
437
|
const planned = []
|
|
438
|
+
let importerSettled = null
|
|
322
439
|
if (drifted.length > 0) {
|
|
323
440
|
// 只对"能读到实际版本"的包做对齐;读不到的(目录都没有)无法对账,留给 lockNote
|
|
324
|
-
const specs = []
|
|
441
|
+
const specs = [] // registry 形态(版本号 / dist-tag)→ **照旧**走既有 pnpm add 通道
|
|
442
|
+
const importerItems = [] // 非 registry 来源且**能由 importer 条目完整表达**(link:)→ 定点写 lock
|
|
443
|
+
const notWritable = [] // 其余非 registry 来源(file:/git+/URL/…)→ 本地不伪造、也不用 pnpm add
|
|
325
444
|
for (const row of drifted) {
|
|
326
445
|
if (row.installed === null) continue
|
|
327
446
|
const plan = await planWriteback(row)
|
|
328
|
-
|
|
329
|
-
if (plan.spec !== null
|
|
447
|
+
const item = { name: row.name, installed: row.installed, spec: plan.spec, reason: plan.reason ?? null, note: plan.note ?? null }
|
|
448
|
+
if (plan.spec !== null && canWriteImporterEntry(plan.spec)) {
|
|
449
|
+
item.channel = 'importer'
|
|
450
|
+
importerItems.push(item)
|
|
451
|
+
} else if (plan.spec !== null && SOURCE_PINNED_RE.test(plan.spec.trim())) {
|
|
452
|
+
// 修 1(0.5.39):非 registry 来源**绝不**再用 `pnpm add` 当持久化手段;本地又无法确定它的
|
|
453
|
+
// lock 记录(上面 IMPORTER_ONLY_RE 的注释说了为什么)⇒ 一个字节都不写,交给下面的文案如实报。
|
|
454
|
+
item.channel = 'importer-skipped'
|
|
455
|
+
notWritable.push(item)
|
|
456
|
+
} else {
|
|
457
|
+
item.channel = 'pnpm'
|
|
458
|
+
if (plan.spec !== null) specs.push(plan.spec)
|
|
459
|
+
}
|
|
460
|
+
planned.push(item)
|
|
330
461
|
}
|
|
331
462
|
if (specs.length > 0) {
|
|
332
463
|
try {
|
|
@@ -336,18 +467,43 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
336
467
|
errors.push(`pnpm add 对齐失败:${error instanceof Error ? error.message : String(error)}`)
|
|
337
468
|
}
|
|
338
469
|
}
|
|
470
|
+
if (importerItems.length > 0) {
|
|
471
|
+
importerSettled = await settleImporterEntries({ profileDir, items: importerItems, deps, registries })
|
|
472
|
+
errors.push(...importerSettled.errors)
|
|
473
|
+
if (importerSettled.changed === true) method = method === null ? 'lock-importer' : `${method}+lock-importer`
|
|
474
|
+
}
|
|
339
475
|
rows = snap()
|
|
340
476
|
drifted = driftedOf(rows)
|
|
341
477
|
}
|
|
342
478
|
// ★ 文案一律按**实际写入形态**生成(读回清单再描述)——"计划写 link:"与"清单里真是 link:"
|
|
343
479
|
// 是两件事:pnpm add 失败、或后续某一步又把 spec 覆盖成版本号时,绝不能报"已钉住"。
|
|
344
480
|
const suggestedActions = []
|
|
481
|
+
const writeOf = new Map((importerSettled?.writes ?? []).map((w) => [w.name, w]))
|
|
345
482
|
for (const item of planned) {
|
|
346
483
|
const actual = profileSpec(profileDir, item.name)
|
|
347
484
|
if (item.spec === null) {
|
|
348
485
|
if (item.note !== null) depNotes.push(item.note)
|
|
349
486
|
continue
|
|
350
487
|
}
|
|
488
|
+
if (item.channel === 'importer') {
|
|
489
|
+
const wrote = writeOf.get(item.name) ?? null
|
|
490
|
+
if (wrote !== null && (wrote.changed === true || wrote.unchanged === true)) {
|
|
491
|
+
if (item.reason !== null) {
|
|
492
|
+
depNotes.push(`${item.name}:${item.reason},已按 link: 形式记录依赖(${actual})—— lock 的 importer 条目已**定点写入**(${wrote.version ?? ''}),不经 npm registry 解析、不经 tarball 完整性校验`)
|
|
493
|
+
}
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
const why = `${item.name}:${item.reason !== null ? `${item.reason};` : ''}${wrote?.reason ?? 'lock importer 条目未能写入'}(清单现在是 ${actual ?? '(没有这个条目)'})—— lock 条目**未写入**`
|
|
497
|
+
depNotes.push(`${why}。请执行「钉住」动作或手动执行:${command(item.name, item.installed)}`)
|
|
498
|
+
suggestedActions.push(suggestedPinAction({ packageName: item.name, version: item.installed, profileDir, reason: why }))
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
if (item.channel === 'importer-skipped') {
|
|
502
|
+
const why = `${item.name}:来源是 ${item.spec}(非 registry)—— lock 里的记录不止 importer 一行(pnpm 还要写 packages/snapshots 的解析记录),本地无法确定 ⇒ **未写入 lock**,也没用 pnpm add 顶替`
|
|
503
|
+
depNotes.push(`${why}。要钉住请执行「钉住」动作或手动执行:${command(item.name, item.installed)}`)
|
|
504
|
+
suggestedActions.push(suggestedPinAction({ packageName: item.name, version: item.installed, profileDir, reason: why }))
|
|
505
|
+
continue
|
|
506
|
+
}
|
|
351
507
|
if (actual === item.spec) {
|
|
352
508
|
if (item.spec.startsWith('link:') && item.reason !== null) {
|
|
353
509
|
depNotes.push(`${item.name}:${item.reason},已按 link: 形式记录依赖(${actual})—— 不经 npm registry 解析、不经 tarball 完整性校验,pnpm 重建 lock 也能装上`)
|
|
@@ -358,7 +514,39 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
358
514
|
depNotes.push(`${why}。请执行「钉住」动作或手动执行:${command(item.name, item.installed)}`)
|
|
359
515
|
suggestedActions.push(suggestedPinAction({ packageName: item.name, version: item.installed, profileDir, reason: why }))
|
|
360
516
|
}
|
|
361
|
-
|
|
517
|
+
// 只读校验没通过/无法确认 ⇒ **不算已持久化**(修 1 的硬要求:不许报成功),把缺口与出路如实写进 lockNote
|
|
518
|
+
const lockVerified = importerSettled === null ? undefined : importerSettled.verified
|
|
519
|
+
const verifyBlocked = lockVerified === false || lockVerified === null
|
|
520
|
+
const verifyNames = (importerSettled?.writes ?? []).filter((w) => w.changed === true).map((w) => w.name)
|
|
521
|
+
const verifyNote = verifyBlocked
|
|
522
|
+
? `${verifyNames.join('、')} 的 lock importer 条目已按来源写入,但 pnpm 的**只读**校验${lockVerified === null ? '无法确认' : '未通过'}(${importerSettled?.verifyNote ?? '原因未知'})—— 按**未持久化**处理,绝不报成功`
|
|
523
|
+
: null
|
|
524
|
+
if (verifyBlocked && verifyNames.length > 0 && !suggestedActions.some((a) => a?.payload?.packageName === verifyNames[0])) {
|
|
525
|
+
const target = planned.find((p) => p.name === verifyNames[0]) ?? { name: verifyNames[0], installed: readInstalledVersion(profileDir, verifyNames[0]) }
|
|
526
|
+
suggestedActions.push(suggestedPinAction({ packageName: target.name, version: target.installed ?? null, profileDir, reason: verifyNote }))
|
|
527
|
+
}
|
|
528
|
+
// 只写 lock 成功、但 node_modules 里的链接还没就位(release/curl/URL 通道「先删再铺」把链接换成了真实目录):
|
|
529
|
+
// 这条**不是**"没写进 lock",文案必须分开说;同时要给「缺口明细 + 一键钉住」。
|
|
530
|
+
const writtenNames = new Set((importerSettled?.writes ?? []).filter((w) => w.changed === true || w.unchanged === true).map((w) => w.name))
|
|
531
|
+
const linkPending = drifted.filter((r) => writtenNames.has(r.name) && typeof r.lock === 'string' && r.lock.trim().startsWith('link:'))
|
|
532
|
+
for (const row of linkPending) {
|
|
533
|
+
if (suggestedActions.some((a) => a?.payload?.packageName === row.name)) continue
|
|
534
|
+
const reason = `${row.name}:lock 条目已按来源写入(${row.lock}),但 node_modules 里的链接尚未就位 —— 点「钉住」可立即用 pnpm 把链接建起来(否则要等下一次 pnpm 操作)`
|
|
535
|
+
depNotes.push(reason)
|
|
536
|
+
suggestedActions.push(suggestedPinAction({ packageName: row.name, version: row.installed, profileDir, reason }))
|
|
537
|
+
}
|
|
538
|
+
const lockUpdated = drifted.length === 0 && !verifyBlocked
|
|
539
|
+
const notWritten = drifted.filter((r) => !linkPending.includes(r))
|
|
540
|
+
const driftedText = drifted.length === 0
|
|
541
|
+
? null
|
|
542
|
+
: [
|
|
543
|
+
linkPending.length > 0
|
|
544
|
+
? `${linkPending.map((r) => `${r.name}:lock 条目已按来源写入(${r.lock}),但 node_modules 里的链接尚未就位 —— 下一次 pnpm 操作会按这条 lock 重建链接`).join(';')}`
|
|
545
|
+
: null,
|
|
546
|
+
notWritten.length > 0
|
|
547
|
+
? `${notWritten.length} 个包没写进 pnpm-lock.yaml(${notWritten.map((r) => `${r.name}:装了 ${r.installed ?? '未知'}/lock 里是 ${r.lock ?? '未记录'}`).join(';')}):之后任何 pnpm 操作(开关插件、dsh plugin add/remove)都可能把它们还原。要钉住请执行:${command(notWritten[0].name, notWritten[0].installed)}`
|
|
548
|
+
: null,
|
|
549
|
+
].filter((x) => x !== null).join(';')
|
|
362
550
|
return {
|
|
363
551
|
method,
|
|
364
552
|
packages: rows.map((r) => ({ name: r.name, installedVersion: r.installed, lockVersion: r.lock, spec: r.spec, aligned: r.installed !== null && (r.installed === r.lock || sourcePinned(r.lock)) })),
|
|
@@ -366,9 +554,19 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
|
|
|
366
554
|
installedVersion: rows.length === 1 ? rows[0].installed : null,
|
|
367
555
|
lockVersion: rows.length === 1 ? rows[0].lock : null,
|
|
368
556
|
lockUpdated,
|
|
557
|
+
// 只读校验的结论如实下发(true=确认可解析 / false=未通过 / null=无法确认;没写过条目时是 undefined)
|
|
558
|
+
lockVerified,
|
|
559
|
+
lockVerifyNote: importerSettled?.verifyNote ?? null,
|
|
560
|
+
lockImporterWrites: (importerSettled?.writes ?? []).map((w) => ({ name: w.name, changed: w.changed === true, version: w.version ?? null, reason: w.reason ?? null })),
|
|
369
561
|
lockNote: lockUpdated
|
|
370
562
|
? null
|
|
371
|
-
:
|
|
563
|
+
: [
|
|
564
|
+
verifyNote,
|
|
565
|
+
// 定点写入失败的逐条原因也要进 lockNote(缺口明细:面板只看 lockNote 时也说得清)
|
|
566
|
+
(importerSettled?.writes ?? []).filter((w) => w.changed !== true && w.unchanged !== true && typeof w.reason === 'string' && w.reason !== '')
|
|
567
|
+
.map((w) => `${w.name}:${w.reason}`).join(';') || null,
|
|
568
|
+
drifted.length === 0 ? null : `${driftedText}`,
|
|
569
|
+
].filter((x) => x !== null).join(';'),
|
|
372
570
|
// 非常规来源(link:)写回时的**用户可见**说明:绝不静默(缺陷②的隐蔽性正是"装完看不出问题")。
|
|
373
571
|
// 文案由上面的"读回真实值"步骤产出,因此永远不会出现"面板说已钉住、磁盘上仍是版本号"。
|
|
374
572
|
depNote: depNotes.length > 0 ? depNotes.join(';') : null,
|
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.39",
|
|
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",
|