@noob-stupid/dsh-plugin-console 0.5.99 → 0.5.100
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/server/domain/patch.js +76 -7
- package/lib/server/infra/fw-root-detect.js +129 -11
- package/package.json +1 -1
|
@@ -1,20 +1,70 @@
|
|
|
1
1
|
// L1 · domain —— patch.js(由 Step 2 从 lib/index.js 原样切出,只搬移未改逻辑)
|
|
2
2
|
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
5
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'
|
|
9
9
|
import { makePackageResolver } from '../infra/package-resolve.js'
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 写补丁前先建父目录(issue #27 的**派生缺口**,0.5.99 加法)。
|
|
13
|
+
*
|
|
14
|
+
* 为什么必须补:补丁路径是从 loader 的 `cordis:include` 条目推导出来的(见 infra/paths.js),
|
|
15
|
+
* 推导一旦指向一个**还不存在**的 profile 目录,本文件里原先 7 处 `writeFile` 全是**裸写** ——
|
|
16
|
+
* 父目录不在就 ENOENT。表现分两种,都很难查:
|
|
17
|
+
* ① 调用方 catch 了异常 ⇒ 面板显示"已启用",文件里却一个字都没有(假成功);
|
|
18
|
+
* ② 异常冒到接口 ⇒ 500,但真正的原因(profile 目录不存在)没人说出来。
|
|
19
|
+
* ⇒ 现在写前 `mkdir(dirname(p), { recursive: true })`:只把"**本来该成功却失败**"的那条路补上;
|
|
20
|
+
* 原本就写得成功的路径一个字节都不变(目录已存在时 recursive mkdir 是幂等 no-op)。
|
|
21
|
+
*
|
|
22
|
+
* mkdir 自己失败时**原样抛出**(绝不吞成"写成功"):要么调用方 catch,要么接口如实报错 ——
|
|
23
|
+
* 但绝不能出现"目录没建出来、函数却正常返回"。
|
|
24
|
+
* `deps.mkdir` 只是给测试用的注入缝(模拟 EPERM),生产默认就是 node:fs/promises#mkdir。
|
|
25
|
+
*/
|
|
26
|
+
async function ensurePatchParentDir(patchPath, deps = {}) {
|
|
27
|
+
const make = typeof deps.mkdir === 'function' ? deps.mkdir : mkdir
|
|
28
|
+
await make(dirname(patchPath), { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 父目录是不是**真的不存在**(`readPatchState` 区分"两种缺失"的判据)。
|
|
33
|
+
*
|
|
34
|
+
* 只在 readFile 已经 ENOENT 之后才被调用 —— 文件正常读取时一次 syscall 都不多。
|
|
35
|
+
* 判据**保守**:只有确证(ENOENT / ENOTDIR)才说"父目录不存在";
|
|
36
|
+
* EPERM/EACCES 这类"读不到但不一定没有"的情况一律返回 false(宁可不说,不谎报路径错)。
|
|
37
|
+
*/
|
|
38
|
+
async function parentDirMissing(patchPath, statFn) {
|
|
39
|
+
try {
|
|
40
|
+
await statFn(dirname(patchPath))
|
|
41
|
+
return false
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 读取补丁文件并扫描:停用块与 insert 行的 id。
|
|
48
|
+
*
|
|
49
|
+
* ★ 0.5.99 加法:「文件不存在」过去被 ENOENT 一起吞成"空补丁",于是**路径推导错了**与
|
|
50
|
+
* **文件真的是空的**在返回值上完全一样 —— issue #27 那个 bug(补丁全写到废弃的 web profile、
|
|
51
|
+
* 在用的 desktop 静默收不到)正是靠这条配合才藏了三周。
|
|
52
|
+
* 现在区分两种缺失,但**一个字都不改既有语义**:
|
|
53
|
+
* ① 父目录在、文件还没建(新 profile 第一次写补丁,**正常**)⇒ 空补丁,`parentMissing: false`;
|
|
54
|
+
* ② 父目录也不在(**路径推导错了**)⇒ 仍然返回空补丁(不抛错、既有调用方行为不变),
|
|
55
|
+
* 只多带 `parentMissing: true` + `missingReason: 'parent-missing'`,让上层看得见。
|
|
56
|
+
* 健康环境下的返回值只比过去多**一个**字段(`parentMissing`)——`missingReason` 仅在缺失时出现。
|
|
57
|
+
* `deps.stat` 是给测试用的注入缝;生产默认 node:fs/promises#stat。公开调用面仍是 `readPatchState(path)`。
|
|
58
|
+
*/
|
|
59
|
+
async function readPatchState(patchPath, deps = {}) {
|
|
60
|
+
const statFn = typeof deps.stat === 'function' ? deps.stat : stat
|
|
13
61
|
let text = ''
|
|
62
|
+
let parentMissing = false
|
|
14
63
|
try {
|
|
15
64
|
text = await readFile(patchPath, 'utf8')
|
|
16
65
|
} catch (error) {
|
|
17
66
|
if (error.code !== 'ENOENT') throw error
|
|
67
|
+
parentMissing = await parentDirMissing(patchPath, statFn)
|
|
18
68
|
}
|
|
19
69
|
const disables = []
|
|
20
70
|
const forced = []
|
|
@@ -39,7 +89,11 @@ async function readPatchState(patchPath) {
|
|
|
39
89
|
if (/^ {2}disabled: true\s*$/u.test(next)) disables.push(disableRow[1])
|
|
40
90
|
else if (/^ {2}disabled: false\s*$/u.test(next)) forced.push(disableRow[1])
|
|
41
91
|
}
|
|
42
|
-
|
|
92
|
+
// ★ 只增字段:健康环境下 = 既有 4 个字段 + `parentMissing: false`;
|
|
93
|
+
// 父目录缺失时才多一个 `missingReason`(见上面的注释)。
|
|
94
|
+
const state = { disables, forced, inserts, text, parentMissing }
|
|
95
|
+
if (parentMissing) state.missingReason = 'parent-missing'
|
|
96
|
+
return state
|
|
43
97
|
}
|
|
44
98
|
|
|
45
99
|
/** 停用:追加 disabled:true 块(已存在则不动)。 */
|
|
@@ -49,6 +103,7 @@ async function disableEntry(patchPath, id) {
|
|
|
49
103
|
if (disables.includes(id)) return { changed: false }
|
|
50
104
|
const clean = sanitizePatchText(text)
|
|
51
105
|
const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
|
|
106
|
+
await ensurePatchParentDir(patchPath)
|
|
52
107
|
await writeFile(patchPath, `${next}${disableBlock(id)}`, 'utf8')
|
|
53
108
|
return { changed: true }
|
|
54
109
|
})
|
|
@@ -60,12 +115,14 @@ async function enableEntry(patchPath, id) {
|
|
|
60
115
|
const { disables, forced, text } = await readPatchState(patchPath)
|
|
61
116
|
const blockRe = new RegExp(`^- id: ${escapeRegExp(id)}\\r?\\n disabled: true\\r?\\n`, 'mu')
|
|
62
117
|
if (blockRe.test(text)) {
|
|
118
|
+
await ensurePatchParentDir(patchPath)
|
|
63
119
|
await writeFile(patchPath, sanitizePatchText(text.replace(blockRe, '')), 'utf8')
|
|
64
120
|
return { changed: true }
|
|
65
121
|
}
|
|
66
122
|
if (forced.includes(id)) return { changed: false }
|
|
67
123
|
const clean = sanitizePatchText(text)
|
|
68
124
|
const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
|
|
125
|
+
await ensurePatchParentDir(patchPath)
|
|
69
126
|
await writeFile(patchPath, `${next}- id: ${id}\n disabled: false\n`, 'utf8')
|
|
70
127
|
return { changed: true }
|
|
71
128
|
})
|
|
@@ -110,6 +167,7 @@ async function appendInsert(patchPath, entryId, packageName) {
|
|
|
110
167
|
const clean = sanitizePatchText(text)
|
|
111
168
|
const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
|
|
112
169
|
const block = `- insert:\n - id: ${entryId}\n name: '${packageName}'\n`
|
|
170
|
+
await ensurePatchParentDir(patchPath)
|
|
113
171
|
await writeFile(patchPath, `${next}${block}`, 'utf8')
|
|
114
172
|
return { changed: true }
|
|
115
173
|
})
|
|
@@ -123,7 +181,10 @@ async function removeInsertRow(patchPath, rowId) {
|
|
|
123
181
|
let next = text.replace(blockRe, '')
|
|
124
182
|
const overrideRe = new RegExp(`^- id: ${escapeRegExp(rowId)}\\s*\\r?\\n {2}disabled: (true|false)\\s*\\r?\\n`, 'mu')
|
|
125
183
|
next = next.replace(overrideRe, '')
|
|
126
|
-
if (next !== text)
|
|
184
|
+
if (next !== text) {
|
|
185
|
+
await ensurePatchParentDir(patchPath)
|
|
186
|
+
await writeFile(patchPath, sanitizePatchText(next), 'utf8')
|
|
187
|
+
}
|
|
127
188
|
})
|
|
128
189
|
}
|
|
129
190
|
|
|
@@ -133,7 +194,10 @@ function removeDisableBlock(patchPath, rowId) {
|
|
|
133
194
|
const { text } = await readPatchState(patchPath)
|
|
134
195
|
const overrideRe = new RegExp(`^- id: ${escapeRegExp(rowId)}\\s*\\r?\\n {2}disabled: (true|false)\\s*\\r?\\n`, 'mu')
|
|
135
196
|
const next = text.replace(overrideRe, '')
|
|
136
|
-
if (next !== text)
|
|
197
|
+
if (next !== text) {
|
|
198
|
+
await ensurePatchParentDir(patchPath)
|
|
199
|
+
await writeFile(patchPath, sanitizePatchText(next), 'utf8')
|
|
200
|
+
}
|
|
137
201
|
})
|
|
138
202
|
}
|
|
139
203
|
|
|
@@ -212,7 +276,12 @@ function parseInsertNames(text) {
|
|
|
212
276
|
async function healPatchSafety(patchPath, deps = {}) {
|
|
213
277
|
const profileDir = dirname(patchPath)
|
|
214
278
|
const readText = deps.readText ?? ((p) => readFile(p, 'utf8'))
|
|
215
|
-
|
|
279
|
+
// 默认写盘实现也走「先建父目录」:注入缝只是把 writeFile 换成别的写盘手段,
|
|
280
|
+
// 不该因此丢掉父目录保证(过去注入 readText/writeText 的调用方拿到的行为不变)。
|
|
281
|
+
const writeText = deps.writeText ?? (async (p, data) => {
|
|
282
|
+
await ensurePatchParentDir(p, deps)
|
|
283
|
+
await writeFile(p, data, 'utf8')
|
|
284
|
+
})
|
|
216
285
|
const { text } = await readPatchState(patchPath)
|
|
217
286
|
const healed = []
|
|
218
287
|
const autoDisabled = []
|
|
@@ -103,28 +103,146 @@ function fwRootFromNpxCache({ env = process.env, home = homedir(), deps = {} } =
|
|
|
103
103
|
return hits[0]
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
106
|
+
/** 路径比较用的归一键(只用于"是不是同一个目录"的去重/比对,不用于任何业务判据)。 */
|
|
107
|
+
function dirKey(dir) {
|
|
108
|
+
if (typeof dir !== 'string' || dir === '') return ''
|
|
109
|
+
let out = dir
|
|
110
|
+
try { out = resolve(dir) } catch {}
|
|
111
|
+
out = out.replace(/[\\/]+$/u, '')
|
|
112
|
+
// Windows 路径大小写不敏感(`C:\Users` 与 `c:\users` 是同一个目录)—— 其他平台保持原样
|
|
113
|
+
return process.platform === 'win32' ? out.toLowerCase() : out
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 两个路径是不是同一个目录(同上:仅用于比对)。 */
|
|
117
|
+
function sameDir(a, b) {
|
|
118
|
+
const ka = dirKey(a)
|
|
119
|
+
return ka !== '' && ka === dirKey(b)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 当前生效的补丁落在哪个 profile 目录?推导不出来返回 `null`(**绝不猜、也绝不报错**)。
|
|
124
|
+
*
|
|
125
|
+
* 为什么要它(0.5.99 加法,issue #27 的同一批现场):`<dshHome>/profiles/` 下**每个**子目录都会
|
|
126
|
+
* 变成框架树候选(包括早已废弃的那个 profile —— 真机上 desktop 在用、web 是 issue #27 的受害者)。
|
|
127
|
+
* 废弃 profile 里往往还留着旧框架的残影,`looksLikeFwRoot()` **分不出死活** ⇒ 谁先试谁被选中。
|
|
128
|
+
* ⇒ 用"当前补丁所在的 profile"给候选排个序:同一 profile 的先试(剩下的照旧)。
|
|
129
|
+
*
|
|
130
|
+
* 取值顺序(都拿不到就返回 null,排序优化自动跳过):
|
|
131
|
+
* ① `deps.derivePatchPath()`(调用方注入,一般就是 infra/paths.js#findPatchPath 的包装)
|
|
132
|
+
* ② `deps.patchPath`(补丁文件全路径)/ `deps.profileDir`(profile 目录)
|
|
133
|
+
* ③ `DSH_PROFILE_DIR`(桌面端/CLI 显式设置,最可信 —— 与 paths.js#defaultPatchPath 同源)
|
|
134
|
+
* ④ `DSH_PROFILE`(只给名字时拼 `profiles/<name>`)
|
|
135
|
+
* ★ 这里刻意**不**去调 `defaultPatchPath()`:它读的是进程级 `process.env` 与真实 home,
|
|
136
|
+
* 在注入了 `env`/`home` 的调用(测试、预检脚本)里会串味成"另一台机器的 profile"。
|
|
137
|
+
*/
|
|
138
|
+
function activeProfileDirOf({ env = process.env, dshHome, deps = {} } = {}) {
|
|
139
|
+
const nonEmpty = (v) => typeof v === 'string' && v.trim() !== '' ? v.trim() : null
|
|
140
|
+
if (typeof deps.derivePatchPath === 'function') {
|
|
141
|
+
try {
|
|
142
|
+
const p = nonEmpty(deps.derivePatchPath())
|
|
143
|
+
if (p !== null) return dirname(resolve(p))
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
const fromPatchPath = nonEmpty(deps.patchPath)
|
|
147
|
+
if (fromPatchPath !== null) return dirname(resolve(fromPatchPath))
|
|
148
|
+
const fromProfileDir = nonEmpty(deps.profileDir)
|
|
149
|
+
if (fromProfileDir !== null) return resolve(fromProfileDir)
|
|
150
|
+
const fromEnvDir = nonEmpty(env.DSH_PROFILE_DIR)
|
|
151
|
+
if (fromEnvDir !== null) return resolve(fromEnvDir)
|
|
152
|
+
const fromEnvName = nonEmpty(env.DSH_PROFILE)
|
|
153
|
+
if (fromEnvName !== null) return join(dshHome, 'profiles', fromEnvName)
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 给 `profiles/<name>/node_modules` 这批候选**排序**(只排,不删 —— 判据仍是既有的 looksLikeFwRoot)。
|
|
159
|
+
*
|
|
160
|
+
* 优先级:
|
|
161
|
+
* 0 与当前生效补丁同 profile("活着的那棵树")→ 先试;
|
|
162
|
+
* 1 像框架树(既有判据 `looksLikeFwRoot`)→ 次之;
|
|
163
|
+
* 2 其余 → 最后。
|
|
164
|
+
* 同档内保持**传入顺序**(= readdir 原顺序)⇒ 结果稳定,不随目录名排序漂移。
|
|
165
|
+
*
|
|
166
|
+
* ★ 排序器只影响**先试谁**,绝不决定"能不能选到":`fwRootFromDshHome()` 末尾仍会按**旧顺序**
|
|
167
|
+
* 把每个候选再兜一遍(见那里的 `legacy` 循环),所以任何原本能选到的树都还能选到。
|
|
168
|
+
*/
|
|
169
|
+
function rankProfileCandidates(candidates, { activeProfileDir = null, looksLike = null } = {}) {
|
|
170
|
+
const list = Array.isArray(candidates) ? candidates.filter((d) => typeof d === 'string' && d !== '') : []
|
|
171
|
+
const isTree = typeof looksLike === 'function' ? looksLike : (dir) => looksLikeFwRoot(dir)
|
|
172
|
+
const tierOf = (dir) => {
|
|
173
|
+
if (activeProfileDir !== null && sameDir(dirname(dir), activeProfileDir)) return 0
|
|
174
|
+
try { if (isTree(dir)) return 1 } catch {}
|
|
175
|
+
return 2
|
|
176
|
+
}
|
|
177
|
+
return list
|
|
178
|
+
.map((dir, index) => ({ dir, index, tier: tierOf(dir) }))
|
|
179
|
+
.sort((a, b) => (a.tier - b.tier) || (a.index - b.index))
|
|
180
|
+
.map((x) => x.dir)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 扫 `<dshHome>/profiles/**` 与各类 node_modules,找一棵像框架树的。
|
|
184
|
+
*
|
|
185
|
+
* ★ 0.5.99 加法(issue #27 的同一批现场):`profiles/<name>/node_modules` 这批候选**不再按
|
|
186
|
+
* readdir 顺序盲试**,而是先试"当前生效补丁同 profile"的那棵、再试像框架树的,其余垫底
|
|
187
|
+
* (见 `rankProfileCandidates`)。前三项(DSH_PROFILE_DIR / DSH_PROFILE / profiles/node_modules)
|
|
188
|
+
* 的顺序与判据**一个字都没动**。
|
|
189
|
+
* ⚠ 兜底:排序之后仍按**旧顺序**把每个候选再排进队列尾部(同一个循环里,已试过的自动跳过),
|
|
190
|
+
* 所以"原本能选到的那棵树"绝不会因为新排序而选不到 —— 这是本改动最重要的不变量。
|
|
191
|
+
*/
|
|
107
192
|
function fwRootFromDshHome({ env = process.env, home = homedir(), deps = {} } = {}) {
|
|
108
193
|
const exists = deps.exists ?? existsSync
|
|
109
194
|
const readdir = deps.readdir ?? ((p) => readdirSync(p))
|
|
110
195
|
const dshHome = (typeof env.DSH_HOME === 'string' && env.DSH_HOME.trim() !== '') ? env.DSH_HOME.trim() : join(home, '.dsh')
|
|
111
196
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
197
|
+
// 同一次调用里每个候选只判定一次(排序用一次、挑选用一次,别重复扫盘)
|
|
198
|
+
const looksCache = new Map()
|
|
199
|
+
const looks = (dir) => {
|
|
200
|
+
if (!looksCache.has(dir)) looksCache.set(dir, looksLikeFwRoot(dir, { deps }))
|
|
201
|
+
return looksCache.get(dir)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const head = []
|
|
205
|
+
// ① 显式 profile 目录(桌面端/CLI 都会设 DSH_PROFILE_DIR)—— 既有最高优先级,位置不动
|
|
206
|
+
if (typeof env.DSH_PROFILE_DIR === 'string' && env.DSH_PROFILE_DIR.trim() !== '') head.push(env.DSH_PROFILE_DIR.trim())
|
|
115
207
|
// ② 按 profile 名
|
|
116
|
-
if (typeof env.DSH_PROFILE === 'string' && env.DSH_PROFILE.trim() !== '')
|
|
117
|
-
// ③
|
|
118
|
-
|
|
208
|
+
if (typeof env.DSH_PROFILE === 'string' && env.DSH_PROFILE.trim() !== '') head.push(join(dshHome, 'profiles', env.DSH_PROFILE.trim()))
|
|
209
|
+
// ③ 共享投影层(各 profile 共用)
|
|
210
|
+
head.push(join(dshHome, 'profiles', 'node_modules'))
|
|
211
|
+
|
|
212
|
+
// ④ 各 profile 自己的 node_modules(readdir 原顺序 = 旧顺序,兜底要用它)
|
|
213
|
+
const legacy = []
|
|
119
214
|
try {
|
|
120
215
|
for (const d of readdir(join(dshHome, 'profiles'))) {
|
|
121
216
|
if (d === 'node_modules') continue
|
|
122
|
-
|
|
217
|
+
legacy.push(join(dshHome, 'profiles', d, 'node_modules'))
|
|
123
218
|
}
|
|
124
219
|
} catch {}
|
|
125
220
|
|
|
126
|
-
|
|
127
|
-
|
|
221
|
+
// 排序(可注入;排序器抛错/返回怪东西时**原样退回旧顺序**,绝不因此少试一棵树)
|
|
222
|
+
let ranked = legacy
|
|
223
|
+
if (legacy.length > 0) {
|
|
224
|
+
const rank = typeof deps.rankCandidates === 'function' ? deps.rankCandidates : rankProfileCandidates
|
|
225
|
+
try {
|
|
226
|
+
const out = rank(legacy, { activeProfileDir: activeProfileDirOf({ env, dshHome, deps }), looksLike: looks, deps })
|
|
227
|
+
if (Array.isArray(out)) ranked = out.filter((d) => typeof d === 'string' && legacy.includes(d))
|
|
228
|
+
} catch { ranked = legacy }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 一个队列,三段顺序:既有前三项 → 排过序的 profile 候选 → **旧顺序兜底**
|
|
232
|
+
const ordered = []
|
|
233
|
+
const seen = new Set()
|
|
234
|
+
const enqueue = (dir) => {
|
|
235
|
+
const key = dirKey(dir)
|
|
236
|
+
if (key === '' || seen.has(key)) return
|
|
237
|
+
seen.add(key)
|
|
238
|
+
ordered.push(dir)
|
|
239
|
+
}
|
|
240
|
+
for (const dir of head) enqueue(dir)
|
|
241
|
+
for (const dir of ranked) enqueue(dir)
|
|
242
|
+
for (const dir of legacy) enqueue(dir)
|
|
243
|
+
|
|
244
|
+
for (const dir of ordered) {
|
|
245
|
+
if (!looks(dir)) continue
|
|
128
246
|
return { fwRoot: dir, via: 'dsh-home' }
|
|
129
247
|
}
|
|
130
248
|
return null
|
|
@@ -206,4 +324,4 @@ function detectFrameworkRoot({ pluginDir = null, entryHint = null, env = process
|
|
|
206
324
|
return { fwRoot: null, via: null, tried }
|
|
207
325
|
}
|
|
208
326
|
|
|
209
|
-
export { detectFrameworkRoot, fwRootFromBase, fwRootFromDshHome, fwRootFromNpxCache, looksLikeFwRoot, realRootOf }
|
|
327
|
+
export { detectFrameworkRoot, fwRootFromBase, fwRootFromDshHome, fwRootFromNpxCache, looksLikeFwRoot, realRootOf, rankProfileCandidates, activeProfileDirOf }
|
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.100",
|
|
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",
|