@hyzyn/dsh-safe 0.11.0 → 0.12.0
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/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/failures.js +12 -0
- package/lib/i18n.js +2 -0
- package/lib/repair.js +10 -3
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -90,7 +90,7 @@ How upgrading works: `dsh-safe update` auto-detects the dsh package name and ins
|
|
|
90
90
|
|
|
91
91
|
## How It Works
|
|
92
92
|
|
|
93
|
-
1. **Failure identification**: when dsh fails to start, stderr carries
|
|
93
|
+
1. **Failure identification**: when dsh fails to start, stderr carries five kinds of signatures (`plugin(s) failed to load: …`, `N entries did not activate` with per-row failures, `failed to apply/import loader entry <id> (<name>)`, outer stack frames `…#<entryId>`, and `duplicate loader entry id: <id>` duplicates). dsh-safe extracts the broken plugin's package name and row id from them.
|
|
94
94
|
2. **Match against real rows**: it scans the profile patch, `$DSH_HOME/cordis.patch.yml` (home layer) and each bundle's patch to build a "row id ↔ plugin package" mapping; only rows that actually exist are disabled, avoiding collateral damage.
|
|
95
95
|
3. **Managed block writing**: it appends a marker-commented managed block at the end of the matching patch file (same convention as `dsh-mcp-config managed`), setting matched rows to `disabled: true`. Existing user content and comments are preserved; a fresh profile's `[]` template is correctly replaced with a block sequence.
|
|
96
96
|
4. **Ledger & restore**: quarantine records live in `$DSH_HOME/dsh-safe/quarantine.json`. Once a plugin upgrade fixes the issue, `dsh-safe restore --profile web --all` removes the managed block and re-mounts the plugin (hot-applied for profiles with `patchReload: live`).
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
90
90
|
|
|
91
91
|
## 工作原理
|
|
92
92
|
|
|
93
|
-
1. **识别失败**:dsh 启动失败时,stderr
|
|
93
|
+
1. **识别失败**:dsh 启动失败时,stderr 里有五类特征(`plugin(s) failed to load: …`、`N entries did not activate` 逐行失败、`failed to apply/import loader entry <id> (<name>)`、外层栈 `…#<entryId>`、`duplicate loader entry id: <id>` 重复挂载)。dsh-safe 从中提取坏插件的包名与行 id。
|
|
94
94
|
2. **对照真实行**:扫描 profile patch、`$DSH_HOME/cordis.patch.yml`(home 层)与各 bundle 的 patch,得到「行 id ↔ 插件包名」对照表;只禁用真实存在的行,避免误伤。
|
|
95
95
|
3. **写入托管区块**:在对应 patch 文件末尾追加带标记注释的区块(与 `dsh-mcp-config managed` 同款约定),把命中的行置为 `disabled: true`。用户已有内容与注释原样保留;全新 profile 的 `[]` 模板会被正确替换成块序列。
|
|
96
96
|
4. **台账与恢复**:隔离记录存 `$DSH_HOME/dsh-safe/quarantine.json`。插件升级修复后用 `dsh-safe restore --profile web --all` 摘除区块恢复挂载(`patchReload: live` 的 profile 热生效)。
|
package/lib/failures.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* `failed to (apply|import|dispose|rollback) loader entry <id> (<name>): <原因>`
|
|
13
13
|
* 4. 外层栈(getOuterStack):
|
|
14
14
|
* ` at file:///…/profiles/web/#<entryId>`
|
|
15
|
+
* 5. 重复挂载:
|
|
16
|
+
* `duplicate loader entry id: <id>`(同一 id 被多个 bundle/行挂载)。
|
|
17
|
+
* 该行同时会被特征 3 命中包装层(如 include 入口),但 include 是机制层
|
|
18
|
+
* 而非元凶——命中本特征时抑制同线的特征 3 记录,避免误隔离机制行。
|
|
15
19
|
*
|
|
16
20
|
* 1–3 给出包名(entry.options.name),4 给出行 id;两者都要在调用方与
|
|
17
21
|
* patch 行对照后才会生效,所以这里允许宽收集。
|
|
@@ -35,8 +39,16 @@ export function parseFailureReport(text) {
|
|
|
35
39
|
const reEntry = new RegExp(`failed to (?:apply|import|dispose|rollback) loader entry (${ID_CLASS}) \\(([^)]+)\\)`)
|
|
36
40
|
const reStackId = new RegExp(`^\\s*at \\S+#(${ID_CLASS})`)
|
|
37
41
|
const reLoadList = /plugin\(s\) failed to load:\s*([^;\n]+);/
|
|
42
|
+
const reDupId = new RegExp(`duplicate loader entry id: (${ID_CLASS})`)
|
|
38
43
|
|
|
39
44
|
for (const line of lines) {
|
|
45
|
+
const dup = reDupId.exec(line)
|
|
46
|
+
if (dup) {
|
|
47
|
+
// 重复挂载:真正元凶是被复制的 id;包装行(apply include 等)是机制层,
|
|
48
|
+
// 若一并记录可能误禁加载机制,故本行只记录 duplicate 的 id。
|
|
49
|
+
entryIds.set(dup[1], line)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
40
52
|
const entry = reEntry.exec(line)
|
|
41
53
|
if (entry) {
|
|
42
54
|
entryIds.set(entry[1], line)
|
package/lib/i18n.js
CHANGED
|
@@ -135,6 +135,7 @@ const ZH = {
|
|
|
135
135
|
repairAmbiguous: '[dsh-safe] {id} 在多个 profile 中都有隔离记录({profiles}),请用 --profile <名> 指定。',
|
|
136
136
|
repairUnsupported: '[dsh-safe] 该失败类型不支持自动修复(可修复范围:包解析失败、导出版本不匹配等重装/升级可能修复的问题)。原因: {reason}\n 可用 dsh-safe explain 查看解读。',
|
|
137
137
|
repairPlan: '[dsh-safe] 将在 profile {profile} 中修复 {name}:执行 {command},成功后自动摘除隔离行',
|
|
138
|
+
repairDuplicate: '[dsh-safe] 这是重复挂载问题(同一 id 被多个 bundle/行挂载),重装包无法修复。请从 profile 的 bundles 清单或 patch 中移除多余的挂载来源,然后 dsh-safe restore 恢复。',
|
|
138
139
|
repairInstallFailed: '[dsh-safe] 插件安装失败(退出码 {code}),隔离状态保持不变。',
|
|
139
140
|
repairDone: '[dsh-safe] 修复完成:跑 dsh-safe <启动命令> 验证;若仍失败会被自动重新隔离。',
|
|
140
141
|
}
|
|
@@ -272,6 +273,7 @@ Notes:
|
|
|
272
273
|
repairAmbiguous: '[dsh-safe] {id} is quarantined in multiple profiles ({profiles}); specify one with --profile <name>.',
|
|
273
274
|
repairUnsupported: '[dsh-safe] this failure type cannot be auto-repaired (repairable: package-resolution failures, export mismatches — problems a reinstall/upgrade may fix). Reason: {reason}\n try dsh-safe explain for an interpretation.',
|
|
274
275
|
repairPlan: '[dsh-safe] repairing {name} in profile {profile}: running {command}, then the quarantine row is removed automatically',
|
|
276
|
+
repairDuplicate: '[dsh-safe] this is a duplicate-mount issue (the same id is mounted by multiple bundles/rows); reinstalling cannot fix it. Remove the redundant mount source from the profile bundles/patch, then run dsh-safe restore.',
|
|
275
277
|
repairInstallFailed: '[dsh-safe] plugin install failed (exit code {code}); quarantine state left unchanged.',
|
|
276
278
|
repairDone: '[dsh-safe] repair finished: run dsh-safe <boot command> to verify; a still-broken plugin will be auto-quarantined again.',
|
|
277
279
|
}
|
package/lib/repair.js
CHANGED
|
@@ -24,12 +24,14 @@ const out = (line) => process.stdout.write(`${line}\n`)
|
|
|
24
24
|
* repair 可修复的失败特征:问题出在"包的安装状态或版本适配"上,重装/升级
|
|
25
25
|
* 插件包可能修复——
|
|
26
26
|
* - 模块解析失败:包缺失/损坏/未安装 → 重装即愈;
|
|
27
|
-
* - ESM 导出不匹配(does not provide an export
|
|
27
|
+
* - ESM 导出不匹配(does not provide an [export|import binding]):插件与
|
|
28
28
|
* 当前 dsh API 版本不兼容,升级插件到适配版本可能修复(真实案例:
|
|
29
29
|
* describe-image 隔离于 dsh-settings 导出变更)。修复失败不恢复、无损。
|
|
30
|
+
* 注意:台账 reason 经 summarizeLine 截断过(160 字符),关键短语可能被
|
|
31
|
+
* "…" 切断,因此导出不匹配的特征只匹配到 "does not provide an" 为止。
|
|
30
32
|
*/
|
|
31
33
|
const REPAIRABLE_RE =
|
|
32
|
-
/Cannot find package|could not be resolved|ERR_MODULE_NOT_FOUND|does not provide an (?:export|import binding)
|
|
34
|
+
/Cannot find package|could not be resolved|ERR_MODULE_NOT_FOUND|does not provide an( (?:export|import binding))?/i
|
|
33
35
|
|
|
34
36
|
function parseRepairArgs(args) {
|
|
35
37
|
const opts = { id: undefined, profile: undefined, to: undefined, yes: false, dryRun: false }
|
|
@@ -111,7 +113,12 @@ export async function cmdRepair(args, { spawn = spawnSync, log = err, write = ou
|
|
|
111
113
|
const { profile, entry } = matches[0]
|
|
112
114
|
const name = entry.name ?? entry.id
|
|
113
115
|
|
|
114
|
-
//
|
|
116
|
+
// 类别门禁:重复挂载是配置层问题(同一 id 多处挂载),重装无意义,给针对性指引
|
|
117
|
+
if (/duplicate loader entry id/i.test(entry.reason ?? '')) {
|
|
118
|
+
log(t('repairDuplicate'))
|
|
119
|
+
return 1
|
|
120
|
+
}
|
|
121
|
+
// 只修"包的安装状态/版本适配"类
|
|
115
122
|
if (!REPAIRABLE_RE.test(entry.reason ?? '')) {
|
|
116
123
|
log(t('repairUnsupported', { reason: summarizeLine(entry.reason) }))
|
|
117
124
|
return 1
|