@noob-stupid/dsh-plugin-console 0.5.35 → 0.5.36

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.
@@ -2,7 +2,7 @@
2
2
  // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFile, writeFile } from 'node:fs/promises'
5
- import { dirname } from 'node:path'
5
+ import { dirname, join } from 'node:path'
6
6
  import { frameworkBases, isFrameworkModuleName } from '../infra/framework-root.js'
7
7
  import { queuedWrite } from '../infra/fsx.js'
8
8
  import { escapeRegExp } from '../infra/mask.js'
@@ -71,11 +71,42 @@ async function enableEntry(patchPath, id) {
71
71
  })
72
72
  }
73
73
 
74
- /** 追加一条 insert 启用行(插件包需已安装到 profile)。 */
74
+ /** 读 profile 清单里的 `dsh.profile.bundles`(包名数组)。
75
+ * 读不到就返回空数组 —— 绝不因为读清单失败而阻断既有写入行为。 */
76
+ async function readProfileBundles(patchPath) {
77
+ try {
78
+ const manifest = JSON.parse(await readFile(join(dirname(patchPath), 'package.json'), 'utf8'))
79
+ const list = manifest?.dsh?.profile?.bundles
80
+ return Array.isArray(list) ? list.filter((x) => typeof x === 'string' && x.trim() !== '') : []
81
+ } catch {
82
+ return []
83
+ }
84
+ }
85
+
86
+ /** 追加一条 insert 启用行(插件包需已安装到 profile)。
87
+ *
88
+ * 幂等护栏(2026-10-03 用户实测事故后加):以下三种情况都**不写**,只回报 `changed:false` + `reason`:
89
+ * ① `same-id` 同 id 已经在 insert 里(原有行为);
90
+ * ② `same-package` **同一个包名**已由别的 insert 行声明 —— 再写一行 = 重复装配,
91
+ * 第二行会因重复注册而挂载失败;
92
+ * ③ `bundle` **同一个包名**已由 `dsh.profile.bundles` 声明 —— bundle 自带的
93
+ * patch 会在启动时自动装配它自己的行,我们再插一行同样是重复装配。
94
+ *
95
+ * 事故复现:用户先用**官方插件管理**启用插件(= 走 bundle 声明),再在我们面板点「启用」
96
+ * → 我们多写了一条 insert 行 → 同一插件被装配两次 → 第二行抛 `already registered`
97
+ * → 面板显示「已启用 + 挂载失败」。 */
75
98
  async function appendInsert(patchPath, entryId, packageName) {
76
99
  return queuedWrite(async () => {
77
100
  const { inserts, text } = await readPatchState(patchPath)
78
- if (inserts.includes(entryId)) return { changed: false }
101
+ if (inserts.includes(entryId)) return { changed: false, reason: 'same-id' }
102
+ const name = String(packageName ?? '').trim()
103
+ if (name !== '') {
104
+ for (const declared of parseInsertNames(text).values()) {
105
+ if (String(declared ?? '').trim() === name) return { changed: false, reason: 'same-package' }
106
+ }
107
+ const bundles = await readProfileBundles(patchPath)
108
+ if (bundles.includes(name)) return { changed: false, reason: 'bundle' }
109
+ }
79
110
  const clean = sanitizePatchText(text)
80
111
  const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
81
112
  const block = `- insert:\n - id: ${entryId}\n name: '${packageName}'\n`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
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",