@hyzyn/dsh-safe 0.5.0 → 0.7.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 -0
- package/README.md +1 -0
- package/lib/cli.js +2 -0
- package/lib/i18n.js +20 -0
- package/lib/repair.js +167 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -48,6 +48,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
48
48
|
| `dsh-safe doctor` | environment check: versions, DSH_HOME, profiles, ledger, patch health |
|
|
49
49
|
| `dsh-safe restore [--profile <name>] (--id <id> \| --all) [--dry-run]` | re-enable auto-disabled plugins (omit `--profile` to cover every profile in the ledger) |
|
|
50
50
|
| `dsh-safe explain [--file <path>]` | interpret a failed-boot stderr with AI (read-only, needs `DSH_SAFE_AI_KEY`) |
|
|
51
|
+
| `dsh-safe repair [id] [--profile <name>] [--to <version>] [-y] [--dry-run]` | reinstall/upgrade a quarantined plugin and auto-restore it (module-resolution failures only; installs via `dsh plugin`'s pnpm channel) |
|
|
51
52
|
| `dsh-safe help` (`-h` / `--help`) | show help |
|
|
52
53
|
| `dsh-safe --version` (`-V`) | show version |
|
|
53
54
|
|
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
48
48
|
| `dsh-safe doctor` | 环境体检:版本、DSH_HOME、profiles、台账、各 patch 健康度 |
|
|
49
49
|
| `dsh-safe restore [--profile <名>] (--id <id> \| --all) [--dry-run]` | 恢复被自动禁用的插件(省略 `--profile` 时遍历台账全部 profile) |
|
|
50
50
|
| `dsh-safe explain [--file <路径>]` | 用 AI 解读一段启动失败 stderr(纯只读,需 `DSH_SAFE_AI_KEY`) |
|
|
51
|
+
| `dsh-safe repair [id] [--profile <名>] [--to <版本>] [-y] [--dry-run]` | 重装/升级被隔离的插件并自动恢复(限模块解析失败类;经 `dsh plugin` 的 pnpm 通道安装) |
|
|
51
52
|
| `dsh-safe help`(`-h` / `--help`) | 显示帮助 |
|
|
52
53
|
| `dsh-safe --version`(`-V`) | 显示版本 |
|
|
53
54
|
|
package/lib/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs'
|
|
|
12
12
|
import { loadLedger, restoreQuarantine } from './quarantine.js'
|
|
13
13
|
import { runWrapped } from './wrap.js'
|
|
14
14
|
import { cmdUpdate, cmdUpdateAndBoot, maybeNotifySelfUpdate } from './update.js'
|
|
15
|
+
import { cmdRepair } from './repair.js'
|
|
15
16
|
import { printDoctor } from './doctor.js'
|
|
16
17
|
import { aiEnabled, explainFailure } from './ai.js'
|
|
17
18
|
import { t } from './i18n.js'
|
|
@@ -181,6 +182,7 @@ export async function main(argv) {
|
|
|
181
182
|
}
|
|
182
183
|
if (cmd === 'list') return cmdList(argv.slice(1))
|
|
183
184
|
if (cmd === 'restore') return cmdRestore(argv.slice(1))
|
|
185
|
+
if (cmd === 'repair') return cmdRepair(argv.slice(1))
|
|
184
186
|
if (cmd === 'update') return cmdUpdate(argv.slice(1))
|
|
185
187
|
if (cmd === 'explain') return cmdExplain(argv.slice(1))
|
|
186
188
|
if (cmd === 'doctor') {
|
package/lib/i18n.js
CHANGED
|
@@ -25,6 +25,8 @@ const ZH = {
|
|
|
25
25
|
dsh-safe update [-y] [--to <版本>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
26
26
|
升级 dsh 与 dsh-safe 自身,并自动恢复被隔离的插件
|
|
27
27
|
dsh-safe explain [--file <路径>] 用 AI 解读一段启动失败的 stderr(需 DSH_SAFE_AI_KEY)
|
|
28
|
+
dsh-safe repair [id] [--profile <名>] [--to <版本>] [-y] [--dry-run]
|
|
29
|
+
重装/升级被隔离的插件并自动恢复(限模块解析失败类;省略 id 时只有一条记录则直接修)
|
|
28
30
|
dsh-safe help 显示本帮助
|
|
29
31
|
dsh-safe --version 显示版本
|
|
30
32
|
|
|
@@ -122,6 +124,14 @@ const ZH = {
|
|
|
122
124
|
doctorAIOff: 'AI 未启用(DSH_SAFE_AI_KEY 未设置)',
|
|
123
125
|
doctorCheckLast: '更新检查 上次 {time}',
|
|
124
126
|
doctorCheckNever: '更新检查 从未执行',
|
|
127
|
+
repairPickOne: '[dsh-safe] 共 {count} 条隔离记录,指定要修复的 id(dsh-safe repair <id>):',
|
|
128
|
+
repairNotSupportedTag: '(不支持自动修复)',
|
|
129
|
+
repairEntryMissing: '[dsh-safe] 台账里没有找到 {id} 对应的隔离记录。',
|
|
130
|
+
repairAmbiguous: '[dsh-safe] {id} 在多个 profile 中都有隔离记录({profiles}),请用 --profile <名> 指定。',
|
|
131
|
+
repairUnsupported: '[dsh-safe] 该失败类型不支持自动修复(仅限模块解析失败类)。原因: {reason}\n 可用 dsh-safe explain 查看解读。',
|
|
132
|
+
repairPlan: '[dsh-safe] 将在 profile {profile} 中修复 {name}:执行 {command},成功后自动摘除隔离行',
|
|
133
|
+
repairInstallFailed: '[dsh-safe] 插件安装失败(退出码 {code}),隔离状态保持不变。',
|
|
134
|
+
repairDone: '[dsh-safe] 修复完成:跑 dsh-safe <启动命令> 验证;若仍失败会被自动重新隔离。',
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
const EN = {
|
|
@@ -144,6 +154,8 @@ Usage:
|
|
|
144
154
|
dsh-safe update [-y] [--to <ver>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
145
155
|
upgrade dsh and dsh-safe itself, auto-restore quarantined plugins
|
|
146
156
|
dsh-safe explain [--file <path>] interpret a failed-boot stderr with AI (needs DSH_SAFE_AI_KEY)
|
|
157
|
+
dsh-safe repair [id] [--profile <name>] [--to <ver>] [-y] [--dry-run]
|
|
158
|
+
reinstall/upgrade a quarantined plugin and auto-restore it (module-resolution failures only; omit id when there is exactly one record)
|
|
147
159
|
dsh-safe help show this help
|
|
148
160
|
dsh-safe --version show version
|
|
149
161
|
|
|
@@ -244,6 +256,14 @@ Notes:
|
|
|
244
256
|
doctorAIOff: 'AI disabled (DSH_SAFE_AI_KEY not set)',
|
|
245
257
|
doctorCheckLast: 'update-check last ran at {time}',
|
|
246
258
|
doctorCheckNever: 'update-check never ran',
|
|
259
|
+
repairPickOne: '[dsh-safe] {count} quarantined record(s); specify the id to repair (dsh-safe repair <id>):',
|
|
260
|
+
repairNotSupportedTag: ' (not auto-repairable)',
|
|
261
|
+
repairEntryMissing: '[dsh-safe] no quarantine record found for {id}.',
|
|
262
|
+
repairAmbiguous: '[dsh-safe] {id} is quarantined in multiple profiles ({profiles}); specify one with --profile <name>.',
|
|
263
|
+
repairUnsupported: '[dsh-safe] this failure type cannot be auto-repaired (module-resolution failures only). Reason: {reason}\n try dsh-safe explain for an interpretation.',
|
|
264
|
+
repairPlan: '[dsh-safe] repairing {name} in profile {profile}: running {command}, then the quarantine row is removed automatically',
|
|
265
|
+
repairInstallFailed: '[dsh-safe] plugin install failed (exit code {code}); quarantine state left unchanged.',
|
|
266
|
+
repairDone: '[dsh-safe] repair finished: run dsh-safe <boot command> to verify; a still-broken plugin will be auto-quarantined again.',
|
|
247
267
|
}
|
|
248
268
|
|
|
249
269
|
const CATALOG = { zh: ZH, en: EN }
|
package/lib/repair.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-safe — `dsh-safe repair <id>`:半自动修复被隔离的插件。
|
|
3
|
+
*
|
|
4
|
+
* 只处理"模块解析失败"类(Cannot find package / could not be resolved /
|
|
5
|
+
* ERR_MODULE_NOT_FOUND)——这类问题重装/升级插件包即可修复。其它类型
|
|
6
|
+
* (apply 抛错、pending 服务等待等)属于代码或配置问题,明确拒接。
|
|
7
|
+
*
|
|
8
|
+
* 流程:台账定位条目 → 类别门禁 → 展示计划 → 确认 → 经 `dsh plugin add`
|
|
9
|
+
* (dsh 官方的 pnpm 转发通道)安装 → 自动摘除隔离行。不内置启动:装好后
|
|
10
|
+
* 由用户跑 dsh-safe 验证,再失败会自动回到隔离管线。安装失败则保持隔离
|
|
11
|
+
* 状态不变,退出码透传。
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from 'node:child_process'
|
|
14
|
+
import { createInterface } from 'node:readline/promises'
|
|
15
|
+
import { loadLedger, restoreQuarantine } from './quarantine.js'
|
|
16
|
+
import { summarizeLine } from './failures.js'
|
|
17
|
+
import { resolveDshSpawnTarget } from './dshpaths.js'
|
|
18
|
+
import { t } from './i18n.js'
|
|
19
|
+
|
|
20
|
+
const err = (line) => process.stderr.write(`${line}\n`)
|
|
21
|
+
const out = (line) => process.stdout.write(`${line}\n`)
|
|
22
|
+
|
|
23
|
+
/** 台账 reason 里判定"模块解析失败"的特征。 */
|
|
24
|
+
const RESOLVE_FAIL_RE = /Cannot find package|could not be resolved|ERR_MODULE_NOT_FOUND/i
|
|
25
|
+
|
|
26
|
+
function parseRepairArgs(args) {
|
|
27
|
+
const opts = { id: undefined, profile: undefined, to: undefined, yes: false, dryRun: false }
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const a = args[i]
|
|
30
|
+
if (a === '-y' || a === '--yes') opts.yes = true
|
|
31
|
+
else if (a === '--dry-run') opts.dryRun = true
|
|
32
|
+
else if (a === '--profile') {
|
|
33
|
+
const v = args[++i]
|
|
34
|
+
if (v === undefined) return { error: { key: 'updateUnknownFlag', params: { arg: '--profile' } } }
|
|
35
|
+
opts.profile = v
|
|
36
|
+
} else if (a.startsWith('--profile=')) opts.profile = a.slice('--profile='.length)
|
|
37
|
+
else if (a === '--to') {
|
|
38
|
+
const v = args[++i]
|
|
39
|
+
if (v === undefined) return { error: { key: 'updateUnknownFlag', params: { arg: '--to' } } }
|
|
40
|
+
opts.to = v
|
|
41
|
+
} else if (a.startsWith('--to=')) opts.to = a.slice('--to='.length)
|
|
42
|
+
else if (!a.startsWith('-') && opts.id === undefined) opts.id = a
|
|
43
|
+
else return { error: { key: 'updateUnknownFlag', params: { arg: a } } }
|
|
44
|
+
}
|
|
45
|
+
if (opts.to !== undefined && !/^[\w.+-]+$/.test(opts.to)) return { error: { key: 'updateToInvalid' } }
|
|
46
|
+
return { opts }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string[]} args
|
|
51
|
+
* @param {{
|
|
52
|
+
* spawn?: typeof spawnSync,
|
|
53
|
+
* log?: (line: string) => void,
|
|
54
|
+
* write?: (line: string) => void,
|
|
55
|
+
* }} [hooks] spawn/log/write 可注入(测试)
|
|
56
|
+
* @returns {Promise<number>} 退出码
|
|
57
|
+
*/
|
|
58
|
+
export async function cmdRepair(args, { spawn = spawnSync, log = err, write = out } = {}) {
|
|
59
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
60
|
+
out(t('helpText', {}))
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
const { opts, error } = parseRepairArgs(args)
|
|
64
|
+
if (error) {
|
|
65
|
+
log(t(error.key, error.params))
|
|
66
|
+
return 2
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 台账定位:省略 --profile 时跨 profile 搜索;省略 id 时——
|
|
70
|
+
// 恰好一条隔离记录 → 直接修它(最常见情况);
|
|
71
|
+
// 多条 → 列出清单让用户挑(标记哪些可自动修复)。
|
|
72
|
+
const ledger = loadLedger()
|
|
73
|
+
let matches = []
|
|
74
|
+
for (const [profile, entries] of Object.entries(ledger.profiles)) {
|
|
75
|
+
if (opts.profile && profile !== opts.profile) continue
|
|
76
|
+
for (const entry of entries ?? []) {
|
|
77
|
+
if (opts.id === undefined || entry.id === opts.id) matches.push({ profile, entry })
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (opts.id === undefined) {
|
|
81
|
+
if (!matches.length) {
|
|
82
|
+
log(t('noRecords'))
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
if (matches.length > 1) {
|
|
86
|
+
log(t('repairPickOne', { count: matches.length }))
|
|
87
|
+
for (const m of matches) {
|
|
88
|
+
const repairable = RESOLVE_FAIL_RE.test(m.entry.reason ?? '')
|
|
89
|
+
log(` - ${m.entry.id}(${m.entry.name ?? '-'})${repairable ? '' : t('repairNotSupportedTag')}`)
|
|
90
|
+
}
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
opts.id = matches[0].entry.id
|
|
94
|
+
}
|
|
95
|
+
if (!matches.length) {
|
|
96
|
+
log(t('repairEntryMissing', { id: opts.id }))
|
|
97
|
+
return 1
|
|
98
|
+
}
|
|
99
|
+
if (matches.length > 1) {
|
|
100
|
+
log(t('repairAmbiguous', { id: opts.id, profiles: matches.map((m) => m.profile).join(', ') }))
|
|
101
|
+
return 2
|
|
102
|
+
}
|
|
103
|
+
const { profile, entry } = matches[0]
|
|
104
|
+
const name = entry.name ?? entry.id
|
|
105
|
+
|
|
106
|
+
// 类别门禁:只修"模块解析失败"类
|
|
107
|
+
if (!RESOLVE_FAIL_RE.test(entry.reason ?? '')) {
|
|
108
|
+
log(t('repairUnsupported', { reason: summarizeLine(entry.reason) }))
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const target = opts.to ?? 'latest'
|
|
113
|
+
const spec = `${name}@${target}`
|
|
114
|
+
const spawnTarget = resolveDshSpawnTarget('dsh')
|
|
115
|
+
const commandDesc = `dsh plugin --profile ${profile} add ${spec}`
|
|
116
|
+
log(t('repairPlan', { profile, name, command: commandDesc }))
|
|
117
|
+
if (opts.dryRun) {
|
|
118
|
+
log(t('dryRunNotice'))
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
if (!opts.yes) {
|
|
122
|
+
if (!process.stdin.isTTY) {
|
|
123
|
+
log(t('updateNonInteractive'))
|
|
124
|
+
return 1
|
|
125
|
+
}
|
|
126
|
+
const ok = await askConfirm(t('updateConfirm'))
|
|
127
|
+
if (!ok) {
|
|
128
|
+
log(t('updateAborted'))
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { status } = spawn(spawnTarget.file, [...spawnTarget.prefix, 'plugin', '--profile', profile, 'add', spec], {
|
|
134
|
+
stdio: 'inherit',
|
|
135
|
+
shell: spawnTarget.shell,
|
|
136
|
+
env: process.env,
|
|
137
|
+
})
|
|
138
|
+
if (status !== 0) {
|
|
139
|
+
log(t('repairInstallFailed', { code: status ?? '?' }))
|
|
140
|
+
return status ?? 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { restored } = restoreQuarantine(profile, [opts.id], false)
|
|
144
|
+
if (!restored.length) {
|
|
145
|
+
log(t('noMatching'))
|
|
146
|
+
} else {
|
|
147
|
+
const e = restored[0]
|
|
148
|
+
write(`[dsh-safe] ${t('restored')} ${e.name ?? e.id} (id: ${e.id})`)
|
|
149
|
+
}
|
|
150
|
+
log(t('repairDone'))
|
|
151
|
+
return 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function askConfirm(question) {
|
|
155
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
156
|
+
return rl.question(question).then(
|
|
157
|
+
(answer) => {
|
|
158
|
+
rl.close()
|
|
159
|
+
const a = answer.trim().toLowerCase()
|
|
160
|
+
return a === 'y' || a === 'yes'
|
|
161
|
+
},
|
|
162
|
+
() => {
|
|
163
|
+
rl.close()
|
|
164
|
+
return false
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
}
|